486 lines
21 KiB
JavaScript
486 lines
21 KiB
JavaScript
'use strict';
|
||
|
||
// State
|
||
const state = {
|
||
connected: false,
|
||
games: [],
|
||
backups: [],
|
||
sortCol: localStorage.getItem('chimera.games.sortCol') || 'title',
|
||
sortDir: localStorage.getItem('chimera.games.sortDir') || 'asc',
|
||
filter: localStorage.getItem('chimera.games.filter') || 'all',
|
||
platformFilter: ['all', 'ps4', 'ps5', 'unknown'].includes(localStorage.getItem('chimera.games.platform'))
|
||
? localStorage.getItem('chimera.games.platform')
|
||
: 'all',
|
||
search: '',
|
||
firmwareFilter: 'all',
|
||
gameView: localStorage.getItem('chimera.games.view') || 'grid',
|
||
gameRenderLimit: 160,
|
||
currentGame: null,
|
||
};
|
||
|
||
// Shared hash summary maps — populated by loadGames() and loadBackporks().
|
||
// _gameHashSummary: game_id → summary for hash_type='game'
|
||
// _backporkHashSummary: 'game_id|firmware_label' → summary for hash_type='backpork'
|
||
const _gameHashSummary = new Map();
|
||
const _backporkHashSummary = new Map();
|
||
const _pageLoadTimestamps = new Map();
|
||
const _pageLoadModes = new Map();
|
||
const _pageLoadInFlight = new Map();
|
||
let _pageWarmTimer = null;
|
||
let _pageWarmRunning = false;
|
||
|
||
// Feature code is route-loaded. Background refreshes only revisit modules that a user
|
||
// already opened; they never download the entire application after startup.
|
||
const _PAGE_WARM_ORDER = [];
|
||
|
||
const _PAGE_STALE_MS = {
|
||
dashboard: 15_000,
|
||
transfers: 5_000,
|
||
'system-view': 20_000,
|
||
media: 30_000,
|
||
default: 45_000,
|
||
};
|
||
|
||
function _populateHashMaps(summaries) {
|
||
_gameHashSummary.clear();
|
||
_backporkHashSummary.clear();
|
||
for (const s of summaries) {
|
||
if (s.hash_type === 'backpork') {
|
||
_backporkHashSummary.set(s.game_id + '|' + (s.firmware_label || ''), s);
|
||
} else {
|
||
_gameHashSummary.set(s.game_id, s);
|
||
}
|
||
}
|
||
}
|
||
|
||
// System View state
|
||
let _svStream = null;
|
||
let _svMuted = false;
|
||
let _svAudioCtx = null; // AudioContext for low-latency capture-card audio
|
||
let _svAudioGain = null; // GainNode — controls mute without stopping the stream
|
||
let _svHotkeys = { mute: 'm', gif: 'g', video: 'v', fullscreen: 'f', popout: 'p' };
|
||
let _svRecording = false;
|
||
let _svRecFrames = []; // Uint8ClampedArray[] — one per captured frame
|
||
let _svRecInterval = null;
|
||
let _svRecTimer = null;
|
||
let _svRecStart = 0;
|
||
let _svRecCanvas = null; // lazy-created hidden 640×360 canvas
|
||
let _svRecCtx = null;
|
||
const GIF_W = 640, GIF_H = 360;
|
||
|
||
// Video recording state
|
||
let _svVidRecorder = null;
|
||
let _svVidWritable = null;
|
||
let _svVidChunks = null;
|
||
let _svVidTimer = null;
|
||
let _svVidStart = 0;
|
||
let _svVidPaused = false;
|
||
let _svVidBytes = 0;
|
||
let _svVidMimeType = '';
|
||
let _svVidExt = '';
|
||
|
||
// Helpers
|
||
const $ = id => document.getElementById(id);
|
||
const _dialogReturnFocus = new WeakMap();
|
||
|
||
function dialogFocusable(container) {
|
||
if (!container) return [];
|
||
return [...container.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')]
|
||
.filter(element => !element.hidden && element.getAttribute('aria-hidden') !== 'true' && element.getClientRects().length);
|
||
}
|
||
|
||
function openAccessibleDialog(overlay, initialFocus = null) {
|
||
if (!overlay) return;
|
||
_dialogReturnFocus.set(overlay, document.activeElement instanceof HTMLElement ? document.activeElement : null);
|
||
overlay.hidden = false;
|
||
overlay.setAttribute('aria-hidden', 'false');
|
||
document.body.classList.add('chimera-dialog-open');
|
||
requestAnimationFrame(() => (initialFocus || dialogFocusable(overlay)[0] || overlay.querySelector('[role="dialog"]'))?.focus?.());
|
||
}
|
||
|
||
function closeAccessibleDialog(overlay) {
|
||
if (!overlay) return;
|
||
overlay.hidden = true;
|
||
overlay.setAttribute('aria-hidden', 'true');
|
||
const returnFocus = _dialogReturnFocus.get(overlay);
|
||
_dialogReturnFocus.delete(overlay);
|
||
if (![...document.querySelectorAll('[role="dialog"][aria-modal="true"]')]
|
||
.some(dialog => !dialog.closest('[hidden]') && dialog.getClientRects().length)) {
|
||
document.body.classList.remove('chimera-dialog-open');
|
||
}
|
||
if (returnFocus?.isConnected) requestAnimationFrame(() => returnFocus.focus());
|
||
}
|
||
|
||
document.addEventListener('keydown', event => {
|
||
const tab = event.target.closest?.('[role="tab"]');
|
||
if (tab && ['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
|
||
const tabs = [...tab.closest('[role="tablist"]')?.querySelectorAll('[role="tab"]') || []].filter(item => !item.disabled && !item.hidden);
|
||
if (tabs.length) {
|
||
event.preventDefault();
|
||
const current = Math.max(0, tabs.indexOf(tab));
|
||
const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : (current + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length;
|
||
tabs[next].focus();
|
||
tabs[next].click();
|
||
return;
|
||
}
|
||
}
|
||
if (event.key !== 'Tab') return;
|
||
const dialogs = [...document.querySelectorAll('[role="dialog"][aria-modal="true"]')]
|
||
.filter(dialog => !dialog.closest('[hidden]') && dialog.getClientRects().length);
|
||
const dialog = dialogs.at(-1);
|
||
if (!dialog) return;
|
||
const focusable = dialogFocusable(dialog);
|
||
if (!focusable.length) { event.preventDefault(); dialog.focus(); return; }
|
||
const first = focusable[0];
|
||
const last = focusable.at(-1);
|
||
if (event.shiftKey && (document.activeElement === first || !dialog.contains(document.activeElement))) { event.preventDefault(); last.focus(); }
|
||
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
||
});
|
||
|
||
document.addEventListener('error', event => {
|
||
const image = event.target;
|
||
if (!(image instanceof HTMLImageElement) || !image.dataset.imageError) return;
|
||
const fallbackSrc = image.dataset.fallbackSrc;
|
||
if (fallbackSrc && image.dataset.fallbackAttempted !== 'true' && image.currentSrc !== fallbackSrc) {
|
||
image.dataset.fallbackAttempted = 'true';
|
||
image.classList.add('is-fallback-artwork');
|
||
image.src = fallbackSrc;
|
||
return;
|
||
}
|
||
image.hidden = true;
|
||
if (image.dataset.imageError === 'fallback' && image.nextElementSibling) image.nextElementSibling.hidden = false;
|
||
}, true);
|
||
|
||
function setGameWorkspaceContext(gameId, target) {
|
||
const normalized = String(gameId || '').trim().toUpperCase();
|
||
if (!/^(?:CUSA|PPSA)\d{5}$/.test(normalized)) return false;
|
||
sessionStorage.setItem('chimera.gameWorkspace', JSON.stringify({ gameId: normalized, target: String(target || ''), at: Date.now() }));
|
||
return true;
|
||
}
|
||
|
||
function consumeGameWorkspaceContext(target) {
|
||
try {
|
||
const raw = sessionStorage.getItem('chimera.gameWorkspace');
|
||
const context = raw ? JSON.parse(raw) : null;
|
||
if (!context || context.target !== target || Date.now() - Number(context.at || 0) > 5 * 60 * 1000) return null;
|
||
sessionStorage.removeItem('chimera.gameWorkspace');
|
||
return /^(?:CUSA|PPSA)\d{5}$/.test(String(context.gameId || '')) ? context.gameId : null;
|
||
} catch (_) {
|
||
sessionStorage.removeItem('chimera.gameWorkspace');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function _isNarrowLayout() {
|
||
return window.matchMedia('(max-width: 900px)').matches;
|
||
}
|
||
|
||
function setMobileNavOpen(open) {
|
||
document.body.classList.toggle('mobile-nav-open', !!open && _isNarrowLayout());
|
||
const isOpen = document.body.classList.contains('mobile-nav-open');
|
||
const toggle = $('mobile-nav-toggle');
|
||
if (toggle) {
|
||
toggle.setAttribute('aria-expanded', String(isOpen));
|
||
toggle.setAttribute('aria-label', isOpen ? 'Close navigation' : 'Open navigation');
|
||
toggle.title = isOpen ? 'Close navigation' : 'Open navigation';
|
||
toggle.textContent = isOpen ? '×' : '☰';
|
||
}
|
||
const companionMore = $('chimera-companion-more');
|
||
if (companionMore) companionMore.setAttribute('aria-expanded', String(isOpen));
|
||
const scrim = $('sidebar-scrim');
|
||
if (scrim) scrim.hidden = !isOpen;
|
||
const companionDock = $('chimera-companion-dock');
|
||
if (companionDock) {
|
||
companionDock.toggleAttribute('inert', isOpen);
|
||
if (isOpen) companionDock.setAttribute('aria-hidden', 'true');
|
||
else companionDock.removeAttribute('aria-hidden');
|
||
}
|
||
if (isOpen) {
|
||
requestAnimationFrame(() => document.querySelector('.chimera-route-link.active, .chimera-domain-home.active')?.scrollIntoView({ block: 'nearest' }));
|
||
}
|
||
}
|
||
|
||
function closeMobileNav() {
|
||
setMobileNavOpen(false);
|
||
}
|
||
|
||
function applyDensity(value) {
|
||
const density = value === 'compact' ? 'compact' : 'comfortable';
|
||
document.body.dataset.density = density;
|
||
return density;
|
||
}
|
||
|
||
function _pageLoader(page, background = false) {
|
||
const loaders = {
|
||
dashboard: () => loadDashboard(),
|
||
games: () => loadGames(),
|
||
packages: () => loadPackages(),
|
||
backporks: () => loadBackporks(),
|
||
backups: () => loadBackups(),
|
||
database: () => loadDatabase(),
|
||
settings: () => loadSettings({ background }),
|
||
jailbreak: () => loadJailbreak(),
|
||
psnotify: () => loadPsNotify(),
|
||
xavatar: () => loadXavatar(),
|
||
'system-view': () => loadSystemView(background ? { passive: true } : {}),
|
||
transfers: () => loadTransfers(),
|
||
media: () => loadMedia(),
|
||
cheats: () => loadCheats({ background }),
|
||
garlic: () => loadGarlic(),
|
||
'elf-arsenal': () => (typeof loadElfArsenal === 'function' ? loadElfArsenal({ background }) : null),
|
||
'pfs-ripper': () => (typeof loadPfsRipper === 'function' ? loadPfsRipper({ background }) : null),
|
||
savemgr: () => loadSaveMgr({ background }),
|
||
ecosystem: () => (typeof loadEcosystem === 'function' ? loadEcosystem({ background }) : null),
|
||
operations: () => (typeof loadOperations === 'function' ? loadOperations({ background }) : null),
|
||
};
|
||
return loaders[page] || null;
|
||
}
|
||
|
||
function _pageStaleMs(page) {
|
||
return _PAGE_STALE_MS[page] || _PAGE_STALE_MS.default;
|
||
}
|
||
|
||
function pageIsStale(page) {
|
||
const last = _pageLoadTimestamps.get(page) || 0;
|
||
return (Date.now() - last) > _pageStaleMs(page);
|
||
}
|
||
|
||
function ensurePageReady(page, { background = false, force = false } = {}) {
|
||
const needsActiveHydrate = !background && _pageLoadModes.get(page) === 'background';
|
||
if (!force && !needsActiveHydrate && !pageIsStale(page)) return _pageLoadInFlight.get(page) || Promise.resolve();
|
||
if (_pageLoadInFlight.has(page)) return _pageLoadInFlight.get(page);
|
||
const run = Promise.resolve()
|
||
.then(() => window.ChimeraModuleLoader?.ensureForPage(page))
|
||
.then(() => {
|
||
const loader = _pageLoader(page, background);
|
||
return loader ? loader() : null;
|
||
})
|
||
.then(() => {
|
||
_pageLoadTimestamps.set(page, Date.now());
|
||
const priorMode = _pageLoadModes.get(page);
|
||
_pageLoadModes.set(page, background && priorMode === 'active' ? 'active' : (background ? 'background' : 'active'));
|
||
})
|
||
.catch(err => {
|
||
if (background) {
|
||
const method = isExpectedUnavailable(err) ? 'info' : 'warn';
|
||
console[method](`[page-load] ${page} was not preloaded: ${err.message}`);
|
||
return;
|
||
}
|
||
throw err;
|
||
})
|
||
.finally(() => {
|
||
if (_pageLoadInFlight.get(page) === run) _pageLoadInFlight.delete(page);
|
||
});
|
||
_pageLoadInFlight.set(page, run);
|
||
return run;
|
||
}
|
||
|
||
function scheduleBackgroundWarm(delay = 600) {
|
||
if (_pageWarmTimer || _pageWarmRunning) return;
|
||
_pageWarmTimer = setTimeout(() => {
|
||
_pageWarmTimer = null;
|
||
startBackgroundWarm().catch(() => {});
|
||
}, delay);
|
||
}
|
||
|
||
async function startBackgroundWarm() {
|
||
if (_pageWarmRunning) return;
|
||
_pageWarmRunning = true;
|
||
try {
|
||
for (const page of _PAGE_WARM_ORDER) {
|
||
const active = document.querySelector('.page.active')?.id?.replace('page-', '');
|
||
if (page === active) continue;
|
||
await ensurePageReady(page, { background: true });
|
||
await new Promise(resolve => setTimeout(resolve, 250));
|
||
}
|
||
} finally {
|
||
_pageWarmRunning = false;
|
||
scheduleBackgroundWarm(_PAGE_STALE_MS.default);
|
||
}
|
||
}
|
||
|
||
function fmt(bytes) {
|
||
const n = Number(bytes);
|
||
if (!n || isNaN(n)) return '—';
|
||
const gb = n / 1073741824;
|
||
return gb >= 1 ? `${gb.toFixed(2)} GB` : `${(n / 1048576).toFixed(1)} MB`;
|
||
}
|
||
|
||
function yesNo(v) {
|
||
return v ? '<span class="pill pill-yes">Yes</span>' : '<span class="pill pill-no">No</span>';
|
||
}
|
||
|
||
function escHtml(s) {
|
||
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
// Pinnable cards registry
|
||
const PINNABLE_CARDS = [
|
||
{ id: 'payload-update', label: 'Payload Update Checker', page: 'jailbreak', hint: 'Check sources for payload updates and push to PS5.' },
|
||
{ id: 'payload-mgr', label: 'Payload Sender', page: 'jailbreak', type: 'widget', hint: 'Quick-send a payload to PS5 via TCP.' },
|
||
{ id: 'psnotify', label: 'Send Notification', page: 'psnotify', type: 'widget', hint: 'Send a PS5 notification from the dashboard.' },
|
||
{ id: 'xavatar-convert', label: 'XAvatar Quick Send', page: 'xavatar', type: 'widget', hint: 'Convert an image and send to PS5 as .xavatar.' },
|
||
{ id: 'xavatar-library', label: 'XAvatar Library', page: 'xavatar', hint: 'Manage .xavatar files on your PS5.' },
|
||
{ id: 'transfers', label: 'Transfer Manager', page: 'transfers', hint: 'Monitor active and queued FTP transfers.' },
|
||
{ id: 'games', label: 'Games Library', page: 'games', hint: 'Browse and manage your PS5 game collection.' },
|
||
{ id: 'cheats', label: 'Cheats', page: 'cheats', hint: 'Browse and install game cheats.' },
|
||
{ id: 'media', label: 'Media', page: 'media', hint: 'Browse screenshots and video clips from PS5.' },
|
||
{ id: 'system-view', label: 'System View', page: 'system-view', hint: 'View your PS5 via capture card stream.' },
|
||
{ id: 'backups', label: 'Backups', page: 'backups', hint: 'Browse and manage game backup files.' },
|
||
// Other page widgets
|
||
{ id: 'dash-savemgr', label: 'Save Manager', page: 'savemgr', type: 'widget', hint: 'Browse and download PS5 save files.' },
|
||
];
|
||
|
||
const _PIN_PAGE_LABELS = {
|
||
jailbreak: 'Payload Workspace', psnotify: 'Notifications', xavatar: 'XAvatar',
|
||
transfers: 'Transfers', games: 'Games', cheats: 'Cheats & patches', media: 'Media',
|
||
'system-view': 'System View', backups: 'Backups', savemgr: 'Local Save Manager',
|
||
ecosystem: 'Console Status', operations: 'Operations',
|
||
};
|
||
|
||
function setStatus(msg, type = '') {
|
||
const el = $('sb-msg');
|
||
el.textContent = msg;
|
||
el.style.color = type === 'error' ? 'var(--red)' : type === 'ok' ? 'var(--green)' : 'var(--yellow)';
|
||
if (msg) setTimeout(() => { if (el.textContent === msg) el.textContent = ''; }, 5000);
|
||
}
|
||
|
||
function titleHue(id){let h=0;for(const c of String(id||''))h=(h*31+c.charCodeAt(0))%360;return h;}
|
||
// Server messages are Dutch; translated at display time via serverError.* keys.
|
||
function friendlyMessage(text) {
|
||
const message = String(text ?? '').trim();
|
||
if (!/^en(?:-|$)/i.test(document.documentElement.lang || 'en')) return message;
|
||
const mapped = window.i18n?.strings?.['serverError.' + message];
|
||
if (mapped) return mapped;
|
||
return message
|
||
.replace(/niet bereikbaar/gi, 'unreachable')
|
||
.replace(/niet beschikbaar/gi, 'unavailable')
|
||
.replace(/niet geconfigureerd/gi, 'not configured')
|
||
.replace(/ontbreekt/gi, 'is missing');
|
||
}
|
||
|
||
function showToast(msg, type = 'info', duration = 5000) {
|
||
const icons = { ok: '✓', error: '✕', info: '●' };
|
||
const safeType = Object.hasOwn(icons, type) ? type : 'info';
|
||
const message = friendlyMessage(msg);
|
||
if (!message) return null;
|
||
const container = $('toast-container');
|
||
const key = `${safeType}:${message.replace(/\s+/g, ' ').toLocaleLowerCase()}`;
|
||
const existing = [...container.querySelectorAll('.toast')].find(item => item.dataset.toastKey === key);
|
||
if (existing) {
|
||
const count = Number(existing.dataset.toastCount || 1) + 1;
|
||
existing.dataset.toastCount = String(count);
|
||
let badge = existing.querySelector('.toast-count');
|
||
if (!badge) {
|
||
badge = document.createElement('span');
|
||
badge.className = 'toast-count';
|
||
existing.querySelector('.toast-body')?.after(badge);
|
||
}
|
||
badge.textContent = `×${count}`;
|
||
clearTimeout(existing._dismissTimer);
|
||
existing._dismissTimer = setTimeout(() => existing._dismiss?.(), duration);
|
||
return existing;
|
||
}
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast toast-${safeType}`;
|
||
toast.dataset.toastKey = key;
|
||
toast.dataset.toastCount = '1';
|
||
toast.setAttribute('role', safeType === 'error' ? 'alert' : 'status');
|
||
const closeLabel = window.t?.('notifications.close', 'Close notification') || 'Close notification';
|
||
const icon = document.createElement('span');
|
||
icon.className = 'toast-icon';
|
||
icon.setAttribute('aria-hidden', 'true');
|
||
icon.textContent = icons[safeType];
|
||
const body = document.createElement('span');
|
||
body.className = 'toast-body';
|
||
body.textContent = message;
|
||
const close = document.createElement('button');
|
||
close.className = 'toast-close';
|
||
close.type = 'button';
|
||
close.setAttribute('aria-label', closeLabel);
|
||
close.textContent = '×';
|
||
toast.append(icon, body, close);
|
||
while (container.children.length >= 4) container.firstElementChild?.remove();
|
||
container.appendChild(toast);
|
||
const dismiss = () => {
|
||
if (!toast.isConnected || toast.classList.contains('toast-out')) return;
|
||
toast.classList.add('toast-out');
|
||
toast.addEventListener('animationend', () => toast.remove(), { once: true });
|
||
setTimeout(() => toast.remove(), 350);
|
||
};
|
||
toast._dismiss = dismiss;
|
||
close.addEventListener('click', dismiss);
|
||
toast._dismissTimer = setTimeout(dismiss, duration);
|
||
toast.addEventListener('mouseenter', () => clearTimeout(toast._dismissTimer));
|
||
toast.addEventListener('mouseleave', () => { toast._dismissTimer = setTimeout(dismiss, 2000); });
|
||
return toast;
|
||
}
|
||
|
||
// ── Navigation ────────────────────────────────────────────────────────────────
|
||
function navigate(page) {
|
||
document.querySelectorAll('.nav-link').forEach(a => a.classList.toggle('active', a.dataset.page === page));
|
||
document.querySelectorAll('.page').forEach(p => p.classList.toggle('active', p.id === `page-${page}`));
|
||
|
||
// Embed pages need #content to be overflow:hidden so the iframe can fill available height
|
||
const isEmbed = page === 'savemgr';
|
||
document.getElementById('content').classList.toggle('embed-page-active', isEmbed);
|
||
ensurePageReady(page, { force: pageIsStale(page) });
|
||
scheduleBackgroundWarm(800);
|
||
closeMobileNav();
|
||
syncPinStates();
|
||
}
|
||
|
||
$('mobile-nav-toggle')?.addEventListener('click', () => {
|
||
setMobileNavOpen(!document.body.classList.contains('mobile-nav-open'));
|
||
});
|
||
|
||
$('sidebar-scrim')?.addEventListener('click', closeMobileNav);
|
||
|
||
window.addEventListener('resize', () => {
|
||
if (!_isNarrowLayout()) closeMobileNav();
|
||
});
|
||
|
||
async function syncPinStates() {
|
||
try {
|
||
const s = await window.pork.getSettings();
|
||
const pinned = new Set(s.pinnedCards || []);
|
||
document.querySelectorAll('.btn-pin').forEach(btn =>
|
||
btn.classList.toggle('pinned', pinned.has(btn.dataset.pinId))
|
||
);
|
||
} catch (_) {}
|
||
}
|
||
|
||
async function loadJailbreak() {
|
||
// Reset UI
|
||
$('payload-check-status').textContent = '';
|
||
$('payload-sources-check-list').innerHTML = '';
|
||
if ($('payload-show-expert')) {
|
||
$('payload-show-expert').checked = localStorage.getItem('chimera.payload.showExpert') === '1';
|
||
}
|
||
try { await loadPayloadCatalog(); } catch (error) {
|
||
$('payload-sources-check-list').innerHTML = `<p class="hint">${escHtml(window.t('payload.catalogLoadFailed', 'Catalog could not be loaded'))}: ${escHtml(error.message)}</p>`;
|
||
}
|
||
await renderLocalPayloads();
|
||
await renderPayloadSender();
|
||
await renderAutoloaderSnapshots();
|
||
try {
|
||
const settings = await window.pork.getSettings();
|
||
if ($('pldmgr-port')) $('pldmgr-port').value = settings.payloadManagerPort || 8084;
|
||
if (!settings.ftpHost) {
|
||
if ($('service-matrix-summary')) $('service-matrix-summary').textContent = window.t('services.ps5NotConfigured', 'PS5 not configured');
|
||
if ($('service-matrix-grid')) $('service-matrix-grid').innerHTML = `<p class="hint">${escHtml(window.t('services.configureProfile', 'Configure a console profile before checking services.'))}</p>`;
|
||
if ($('pldmgr-version')) $('pldmgr-version').textContent = 'offline';
|
||
if ($('pldmgr-status')) $('pldmgr-status').textContent = window.t('services.ps5NotConfigured', 'PS5 not configured');
|
||
return;
|
||
}
|
||
await loadPs5ServiceMatrix();
|
||
await connectPs5PayloadManager({ quiet: true });
|
||
} catch (_) {}
|
||
}
|
||
|
||
function isExpectedUnavailable(error) {
|
||
if (error?.expected || ['NOT_CONFIGURED', 'UNAVAILABLE', 'UPSTREAM_UNAVAILABLE'].includes(error?.code)) return true;
|
||
return /niet (?:bereikbaar|verbonden|beschikbaar|actief|geconfigureerd|gevonden)|maak eerst verbinding|not (?:reachable|connected|available|active|configured|found|mounted)|connect (?:to )?(?:the )?PS5|offline|ECONNREFUSED|timed? out|reageerde niet op tijd|HTTP (?:403|429|5\d\d)|rate.?limit|ontbreekt/i.test(String(error?.message || ''));
|
||
}
|
||
|