SHA256
Палитра «Сияние» по умолчанию, новые роли цветов. Лента канала: плоские записи, ряд действий с равным шагом иконок, дата у записи. Обсуждение со строкой ввода, личный чат, шторки. Вход и регистрация: стартовый экран (надпись «СИЯНИЕ», сияние мандалы), настройки входа со статусами серверов, статус логина в одну строку, вход с другого устройства (код, таймер, шаги), сохранение ключей с переключателями. Корневые разделы – заголовок по центру; фильтр чатов – переключатель; уведомления – список как в чатах; фото – просмотр на весь экран. Общее: один зазор под шапкой (--page-top-gap), переключатели высотой с поле ввода, второстепенные кнопки по одной роли (в том числе на экранах устройств и серверов), иконки только из ui-icon.js, полоса прокрутки скрыта, среднее тире в текстах интерфейса. Что нужно от сервера и разработчика – docs/UI-Design/UI-v4-for-dev.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
209 lines
7.1 KiB
JavaScript
209 lines
7.1 KiB
JavaScript
import { createTopBar } from '../components/topbar.js';
|
|
import {
|
|
authorizeLocalDemoSession,
|
|
isLocalDemoAvailable,
|
|
saveEntrySettings,
|
|
state,
|
|
} from '../state.js';
|
|
import { checkServerAvailabilityByKey, resolveAndCheckShineServerLogin } from '../services/server-health-service.js';
|
|
|
|
export const pageMeta = { id: 'entry-settings-view', title: 'Настройки входа', showAppChrome: false };
|
|
|
|
const SERVER_FIELDS = [
|
|
{ key: 'solanaServer', label: 'Сервер Solana' },
|
|
{ key: 'shineServerLogin', label: 'Сервер Сияния' },
|
|
{ key: 'arweaveServer', label: 'Сервер Arweave' },
|
|
];
|
|
|
|
export function render({ navigate }) {
|
|
const screen = document.createElement('section');
|
|
screen.className = 'stack entry-settings';
|
|
|
|
const draft = {
|
|
language: state.entrySettings.language,
|
|
solanaServer: state.entrySettings.solanaServer,
|
|
shineServerLogin: state.entrySettings.shineServerLogin,
|
|
shineServerHttp: state.entrySettings.shineServerHttp,
|
|
arweaveServer: state.entrySettings.arweaveServer,
|
|
statuses: { ...state.entrySettings.statuses },
|
|
};
|
|
|
|
const timers = new Map();
|
|
|
|
let disposed = false;
|
|
const checks = [];
|
|
|
|
const body = document.createElement('div');
|
|
body.className = 'entry-servers';
|
|
|
|
SERVER_FIELDS.forEach((field) => {
|
|
const block = document.createElement('div');
|
|
block.className = 'entry-server';
|
|
|
|
const head = document.createElement('div');
|
|
head.className = 'entry-server__head';
|
|
|
|
const title = document.createElement('label');
|
|
title.className = 'field-label';
|
|
title.htmlFor = `entry-${field.key}`;
|
|
title.textContent = field.label;
|
|
|
|
// Статус — он же кнопка повторной проверки.
|
|
const checkButton = document.createElement('button');
|
|
checkButton.className = 'text-btn entry-status';
|
|
checkButton.type = 'button';
|
|
|
|
const input = document.createElement('input');
|
|
input.className = 'input';
|
|
input.id = `entry-${field.key}`;
|
|
input.type = 'text';
|
|
input.autocapitalize = 'off';
|
|
input.spellcheck = false;
|
|
input.value = draft[field.key];
|
|
|
|
const hint = document.createElement('p');
|
|
hint.className = 'entry-server__hint';
|
|
|
|
const applyStatus = (value, exactAddress = '') => {
|
|
if (disposed) return;
|
|
if (value !== 'checking') draft.statuses[field.key] = value;
|
|
checkButton.classList.remove('is-available', 'is-unavailable', 'is-checking');
|
|
checkButton.disabled = false;
|
|
|
|
if (value === 'available') {
|
|
checkButton.textContent = 'Доступен';
|
|
checkButton.classList.add('is-available');
|
|
} else if (value === 'unavailable') {
|
|
checkButton.textContent = 'Недоступен';
|
|
checkButton.classList.add('is-unavailable');
|
|
} else if (value === 'checking') {
|
|
checkButton.textContent = 'Проверка…';
|
|
checkButton.classList.add('is-checking');
|
|
checkButton.disabled = true;
|
|
} else {
|
|
checkButton.textContent = 'Проверить';
|
|
}
|
|
checkButton.setAttribute('aria-label', `${field.label}: ${checkButton.textContent}. Проверить ещё раз`);
|
|
|
|
if (field.key === 'shineServerLogin') {
|
|
const address = exactAddress || draft.shineServerHttp;
|
|
hint.textContent = address || '';
|
|
hint.hidden = !address;
|
|
}
|
|
};
|
|
|
|
let checkSeq = 0;
|
|
const runCheck = async () => {
|
|
draft[field.key] = input.value.trim();
|
|
window.clearTimeout(timers.get(field.key));
|
|
// Результат устаревшей проверки (адрес успели поменять) не должен перезаписать новый.
|
|
const seq = ++checkSeq;
|
|
applyStatus('checking');
|
|
try {
|
|
if (field.key === 'shineServerLogin') {
|
|
const resolved = await resolveAndCheckShineServerLogin(input.value, draft.solanaServer);
|
|
if (seq !== checkSeq) return;
|
|
draft.shineServerHttp = resolved.httpBase;
|
|
draft.shineServer = resolved.wsUrl;
|
|
applyStatus(resolved.status, resolved.httpBase);
|
|
} else {
|
|
const next = await checkServerAvailabilityByKey(field.key, input.value, {
|
|
solanaEndpoint: draft.solanaServer,
|
|
});
|
|
if (seq !== checkSeq) return;
|
|
applyStatus(next);
|
|
}
|
|
} catch {
|
|
if (seq === checkSeq) applyStatus('unavailable');
|
|
}
|
|
};
|
|
|
|
applyStatus(draft.statuses[field.key]);
|
|
hint.hidden = field.key !== 'shineServerLogin' || !hint.textContent;
|
|
checks.push(runCheck);
|
|
|
|
checkButton.addEventListener('click', runCheck);
|
|
input.addEventListener('input', () => {
|
|
draft[field.key] = input.value;
|
|
checkSeq += 1;
|
|
applyStatus('idle');
|
|
window.clearTimeout(timers.get(field.key));
|
|
timers.set(field.key, window.setTimeout(() => {
|
|
void runCheck();
|
|
}, 3000));
|
|
});
|
|
input.addEventListener('change', () => {
|
|
void runCheck();
|
|
});
|
|
input.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
void runCheck();
|
|
}
|
|
});
|
|
|
|
head.append(title, checkButton);
|
|
block.append(head, input, hint);
|
|
body.append(block);
|
|
});
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'entry-settings__actions';
|
|
|
|
if (isLocalDemoAvailable()) {
|
|
const localDemoButton = document.createElement('button');
|
|
localDemoButton.className = 'secondary-btn';
|
|
localDemoButton.type = 'button';
|
|
localDemoButton.textContent = 'Открыть локальный тестовый режим';
|
|
localDemoButton.addEventListener('click', () => {
|
|
if (!authorizeLocalDemoSession()) return;
|
|
navigate('messages-list');
|
|
});
|
|
actions.append(localDemoButton);
|
|
}
|
|
|
|
const serverUiButton = document.createElement('button');
|
|
serverUiButton.className = 'secondary-btn';
|
|
serverUiButton.type = 'button';
|
|
serverUiButton.textContent = 'Настроить свой сервер ↗';
|
|
serverUiButton.addEventListener('click', () => {
|
|
const url = new URL('server-ui.html', window.location.href);
|
|
window.open(url.toString(), '_blank', 'noopener');
|
|
});
|
|
|
|
const saveButton = document.createElement('button');
|
|
saveButton.className = 'primary-btn';
|
|
saveButton.type = 'button';
|
|
saveButton.textContent = 'Сохранить';
|
|
saveButton.addEventListener('click', async () => {
|
|
try {
|
|
await saveEntrySettings(draft);
|
|
navigate('start-view');
|
|
} catch (error) {
|
|
window.alert(error?.message || 'Не удалось сохранить настройки входа.');
|
|
}
|
|
});
|
|
|
|
actions.prepend(saveButton);
|
|
actions.append(serverUiButton);
|
|
|
|
screen.append(
|
|
createTopBar({
|
|
title: 'Настройки входа',
|
|
back: { label: '←', onClick: () => navigate('start-view') },
|
|
}),
|
|
body,
|
|
actions,
|
|
);
|
|
|
|
screen.cleanup = () => {
|
|
disposed = true;
|
|
timers.forEach((timerId) => window.clearTimeout(timerId));
|
|
};
|
|
|
|
// Серверы проверяются сразу — зелёный «Доступен» видно без лишних нажатий.
|
|
checks.forEach((check) => { void check(); });
|
|
|
|
return screen;
|
|
}
|