// ── Payload Manager Settings modal ──────────────────────────────────────────── $('btn-payload-settings').addEventListener('click', async () => { const s = await window.pork.getSettings(); $('pm-local-path').value = s.payloadLocalPath || ''; $('pm-remote-path').value = s.payloadRemotePath || ''; openAccessibleDialog($('payload-settings-overlay'), $('pm-remote-path')); }); $('btn-pm-browse-local').addEventListener('click', async () => { const folder = await window.pork.selectFolder(); if (folder) $('pm-local-path').value = folder; }); $('btn-pm-settings-save').addEventListener('click', async () => { await window.pork.setSettings({ payloadLocalPath: $('pm-local-path').value.trim(), payloadRemotePath: $('pm-remote-path').value.trim(), }); closeAccessibleDialog($('payload-settings-overlay')); }); ['payload-settings-close', 'btn-pm-settings-cancel'].forEach(id => $(id).addEventListener('click', () => closeAccessibleDialog($('payload-settings-overlay'))) ); $('payload-settings-overlay').addEventListener('click', event => { if (event.target === $('payload-settings-overlay')) closeAccessibleDialog($('payload-settings-overlay')); }); $('payload-settings-overlay').addEventListener('keydown', event => { if (event.key === 'Escape') closeAccessibleDialog($('payload-settings-overlay')); }); // ── Transfer Manager ────────────────────────────────────────────────────────── function fmtSpeed(bps) { if (!bps || bps < 0) return '—'; if (bps < 1024) return `${Math.round(bps)} B/s`; if (bps < 1048576) return `${(bps / 1024).toFixed(1)} KB/s`; return `${(bps / 1048576).toFixed(1)} MB/s`; } let _transferState = { jobs: [], paused: false, maxConcurrent: 1, activeCount: 0, queuedCount: 0, interruptedCount: 0, policy: {} }; function renderTransferPage(st) { _transferState = st; // Stats bar $('tstat-active').textContent = st.activeCount; $('tstat-queued').textContent = st.queuedCount; $('tstat-interrupted').textContent = st.interruptedCount || 0; $('tstat-concurrent').textContent = st.maxConcurrent; const slider = $('transfer-concurrent-slider'); if (slider && parseInt(slider.value) !== st.maxConcurrent) slider.value = st.maxConcurrent; if ($('transfer-window-start') && document.activeElement !== $('transfer-window-start')) $('transfer-window-start').value = st.policy?.windowStart || ''; if ($('transfer-window-end') && document.activeElement !== $('transfer-window-end')) $('transfer-window-end').value = st.policy?.windowEnd || ''; if ($('transfer-policy-state')) $('transfer-policy-state').textContent = st.policy?.windowStart && st.policy?.windowEnd ? `${st.policy.windowStart}–${st.policy.windowEnd} · ${window.t('transfers.policy.bandwidthUnsupported', 'bandwidth limiting is not supported by the current FTP layer')}` : `${window.t('transfers.policy.noWindow', 'No time window')} · ${window.t('transfers.policy.bandwidthUnsupported', 'bandwidth limiting is not supported by the current FTP layer')}`; // Total speed from all active jobs const totalSpeed = st.jobs .filter(j => j.status === 'active') .reduce((sum, j) => sum + (j.progress?.speedBps || 0), 0); $('tstat-speed').textContent = fmtSpeed(totalSpeed); // Pause/resume button const pauseBtn = $('btn-transfer-pause'); if (pauseBtn) { pauseBtn.textContent = st.paused ? `▶ ${window.t('transfers.resumeAll', 'Resume All')}` : `⏸ ${window.t('transfers.pauseAll', 'Pause All')}`; pauseBtn.disabled = !st.paused && !st.jobs.some(job => ['queued', 'active', 'retrying'].includes(job.status)); } const clearBtn = $('btn-transfer-clear'); if (clearBtn) clearBtn.disabled = !st.jobs.some(job => ['done', 'error', 'cancelled'].includes(job.status)); // Job list const list = $('transfers-list'); const empty = $('transfers-empty'); const visibleJobs = st.jobs.filter(j => j.status !== 'cancelled'); if (!visibleJobs.length) { list.innerHTML = ''; empty.hidden = false; return; } empty.hidden = true; list.innerHTML = visibleJobs.map(job => { const statusText = { queued: 'Queued', active: 'Transferring…', done: 'Done', error: 'Error', cancelled: 'Cancelled', interrupted: window.t('transfers.status.interrupted', 'Interrupted after restart'), retrying: 'Retrying…', }[job.status] || job.status; const typeLabel = { upload: 'UPLOAD', download: 'DOWNLOAD', pork: 'PORK' }[job.type] || (job.type || '').toUpperCase(); const typeClass = { upload: 'type-upload', download: 'type-download', pork: 'type-pork' }[job.type] || ''; const pct = job.progress?.percent || 0; const speed = job.status === 'active' ? fmtSpeed(job.progress?.speedBps) : ''; const file = job.progress?.file || ''; const filesDone = job.progress?.filesDone || 0; const filesTotal = job.progress?.filesTotal || 0; const transferred = job.progress?.transferred || 0; const total = job.progress?.total || 0; const showCancel = job.cancellable !== false && ['queued', 'active', 'retrying'].includes(job.status); const showRetry = ['error', 'interrupted'].includes(job.status) && job.retryable; const showBar = job.status === 'active' || job.status === 'done'; const currentBytes = job.progress?.currentBytes || 0; const currentFileSize = job.progress?.currentFileSize || 0; // File counter or single-file byte progress const sizeStr = filesTotal > 1 ? `File ${filesDone}/${filesTotal}` : (total > 0 ? `${fmt(transferred)} / ${fmt(total)}` : ''); // Current-file byte progress for directory uploads (shows activity on large files) const fileByteStr = (filesTotal > 1 && currentFileSize > 0 && job.status === 'active') ? `${fmt(currentBytes)} / ${fmt(currentFileSize)}` : ''; const pctLabel = (showBar && job.status === 'active') ? `${pct}%` : (job.status === 'done' ? '100%' : ''); return `
${escHtml(job.label)} ${typeLabel} ${statusText} ${showRetry ? `` : ''} ${showCancel ? `` : ''}
${showBar ? `
${pctLabel ? `${pctLabel}` : ''}
` : ''}
${speed ? `${speed}` : ''} ${file ? `${escHtml(file)}` : ''} ${fileByteStr ? `${fileByteStr}` : sizeStr ? `${sizeStr}` : ''} ${job.error ? `${escHtml(job.error)}` : ''} ${job.cancellable === false && ['queued', 'active', 'retrying'].includes(job.status) ? 'Upstream offers no safe cancellation; only task status is tracked.' : ''}
`; }).join(''); } function updateNavTransferBadge(st) { const badge = $('nav-transfer-badge'); if (!badge) return; const n = (st.activeCount || 0) + (st.queuedCount || 0); badge.hidden = n === 0; badge.textContent = n; } async function loadTransfers() { const st = await window.pork.transferState(); renderTransferPage(st); updateNavTransferBadge(st); } $('btn-transfer-pause').addEventListener('click', async () => { if (_transferState.paused) await window.pork.transferResume(); else await window.pork.transferPause(); }); $('btn-transfer-clear').addEventListener('click', () => window.pork.transferClearDone()); $('transfer-concurrent-slider').addEventListener('input', async event => { const n = parseInt(event.target.value); $('tstat-concurrent').textContent = n; await window.pork.transferSetConcurrent(n); }); $('transfers-list').addEventListener('click', event => { const retry = event.target.closest('.btn-retry-transfer'); if (retry) { window.pork.transferRetry?.(retry.dataset.jobId).catch(error => showToast(error.message, 'error')); return; } const cancel = event.target.closest('.btn-cancel-transfer'); if (cancel?.dataset.jobId) window.pork.transferCancel(cancel.dataset.jobId); }); $('btn-transfer-policy-save').addEventListener('click', async () => { try { await window.pork.transferSetPolicy?.({ windowStart: $('transfer-window-start').value || null, windowEnd: $('transfer-window-end').value || null, }); showToast(window.t('transfers.policy.saved', 'Transfer policy saved.'), 'ok'); } catch (error) { showToast(error.message, 'error'); } }); window.pork.on('transfer:update', state => { renderTransferPage(state); updateNavTransferBadge(state); }); window.addEventListener('chimera:language-changed', () => { if (document.querySelector('#page-transfers.active')) renderTransferPage(_transferState); });