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 = `
Загрузить аватар (Arweave)
Поддерживаются JPEG, PNG, WebP. Перед загрузкой изображение будет оптимизировано.
Transaction ID:
`;
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;
}