// ── PS Avatar ───────────────────────────────────────────────────────────────── let _xavatarClient = null; let _xavatarLastResult = null; // { buffer: ArrayBuffer, filename: string } const _xavatarThumbCache = new Map(); // remotePath → blob URL let _ps5ProfileInventory = null; let _ps5ProfileSelectedUid = null; let _ps5ProfileBound = false; function ps5ProfileText(key, fallback) { return typeof window.t === 'function' ? window.t(`ps5Profile.${key}`, fallback) : fallback; } function ps5ProfileSetAction(message, state = '') { const target = $('ps5-profile-action-state'); if (!target) return; target.textContent = message || ''; target.className = `ps5-profile-action-state${state ? ` is-${state}` : ''}`; } function ps5ProfileSelectedUser() { return _ps5ProfileInventory?.users?.find(user => user.uid === _ps5ProfileSelectedUid) || null; } async function loadPs5ProfileRuntime() { const card = document.querySelector('.ps5-profile-runtime'); const title = $('ps5-profile-runtime-title'); const state = $('ps5-profile-runtime-state'); const detail = $('ps5-profile-runtime-detail'); const button = $('btn-ps5-profile-start-payload'); if (!card || !state || !detail || !button || !window.pork?.ps5ProfileRuntimePreflight) return null; card.classList.remove('is-ready'); button.disabled = true; state.textContent = ps5ProfileText('runtimeCheckingShort', 'Checking…'); try { const result = await window.pork.ps5ProfileRuntimePreflight(); if (title) title.textContent = `PS5Upload ${result.version || '5.4.19'}`; const artifactsReady = result.engine?.verified && result.engine?.available && result.payload?.verified && result.console?.configured; if (result.ready) { card.classList.add('is-ready'); state.textContent = ps5ProfileText('runtimeReady', 'Ready to start'); detail.textContent = ps5ProfileText('runtimeReadyDetail', 'Exact engine and ELF hashes verified; the PS5 loader is reachable on port 9021.'); button.disabled = false; } else if (artifactsReady && result.loader?.reason === 'unreachable') { state.textContent = ps5ProfileText('runtimeLoaderClosed', 'Loader waiting'); detail.textContent = ps5ProfileText('runtimeLoaderHelp', 'Engine and ELF are verified. Start the jailbreak ELF loader on the PS5, then refresh this check.'); } else { state.textContent = ps5ProfileText('runtimeBlocked', 'Preflight blocked'); detail.textContent = ps5ProfileText('runtimeBlockedDetail', 'A pinned artifact, configured PS5 host or local engine check failed.'); } return result; } catch (error) { state.textContent = ps5ProfileText('runtimeBlocked', 'Preflight blocked'); detail.textContent = error.message; return null; } } function renderPs5ProfileOperations(operations = []) { const target = $('ps5-profile-operations'); if (!target) return; if (!operations.length) { target.innerHTML = `
${escHtml(ps5ProfileText('noActivity', 'No prepared profile changes yet.'))}
`; return; } target.innerHTML = operations.slice().reverse().slice(0, 8).map(operation => { const canRollback = ['verified', 'verification_failed', 'failed', 'interrupted'].includes(operation.status); const label = operation.kind === 'rename' ? ps5ProfileText('renameActivity', 'Display name') : ps5ProfileText('avatarActivity', 'Profile image'); const detail = operation.kind === 'rename' ? `${operation.snapshot?.username || ''} → ${operation.desired?.username || ''}` : operation.uidHex; return `
${escHtml(label)} ${escHtml(operation.status)}${escHtml(detail)} · ${escHtml(new Date(operation.updatedAt || operation.createdAt).toLocaleString())}
${canRollback ? `` : ''}
`; }).join(''); } function selectPs5LocalProfile(uid) { _ps5ProfileSelectedUid = Number(uid); const user = ps5ProfileSelectedUser(); document.querySelectorAll('.ps5-profile-user').forEach(card => card.classList.toggle('is-selected', Number(card.dataset.profileUid) === _ps5ProfileSelectedUid)); const name = $('ps5-profile-name'); const rename = $('btn-ps5-profile-rename'); const applyAvatar = $('btn-ps5-profile-apply-avatar'); if (!user) return; $('ps5-profile-selected-title').textContent = user.username; $('ps5-profile-selected-uid').textContent = user.uidHex; $('ps5-profile-avatar-initial').textContent = [...user.username][0]?.toUpperCase() || '?'; name.value = user.username; name.disabled = false; rename.disabled = false; applyAvatar.disabled = !_xavatarClient?._masterCanvas; ps5ProfileSetAction(user.avatar?.available ? ps5ProfileText('avatarObserved', 'Current profile image evidence was read successfully.') : ps5ProfileText('avatarDefault', 'This user currently has no recoverable custom image. Applying is blocked until exact recovery is possible.')); const preview = $('ps5-profile-avatar-preview'); const shell = preview?.closest('.ps5-profile-avatar-shell'); if (preview && shell && !_xavatarClient?._masterCanvas) { shell.classList.remove('has-preview'); window.pork.ps5ProfileCurrentAvatar({ uid: user.uid }).then(result => { if (_ps5ProfileSelectedUid !== user.uid || !result?.dataUrl || _xavatarClient?._masterCanvas) return; const image = new Image(); image.onload = () => { const context = preview.getContext('2d'); context.clearRect(0, 0, preview.width, preview.height); context.drawImage(image, 0, 0, preview.width, preview.height); shell.classList.add('has-preview'); }; image.src = result.dataUrl; }).catch(() => {}); } } async function loadPs5LocalProfiles() { const provider = $('ps5-profile-provider-state'); const usersTarget = $('ps5-profile-users'); if (!provider || !usersTarget || !window.pork?.ps5ProfileInspect) return; if (!_ps5ProfileBound) bindPs5ProfileWorkspace(); loadPs5ProfileRuntime(); provider.className = 'ps5-profile-provider-state'; provider.textContent = ps5ProfileText('checking', 'Checking the guarded profile bridge…'); usersTarget.innerHTML = `
${escHtml(ps5ProfileText('loading', 'Reading local users…'))}
`; try { const [inventory, operations] = await Promise.all([window.pork.ps5ProfileInspect(), window.pork.ps5ProfileListOperations()]); _ps5ProfileInventory = inventory; renderPs5ProfileOperations(operations); if (!inventory.available) { provider.classList.add('is-blocked'); provider.textContent = ps5ProfileText('bridgeBlocked', 'Profile bridge unavailable · configure PS5 host and pinned PS5Upload 5.4.19'); usersTarget.innerHTML = `
${escHtml(ps5ProfileText('bridgeHelp', 'Start the pinned loopback bridge and configure the PS5 host. No console change is possible while this gate is closed.'))}
`; return; } provider.classList.add('is-ready'); provider.textContent = `${ps5ProfileText('bridgeReady', 'Verified bridge')} · ${inventory.provider?.version || '5.4.19'}`; usersTarget.innerHTML = inventory.users.length ? inventory.users.map(user => ``).join('') : `
${escHtml(ps5ProfileText('noUsers', 'No local users were reported.'))}
`; const preferred = inventory.users.find(user => user.uid === _ps5ProfileSelectedUid) || inventory.users.find(user => user.foreground) || inventory.users[0]; if (preferred) selectPs5LocalProfile(preferred.uid); } catch (error) { provider.classList.add('is-blocked'); provider.textContent = ps5ProfileText('bridgeError', 'Profile bridge check failed'); usersTarget.innerHTML = `
${escHtml(error.message)}
`; } } function bindPs5ProfileWorkspace() { if (_ps5ProfileBound) return; _ps5ProfileBound = true; $('btn-ps5-profile-refresh')?.addEventListener('click', loadPs5LocalProfiles); $('btn-ps5-profile-start-payload')?.addEventListener('click', async () => { const button = $('btn-ps5-profile-start-payload'); const confirmation = await showExactConfirmation( ps5ProfileText('startPayloadConfirm', 'Send only the verified PS5Upload 5.4.19 ELF to the configured PS5 loader? This payload remains active until rest mode or reboot.'), 'START PS5 PROFILE PAYLOAD', ); if (!confirmation) return; button.disabled = true; ps5ProfileSetAction(ps5ProfileText('startingPayload', 'Sending the verified payload and checking the management service…')); try { const result = await window.pork.ps5ProfileStartPayload({ confirmation }); if (!result.managementReady) throw new Error(ps5ProfileText('payloadNotReady', 'The ELF was sent, but the PS5 management service did not become ready. Check the jailbreak and firmware compatibility.')); ps5ProfileSetAction(ps5ProfileText('payloadReady', 'Verified payload active. Reading local profiles…'), 'ok'); await loadPs5LocalProfiles(); } catch (error) { ps5ProfileSetAction(error.message, 'error'); await loadPs5ProfileRuntime(); } }); $('ps5-profile-users')?.addEventListener('click', event => { const card = event.target.closest('.ps5-profile-user'); if (card) selectPs5LocalProfile(card.dataset.profileUid); }); $('btn-ps5-profile-rename')?.addEventListener('click', async () => { const user = ps5ProfileSelectedUser(); if (!user) return; ps5ProfileSetAction(ps5ProfileText('preparing', 'Creating recovery snapshot and checking fresh evidence…')); try { const operation = await window.pork.ps5ProfilePrepareRename({ uid: user.uid, name: $('ps5-profile-name').value }); const confirmation = await showExactConfirmation(ps5ProfileText('renameConfirm', 'Apply this local display name after the recovery snapshot and fresh-evidence check?'), operation.confirmation); if (!confirmation) return void loadPs5LocalProfiles(); await window.pork.ps5ProfileApply({ id: operation.id, confirmation }); ps5ProfileSetAction(ps5ProfileText('verifiedRestart', 'Verified on the console. Restart the PS5 to show the change everywhere.'), 'ok'); await loadPs5LocalProfiles(); } catch (error) { ps5ProfileSetAction(error.message, 'error'); } }); $('btn-ps5-profile-apply-avatar')?.addEventListener('click', async () => { const user = ps5ProfileSelectedUser(); if (!user || !_xavatarClient?._masterCanvas) return; ps5ProfileSetAction(ps5ProfileText('converting', 'Building the canonical profile image set…')); try { const converted = await _xavatarClient.convert(); const operation = await window.pork.ps5ProfilePrepareAvatar({ uid: user.uid, archive: converted.buffer }); const confirmation = await showExactConfirmation(ps5ProfileText('avatarConfirm', 'Apply this reviewed image after preserving the current profile image?'), operation.confirmation); if (!confirmation) return void loadPs5LocalProfiles(); await window.pork.ps5ProfileApply({ id: operation.id, confirmation }); ps5ProfileSetAction(ps5ProfileText('verifiedRestart', 'Verified on the console. Restart the PS5 to show the change everywhere.'), 'ok'); await loadPs5LocalProfiles(); } catch (error) { ps5ProfileSetAction(error.message, 'error'); } }); $('ps5-profile-operations')?.addEventListener('click', async event => { const button = event.target.closest('.ps5-profile-rollback'); const row = button?.closest('.ps5-profile-operation'); if (!row) return; const phrase = `ROLLBACK PS5 PROFILE ${row.dataset.profileUid}`; const confirmation = await showExactConfirmation(ps5ProfileText('rollbackConfirm', 'Restore the exact local profile state captured before this operation?'), phrase); if (!confirmation) return; button.disabled = true; try { await window.pork.ps5ProfileRollback({ id: row.dataset.profileOperation, confirmation }); ps5ProfileSetAction(ps5ProfileText('rollbackVerified', 'Previous local profile state restored and verified.'), 'ok'); await loadPs5LocalProfiles(); } catch (error) { ps5ProfileSetAction(error.message, 'error'); button.disabled = false; } }); } async function loadXavatarCompanion() { const status = $('xavatar-companion-status'); const button = $('btn-xavatar-install-companion'); if (!status || !button || !window.pork.xavatarCompanion) return; try { const companion = await window.pork.xavatarCompanion(); button.disabled = !companion.installerReady; button.dataset.ready = companion.installerReady ? 'true' : 'false'; status.textContent = companion.installerReady ? `${companion.titleId} · v${companion.version} · ${(companion.size / 1024 / 1024).toFixed(1)} MB · etaHEN DPI ready` : `${companion.titleId} · v${companion.version} · ${companion.installerError}`; status.title = `Expected SHA-256: ${companion.sha256}`; } catch (error) { button.disabled = true; status.textContent = `Installer check failed: ${error.message}`; } if (button.dataset.bound) return; button.dataset.bound = 'true'; button.addEventListener('click', async () => { const confirmation = await showExactConfirmation( "Install Lapy's Avatar Changer through etaHEN Direct Package Installer? The PS5 downloads the fixed reviewed package URL; Chimera does not launch the app.", 'INSTALL XAVATAR', ); if (!confirmation) return; button.disabled = true; status.textContent = 'Submitting package to etaHEN DPI…'; try { await window.pork.xavatarInstallCompanion(confirmation); status.textContent = 'Installation submitted to etaHEN DPI. Follow the package progress on your PS5.'; showToast('Avatar Changer installation submitted.', 'ok', 9000); } catch (error) { status.textContent = `Installation failed: ${error.message}`; showToast(error.message, 'error', 10000); } await loadXavatarCompanion(); }); } function loadXavatar() { // Refresh the library on every navigate; only initialise the converter once. loadXavatarLibrary(); loadXavatarCompanion(); loadPs5LocalProfiles(); if (_xavatarClient) return; if (!window.XavatarClient) { $('xavatar-status').textContent = 'XavatarClient not available — ensure the module script loaded.'; return; } // Build the API object — prefer the dedicated xavatarAPI bridge; fall back to // the pork bridge channel so conversion still works even if the separate // xavatarElectronModule preload failed to expose window.xavatarAPI. let _xavApi = window.xavatarAPI; if (!_xavApi) { console.info('[Chimera] Dedicated XAvatar bridge unavailable; using the supported web compatibility path.'); _xavApi = { async convertFromCanvas(canvas, opts = {}) { const ctx = canvas.getContext('2d'); const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const rgba = new Uint8Array(imgData.data.buffer); const res = await window.pork.xavatarConvertCanvas( rgba, canvas.width, canvas.height, opts.filename || 'avatar' ); if (!res || !res.ok) throw new Error((res && res.error) || 'Conversion failed'); return { buffer: res.buffer && res.buffer.buffer ? res.buffer.buffer : res.buffer, filename: res.filename }; } }; } _xavatarClient = new window.XavatarClient({ api: _xavApi }); const dropZone = $('xavatar-drop-zone'); const fileInput = $('xavatar-file-input'); const preview = $('xavatar-preview'); const status = $('xavatar-status'); const convertBtn = $('btn-xavatar-convert'); const sendBtn = $('btn-xavatar-send-ps5'); const convProgress = $('xavatar-conv-progress'); const convBar = $('xavatar-conv-bar'); function _xavatarSetProgress(pct, label) { if (!convProgress) return; convProgress.hidden = pct == null; if (pct != null) { convBar.style.width = `${pct}%`; convBar.textContent = label || ''; } } _xavatarClient.on('image-ready', (canvas) => { const ctx = preview.getContext('2d'); preview.width = canvas.width; preview.height = canvas.height; ctx.drawImage(canvas, 0, 0); preview.hidden = false; convertBtn.disabled = false; sendBtn.disabled = false; _xavatarLastResult = null; // new image — invalidate previous conversion _xavatarSetProgress(null); status.textContent = 'Image loaded — convert & download, or send directly to PS5'; dropZone.classList.add('xavatar-drop-zone--has-image'); const profilePreview = $('ps5-profile-avatar-preview'); const profileShell = profilePreview?.closest('.ps5-profile-avatar-shell'); if (profilePreview && profileShell) { const profileContext = profilePreview.getContext('2d'); profileContext.clearRect(0, 0, profilePreview.width, profilePreview.height); profileContext.drawImage(canvas, 0, 0, profilePreview.width, profilePreview.height); profileShell.classList.add('has-preview'); $('btn-ps5-profile-apply-avatar').disabled = !ps5ProfileSelectedUser(); } }); _xavatarClient.on('load-error', (err) => { status.textContent = 'Failed to load image: ' + err.message; }); _xavatarClient.on('convert-start', () => { convertBtn.disabled = true; convertBtn.textContent = 'Converting…'; status.textContent = 'Converting…'; }); _xavatarClient.on('convert-error', (err) => { convertBtn.disabled = false; convertBtn.textContent = 'Convert & Download'; _xavatarSetProgress(null); status.textContent = 'Error: ' + err.message; }); _xavatarClient.wireFileInput(fileInput); _xavatarClient.wireDropZone(dropZone); _xavatarClient.wirePaste(document); $('btn-xavatar-browse').addEventListener('click', () => fileInput.click()); dropZone.addEventListener('click', (e) => { if (e.target !== $('btn-xavatar-browse')) fileInput.click(); }); dropZone.addEventListener('dragover', () => dropZone.classList.add('xavatar-drop-zone--over')); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('xavatar-drop-zone--over')); dropZone.addEventListener('drop', () => dropZone.classList.remove('xavatar-drop-zone--over')); convertBtn.addEventListener('click', async () => { status.textContent = ''; _xavatarSetProgress(null); try { const result = await _xavatarClient.convert(); _xavatarLastResult = result; convertBtn.disabled = false; convertBtn.textContent = 'Convert & Download'; const saved = await window.pork.xavatarSaveFile(result.buffer, result.filename); status.textContent = saved?.saved ? `Saved: ${result.filename}` : 'Converted — not saved.'; } catch (err) { convertBtn.disabled = false; convertBtn.textContent = 'Convert & Download'; if (!$('xavatar-status').textContent) { status.textContent = 'Error: ' + err.message; } } }); sendBtn.addEventListener('click', async () => { if (!_xavatarClient?._masterCanvas) return; convertBtn.disabled = true; sendBtn.disabled = true; sendBtn.textContent = 'Working…'; status.textContent = ''; try { // ── Step 1: Convert ────────────────────────────────────────────────── _xavatarSetProgress(10, 'Converting…'); status.textContent = 'Converting image to .xavatar…'; const result = await _xavatarClient.convert(); _xavatarLastResult = result; // ── Step 2: Random filename ────────────────────────────────────────── const pad = n => String(n).padStart(5, '0'); const rnd = pad(Math.floor(Math.random() * 100000)); const rndName = `chimera${rnd}.xavatar`; _xavatarSetProgress(55, 'Uploading…'); status.textContent = `Queuing upload → /data/AVATARS/${rndName}…`; // ── Step 3: Upload ────────────────────────────────────────────────── await window.pork.xavatarUploadToPs5(result.buffer, rndName); _xavatarSetProgress(100, 'Queued ✓'); status.textContent = `Queued → /data/AVATARS/${rndName}`; sendBtn.textContent = '↗ Send to PS5'; sendBtn.disabled = false; convertBtn.disabled = false; convertBtn.textContent = 'Convert & Download'; if ($('xavatar-notify-toggle')?.checked) { window.pork.psnotifySend('XAvatar Queued', rndName).catch(() => {}); } setTimeout(() => _xavatarSetProgress(null), 3000); } catch (e) { _xavatarSetProgress(null); convertBtn.disabled = false; sendBtn.disabled = false; sendBtn.textContent = '↗ Send to PS5'; convertBtn.textContent = 'Convert & Download'; status.textContent = 'Error: ' + e.message; } }); $('btn-xavatar-refresh-lib').addEventListener('click', loadXavatarLibrary); } function _xavatarFmtSize(bytes) { if (!bytes) return ''; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } async function loadXavatarLibrary() { const listEl = $('xavatar-library-list'); const emptyEl = $('xavatar-library-empty'); if (!listEl) return; listEl.innerHTML = '
Loading…
'; emptyEl.hidden = true; try { const ftpStatus = await window.pork.ftpStatus().catch(() => ({ connected: false })); if (!ftpStatus?.connected) { listEl.innerHTML = ''; emptyEl.hidden = false; emptyEl.textContent = 'Connect to PS5 FTP to browse /data/AVATARS.'; return; } const files = await window.pork.ftpListDir('/data/AVATARS'); const xavs = files.filter(f => !f.isDir); if (!xavs.length) { listEl.innerHTML = ''; emptyEl.hidden = false; return; } emptyEl.hidden = true; listEl.innerHTML = '
' + xavs.map(f => { const rpath = `/data/AVATARS/${escHtml(f.name)}`; return `
${escHtml(f.name)}
👤
${escHtml(f.name)} ${_xavatarFmtSize(f.size)}
`; }).join('') + '
'; _setupXavatarThumbObserver(listEl); } catch (err) { listEl.innerHTML = ''; emptyEl.hidden = false; emptyEl.textContent = 'Failed to load library: ' + err.message; } } /** Attach IntersectionObserver to lazily load thumbnails as cards scroll into view */ function _setupXavatarThumbObserver(container) { const imgs = container.querySelectorAll('img.xavatar-thumb'); if (!imgs.length) return; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; observer.unobserve(img); _loadXavatarThumb(img, img.dataset.xaPath); } }); }, { rootMargin: '120px', threshold: 0.01 }); imgs.forEach(img => observer.observe(img)); } /** Fetch + extract avatar.png from a .xavatar on FTP, cache as blob URL */ async function _getXavatarThumbUrl(rpath) { let url = _xavatarThumbCache.get(rpath); if (!url) { const xavBuf = await window.pork.ftpDownloadFile(rpath); const pngBuf = await window.pork.xavatarExtractPng(xavBuf); const blob = new Blob([pngBuf], { type: 'image/png' }); url = URL.createObjectURL(blob); _xavatarThumbCache.set(rpath, url); } return url; } /** Load one thumbnail into an img element */ async function _loadXavatarThumb(img, rpath) { const card = img.closest?.('.xavatar-lib-card'); try { const url = await _getXavatarThumbUrl(rpath); img.src = url; img.onload = () => card?.classList.add('xavatar-thumb-loaded'); img.onerror = () => card?.classList.add('xavatar-thumb-error'); } catch (_) { card?.classList.add('xavatar-thumb-error'); } } // ── XAvatar full-size modal ─────────────────────────────────────────────────── let _xavatarModalPath = null; function _xavatarOpenModal(rpath, name) { _xavatarModalPath = rpath; const modal = $('xavatar-thumb-modal'); const img = $('xavatar-modal-img'); const namEl = $('xavatar-modal-name'); img.src = ''; img.alt = 'Loading…'; namEl.textContent = name; openAccessibleDialog(modal, $('btn-xavatar-modal-close')); // reset modal send button const sendBtn = $('btn-xavatar-modal-send'); if (sendBtn) { sendBtn.disabled = false; sendBtn.textContent = '↗ Send to PS5'; } _getXavatarThumbUrl(rpath) .then(url => { if (_xavatarModalPath === rpath) { img.src = url; img.alt = name; } }) .catch(() => { if (_xavatarModalPath === rpath) img.alt = 'Preview unavailable'; }); } // Close modal on backdrop click or close button document.addEventListener('click', e => { const modal = $('xavatar-thumb-modal'); if (!modal || modal.hidden) return; if (e.target === modal || e.target.id === 'btn-xavatar-modal-close') { closeAccessibleDialog(modal); _xavatarModalPath = null; } }); document.addEventListener('keydown', event => { const modal = $('xavatar-thumb-modal'); if (event.key === 'Escape' && modal && !modal.hidden) { closeAccessibleDialog(modal); _xavatarModalPath = null; } }); // ── XAvatar: format picker dialog ────────────────────────────────────────── function _xavatarPickFormat() { return new Promise(resolve => { const dlg = document.createElement('dialog'); dlg.style.cssText = [ 'border:none', 'border-radius:10px', 'padding:22px 26px 18px', 'background:var(--bg2,#1e1e2e)', 'color:var(--fg,#cdd6f4)', 'box-shadow:0 8px 32px rgba(0,0,0,.65)', 'text-align:center', 'min-width:260px', 'position:fixed', 'top:50%', 'left:50%', 'transform:translate(-50%,-50%)', 'margin:0', ].join(';'); dlg.innerHTML = [ '

Choose download format

', '
', ' ', ' ', ' ', '
', ].join(''); dlg.setAttribute('aria-labelledby', '_xa-fmt-title'); document.body.appendChild(dlg); dlg.showModal(); const done = (val) => { dlg.removeEventListener('close', onClose); dlg.close(); dlg.remove(); resolve(val); }; const onClose = () => { dlg.remove(); resolve(null); }; dlg.addEventListener('close', onClose); dlg.querySelector('#_xa-fmt-xav').addEventListener('click', () => done('xavatar')); dlg.querySelector('#_xa-fmt-png').addEventListener('click', () => done('png')); dlg.querySelector('#_xa-fmt-cancel').addEventListener('click', () => done(null)); }); } // Modal action buttons function _xavatarBindModalButtons() { const modalDownload = $('btn-xavatar-modal-download'); const modalToPng = $('btn-xavatar-modal-topng'); const modalSend = $('btn-xavatar-modal-send'); if (!modalDownload) return; modalDownload.addEventListener('click', async () => { if (!_xavatarModalPath) return; const fmt = await _xavatarPickFormat(); if (!fmt) return; const name = $('xavatar-modal-name')?.textContent || 'avatar.xavatar'; modalDownload.disabled = true; try { const buf = await window.pork.ftpDownloadFile(_xavatarModalPath); if (fmt === 'png') { const pngName = name.replace(/\.xavatar$/i, '') + '.png'; const pngBuf = await window.pork.xavatarExtractPng(buf); await window.pork.xavatarSaveFile(pngBuf, pngName); } else { await window.pork.xavatarSaveFile(buf, name); } } catch (err) { showToast('Download failed: ' + err.message, 'error'); } modalDownload.disabled = false; }); modalToPng.addEventListener('click', async () => { if (!_xavatarModalPath) return; const name = $('xavatar-modal-name')?.textContent || 'avatar'; const pngName = name.replace(/\.xavatar$/i, '') + '.png'; modalToPng.disabled = true; try { const xavBuf = await window.pork.ftpDownloadFile(_xavatarModalPath); const pngBuf = await window.pork.xavatarExtractPng(xavBuf); await window.pork.xavatarSaveFile(pngBuf, pngName); } catch (err) { showToast('PNG save failed: ' + err.message, 'error'); } modalToPng.disabled = false; }); modalSend.addEventListener('click', async () => { if (!_xavatarModalPath) return; const name = $('xavatar-modal-name')?.textContent || 'avatar.xavatar'; modalSend.disabled = true; modalSend.textContent = 'Sending…'; try { const buf = await window.pork.ftpDownloadFile(_xavatarModalPath); await window.pork.xavatarUploadToPs5(buf, name); modalSend.textContent = '↗ Send to PS5'; modalSend.disabled = false; showToast(`Queued: /data/AVATARS/${name}`, 'success'); } catch (err) { showToast('Upload failed: ' + err.message, 'error'); modalSend.textContent = '↗ Send to PS5'; modalSend.disabled = false; } }); } // Bind now if the DOM is already parsed, otherwise wait — this module may be // loaded after DOMContentLoaded has already fired. if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', _xavatarBindModalButtons); else _xavatarBindModalButtons(); // ── XAvatar: card thumbnail click → open modal ──────────────────────────────── document.addEventListener('click', e => { const card = e.target.closest('.xavatar-lib-card'); if (card && !e.target.closest('[data-xa]')) { _xavatarOpenModal(card.dataset.xaCardPath, card.dataset.xaCardName); } }); // ── XAvatar: library card action buttons (↓ download / ✕ delete) ───────────── document.addEventListener('click', async e => { const btn = e.target.closest('[data-xa]'); if (!btn) return; const action = btn.dataset.xa; const rpath = btn.dataset.xaPath; const name = btn.dataset.xaName; btn.disabled = true; try { if (action === 'download') { const fmt = await _xavatarPickFormat(); if (!fmt) { btn.disabled = false; return; } const buf = await window.pork.ftpDownloadFile(rpath); if (fmt === 'png') { const pngName = name.replace(/\.xavatar$/i, '') + '.png'; const pngBuf = await window.pork.xavatarExtractPng(buf); await window.pork.xavatarSaveFile(pngBuf, pngName); } else { await window.pork.xavatarSaveFile(buf, name); } } else if (action === 'delete') { // Native browser dialogs are blocked in this app; use the in-app modal. const confirmation = await showExactConfirmation(`Delete /data/AVATARS/${name} from your PS5?`, 'VERWIJDER VAN PS5'); if (!confirmation) { btn.disabled = false; return; } await window.pork.ftpDeleteFile(rpath, confirmation); btn.closest('.xavatar-lib-card')?.remove(); _xavatarThumbCache.delete(rpath); const grid = $('xavatar-library-list')?.querySelector('.xavatar-lib-grid'); if (grid && !grid.children.length) $('xavatar-library-empty').hidden = false; } } catch (err) { showToast('XAvatar: ' + err.message, 'error'); } btn.disabled = false; });