214 lines
11 KiB
JavaScript
214 lines
11 KiB
JavaScript
// ELF Arsenal native Chimera client. The upstream WebUI remains available in a
|
|
// separate browser tab, but is no longer embedded: its opaque iframe errors
|
|
// cannot be diagnosed and its storage assumptions conflict with sandboxing.
|
|
|
|
let _eaPath = '/';
|
|
let _eaConnected = false;
|
|
|
|
function eaFriendlyError(value) {
|
|
const message = String(value || '').trim();
|
|
if (/Configureer eerst een geldig PS5-adres/i.test(message)) return 'Configure a PS5 host in Settings first.';
|
|
if (/ELF Arsenal-payload.*gestart/i.test(message)) return 'Start the ELF Arsenal payload explicitly, then retry.';
|
|
return message || 'ELF Arsenal is unavailable on port 6969.';
|
|
}
|
|
|
|
function eaBytes(value) {
|
|
const bytes = Number(value || 0);
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
|
}
|
|
|
|
function eaJoin(base, name) {
|
|
const cleanName = String(name || '').replace(/^\/+|\/+$/g, '');
|
|
return base === '/' ? `/${cleanName}` : `${String(base).replace(/\/+$/, '')}/${cleanName}`;
|
|
}
|
|
|
|
function eaParent(value) {
|
|
const parts = String(value || '/').split('/').filter(Boolean);
|
|
parts.pop();
|
|
return parts.length ? `/${parts.join('/')}` : '/';
|
|
}
|
|
|
|
function eaSetStatus(message, type = '') {
|
|
const status = document.getElementById('ea-embed-status');
|
|
if (!status) return;
|
|
status.textContent = message;
|
|
status.className = `ea-native-status ${type}`;
|
|
}
|
|
|
|
function eaRenderList(packet) {
|
|
const target = document.getElementById('ea-native-list');
|
|
const pathLabel = document.getElementById('ea-native-path');
|
|
if (!target || !pathLabel) return;
|
|
pathLabel.innerHTML = `<button class="btn btn-xs ea-native-parent" ${_eaPath === '/' ? 'disabled' : ''} data-path="${escHtml(eaParent(_eaPath))}">← Up</button><code>${escHtml(_eaPath)}</code>`;
|
|
const entries = (packet.entries || []).slice().sort((left, right) =>
|
|
Number(Boolean(right.is_dir)) - Number(Boolean(left.is_dir)) || String(left.name).localeCompare(String(right.name)));
|
|
target.innerHTML = entries.length ? entries.map(entry => {
|
|
const remotePath = eaJoin(_eaPath, entry.name);
|
|
const isElf = !entry.is_dir && /\.elf$/i.test(entry.name);
|
|
return `<article class="ea-native-row" data-path="${escHtml(remotePath)}" data-name="${escHtml(entry.name)}" data-dir="${entry.is_dir ? 'true' : 'false'}">
|
|
<span class="ea-native-icon" aria-hidden="true">${entry.is_dir ? '📁' : isElf ? '◆' : '•'}</span>
|
|
<div><strong>${escHtml(entry.name)}</strong><span>${entry.is_dir ? 'Directory' : eaBytes(entry.size)}${entry.mode ? ` · ${escHtml(entry.mode)}` : ''}</span></div>
|
|
<div class="ecosystem-inline-actions">
|
|
${entry.is_dir ? '<button class="btn btn-sm ea-native-open">Open</button>' : `<button class="btn btn-sm ea-native-download">Download</button>${isElf ? '<button class="btn btn-sm btn-accent ea-native-launch">Launch ELF</button>' : ''}`}
|
|
</div>
|
|
</article>`;
|
|
}).join('') : '<div class="operations-empty"><strong>This directory is empty</strong><span>Choose another safe root or upload a file explicitly.</span></div>';
|
|
const lock = document.getElementById('ea-native-lock');
|
|
if (lock) {
|
|
lock.textContent = packet.safety_lock ? 'Safety lock enabled' : 'Safety lock not reported';
|
|
lock.className = `operations-status ${packet.safety_lock ? 'operations-status-compatible' : 'operations-status-unknown'}`;
|
|
}
|
|
}
|
|
|
|
async function eaLoadDirectory(path = _eaPath) {
|
|
_eaPath = String(path || '/');
|
|
const target = document.getElementById('ea-native-list');
|
|
if (target) target.innerHTML = '<p class="hint">Loading directory…</p>';
|
|
try {
|
|
const [packet, job] = await Promise.all([
|
|
window.pork.eaList(_eaPath),
|
|
window.pork.eaJobStatus().catch(() => ({ busy: false })),
|
|
]);
|
|
eaRenderList(packet);
|
|
const jobTarget = document.getElementById('ea-native-job');
|
|
if (jobTarget) jobTarget.textContent = job.busy
|
|
? `File operation active${job.progress != null ? ` · ${job.progress}%` : ''}`
|
|
: 'No active file operation';
|
|
} catch (error) {
|
|
if (target) target.innerHTML = `<div class="operations-empty"><strong>Directory unavailable</strong><span>${escHtml(error.message)}</span></div>`;
|
|
eaSetStatus(`File API error: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
async function eaReload() {
|
|
eaSetStatus('Checking ELF Arsenal…');
|
|
try {
|
|
const result = await window.pork.eaEmbedUrl();
|
|
const open = document.getElementById('ea-embed-open');
|
|
_eaConnected = result.connected === true;
|
|
if (!_eaConnected) {
|
|
if (open) { open.disabled = true; delete open.dataset.url; }
|
|
eaSetStatus(eaFriendlyError(result.error), 'error');
|
|
const list = document.getElementById('ea-native-list');
|
|
if (list) list.innerHTML = '<div class="operations-empty"><strong>ELF Arsenal is offline</strong><span>Start it explicitly through Payload Manager, then retry.</span></div>';
|
|
return;
|
|
}
|
|
if (open) {
|
|
open.disabled = false;
|
|
open.dataset.url = result.directUrl || result.url;
|
|
}
|
|
eaSetStatus(result.version ? `Online · ${result.version}` : 'Online', 'ok');
|
|
document.getElementById('ea-native-version').textContent = result.version || 'Unknown';
|
|
await eaLoadDirectory(_eaPath);
|
|
} catch (error) {
|
|
_eaConnected = false;
|
|
eaSetStatus(`Connection error: ${eaFriendlyError(error.message)}`, 'error');
|
|
}
|
|
}
|
|
|
|
function buildElfArsenalEmbed() {
|
|
const root = document.getElementById('page-elf-arsenal');
|
|
if (!root) return;
|
|
root.innerHTML = `
|
|
<div class="page-header">
|
|
<div>
|
|
<h1>ELF Arsenal</h1>
|
|
<p class="page-subtitle">Native Chimera file browser and launcher for the ELF Arsenal service on your PS5.</p>
|
|
</div>
|
|
<div class="header-tools">
|
|
<span id="ea-embed-status" class="ea-native-status">Checking connection…</span>
|
|
<button id="ea-embed-start" class="btn btn-accent">Start via Payload Manager</button>
|
|
<button id="ea-embed-open" class="btn" disabled>Open original WebUI</button>
|
|
<button id="ea-embed-reload" class="btn">Refresh</button>
|
|
</div>
|
|
</div>
|
|
<div class="ecosystem-workflow-strip ea-workflow" aria-label="ELF Arsenal workflow">
|
|
<span><b>1</b><strong>Start service</strong><small>Payload Manager, explicit</small></span>
|
|
<span><b>2</b><strong>Browse</strong><small>Choose a reviewed ELF</small></span>
|
|
<span><b>3</b><strong>Inspect</strong><small>Path and operation</small></span>
|
|
<span><b>4</b><strong>Launch</strong><small>Exact confirmation</small></span>
|
|
</div>
|
|
<div class="ea-native-summary">
|
|
<div class="operations-metric"><strong id="ea-native-version">—</strong><span>Version</span></div>
|
|
<div class="operations-metric"><strong id="ea-native-job">No active job</strong><span>File operation</span></div>
|
|
<div class="operations-metric"><strong id="ea-native-lock" class="operations-status operations-status-unknown">Unknown</strong><span>Upstream safety lock</span></div>
|
|
</div>
|
|
<div class="card ea-native-card">
|
|
<div class="card-header ecosystem-card-header">
|
|
<div><h3>PS5 files</h3><p class="hint">Browse with Chimera's authenticated API. The original upstream WebUI opens separately and can no longer crash this page.</p></div>
|
|
<div class="ecosystem-inline-actions">
|
|
<button class="btn btn-sm ea-native-root" data-path="/">Root</button>
|
|
<button class="btn btn-sm ea-native-root" data-path="/data">Data</button>
|
|
<button class="btn btn-sm ea-native-root" data-path="/mnt">USB / mounts</button>
|
|
<button id="ea-native-upload" class="btn btn-sm btn-accent">Upload here</button>
|
|
</div>
|
|
</div>
|
|
<div id="ea-native-path" class="ea-native-path"></div>
|
|
<div id="ea-native-list" class="ea-native-list"><p class="hint">Waiting for ELF Arsenal…</p></div>
|
|
</div>
|
|
<p class="hint">FTP alone is not enough: ELF Arsenal must be running on port 6969. Launch and upload actions always require an explicit confirmation.</p>`;
|
|
|
|
document.getElementById('ea-embed-open').addEventListener('click', () => {
|
|
const url = document.getElementById('ea-embed-open')?.dataset.url;
|
|
if (url) window.pork.openShell(url);
|
|
});
|
|
document.getElementById('ea-embed-start').addEventListener('click', async (event) => {
|
|
const button = event.currentTarget;
|
|
button.disabled = true;
|
|
eaSetStatus('Starting ELF Arsenal explicitly through Payload Manager…', 'warning');
|
|
try {
|
|
await window.pork.eaStart();
|
|
showToast('ELF Arsenal started. Rechecking the service.', 'ok');
|
|
await eaReload();
|
|
} catch (error) {
|
|
eaSetStatus(`Start failed: ${error.message}`, 'error');
|
|
showToast(`ELF Arsenal start failed: ${error.message}`, 'error');
|
|
} finally {
|
|
button.disabled = false;
|
|
}
|
|
});
|
|
document.getElementById('ea-embed-reload').addEventListener('click', eaReload);
|
|
document.getElementById('ea-native-path').addEventListener('click', event => {
|
|
const button = event.target.closest('.ea-native-parent');
|
|
if (button) eaLoadDirectory(button.dataset.path);
|
|
});
|
|
root.querySelectorAll('.ea-native-root').forEach(button => button.addEventListener('click', () => eaLoadDirectory(button.dataset.path)));
|
|
document.getElementById('ea-native-list').addEventListener('click', async event => {
|
|
const row = event.target.closest('.ea-native-row');
|
|
if (!row) return;
|
|
const button = event.target.closest('button');
|
|
try {
|
|
if (button?.classList.contains('ea-native-open')) await eaLoadDirectory(row.dataset.path);
|
|
if (button?.classList.contains('ea-native-download')) await window.pork.eaDownload(row.dataset.path, row.dataset.name);
|
|
if (button?.classList.contains('ea-native-launch')) {
|
|
const confirmation = await showExactConfirmation(`Launch ${row.dataset.path} through ELF Arsenal?`, 'LAUNCH ELF');
|
|
if (!confirmation) return;
|
|
await window.pork.eaLaunch(row.dataset.path, [], false);
|
|
showToast(`${row.dataset.name} launch request sent.`, 'ok');
|
|
}
|
|
} catch (error) { showToast(error.message, 'error', 10000); }
|
|
});
|
|
document.getElementById('ea-native-upload').addEventListener('click', async () => {
|
|
if (!_eaConnected) return showToast('Start ELF Arsenal first.', 'error');
|
|
const confirmation = await showExactConfirmation(`Upload one selected file to ${_eaPath}?`, 'UPLOAD FILE');
|
|
if (!confirmation) return;
|
|
try {
|
|
const uploadRoot = _eaPath === '/' ? '/' : `${_eaPath.replace(/\/+$/, '')}/`;
|
|
const result = await window.pork.eaUpload(null, uploadRoot);
|
|
if (result) {
|
|
showToast(`Uploaded ${eaBytes(result.bytes)} to ${result.remotePath}.`, 'ok');
|
|
await eaLoadDirectory(_eaPath);
|
|
}
|
|
} catch (error) { showToast(error.message, 'error', 10000); }
|
|
});
|
|
eaReload();
|
|
}
|
|
|
|
async function loadElfArsenal() {
|
|
if (!document.getElementById('ea-native-list')) buildElfArsenalEmbed();
|
|
else await eaReload();
|
|
}
|