import { renderHeader } from '../components/header.js'; import { addAppLogEntry, authService, state } from '../state.js'; import { canInstallPwa, isStandalonePwaMode, onPwaInstallAvailabilityChange, promptPwaInstall, } from '../services/pwa-install-service.js'; import { initPwaPush } from '../services/pwa-push-service.js'; import { getArweaveWalletFromStoredDeviceKey } from '../services/arweave-wallet-service.js'; import { prepareAvatarImageFile, uploadArweaveFile, validateAvatarSourceFile, } from '../services/arweave-file-service.js'; export const pageMeta = { id: 'developer-settings-view', title: 'Настройки разработчика' }; function clearArweaveJwk(walletCtx) { if (!walletCtx?.jwk || typeof walletCtx.jwk !== 'object') return; Object.keys(walletCtx.jwk).forEach((key) => { walletCtx.jwk[key] = ''; }); walletCtx.jwk = null; } 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 openDeveloperAvatarUploadModal({ walletLogin, storagePwd, gateway } = {}) { const root = document.getElementById('modal-root'); if (!root) return; root.innerHTML = ` `; const modal = root.querySelector('#settings-dev-avatar-modal'); const loginInput = root.querySelector('#settings-dev-avatar-login'); const fileInput = root.querySelector('#settings-dev-avatar-file'); const metaEl = root.querySelector('#settings-dev-avatar-meta'); const errorEl = root.querySelector('#settings-dev-avatar-error'); const cancelBtn = root.querySelector('#settings-dev-avatar-cancel'); const uploadBtn = root.querySelector('#settings-dev-avatar-upload'); const resultCard = root.querySelector('#settings-dev-avatar-result'); const txidEl = root.querySelector('#settings-dev-avatar-txid'); const copyBtn = root.querySelector('#settings-dev-avatar-copy'); if (loginInput instanceof HTMLInputElement) { loginInput.value = String(walletLogin || '').trim(); } let isClosed = false; const closeModal = () => { if (isClosed) return; isClosed = true; root.innerHTML = ''; }; const setError = (text) => { if (errorEl) errorEl.textContent = String(text || ''); }; const setMeta = (text) => { if (metaEl) metaEl.textContent = String(text || ''); }; const setTxId = (txId) => { if (!(resultCard instanceof HTMLElement) || !(txidEl instanceof HTMLElement)) return; const value = String(txId || '').trim(); txidEl.textContent = value; resultCard.hidden = !value; }; modal?.addEventListener('click', (event) => { if (event.target === modal) closeModal(); }); cancelBtn?.addEventListener('click', closeModal); copyBtn?.addEventListener('click', async () => { const txId = String(txidEl?.textContent || '').trim(); if (!txId) return; try { await navigator.clipboard.writeText(txId); setError(''); } catch { setError('Не удалось скопировать TX ID.'); } }); uploadBtn?.addEventListener('click', async () => { setError(''); setTxId(''); const targetLogin = String(loginInput?.value || '').trim(); const file = fileInput?.files?.[0] || null; if (!targetLogin) { setError('Введите логин пользователя.'); return; } if (!file) { setError('Выберите файл изображения.'); return; } if (!String(walletLogin || '').trim() || !String(storagePwd || '').trim()) { setError('Нет активной сессии. Войдите заново и повторите.'); return; } uploadBtn.disabled = true; let walletCtx = null; try { validateAvatarSourceFile(file); setMeta('Подготовка изображения...'); const optimized = await prepareAvatarImageFile(file); setMeta( `Файл подготовлен: ${formatBytes(optimized.originalSizeBytes)} → ${formatBytes(optimized.optimizedSizeBytes)} ` + `(${optimized.width}x${optimized.height}, ${optimized.contentType})`, ); walletCtx = await getArweaveWalletFromStoredDeviceKey({ login: walletLogin, storagePwd, onStatus: (message) => setMeta(message), }); setMeta('Загрузка в Arweave...'); const uploaded = await uploadArweaveFile({ gateway, jwk: walletCtx?.jwk, file: optimized.file, tags: [ { name: 'SHiNE-Profile-Login', value: targetLogin }, ], }); const txId = String(uploaded?.id || '').trim(); if (!txId) throw new Error('Пустой Transaction ID'); setMeta('Загрузка завершена.'); setTxId(txId); } catch (error) { const message = String(error?.message || ''); if ( message === 'Выберите файл изображения.' || message === 'Поддерживаются только JPEG, PNG или WebP.' || message === 'Файл слишком большой. Максимум 10 MB.' || message === 'Не удалось подготовить изображение.' ) { setError(message); } else { setError('Не удалось загрузить аватар в Arweave.'); } } finally { clearArweaveJwk(walletCtx); uploadBtn.disabled = false; } }); } async function forceUiUpdateNow() { try { window.location.hash = '#/settings-view'; } catch {} if (!('serviceWorker' in navigator)) { window.location.reload(); return; } try { const registrations = await navigator.serviceWorker.getRegistrations(); await Promise.all(registrations.map(async (registration) => { try { await registration.update(); } catch {} if (registration.waiting) { try { registration.waiting.postMessage({ type: 'SKIP_WAITING' }); } catch {} } })); } catch {} window.setTimeout(() => window.location.reload(), 450); } function showClientUpdateHelp() { window.alert( 'Если UI не обновился:\n\n' + '1) Закройте вкладки с SHiNE.\n' + '2) Откройте chrome://settings/siteData и удалите данные для shineup.me.\n' + '3) Если приложение установлено как PWA — удалите его с устройства.\n' + '4) Откройте https://shineup.me заново и выполните вход.\n' + '5) Если всё ещё старая версия — откройте в режиме инкогнито и проверьте версию в Настройки -> Версии.' ); } export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; screen.append( renderHeader({ title: 'Настройки разработчика', leftAction: { label: '←', onClick: () => navigate('settings-view') }, }), ); const card = document.createElement('div'); card.className = 'card stack settings-developer-card'; card.innerHTML = ` `; const appLogBtn = card.querySelector('#settings-app-log'); const diagnosticsBtn = card.querySelector('#settings-pwa-diagnostics'); const pwaInstallBtn = card.querySelector('#settings-pwa-install'); const uploadAvatarBtn = card.querySelector('#settings-upload-avatar'); const forceUpdateBtn = card.querySelector('#settings-force-ui-update'); const forceUpdateHelpBtn = card.querySelector('#settings-force-update-help'); appLogBtn?.addEventListener('click', () => navigate('app-log-view')); diagnosticsBtn?.addEventListener('click', () => navigate('pwa-diagnostics-view')); uploadAvatarBtn?.addEventListener('click', () => { openDeveloperAvatarUploadModal({ walletLogin: state.session.login, storagePwd: state.session.storagePwdInMemory, gateway: state.entrySettings.arweaveServer, }); }); forceUpdateHelpBtn?.addEventListener('click', showClientUpdateHelp); forceUpdateBtn?.addEventListener('click', async () => { forceUpdateBtn.disabled = true; try { addAppLogEntry({ level: 'info', source: 'ui-update', message: 'Пользователь запросил принудительное обновление UI', }); await forceUiUpdateNow(); } finally { forceUpdateBtn.disabled = false; } }); const syncPwaButtonLabel = () => { if (isStandalonePwaMode()) { pwaInstallBtn.textContent = 'PWA установлено (проверить WebPush)'; return; } if (canInstallPwa()) { pwaInstallBtn.textContent = 'Зарегистрировать PWA'; return; } pwaInstallBtn.textContent = 'Как установить PWA'; }; const unsubscribeInstallAvailability = onPwaInstallAvailabilityChange(() => { syncPwaButtonLabel(); }); syncPwaButtonLabel(); pwaInstallBtn.addEventListener('click', async () => { pwaInstallBtn.disabled = true; try { await initPwaPush({ authService, onLog: (entry) => addAppLogEntry(entry), }); if (canInstallPwa()) { const result = await promptPwaInstall(); const accepted = result.outcome === 'accepted'; addAppLogEntry({ level: 'info', source: 'pwa-install', message: accepted ? 'Пользователь принял установку PWA' : 'Пользователь отклонил установку PWA', details: { outcome: result.outcome || 'unknown' }, }); if (accepted) { window.alert('Установка PWA подтверждена. Проверьте приложение на главном экране устройства.'); } } else if (!isStandalonePwaMode()) { window.alert('Для установки откройте меню браузера и выберите "Установить приложение" или "Добавить на главный экран".'); } else { window.alert('PWA уже установлено. WebPush перерегистрирован.'); } } catch (error) { addAppLogEntry({ level: 'warn', source: 'pwa-install', message: 'Не удалось зарегистрировать PWA/WebPush', details: { error: error?.message || 'unknown' }, }); window.alert(`Ошибка регистрации PWA: ${error?.message || 'unknown'}`); } finally { pwaInstallBtn.disabled = false; syncPwaButtonLabel(); } }); screen.cleanup = () => { unsubscribeInstallAvailability(); }; screen.append(card); return screen; }