Files
chimera-Public/renderer/modules/transfers.js
T

217 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── 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 `
<div class="transfer-item transfer-item-${job.status}">
<div class="transfer-item-header">
<span class="transfer-label" title="${escHtml(job.localPath || job.remotePath || '')}">${escHtml(job.label)}</span>
<span class="transfer-type-badge ${typeClass}">${typeLabel}</span>
<span class="transfer-status-text">${statusText}</span>
${showRetry ? `<button class="btn btn-sm btn-retry-transfer" data-job-id="${escHtml(job.id)}">Retry</button>` : ''}
${showCancel ? `<button class="btn-cancel-transfer" data-job-id="${job.id}" title="Cancel">×</button>` : ''}
</div>
${showBar ? `
<div class="transfer-progress-wrap">
<div class="transfer-progress-bar">
<div class="transfer-progress-fill" style="width:${pct}%"></div>
</div>
${pctLabel ? `<span class="transfer-pct-label">${pctLabel}</span>` : ''}
</div>` : ''}
<div class="transfer-meta">
${speed ? `<span class="transfer-speed">${speed}</span>` : ''}
${file ? `<span class="transfer-file">${escHtml(file)}</span>` : ''}
${fileByteStr ? `<span class="transfer-sizes">${fileByteStr}</span>` : sizeStr ? `<span class="transfer-sizes">${sizeStr}</span>` : ''}
${job.error ? `<span class="transfer-error">${escHtml(job.error)}</span>` : ''}
${job.cancellable === false && ['queued', 'active', 'retrying'].includes(job.status) ? '<span class="transfer-error">Upstream offers no safe cancellation; only task status is tracked.</span>' : ''}
</div>
</div>`;
}).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);
});