'use strict'; // --------------------------------------------------------------------------- // Cloud import — browse a user-configured WebDAV server and pull a book in. // // The download is proxied by the server because the remote host is a different // origin and sends no CORS headers. It still lands here as raw bytes and goes // through _importEbookBuffer like any local file, so what gets stored is // ciphertext the server cannot read. // --------------------------------------------------------------------------- let _cloudSources = []; let _cloudSourceId = null; let _cloudPath = ''; async function toggleCloudImport() { const panel = $('cloud-browser'); if (!panel) return; if (panel.style.display !== 'none') { closeCloudImport(); return; } try { const res = await fetch('/books/cloud/sources/'); _cloudSources = (await res.json()).sources || []; } catch (e) { await customAlert('Cloud-Verbindungen konnten nicht geladen werden: ' + e.message); return; } if (!_cloudSources.length) { await customAlert( 'Noch keine Cloud-Verbindung eingerichtet. Unter Einstellungen → ' + '„Cloud-Verbindungen für Bücher“ kannst du eine hinzufügen.' ); return; } const select = $('cloud-source-select'); if (select) { select.innerHTML = ''; for (const source of _cloudSources) { const option = document.createElement('option'); option.value = source.id; option.textContent = source.label; select.appendChild(option); } select.style.display = _cloudSources.length > 1 ? '' : 'none'; } panel.style.display = ''; _cloudSourceId = _cloudSources[0].id; _cloudBrowse(''); } function closeCloudImport() { const panel = $('cloud-browser'); if (panel) panel.style.display = 'none'; } function _onCloudSourceChange(value) { _cloudSourceId = parseInt(value, 10); _cloudBrowse(''); } function _cloudParentPath(path) { const parts = path.split('/').filter(Boolean); parts.pop(); return parts.join('/'); } function _formatBytes(n) { if (!n) return ''; if (n < 1024) return n + ' B'; if (n < 1024 * 1024) return Math.round(n / 1024) + ' KB'; return (n / 1024 / 1024).toFixed(1) + ' MB'; } function _cloudEntryRow(label, onClick, sizeText) { const li = document.createElement('li'); li.className = 'cloud-entry'; const button = document.createElement('button'); button.type = 'button'; button.className = 'cloud-entry-name'; button.textContent = label; button.addEventListener('click', onClick); li.appendChild(button); if (sizeText) { const size = document.createElement('span'); size.className = 'cloud-entry-size'; size.textContent = sizeText; li.appendChild(size); } return li; } // Books chosen in this folder of the cloud (path -> entry), imported together. let _cloudChosen = new Map(); function _cloudChosenBar() { let bar = $('cloud-chosen'); const listEl = $('cloud-entries'); if (!bar && listEl) { bar = document.createElement('div'); bar.id = 'cloud-chosen'; bar.className = 'cloud-chosen'; listEl.parentNode.insertBefore(bar, listEl); } if (!bar) return; const books = Array.from(listEl.querySelectorAll('.cloud-check')); const n = _cloudChosen.size; bar.innerHTML = books.length ? `` + `` : ''; const all = bar.querySelector('.cloud-all input'); if (all) all.onchange = () => { books.forEach(c => { c.checked = all.checked; c.onchange(); }); }; const go = bar.querySelector('button'); if (go) go.onclick = () => _cloudImportChosen(); } async function _cloudImportChosen() { const entries = Array.from(_cloudChosen.values()); if (!entries.length) return; _cloudChosen = new Map(); const statusEl = $('cloud-status'); await _importMany(entries.map(entry => ({ name: entry.name, size: entry.size, buf: async () => { const res = await fetch(`/books/cloud/${_cloudSourceId}/fetch/?path=${encodeURIComponent(entry.path)}`); if (!res.ok) throw new Error('HTTP ' + res.status); return res.arrayBuffer(); }, })), statusEl); $('cloud-entries').querySelectorAll('.cloud-check').forEach(c => { c.checked = false; }); _cloudChosenBar(); } async function _cloudBrowse(path) { const listEl = $('cloud-entries'); const pathEl = $('cloud-path'); const statusEl = $('cloud-status'); if (!listEl) return; if (statusEl) statusEl.textContent = 'Lade…'; listEl.innerHTML = ''; _cloudChosen = new Map(); _cloudChosenBar(); let data; try { const res = await fetch(`/books/cloud/${_cloudSourceId}/browse/?path=${encodeURIComponent(path)}`); data = await res.json(); if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); } catch (e) { if (statusEl) statusEl.textContent = 'Fehler: ' + e.message; return; } _cloudPath = data.path || ''; if (pathEl) pathEl.textContent = '/' + _cloudPath; if (statusEl) statusEl.textContent = ''; if (_cloudPath) { listEl.appendChild(_cloudEntryRow('⬑ ..', () => _cloudBrowse(_cloudParentPath(_cloudPath)))); } const entries = (data.entries || []).filter(e => e.is_dir || e.is_book); if (!entries.length && statusEl) { statusEl.textContent = 'Keine Bücher oder Ordner in diesem Verzeichnis.'; } for (const entry of entries) { if (entry.is_dir) { listEl.appendChild(_cloudEntryRow('📁 ' + entry.name, () => _cloudBrowse(entry.path))); continue; } // A book: its name imports it alone, its box chooses it for importing several. const row = _cloudEntryRow('📖 ' + entry.name, () => _cloudImport(entry), _formatBytes(entry.size)); const box = document.createElement('input'); box.type = 'checkbox'; box.className = 'cloud-check'; box.setAttribute('aria-label', entry.name + ' auswählen'); box.onchange = () => { if (box.checked) _cloudChosen.set(entry.path, entry); else _cloudChosen.delete(entry.path); _cloudChosenBar(); }; row.prepend(box); listEl.appendChild(row); } _cloudChosenBar(); } async function _cloudImport(entry) { const statusEl = $('cloud-status'); if (statusEl) statusEl.textContent = `Lade „${entry.name}“…`; try { const res = await fetch(`/books/cloud/${_cloudSourceId}/fetch/?path=${encodeURIComponent(entry.path)}`); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || `HTTP ${res.status}`); } const buf = await res.arrayBuffer(); if (await _importEbookBuffer(buf, entry.name, statusEl) && statusEl) { statusEl.textContent = `✓ „${entry.name}“ importiert`; } } catch (e) { if (statusEl) statusEl.textContent = 'Import fehlgeschlagen: ' + e.message; } }