import { getArweaveBalance, getArweaveWalletFromStoredDeviceKey } from '../services/arweave-wallet-service.js'; import { buildArweaveDataUrl, getArweaveUploadPrice, prepareAvatarImageFile, uploadArweaveFile, validateArweaveTxId, validateAvatarSourceFile, } from '../services/arweave-file-service.js'; import { saveProfileAvatarArweave } from '../services/user-profile-params.js'; function escapeHtml(text) { return String(text || '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function formatBytes(bytes) { const value = Number(bytes || 0); if (!Number.isFinite(value) || value <= 0) return '0 B'; if (value < 1024) return `${value} B`; if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; return `${(value / (1024 * 1024)).toFixed(2)} MB`; } function formatAr(ar) { const n = Number(ar); if (!Number.isFinite(n)) return '0'; return n.toLocaleString('ru-RU', { maximumFractionDigits: 6 }); } function clearJwk(ctx) { if (!ctx?.jwk || typeof ctx.jwk !== 'object') return; Object.keys(ctx.jwk).forEach((key) => { ctx.jwk[key] = ''; }); ctx.jwk = null; } function setNodeText(node, text) { if (node) node.textContent = String(text || ''); } export function openAvatarWizard({ login, storagePwd, gateway, navigate, onAvatarSaved, onStatus, } = {}) { const root = document.getElementById('modal-root'); if (!root) return Promise.resolve(false); const cleanLogin = String(login || '').trim(); const cleanStoragePwd = String(storagePwd || '').trim(); const cleanGateway = String(gateway || '').trim(); if (!cleanLogin || !cleanStoragePwd) { return Promise.reject(new Error('Нет активной сессии.')); } let closed = false; let lastPreviewUrl = ''; let walletCtx = null; let balanceInfo = null; let optimized = null; let priceInfo = null; let uploadedTxId = ''; function revokePreviewUrl() { if (!lastPreviewUrl) return; URL.revokeObjectURL(lastPreviewUrl); lastPreviewUrl = ''; } function close(result = false, resolve) { if (closed) return; closed = true; revokePreviewUrl(); clearJwk(walletCtx); walletCtx = null; root.innerHTML = ''; resolve(result); } async function ensurePreviewImage(url, imageEl) { return new Promise((resolve, reject) => { const img = imageEl; img.onload = () => resolve(true); img.onerror = () => reject(new Error('Не удалось загрузить изображение по этому Transaction ID')); img.src = url; }); } return new Promise((resolve) => { const showStepChoice = () => { if (closed) return; root.innerHTML = ` `; const modal = root.querySelector('[data-avatar-wizard-modal="true"]'); modal?.addEventListener('click', (event) => { if (event.target === modal) close(false, resolve); }); root.querySelector('[data-action="cancel"]')?.addEventListener('click', () => close(false, resolve)); root.querySelector('[data-action="use-existing"]')?.addEventListener('click', showStepExistingInput); root.querySelector('[data-action="upload-new"]')?.addEventListener('click', () => { void showStepUpload(); }); }; const showStepExistingInput = () => { if (closed) return; root.innerHTML = ` `; const modal = root.querySelector('[data-avatar-wizard-modal="true"]'); const inputEl = root.querySelector('#avatar-existing-txid'); const errorEl = root.querySelector('[data-error="true"]'); modal?.addEventListener('click', (event) => { if (event.target === modal) close(false, resolve); }); root.querySelector('[data-action="back"]')?.addEventListener('click', showStepChoice); root.querySelector('[data-action="next"]')?.addEventListener('click', () => { const txId = String(inputEl?.value || '').trim(); if (!validateArweaveTxId(txId)) { setNodeText(errorEl, 'Некорректный Transaction ID Arweave.'); return; } setNodeText(errorEl, ''); void showStepExistingPreview(txId); }); window.setTimeout(() => inputEl?.focus(), 0); }; const showStepExistingPreview = async (txId) => { if (closed) return; const previewUrl = buildArweaveDataUrl({ gateway: cleanGateway, txId }); root.innerHTML = ` `; const modal = root.querySelector('[data-avatar-wizard-modal="true"]'); const imageEl = root.querySelector('[data-preview-image="true"]'); const errorEl = root.querySelector('[data-error="true"]'); const saveBtn = root.querySelector('[data-action="save"]'); modal?.addEventListener('click', (event) => { if (event.target === modal) close(false, resolve); }); root.querySelector('[data-action="back"]')?.addEventListener('click', showStepExistingInput); if (saveBtn instanceof HTMLButtonElement) saveBtn.disabled = true; try { await ensurePreviewImage(previewUrl, imageEl); if (saveBtn instanceof HTMLButtonElement) saveBtn.disabled = false; } catch (error) { setNodeText(errorEl, 'Не удалось загрузить изображение по этому Transaction ID'); if (saveBtn instanceof HTMLButtonElement) saveBtn.disabled = true; } saveBtn?.addEventListener('click', async () => { try { await saveProfileAvatarArweave(cleanLogin, txId); if (typeof onAvatarSaved === 'function') await onAvatarSaved(); close(true, resolve); } catch { setNodeText(errorEl, 'Не удалось сохранить аватар в профиль.'); } }); }; const showStepZeroBalance = () => { if (closed) return; root.innerHTML = ` `; root.querySelector('[data-action="to-wallet"]')?.addEventListener('click', () => { close(false, resolve); if (typeof navigate === 'function') navigate('wallet-view'); }); root.querySelector('[data-action="cancel"]')?.addEventListener('click', () => close(false, resolve)); }; const showStepUploadForm = () => { if (closed) return; root.innerHTML = ` `; const modal = root.querySelector('[data-avatar-wizard-modal="true"]'); const fileInput = root.querySelector('#avatar-file-input'); const errorEl = root.querySelector('[data-error="true"]'); const metaEl = root.querySelector('[data-meta="true"]'); const previewWrap = root.querySelector('[data-preview-wrap="true"]'); const previewImage = root.querySelector('[data-preview-image="true"]'); const uploadBtn = root.querySelector('[data-action="upload"]'); let selectedFile = null; optimized = null; priceInfo = null; modal?.addEventListener('click', (event) => { if (event.target === modal) close(false, resolve); }); root.querySelector('[data-action="back"]')?.addEventListener('click', showStepChoice); fileInput?.addEventListener('change', async () => { setNodeText(errorEl, ''); setNodeText(metaEl, ''); uploadBtn.disabled = true; revokePreviewUrl(); selectedFile = fileInput.files?.[0] || null; if (!selectedFile) { setNodeText(errorEl, 'Выберите файл изображения.'); return; } try { validateAvatarSourceFile(selectedFile); optimized = await prepareAvatarImageFile(selectedFile); priceInfo = await getArweaveUploadPrice({ gateway: cleanGateway, byteLength: optimized.file.size, }); lastPreviewUrl = URL.createObjectURL(optimized.file); previewImage.src = lastPreviewUrl; previewWrap.hidden = false; const hasFunds = BigInt(balanceInfo.winston) >= BigInt(priceInfo.winston); metaEl.innerHTML = `
Исходный размер: ${escapeHtml(formatBytes(optimized.originalSizeBytes))}
Итоговый размер: ${escapeHtml(formatBytes(optimized.optimizedSizeBytes))}
Итоговое разрешение: ${escapeHtml(`${optimized.width} × ${optimized.height}`)}
Тип файла: ${escapeHtml(optimized.contentType)}
Примерная цена загрузки: ${escapeHtml(formatAr(priceInfo.ar))} AR
`; if (!hasFunds) { setNodeText(errorEl, 'Недостаточно средств для загрузки.'); uploadBtn.disabled = true; return; } uploadBtn.disabled = false; } catch (error) { const message = String(error?.message || ''); if (message === 'Выберите файл изображения.' || message === 'Поддерживаются только JPEG, PNG или WebP.' || message === 'Файл слишком большой. Максимум 10 MB.') { setNodeText(errorEl, message); } else if (message.includes('цену загрузки')) { setNodeText(errorEl, 'Не удалось получить баланс Arweave.'); } else { setNodeText(errorEl, 'Не удалось подготовить изображение.'); } } }); uploadBtn?.addEventListener('click', async () => { setNodeText(errorEl, ''); if (!optimized?.file) { setNodeText(errorEl, 'Выберите файл изображения.'); return; } if (!priceInfo || BigInt(balanceInfo.winston) < BigInt(priceInfo.winston)) { setNodeText(errorEl, 'Недостаточно средств для загрузки.'); return; } uploadBtn.disabled = true; try { const uploaded = await uploadArweaveFile({ gateway: cleanGateway, jwk: walletCtx?.jwk, file: optimized.file, tags: [ { name: 'SHiNE-Login', value: cleanLogin }, ], }); uploadedTxId = String(uploaded.id || '').trim(); if (!uploadedTxId) { throw new Error('Пустой Transaction ID'); } showStepUploaded(); } catch { setNodeText(errorEl, 'Не удалось загрузить файл в Arweave.'); uploadBtn.disabled = false; } }); }; const showStepUploaded = () => { if (closed) return; root.innerHTML = ` `; const errorEl = root.querySelector('[data-error="true"]'); root.querySelector('[data-action="copy-id"]')?.addEventListener('click', async () => { try { await navigator.clipboard.writeText(uploadedTxId); setNodeText(errorEl, ''); } catch { setNodeText(errorEl, 'Не удалось скопировать Transaction ID.'); } }); root.querySelector('[data-action="set-avatar"]')?.addEventListener('click', async () => { try { await saveProfileAvatarArweave(cleanLogin, uploadedTxId); if (typeof onAvatarSaved === 'function') await onAvatarSaved(); close(true, resolve); } catch { setNodeText(errorEl, 'Не удалось сохранить аватар в профиль.'); } }); root.querySelector('[data-action="close"]')?.addEventListener('click', () => close(false, resolve)); }; const showStepUpload = async () => { if (closed) return; root.innerHTML = ` `; const loadingEl = root.querySelector('[data-loading="true"]'); const errorEl = root.querySelector('[data-error="true"]'); root.querySelector('[data-action="back"]')?.addEventListener('click', showStepChoice); try { walletCtx = await getArweaveWalletFromStoredDeviceKey({ login: cleanLogin, storagePwd: cleanStoragePwd, onStatus: (message) => { setNodeText(loadingEl, message); if (typeof onStatus === 'function') onStatus(message); }, }); balanceInfo = await getArweaveBalance({ gateway: cleanGateway, address: walletCtx.address, }); } catch (error) { setNodeText(errorEl, 'Не удалось получить баланс Arweave.'); return; } if (BigInt(balanceInfo.winston) <= 0n) { showStepZeroBalance(); return; } showStepUploadForm(); }; showStepChoice(); }); }