// Страница «Синхронизация»: токен + Скачать. // Скачивание: GET /download/{id} -> zstd -> проверка SQLite -> запись в OPFS. import { TOKEN_LENGTH, isValidToken, loadSavedToken, normalizeToken, saveToken, } from './token.ts'; import { fetchBackupBytes } from './sync.ts'; import { decompressBackup } from './zstd.ts'; import { writeDatabaseFile } from './opfs.ts'; export function renderSyncPage(root: HTMLElement): void { root.innerHTML = `

Синхронизация

Токен сохранения — 24 символа: латинские буквы и цифры.

0 / 24

Введите токен из 24 символов

`; const tokenInput = root.querySelector('#token-input')!; const tokenError = root.querySelector('#token-error')!; const tokenCounter = root.querySelector('#token-counter')!; const btnDownload = root.querySelector('#btn-download')!; const statusEl = root.querySelector('#status')!; function setStatus(msg: string): void { statusEl.textContent = msg; console.log(`[JapLearn] ${msg}`); } function currentToken(): string { return normalizeToken(tokenInput.value); } function setBusy(busy: boolean): void { btnDownload.disabled = busy || !isValidToken(currentToken()); tokenInput.disabled = busy; } function refreshUI(): void { const token = currentToken(); const len = token.length; tokenCounter.textContent = `${len} / ${TOKEN_LENGTH}`; const ok = isValidToken(token); const empty = len === 0; tokenInput.classList.toggle('invalid', !empty && !ok); tokenInput.setAttribute('aria-invalid', String(!empty && !ok)); if (empty) { tokenError.textContent = ''; } else if (len !== TOKEN_LENGTH) { tokenError.textContent = `Нужно 24 символа, сейчас ${len}`; } else { tokenError.textContent = ok ? '' : 'Только латинские буквы и цифры'; } btnDownload.disabled = !ok; } async function handleDownload(): Promise { const token = currentToken(); if (!isValidToken(token)) return; saveToken(token); setBusy(true); setStatus('Скачивание сохранения...'); try { const compressed = await fetchBackupBytes(token); setStatus(`Скачано ${(compressed.length / 1024).toFixed(1)} КБ, распаковка...`); const raw = await decompressBackup(compressed); setStatus(`Распаковано ${(raw.length / 1024).toFixed(1)} КБ, сохранение в OPFS...`); await writeDatabaseFile(raw); setStatus(`Готово: база ${(raw.length / 1024).toFixed(1)} КБ сохранена локально (OPFS).`); } catch (err) { setStatus(`Ошибка скачивания: ${err instanceof Error ? err.message : String(err)}`); } finally { setBusy(false); refreshUI(); } } tokenInput.addEventListener('input', () => { refreshUI(); }); tokenInput.addEventListener('change', () => { const token = currentToken(); tokenInput.value = token; if (isValidToken(token)) { saveToken(token); setStatus('Токен сохранён локально. Можно скачивать.'); } refreshUI(); }); btnDownload.addEventListener('click', () => { void handleDownload(); }); const saved = loadSavedToken(); if (saved) { tokenInput.value = saved; } refreshUI(); if (isValidToken(normalizeToken(saved))) { setStatus('Токен восстановлен. Нажми «Скачать», чтобы получить базу.'); } }