SHA256
Звонки: расширенная диагностика + экран настроек разработчика + обновление TURN-конфига
This commit is contained in:
@@ -18,7 +18,7 @@ export function render({ navigate }) {
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Лог приложения',
|
||||
leftAction: { label: '←', onClick: () => navigate('settings-view') },
|
||||
leftAction: { label: '←', onClick: () => navigate('developer-settings-view') },
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
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 = `
|
||||
<div class="modal" id="settings-dev-avatar-modal">
|
||||
<div class="modal-card stack settings-dev-avatar-modal-card">
|
||||
<h3 class="modal-title">Загрузить аватар (Arweave)</h3>
|
||||
<label class="field-label" for="settings-dev-avatar-login">Логин пользователя</label>
|
||||
<input class="input" id="settings-dev-avatar-login" type="text" maxlength="60" placeholder="Например: aidar" value="" />
|
||||
<label class="field-label" for="settings-dev-avatar-file">Файл изображения</label>
|
||||
<input class="input" id="settings-dev-avatar-file" type="file" accept="image/jpeg,image/png,image/webp" />
|
||||
<p class="meta-muted">Поддерживаются JPEG, PNG, WebP. Перед загрузкой изображение будет оптимизировано.</p>
|
||||
<div class="settings-dev-avatar-meta" id="settings-dev-avatar-meta"></div>
|
||||
<p class="inline-error settings-dev-avatar-error" id="settings-dev-avatar-error"></p>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" id="settings-dev-avatar-cancel" type="button">Закрыть</button>
|
||||
<button class="primary-btn" id="settings-dev-avatar-upload" type="button">Загрузить</button>
|
||||
</div>
|
||||
<div class="card stack settings-dev-avatar-result" id="settings-dev-avatar-result" hidden>
|
||||
<p class="meta-muted">Transaction ID:</p>
|
||||
<p class="key-value" id="settings-dev-avatar-txid"></p>
|
||||
<button class="ghost-btn" id="settings-dev-avatar-copy" type="button">Скопировать TX ID</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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() {
|
||||
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 = `
|
||||
<button class="text-btn" type="button" id="settings-force-ui-update">Принудительно обновить UI</button>
|
||||
<button class="text-btn" type="button" id="settings-force-update-help">Клиент не обновляется?</button>
|
||||
<button class="text-btn" type="button" id="settings-app-log">Лог приложения</button>
|
||||
<button class="text-btn" type="button" id="settings-pwa-diagnostics">Диагностика PWA / Push</button>
|
||||
<button class="text-btn" type="button" id="settings-pwa-install">Как установить PWA</button>
|
||||
<button class="text-btn" type="button" id="settings-upload-avatar">Загрузить аватар</button>
|
||||
`;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -272,7 +272,7 @@ export function render({ navigate }) {
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Диагностика PWA / Push',
|
||||
leftAction: { label: '←', onClick: () => navigate('settings-view') },
|
||||
leftAction: { label: '←', onClick: () => navigate('developer-settings-view') },
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { addAppLogEntry, authService, closeCurrentSessionAndSignOut, 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';
|
||||
import { addAppLogEntry, authService, closeCurrentSessionAndSignOut } from '../state.js';
|
||||
|
||||
export const pageMeta = { id: 'settings-view', title: 'Настройки' };
|
||||
|
||||
@@ -38,168 +25,6 @@ function formatVersionForUi(rawValue) {
|
||||
return value;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="modal" id="settings-dev-avatar-modal">
|
||||
<div class="modal-card stack settings-dev-avatar-modal-card">
|
||||
<h3 class="modal-title">Загрузить аватар (Arweave)</h3>
|
||||
<label class="field-label" for="settings-dev-avatar-login">Логин пользователя</label>
|
||||
<input class="input" id="settings-dev-avatar-login" type="text" maxlength="60" placeholder="Например: aidar" value="" />
|
||||
<label class="field-label" for="settings-dev-avatar-file">Файл изображения</label>
|
||||
<input class="input" id="settings-dev-avatar-file" type="file" accept="image/jpeg,image/png,image/webp" />
|
||||
<p class="meta-muted">Поддерживаются JPEG, PNG, WebP. Перед загрузкой изображение будет оптимизировано.</p>
|
||||
<div class="settings-dev-avatar-meta" id="settings-dev-avatar-meta"></div>
|
||||
<p class="inline-error settings-dev-avatar-error" id="settings-dev-avatar-error"></p>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" id="settings-dev-avatar-cancel" type="button">Закрыть</button>
|
||||
<button class="primary-btn" id="settings-dev-avatar-upload" type="button">Загрузить</button>
|
||||
</div>
|
||||
<div class="card stack settings-dev-avatar-result" id="settings-dev-avatar-result" hidden>
|
||||
<p class="meta-muted">Transaction ID:</p>
|
||||
<p class="key-value" id="settings-dev-avatar-txid"></p>
|
||||
<button class="ghost-btn" id="settings-dev-avatar-copy" type="button">Скопировать TX ID</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
@@ -218,79 +43,16 @@ export function render({ navigate }) {
|
||||
<button class="text-btn" type="button" id="settings-device">Устройства</button>
|
||||
<button class="text-btn" type="button" id="settings-servers">Настройки серверов</button>
|
||||
<button class="text-btn" type="button" id="settings-language">Язык / Language</button>
|
||||
<button class="text-btn" type="button" id="settings-force-update-help">Клиент не обновляется?</button>
|
||||
<button class="text-btn" type="button" id="settings-developer">Настройки разработчика</button>
|
||||
<button class="text-btn" type="button" id="settings-signout">Завершить текущий сеанс</button>
|
||||
`;
|
||||
|
||||
card.querySelector('#settings-device').addEventListener('click', () => navigate('device-view'));
|
||||
card.querySelector('#settings-servers').addEventListener('click', () => navigate('server-settings-view'));
|
||||
card.querySelector('#settings-language').addEventListener('click', () => navigate('language-view'));
|
||||
card.querySelector('#settings-force-update-help').addEventListener('click', () => {
|
||||
window.alert(
|
||||
'Если UI не обновился:\n\n'
|
||||
+ '1) Закройте вкладки с SHiNE.\n'
|
||||
+ '2) Откройте chrome://settings/siteData и удалите данные для shineup.me.\n'
|
||||
+ '3) Если приложение установлено как PWA — удалите его с устройства.\n'
|
||||
+ '4) Откройте https://shineup.me заново и выполните вход.\n'
|
||||
+ '5) Если всё ещё старая версия — откройте в режиме инкогнито и проверьте версию в Настройки -> Версии.'
|
||||
);
|
||||
});
|
||||
card.querySelector('#settings-developer').addEventListener('click', () => navigate('developer-settings-view'));
|
||||
|
||||
const signOutBtn = card.querySelector('#settings-signout');
|
||||
const developerCard = document.createElement('div');
|
||||
developerCard.className = 'card stack settings-developer-card';
|
||||
developerCard.innerHTML = `
|
||||
<button class="text-btn" type="button" id="settings-dev-toggle">Для разработчиков</button>
|
||||
<div class="stack settings-developer-panel" id="settings-dev-panel" hidden>
|
||||
<button class="text-btn" type="button" id="settings-app-log">Лог приложения</button>
|
||||
<button class="text-btn" type="button" id="settings-pwa-diagnostics">Диагностика PWA / Push</button>
|
||||
<button class="text-btn" type="button" id="settings-pwa-install">Как установить PWA</button>
|
||||
<button class="text-btn" type="button" id="settings-upload-avatar">Загрузить аватар</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const developerToggleBtn = developerCard.querySelector('#settings-dev-toggle');
|
||||
const developerPanel = developerCard.querySelector('#settings-dev-panel');
|
||||
const appLogBtn = developerCard.querySelector('#settings-app-log');
|
||||
const diagnosticsBtn = developerCard.querySelector('#settings-pwa-diagnostics');
|
||||
const pwaInstallBtn = developerCard.querySelector('#settings-pwa-install');
|
||||
const uploadAvatarBtn = developerCard.querySelector('#settings-upload-avatar');
|
||||
|
||||
developerToggleBtn?.addEventListener('click', () => {
|
||||
const isHidden = developerPanel?.hidden !== false;
|
||||
if (developerPanel) developerPanel.hidden = !isHidden;
|
||||
if (developerToggleBtn) {
|
||||
developerToggleBtn.textContent = isHidden ? 'Для разработчиков ▲' : 'Для разработчиков ▼';
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
const syncPwaButtonLabel = () => {
|
||||
if (isStandalonePwaMode()) {
|
||||
pwaInstallBtn.textContent = 'PWA установлено (проверить WebPush)';
|
||||
return;
|
||||
}
|
||||
if (canInstallPwa()) {
|
||||
pwaInstallBtn.textContent = 'Зарегистрировать PWA';
|
||||
return;
|
||||
}
|
||||
pwaInstallBtn.textContent = 'Как установить PWA';
|
||||
};
|
||||
|
||||
const unsubscribeInstallAvailability = onPwaInstallAvailabilityChange(() => {
|
||||
syncPwaButtonLabel();
|
||||
});
|
||||
syncPwaButtonLabel();
|
||||
|
||||
signOutBtn.addEventListener('click', async () => {
|
||||
const confirmed = window.confirm(
|
||||
'Завершить текущую сессию на сервере, отключиться, очистить локальные данные и перейти на стартовый экран?'
|
||||
@@ -302,7 +64,7 @@ export function render({ navigate }) {
|
||||
addAppLogEntry({
|
||||
level: 'info',
|
||||
source: 'session',
|
||||
message: `Запрошено завершение текущей сессии: ${state.session.sessionId || 'unknown'}`,
|
||||
message: 'Запрошено завершение текущей сессии',
|
||||
});
|
||||
await closeCurrentSessionAndSignOut({
|
||||
infoMessage: 'Сеанс завершён. Выполните вход заново.',
|
||||
@@ -312,45 +74,6 @@ export function render({ navigate }) {
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
const versionCard = document.createElement('div');
|
||||
versionCard.className = 'card stack';
|
||||
|
||||
@@ -394,12 +117,10 @@ export function render({ navigate }) {
|
||||
}
|
||||
})();
|
||||
|
||||
screen.append(card);
|
||||
screen.cleanup = () => {
|
||||
isDisposed = true;
|
||||
unsubscribeInstallAvailability();
|
||||
};
|
||||
screen.append(card);
|
||||
screen.append(versionCard);
|
||||
screen.append(developerCard);
|
||||
return screen;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user