first commit
This commit is contained in:
commit
58cfd95600
35 files changed
+5357
No files matched your search
@@ -0,0 +1,577 @@
|
||||
'use strict';
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const NS = 'http://www.w3.org/2000/svg';
|
||||
const UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||
const ACTIVE = new Set(['downloading', 'preparing', 'extracting', 'converting', 'vimm_browser']);
|
||||
const LABELS = {
|
||||
queued: 'W kolejce', downloading: 'Pobieranie', paused: 'Wstrzymano',
|
||||
preparing: 'Przygotowanie', extracting: 'Rozpakowanie', converting: 'Konwersja',
|
||||
done: 'Gotowe', error: 'Błąd', cancelled: 'Anulowano', vimm_browser: 'Vimm'
|
||||
};
|
||||
const S = { archiveId: '', archiveTitle: '', archiveFiles: [], selected: new Set(),
|
||||
jobs: [], filter: 'all', connected: false, hostRoot: '/xbox', magnetPreview: null,
|
||||
magnetFiles: [], magnetSelected: new Set(), magnetName: '', magnetTitle: '', magnetFormat: 'xex' };
|
||||
|
||||
function normalizeMagnet(raw) {
|
||||
return raw.trim()
|
||||
.replace(/^[\'"`]*\*{0,2}magnet\*{0,2}:\?/i, 'magnet:?')
|
||||
.replace(/&|&|&/gi, '&')
|
||||
.replace(/\\\s*/g, '')
|
||||
.replace(/\s+/g, '')
|
||||
.replace(/[\'"`]+$/g, '');
|
||||
}
|
||||
|
||||
function bytes(n) {
|
||||
n = Math.max(0, Number(n) || 0);
|
||||
let i = 0;
|
||||
while (n >= 1024 && i < UNITS.length - 1) { n /= 1024; i++; }
|
||||
return (i ? n.toFixed(1) : n.toFixed(0)) + ' ' + UNITS[i];
|
||||
}
|
||||
|
||||
function el(tag, cls, text) {
|
||||
const node = document.createElement(tag);
|
||||
if (cls) node.className = cls;
|
||||
if (text !== undefined && text !== null) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function icon(name) {
|
||||
const svg = document.createElementNS(NS, 'svg');
|
||||
svg.setAttribute('class', 'icon');
|
||||
svg.setAttribute('aria-hidden', 'true');
|
||||
const use = document.createElementNS(NS, 'use');
|
||||
use.setAttribute('href', '#i-' + name);
|
||||
svg.append(use);
|
||||
return svg;
|
||||
}
|
||||
|
||||
function makeButton(label, glyph, cls = 'secondary') {
|
||||
const button = el('button', cls);
|
||||
button.type = 'button';
|
||||
button.append(icon(glyph), document.createTextNode(label));
|
||||
return button;
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const response = await fetch('/api' + path, {
|
||||
...options,
|
||||
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }
|
||||
});
|
||||
let result;
|
||||
try { result = await response.json(); }
|
||||
catch { result = { detail: 'Nieprawidłowa odpowiedź serwera' }; }
|
||||
if (!response.ok) {
|
||||
throw new Error(typeof result.detail === 'string' ? result.detail : JSON.stringify(result.detail || result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function connection(online) {
|
||||
S.connected = online;
|
||||
$('connectionState').classList.toggle('online', online);
|
||||
$('connectionState').classList.toggle('offline', !online);
|
||||
$('connectionText').textContent = online ? 'Połączono' : 'Brak połączenia';
|
||||
}
|
||||
|
||||
function flash(message, bad = false) {
|
||||
const t = $('toast');
|
||||
t.textContent = message;
|
||||
t.classList.toggle('bad', bad);
|
||||
t.style.display = 'block';
|
||||
clearTimeout(window._toastTimeout);
|
||||
window._toastTimeout = setTimeout(() => { t.style.display = 'none'; }, 5000);
|
||||
}
|
||||
|
||||
function filterJobs(jobs) {
|
||||
if (S.filter === 'active') return jobs.filter(j => !['done', 'error', 'cancelled'].includes(j.state));
|
||||
if (S.filter === 'done') return jobs.filter(j => j.state === 'done');
|
||||
if (S.filter === 'error') return jobs.filter(j => j.state === 'error');
|
||||
return jobs;
|
||||
}
|
||||
|
||||
function sourceDescription(job) {
|
||||
if (job.source_type === 'archive_many') {
|
||||
try { return 'Internet Archive · ' + JSON.parse(job.source).length + ' plików'; }
|
||||
catch { return 'Internet Archive'; }
|
||||
}
|
||||
if (job.source_type === 'import') return 'Import · ' + job.source;
|
||||
if (job.source_type === 'magnet') return 'Magnet';
|
||||
if (job.source_type === 'torrent_selected') {
|
||||
try { return 'Torrent · ' + JSON.parse(job.source).count + ' plików'; }
|
||||
catch { return 'Torrent'; }
|
||||
}
|
||||
if (job.source_type === 'vimm') return 'Vimm · ' + job.source;
|
||||
return job.source;
|
||||
}
|
||||
|
||||
function jobAction(label, glyph, action, job, danger = false) {
|
||||
const button = makeButton(label, glyph, 'action' + (danger ? ' danger' : ''));
|
||||
button.addEventListener('click', async () => {
|
||||
if (action === 'remove' && !confirm('Usunąć wpis? Pobrane pliki zostaną na dysku.')) return;
|
||||
if (action === 'cancel' && !confirm('Anulować to zadanie?')) return;
|
||||
button.disabled = true;
|
||||
try {
|
||||
await api('/jobs/' + encodeURIComponent(job.id) + '/' + action, { method: 'POST' });
|
||||
await refreshJobs();
|
||||
} catch (e) { flash(e.message, true); }
|
||||
finally { button.disabled = false; }
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
async function showLog(job) {
|
||||
try {
|
||||
const response = await fetch('/api/jobs/' + encodeURIComponent(job.id) + '/log');
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
$('logContent').textContent = await response.text();
|
||||
$('logDialog').showModal();
|
||||
} catch (e) { flash(e.message, true); }
|
||||
}
|
||||
|
||||
function hostPath(path) {
|
||||
if (typeof path !== 'string') return '';
|
||||
if (path === '/data/raw') return S.hostRoot + '/downloads';
|
||||
if (path.startsWith('/data/raw/')) return S.hostRoot + '/downloads/' + path.slice('/data/raw/'.length);
|
||||
if (path === '/data') return S.hostRoot;
|
||||
if (path.startsWith('/data/')) return S.hostRoot + '/' + path.slice('/data/'.length);
|
||||
return path;
|
||||
}
|
||||
|
||||
function jobRow(job) {
|
||||
const card = el('article', 'job');
|
||||
const header = el('div', 'job-top');
|
||||
const heading = el('div', 'job-heading');
|
||||
heading.append(el('div', 'job-name', job.title), el('div', 'job-sub', sourceDescription(job)));
|
||||
header.append(heading);
|
||||
const statusClass = job.state === 'done' ? 'done' : job.state === 'error' ? 'error' : ACTIVE.has(job.state) ? 'running' : '';
|
||||
header.append(el('span', 'job-status ' + statusClass, LABELS[job.state] || job.state));
|
||||
card.append(header);
|
||||
|
||||
const pct = Math.max(0, Math.min(100, Number(job.progress) || 0));
|
||||
const unknown = job.state === 'vimm_browser';
|
||||
const progress = el('div', 'job-progress');
|
||||
const track = el('div', 'progressbar' + (job.state === 'error' ? ' is-error' : ''));
|
||||
const fill = el('span');
|
||||
fill.style.width = pct + '%';
|
||||
track.append(fill);
|
||||
progress.append(track, el('span', 'progress-text', unknown ? '—' : pct.toFixed(1) + '%'));
|
||||
card.append(progress);
|
||||
|
||||
const meta = el('div', 'job-meta');
|
||||
meta.append(el('span', 'note-text', job.note || 'Oczekiwanie'));
|
||||
meta.append(el('span', 'speed', Number(job.speed) > 0 ? bytes(job.speed) + '/s' : ''));
|
||||
card.append(meta);
|
||||
|
||||
if (job.state === 'error' && job.error) card.append(el('div', 'job-error', job.error));
|
||||
if (job.output) card.append(el('div', 'job-output', 'Zapisano: ' + hostPath(job.output)));
|
||||
|
||||
const bottom = el('div', 'job-bottom');
|
||||
const chips = el('div', 'job-chips');
|
||||
chips.append(el('span', 'job-chip', (job.format || 'raw').toUpperCase()));
|
||||
bottom.append(chips);
|
||||
const actions = el('div', 'job-actions');
|
||||
if (['queued', 'downloading'].includes(job.state)) {
|
||||
actions.append(jobAction('Pauza', 'pause', 'pause', job), jobAction('Anuluj', 'x', 'cancel', job, true));
|
||||
}
|
||||
if (job.state === 'paused') {
|
||||
actions.append(jobAction('Wznów', 'play', 'resume', job), jobAction('Anuluj', 'x', 'cancel', job, true));
|
||||
}
|
||||
if (['error', 'cancelled'].includes(job.state)) actions.append(jobAction('Ponów', 'retry', 'retry', job));
|
||||
if (['error', 'done', 'cancelled', 'paused'].includes(job.state)) actions.append(jobAction('Usuń', 'trash', 'remove', job, true));
|
||||
const log = makeButton('Log', 'file', 'action');
|
||||
log.addEventListener('click', () => showLog(job));
|
||||
actions.append(log);
|
||||
bottom.append(actions);
|
||||
card.append(bottom);
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderJobs() {
|
||||
$('jobsCount').textContent = String(S.jobs.length);
|
||||
$('countQueued').textContent = String(S.jobs.filter(j => j.state === 'queued' || j.state === 'paused').length);
|
||||
$('countActive').textContent = String(S.jobs.filter(j => ACTIVE.has(j.state)).length);
|
||||
$('countDone').textContent = String(S.jobs.filter(j => j.state === 'done').length);
|
||||
const list = $('jobsList');
|
||||
list.replaceChildren();
|
||||
const filtered = filterJobs(S.jobs);
|
||||
if (!filtered.length) {
|
||||
list.append(el('div', 'empty-state', S.filter === 'all' ? 'Brak zadań. Dodaj link powyżej.' : 'Brak zadań w tym widoku.'));
|
||||
return;
|
||||
}
|
||||
for (const job of filtered) list.append(jobRow(job));
|
||||
}
|
||||
|
||||
async function refreshJobs(showError = false) {
|
||||
try {
|
||||
S.jobs = await api('/jobs');
|
||||
renderJobs();
|
||||
connection(true);
|
||||
} catch (e) {
|
||||
connection(false);
|
||||
if (showError) flash('Nie można odczytać kolejki: ' + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSystem() {
|
||||
try {
|
||||
const info = await api('/system');
|
||||
$('storageNum').textContent = bytes(info.free);
|
||||
const percent = info.total > 0 ? (100 * (1 - info.free / info.total)) : 0;
|
||||
$('storageFill').style.width = Math.max(0, Math.min(100, percent)) + '%';
|
||||
$('storageText').textContent = bytes(info.total) + ' łącznie';
|
||||
const hostRoot = String(info.host_data_dir || '/xbox').replace(/\/+$/, '');
|
||||
S.hostRoot = hostRoot;
|
||||
$('importHostPath').textContent = hostRoot + '/imports/';
|
||||
$('readyXexPath').textContent = hostRoot + '/ready/xex/';
|
||||
$('readyGodPath').textContent = hostRoot + '/ready/god/';
|
||||
$('downloadHostPath').textContent = hostRoot + '/downloads/';
|
||||
$('workHostPath').textContent = hostRoot + '/work/';
|
||||
connection(true);
|
||||
} catch {
|
||||
$('storageText').textContent = 'Nie udało się odczytać dysku';
|
||||
connection(false);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tab of document.querySelectorAll('.tab')) {
|
||||
tab.addEventListener('click', () => {
|
||||
for (const other of document.querySelectorAll('.tab')) {
|
||||
const selected = other === tab;
|
||||
other.classList.toggle('active', selected);
|
||||
other.setAttribute('aria-selected', String(selected));
|
||||
}
|
||||
for (const name of ['links', 'archive', 'import']) {
|
||||
$('pane-' + name).classList.toggle('hidden', name !== tab.dataset.tab);
|
||||
}
|
||||
if (tab.dataset.tab === 'import') refreshImports();
|
||||
});
|
||||
}
|
||||
|
||||
for (const filter of document.querySelectorAll('.filter')) {
|
||||
filter.addEventListener('click', () => {
|
||||
S.filter = filter.dataset.filter;
|
||||
for (const other of document.querySelectorAll('.filter')) {
|
||||
const selected = other === filter;
|
||||
other.classList.toggle('active', selected);
|
||||
other.setAttribute('aria-pressed', String(selected));
|
||||
}
|
||||
renderJobs();
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshArchiveSession() {
|
||||
try {
|
||||
const result = await api('/archive/session');
|
||||
$('archiveSessionText').textContent = result.authenticated ? ('Sesja zapisana · ' + result.email) : 'Niezalogowano';
|
||||
$('archiveLoginForm').classList.toggle('hidden', result.authenticated);
|
||||
$('archiveLogout').classList.toggle('hidden', !result.authenticated);
|
||||
} catch (e) { $('archiveSessionText').textContent = 'Błąd sprawdzania sesji'; }
|
||||
}
|
||||
|
||||
$('archiveLoginForm').addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const button = $('archiveLoginForm').querySelector('[type=submit]');
|
||||
button.disabled = true;
|
||||
try {
|
||||
const result = await api('/archive/session', { method: 'POST', body: JSON.stringify({
|
||||
email: $('archiveEmail').value.trim(), password: $('archivePassword').value
|
||||
}) });
|
||||
$('archivePassword').value = '';
|
||||
flash('Zapisano sesję Archive.org: ' + result.email);
|
||||
await refreshArchiveSession();
|
||||
} catch (e) { $('archivePassword').value = ''; flash('Archive.org: ' + e.message, true); }
|
||||
finally { button.disabled = false; }
|
||||
});
|
||||
|
||||
$('archiveLogout').addEventListener('click', async () => {
|
||||
try {
|
||||
await api('/archive/session', { method: 'DELETE' });
|
||||
await refreshArchiveSession();
|
||||
flash('Usunięto zapisaną sesję Archive.org.');
|
||||
} catch (e) { flash(e.message, true); }
|
||||
});
|
||||
|
||||
function renderMagnet() {
|
||||
const list = $('magnetFiles');
|
||||
list.replaceChildren();
|
||||
const text = $('magnetFilter').value.toLowerCase();
|
||||
let displayed = 0;
|
||||
for (const file of S.magnetFiles) {
|
||||
if (!file.name.toLowerCase().includes(text)) continue;
|
||||
if (displayed >= 500) break;
|
||||
displayed++;
|
||||
const row = el('label', 'file-row');
|
||||
const box = el('input');
|
||||
box.type = 'checkbox';
|
||||
box.checked = S.magnetSelected.has(file.index);
|
||||
box.disabled = Boolean(file.padding);
|
||||
box.addEventListener('change', () => {
|
||||
if (box.checked) S.magnetSelected.add(file.index);
|
||||
else S.magnetSelected.delete(file.index);
|
||||
$('magnetSelectedCount').textContent = 'Wybrano: ' + S.magnetSelected.size;
|
||||
});
|
||||
row.append(box, el('span', 'file-name', file.name),
|
||||
el('span', 'size', file.padding ? 'Systemowy' : bytes(file.size)));
|
||||
list.append(row);
|
||||
}
|
||||
if (!displayed) list.append(el('div', 'empty-state', 'Brak plików.'));
|
||||
if (displayed >= 500) list.append(el('div', 'empty-state', 'Pierwsze 500 plików. Zawęź filtr, aby wyszukać kolejne.'));
|
||||
$('magnetSelectedCount').textContent = 'Wybrano: ' + S.magnetSelected.size;
|
||||
}
|
||||
|
||||
async function pollMagnet(previewId) {
|
||||
if (S.magnetPreview !== previewId || !$('magnetDialog').open) return;
|
||||
try {
|
||||
const data = await api('/magnet/inspect/' + encodeURIComponent(previewId));
|
||||
if (S.magnetPreview !== previewId || !$('magnetDialog').open) return;
|
||||
if (data.status === 'ready') {
|
||||
S.magnetName = data.name;
|
||||
S.magnetFiles = data.files;
|
||||
S.magnetSelected = new Set((data.suggested || []).filter(i => !data.files.find(f => f.index === i)?.padding));
|
||||
$('magnetName').textContent = data.name + ' (' + data.files.length + ' plików)';
|
||||
$('magnetStatus').textContent = 'Metadane pobrane. Wybierz pliki.';
|
||||
$('magnetResults').classList.remove('hidden');
|
||||
renderMagnet();
|
||||
return;
|
||||
}
|
||||
if (data.status === 'loading') {
|
||||
const seconds = Number(data.elapsed) || 0;
|
||||
const connections = Number(data.connections) || 0;
|
||||
const known = Number(data.known_peers) || 0;
|
||||
let notice = 'Metadane: ' + seconds + ' s | połączenia: ' + connections + ' | znani peerzy: ' + known;
|
||||
if (seconds >= 25 && connections === 0)
|
||||
notice += ' | Brak połączeń. Sprawdź UDP/DHT lub wczytaj .torrent.';
|
||||
$('magnetStatus').textContent = notice;
|
||||
}
|
||||
if (data.status === 'error') {
|
||||
$('magnetStatus').textContent = 'Błąd: ' + data.error;
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
$('magnetStatus').textContent = 'Błąd: ' + e.message;
|
||||
return;
|
||||
}
|
||||
setTimeout(() => pollMagnet(previewId), 3000);
|
||||
}
|
||||
|
||||
async function inspectMagnet(magnet, title, format) {
|
||||
S.magnetPreview = null;
|
||||
S.magnetTitle = title;
|
||||
S.magnetFormat = format;
|
||||
S.magnetFiles = [];
|
||||
S.magnetSelected.clear();
|
||||
$('magnetStatus').textContent = 'Oczekiwanie na metadane z sieci BitTorrent…';
|
||||
$('magnetResults').classList.add('hidden');
|
||||
$('magnetFilter').value = '';
|
||||
$('magnetDialog').showModal();
|
||||
try {
|
||||
const data = await api('/magnet/inspect', { method: 'POST', body: JSON.stringify({ magnet }) });
|
||||
if (!$('magnetDialog').open) return;
|
||||
S.magnetPreview = data.preview_id;
|
||||
pollMagnet(data.preview_id);
|
||||
} catch (e) { $('magnetStatus').textContent = 'Błąd: ' + e.message; }
|
||||
}
|
||||
|
||||
async function uploadTorrent(file, fromLinks) {
|
||||
if (!file) return;
|
||||
if (file.size > 32 * 1024 * 1024) return flash('Limit pliku .torrent: 32 MiB.', true);
|
||||
if (fromLinks) {
|
||||
S.magnetTitle = $('linkTitle').value.trim();
|
||||
S.magnetFormat = $('linkFormat').value;
|
||||
}
|
||||
const previous = S.magnetPreview;
|
||||
S.magnetPreview = null;
|
||||
S.magnetFiles = [];
|
||||
S.magnetSelected.clear();
|
||||
$('magnetResults').classList.add('hidden');
|
||||
$('magnetFilter').value = '';
|
||||
$('magnetStatus').textContent = 'Wczytywanie pliku .torrent...';
|
||||
if (!$('magnetDialog').open) $('magnetDialog').showModal();
|
||||
if (previous) await api('/magnet/inspect/' + encodeURIComponent(previous) + '/cancel', { method: 'POST' }).catch(() => {});
|
||||
try {
|
||||
const response = await fetch('/api/magnet/upload', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/x-bittorrent' }, body: file
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.detail || 'HTTP ' + response.status);
|
||||
if (!$('magnetDialog').open) return;
|
||||
S.magnetPreview = data.preview_id;
|
||||
await pollMagnet(data.preview_id);
|
||||
} catch (error) {
|
||||
$('magnetStatus').textContent = 'Błąd: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of ['torrentFileLinks', 'torrentFileDialog']) {
|
||||
$(id).addEventListener('change', async (event) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = '';
|
||||
await uploadTorrent(file, id === 'torrentFileLinks');
|
||||
});
|
||||
}
|
||||
|
||||
$('magnetFilter').addEventListener('input', renderMagnet);
|
||||
$('magnetToggleAll').addEventListener('click', () => {
|
||||
const visible = S.magnetFiles.filter(f => !f.padding && f.name.toLowerCase().includes($('magnetFilter').value.toLowerCase())).slice(0, 500);
|
||||
const allSelected = visible.every(f => S.magnetSelected.has(f.index));
|
||||
for (const file of visible) {
|
||||
if (allSelected) S.magnetSelected.delete(file.index);
|
||||
else S.magnetSelected.add(file.index);
|
||||
}
|
||||
renderMagnet();
|
||||
});
|
||||
$('closeMagnet').addEventListener('click', () => $('magnetDialog').close());
|
||||
$('magnetDialog').addEventListener('close', () => {
|
||||
const preview = S.magnetPreview;
|
||||
S.magnetPreview = null;
|
||||
if (preview) api('/magnet/inspect/' + encodeURIComponent(preview) + '/cancel', { method: 'POST' }).catch(() => {});
|
||||
});
|
||||
$('magnetAdd').addEventListener('click', async () => {
|
||||
if (!S.magnetSelected.size) return flash('Zaznacz co najmniej jeden plik.', true);
|
||||
const button = $('magnetAdd');
|
||||
button.disabled = true;
|
||||
try {
|
||||
await api('/magnet/jobs', { method: 'POST', body: JSON.stringify({
|
||||
preview_id: S.magnetPreview, files: [...S.magnetSelected],
|
||||
title: S.magnetTitle, format: S.magnetFormat
|
||||
}) });
|
||||
$('urls').value = '';
|
||||
S.magnetPreview = null;
|
||||
$('magnetDialog').close();
|
||||
flash('Wybrane pliki dodano do kolejki.');
|
||||
await refreshJobs();
|
||||
} catch (e) { flash(e.message, true); }
|
||||
finally { button.disabled = false; }
|
||||
});
|
||||
|
||||
$('linksForm').addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const raw = $('urls').value.trim();
|
||||
if (!raw) return flash('Podaj co najmniej jeden link.', true);
|
||||
const button = $('linksForm').querySelector('[type=submit]');
|
||||
const title = $('linkTitle').value.trim();
|
||||
const format = $('linkFormat').value;
|
||||
if (/^[\'"`]*\*{0,2}magnet\*{0,2}:\?/i.test(raw)) {
|
||||
const magnet = normalizeMagnet(raw);
|
||||
if ((magnet.match(/magnet:\?/gi) || []).length !== 1)
|
||||
return flash('Magnet wklej pojedynczo, aby wybrać pliki.', true);
|
||||
await inspectMagnet(magnet, title, format);
|
||||
return;
|
||||
}
|
||||
const items = raw.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
||||
let ok = 0;
|
||||
const errors = [];
|
||||
button.disabled = true;
|
||||
try {
|
||||
for (const url of items) {
|
||||
try { await api('/jobs', { method: 'POST', body: JSON.stringify({ url, title, format }) }); ok++; }
|
||||
catch (e) { errors.push(url.slice(0, 65) + ': ' + e.message); }
|
||||
}
|
||||
if (ok) {
|
||||
$('urls').value = '';
|
||||
flash('Dodano zadań: ' + ok);
|
||||
await refreshJobs();
|
||||
}
|
||||
if (errors.length) flash(errors.join('\n'), true);
|
||||
} finally { button.disabled = false; }
|
||||
});
|
||||
|
||||
$('archiveForm').addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const button = $('archiveForm').querySelector('[type=submit]');
|
||||
button.disabled = true;
|
||||
$('archiveResults').classList.add('hidden');
|
||||
try {
|
||||
const data = await api('/archive/lookup', { method: 'POST', body: JSON.stringify({ identifier: $('archiveId').value }) });
|
||||
S.archiveId = data.identifier;
|
||||
S.archiveTitle = data.title;
|
||||
S.archiveFiles = data.files;
|
||||
S.selected.clear();
|
||||
$('archiveName').textContent = data.title + ' (' + data.files.length + ')';
|
||||
$('archiveResults').classList.remove('hidden');
|
||||
renderArchive();
|
||||
} catch (e) { flash('Internet Archive: ' + e.message, true); }
|
||||
finally { button.disabled = false; }
|
||||
});
|
||||
|
||||
function renderArchive() {
|
||||
const list = $('archiveFiles');
|
||||
list.replaceChildren();
|
||||
const text = $('archiveFilter').value.toLowerCase();
|
||||
let displayed = 0;
|
||||
for (const file of S.archiveFiles) {
|
||||
if (!file.name.toLowerCase().includes(text)) continue;
|
||||
if (displayed >= 400) break;
|
||||
displayed++;
|
||||
const row = el('label', 'file-row');
|
||||
const checkbox = el('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.checked = S.selected.has(file.name);
|
||||
checkbox.addEventListener('change', () => {
|
||||
if (checkbox.checked) S.selected.add(file.name);
|
||||
else S.selected.delete(file.name);
|
||||
$('selectedCount').textContent = 'Wybrano: ' + S.selected.size;
|
||||
});
|
||||
row.append(checkbox, el('span', 'file-name', file.name), el('span', 'size', bytes(file.size)));
|
||||
list.append(row);
|
||||
}
|
||||
if (!displayed) list.append(el('div', 'empty-state', 'Brak plików.'));
|
||||
$('selectedCount').textContent = 'Wybrano: ' + S.selected.size;
|
||||
}
|
||||
|
||||
$('archiveFilter').addEventListener('input', renderArchive);
|
||||
$('archiveAdd').addEventListener('click', async () => {
|
||||
if (!S.selected.size) return flash('Zaznacz pliki.', true);
|
||||
const button = $('archiveAdd');
|
||||
button.disabled = true;
|
||||
try {
|
||||
await api('/archive/jobs', { method: 'POST', body: JSON.stringify({
|
||||
identifier: S.archiveId, files: [...S.selected], title: S.archiveTitle, format: $('archiveFormat').value
|
||||
}) });
|
||||
S.selected.clear();
|
||||
renderArchive();
|
||||
flash('Dodano do kolejki.');
|
||||
await refreshJobs();
|
||||
} catch (e) { flash(e.message, true); }
|
||||
finally { button.disabled = false; }
|
||||
});
|
||||
|
||||
async function refreshImports() {
|
||||
try {
|
||||
const files = await api('/imports');
|
||||
const list = $('importList');
|
||||
list.replaceChildren();
|
||||
if (!files.length) {
|
||||
list.append(el('div', 'empty-state', 'Katalog data/imports/ jest pusty.'));
|
||||
return;
|
||||
}
|
||||
for (const file of files) {
|
||||
const row = el('div', 'file-row');
|
||||
row.append(icon('file'), el('span', 'file-name', file.name), el('span', 'size', bytes(file.size)));
|
||||
const button = makeButton('Przetwórz', 'play');
|
||||
button.addEventListener('click', async () => {
|
||||
button.disabled = true;
|
||||
try {
|
||||
await api('/imports/jobs', { method: 'POST', body: JSON.stringify({
|
||||
filename: file.name, format: $('importFormat').value
|
||||
}) });
|
||||
flash('Dodano: ' + file.name);
|
||||
await refreshJobs();
|
||||
} catch (e) { flash(e.message, true); }
|
||||
finally { button.disabled = false; }
|
||||
});
|
||||
row.append(button);
|
||||
list.append(row);
|
||||
}
|
||||
} catch (e) { flash('Import: ' + e.message, true); }
|
||||
}
|
||||
|
||||
$('refreshImports').addEventListener('click', refreshImports);
|
||||
$('refreshJobs').addEventListener('click', () => refreshJobs(true));
|
||||
$('closeLog').addEventListener('click', () => $('logDialog').close());
|
||||
refreshJobs();
|
||||
refreshSystem();
|
||||
refreshArchiveSession();
|
||||
setInterval(() => refreshJobs(), 3000);
|
||||
setInterval(refreshSystem, 30000);
|
||||
Reference in new issue
Block a user