import { renderHeader } from '../components/header.js'; import { authService, authorizeSession, clearAuthMessages, clearBrowserClientData, refreshSessions, setAuthBusy, setAuthError, setAuthInfo, state, terminateCurrentSession, } from '../state.js'; import { clearClientAuthData, saveEncryptedUserSecrets } from '../services/key-vault.js'; import { clearStoredMessages } from '../services/message-store.js'; import { describeTransferKeys, parseKeyTransferText, } from '../services/qr-key-transfer-service.js'; import { toUserMessage } from '../services/ui-error-texts.js'; export const pageMeta = { id: 'login-camera-view', title: 'Войти по QR-коду', showAppChrome: false }; function canUseBarcodeDetector() { return typeof window.BarcodeDetector === 'function'; } async function createQrDetector() { if (!canUseBarcodeDetector()) return null; try { const formats = await window.BarcodeDetector.getSupportedFormats?.(); if (Array.isArray(formats) && !formats.includes('qr_code')) return null; } catch { // Некоторые браузеры не реализуют getSupportedFormats, но сам detector работает. } return new window.BarcodeDetector({ formats: ['qr_code'] }); } function setStatus(statusEl, message, kind = 'info') { statusEl.classList.toggle('is-unavailable', kind === 'error'); statusEl.classList.toggle('is-available', kind !== 'error'); statusEl.textContent = message; statusEl.style.display = message ? '' : 'none'; } function escapeHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function renderParsedTransfer(resultEl, transfer) { const keys = describeTransferKeys(transfer.keys); resultEl.innerHTML = `

Отсканированный логин: ${escapeHtml(transfer.login)}

Получены ключи: ${escapeHtml(keys.join(', '))}

Войти под этим логином и очистить локальную историю старого логина?

`; } export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; clearAuthMessages(); const frame = document.createElement('div'); frame.className = 'camera-shell'; frame.innerHTML = `
Наведите QR-код переноса ключей в рамку
`; const manualCard = document.createElement('details'); manualCard.className = 'card stack'; manualCard.innerHTML = ` Ввести QR-текст вручную `; const resultCard = document.createElement('div'); resultCard.className = 'card stack'; resultCard.style.display = 'none'; resultCard.innerHTML = `
`; const status = document.createElement('p'); status.className = 'status-line is-unavailable'; status.style.display = 'none'; const backButton = document.createElement('button'); backButton.className = 'ghost-btn'; backButton.type = 'button'; backButton.textContent = 'Назад'; const video = frame.querySelector('video'); const cameraError = frame.querySelector('#login-camera-error'); const manualInput = manualCard.querySelector('#login-qr-manual'); const parseManualButton = manualCard.querySelector('#login-qr-manual-parse'); const resultEl = resultCard.querySelector('#login-qr-result'); const cancelButton = resultCard.querySelector('#login-qr-cancel'); const confirmButton = resultCard.querySelector('#login-qr-confirm'); let stream = null; let detector = null; let scanTimer = 0; let scannedTransfer = null; let stopped = false; const stopCamera = () => { stopped = true; if (scanTimer) { window.clearTimeout(scanTimer); scanTimer = 0; } if (stream) { stream.getTracks().forEach((track) => track.stop()); stream = null; } }; const showTransfer = (transfer) => { scannedTransfer = transfer; stopCamera(); renderParsedTransfer(resultEl, transfer); resultCard.style.display = ''; setStatus(status, '', 'info'); }; const parseTransferText = (text) => { try { const transfer = parseKeyTransferText(text); if (!transfer.keys.deviceKey) { throw new Error('В QR-коде нет device key для входа'); } showTransfer(transfer); } catch (error) { setStatus(status, error?.message || 'Не удалось прочитать QR-код.', 'error'); } }; const scanLoop = async () => { if (stopped || !detector || !video || video.readyState < 2) { if (!stopped) scanTimer = window.setTimeout(scanLoop, 250); return; } try { const codes = await detector.detect(video); const text = String(codes?.[0]?.rawValue || '').trim(); if (text) { parseTransferText(text); return; } } catch { // Ошибки отдельных кадров игнорируем, камера продолжит сканирование. } if (!stopped) scanTimer = window.setTimeout(scanLoop, 300); }; const startCamera = async () => { try { detector = await createQrDetector(); if (!detector) { throw new Error('Этот браузер не поддерживает сканирование QR через камеру. Используйте ручной ввод QR-текста.'); } if (!navigator.mediaDevices?.getUserMedia) { throw new Error('Камера не поддерживается в этом браузере.'); } stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false }); video.srcObject = stream; await video.play?.(); scanLoop(); } catch (error) { cameraError.textContent = error?.message || 'Не удалось открыть камеру. Проверьте разрешения браузера.'; cameraError.style.display = ''; setStatus(status, cameraError.textContent, 'error'); } }; parseManualButton.addEventListener('click', () => parseTransferText(manualInput.value)); cancelButton.addEventListener('click', () => { scannedTransfer = null; resultCard.style.display = 'none'; stopped = false; void startCamera(); }); confirmButton.addEventListener('click', async () => { if (!scannedTransfer) return; confirmButton.disabled = true; cancelButton.disabled = true; setAuthBusy(true); setAuthError(''); setStatus(status, 'Входим по QR-коду...', 'info'); try { await authService.reconnect(state.entrySettings.shineServer); const session = await authService.createSessionFromImportedSecrets(scannedTransfer.login, scannedTransfer.keys); await clearStoredMessages().catch(() => {}); clearBrowserClientData(); await clearClientAuthData().catch(() => {}); await terminateCurrentSession(); await saveEncryptedUserSecrets(session.login, session.storagePwd, scannedTransfer.keys); await authService.persistSessionMaterial(session.login, session.sessionMaterial); const resumed = await authService.resumeSession(session.login, session.sessionId); authorizeSession({ login: resumed.login || session.login, sessionId: resumed.sessionId || session.sessionId, storagePwd: resumed.storagePwd || session.storagePwd, }); state.loginDraft.login = resumed.login || session.login; state.loginDraft.password = ''; await refreshSessions(); setAuthInfo(`Вход по QR-коду выполнен для @${resumed.login || session.login}.`); navigate('profile-view'); } catch (error) { const message = toUserMessage(error, 'Не удалось войти по QR-коду.'); setAuthError(message); setStatus(status, message, 'error'); } finally { setAuthBusy(false); confirmButton.disabled = false; cancelButton.disabled = false; } }); backButton.addEventListener('click', () => { stopCamera(); navigate('login-view'); }); screen.append( renderHeader({ title: 'Войти по QR-коду', leftAction: { label: '←', onClick: () => { stopCamera(); navigate('login-view'); }, }, }), frame, manualCard, resultCard, status, backButton, ); void startCamera(); screen.cleanup = stopCamera; return screen; }