415 lines
13 KiB
TypeScript
415 lines
13 KiB
TypeScript
// ui.ts - Управление UI элементами
|
||
|
||
export interface DOMElements {
|
||
serverTime: HTMLElement | null;
|
||
status: HTMLElement | null;
|
||
meta: HTMLElement | null;
|
||
latest: HTMLElement | null;
|
||
lastByte: HTMLElement | null;
|
||
history: HTMLElement | null;
|
||
canvas: HTMLCanvasElement | null;
|
||
bars: NodeListOf<HTMLElement>;
|
||
barFill: HTMLElement | null;
|
||
bar2: HTMLElement | null;
|
||
bar3: HTMLElement | null;
|
||
soundLevel: HTMLElement | null;
|
||
heightLevel: HTMLElement | null;
|
||
historyHeader: HTMLElement | null;
|
||
historyBody: HTMLElement | null;
|
||
audioIndicator: HTMLElement | null;
|
||
}
|
||
|
||
export const DOM: DOMElements = {
|
||
serverTime: null,
|
||
status: null,
|
||
meta: null,
|
||
latest: null,
|
||
lastByte: null,
|
||
history: null,
|
||
canvas: null,
|
||
bars: document.querySelectorAll(".sig-bar"),
|
||
barFill: null,
|
||
bar2: null,
|
||
bar3: null,
|
||
soundLevel: null,
|
||
heightLevel: null,
|
||
historyHeader: null,
|
||
historyBody: null,
|
||
audioIndicator: null,
|
||
};
|
||
|
||
export function initDOM(): void {
|
||
DOM.serverTime = document.getElementById("server-time");
|
||
DOM.status = document.getElementById("status");
|
||
DOM.meta = document.getElementById("meta");
|
||
DOM.latest = document.getElementById("latest");
|
||
DOM.lastByte = document.getElementById("last-byte");
|
||
DOM.history = document.getElementById("history");
|
||
DOM.canvas = document.getElementById("signalChart") as HTMLCanvasElement;
|
||
DOM.barFill = document.getElementById("bar1");
|
||
DOM.bar2 = document.getElementById("bar2");
|
||
DOM.bar3 = document.getElementById("bar3");
|
||
DOM.soundLevel = document.getElementById("sound-level");
|
||
DOM.heightLevel = document.getElementById("height-level");
|
||
DOM.historyHeader = document.getElementById("history-header");
|
||
DOM.historyBody = document.getElementById("history-body");
|
||
DOM.bars = document.querySelectorAll(".sig-bar");
|
||
DOM.audioIndicator = document.getElementById("audio-indicator");
|
||
}
|
||
|
||
export function setServerTime(v: string): void {
|
||
if (DOM.serverTime) DOM.serverTime.textContent = v;
|
||
}
|
||
|
||
export function setStatus(v: string, isError: boolean = false): void {
|
||
if (!DOM.status) return;
|
||
DOM.status.textContent = v;
|
||
|
||
DOM.status.classList.remove("status-ok", "status-error", "status-lost");
|
||
|
||
if (isError) {
|
||
if (v === "Нет связи") {
|
||
DOM.status.classList.add("status-lost");
|
||
} else {
|
||
DOM.status.classList.add("status-error");
|
||
}
|
||
} else {
|
||
DOM.status.classList.add("status-ok");
|
||
}
|
||
}
|
||
|
||
export function setMeta(v: string): void {
|
||
if (DOM.meta) DOM.meta.textContent = v;
|
||
}
|
||
|
||
// ===== ОСНОВНОЙ ПРОГРЕСС-БАР =====
|
||
export function setBars(p: number, noConnection: boolean = false): void {
|
||
const el = DOM.barFill;
|
||
if (!el) return;
|
||
|
||
if (noConnection) {
|
||
el.style.width = "0%";
|
||
el.classList.add("no-connection");
|
||
} else {
|
||
el.classList.remove("no-connection");
|
||
const percent = Math.max(0, Math.min(100, p));
|
||
el.style.width = `${percent}%`;
|
||
el.style.background = '#e6852d';
|
||
el.style.backgroundColor = '#e6852d';
|
||
el.style.backgroundImage = 'none';
|
||
}
|
||
}
|
||
|
||
// ===== ИНДИКАТОРЫ УРОВНЯ СИГНАЛА =====
|
||
export function setSignal(level: number, noConnection: boolean = false): void {
|
||
const bars = DOM.bars;
|
||
if (!bars || bars.length < 3) return;
|
||
|
||
bars.forEach(bar => {
|
||
bar.classList.remove("active-1", "active-2", "active-3", "dim", "no-connection");
|
||
if (noConnection) {
|
||
bar.classList.add("no-connection");
|
||
} else {
|
||
bar.classList.add("dim");
|
||
}
|
||
});
|
||
|
||
if (noConnection) return;
|
||
|
||
if (level >= 1) {
|
||
bars[0].classList.add("active-1");
|
||
bars[0].classList.remove("dim");
|
||
}
|
||
if (level >= 2) {
|
||
bars[1].classList.add("active-2");
|
||
bars[1].classList.remove("dim");
|
||
}
|
||
if (level >= 3) {
|
||
bars[2].classList.add("active-3");
|
||
bars[2].classList.remove("dim");
|
||
}
|
||
}
|
||
|
||
export function setAudioIndicator(active: boolean, level: number = 0): void {
|
||
const el = DOM.audioIndicator;
|
||
if (!el) return;
|
||
|
||
// Проверяем громкость
|
||
const volume = localStorage.getItem('gport_volume');
|
||
const currentVolume = volume ? parseInt(volume) : 0;
|
||
|
||
if (active && level > 0 && currentVolume > 0) {
|
||
el.textContent = '🔊';
|
||
el.style.color = '#00ff88';
|
||
el.style.opacity = '1';
|
||
el.style.animation = 'pulse 1s ease-in-out infinite';
|
||
el.classList.remove('muted');
|
||
el.classList.add('active');
|
||
} else {
|
||
el.textContent = '🔇';
|
||
el.style.color = '#666';
|
||
el.style.opacity = '0.5';
|
||
el.style.animation = 'none';
|
||
el.classList.add('muted');
|
||
el.classList.remove('active');
|
||
}
|
||
}
|
||
|
||
export function setSoundLevel(level: number): void {
|
||
if (DOM.bar2) {
|
||
const percent = Math.max(0, Math.min(100, level));
|
||
DOM.bar2.style.width = `${percent}%`;
|
||
}
|
||
if (DOM.soundLevel) {
|
||
DOM.soundLevel.textContent = Math.round(level).toString();
|
||
}
|
||
}
|
||
|
||
export function setHeightLevel(level: number): void {
|
||
if (DOM.bar3) {
|
||
const percent = Math.max(0, Math.min(100, level));
|
||
DOM.bar3.style.width = `${percent}%`;
|
||
}
|
||
if (DOM.heightLevel) {
|
||
DOM.heightLevel.textContent = Math.round(level).toString();
|
||
}
|
||
}
|
||
|
||
// ===== 8 КАНАЛОВ =====
|
||
export interface ChannelState {
|
||
value: number; // Максимальное значение (для прогресс-бара)
|
||
currentValue: number; // Текущее значение (для отображения)
|
||
strength: number; // Максимальный уровень
|
||
currentStrength: number; // Текущий уровень
|
||
expanded: boolean;
|
||
}
|
||
|
||
export const channels: ChannelState[] = Array(8).fill(null).map(() => ({
|
||
value: 0,
|
||
currentValue: 0,
|
||
strength: 0,
|
||
currentStrength: 0,
|
||
expanded: true
|
||
}));
|
||
|
||
export let channelElements: any[] = [];
|
||
|
||
export function initChannels() {
|
||
const container = document.getElementById('channelsContainer');
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '';
|
||
channelElements = [];
|
||
|
||
for (let i = 0; i < 8; i++) {
|
||
const item = document.createElement('div');
|
||
item.className = 'channel-item';
|
||
item.dataset.channel = String(i);
|
||
|
||
if (i === 0) {
|
||
item.classList.add('channel-active');
|
||
}
|
||
|
||
item.innerHTML = `
|
||
<div class="channel-header" data-channel="${i}">
|
||
<span class="channel-label">Канал ${i + 1}</span>
|
||
<span style="display:flex;align-items:center;gap:6px;">
|
||
<span class="channel-value" id="ch-val-${i}">--</span>
|
||
<span class="channel-toggle" id="ch-toggle-${i}">▼</span>
|
||
</span>
|
||
</div>
|
||
<div class="channel-body" id="ch-body-${i}">
|
||
<div class="channel-progress">
|
||
<div class="progress">
|
||
<div class="fill" id="ch-bar-${i}" style="width:0%; background: #e6852d;"></div>
|
||
</div>
|
||
<div class="signal-icon" id="ch-signal-${i}">
|
||
<div class="sig-bar sig-bar-1"></div>
|
||
<div class="sig-bar sig-bar-2"></div>
|
||
<div class="sig-bar sig-bar-3"></div>
|
||
</div>
|
||
</div>
|
||
<div class="channel-stats" id="ch-stats-${i}">--</div>
|
||
</div>
|
||
`;
|
||
|
||
container.appendChild(item);
|
||
|
||
channelElements[i] = {
|
||
item: item,
|
||
value: document.getElementById(`ch-val-${i}`),
|
||
bar: document.getElementById(`ch-bar-${i}`),
|
||
signal: document.getElementById(`ch-signal-${i}`),
|
||
stats: document.getElementById(`ch-stats-${i}`),
|
||
toggle: document.getElementById(`ch-toggle-${i}`),
|
||
body: document.getElementById(`ch-body-${i}`)
|
||
};
|
||
|
||
const header = item.querySelector('.channel-header');
|
||
header?.addEventListener('click', () => {
|
||
toggleChannel(i);
|
||
});
|
||
}
|
||
}
|
||
|
||
export function toggleChannel(index: number) {
|
||
const ch = channels[index];
|
||
ch.expanded = !ch.expanded;
|
||
|
||
const el = channelElements[index];
|
||
if (!el) return;
|
||
|
||
if (ch.expanded) {
|
||
el.body.classList.remove('hidden');
|
||
el.toggle.textContent = '▼';
|
||
} else {
|
||
el.body.classList.add('hidden');
|
||
el.toggle.textContent = '▶';
|
||
}
|
||
}
|
||
|
||
export function getChannelElement(index: number) {
|
||
return channelElements[index] || null;
|
||
}
|
||
|
||
/**
|
||
* Установка МАКСИМАЛЬНОГО значения для прогресс-бара
|
||
* (используется для отображения пика)
|
||
*/
|
||
export function setChannelValue(index: number, value: number, strength: number) {
|
||
if (index < 0 || index > 7) return;
|
||
|
||
const ch = channels[index];
|
||
const el = channelElements[index];
|
||
if (!el) return;
|
||
|
||
ch.value = value;
|
||
ch.strength = strength;
|
||
|
||
const maxValue = 63;
|
||
const percent = (value / maxValue) * 100;
|
||
|
||
// Обновляем прогресс-бар
|
||
if (el.bar) {
|
||
el.bar.style.width = `${Math.min(100, percent)}%`;
|
||
el.bar.style.background = '#e6852d';
|
||
el.bar.style.backgroundColor = '#e6852d';
|
||
el.bar.style.backgroundImage = 'none';
|
||
}
|
||
|
||
// Обновляем индикатор сигнала (уровень)
|
||
const bars = el.signal?.querySelectorAll('.sig-bar');
|
||
if (bars) {
|
||
bars.forEach((bar: Element, i: number) => {
|
||
const isActive = i < strength;
|
||
(bar as HTMLElement).style.opacity = isActive ? '1' : '0.2';
|
||
});
|
||
}
|
||
|
||
// Обновляем статистику
|
||
const strengthNames = ['Отсутствует', 'Низкая', 'Средняя', 'Высокая'];
|
||
if (el.stats) {
|
||
el.stats.textContent = `${ch.currentValue}/${maxValue} | ${strengthNames[ch.currentStrength] || '---'} (пик: ${value})`;
|
||
}
|
||
|
||
if (el.item) {
|
||
el.item.classList.remove('no-data');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Установка ТЕКУЩЕГО значения для отображения в цифрах
|
||
* (используется для показа реального состояния)
|
||
*/
|
||
export function setChannelCurrentValue(index: number, value: number, strength: number) {
|
||
if (index < 0 || index > 7) return;
|
||
|
||
const ch = channels[index];
|
||
const el = channelElements[index];
|
||
if (!el) return;
|
||
|
||
ch.currentValue = value;
|
||
ch.currentStrength = strength;
|
||
|
||
// Обновляем числовое значение
|
||
if (el.value) {
|
||
el.value.textContent = value.toString();
|
||
}
|
||
|
||
// Обновляем статистику (показываем текущее значение и пик)
|
||
const maxValue = 63;
|
||
const strengthNames = ['Отсутствует', 'Низкая', 'Средняя', 'Высокая'];
|
||
if (el.stats) {
|
||
el.stats.textContent = `${value}/${maxValue} | ${strengthNames[strength] || '---'} (пик: ${ch.value})`;
|
||
}
|
||
|
||
// Обновляем индикатор сигнала (текущий уровень)
|
||
const bars = el.signal?.querySelectorAll('.sig-bar');
|
||
if (bars) {
|
||
bars.forEach((bar: Element, i: number) => {
|
||
const isActive = i < strength;
|
||
// Если текущий уровень меньше максимального - добавляем класс dim
|
||
if (isActive) {
|
||
(bar as HTMLElement).classList.remove('dim');
|
||
(bar as HTMLElement).classList.add('active-' + (i + 1));
|
||
} else {
|
||
(bar as HTMLElement).classList.add('dim');
|
||
(bar as HTMLElement).classList.remove('active-' + (i + 1));
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
export function updateAllChannels(rawData?: number[]) {
|
||
for (let i = 0; i < 8; i++) {
|
||
setChannelValue(i, 0, 0);
|
||
setChannelCurrentValue(i, 0, 0);
|
||
if (channelElements[i]) {
|
||
channelElements[i].item.classList.remove('channel-active');
|
||
}
|
||
}
|
||
|
||
if (rawData && rawData.length > 0) {
|
||
const lastByte = rawData[rawData.length - 1];
|
||
const value = lastByte & 0x3F;
|
||
const strength = (lastByte >> 6) & 0x3;
|
||
|
||
setChannelValue(0, value, strength);
|
||
setChannelCurrentValue(0, value, strength);
|
||
if (channelElements[0]) {
|
||
channelElements[0].item.classList.add('channel-active');
|
||
}
|
||
|
||
if (DOM.lastByte) {
|
||
DOM.lastByte.textContent = value.toString();
|
||
}
|
||
} else {
|
||
if (DOM.lastByte) {
|
||
DOM.lastByte.textContent = '--';
|
||
}
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initChannels();
|
||
updateAllChannels();
|
||
|
||
const expandAll = document.getElementById('expandAllBtn');
|
||
const collapseAll = document.getElementById('collapseAllBtn');
|
||
|
||
if (expandAll) {
|
||
expandAll.addEventListener('click', () => {
|
||
channels.forEach((ch, i) => {
|
||
if (!ch.expanded) toggleChannel(i);
|
||
});
|
||
});
|
||
}
|
||
|
||
if (collapseAll) {
|
||
collapseAll.addEventListener('click', () => {
|
||
channels.forEach((ch, i) => {
|
||
if (ch.expanded) toggleChannel(i);
|
||
});
|
||
});
|
||
}
|
||
}); |