SHA256
Первая версия вложенных файлов работает
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { createTopBar } from '../components/topbar.js';
|
||||
import {
|
||||
isDeveloperToolsEnabled,
|
||||
isDmFileTransferEnabled,
|
||||
setDeveloperToolsEnabled,
|
||||
setDmFileTransferEnabled,
|
||||
} from '../services/feature-settings.js';
|
||||
|
||||
export const pageMeta = { id: 'advanced-settings-view', title: 'Дополнительные настройки' };
|
||||
|
||||
function createToggleRow({ id, title, hint, checked }) {
|
||||
const label = document.createElement('label');
|
||||
label.className = 'card settings-feature-toggle-row';
|
||||
label.htmlFor = id;
|
||||
label.innerHTML = `
|
||||
<span class="settings-feature-toggle-copy">
|
||||
<strong class="settings-feature-toggle-title">${title}</strong>
|
||||
<span class="settings-feature-toggle-hint">${hint}</span>
|
||||
</span>
|
||||
<input class="settings-feature-switch" id="${id}" type="checkbox" ${checked ? 'checked' : ''} />
|
||||
`;
|
||||
return label;
|
||||
}
|
||||
|
||||
export function render({ navigate, chrome }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack advanced-settings-screen';
|
||||
|
||||
chrome?.setTopbar(createTopBar({
|
||||
title: 'Дополнительные настройки',
|
||||
back: { label: '←', onClick: () => navigate('settings-view') },
|
||||
}));
|
||||
|
||||
const intro = document.createElement('div');
|
||||
intro.className = 'card stack advanced-settings-intro';
|
||||
intro.innerHTML = `
|
||||
<p class="field-label">Скрытые возможности SHiNE</p>
|
||||
<p class="meta-muted">Этот экран открывается пятью быстрыми нажатиями по логотипу в обычных настройках и имеет постоянный адрес <code>/settings/advanced</code>.</p>
|
||||
`;
|
||||
|
||||
const developerRow = createToggleRow({
|
||||
id: 'advanced-developer-tools',
|
||||
title: 'Настройки разработчика',
|
||||
hint: 'Показывать блок «Версии» и кнопку настроек разработчика в обычных настройках.',
|
||||
checked: isDeveloperToolsEnabled(),
|
||||
});
|
||||
|
||||
const filesRow = createToggleRow({
|
||||
id: 'advanced-dm-files',
|
||||
title: 'Передача файлов в личных сообщениях',
|
||||
hint: 'Разрешить отправку новых зашифрованных файлов через удержание кнопки эмодзи. Полученные ранее файлы останутся доступными.',
|
||||
checked: isDmFileTransferEnabled(),
|
||||
});
|
||||
|
||||
const developerInput = developerRow.querySelector('#advanced-developer-tools');
|
||||
const filesInput = filesRow.querySelector('#advanced-dm-files');
|
||||
developerInput?.addEventListener('change', () => setDeveloperToolsEnabled(developerInput.checked));
|
||||
filesInput?.addEventListener('change', () => setDmFileTransferEnabled(filesInput.checked));
|
||||
|
||||
screen.append(intro, developerRow, filesRow);
|
||||
return screen;
|
||||
}
|
||||
@@ -29,7 +29,13 @@ import {
|
||||
} from '../components/emoji-picker.js?v=202607152130';
|
||||
import { isTextToSpeechConfigured, speakTextBySettings } from '../services/speech-tools-service.js';
|
||||
import { showToast } from '../services/channels-ux.js';
|
||||
import { buildDmReplyTechBlock, parseDmTechBlocks, sanitizeUserDmTextForSend } from '../services/dm-tech-blocks.js';
|
||||
import { buildDmFileTechBlock, buildDmReplyTechBlock, parseDmTechBlocks, sanitizeUserDmTextForSend } from '../services/dm-tech-blocks.js';
|
||||
import { isDmFileTransferEnabled } from '../services/feature-settings.js';
|
||||
import {
|
||||
downloadAndDecryptDmFile,
|
||||
encryptAndUploadDmFile,
|
||||
formatDmFileSize,
|
||||
} from '../services/dm-file-service.js';
|
||||
import { loadProfileSnapshot } from '../services/user-profile-params.js';
|
||||
import { makeProfileLinksRoute, makeProfileRoute } from '../services/shine-routes.js';
|
||||
|
||||
@@ -686,10 +692,10 @@ function renderLog(
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
const parsedText = parseDmTechBlocks(String(msg?.text || ''));
|
||||
const autoKind = parsedText.callSummary ? 'call-tech' : '';
|
||||
const autoKind = parsedText.callSummary ? 'call-tech' : (parsedText.fileAttachment ? 'file-tech' : '');
|
||||
const bubbleKind = String(msg?.kind || autoKind || '').trim();
|
||||
bubble.className = `bubble ${msg.from}${bubbleKind ? ` ${bubbleKind}` : ''}`;
|
||||
const emojiOnly = !parsedText.callSummary && !parsedText.replyRef && isTwemojiOnlyMessage(parsedText.displayText || '');
|
||||
const emojiOnly = !parsedText.callSummary && !parsedText.fileAttachment && !parsedText.replyRef && isTwemojiOnlyMessage(parsedText.displayText || '');
|
||||
if (emojiOnly) {
|
||||
bubble.classList.add('bubble--emoji-only');
|
||||
}
|
||||
@@ -734,6 +740,50 @@ function renderLog(
|
||||
bubble.append(callCard);
|
||||
}
|
||||
|
||||
if (parsedText.fileAttachment) {
|
||||
const attachment = parsedText.fileAttachment;
|
||||
const fileCard = document.createElement('div');
|
||||
fileCard.className = 'dm-file-card';
|
||||
|
||||
const fileIcon = document.createElement('span');
|
||||
fileIcon.className = 'dm-file-card__icon';
|
||||
fileIcon.textContent = '📎';
|
||||
|
||||
const fileCopy = document.createElement('span');
|
||||
fileCopy.className = 'dm-file-card__copy';
|
||||
const fileName = document.createElement('strong');
|
||||
fileName.className = 'dm-file-card__name';
|
||||
fileName.textContent = attachment.name || 'file';
|
||||
const fileMeta = document.createElement('span');
|
||||
fileMeta.className = 'dm-file-card__meta';
|
||||
fileMeta.textContent = formatDmFileSize(attachment.size);
|
||||
fileCopy.append(fileName, fileMeta);
|
||||
|
||||
const downloadBtn = document.createElement('button');
|
||||
downloadBtn.type = 'button';
|
||||
downloadBtn.className = 'ui-button dm-file-card__download';
|
||||
downloadBtn.textContent = 'Скачать';
|
||||
downloadBtn.addEventListener('click', async (event) => {
|
||||
event.stopPropagation();
|
||||
if (downloadBtn.disabled) return;
|
||||
downloadBtn.disabled = true;
|
||||
const previousLabel = downloadBtn.textContent;
|
||||
downloadBtn.textContent = 'Скачивание…';
|
||||
try {
|
||||
await downloadAndDecryptDmFile(attachment);
|
||||
showToast('Файл расшифрован и передан браузеру для сохранения', { timeoutMs: 1600 });
|
||||
} catch (error) {
|
||||
showToast(error?.message || 'Не удалось скачать файл', { kind: 'error', timeoutMs: 2400 });
|
||||
} finally {
|
||||
downloadBtn.disabled = false;
|
||||
downloadBtn.textContent = previousLabel;
|
||||
}
|
||||
});
|
||||
|
||||
fileCard.append(fileIcon, fileCopy, downloadBtn);
|
||||
bubble.append(fileCard);
|
||||
}
|
||||
|
||||
const textNode = document.createElement('div');
|
||||
textNode.className = 'bubble-text';
|
||||
renderMessageText(textNode, parsedText.displayText || '');
|
||||
@@ -764,7 +814,7 @@ function renderLog(
|
||||
bubble.append(replyBox);
|
||||
}
|
||||
}
|
||||
if (!parsedText.callSummary) {
|
||||
if (!parsedText.callSummary && !parsedText.fileAttachment) {
|
||||
bubble.append(textNode);
|
||||
}
|
||||
|
||||
@@ -1118,7 +1168,9 @@ export function render({ navigate, route, chrome }) {
|
||||
</div>
|
||||
<div class="emoji-picker-slot" id="chat-emoji-picker-slot" hidden></div>
|
||||
<textarea class="input dm-input" name="message" rows="1" placeholder="Введите сообщение" maxlength="12000" enterkeyhint="enter"></textarea>
|
||||
<input id="chat-file-input" type="file" hidden />
|
||||
<div class="dm-actions-col">
|
||||
<button class="ghost-btn dm-file-btn" type="button" id="chat-file-toggle" aria-label="Прикрепить файл" title="Прикрепить файл" hidden>📎</button>
|
||||
<button class="ghost-btn dm-emoji-btn" type="button" id="chat-emoji-toggle" aria-label="Эмодзи" title="Эмодзи"><span class="dm-emoji-btn__glyph">☺</span></button>
|
||||
<button class="primary-btn dm-send-btn dm-send-icon-btn" type="submit" title="Отправить">➤</button>
|
||||
</div>
|
||||
@@ -1127,6 +1179,9 @@ export function render({ navigate, route, chrome }) {
|
||||
const input = form.elements.message;
|
||||
const emojiSlot = form.querySelector('#chat-emoji-picker-slot');
|
||||
const emojiToggle = form.querySelector('#chat-emoji-toggle');
|
||||
const fileToggle = form.querySelector('#chat-file-toggle');
|
||||
const fileInput = form.querySelector('#chat-file-input');
|
||||
const fileTransferEnabled = isDmFileTransferEnabled();
|
||||
const editBanner = form.querySelector('#chat-edit-banner');
|
||||
const editBannerText = form.querySelector('#chat-edit-banner-text');
|
||||
const editCancelBtn = form.querySelector('#chat-edit-cancel');
|
||||
@@ -1135,6 +1190,9 @@ export function render({ navigate, route, chrome }) {
|
||||
let inputFocused = false;
|
||||
let emojiPickerOpen = false;
|
||||
let emojiSelection = null;
|
||||
let emojiLongPressTimer = null;
|
||||
let emojiLongPressTriggered = false;
|
||||
let fileActionVisible = false;
|
||||
|
||||
const setHistoryLoadingState = (isLoading) => {
|
||||
historyLoader.hidden = !isLoading;
|
||||
@@ -1162,6 +1220,19 @@ export function render({ navigate, route, chrome }) {
|
||||
emojiToggle?.setAttribute('aria-expanded', 'false');
|
||||
};
|
||||
|
||||
const clearEmojiLongPress = () => {
|
||||
if (emojiLongPressTimer != null) {
|
||||
window.clearTimeout(emojiLongPressTimer);
|
||||
emojiLongPressTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const setFileActionVisible = (visible) => {
|
||||
fileActionVisible = Boolean(visible && fileTransferEnabled);
|
||||
if (fileToggle) fileToggle.hidden = !fileActionVisible;
|
||||
syncComposerLayout();
|
||||
};
|
||||
|
||||
// На сенсорных устройствах фокус в textarea поднимает клавиатуру поверх пикера —
|
||||
// поэтому при вставке эмодзи фокусируем поле только там, где есть «точный» указатель.
|
||||
const isFinePointer = window.matchMedia?.('(hover: hover) and (pointer: fine)')?.matches ?? true;
|
||||
@@ -1453,6 +1524,117 @@ export function render({ navigate, route, chrome }) {
|
||||
}
|
||||
};
|
||||
|
||||
const sendFileAttachmentMessage = async (attachment) => {
|
||||
const replying = activeEdit ? null : activeReply;
|
||||
const fileBlock = buildDmFileTechBlock(attachment);
|
||||
const replyBlock = replying ? buildDmReplyTechBlock({ baseKey: replying.baseKey }) : '';
|
||||
const fallbackText = `📎 ${String(attachment?.name || 'file')}`;
|
||||
const finalText = `${replyBlock}${fileBlock}${fallbackText}`;
|
||||
|
||||
// Как и обычное исходящее сообщение, отправка файла сразу закрывает текущий
|
||||
// визуальный раздел «Новые сообщения», не дожидаясь ответа сервера.
|
||||
hideUnreadSeparator({ rerender: false });
|
||||
|
||||
const tempId = addOutgoingPendingMessage(chatId, finalText);
|
||||
renderChatLog({ scrollMode: 'latest', markAsRead: false });
|
||||
scrollToLatestMessageSmart(log, { smoothIfNearBottom: true });
|
||||
|
||||
try {
|
||||
const result = await authService.sendDirectMessage({
|
||||
login: state.session.login,
|
||||
toLogin: chatId,
|
||||
text: finalText,
|
||||
storagePwd: state.session.storagePwdInMemory,
|
||||
});
|
||||
markOutgoingSent(tempId, {
|
||||
messageKey: result?.outgoingKey || '',
|
||||
baseKey: result?.baseKey || result?.localBaseKey || '',
|
||||
deliveryState: result?.deliveryState || 'accepted',
|
||||
});
|
||||
|
||||
const localRevisionApplied = await applyLocalRevision({
|
||||
localOutgoingBlobB64: result?.localOutgoingBlobB64 || '',
|
||||
fallbackMessageKey: result?.outgoingKey || '',
|
||||
fallbackBaseKey: result?.baseKey || result?.localBaseKey || '',
|
||||
deliveryState: result?.deliveryState || 'accepted',
|
||||
});
|
||||
|
||||
if (replying) cancelReplyMode({ restoreDraft: false });
|
||||
renderChatLog({ scrollMode: 'latest', markAsRead: false });
|
||||
if (localRevisionApplied) notifyUnreadStateUpdated();
|
||||
scrollToLatestMessageSmart(log, { smoothIfNearBottom: true });
|
||||
window.requestAnimationFrame(() => scrollToLatestMessageSmart(log, { smoothIfNearBottom: true }));
|
||||
|
||||
addAppLogEntry({
|
||||
level: 'info',
|
||||
source: 'outgoing-dm-file',
|
||||
message: `Файл отправлен для ${chatId}`,
|
||||
details: {
|
||||
toLogin: chatId,
|
||||
fileId: attachment?.id || '',
|
||||
fileSize: Number(attachment?.size || 0),
|
||||
messageId: result?.outgoingKey || '',
|
||||
replyToBaseKey: replying?.baseKey || '',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
addChatMessage(chatId, `Ошибка отправки файла: ${error?.message || 'unknown'}`);
|
||||
addAppLogEntry({
|
||||
level: 'warn',
|
||||
source: 'outgoing-dm-file',
|
||||
message: 'Файл загружен, но DM с ключом не отправлен',
|
||||
details: {
|
||||
toLogin: chatId,
|
||||
fileId: attachment?.id || '',
|
||||
error: error?.message || 'unknown',
|
||||
},
|
||||
});
|
||||
renderChatLog({ scrollMode: 'latest', markAsRead: false });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectedFile = async (file) => {
|
||||
if (!fileTransferEnabled) {
|
||||
showToast('Передача файлов отключена в дополнительных настройках.', { kind: 'error', timeoutMs: 1800 });
|
||||
return;
|
||||
}
|
||||
if (activeEdit) {
|
||||
showToast('Сначала завершите редактирование сообщения.', { kind: 'error', timeoutMs: 1600 });
|
||||
return;
|
||||
}
|
||||
if (!file) return;
|
||||
|
||||
const originalLabel = fileToggle?.textContent || '📎';
|
||||
if (fileToggle) {
|
||||
fileToggle.disabled = true;
|
||||
fileToggle.textContent = '…';
|
||||
fileToggle.setAttribute('aria-label', 'Шифрование и загрузка файла');
|
||||
}
|
||||
showToast(`Шифрую и загружаю «${String(file.name || 'file')}»…`, { timeoutMs: 1600 });
|
||||
|
||||
try {
|
||||
const attachment = await encryptAndUploadDmFile({
|
||||
file,
|
||||
login: state.session.login,
|
||||
sessionId: state.session.sessionId,
|
||||
wsUrl: authService.serverUrl,
|
||||
});
|
||||
await sendFileAttachmentMessage(attachment);
|
||||
setFileActionVisible(false);
|
||||
showToast('Файл отправлен.', { timeoutMs: 1200 });
|
||||
} catch (error) {
|
||||
showToast(error?.message || 'Не удалось отправить файл', { kind: 'error', timeoutMs: 2600 });
|
||||
} finally {
|
||||
if (fileToggle) {
|
||||
fileToggle.disabled = false;
|
||||
fileToggle.textContent = originalLabel;
|
||||
fileToggle.setAttribute('aria-label', 'Прикрепить файл');
|
||||
}
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenActions = (msg, event) => {
|
||||
const parsed = parseDmTechBlocks(String(msg?.text || ''));
|
||||
const readState = resolveEffectiveReadState(getChatMessages(chatId), msg);
|
||||
@@ -1465,7 +1647,7 @@ export function render({ navigate, route, chrome }) {
|
||||
: '',
|
||||
canReply: true,
|
||||
showReadAloud: isTextToSpeechReady,
|
||||
canEdit: msg?.from === 'out' && Number(msg?.messageType || 0) === 2 && !parsed?.callSummary,
|
||||
canEdit: msg?.from === 'out' && Number(msg?.messageType || 0) === 2 && !parsed?.callSummary && !parsed?.fileAttachment,
|
||||
canDelete: (
|
||||
(msg?.from === 'out' && Number(msg?.messageType || 0) === 2)
|
||||
|| (msg?.from === 'in' && Number(msg?.messageType || 0) === 1)
|
||||
@@ -1577,9 +1759,38 @@ export function render({ navigate, route, chrome }) {
|
||||
emojiToggle?.setAttribute('aria-expanded', 'false');
|
||||
emojiToggle?.addEventListener('pointerdown', (event) => {
|
||||
rememberEmojiSelection();
|
||||
emojiLongPressTriggered = false;
|
||||
clearEmojiLongPress();
|
||||
if (fileTransferEnabled) {
|
||||
emojiLongPressTimer = window.setTimeout(() => {
|
||||
emojiLongPressTimer = null;
|
||||
emojiLongPressTriggered = true;
|
||||
closeEmojiPicker();
|
||||
setFileActionVisible(true);
|
||||
}, 520);
|
||||
}
|
||||
try {
|
||||
emojiToggle.setPointerCapture?.(event.pointerId);
|
||||
} catch {
|
||||
// Pointer capture is optional; long press still works without it.
|
||||
}
|
||||
event.preventDefault();
|
||||
});
|
||||
emojiToggle?.addEventListener('click', () => {
|
||||
emojiToggle?.addEventListener('pointerup', clearEmojiLongPress);
|
||||
emojiToggle?.addEventListener('pointercancel', clearEmojiLongPress);
|
||||
emojiToggle?.addEventListener('pointerleave', (event) => {
|
||||
if (event.buttons) clearEmojiLongPress();
|
||||
});
|
||||
emojiToggle?.addEventListener('contextmenu', (event) => {
|
||||
if (fileTransferEnabled) event.preventDefault();
|
||||
});
|
||||
emojiToggle?.addEventListener('click', (event) => {
|
||||
if (emojiLongPressTriggered) {
|
||||
emojiLongPressTriggered = false;
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
setFileActionVisible(false);
|
||||
emojiPickerOpen = !emojiPickerOpen;
|
||||
emojiSlot.hidden = !emojiPickerOpen;
|
||||
emojiToggle.setAttribute('aria-expanded', String(emojiPickerOpen));
|
||||
@@ -1587,9 +1798,18 @@ export function render({ navigate, route, chrome }) {
|
||||
// открытии пикера прячем клавиатуру, чтобы поле ввода оставалось видно.
|
||||
if (emojiPickerOpen && !isFinePointer) input?.blur();
|
||||
});
|
||||
fileToggle?.addEventListener('click', () => {
|
||||
if (!fileTransferEnabled || fileToggle.disabled) return;
|
||||
fileInput?.click();
|
||||
});
|
||||
fileInput?.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0] || null;
|
||||
if (file) void handleSelectedFile(file);
|
||||
});
|
||||
screen.addEventListener('pointerdown', (event) => {
|
||||
if (!emojiPickerOpen || form.contains(event.target)) return;
|
||||
closeEmojiPicker();
|
||||
if (form.contains(event.target)) return;
|
||||
if (emojiPickerOpen) closeEmojiPicker();
|
||||
if (fileActionVisible) setFileActionVisible(false);
|
||||
});
|
||||
form.querySelector('.dm-send-btn')?.addEventListener('pointerdown', (event) => {
|
||||
event.preventDefault();
|
||||
@@ -1658,6 +1878,7 @@ export function render({ navigate, route, chrome }) {
|
||||
screen.cleanup = () => {
|
||||
hideUnreadSeparator({ rerender: false });
|
||||
stopAllTwemojiAnimations();
|
||||
clearEmojiLongPress();
|
||||
window.removeEventListener('shine-chat-messages-updated', handleIncomingChatRefresh);
|
||||
window.removeEventListener('shine-dm-delivery-updated', handleDeliveryRefresh);
|
||||
boundScrollContainer?.removeEventListener('scroll', handleHistoryScroll);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createTopBar } from '../components/topbar.js';
|
||||
import { addAppLogEntry, authService, closeSavedProfile, state } from '../state.js';
|
||||
import { isDeveloperToolsEnabled } from '../services/feature-settings.js';
|
||||
|
||||
export const pageMeta = { id: 'settings-view', title: 'Настройки' };
|
||||
|
||||
@@ -35,6 +36,24 @@ export function render({navigate, chrome}) {
|
||||
back: { label: '←', onClick: () => navigate('profile-view') },
|
||||
}));
|
||||
|
||||
const logoButton = document.createElement('button');
|
||||
logoButton.type = 'button';
|
||||
logoButton.className = 'settings-shine-logo-button';
|
||||
logoButton.setAttribute('aria-label', 'Логотип SHiNE');
|
||||
logoButton.title = 'SHiNE';
|
||||
logoButton.innerHTML = '<img class="settings-shine-logo" src="/img/shine-logo-transparent-final_big.png" alt="SHiNE" />';
|
||||
|
||||
let logoTapCount = 0;
|
||||
let lastLogoTapAt = 0;
|
||||
logoButton.addEventListener('click', () => {
|
||||
const now = Date.now();
|
||||
logoTapCount = now - lastLogoTapAt <= 900 ? logoTapCount + 1 : 1;
|
||||
lastLogoTapAt = now;
|
||||
if (logoTapCount < 5) return;
|
||||
logoTapCount = 0;
|
||||
navigate('advanced-settings-view');
|
||||
});
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
card.innerHTML = `
|
||||
@@ -93,61 +112,64 @@ export function render({navigate, chrome}) {
|
||||
}
|
||||
});
|
||||
|
||||
const versionCard = document.createElement('div');
|
||||
versionCard.className = 'card stack';
|
||||
screen.append(logoButton, card);
|
||||
|
||||
const title = document.createElement('p');
|
||||
title.className = 'field-label';
|
||||
title.textContent = 'Версии';
|
||||
if (isDeveloperToolsEnabled()) {
|
||||
const versionCard = document.createElement('div');
|
||||
versionCard.className = 'card stack';
|
||||
|
||||
const clientVersion = document.createElement('p');
|
||||
clientVersion.className = 'meta-muted';
|
||||
clientVersion.textContent = `Клиент: ${formatVersionForUi(window.__SHINE_CLIENT_VERSION__)}`;
|
||||
const title = document.createElement('p');
|
||||
title.className = 'field-label';
|
||||
title.textContent = 'Версии';
|
||||
|
||||
const uiBuild = document.createElement('p');
|
||||
uiBuild.className = 'meta-muted';
|
||||
uiBuild.textContent = `Сборка UI: ${formatVersionForUi(window.__SHINE_BUILD_HASH__)}`;
|
||||
const clientVersion = document.createElement('p');
|
||||
clientVersion.className = 'meta-muted';
|
||||
clientVersion.textContent = `Клиент: ${formatVersionForUi(window.__SHINE_CLIENT_VERSION__)}`;
|
||||
|
||||
const serverVersion = document.createElement('p');
|
||||
serverVersion.className = 'meta-muted';
|
||||
serverVersion.textContent = 'Сервер: загружается...';
|
||||
const uiBuild = document.createElement('p');
|
||||
uiBuild.className = 'meta-muted';
|
||||
uiBuild.textContent = `Сборка UI: ${formatVersionForUi(window.__SHINE_BUILD_HASH__)}`;
|
||||
|
||||
versionCard.append(title, clientVersion, uiBuild, serverVersion);
|
||||
const serverVersion = document.createElement('p');
|
||||
serverVersion.className = 'meta-muted';
|
||||
serverVersion.textContent = 'Сервер: загружается...';
|
||||
|
||||
const developerCard = document.createElement('div');
|
||||
developerCard.className = 'card stack';
|
||||
developerCard.innerHTML = `
|
||||
<button class="shine-btn shine-btn--settings" type="button" id="settings-developer">Настройки разработчика</button>
|
||||
`;
|
||||
developerCard.querySelector('#settings-developer').addEventListener('click', () => navigate('developer-settings-view'));
|
||||
versionCard.append(title, clientVersion, uiBuild, serverVersion);
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
let value = '';
|
||||
const developerCard = document.createElement('div');
|
||||
developerCard.className = 'card stack';
|
||||
developerCard.innerHTML = `
|
||||
<button class="shine-btn shine-btn--settings" type="button" id="settings-developer">Настройки разработчика</button>
|
||||
`;
|
||||
developerCard.querySelector('#settings-developer').addEventListener('click', () => navigate('developer-settings-view'));
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const pingResp = await authService.ws.request('Ping', { ts: Date.now() }, 7000);
|
||||
value = String(pingResp?.payload?.serverVersion || pingResp?.serverVersion || '').trim();
|
||||
} catch {
|
||||
// fallback below
|
||||
}
|
||||
let value = '';
|
||||
try {
|
||||
const pingResp = await authService.ws.request('Ping', { ts: Date.now() }, 7000);
|
||||
value = String(pingResp?.payload?.serverVersion || pingResp?.serverVersion || '').trim();
|
||||
} catch {
|
||||
// fallback below
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
const infoResp = await authService.ws.request('GetServerInfo', {});
|
||||
value = String(infoResp?.payload?.version || '').trim();
|
||||
if (!value) {
|
||||
const infoResp = await authService.ws.request('GetServerInfo', {});
|
||||
value = String(infoResp?.payload?.version || '').trim();
|
||||
}
|
||||
if (!isDisposed) serverVersion.textContent = `Сервер: ${formatVersionForUi(value)}`;
|
||||
} catch {
|
||||
if (!isDisposed) {
|
||||
serverVersion.textContent = 'Сервер: недоступно';
|
||||
}
|
||||
}
|
||||
if (!isDisposed) serverVersion.textContent = `Сервер: ${formatVersionForUi(value)}`;
|
||||
} catch {
|
||||
if (!isDisposed) {
|
||||
serverVersion.textContent = 'Сервер: недоступно';
|
||||
}
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
||||
screen.append(versionCard, developerCard);
|
||||
}
|
||||
|
||||
screen.cleanup = () => {
|
||||
isDisposed = true;
|
||||
};
|
||||
screen.append(card);
|
||||
screen.append(versionCard);
|
||||
screen.append(developerCard);
|
||||
return screen;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user