862 lines
42 KiB
JavaScript
862 lines
42 KiB
JavaScript
|
|
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<b?6:0))/6; break;
|
|
case g: h = ((b-r)/d + 2)/6; break;
|
|
case b: h = ((r-g)/d + 4)/6; break;
|
|
}
|
|
}
|
|
l = Math.max(0, l - amount);
|
|
if (satCap !== null) s = Math.min(s, satCap);
|
|
const hue2rgb = (p, q, t) => {
|
|
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 => `<option value="${escHtml(profile.id)}">${escHtml(profile.label)} · ${escHtml(profile.host || settingsT('settings.status.hostUnknown', 'host unknown'))}${profile.active ? ` · ${settingsT('settings.status.active', 'active')}` : ''}</option>`).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 => `<div class="ecosystem-service-row" data-worker-id="${escHtml(worker.id)}"><div><strong>${escHtml(worker.label)}</strong><span>${escHtml(worker.endpoint)} · ${worker.paired ? settingsT('settings.status.paired', 'paired') : settingsT('settings.status.notPaired', 'not paired')}</span></div><div class="operations-inline-actions"><button class="btn btn-sm companion-worker-test">Capabilities</button><button class="btn btn-sm companion-worker-pair">${worker.paired ? settingsT('settings.companion.rotateKey', 'Rotate key') : 'Pairing key'}</button><button class="btn btn-sm btn-danger companion-worker-remove">${settingsT('btn.remove', 'Remove')}</button></div></div>`).join('') : `<p class="hint">${settingsT('settings.companion.empty', 'No companion workers configured.')}</p>`;
|
|
}
|
|
|
|
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 = `<strong>${escHtml(settingsT('settings.passkeys.preparedTitle', 'Passkeys: safely prepared, not active yet'))}</strong><p>${escHtml(settingsT('settings.passkeys.runtimeUnavailable', 'This runtime does not expose the required enrollment service.'))}</p>`;
|
|
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 = `<strong>Passkeys: ${status.passkeys?.available ? settingsT('settings.status.available', 'available') : settingsT('settings.passkeys.prepared', 'safely prepared, not active yet')}</strong><p>${escHtml(status.passkeys?.reason || settingsT('settings.status.none', 'No status.'))}</p>`;
|
|
}
|
|
|
|
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 => `<label class="ecosystem-check"><input type="checkbox" data-notification-rule="${escHtml(rule)}" ${status.rules?.includes(rule) ? 'checked' : ''}> ${escHtml(settingsT(`settings.notifications.rule.${rule}`, rule.replace(/-/g, ' ')))}</label>`).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 = `<div class="ecosystem-update-list">${(packet.profiles || []).map(profile => `<div class="ecosystem-service-row"><div><strong>${profile.active ? '● ' : ''}${escHtml(profile.label)}</strong><span>${escHtml(profile.host)} · firmware ${escHtml(profile.firmware)} · ${escHtml(profile.hen)} ${escHtml(profile.henVersion)}</span></div><span class="hint">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')}</span></div>`).join('')}</div>`;
|
|
}
|
|
|
|
$('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 = `
|
|
<div class="ecosystem-metrics">
|
|
<div class="operations-metric"><strong>${sources.roots.length}</strong><span>roots inspected</span></div>
|
|
<div class="operations-metric"><strong>${sources.recommendations.length}</strong><span>high-confidence suggestions</span></div>
|
|
<div class="operations-metric"><strong>${protection.saveVault.eligible ? 'Ready' : 'Setup'}</strong><span>Save Vault</span></div>
|
|
</div>
|
|
${(sources.roots || []).map(item => `<div class="ecosystem-service-row"><div><strong>${escHtml(item.root)}</strong><span>${escHtml(item.classification)} · ${escHtml(item.confidence)} confidence · ${item.writable ? 'writable' : 'read-only/unavailable'}</span></div><span class="badge">${escHtml(item.exists ? 'inspected' : item.error || 'missing')}</span></div>`).join('')}
|
|
<p class="hint">No configuration changes were applied. Enable Save Vault, replication and restore drills from Settings → Automation after reviewing their targets.</p>`;
|
|
} 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 `<option value="${escHtml(l.code)}"${l.code === selectedCode ? ' selected' : ''}>${label}</option>`;
|
|
}).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 = '<p class="hint" style="margin:6px 0">No folders added yet. Click “+ Add Folder…” to get started.</p>';
|
|
return;
|
|
}
|
|
cont.innerHTML = localBackupPaths.map((p, i) => `
|
|
<div class="remote-path-row" data-backup-idx="${i}">
|
|
<span class="remote-path-value" title="${escHtml(p)}" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(p)}</span>
|
|
<button class="btn btn-sm btn-backup-path-remove" style="margin-left:auto;flex-shrink:0">✕</button>
|
|
</div>
|
|
`).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 = '<p class="hint" style="margin:6px 0">No paths configured. Scan will use standard PS5 paths.</p>';
|
|
return;
|
|
}
|
|
cont.innerHTML = remoteGamePaths.map((p, i) => `
|
|
<div class="remote-path-row" data-idx="${i}">
|
|
<span class="remote-path-label">${escHtml(p.label)}</span>
|
|
<span class="remote-path-value" title="${escHtml(p.path)}">${escHtml(p.path)}</span>
|
|
<button class="btn btn-sm btn-remote-path-remove" style="margin-left:auto">✕</button>
|
|
</div>
|
|
`).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();
|
|
});
|
|
|