392 lines
16 KiB
JavaScript
392 lines
16 KiB
JavaScript
// ── Manage panel ──────────────────────────────────────────────────────────────
|
|
let uploadInProgress = false;
|
|
let uploadLocalPath = null;
|
|
|
|
window.pork.on('ftp:upload:progress', info => {
|
|
const log = $('manage-upload-log');
|
|
if (!log) return;
|
|
// Directory upload shape has filesTotal; single-file shape has bytes/size
|
|
if (info.filesTotal != null) {
|
|
const pct = info.filesTotal > 0 ? Math.round((info.filesDone / info.filesTotal) * 100) : 0;
|
|
log.textContent = `Uploading… File ${info.filesDone}/${info.filesTotal} (${pct}%)${info.file ? ' ' + info.file : ''}`;
|
|
} else {
|
|
const done = info.bytes || 0;
|
|
const total = info.size || 0;
|
|
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
log.textContent = `Uploading… ${pct}% (${fmt(done)} / ${fmt(total)})`;
|
|
}
|
|
});
|
|
|
|
async function renderManagePanel(g, backups) {
|
|
// ── Backup file list (local only — backpork entries shown in Backpork Versions) ─
|
|
const list = $('manage-backup-list');
|
|
backups = backups.filter(b => b.source !== 'backpork');
|
|
$('mtab-manage-count').textContent = backups.length || '';
|
|
|
|
if (!backups.length) {
|
|
list.innerHTML = '<div class="prospero-placeholder">No backup files recorded.</div>';
|
|
} else {
|
|
list.innerHTML = backups.map(b => {
|
|
const filename = b.backup_path.replace(/\\/g, '/').split('/').pop();
|
|
return `
|
|
<div class="manage-backup-item" data-backup-id="${b.id}" data-game="${escHtml(g.game_id)}"
|
|
data-local="${escHtml(b.backup_path)}" data-folder="${escHtml(b.backup_path)}">
|
|
<div class="manage-backup-info">
|
|
<span class="manage-filename" title="${escHtml(b.backup_path)}">${escHtml(filename)}</span>
|
|
<span class="manage-size">${fmt(b.size)}</span>
|
|
<span class="pill pill-num">${escHtml(b.backup_type || 'dump')}</span>
|
|
</div>
|
|
<div class="manage-backup-actions">
|
|
<button class="btn btn-sm manage-btn-folder">Open Folder</button>
|
|
<button class="btn btn-sm btn-teal manage-btn-upload">Upload to PS5</button>
|
|
<button class="btn btn-sm btn-danger manage-btn-remove">Remove Record</button>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
// ── Upload section state ────────────────────────────────────────────────────
|
|
uploadLocalPath = null;
|
|
$('manage-remote-path').value = '';
|
|
$('manage-remote-path').readOnly = true;
|
|
$('btn-manage-upload').disabled = true;
|
|
$('manage-upload-log').hidden = true;
|
|
|
|
// ── Backed-up / Installed status buttons ─────────────────────────────────────
|
|
const toggleBackedBtn = $('btn-manage-toggle-backed');
|
|
if (g.backed_up) {
|
|
toggleBackedBtn.textContent = 'Clear Backed Up Status';
|
|
toggleBackedBtn.className = 'btn btn-danger';
|
|
} else {
|
|
toggleBackedBtn.textContent = 'Mark as Backed Up';
|
|
toggleBackedBtn.className = 'btn btn-teal';
|
|
}
|
|
const toggleInstBtn = $('btn-manage-toggle-installed');
|
|
if (g.installed) {
|
|
toggleInstBtn.textContent = 'Mark as Not Installed';
|
|
toggleInstBtn.className = 'btn btn-danger';
|
|
} else {
|
|
toggleInstBtn.textContent = 'Mark as Installed';
|
|
toggleInstBtn.className = 'btn';
|
|
}
|
|
|
|
// ── Install location display ──────────────────────────────────────────────────
|
|
renderManageInstallLocation(g);
|
|
|
|
// ── Backpork versions section ────────────────────────────────────────────────
|
|
await renderBackporkSection(g);
|
|
}
|
|
|
|
// ── Manage: install location display ─────────────────────────────────────────
|
|
|
|
function renderManageInstallLocation(g) {
|
|
const locRow = $('manage-install-location');
|
|
const noLocRow = $('manage-no-location');
|
|
const locPath = $('manage-loc-path');
|
|
const deleteBtn = $('btn-manage-delete-ps5');
|
|
const backupBtn = $('btn-manage-backup-ps5');
|
|
|
|
if (g?.ftp_path) {
|
|
locRow.hidden = false;
|
|
noLocRow.hidden = true;
|
|
locPath.textContent = g.ftp_path;
|
|
deleteBtn.disabled = !state.connected;
|
|
backupBtn.disabled = !state.connected;
|
|
} else {
|
|
locRow.hidden = true;
|
|
noLocRow.hidden = false;
|
|
locPath.textContent = '';
|
|
deleteBtn.disabled = true;
|
|
backupBtn.disabled = true;
|
|
}
|
|
}
|
|
|
|
$('btn-manage-forget-loc').addEventListener('click', async () => {
|
|
if (!state.currentGame) return;
|
|
const gameId = state.currentGame.game_id;
|
|
await window.pork.updateGame({ game_id: gameId, fields: { ftp_path: '' } });
|
|
state.currentGame = { ...state.currentGame, ftp_path: '' };
|
|
renderManageInstallLocation(state.currentGame);
|
|
setStatus('PS5 install location forgotten', 'ok');
|
|
});
|
|
|
|
$('btn-manage-backup-ps5').addEventListener('click', async () => {
|
|
if (!state.currentGame || !state.connected) return;
|
|
const { game_id, ftp_path } = state.currentGame;
|
|
if (!ftp_path) { setStatus('No PS5 location stored — run a scan first', 'error'); return; }
|
|
|
|
const btn = $('btn-manage-backup-ps5');
|
|
btn.disabled = true;
|
|
try {
|
|
await window.pork.ftpDownloadFolder(game_id);
|
|
setStatus('Backup queued — see Transfers for progress', 'ok');
|
|
} catch (e) {
|
|
setStatus(e.message, 'error');
|
|
} finally {
|
|
btn.disabled = !state.connected || !state.currentGame?.ftp_path;
|
|
}
|
|
});
|
|
|
|
$('btn-manage-delete-ps5').addEventListener('click', async () => {
|
|
if (!state.currentGame) return;
|
|
const g = state.currentGame;
|
|
if (!g.ftp_path) { setStatus('No PS5 location stored — cannot delete', 'error'); return; }
|
|
|
|
const confirmation = await showExactConfirmation(
|
|
`Delete "${g.title || g.game_id}" from PS5?\n\nFolder: ${g.ftp_path}\n\nThis cannot be undone.`,
|
|
'VERWIJDER VAN PS5'
|
|
);
|
|
if (!confirmation) return;
|
|
|
|
const btn = $('btn-manage-delete-ps5');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Deleting…';
|
|
|
|
try {
|
|
await window.pork.ftpDeleteGame(g.game_id, confirmation);
|
|
state.currentGame = { ...g, availability: { ...(g.availability || {}), state: 'absent', confirmedInstallCount: 0 }, ftp_path: '' };
|
|
state.games = await window.pork.listGames({});
|
|
renderManageInstallLocation(state.currentGame);
|
|
const toggleInstBtn = $('btn-manage-toggle-installed');
|
|
toggleInstBtn.textContent = 'Mark as Installed';
|
|
toggleInstBtn.className = 'btn';
|
|
setStatus(`${g.title || g.game_id} deleted from PS5`, 'ok');
|
|
} catch (err) {
|
|
setStatus(`Delete failed: ${err.message}`, 'error');
|
|
} finally {
|
|
btn.textContent = 'Delete from PS5';
|
|
btn.disabled = !state.connected || !state.currentGame?.ftp_path;
|
|
}
|
|
});
|
|
|
|
// ── Backpork section in Manage tab ────────────────────────────────────────────
|
|
let porkInProgress = false;
|
|
|
|
window.pork.on('backporks:pork:progress', ({ file, filesDone, filesTotal, bytesOverall }) => {
|
|
const fill = $('pork-progress-fill');
|
|
const label = $('pork-progress-label');
|
|
const pct = filesTotal > 0 ? Math.round((filesDone / filesTotal) * 100) : 0;
|
|
if (fill) fill.style.width = `${pct}%`;
|
|
if (label) label.textContent = `Uploading ${filesDone + 1}/${filesTotal}: ${escHtml(file || '')}`;
|
|
});
|
|
|
|
async function renderBackporkSection(g) {
|
|
const section = $('manage-backpork-section');
|
|
const entries = await window.pork.backporksEntries(g.game_id);
|
|
|
|
if (!entries.length) { section.hidden = true; return; }
|
|
section.hidden = false;
|
|
|
|
// Porked-to status
|
|
const statusEl = $('manage-porked-status');
|
|
if (g.porked_firmware) {
|
|
const when = g.porked_at ? ` · ${new Date(g.porked_at).toLocaleDateString()}` : '';
|
|
statusEl.innerHTML = `
|
|
<div class="manage-porked-status">
|
|
✓ Porked to firmware <strong>${escHtml(g.porked_firmware)}</strong>${escHtml(when)}
|
|
<button class="clear-pork btn btn-sm" data-game="${escHtml(g.game_id)}">Clear</button>
|
|
</div>`;
|
|
} else {
|
|
statusEl.innerHTML = '';
|
|
}
|
|
|
|
// Entry list
|
|
const listEl = $('manage-backpork-list');
|
|
listEl.innerHTML = entries.map(e => {
|
|
const gamePath = e.game_path || e.folder_path;
|
|
const displayPath = gamePath.replace(/\\/g, '/').split('/').pop();
|
|
return `
|
|
<div class="manage-backpork-entry"
|
|
data-fw="${escHtml(e.folder_name)}"
|
|
data-path="${escHtml(e.folder_path)}"
|
|
data-game="${escHtml(g.game_id)}">
|
|
<span class="manage-backpork-fw">${escHtml(e.folder_name)}</span>
|
|
<span class="manage-backpork-path" title="${escHtml(gamePath)}">${escHtml(displayPath)}</span>
|
|
<button class="btn btn-sm manage-btn-view-backpork"
|
|
data-open="${escHtml(gamePath)}">View Backpork</button>
|
|
<button class="btn btn-sm btn-teal pork-btn"
|
|
${!state.connected ? 'disabled title="Connect to PS5 first"' : ''}>
|
|
Pork to PS5
|
|
</button>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
// Reset progress bar
|
|
$('manage-pork-progress').hidden = true;
|
|
$('pork-progress-fill').style.width = '0%';
|
|
}
|
|
|
|
// Clear porked status
|
|
$('manage-porked-status').addEventListener('click', async e => {
|
|
const btn = e.target.closest('.clear-pork');
|
|
if (!btn) return;
|
|
const gameId = btn.dataset.game;
|
|
await window.pork.updateGame({ game_id: gameId, fields: { porked_firmware: null, porked_at: null } });
|
|
state.games = await window.pork.listGames({});
|
|
const updated = state.games.find(x => x.game_id === gameId) || state.currentGame;
|
|
state.currentGame = { ...state.currentGame, porked_firmware: null, porked_at: null };
|
|
await renderBackporkSection(state.currentGame);
|
|
setStatus('Porked status cleared', 'ok');
|
|
});
|
|
|
|
// Backpork list click delegation
|
|
$('manage-backpork-list').addEventListener('click', async e => {
|
|
// ── View Backpork folder ─────────────────────────────────────────────────────
|
|
const viewBtn = e.target.closest('.manage-btn-view-backpork');
|
|
if (viewBtn) {
|
|
window.pork.openShell(viewBtn.dataset.open);
|
|
return;
|
|
}
|
|
|
|
// Pork to PS5
|
|
const btn = e.target.closest('.pork-btn');
|
|
if (!btn || porkInProgress) return;
|
|
const entry = btn.closest('.manage-backpork-entry');
|
|
if (!entry) return;
|
|
|
|
const gameId = entry.dataset.game;
|
|
const folderName = entry.dataset.fw;
|
|
const folderPath = entry.dataset.path;
|
|
|
|
// Ensure a destination is remembered before porking
|
|
const ftpDest = await ensureGameFtpPath(gameId);
|
|
if (!ftpDest) return; // user cancelled picker
|
|
|
|
porkInProgress = true;
|
|
btn.disabled = true;
|
|
$('manage-pork-progress').hidden = false;
|
|
$('pork-progress-fill').style.width = '0%';
|
|
$('pork-progress-label').textContent = 'Starting…';
|
|
setStatus(`Porking ${gameId} (${folderName})…`);
|
|
|
|
try {
|
|
const { remotePath } = await window.pork.backporksPork(gameId, folderName, folderPath);
|
|
$('manage-pork-progress').hidden = true;
|
|
setStatus(`Porked to PS5 at ${remotePath}`, 'ok');
|
|
|
|
// Refresh game state
|
|
state.games = await window.pork.listGames({});
|
|
const updated = state.games.find(x => x.game_id === gameId);
|
|
if (updated) {
|
|
state.currentGame = updated;
|
|
await renderBackporkSection(updated);
|
|
// Update installed toggle
|
|
const toggleInstBtn = $('btn-manage-toggle-installed');
|
|
toggleInstBtn.textContent = 'Mark as Not Installed';
|
|
toggleInstBtn.className = 'btn btn-danger';
|
|
}
|
|
} catch (err) {
|
|
$('manage-pork-progress').hidden = true;
|
|
setStatus(`Pork failed: ${err.message}`, 'error');
|
|
} finally {
|
|
porkInProgress = false;
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
|
|
// Toggle installed status
|
|
$('btn-manage-toggle-installed').addEventListener('click', async () => {
|
|
setStatus('Install status is derived from complete console scan evidence.', 'info');
|
|
});
|
|
|
|
// ── Delegation for all manage-panel interactions (attached once, never re-bound)
|
|
$('manage-backup-list').addEventListener('click', async e => {
|
|
const item = e.target.closest('.manage-backup-item');
|
|
if (!item) return;
|
|
|
|
const backupId = Number(item.dataset.backupId);
|
|
const gameId = item.dataset.game;
|
|
const local = item.dataset.local;
|
|
const folder = item.dataset.folder;
|
|
|
|
// ── Open Folder ─────────────────────────────────────────────────────────────
|
|
if (e.target.closest('.manage-btn-folder')) {
|
|
window.pork.openShell(folder);
|
|
return;
|
|
}
|
|
|
|
// ── Upload to PS5 — inline expand ───────────────────────────────────────────
|
|
if (e.target.closest('.manage-btn-upload')) {
|
|
if (uploadInProgress) return;
|
|
await openInstallModal(gameId, 'pfs');
|
|
return;
|
|
}
|
|
|
|
// ── Remove backup record ─────────────────────────────────────────────────────
|
|
if (e.target.closest('.manage-btn-remove')) {
|
|
const btn = e.target.closest('.manage-btn-remove');
|
|
const confirmation = await showExactConfirmation('Remove this backup record from Chimera?', 'VERWIJDER');
|
|
if (!confirmation) return;
|
|
btn.disabled = true;
|
|
try {
|
|
await window.pork.deleteBackup(backupId, confirmation);
|
|
setStatus('Backup record removed', 'ok');
|
|
|
|
// Refresh the modal's backup-dependent sections
|
|
const backups = await window.pork.listBackups(gameId);
|
|
const localBackups = backups.filter(b => b.source !== 'backpork');
|
|
state.games = await window.pork.listGames({});
|
|
const g = state.games.find(x => x.game_id === gameId) || state.currentGame;
|
|
state.currentGame = g;
|
|
|
|
await renderManagePanel(g, backups);
|
|
await renderHashSection(gameId, backups);
|
|
$('modal-backup-list').innerHTML = localBackups.length
|
|
? localBackups.map(b => `<li><span>${escHtml(b.backup_path)}</span><span>${fmt(b.size)}</span></li>`).join('')
|
|
: '<li style="justify-content:center">No backups recorded</li>';
|
|
} catch (err) {
|
|
setStatus(err.message, 'error');
|
|
btn.disabled = false;
|
|
}
|
|
return;
|
|
}
|
|
});
|
|
|
|
$('btn-manage-upload').addEventListener('click', async () => {
|
|
if (uploadInProgress || !uploadLocalPath) return;
|
|
const remotePath = $('manage-remote-path').value.trim();
|
|
if (!remotePath) { setStatus('Enter a remote path on the PS5', 'error'); return; }
|
|
|
|
uploadInProgress = true;
|
|
$('btn-manage-upload').disabled = true;
|
|
const log = $('manage-upload-log');
|
|
log.hidden = false;
|
|
log.textContent = 'Starting upload…';
|
|
|
|
try {
|
|
await window.pork.ftpUpload({ localPath: uploadLocalPath, remotePath });
|
|
log.textContent = 'Upload complete ✓';
|
|
setStatus('Uploaded to PS5 successfully', 'ok');
|
|
uploadLocalPath = null;
|
|
$('manage-remote-path').value = '';
|
|
$('manage-remote-path').readOnly = true;
|
|
document.querySelectorAll('.manage-item-selected').forEach(el => el.classList.remove('manage-item-selected'));
|
|
} catch (e) {
|
|
log.textContent = `Upload failed: ${e.message}`;
|
|
setStatus(e.message, 'error');
|
|
$('btn-manage-upload').disabled = false;
|
|
} finally {
|
|
uploadInProgress = false;
|
|
}
|
|
});
|
|
|
|
$('btn-manage-toggle-backed').addEventListener('click', async () => {
|
|
setStatus('Protection status is derived from registered local-copy evidence.', 'info');
|
|
});
|
|
|
|
$('btn-manage-refresh-meta').addEventListener('click', async () => {
|
|
if (!state.currentGame) return;
|
|
const { game_id } = state.currentGame;
|
|
$('btn-manage-refresh-meta').disabled = true;
|
|
setStatus('Refreshing metadata…');
|
|
try {
|
|
await window.pork.prosperoFetch(game_id);
|
|
setStatus('Metadata refreshed', 'ok');
|
|
} catch (e) {
|
|
setStatus(`Refresh failed: ${e.message}`, 'error');
|
|
} finally {
|
|
$('btn-manage-refresh-meta').disabled = false;
|
|
}
|
|
});
|
|
|