477 lines
22 KiB
JavaScript
477 lines
22 KiB
JavaScript
// ── PS5 Autoloader ─────────────────────────────────────────────────────────────
|
|
let _alSequence = []; // { type: 'payload'|'delay', filename?: string, ms?: number }
|
|
let _alHeader = ''; // comment block preserved from original file
|
|
|
|
const _AL_DEFAULT_HEADER =
|
|
`#
|
|
# ps5_autoloader - autoload.txt
|
|
# Edited with Chimera
|
|
# -----------------------------------------------------------------------------------------
|
|
# Usage:
|
|
# - Put one filename per line (e.g., payload.elf or script.js).
|
|
# - Supported payload types: .elf, .bin, .js
|
|
# - Lines starting with '!' are sleep commands (example: !1000 sleeps for 1000 ms).
|
|
# -----------------------------------------------------------------------------------------
|
|
`;
|
|
|
|
function _alParse(content) {
|
|
if (!content) return { header: _AL_DEFAULT_HEADER, entries: [] };
|
|
const lines = content.split(/\r?\n/);
|
|
const headerLines = [];
|
|
const entries = [];
|
|
let headerDone = false;
|
|
for (const line of lines) {
|
|
const t = line.trim();
|
|
if (!headerDone) {
|
|
if (t.startsWith('#') || t === '') { headerLines.push(line); continue; }
|
|
headerDone = true;
|
|
}
|
|
if (t === '' || t.startsWith('#')) continue;
|
|
if (t.startsWith('!')) {
|
|
const ms = parseInt(t.slice(1), 10);
|
|
if (!isNaN(ms)) entries.push({ type: 'delay', ms });
|
|
} else {
|
|
entries.push({ type: 'payload', filename: t });
|
|
}
|
|
}
|
|
const header = headerLines.join('\n').trimEnd() + '\n\n';
|
|
return { header: header || _AL_DEFAULT_HEADER, entries };
|
|
}
|
|
|
|
function _alSerialize(header, entries) {
|
|
const lines = [header.trimEnd(), ''];
|
|
for (const e of entries)
|
|
lines.push(e.type === 'delay' ? `!${e.ms}` : e.filename);
|
|
return lines.join('\n') + '\n';
|
|
}
|
|
|
|
let _alDragIdx = -1;
|
|
|
|
function _alSecsFmt(ms) {
|
|
const s = ms / 1000;
|
|
return s === Math.floor(s) ? s.toFixed(1) + 's' : s.toFixed(2) + 's';
|
|
}
|
|
|
|
function _alBuildRow(entry, i) {
|
|
const del = `<button class="al-del" data-al="remove" data-idx="${i}" title="Remove">✕</button>`;
|
|
if (entry.type === 'delay') {
|
|
const capped = Math.min(entry.ms, 10000);
|
|
const presets = [250, 500, 1000, 2000, 3000, 5000].map(p => {
|
|
const lbl = p < 1000 ? `${p}ms` : `${p / 1000}s`;
|
|
const active = entry.ms === p ? ' al-preset-active' : '';
|
|
return `<button class="al-preset${active}" data-al="preset" data-idx="${i}" data-ms="${p}">${lbl}</button>`;
|
|
}).join('');
|
|
return `<div class="al-row al-delay" draggable="true" data-idx="${i}">
|
|
<span class="al-grip" title="Drag to reorder">⋮⋮</span>
|
|
<span class="al-delay-clock">⏱</span>
|
|
<div class="al-delay-body">
|
|
<div class="al-delay-top">
|
|
<span class="al-delay-lbl">Wait</span>
|
|
<input class="al-delay-num" type="number" min="0" max="60000" step="50"
|
|
value="${entry.ms}" data-al="delay-num" data-idx="${i}">
|
|
<span class="al-delay-ms">ms</span>
|
|
<span class="al-delay-secs" data-idx="${i}">(${_alSecsFmt(entry.ms)})</span>
|
|
<div class="al-presets">${presets}</div>
|
|
</div>
|
|
<input class="al-slider" type="range" min="0" max="10000" step="50"
|
|
value="${capped}" data-al="delay-slider" data-idx="${i}">
|
|
</div>
|
|
${del}
|
|
</div>`;
|
|
}
|
|
const ext = (entry.filename.split('.').pop() || '').toLowerCase();
|
|
const extClass = { elf: 'al-ext-elf', bin: 'al-ext-bin', js: 'al-ext-js' }[ext] || '';
|
|
return `<div class="al-row al-payload" draggable="true" data-idx="${i}">
|
|
<span class="al-grip" title="Drag to reorder">⋮⋮</span>
|
|
<span class="al-ext ${extClass}">${escHtml(ext || '?')}</span>
|
|
<span class="al-name" title="${escHtml(entry.filename)}">${escHtml(entry.filename)}</span>
|
|
<button class="al-ins-delay" data-al="ins-delay" data-idx="${i}" title="Insert delay after this payload">+⏱</button>
|
|
${del}
|
|
</div>`;
|
|
}
|
|
|
|
function _alRenderSequence() {
|
|
const seq = $('autoloader-sequence');
|
|
if (!seq) return;
|
|
if (!_alSequence.length) {
|
|
seq.innerHTML = `<div class="al-empty">
|
|
<div class="al-empty-icon">📄</div>
|
|
<div>No entries yet.</div>
|
|
<div class="al-empty-hint">Click a file on the left to add it to the sequence.</div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
seq.innerHTML = _alSequence.map(_alBuildRow).join('');
|
|
seq.querySelectorAll('.al-row[draggable]').forEach(row => {
|
|
row.addEventListener('dragstart', e => {
|
|
_alDragIdx = parseInt(row.dataset.idx, 10);
|
|
row.classList.add('al-dragging');
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
});
|
|
row.addEventListener('dragend', () => {
|
|
row.classList.remove('al-dragging');
|
|
seq.querySelectorAll('.al-drag-over').forEach(r => r.classList.remove('al-drag-over'));
|
|
});
|
|
row.addEventListener('dragover', e => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
seq.querySelectorAll('.al-drag-over').forEach(r => r.classList.remove('al-drag-over'));
|
|
if (parseInt(row.dataset.idx, 10) !== _alDragIdx) row.classList.add('al-drag-over');
|
|
});
|
|
row.addEventListener('drop', e => {
|
|
e.preventDefault();
|
|
let toIdx = parseInt(row.dataset.idx, 10);
|
|
if (_alDragIdx === -1 || _alDragIdx === toIdx) return;
|
|
const [item] = _alSequence.splice(_alDragIdx, 1);
|
|
if (_alDragIdx < toIdx) toIdx--;
|
|
_alSequence.splice(toIdx, 0, item);
|
|
_alDragIdx = -1;
|
|
_alRenderSequence();
|
|
$('btn-autoloader-save').disabled = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
function _alRenderFiles(files) {
|
|
const list = $('autoloader-file-list');
|
|
if (!list) return;
|
|
const payloads = files.filter(f => !f.isDir && /\.(elf|bin|js)$/i.test(f.name));
|
|
if (!payloads.length) {
|
|
list.innerHTML = '<p class="hint autoloader-empty-hint">No .elf / .bin / .js files found in /data/ps5_autoloader.</p>';
|
|
return;
|
|
}
|
|
list.innerHTML = payloads.map(f => {
|
|
const ext = (f.name.split('.').pop() || '').toLowerCase();
|
|
const extClass = { elf: 'al-ext-elf', bin: 'al-ext-bin', js: 'al-ext-js' }[ext] || '';
|
|
return `<div class="al-file-card" data-al-add="${escHtml(f.name)}" title="Click to add to sequence">
|
|
<span class="al-ext ${extClass}">${escHtml(ext)}</span>
|
|
<span class="al-file-name">${escHtml(f.name)}</span>
|
|
<button class="btn-al-save-local" data-filename="${escHtml(f.name)}" title="Save to local payloads folder">⇣</button>
|
|
<span class="al-file-add">+</span>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
async function loadAutoloader() {
|
|
const btn = $('btn-autoloader-refresh');
|
|
const status = $('autoloader-status');
|
|
btn.disabled = true;
|
|
status.textContent = 'Loading…';
|
|
try {
|
|
const files = await window.pork.autoloaderList();
|
|
const content = await window.pork.autoloaderRead();
|
|
const { header, entries } = _alParse(content);
|
|
_alHeader = header;
|
|
_alSequence = entries;
|
|
_alRenderFiles(files);
|
|
_alRenderSequence();
|
|
$('btn-autoloader-save').disabled = false;
|
|
status.textContent = content
|
|
? `Loaded \u2014 ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'}`
|
|
: 'autoload.txt not found \u2014 sequence is empty';
|
|
} catch (e) {
|
|
status.textContent = `Error: ${e.message}`;
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function saveAutoloader() {
|
|
const btn = $('btn-autoloader-save');
|
|
const status = $('autoloader-status');
|
|
btn.disabled = true;
|
|
status.textContent = 'Saving\u2026';
|
|
try {
|
|
const content = _alSerialize(_alHeader, _alSequence);
|
|
await window.pork.autoloaderSave(content);
|
|
status.textContent = 'Saved to PS5 \u2713';
|
|
} catch (e) {
|
|
status.textContent = `Error: ${e.message}`;
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
$('btn-autoloader-refresh').addEventListener('click', loadAutoloader);
|
|
$('btn-autoloader-save').addEventListener('click', saveAutoloader);
|
|
|
|
const _persistButton = $('btn-pldmgr-persist-repair');
|
|
if (_persistButton && window.pork.payloadManagerAutoloadRepairPlan) {
|
|
$('pldmgr-persistence-panel').hidden = false;
|
|
_persistButton.addEventListener('click', async () => {
|
|
const status = $('pldmgr-persistence-status');
|
|
_persistButton.disabled = true;
|
|
status.textContent = window.t('payload.persistence.preflight');
|
|
try {
|
|
const plan = await window.pork.payloadManagerAutoloadRepairPlan();
|
|
if (plan.blocked) {
|
|
throw new Error(window.tf('payload.persistence.blocked', 'Other boot entries found: {entries}. Chimera will not remove them.', { entries: plan.unsupportedEntries.join(', ') }));
|
|
}
|
|
const confirmation = await showExactConfirmation(
|
|
window.t('payload.persistence.confirm'), plan.confirmation
|
|
);
|
|
if (!confirmation) return void (status.textContent = window.t('payload.persistence.cancelled'));
|
|
status.textContent = window.t('payload.persistence.applying');
|
|
const result = await window.pork.payloadManagerAutoloadRepairApply({ planDigest: plan.planDigest, confirmation });
|
|
status.textContent = window.tf('payload.persistence.success', 'Persistent v0.5.1 verified. Snapshot: {snapshot}. Reboot check required.', { snapshot: result.snapshotId });
|
|
await loadAutoloader();
|
|
} catch (error) {
|
|
status.textContent = window.tf('payload.persistence.error', 'Persistence repair failed: {message}', { message: error.message });
|
|
} finally {
|
|
_persistButton.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
const _rollbackButton = $('btn-pldmgr-persist-rollback');
|
|
if (_rollbackButton && window.pork.payloadManagerAutoloadRollbackPlan) {
|
|
_rollbackButton.addEventListener('click', async () => {
|
|
const status = $('pldmgr-persistence-status');
|
|
_rollbackButton.disabled = true;
|
|
status.textContent = window.t('payload.persistence.rollbackPreflight');
|
|
try {
|
|
const plan = await window.pork.payloadManagerAutoloadRollbackPlan();
|
|
if (plan.alreadyRestored) return void (status.textContent = window.t('payload.persistence.cancelled'));
|
|
const confirmation = await showExactConfirmation(window.t('payload.persistence.rollbackConfirm'), plan.confirmation);
|
|
if (!confirmation) return void (status.textContent = window.t('payload.persistence.cancelled'));
|
|
status.textContent = window.t('payload.persistence.rollbackApplying');
|
|
const result = await window.pork.payloadManagerAutoloadRollbackApply({ planDigest: plan.planDigest, confirmation });
|
|
status.textContent = window.tf('payload.persistence.rollbackSuccess', 'Original boot state restored. Safety snapshot: {snapshot}. Reboot required.', { snapshot: result.safetySnapshotId });
|
|
await loadAutoloader();
|
|
} catch (error) {
|
|
status.textContent = window.tf('payload.persistence.error', 'Persistence repair failed: {message}', { message: error.message });
|
|
} finally { _rollbackButton.disabled = false; }
|
|
});
|
|
}
|
|
|
|
function _etaHenPersistenceAction({ buttonId, plan, apply, preflight, confirm, success, snapshot = 'snapshotId' }) {
|
|
const button = $(buttonId);
|
|
if (!button || !window.pork[plan]) return;
|
|
button.addEventListener('click', async () => {
|
|
const status = $('etahen-persistence-status');
|
|
button.disabled = true;
|
|
status.textContent = window.t(preflight);
|
|
try {
|
|
const packet = await window.pork[plan]();
|
|
const entries = [...(packet.unexpectedFiles || []), ...(packet.unsupportedEntries || [])];
|
|
if (packet.blocked) throw new Error(window.tf('payload.etahenPersistence.blocked', 'Other boot content found: {entries}.', { entries: entries.join(', ') }));
|
|
if (packet.alreadyRestored) return void (status.textContent = window.t('payload.persistence.cancelled'));
|
|
const confirmation = await showExactConfirmation(window.t(confirm), packet.confirmation);
|
|
if (!confirmation) return void (status.textContent = window.t('payload.persistence.cancelled'));
|
|
const result = await window.pork[apply]({ planDigest: packet.planDigest, confirmation });
|
|
status.textContent = window.tf(success, 'etaHEN 2.6B boot state updated. Snapshot: {snapshot}. Reboot required.', { snapshot: result[snapshot] });
|
|
await loadAutoloader();
|
|
} catch (error) { status.textContent = window.tf('payload.etahenPersistence.error', 'etaHEN persistence failed: {message}', { message: error.message }); }
|
|
finally { button.disabled = false; }
|
|
});
|
|
}
|
|
_etaHenPersistenceAction({ buttonId: 'btn-etahen-persist', plan: 'etaHenAutoloadPersistencePlan', apply: 'etaHenAutoloadPersistenceApply', preflight: 'payload.etahenPersistence.preflight', confirm: 'payload.etahenPersistence.confirm', success: 'payload.etahenPersistence.success' });
|
|
_etaHenPersistenceAction({ buttonId: 'btn-etahen-persist-rollback', plan: 'etaHenAutoloadPersistenceRollbackPlan', apply: 'etaHenAutoloadPersistenceRollback', preflight: 'payload.etahenPersistence.rollbackPreflight', confirm: 'payload.etahenPersistence.rollbackConfirm', success: 'payload.etahenPersistence.rollbackSuccess', snapshot: 'safetySnapshotId' });
|
|
|
|
function _fixedPayloadStart({ buttonId, api, exact, confirmKey, startingKey, startedKey }) {
|
|
const button = $(buttonId);
|
|
if (!button || !window.pork[api]) return;
|
|
button.addEventListener('click', async () => {
|
|
const status = $('etahen-persistence-status');
|
|
const confirmation = await showExactConfirmation(window.t(confirmKey), exact);
|
|
if (!confirmation) return void (status.textContent = window.t('payload.persistence.cancelled'));
|
|
button.disabled = true;
|
|
status.textContent = window.t(startingKey);
|
|
try {
|
|
const result = await window.pork[api]({ confirmation });
|
|
status.textContent = window.tf(startedKey, '', { bytes: result.bytes });
|
|
} catch (error) { status.textContent = window.tf('payload.etahenPersistence.error', '', { message: error.message }); }
|
|
finally { button.disabled = false; }
|
|
});
|
|
}
|
|
_fixedPayloadStart({ buttonId: 'btn-etahen-direct-start', api: 'etaHenDirectStart', exact: 'START DIRECT ETAHEN 2.6B', confirmKey: 'payload.etahenPersistence.startConfirm', startingKey: 'payload.etahenPersistence.starting', startedKey: 'payload.etahenPersistence.started' });
|
|
_fixedPayloadStart({ buttonId: 'btn-pldmgr-verified-start', api: 'payloadManagerVerifiedStart', exact: 'START VERIFIED PS5 PAYLOAD MANAGER 0.5.1', confirmKey: 'payload.persistence.startConfirm', startingKey: 'payload.persistence.starting', startedKey: 'payload.persistence.started' });
|
|
|
|
$('btn-autoloader-add-delay').addEventListener('click', () => {
|
|
_alSequence.push({ type: 'delay', ms: 1000 });
|
|
_alRenderSequence();
|
|
$('btn-autoloader-save').disabled = false;
|
|
});
|
|
|
|
$('autoloader-sequence').addEventListener('click', e => {
|
|
const btn = e.target.closest('[data-al]');
|
|
if (!btn) return;
|
|
const action = btn.dataset.al;
|
|
const idx = parseInt(btn.dataset.idx, 10);
|
|
if (action === 'remove') {
|
|
_alSequence.splice(idx, 1);
|
|
_alRenderSequence();
|
|
$('btn-autoloader-save').disabled = false;
|
|
} else if (action === 'preset') {
|
|
const ms = parseInt(btn.dataset.ms, 10);
|
|
_alSequence[idx].ms = ms;
|
|
_alRenderSequence();
|
|
$('btn-autoloader-save').disabled = false;
|
|
} else if (action === 'ins-delay') {
|
|
_alSequence.splice(idx + 1, 0, { type: 'delay', ms: 1000 });
|
|
_alRenderSequence();
|
|
$('btn-autoloader-save').disabled = false;
|
|
}
|
|
});
|
|
|
|
$('autoloader-sequence').addEventListener('input', e => {
|
|
const el = e.target;
|
|
const al = el.dataset.al;
|
|
if (!al) return;
|
|
const idx = parseInt(el.dataset.idx, 10);
|
|
if (al === 'delay-num' || al === 'delay-slider') {
|
|
const ms = Math.max(0, parseInt(el.value, 10) || 0);
|
|
_alSequence[idx].ms = ms;
|
|
const row = el.closest('.al-row');
|
|
const num = row.querySelector('[data-al="delay-num"]');
|
|
const slider = row.querySelector('[data-al="delay-slider"]');
|
|
const secs = row.querySelector('.al-delay-secs');
|
|
if (num) num.value = ms;
|
|
if (slider) slider.value = Math.min(ms, 10000);
|
|
if (secs) secs.textContent = `(${_alSecsFmt(ms)})`;
|
|
row.querySelectorAll('.al-preset').forEach(p =>
|
|
p.classList.toggle('al-preset-active', parseInt(p.dataset.ms, 10) === ms)
|
|
);
|
|
$('btn-autoloader-save').disabled = false;
|
|
}
|
|
});
|
|
|
|
// ── Autoloader Snapshots ─────────────────────────────────────────────────────────────────────────────
|
|
async function renderAutoloaderSnapshots() {
|
|
const list = $('snapshot-list');
|
|
if (!list) return;
|
|
list.innerHTML = '<p class="hint autoloader-empty-hint">Loading…</p>';
|
|
try {
|
|
const snaps = await window.pork.snapshotList();
|
|
if (!snaps.length) {
|
|
list.innerHTML = '<p class="hint autoloader-empty-hint">No snapshots yet. Take one above.</p>';
|
|
return;
|
|
}
|
|
list.innerHTML = snaps.map(s => {
|
|
const d = new Date(s.createdAt);
|
|
const dateStr = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
const fc = (s.files || []).length;
|
|
const alStr = s.hasAutoload ? ' + autoload.txt' : '';
|
|
return `<div class="snapshot-card">
|
|
<div class="snapshot-card-info">
|
|
<span class="snapshot-card-label">${escHtml(s.label || 'Snapshot')}</span>
|
|
<span class="snapshot-card-meta">${dateStr} \u2014 ${fc} payload${fc !== 1 ? 's' : ''}${alStr}</span>
|
|
</div>
|
|
<div class="snapshot-card-actions">
|
|
<button class="btn btn-teal btn-sm btn-snap-restore" data-snap-id="${escHtml(s.id)}">↑ Restore</button>
|
|
<button class="btn btn-sm btn-snap-export" data-snap-id="${escHtml(s.id)}" title="Export snapshot to .porksnap file">↘ Export</button>
|
|
<button class="btn btn-sm btn-snap-delete" data-snap-id="${escHtml(s.id)}" title="Delete snapshot">✕</button>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
list.querySelectorAll('.btn-snap-restore').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const id = btn.dataset.snapId;
|
|
const confirmation = await showExactConfirmation(
|
|
'Restore this snapshot to your PS5?\n\nAll payload files and autoload.txt will be uploaded back to /data/ps5_autoloader, overwriting whatever is currently there.',
|
|
'RESTORE AUTOLOADER'
|
|
);
|
|
if (!confirmation) return;
|
|
const orig = btn.textContent;
|
|
btn.disabled = true;
|
|
btn.textContent = '⋯ Uploading…';
|
|
try {
|
|
const res = await window.pork.snapshotRestore(id, confirmation);
|
|
btn.textContent = '✓ Restored';
|
|
const errNote = res.errors?.length ? ` (${res.errors.length} file${res.errors.length !== 1 ? 's' : ''} failed)` : '';
|
|
showToast(`Snapshot restored ✓ — ${res.fileCount} file${res.fileCount !== 1 ? 's' : ''} uploaded to PS5${errNote}`, res.errors?.length ? 'info' : 'ok');
|
|
setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 3000);
|
|
} catch (er) {
|
|
btn.textContent = orig;
|
|
btn.disabled = false;
|
|
showToast('Restore failed: ' + er.message, 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
list.querySelectorAll('.btn-snap-export').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const id = btn.dataset.snapId;
|
|
const orig = btn.textContent;
|
|
btn.disabled = true;
|
|
btn.textContent = '⋯';
|
|
try {
|
|
const res = await window.pork.snapshotExport(id);
|
|
if (res?.canceled) { btn.textContent = orig; btn.disabled = false; return; }
|
|
btn.textContent = '✓ Exported';
|
|
showToast('Snapshot exported ✓', 'ok');
|
|
setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 2500);
|
|
} catch (er) {
|
|
btn.textContent = orig;
|
|
btn.disabled = false;
|
|
showToast('Export failed: ' + er.message, 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
list.querySelectorAll('.btn-snap-delete').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const id = btn.dataset.snapId;
|
|
const confirmation = await showExactConfirmation('Delete this snapshot? This cannot be undone.', 'VERWIJDER');
|
|
if (!confirmation) return;
|
|
try {
|
|
await window.pork.snapshotDelete(id, confirmation);
|
|
await renderAutoloaderSnapshots();
|
|
} catch (er) {
|
|
showToast('Delete failed: ' + er.message, 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
} catch (e) {
|
|
list.innerHTML = `<p class="hint autoloader-empty-hint" style="color:var(--red)">Error: ${escHtml(e.message)}</p>`;
|
|
}
|
|
}
|
|
|
|
$('btn-snapshot-import').addEventListener('click', async () => {
|
|
const btn = $('btn-snapshot-import');
|
|
const status = $('snapshot-take-status');
|
|
const orig = btn.textContent;
|
|
btn.disabled = true;
|
|
btn.textContent = '⋯ Importing…';
|
|
status.textContent = '';
|
|
try {
|
|
let res = await window.pork.snapshotImport();
|
|
if (res?.canceled) { btn.textContent = orig; btn.disabled = false; return; }
|
|
if (res?.needsLabel) {
|
|
const label = await showPrompt('Enter a label for this snapshot:', 'Imported ZIP');
|
|
if (label === null) { btn.textContent = orig; btn.disabled = false; return; } // user cancelled
|
|
res = await window.pork.snapshotImport({ label: label.trim() || 'Imported ZIP', filePath: res.filePath });
|
|
if (res?.canceled) { btn.textContent = orig; btn.disabled = false; return; }
|
|
}
|
|
status.textContent = `Imported ✓ — "${res.meta.label}"`;
|
|
await renderAutoloaderSnapshots();
|
|
} catch (e) {
|
|
status.textContent = 'Import failed: ' + e.message;
|
|
} finally {
|
|
btn.textContent = orig;
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
|
|
$('btn-snapshot-take').addEventListener('click', async () => {
|
|
const btn = $('btn-snapshot-take');
|
|
const status = $('snapshot-take-status');
|
|
const label = $('snapshot-label').value.trim() || 'Snapshot';
|
|
btn.disabled = true;
|
|
status.textContent = 'Downloading from PS5\u2026';
|
|
try {
|
|
const { meta } = await window.pork.snapshotTake(label);
|
|
const fc = meta.files.length;
|
|
const alStr = meta.hasAutoload ? ' + autoload.txt' : '';
|
|
status.textContent = `Saved \u2713 \u2014 ${fc} payload${fc !== 1 ? 's' : ''}${alStr}`;
|
|
$('snapshot-label').value = '';
|
|
await renderAutoloaderSnapshots();
|
|
} catch (e) {
|
|
status.textContent = 'Error: ' + e.message;
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
|