import { api, onUnauthorized } from "./api.js"; import { element } from "./dom.js"; import { formatBytes } from "./format.js"; import { isActive, renderJobs, updateSpeeds } from "./jobs.js"; import { setupLogin } from "./login.js"; import { loadSettings, openSettings, setupSettings } from "./settings.js"; import { renderTree, topLevelSelection } from "./tree.js"; const pollIntervalMs = 1500; const elements = { loginView: document.getElementById("login-view"), loginForm: document.getElementById("login-form"), loginError: document.getElementById("login-error"), appView: document.getElementById("app-view"), logoutButton: document.getElementById("logout-button"), settingsButton: document.getElementById("settings-button"), diskStatus: document.getElementById("disk-status"), downloadForm: document.getElementById("download-form"), downloadUrl: document.getElementById("download-url"), autoExtract: document.getElementById("auto-extract"), downloadError: document.getElementById("download-error"), jobList: document.getElementById("job-list"), jobEmpty: document.getElementById("job-empty"), detail: document.getElementById("job-detail"), detailTitle: document.getElementById("detail-title"), closeDetailButton: document.getElementById("close-detail-button"), fileTree: document.getElementById("file-tree"), selectionSummary: document.getElementById("selection-summary"), librarySelect: document.getElementById("library-select"), subfolderInput: document.getElementById("subfolder-input"), cleanupCheckbox: document.getElementById("cleanup-checkbox"), moveButton: document.getElementById("move-button"), actionError: document.getElementById("action-error"), }; const state = { jobs: [], selectedJobId: null, treeStatus: null, selection: new Set(), pollTimer: null, refreshing: false, }; function showLogin() { stopPolling(); closeDetail(); elements.appView.hidden = true; elements.loginView.hidden = false; } async function showApp() { elements.loginView.hidden = true; elements.appView.hidden = false; applyDefaults(await loadSettings()); await loadLibraries(); await refresh(); startPolling(); } function applyDefaults(settings) { elements.autoExtract.checked = settings.autoExtract; elements.cleanupCheckbox.checked = settings.cleanupAfterMove; } async function showSettings() { try { await openSettings(); } catch (error) { window.alert(error.message); } } function startPolling() { stopPolling(); state.pollTimer = window.setInterval(refresh, pollIntervalMs); } function stopPolling() { if (state.pollTimer !== null) window.clearInterval(state.pollTimer); state.pollTimer = null; } async function loadLibraries() { const destinations = (await api.libraries()) || []; const previousValue = elements.librarySelect.value; const groups = new Map(); for (const destination of destinations) { if (!groups.has(destination.root)) { const group = document.createElement("optgroup"); group.label = destination.root; groups.set(destination.root, group); } const option = element("option", "", destination.name); option.value = destination.path; option.title = destination.path; groups.get(destination.root).append(option); } elements.librarySelect.replaceChildren(...groups.values()); if (destinations.some((destination) => destination.path === previousValue)) { elements.librarySelect.value = previousValue; } } async function refresh() { if (state.refreshing) return; state.refreshing = true; try { const [jobs, disk] = await Promise.all([api.jobs(), api.disk()]); state.jobs = jobs || []; updateSpeeds(state.jobs); elements.diskStatus.textContent = `${formatBytes(disk.availableBytes)} free`; renderJobList(); await syncDetail(); } catch (error) { if (error.status !== 401) console.error(error); } finally { state.refreshing = false; } } function renderJobList() { elements.jobEmpty.hidden = state.jobs.length > 0; renderJobs(elements.jobList, state.jobs, state.selectedJobId, { onBrowse: openDetail, onDelete: deleteJob, }); } function selectedJob() { return state.jobs.find((job) => job.id === state.selectedJobId); } function openDetail(job) { state.selectedJobId = job.id; state.treeStatus = null; state.selection.clear(); elements.actionError.textContent = ""; elements.detail.hidden = false; renderJobList(); syncDetail(); loadLibraries().catch((error) => { elements.actionError.textContent = error.message; }); elements.detail.scrollIntoView({ behavior: "smooth", block: "start" }); } function closeDetail() { state.selectedJobId = null; state.treeStatus = null; state.selection.clear(); elements.detail.hidden = true; elements.fileTree.replaceChildren(); } async function syncDetail() { const job = selectedJob(); if (!job) { if (state.selectedJobId) closeDetail(); return; } elements.detailTitle.textContent = job.fileName; if (job.status === state.treeStatus) return; state.treeStatus = job.status; if (job.status === "ready") { await loadTree(job.id); return; } state.selection.clear(); updateSelectionSummary(); const text = job.status === "completed" ? "Files were moved and temporary files were removed." : "Files will be available when this download is ready."; elements.fileTree.replaceChildren(element("p", "muted", text)); } async function loadTree(jobId) { try { const nodes = (await api.files(jobId)) || []; state.selection.clear(); renderTree(elements.fileTree, nodes, state.selection, { onSelectionChange: updateSelectionSummary, onExtract: extractArchive, }); updateSelectionSummary(); } catch (error) { elements.actionError.textContent = error.message; } } function updateSelectionSummary() { const count = topLevelSelection(state.selection).length; const job = selectedJob(); elements.selectionSummary.textContent = count === 0 ? "Nothing selected" : `${count} item(s) selected`; elements.moveButton.disabled = count === 0 || !job || job.status !== "ready"; } async function submitDownload(event) { event.preventDefault(); elements.downloadError.textContent = ""; try { await api.createJob(elements.downloadUrl.value.trim(), elements.autoExtract.checked); elements.downloadUrl.value = ""; await refresh(); } catch (error) { elements.downloadError.textContent = error.message; } } async function extractArchive(path) { elements.actionError.textContent = ""; try { await api.extract(state.selectedJobId, path); await refresh(); } catch (error) { elements.actionError.textContent = error.message; } } async function moveSelection() { elements.actionError.textContent = ""; const paths = topLevelSelection(state.selection); if (paths.length === 0) return; try { await api.move(state.selectedJobId, { paths, destination: elements.librarySelect.value, subfolder: elements.subfolderInput.value.trim(), cleanup: elements.cleanupCheckbox.checked, }); elements.subfolderInput.value = ""; await refresh(); } catch (error) { elements.actionError.textContent = error.message; } } async function deleteJob(job) { const question = isActive(job) ? `Cancel and remove "${job.fileName}"?` : `Remove "${job.fileName}" and its temporary files?`; if (!window.confirm(question)) return; try { await api.remove(job.id); if (job.id === state.selectedJobId) closeDetail(); await refresh(); } catch (error) { window.alert(error.message); } } async function logout() { try { await api.logout(); } finally { showLogin(); } } async function init() { onUnauthorized(showLogin); setupLogin(elements.loginForm, elements.loginError, showApp); elements.downloadForm.addEventListener("submit", submitDownload); elements.moveButton.addEventListener("click", moveSelection); elements.closeDetailButton.addEventListener("click", () => { closeDetail(); renderJobList(); }); elements.logoutButton.addEventListener("click", logout); elements.settingsButton.addEventListener("click", showSettings); setupSettings((settings) => { applyDefaults(settings); refresh(); }); try { const session = await api.session(); if (session && session.authenticated) await showApp(); else showLogin(); } catch { showLogin(); } } init();