function darkenHex(hex, amount = 0.18, satCap = null) { const r = parseInt(hex.slice(1,3), 16) / 255; const g = parseInt(hex.slice(3,5), 16) / 255; const b = parseInt(hex.slice(5,7), 16) / 255; const max = Math.max(r,g,b), min = Math.min(r,g,b); let h = 0, s = 0, l = (max+min)/2; if (max !== min) { const d = max - min; s = l > 0.5 ? d/(2-max-min) : d/(max+min); switch(max) { case r: h = ((g-b)/d + (g { if (t<0) t+=1; if (t>1) t-=1; if (t<1/6) return p+(q-p)*6*t; if (t<1/2) return q; if (t<2/3) return p+(q-p)*(2/3-t)*6; return p; }; const q = l < 0.5 ? l*(1+s) : l+s-l*s; const p = 2*l - q; const toHex = v => Math.round(hue2rgb(p,q,v)*255).toString(16).padStart(2,'0'); return `#${toHex(h+1/3)}${toHex(h)}${toHex(h-1/3)}`; } function applyAccent(hex) { if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return; // --accent = tint (text/borders); --accent-d = fill (buttons); --accent-rgb tracks the fill. const fill = darkenHex(hex, 0.26, 0.78); const r = parseInt(fill.slice(1,3), 16); const g = parseInt(fill.slice(3,5), 16); const b = parseInt(fill.slice(5,7), 16); const root = document.documentElement; root.style.setProperty('--accent', hex); root.style.setProperty('--accent-d', fill); root.style.setProperty('--accent-rgb', `${r}, ${g}, ${b}`); const swatch = $('accent-swatch'); const picker = $('s-accent-color'); const hexEl = $('s-accent-hex'); if (swatch) swatch.style.background = fill; if (picker) picker.value = hex; if (hexEl) hexEl.value = hex.toUpperCase(); } let settingsTimer = null; let remoteGamePaths = []; let localBackupPaths = []; let _userDirty = false; let _passDirty = false; const settingsT = (key, fallback) => typeof window.t === 'function' ? window.t(key, fallback) : fallback; function renderConsoleProfiles(packet) { const select = $('console-profile-active'); if (!select) return; select.innerHTML = (packet.profiles || []).map(profile => ``).join(''); select.value = packet.activeId || ''; const active = (packet.profiles || []).find(profile => profile.id === packet.activeId); $('console-profile-status').textContent = active ? `${active.platform} · Remote Play ${active.secrets?.remotePlayKey ? settingsT('settings.status.linked', 'linked') : settingsT('settings.status.notLinked', 'not linked')} · Sentinel ${active.secrets?.sentinelPairingKey ? settingsT('settings.status.paired', 'paired') : settingsT('settings.status.notPaired', 'not paired')}` : settingsT('settings.status.noActiveProfile', 'No active profile'); } function renderConsoleIdentity(identity) { if (!$('settings-identity-profile')) return; const unavailable = settingsT('settings.identity.unavailable', 'Not observed'); $('settings-identity-profile').textContent = identity?.profileLabel || unavailable; $('settings-identity-local').textContent = identity?.local?.username || unavailable; $('settings-identity-account').textContent = identity?.local?.accountIdMasked || unavailable; $('settings-identity-psn').textContent = identity?.linkedPsn?.linked && identity.linkedPsn.onlineId ? identity.linkedPsn.onlineId : unavailable; const observed = identity?.provenance?.observedAt ? new Date(identity.provenance.observedAt).toLocaleString() : settingsT('settings.identity.noTimestamp', 'no timestamp'); $('settings-identity-evidence').textContent = `${identity?.provenance?.source || 'unknown'} · ${observed}`; } async function loadConsoleProfiles() { if (!window.pork.consoleProfiles || !$('console-profile-active')) return; renderConsoleProfiles(await window.pork.consoleProfiles()); if (window.pork.consoleIdentity) renderConsoleIdentity(await window.pork.consoleIdentity()); } function renderCompanionWorkers(workers) { const target = $('companion-workers-list'); if (!target) return; target.innerHTML = workers.length ? workers.map(worker => `
${escHtml(worker.label)}${escHtml(worker.endpoint)} · ${worker.paired ? settingsT('settings.status.paired', 'paired') : settingsT('settings.status.notPaired', 'not paired')}
`).join('') : `

${settingsT('settings.companion.empty', 'No companion workers configured.')}

`; } async function loadCompanionWorkers() { if (window.pork.companionWorkers && $('companion-workers-list')) renderCompanionWorkers(await window.pork.companionWorkers()); } async function loadTotpStatus() { if (!$('totp-settings-status')) return; if (!window.pork.totpStatus) { $('totp-settings-status').textContent = settingsT('settings.twoFactor.unavailable', 'Unavailable in this runtime'); $('totp-settings-begin').disabled = true; $('totp-settings-disable').hidden = true; $('passkey-readiness').innerHTML = `${escHtml(settingsT('settings.passkeys.preparedTitle', 'Passkeys: safely prepared, not active yet'))}

${escHtml(settingsT('settings.passkeys.runtimeUnavailable', 'This runtime does not expose the required enrollment service.'))}

`; return; } let status; try { status = await Promise.race([ window.pork.totpStatus(), new Promise((_, reject) => setTimeout(() => reject(new Error(settingsT('settings.twoFactor.timeout', 'Status check timed out'))), 3500)), ]); } catch (error) { $('totp-settings-status').textContent = settingsT('settings.twoFactor.unavailable', 'Temporarily unavailable'); $('totp-settings-status').title = error.message; $('totp-settings-begin').disabled = true; return; } $('totp-settings-begin').disabled = false; $('totp-settings-status').textContent = status.enabled ? `${settingsT('settings.status.active', 'Active')} · ${status.recoveryCodesRemaining} ${settingsT('settings.twoFactor.codesRemaining', 'recovery codes remaining')}` : status.pending ? settingsT('settings.twoFactor.pending', 'Pairing started but not confirmed') : settingsT('settings.status.inactive', 'Inactive'); $('totp-settings-begin').textContent = status.enabled ? settingsT('settings.twoFactor.repair', 'Pair authenticator again') : settingsT('settings.twoFactor.pair', 'Pair authenticator'); $('totp-settings-disable').hidden = !status.enabled; $('passkey-readiness').innerHTML = `Passkeys: ${status.passkeys?.available ? settingsT('settings.status.available', 'available') : settingsT('settings.passkeys.prepared', 'safely prepared, not active yet')}

${escHtml(status.passkeys?.reason || settingsT('settings.status.none', 'No status.'))}

`; } async function loadGenericNotificationStatus() { if (!window.pork.genericNotificationStatus || !$('generic-notification-status')) return; const status = await window.pork.genericNotificationStatus(); $('generic-notification-enabled').checked = status.enabled; $('generic-notification-endpoint').value = ''; $('generic-notification-endpoint').placeholder = status.endpointConfigured ? `${settingsT('settings.notifications.configuredFor', 'Configured for')} ${status.endpointHost}; ${settingsT('settings.notifications.leaveBlank', 'leave blank to keep')}` : 'https://notify.example/chimera'; $('generic-notification-token').value = ''; $('generic-notification-cooldown').value = status.cooldownMinutes || 60; $('generic-notification-rules').innerHTML = (status.availableRules || []).map(rule => ``).join(''); $('generic-notification-status').textContent = `${status.format} · endpoint ${status.endpointConfigured ? status.endpointHost : settingsT('settings.status.notConfigured', 'not configured')} · bearer ${status.bearerConfigured ? settingsT('settings.status.encrypted', 'configured and encrypted') : settingsT('settings.status.notConfigured', 'not configured')}`; } function renderConsoleComparison(packet) { const target = $('console-profiles-comparison'); target.hidden = false; target.innerHTML = `
${(packet.profiles || []).map(profile => `
${profile.active ? '● ' : ''}${escHtml(profile.label)}${escHtml(profile.host)} · firmware ${escHtml(profile.firmware)} · ${escHtml(profile.hen)} ${escHtml(profile.henVersion)}
Remote Play ${profile.pairing?.remotePlay ? settingsT('settings.status.linked', 'linked') : settingsT('settings.status.notLinked', 'not linked')} · Sentinel ${profile.pairing?.sentinel ? settingsT('settings.status.paired', 'paired') : settingsT('settings.status.notPaired', 'not paired')} · ${settingsT('settings.status.storage', 'storage')} ${escHtml(profile.storage?.status || 'unknown')}
`).join('')}
`; } $('console-profile-activate')?.addEventListener('click', async event => { try { const result = await window.pork.consoleProfileActivate($('console-profile-active').value); renderConsoleProfiles(result); await loadSettings(); showToast('Console profile activated; no automatic connection was started.', 'ok', 7000); } catch (error) { showToast(error.message, 'error'); } }); $('console-profile-new')?.addEventListener('submit', async event => { event.preventDefault(); try { const result = await window.pork.consoleProfileSave({ label: $('console-profile-new-label').value.trim(), host: $('console-profile-new-host').value.trim(), makeActive: false }); renderConsoleProfiles(result); event.target.reset(); showToast(settingsT('settings.consoleProfiles.added', 'Console profile added without starting a connection.'), 'ok'); } catch (error) { showToast(error.message, 'error'); } }); $('console-profiles-compare')?.addEventListener('click', async event => { try { renderConsoleComparison(await window.pork.consoleProfilesCompare()); } catch (error) { showToast(error.message, 'error'); } }); $('companion-worker-form')?.addEventListener('submit', async event => { event.preventDefault(); try { renderCompanionWorkers(await window.pork.companionWorkerSave({ id: $('companion-worker-id').value.trim(), label: $('companion-worker-label').value.trim(), endpoint: $('companion-worker-endpoint').value.trim() })); event.target.reset(); showToast('Companion worker saved; no job was started.', 'ok'); } catch (error) { showToast(error.message, 'error'); } }); $('companion-workers-list')?.addEventListener('click', async event => { const row = event.target.closest('[data-worker-id]'); if (!row) return; const id = row.dataset.workerId; try { if (event.target.closest('.companion-worker-test')) { const status = await window.pork.companionWorkerCapabilities(id); showToast(`Worker verified: ${status.capabilities.join(', ') || 'no supported capabilities'}; shell disabled.`, 'ok', 9000); } if (event.target.closest('.companion-worker-pair')) { const workers = await window.pork.companionWorkers(); const rotate = workers.find(worker => worker.id === id)?.paired === true; const phrase = `PAIR WORKER ${id}`; const confirmation = await showExactConfirmation(`${rotate ? 'Rotate' : 'Create'} the pairing key for this worker?`, phrase); if (!confirmation) return; const result = await window.pork.companionWorkerPair({ id, rotate, confirmation }); if (result.pairingKey) { const output = $('companion-worker-pairing-output'); output.hidden = false; output.textContent = `Worker: ${id}\nPairing key (eenmalig zichtbaar): ${result.pairingKey}\n\nImporteer lokaal in Chimera Worker en wis deze weergave.`; } await loadCompanionWorkers(); } if (event.target.closest('.companion-worker-remove') && await showExactConfirmation(`Remove ${id} and erase its pairing key?`, 'DELETE')) renderCompanionWorkers(await window.pork.companionWorkerRemove(id)); } catch (error) { showToast(error.message, 'error', 10000); } }); $('totp-settings-begin')?.addEventListener('click', async event => { if (!await showConfirm('Pair a new authenticator? TOTP becomes active only after you confirm a valid code.')) return; try { const enrollment = await window.pork.totpBegin(); const output = $('totp-settings-output'); output.hidden = false; output.textContent = `One-time TOTP secret: ${enrollment.secret}\n\nAuthenticator URI:\n${enrollment.uri}`; const code = await showPrompt('Add the secret or URI to your authenticator, then enter the current 6-digit code:'); if (code == null) return; const confirmed = await window.pork.totpConfirm(code); output.textContent = `TOTP is active. Save these recovery codes now; they will not be shown again:\n\n${confirmed.recoveryCodes.join('\n')}`; await loadTotpStatus(); showToast('Two-factor authentication is active.', 'ok', 8000); } catch (error) { showToast(error.message, 'error', 10000); } }); $('totp-settings-disable')?.addEventListener('click', async () => { const code = await showPrompt('Enter a valid authenticator or recovery code to disable TOTP:'); if (code == null) return; if (!await showExactConfirmation('Disable two-factor authentication?', 'UITSCHAKELEN')) return; try { await window.pork.totpDisable(code); $('totp-settings-output').hidden = true; await loadTotpStatus(); showToast('Two-factor authentication disabled.', 'ok'); } catch (error) { showToast(error.message, 'error'); } }); $('generic-notification-form')?.addEventListener('submit', async event => { event.preventDefault(); try { const token = $('generic-notification-token').value; const endpoint = $('generic-notification-endpoint').value.trim(); const rules = [...document.querySelectorAll('[data-notification-rule]:checked')].map(input => input.dataset.notificationRule); await window.pork.genericNotificationSave({ enabled: $('generic-notification-enabled').checked, rules, cooldownMinutes: Number($('generic-notification-cooldown').value), ...(endpoint ? { endpoint } : {}), ...(token ? { bearerToken: token } : {}) }); await loadGenericNotificationStatus(); showToast('Generic notification settings saved.', 'ok'); } catch (error) { showToast(error.message, 'error'); } }); $('generic-notification-test')?.addEventListener('click', async event => { try { await window.pork.genericNotificationTest(); showToast('Test notification delivered.', 'ok'); } catch (error) { showToast(error.message, 'error'); } }); $('settings-automation-analyze')?.addEventListener('click', async event => { const button = event.currentTarget; button.disabled = true; try { const [sources, protection] = await Promise.all([window.pork.automationSourcesAnalyze(), window.pork.automationProtectionStatus()]); $('settings-automation-analysis').innerHTML = `
${sources.roots.length}roots inspected
${sources.recommendations.length}high-confidence suggestions
${protection.saveVault.eligible ? 'Ready' : 'Setup'}Save Vault
${(sources.roots || []).map(item => `
${escHtml(item.root)}${escHtml(item.classification)} · ${escHtml(item.confidence)} confidence · ${item.writable ? 'writable' : 'read-only/unavailable'}
${escHtml(item.exists ? 'inspected' : item.error || 'missing')}
`).join('')}

No configuration changes were applied. Enable Save Vault, replication and restore drills from Settings → Automation after reviewing their targets.

`; } catch (error) { showToast(error.message, 'error'); } finally { button.disabled = false; } }); function scheduleSettingsSave() { clearTimeout(settingsTimer); settingsTimer = setTimeout(commitSettingsSave, 600); } let _webhookDirty = false; $('s-discord-webhook')?.addEventListener('input', () => { _webhookDirty = true; }); async function commitSettingsSave() { const payload = { ftpHost: $('s-host').value.trim(), ftpPorts: $('s-ports').value.split(',').map(p => p.trim()).filter(Boolean), backupPaths: localBackupPaths, backupPath: localBackupPaths[0] || '', payloadLocalPath: $('s-payload-local').value.trim(), payloadRemotePath:$('s-payload-remote').value.trim(), cheatsRemotePath: $('s-cheats-remote').value.trim(), mediaPsNotifyEnabled: $('s-media-discord-notify').checked, autoConnect: $('s-auto-connect').checked, timeFormat: $('s-time-format').value, hashConcurrency: Number($('s-hash-concurrency')?.value) || 8, }; if (_userDirty) payload.ftpUser = $('s-user').value.trim(); if (_passDirty) payload.ftpPass = $('s-pass').value; if (_webhookDirty) payload.discordWebhookUrl = $('s-discord-webhook').value.trim(); await window.pork.setSettings(payload); _passDirty = false; _webhookDirty = false; $('d-host').value = $('s-host').value; $('d-ports').value = $('s-ports').value; setStatus('Settings saved', 'ok'); } async function loadSettings(options = {}) { const background = !!options.background; const s = await window.pork.getSettings(); $('s-host').value = s.ftpHost || ''; $('s-ports').value = (s.ftpPorts && s.ftpPorts.length) ? s.ftpPorts.join(', ') : '1337, 2121, 21'; localBackupPaths = s.backupPaths || (s.backupPath ? [s.backupPath] : []); renderLocalBackupPaths(); $('s-payload-local').value = s.payloadLocalPath || ''; $('s-payload-remote').value = s.payloadRemotePath || '/data/payloads/'; $('s-cheats-remote').value = s.cheatsRemotePath || '/data/etaHEN/cheats/'; $('s-discord-webhook').value = ''; $('s-discord-webhook').placeholder = s.discordWebhookConfigured ? 'Stored securely — enter to replace' : 'https://discord.com/api/webhooks/...'; $('s-media-discord-notify').checked = s.mediaPsNotifyEnabled !== false; // default true $('media-discord-psn-detail').hidden = s.mediaPsNotifyEnabled === false; $('s-media-psn-download').checked = s.mediaPsNotifyDiscordDownload !== false; $('s-media-psn-convert').checked = s.mediaPsNotifyDiscordConvert !== false; $('s-media-psn-upload').checked = s.mediaPsNotifyDiscordUpload !== false; $('s-media-psn-done').checked = s.mediaPsNotifyDiscordDone !== false; $('s-media-psn-error').checked = s.mediaPsNotifyDiscordError !== false; $('s-auto-connect').checked = s.autoConnect === true; _timeFormat = s.timeFormat || '12h'; $('s-time-format').value = _timeFormat; if ($('s-interface-density')) $('s-interface-density').value = applyDensity(s.interfaceDensity); if (!background) { $('d-host').value = s.ftpHost || ''; $('d-ports').value = (s.ftpPorts && s.ftpPorts.length) ? s.ftpPorts.join(', ') : '1337, 2121, 21'; } applyAccent(s.accentColor || '#CBBAFF'); remoteGamePaths = s.remoteGamePaths || []; renderRemoteGamePaths(); await renderSettingsPayloadSources(); if (s.svHotkeys) _svHotkeys = { ..._svHotkeys, ...s.svHotkeys }; const hkMap = { 'hk-mute': 'mute', 'hk-gif': 'gif', 'hk-video': 'video', 'hk-fullscreen': 'fullscreen', 'hk-popout': 'popout' }; Object.entries(hkMap).forEach(([id, action]) => { const el = $(id); if (el) el.value = _svHotkeys[action] || ''; }); let convMode = s.gameConversionMode || 'pfs'; if (window.pork.isWebMode) { for (const unavailable of ['ffpkg', 'exfat']) { const radio = document.querySelector(`input[name="conv-mode"][value="${unavailable}"]`); if (radio) radio.disabled = true; radio?.closest('.radio-card')?.setAttribute('title', settingsT('settings.windowsModeUnavailable', 'This Windows mode is unavailable in the Unraid container. Use FFPFSC.')); const queueButton = document.querySelector(`.conv-mode-btn[data-conv-mode="${unavailable}"]`); if (queueButton) queueButton.disabled = true; } if (!['pfs', 'ffpfsc'].includes(convMode)) { convMode = 'ffpfsc'; await window.pork.setSettings({ gameConversionMode: convMode }); } } const modeRadio = document.querySelector(`input[name="conv-mode"][value="${convMode}"]`); if (modeRadio) modeRadio.checked = true; applyConvModeUI(convMode); $('s-conv-ufs2-path').value = s.ufs2ToolPath || ''; $('s-conv-exfat-path').value = s.osfmountPath || s.exfatToolPath || ''; $('s-conv-output-dir').value = s.convOutputDir || ''; $('s-conv-temp-dir').value = s.convTempDir || ''; $('s-conv-ftp-upload').checked = !!s.convFtpUpload; $('s-conv-delete-after').checked = !!s.convDeleteAfter; $('s-ps4-package-builder').value = s.ps4PackageBuilderPath || ''; $('s-ps4-package-output').value = s.ps4PackageOutputPath || ''; window.pork.ps4PackageBuilderStatus().then(status => { $('s-ps4-package-status').textContent = status.available ? settingsT('ps4Packages.settings.ready', 'Ready') : settingsT('ps4Packages.settings.blocked', 'Not configured'); $('s-ps4-package-status').className = `badge ${status.available ? 'badge-green' : 'badge-orange'}`; }).catch(error => { $('s-ps4-package-status').textContent = error.message; }); $('s-ps5-native-package-builder').value = s.ps5NativePackageBuilderPath || ''; $('s-ps5-native-package-output').value = s.ps5NativePackageOutputPath || ''; window.pork.ps5NativePackageBuilderStatus().then(status => { $('s-ps5-native-package-status').textContent = status.available ? settingsT('ps5NativePackages.settings.ready', 'Pinned bridge ready') : settingsT('ps5NativePackages.settings.blocked', 'Research bridge not configured'); $('s-ps5-native-package-status').className = `badge ${status.available ? 'badge-green' : 'badge-orange'}`; }).catch(error => { $('s-ps5-native-package-status').textContent = error.message; }); $('conv-delete-after-row').hidden = false; $('s-conv-delete-after').disabled = !s.convFtpUpload; $('conv-delete-after-row').style.opacity = s.convFtpUpload ? '' : '0.45'; $('s-conv-psn-enabled').checked = s.convPsNotifyEnabled !== false; $('conv-psn-detail').hidden = !s.convPsNotifyEnabled; $('s-conv-psn-queued').checked = s.convPsNotifyOnGameQueued !== false; $('s-conv-psn-batch').checked = s.convPsNotifyOnBatchQueued !== false; $('s-conv-psn-copy').checked = s.convPsNotifyOnCopyStart !== false; $('s-conv-psn-convert').checked = s.convPsNotifyOnConvertStart!== false; $('s-conv-psn-done').checked = s.convPsNotifyOnJobDone !== false; await refreshConvToolStatus(); if ($('s-hash-concurrency')) $('s-hash-concurrency').value = s.hashConcurrency || 8; if ($('s-webui-port')) $('s-webui-port').value = s.webUiPort || 6967; if (window.pork.isWebMode) { $('s-webui-enabled').disabled = true; $('s-webui-port').disabled = true; $('btn-webui-apply').disabled = true; $('s-webui-enabled').closest('.toggle-row')?.setAttribute('title', settingsT('settings.serverManagedByUnraid', 'This web server is managed as an Unraid container.')); } await refreshWebuiStatus(); const _langCode = s.language || 'en'; try { await refreshLangDropdown(_langCode); } catch (_) {} await window.i18n.load(_langCode); await Promise.all([ loadConsoleProfiles(), loadCompanionWorkers(), loadTotpStatus(), loadGenericNotificationStatus(), ]); } ['s-host', 's-ports', 's-hash-concurrency'].forEach(id => { const el = $(id); if (el) el.addEventListener('input', scheduleSettingsSave); }); async function refreshLangDropdown(selectedCode) { const langs = await window.pork.langList(); const langSel = $('s-language'); langSel.innerHTML = langs.map(l => { const label = l.source === 'imported' ? `${escHtml(l.name)} ✦` : escHtml(l.name); return ``; }).join(''); } $('s-language').addEventListener('change', async function() { const code = this.value; await window.pork.setSettings({ language: code }); await window.i18n.load(code); await Promise.all([ loadConsoleProfiles(), loadCompanionWorkers(), loadTotpStatus(), loadGenericNotificationStatus(), ]); setStatus(window.t('status.languageChanged', 'Language changed'), 'ok'); }); $('s-interface-density')?.addEventListener('change', async function() { const interfaceDensity = applyDensity(this.value); await window.pork.setSettings({ interfaceDensity }); setStatus(window.t('settings.density.saved', 'Interface density updated.'), 'ok'); }); $('btn-import-lang').addEventListener('click', async () => { const btn = $('btn-import-lang'); btn.disabled = true; try { const res = await window.pork.langImport(); if (res.canceled) return; const currentCode = $('s-language').value; await refreshLangDropdown(currentCode); setStatus(`${window.t('lang.importDone', 'Language imported')}: ${res.name}`, 'ok'); if (res.code === currentCode) await window.i18n.load(res.code); } catch (e) { setStatus(`${window.t('lang.importError', 'Import failed')}: ${e.message}`, 'error'); } finally { btn.disabled = false; } }); $('btn-export-lang').addEventListener('click', async () => { const btn = $('btn-export-lang'); btn.disabled = true; try { const res = await window.pork.langExportBase(); if (!res.canceled) setStatus(window.t('lang.exportDone', 'English base file exported.'), 'ok'); } catch (e) { setStatus(`${window.t('lang.exportError', 'Export failed')}: ${e.message}`, 'error'); } finally { btn.disabled = false; } }); $('d-host').addEventListener('input', () => { $('s-host').value = $('d-host').value; scheduleSettingsSave(); }); $('d-ports').addEventListener('input', () => { $('s-ports').value = $('d-ports').value; scheduleSettingsSave(); }); $('s-user').addEventListener('input', () => { _userDirty = true; scheduleSettingsSave(); }); $('s-pass').addEventListener('input', () => { _passDirty = true; scheduleSettingsSave(); }); function renderLocalBackupPaths() { const cont = $('settings-backup-paths-list'); if (!cont) return; if (!localBackupPaths.length) { cont.innerHTML = '

No folders added yet. Click “+ Add Folder…” to get started.

'; return; } cont.innerHTML = localBackupPaths.map((p, i) => `
${escHtml(p)}
`).join(''); } async function saveLocalBackupPaths() { await window.pork.setSettings({ backupPaths: localBackupPaths, backupPath: localBackupPaths[0] || '', }); setStatus('Settings saved', 'ok'); } $('settings-backup-paths-list').addEventListener('click', async e => { const row = e.target.closest('[data-backup-idx]'); if (!row || !e.target.closest('.btn-backup-path-remove')) return; const idx = Number(row.dataset.backupIdx); const ok = await showConfirm(`Remove "${escHtml(localBackupPaths[idx])}" from game source folders?`); if (!ok) return; localBackupPaths = localBackupPaths.filter((_, i) => i !== idx); await saveLocalBackupPaths(); renderLocalBackupPaths(); }); $('btn-add-backup-path').addEventListener('click', async () => { const folder = await window.pork.selectFolder(); if (!folder) return; if (localBackupPaths.includes(folder)) { setStatus('That folder is already in the list', 'ok'); return; } localBackupPaths = [...localBackupPaths, folder]; await saveLocalBackupPaths(); renderLocalBackupPaths(); }); $('btn-browse-payload-local').addEventListener('click', async () => { const folder = await window.pork.selectFolder(); if (!folder) return; $('s-payload-local').value = folder; await commitSettingsSave(); }); $('s-payload-remote').addEventListener('input', scheduleSettingsSave); $('s-cheats-remote').addEventListener('input', scheduleSettingsSave); $('s-discord-webhook').addEventListener('input', scheduleSettingsSave); $('btn-browse-cheats-remote').addEventListener('click', async () => { const cur = $('s-cheats-remote').value.trim() || '/'; const p = await openFtpBrowser(cur); if (p) { $('s-cheats-remote').value = p; scheduleSettingsSave(); } }); $('s-auto-connect').addEventListener('change', commitSettingsSave); $('s-time-format').addEventListener('change', e => { _timeFormat = e.target.value; commitSettingsSave(); renderMediaGrid(); }); $('s-media-discord-notify').addEventListener('change', function() { $('media-discord-psn-detail').hidden = !this.checked; commitSettingsSave(); }); [ ['s-media-psn-download', 'mediaPsNotifyDiscordDownload'], ['s-media-psn-convert', 'mediaPsNotifyDiscordConvert'], ['s-media-psn-upload', 'mediaPsNotifyDiscordUpload'], ['s-media-psn-done', 'mediaPsNotifyDiscordDone'], ['s-media-psn-error', 'mediaPsNotifyDiscordError'], ].forEach(([id, key]) => { $(id).addEventListener('change', function() { window.pork.setSettings({ [key]: this.checked }); }); }); function applyConvModeUI(mode) { const isPfs = mode === 'pfs'; const isFfpkg = mode === 'ffpkg'; const isExfat = mode === 'exfat'; const isConv = !isPfs; $('conv-ffpkg-fields').hidden = !isFfpkg; $('conv-exfat-fields').hidden = !isExfat; $('conv-shared-fields').hidden = !isConv; $('conv-notify-fields').hidden = !isConv; const badge = $('conv-queue-mode-badge'); if (badge) badge.textContent = isPfs ? 'Raw Dumps' : isFfpkg ? 'FFPKG' : isExfat ? 'ExFAT' : 'FFPFSC'; } async function refreshConvToolStatus() { const mode = document.querySelector('input[name="conv-mode"]:checked')?.value || 'pfs'; if (mode === 'ffpkg') { try { const info = await window.pork.convToolInfo(); const el = $('s-conv-ufs2-status'); if (!el) return; if (info.toolAvailable) { el.textContent = '✓ UFS2Tool.exe found'; el.style.color = 'var(--green)'; } else { el.textContent = info.toolPath ? '✗ File not found at configured path' : '✗ Not configured'; el.style.color = 'var(--red)'; } } catch (_) {} } if (mode === 'exfat') { try { const info = await window.pork.convExfatToolInfo(); const el = $('s-conv-exfat-status'); if (!el) return; if (info.toolAvailable) { el.textContent = `✓ OSFMount found — native exFAT (${info.osfmountPath || info.toolPath})`; el.style.color = 'var(--green)'; } else { el.textContent = '✗ OSFMount not found — install OSFMount or pick osfmount.com'; el.style.color = 'var(--red)'; } } catch (_) {} } } document.querySelectorAll('input[name="conv-mode"]').forEach(radio => { radio.addEventListener('change', async () => { const mode = radio.value; applyConvModeUI(mode); await window.pork.setSettings({ gameConversionMode: mode }); await refreshConvToolStatus(); setStatus('Conversion mode saved', 'ok'); }); }); document.querySelectorAll('input[name="ufs2-method"]').forEach(radio => { radio.addEventListener('change', async () => { await window.pork.setSettings({ ufs2Method: radio.value }); setStatus('UFS2 method saved', 'ok'); }); }); $('btn-conv-ufs2-pick').addEventListener('click', async () => { const info = await window.pork.convToolPick(); if (!info) return; $('s-conv-ufs2-path').value = info.toolPath || ''; const el = $('s-conv-ufs2-status'); if (el) { el.textContent = info.toolAvailable ? '✓ UFS2Tool.exe found' : '✗ File not found'; el.style.color = info.toolAvailable ? 'var(--green)' : 'var(--red)'; } setStatus('UFS2Tool path saved', 'ok'); }); $('btn-conv-exfat-pick').addEventListener('click', async () => { const info = await window.pork.convExfatToolPick(); if (!info) return; $('s-conv-exfat-path').value = info.toolPath || ''; await refreshConvToolStatus(); setStatus('ExFAT tool path saved', 'ok'); }); $('btn-conv-output-pick').addEventListener('click', async () => { const p = await window.pork.convPickOutputDir(); if (!p) return; $('s-conv-output-dir').value = p; setStatus('Output directory saved', 'ok'); }); $('btn-conv-temp-pick').addEventListener('click', async () => { const p = await window.pork.convPickTempDir(); if (!p) return; $('s-conv-temp-dir').value = p; setStatus('Temp directory saved', 'ok'); }); $('s-conv-ftp-upload').addEventListener('change', async function() { const ftpEnabled = this.checked; $('s-conv-delete-after').disabled = !ftpEnabled; $('conv-delete-after-row').style.opacity = ftpEnabled ? '' : '0.45'; if (!ftpEnabled) { $('s-conv-delete-after').checked = false; await window.pork.setSettings({ convFtpUpload: false, convDeleteAfter: false }); } else { await window.pork.setSettings({ convFtpUpload: true }); } setStatus('Settings saved', 'ok'); }); $('s-conv-delete-after').addEventListener('change', async function() { await window.pork.setSettings({ convDeleteAfter: this.checked }); setStatus('Settings saved', 'ok'); }); $('btn-ps4-package-builder').addEventListener('click', async () => { const folder = await window.pork.selectFolder(); if (!folder) return; await window.pork.setSettings({ ps4PackageBuilderPath: folder }); $('s-ps4-package-builder').value = folder; const status = await window.pork.ps4PackageBuilderStatus(); $('s-ps4-package-status').textContent = status.available ? settingsT('ps4Packages.settings.ready', 'Ready') : settingsT('ps4Packages.settings.blocked', 'Not configured'); }); $('btn-ps4-package-output').addEventListener('click', async () => { const folder = await window.pork.selectFolder(); if (!folder) return; await window.pork.setSettings({ ps4PackageOutputPath: folder }); $('s-ps4-package-output').value = folder; showToast(settingsT('ps4Packages.settings.restart', 'Restart Chimera to activate the new package vault.'), 'info'); }); $('btn-ps5-native-package-builder').addEventListener('click', async () => { const folder = await window.pork.selectFolder(); if (!folder) return; await window.pork.setSettings({ ps5NativePackageBuilderPath: folder }); $('s-ps5-native-package-builder').value = folder; const status = await window.pork.ps5NativePackageBuilderStatus(); $('s-ps5-native-package-status').textContent = status.available ? settingsT('ps5NativePackages.settings.ready', 'Pinned bridge ready') : settingsT('ps5NativePackages.settings.blocked', 'Research bridge not configured'); }); $('btn-ps5-native-package-output').addEventListener('click', async () => { const folder = await window.pork.selectFolder(); if (!folder) return; await window.pork.setSettings({ ps5NativePackageOutputPath: folder }); $('s-ps5-native-package-output').value = folder; showToast(settingsT('ps5NativePackages.settings.restart', 'Restart Chimera to activate the native package vault.'), 'info'); }); $('s-conv-psn-enabled').addEventListener('change', async function() { $('conv-psn-detail').hidden = !this.checked; await window.pork.setSettings({ convPsNotifyEnabled: this.checked }); setStatus('Settings saved', 'ok'); }); [ ['s-conv-psn-queued', 'convPsNotifyOnGameQueued'], ['s-conv-psn-batch', 'convPsNotifyOnBatchQueued'], ['s-conv-psn-copy', 'convPsNotifyOnCopyStart'], ['s-conv-psn-convert', 'convPsNotifyOnConvertStart'], ['s-conv-psn-done', 'convPsNotifyOnJobDone'], ].forEach(([id, key]) => { $(id).addEventListener('change', function() { window.pork.setSettings({ [key]: this.checked }); }); }); $('settings-ftp-presets').addEventListener('click', e => { const btn = e.target.closest('[data-add-port]'); if (!btn) return; const port = btn.dataset.addPort; const current = $('s-ports').value.split(',').map(p => p.trim()).filter(Boolean); if (!current.includes(port)) { current.push(port); $('s-ports').value = current.join(', '); commitSettingsSave(); } }); { const hkMap = { 'hk-mute': 'mute', 'hk-gif': 'gif', 'hk-video': 'video', 'hk-fullscreen': 'fullscreen', 'hk-popout': 'popout' }; Object.entries(hkMap).forEach(([id, action]) => { const input = $(id); if (!input) return; input.addEventListener('focus', () => { input.dataset.prev = input.value; input.value = ''; input.placeholder = 'press a key…'; input.classList.add('capturing'); }); input.addEventListener('blur', () => { if (!input.value) input.value = input.dataset.prev || ''; input.placeholder = '—'; input.classList.remove('capturing'); }); input.addEventListener('keydown', async e => { e.preventDefault(); if (e.key === 'Escape') { input.value = input.dataset.prev || ''; input.blur(); return; } const key = e.key.length === 1 ? e.key.toLowerCase() : e.key; input.value = key; _svHotkeys[action] = key; input.blur(); await window.pork.setSettings({ svHotkeys: { ..._svHotkeys } }); setStatus('Hotkey saved', 'ok'); }); }); } function renderRemoteGamePaths() { const cont = $('settings-remote-paths-list'); if (!remoteGamePaths.length) { cont.innerHTML = '

No paths configured. Scan will use standard PS5 paths.

'; return; } cont.innerHTML = remoteGamePaths.map((p, i) => `
${escHtml(p.label)} ${escHtml(p.path)}
`).join(''); } async function saveRemoteGamePaths() { await window.pork.setSettings({ remoteGamePaths }); setStatus('Settings saved', 'ok'); } $('btn-add-remote-path').addEventListener('click', async () => { const label = $('new-remote-label').value.trim(); const path = $('new-remote-path').value.trim(); if (!path) { setStatus('Enter a remote path', 'error'); return; } remoteGamePaths = [...remoteGamePaths, { label: label || path, path }]; await saveRemoteGamePaths(); renderRemoteGamePaths(); $('new-remote-label').value = ''; $('new-remote-path').value = ''; }); $('settings-remote-paths-list').addEventListener('click', async e => { const row = e.target.closest('[data-idx]'); if (!row || !e.target.closest('.btn-remote-path-remove')) return; const idx = Number(row.dataset.idx); const ok = await showConfirm(`Remove "${escHtml(remoteGamePaths[idx]?.label)}" from remote game paths?`); if (!ok) return; remoteGamePaths = remoteGamePaths.filter((_, i) => i !== idx); await saveRemoteGamePaths(); renderRemoteGamePaths(); }); async function refreshWebuiStatus() { try { const status = await window.pork.webuiStatus(); const badge = $('webui-status-badge'); const urlRow = $('webui-url-row'); const urlEl = $('webui-url'); const toggle = $('s-webui-enabled'); if (badge) badge.hidden = !status.running; if (toggle) toggle.checked = status.running; if (urlRow && urlEl) { if (status.running) { const port = status.port; const hosts = Array.isArray(status.hosts) && status.hosts.length ? status.hosts : ['localhost']; const primary = (window.pork.isWebMode && location.hostname) ? location.hostname : hosts[0]; const primaryUrl = status.publicOrigin || (window.pork.isWebMode ? location.origin : `http://${primary}:${port}`); urlEl.textContent = primaryUrl; urlRow.hidden = false; const openBtn = $('btn-webui-open'); if (openBtn) openBtn.onclick = () => window.pork.openShell ? window.pork.openShell(primaryUrl) : window.open(primaryUrl, '_blank'); } else { urlRow.hidden = true; } } } catch (_) {} } $('s-webui-enabled').addEventListener('change', async function () { const btn = $('btn-webui-apply'); if (btn) btn.disabled = true; try { if (this.checked) { const port = Number($('s-webui-port').value) || 6967; await window.pork.setSettings({ webUiEnabled: true, webUiPort: port }); const res = await window.pork.webuiStart(); if (!res.ok) { setStatus(`Web UI failed: ${res.error}`, 'error'); this.checked = false; } else setStatus('Web UI server started', 'ok'); } else { await window.pork.setSettings({ webUiEnabled: false }); const res = await window.pork.webuiStop(); if (res.ok) setStatus('Web UI server stopped', 'ok'); } } catch (e) { setStatus(`Web UI error: ${e.message}`, 'error'); } finally { if (btn) btn.disabled = false; await refreshWebuiStatus(); } }); $('btn-webui-apply').addEventListener('click', async () => { const port = Number($('s-webui-port').value) || 6967; await window.pork.setSettings({ webUiPort: port }); if ($('s-webui-enabled').checked) { await window.pork.webuiStop(); const res = await window.pork.webuiStart(); if (res.ok) setStatus(`Web UI restarted on port ${port}`, 'ok'); else setStatus(`Web UI restart failed: ${res.error}`, 'error'); } else { setStatus('Port saved', 'ok'); } await refreshWebuiStatus(); });