217 lines
9.5 KiB
JavaScript
217 lines
9.5 KiB
JavaScript
// ── 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);
|
||
});
|
||
|