Files
WFL/templates/match_workspace.html

1862 lines
72 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Cache-Control"
content="no-store, no-cache, must-revalidate, max-age=0"
/>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>ЖФЛ Управление vMix</title>
<link rel="icon" href="/static/smith.ico" type="image/x-icon">
<link rel="stylesheet" href="/static/styles.css?v={{ static_version('styles.css') }}">
<style>
.player-link-btn,
.coach-link-btn {
display: block;
width: 100%;
padding: 0;
margin: 0;
border: none;
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
font: inherit;
}
.player-link-btn .last-name,
.player-link-btn .first-name,
.coach-link-btn .coach-name {
color: #8ab4ff;
text-decoration: underline;
text-underline-offset: 2px;
}
.player-link-btn:hover .last-name,
.player-link-btn:hover .first-name,
.coach-link-btn:hover .coach-name {
color: #a9c7ff;
}
.player-link-btn:focus-visible,
.coach-link-btn:focus-visible {
outline: 2px solid rgba(138, 180, 255, 0.45);
outline-offset: 2px;
border-radius: 6px;
}
.vmix-download-btn {
text-decoration: none;
}
.vmix-icon {
width: 10px;
height: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
background: linear-gradient(135deg, #ff5f6d 0%, #7b61ff 100%);
color: #ffffff;
font-size: 11px;
font-weight: 800;
font-family: Arial, sans-serif;
letter-spacing: 0.02em;
box-shadow: 0 0 10px rgba(123, 97, 255, 0.28);
object-fit: contain; /* чтобы не растягивалась */
display: block;
}
.vmix-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 10px;
background: #151515;
color: #fff;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.12);
transition: 0.2s ease;
}
.vmix-btn:hover {
background: #222;
}
</style>
</head>
<body>
<div class="page">
<div class="topbar">
<div class="topbar-inner">
<div class="match-header">
<div class="team-box">
{% if session[15] %}
<img
src="{{ session[15] }}"
alt="{{ session[14] }}"
class="team-logo"
/>
{% endif %}
<div>
<div class="team-name">{{ session[14] }}</div>
</div>
</div>
<div class="match-meta">
<div class="match-tour">{{ session[10] or "Тур не указан" }}</div>
<div class="match-time">
{{ session[9] or "Время не указано" }}
</div>
<div class="match-place">
{{ session[12] or "Стадион не указан" }}
</div>
</div>
<div class="team-box right">
<div>
<div class="team-name">{{ session[18] }}</div>
</div>
{% if session[19] %}
<img
src="{{ session[19] }}"
alt="{{ session[18] }}"
class="team-logo"
/>
{% endif %}
</div>
<div class="header-actions compact">
<form
method="post"
action="/admin/session/{{ session[3] }}/load-match-data"
id="loadMatchDataForm"
>
<button
type="submit"
class="icon-btn header-icon"
title="Загрузить данные"
>
🌐
</button>
</form>
<a href="/admin/session/{{ session[3] }}/download-vmix-page" class="vmix-btn">
<img src="/static/images/vmix_icon.png" class="vmix-icon" alt="vMix">
<a
href="/info"
target="_blank"
class="icon-btn header-icon"
title="Информация"
style="text-decoration: none;"
>
?
</a>
</a>
<form
method="post"
action="/admin/session/{{ session[3] }}/close"
>
<button
type="submit"
class="icon-btn header-icon"
title="Закрыть сессию"
>
</button>
</form>
<button
type="button"
id="editModeToggle"
class="icon-btn header-icon"
title="Режим редактирования"
onclick="toggleEditMode()"
>
✏️
</button>
{% if current_user and current_user.get("role") == "admin" %}
<a
href="/admin/db"
class="icon-btn header-icon"
title="База данных"
style="text-decoration: none; display: inline-flex; align-items: center; justify-content: center;"
>
🗄️
</a>
{% endif %}
<form method="post" action="/logout">
<button
type="submit"
class="icon-btn header-icon danger"
title="Выйти"
>
</button>
</form>
</div>
</div>
</div>
</div>
<div class="content">
<div
id="sessionStatusBanner"
class="empty-box"
style="
display: none;
margin-bottom: 16px;
border: 1px solid rgba(255, 193, 7, 0.35);
background: rgba(255, 193, 7, 0.08);
"
>
Сессия истекла. Выполняется переход на страницу входа…
</div>
<div class="tabs">
<a
class="tab-link {% if tab == 'game' %}active{% endif %}"
href="/admin/session/{{ session[3] }}?tab=game"
>Игра</a
>
<a
class="tab-link {% if tab == 'formations' %}active{% endif %}"
href="/admin/session/{{ session[3] }}?tab=formations"
>Расстановки команд</a
>
<a
class="tab-link {% if tab == 'referees' %}active{% endif %}"
href="/admin/session/{{ session[3] }}?tab=referees"
data-vmix-title="СУДЕЙСКАЯ БРИГАДА"
>Судьи</a
>
<a
class="tab-link {% if tab == 'tour_schedule' %}active{% endif %}"
href="/admin/session/{{ session[3] }}?tab=tour_schedule"
data-vmix-title="РАСПИСАНИЕ ТУРА"
>
Расписание тура
</a>
{% if is_russian_cup %}
<a
class="tab-link {% if tab == 'penalties' %}active{% endif %}"
href="/admin/session/{{ session[3] }}?tab=penalties"
data-vmix-title="ПЕНАЛЬТИ"
>
Пенальти
</a>
{% else %}
<a
class="tab-link {% if tab == 'standings' %}active{% endif %}"
href="/admin/session/{{ session[3] }}?tab=standings"
data-vmix-title="ТУРНИРНАЯ ТАБЛИЦА"
>
Турнирная таблица
</a>
{% endif %}
</div>
{% if tab == 'game' %}
<div class="game-layout">
<div class="game-main">
<div class="panel">
<div class="match-toolbar">
<div class="match-toolbar-left">
<button
type="button"
class="btn btn-primary"
data-period-btn="1H"
onclick="setMatchPeriod('1H')"
>
1-й тайм
</button>
<button
type="button"
class="btn btn-secondary"
data-period-btn="HT"
onclick="setMatchPeriod('HT')"
>
Перерыв
</button>
<button
type="button"
class="btn btn-primary"
data-period-btn="2H"
onclick="setMatchPeriod('2H')"
>
2-й тайм
</button>
<button
type="button"
class="btn btn-secondary"
data-period-btn="FT"
onclick="setMatchPeriod('FT')"
>
Завершить
</button>
</div>
<div class="match-toolbar-right">
<span class="period-badge" id="periodBadge"
>Матч не начат</span
>
<div class="timer-box timer-box-clickable" id="matchTimer" onclick="openClockEditor()" title="Нажмите, чтобы изменить время">00:00</div>
<div class="score-box" id="matchScore">0 : 0</div>
<div class="extra-time-wrap">
<div class="extra-time-control">
<input
type="number"
min="0"
step="1"
class="extra-time-input"
id="extraTimeMinutes"
placeholder="+мин"
/>
<div class="extra-time-arrows">
<button type="button" onclick="stepExtraTime(1)">
&#9650;
</button>
<button type="button" onclick="stepExtraTime(-1)">
&#9660;
</button>
</div>
</div>
<button
type="button"
class="btn btn-secondary"
onclick="applyExtraTime()"
>
Доп. время
</button>
</div>
</div>
</div>
</div>
<div class="lineups-grid">
<div class="panel">
{% if coaches.home %}
<div class="coaches-block" id="homeCoachesBlock">
{% for c in coaches.home %}
<div class="coach-line">
<button
type="button"
class="coach-left vmix-person-trigger player-link-btn coach-link-btn"
data-vmix-person-type="coach"
data-side="home"
data-name="{{ c.coach_name }}"
data-role="{{ c.role or '' }}"
title="Показать титр тренера"
>
<span class="coach-name">{{ c.coach_name }}</span>
{% if c.role %}
<span class="coach-badge">{{ c.role }}</span>
{% endif %}
</button>
<div class="coach-actions">
<button
type="button"
class="icon-btn yellow-card"
title="Жёлтая карточка"
data-coach-side="home"
data-coach-name="{{ c.coach_name }}"
data-card-type="yellow"
onclick="addCoachEvent('home', 'yellow', this.dataset.coachName, this)"
></button>
<button
type="button"
class="icon-btn red-card"
title="Красная карточка"
data-coach-side="home"
data-coach-name="{{ c.coach_name }}"
data-card-type="red"
onclick="addCoachEvent('home', 'red', this.dataset.coachName, this)"
></button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<div class="section-title">Основной состав</div>
{% if lineups.home_starting %}
<table class="game-lineup-table">
<thead>
<tr>
<th class="num-col">#</th>
<th class="player-col">Игрок</th>
<th class="actions-col">Действия</th>
</tr>
</thead>
<tbody>
{% for p in lineups.home_starting %}
<tr
class="lineup-row"
data-side="home"
data-role="starting"
data-player-id="{{ p.player_id | default(p.number) }}"
data-player-name="{{ p.player_name or '' }}"
data-player-last="{{ p.last_name or p.player_name or '' }}"
data-player-first="{{ p.first_name or '' }}"
data-player-number="{{ p.number or '' }}"
data-player-position="{{ p.position or '' }}"
data-player-captain="{{ 1 if p.is_captain else 0 }}"
data-player-photo-enabled="{{ 1 if p.photo_enabled else 0 }}"
>
<td class="num-cell">
<span class="num-with-photo-status">
{% if not p.photo_enabled %}
<span class="no-photo-icon" title="Фото выключено: в JSON будет подставлена EMPTY.png" aria-label="Фото выключено">📷</span>
{% endif %}
<span class="player-number-value">{{ p.number }}</span>
</span>
</td>
<td class="player-cell" title="Показать титр игрока">
<div
class="player-inline vmix-person-trigger player-link"
data-vmix-person-type="player"
data-side="home"
data-name="{{ (((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '') }}"
data-first-name="{{ p.first_name or '' }}"
data-last-name="{{ p.last_name or '' }}"
data-role="{{ p.position or '' }}"
>
<div class="player-inline-text">
<span class="last-name">{{ p.last_name or p.player_name }}</span>
{% if p.first_name %}
<span class="first-name">{{ p.first_name }}</span>
{% endif %}
</div>
<span class="badges">
{% if p.position and p.position|lower in ['вр.', 'вр', 'вратарь', 'gk', 'goalkeeper'] %}
<span class="gk-badge">ВР</span>
{% endif %}
{% if p.is_captain %}
<span class="captain-badge">К</span>
{% endif %}
</span>
</div>
</td> <td class="actions-col">
<div class="row-actions">
<button
type="button"
class="icon-btn goal"
title="Гол"
onclick="addPlayerEvent('home', 'goal', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }})"
>
</button>
<button
type="button"
class="icon-btn"
title="Автогол"
onclick="addPlayerEvent('home', 'own_goal', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }})"
>
АГ
</button>
<button
type="button"
class="icon-btn"
title="Пенальти"
onclick="addPlayerEvent('home', 'penalty', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }})"
>
П
</button>
<button
type="button"
class="icon-btn sub"
title="Замена"
onclick="beginSubstitution('home', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }}, this)"
>
</button>
<button
type="button"
class="icon-btn yellow-card"
title="Жёлтая карточка"
onclick="addPlayerEvent('home', 'yellow', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }})"
></button>
<button
type="button"
class="icon-btn red-card"
title="Красная карточка"
onclick="addPlayerEvent('home', 'red', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }})"
></button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-box">Основной состав пока не загружен.</div>
{% endif %}
<div class="section-title">Запасные</div>
<div class="bench-pick-hint" id="homeBenchHint">
Выбери игрока из запасных для завершения замены
</div>
{% if lineups.home_bench %}
<table class="game-lineup-table">
<thead>
<tr>
<th class="num-col">#</th>
<th class="player-col">Игрок</th>
<th class="actions-col">Действия</th>
</tr>
</thead>
<tbody>
{% for p in lineups.home_bench %}
<tr
class="lineup-row"
data-side="home"
data-role="bench"
data-player-id="{{ p.player_id | default(p.number) }}"
data-player-name="{{ p.player_name or '' }}"
data-player-last="{{ p.last_name or p.player_name or '' }}"
data-player-first="{{ p.first_name or '' }}"
data-player-number="{{ p.number or '' }}"
data-player-position="{{ p.position or '' }}"
data-player-captain="{{ 1 if p.is_captain else 0 }}"
data-player-photo-enabled="{{ 1 if p.photo_enabled else 0 }}"
>
<td class="num-cell">
<span class="num-with-photo-status">
{% if not p.photo_enabled %}
<span class="no-photo-icon" title="Фото выключено: в JSON будет подставлена EMPTY.png" aria-label="Фото выключено">📷</span>
{% endif %}
<span class="player-number-value">{{ p.number }}</span>
</span>
</td>
<td class="player-cell" title="Показать титр игрока">
<div
class="player-inline vmix-person-trigger player-link"
data-vmix-person-type="player"
data-side="home"
data-name="{{ (((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '') }}"
data-first-name="{{ p.first_name or '' }}"
data-last-name="{{ p.last_name or '' }}"
data-role="{{ p.position or '' }}"
>
<div class="player-inline-text">
<span class="last-name">{{ p.last_name or p.player_name }}</span>
{% if p.first_name %}
<span class="first-name">{{ p.first_name }}</span>
{% endif %}
</div>
<span class="badges">
{% if p.position and p.position|lower in ['вр.', 'вр', 'вратарь', 'gk', 'goalkeeper'] %}
<span class="gk-badge">ВР</span>
{% endif %}
{% if p.is_captain %}
<span class="captain-badge">К</span>
{% endif %}
</span>
</div>
</td> <td class="actions-col">
<div class="row-actions">
<button
type="button"
class="icon-btn sub"
title="Замена"
onclick="selectBenchPlayer('home', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }}, this)"
>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-box">Запасные пока не загружены.</div>
{% endif %}
</div>
<div class="panel">
{% if coaches.away %}
<div
class="coaches-block coaches-block-right"
id="awayCoachesBlock"
>
{% for c in coaches.away %}
<div class="coach-line">
<button
type="button"
class="coach-left vmix-person-trigger player-link-btn coach-link-btn"
data-vmix-person-type="coach"
data-side="away"
data-name="{{ c.coach_name }}"
data-role="{{ c.role or '' }}"
title="Показать титр тренера"
>
<span class="coach-name">{{ c.coach_name }}</span>
{% if c.role %}
<span class="coach-badge">{{ c.role }}</span>
{% endif %}
</button>
<div class="coach-actions">
<button
type="button"
class="icon-btn yellow-card"
title="Жёлтая карточка"
data-coach-side="away"
data-coach-name="{{ c.coach_name }}"
data-card-type="yellow"
onclick="addCoachEvent('away', 'yellow', this.dataset.coachName, this)"
></button>
<button
type="button"
class="icon-btn red-card"
title="Красная карточка"
data-coach-side="away"
data-coach-name="{{ c.coach_name }}"
data-card-type="red"
onclick="addCoachEvent('away', 'red', this.dataset.coachName, this)"
></button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<div class="section-title">Основной состав</div>
{% if lineups.away_starting %}
<table class="game-lineup-table">
<thead>
<tr>
<th class="num-col">#</th>
<th class="player-col">Игрок</th>
<th class="actions-col">Действия</th>
</tr>
</thead>
<tbody>
{% for p in lineups.away_starting %}
<tr
class="lineup-row"
data-side="away"
data-role="starting"
data-player-id="{{ p.player_id | default(p.number) }}"
data-player-name="{{ p.player_name or '' }}"
data-player-last="{{ p.last_name or p.player_name or '' }}"
data-player-first="{{ p.first_name or '' }}"
data-player-number="{{ p.number or '' }}"
data-player-position="{{ p.position or '' }}"
data-player-captain="{{ 1 if p.is_captain else 0 }}"
data-player-photo-enabled="{{ 1 if p.photo_enabled else 0 }}"
>
<td class="num-cell">
<span class="num-with-photo-status">
{% if not p.photo_enabled %}
<span class="no-photo-icon" title="Фото выключено: в JSON будет подставлена EMPTY.png" aria-label="Фото выключено">📷</span>
{% endif %}
<span class="player-number-value">{{ p.number }}</span>
</span>
</td>
<td class="player-cell" title="Показать титр игрока">
<div
class="player-inline vmix-person-trigger player-link"
data-vmix-person-type="player"
data-side="away"
data-name="{{ (((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '') }}"
data-first-name="{{ p.first_name or '' }}"
data-last-name="{{ p.last_name or '' }}"
data-role="{{ p.position or '' }}"
>
<div class="player-inline-text">
<span class="last-name">{{ p.last_name or p.player_name }}</span>
{% if p.first_name %}
<span class="first-name">{{ p.first_name }}</span>
{% endif %}
</div>
<span class="badges">
{% if p.position and p.position|lower in ['вр.', 'вр', 'вратарь', 'gk', 'goalkeeper'] %}
<span class="gk-badge">ВР</span>
{% endif %}
{% if p.is_captain %}
<span class="captain-badge">К</span>
{% endif %}
</span>
</div>
</td> <td class="actions-col">
<div class="row-actions">
<button
type="button"
class="icon-btn goal"
title="Гол"
onclick="addPlayerEvent('away', 'goal', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }})"
>
</button>
<button
type="button"
class="icon-btn"
title="Автогол"
onclick="addPlayerEvent('away', 'own_goal', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }})"
>
АГ
</button>
<button
type="button"
class="icon-btn"
title="Пенальти"
onclick="addPlayerEvent('away', 'penalty', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }})"
>
П
</button>
<button
type="button"
class="icon-btn sub"
title="Замена"
onclick="beginSubstitution('away', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }}, this)"
>
</button>
<button
type="button"
class="icon-btn yellow-card"
title="Жёлтая карточка"
onclick="addPlayerEvent('away', 'yellow', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }})"
></button>
<button
type="button"
class="icon-btn red-card"
title="Красная карточка"
onclick="addPlayerEvent('away', 'red', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }})"
></button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-box">Основной состав пока не загружен.</div>
{% endif %}
<div class="section-title">Запасные</div>
<div class="bench-pick-hint" id="awayBenchHint">
Выбери игрока из запасных для завершения замены
</div>
{% if lineups.away_bench %}
<table class="game-lineup-table">
<thead>
<tr>
<th class="num-col">#</th>
<th class="player-col">Игрок</th>
<th class="actions-col">Действия</th>
</tr>
</thead>
<tbody>
{% for p in lineups.away_bench %}
<tr
class="lineup-row"
data-side="away"
data-role="bench"
data-player-id="{{ p.player_id | default(p.number) }}"
data-player-name="{{ p.player_name or '' }}"
data-player-last="{{ p.last_name or p.player_name or '' }}"
data-player-first="{{ p.first_name or '' }}"
data-player-number="{{ p.number or '' }}"
data-player-position="{{ p.position or '' }}"
data-player-captain="{{ 1 if p.is_captain else 0 }}"
data-player-photo-enabled="{{ 1 if p.photo_enabled else 0 }}"
>
<td class="num-cell">
<span class="num-with-photo-status">
{% if not p.photo_enabled %}
<span class="no-photo-icon" title="Фото выключено: в JSON будет подставлена EMPTY.png" aria-label="Фото выключено">📷</span>
{% endif %}
<span class="player-number-value">{{ p.number }}</span>
</span>
</td>
<td class="player-cell" title="Показать титр игрока">
<div
class="player-inline vmix-person-trigger player-link"
data-vmix-person-type="player"
data-side="away"
data-name="{{ (((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '') }}"
data-first-name="{{ p.first_name or '' }}"
data-last-name="{{ p.last_name or '' }}"
data-role="{{ p.position or '' }}"
>
<div class="player-inline-text">
<span class="last-name">{{ p.last_name or p.player_name }}</span>
{% if p.first_name %}
<span class="first-name">{{ p.first_name }}</span>
{% endif %}
</div>
<span class="badges">
{% if p.position and p.position|lower in ['вр.', 'вр', 'вратарь', 'gk', 'goalkeeper'] %}
<span class="gk-badge">ВР</span>
{% endif %}
{% if p.is_captain %}
<span class="captain-badge">К</span>
{% endif %}
</span>
</div>
</td> <td class="actions-col">
<div class="row-actions">
<button
type="button"
class="icon-btn sub"
title="Замена"
onclick="selectBenchPlayer('away', {{ p.player_id | default(p.number) | tojson }}, {{ ((((p.first_name or '') ~ ' ' ~ (p.last_name or '')) | trim) or (p.player_name or '')) | tojson }}, this)"
>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-box">Запасные пока не загружены.</div>
{% endif %}
</div>
</div>
</div>
<div class="panel events-panel">
<div class="panel-title-row">
<div>
<div class="panel-title">События матча</div>
<div class="small-meta">
Слева хозяева, справа гости. Время идёт сверху вниз по общей
шкале.
</div>
</div>
<button
type="button"
class="btn btn-secondary"
onclick="openClearMatchModal()"
>
Очистить
</button>
</div>
<div class="events-list" id="eventsList">
<div class="empty-box">Событий пока нет.</div>
</div>
</div>
</div>
{% elif tab == 'formations' %}
<div class="lineups-grid">
<div class="panel">
<div class="team-panel-title">Расстановка — {{ session[14] }}</div>
<div class="formation-toolbar">
<select id="home-preset" class="formation-select" data-edit-lock onchange="saveSelectedPreset('home')">
<option value="4-4-2">4-4-2</option>
<option value="4-3-3">4-3-3</option>
<option value="4-2-3-1">4-2-3-1</option>
<option value="3-5-2">3-5-2</option>
<option value="5-3-2">5-3-2</option>
<option value="4-1-4-1">4-1-4-1</option>
<option value="3-4-3">3-4-3</option>
<option value="4-5-1">4-5-1</option>
<option value="5-4-1">5-4-1</option>
<option value="4-3-1-2">4-3-1-2</option>
<option value="3-6-1">3-6-1</option>
</select>
<button
type="button"
class="btn btn-secondary"
data-edit-lock
onclick="applyPreset('home')"
>
Применить расстановку
</button>
<button
type="button"
class="btn btn-primary"
data-edit-lock
onclick="saveFormation('home')"
>
Сохранить
</button>
</div>
<div class="pitch-half" id="pitch-home">
<div class="pitch-center-line"></div>
<div class="pitch-center-circle"></div>
</div>
</div>
<div class="panel">
<div class="team-panel-title">Расстановка — {{ session[18] }}</div>
<div class="formation-toolbar">
<select id="away-preset" class="formation-select" data-edit-lock onchange="saveSelectedPreset('away')">
<option value="4-4-2">4-4-2</option>
<option value="4-3-3">4-3-3</option>
<option value="4-2-3-1">4-2-3-1</option>
<option value="3-5-2">3-5-2</option>
<option value="5-3-2">5-3-2</option>
<option value="4-1-4-1">4-1-4-1</option>
<option value="3-4-3">3-4-3</option>
<option value="4-5-1">4-5-1</option>
<option value="5-4-1">5-4-1</option>
<option value="4-3-1-2">4-3-1-2</option>
<option value="3-6-1">3-6-1</option>
</select>
<button
type="button"
class="btn btn-secondary"
data-edit-lock
onclick="applyPreset('away')"
>
Применить расстановку
</button>
<button
type="button"
class="btn btn-primary"
data-edit-lock
onclick="saveFormation('away')"
>
Сохранить
</button>
</div>
<div class="pitch-half" id="pitch-away">
<div class="pitch-center-line"></div>
<div class="pitch-center-circle"></div>
</div>
</div>
</div>
{% elif tab == 'referees' %}
<div class="panel">
<div class="team-panel-title">Судейская бригада</div>
<!-- сюда JS рендерит таблицу -->
<div id="refereesEditorWrap"></div>
<!-- кнопки редактирования -->
<div class="referee-edit-controls" data-edit-lock-wrap>
<button
type="button"
class="btn btn-secondary"
onclick="addRefereeFromSelect()"
>
Добавить судью
</button>
<button
type="button"
class="btn btn-primary"
onclick="saveReferees()"
>
Сохранить
</button>
</div>
</div>
{% elif tab == 'tour_schedule' %}
<div class="panel">
<div class="team-panel-title">Расписание тура</div>
{% set month_names = { 1: 'января', 2: 'февраля', 3: 'марта', 4:
'апреля', 5: 'мая', 6: 'июня', 7: 'июля', 8: 'августа', 9: 'сентября',
10: 'октября', 11: 'ноября', 12: 'декабря' } %} {% if tour_schedule %}
<table class="table-compact tour-table">
<thead>
<tr>
<th class="tour-date-col">Дата</th>
<th class="tour-match-col">Матч</th>
<th class="tour-meta-col">Счёт / статус</th>
<th class="tour-stadium-col">Стадион</th>
<th class="tour-channel-col">Канал</th>
<th class="tour-actions-col tour-edit-only">Правка</th>
</tr>
</thead>
<tbody>
{% for m in tour_schedule %}
{% set row_key = m.match_external_id %}
{% set home_score_value = '' if m.home_score is none else m.home_score %}
{% set away_score_value = '' if m.away_score is none else m.away_score %}
<tr class="tour-schedule-row" data-schedule-row="{{ row_key }}">
<td class="tour-date-cell">
{% if m.match_date %}
{{ m.match_date.day }} {{ month_names[m.match_date.month] }}
{{ m.match_date.year }}
{% if m.match_date.hour != 0 or m.match_date.minute != 0 %}
{{ "%02d:%02d"|format(m.match_date.hour, m.match_date.minute) }}
{% endif %} {% else %} — {% endif %}
</td>
<td class="tour-match-cell">
{{ m.home_team_name }} — {{ m.away_team_name }}
{% if m.match_external_id == session[8] %}
<span class="gk-badge">ТЕКУЩИЙ</span>
{% endif %}
</td>
<td class="tour-meta-cell">
<div class="tour-meta-wrap tour-meta-display" data-schedule-display="{{ row_key }}">
{% if m.home_score is not none and m.away_score is not none %}
<span class="score-badge">{{ m.home_score }} : {{ m.away_score }}</span>
{% endif %}
{% if m.status == 'finished' %}
<span class="status-badge status-finished">Завершён</span>
{% elif m.status == 'live' %}
<span class="status-badge status-live">Live</span>
{% elif m.status == 'scheduled' %}
<span class="status-badge status-scheduled">Не начался</span>
{% else %}
<span class="status-badge status-scheduled">{{ m.status or "—" }}</span>
{% endif %}
</div>
<div class="tour-score-edit" data-schedule-edit="{{ row_key }}" hidden>
<div class="tour-edit-line">
<div class="tour-score-inputs" aria-label="Счёт">
<input
type="number"
min="0"
class="tour-score-input"
data-match-id="{{ row_key }}"
data-score-side="home"
value="{{ home_score_value }}"
data-initial-value="{{ home_score_value }}"
placeholder="—"
disabled
>
<span class="tour-score-separator">:</span>
<input
type="number"
min="0"
class="tour-score-input"
data-match-id="{{ row_key }}"
data-score-side="away"
value="{{ away_score_value }}"
data-initial-value="{{ away_score_value }}"
placeholder="—"
disabled
>
</div>
<select
class="tour-status-select"
data-match-id="{{ row_key }}"
data-initial-status="{{ m.status or 'scheduled' }}"
disabled
>
<option value="scheduled" {% if m.status == 'scheduled' or not m.status %}selected{% endif %}>Не начался</option>
<option value="live" {% if m.status == 'live' %}selected{% endif %}>Live</option>
<option value="finished" {% if m.status == 'finished' %}selected{% endif %}>Завершён</option>
</select>
</div>
</div>
</td>
<td class="stadium-cell">
{{ m.stadium_name or "—" }}
</td>
<td class="tour-channel-cell">
{% set channel = m.channel or "" %}
<label class="channel-check">
<input
type="checkbox"
class="tour-channel-checkbox"
data-channel="match"
data-match-id="{{ m.match_external_id }}"
onchange="saveTourChannel('{{ m.match_external_id }}')"
{% if channel in ['match', 'match_and_premier'] %}checked{% endif %}
>
Матч
</label>
<label class="channel-check">
<input
type="checkbox"
class="tour-channel-checkbox"
data-channel="match_premier"
data-match-id="{{ m.match_external_id }}"
onchange="saveTourChannel('{{ m.match_external_id }}')"
{% if channel in ['match_premier', 'match_and_premier'] %}checked{% endif %}
>
Матч Премьер
</label>
</td>
<td class="tour-actions-cell tour-edit-only">
<div class="tour-save-actions" data-schedule-save-actions="{{ row_key }}" hidden>
<button
type="button"
class="btn btn-primary tour-save-btn"
onclick="saveTourScheduleMatch('{{ row_key }}')"
title="Сохранить счёт и статус"
></button>
<button
type="button"
class="btn btn-secondary tour-cancel-btn"
onclick="cancelTourScheduleEdit('{{ row_key }}')"
title="Отмена"
>×</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-box">Расписание тура не найдено.</div>
{% endif %}
</div>
{% elif tab == 'standings' %}
<div class="panel">
<div class="team-panel-title">Турнирная таблица</div>
{% if standings %}
<table class="standings-table">
<thead>
<tr>
<th class="st-pos-col">#</th>
<th class="st-logo-col"></th>
<th class="st-team-col">Команда</th>
<th class="st-stat-col">И</th>
<th class="st-stat-col">В</th>
<th class="st-stat-col">Н</th>
<th class="st-stat-col">П</th>
<th class="st-gfga-col">М</th>
<th class="st-points-col">О</th>
</tr>
</thead>
<tbody>
{% for s in standings %}
<tr
class="{% if s.team_name == session[14] %}standings-row-home{% elif s.team_name == session[18] %}standings-row-away{% endif %}"
>
<td class="num-cell">{{ s.position }}</td>
<td class="st-logo-col">
{% if s.team_logo %}
<img
src="{{ s.team_logo }}"
alt="{{ s.team_name }}"
class="standings-logo"
/>
{% endif %}
</td>
<td class="player-name">{{ s.team_name }}</td>
<td class="num-cell">{{ s.played }}</td>
<td class="num-cell">{{ s.wins }}</td>
<td class="num-cell">{{ s.draws }}</td>
<td class="num-cell">{{ s.losses }}</td>
<td class="num-cell">
{{ s.goals_for }}-{{ s.goals_against }}
</td>
<td class="num-cell">{{ s.points }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-box">Турнирная таблица пока не загружена.</div>
{% endif %}
</div>
{% elif tab == 'penalties' and is_russian_cup %}
<div class="panel penalty-panel" id="penaltyPanel">
<div class="penalty-header">
<div>
<div class="team-panel-title">Серия пенальти</div>
<div class="penalty-subtitle">Быстро отмечай удары для Кубка России. В vMix всегда уходят 5 строк: 15, затем 610 снова в строки 15.</div>
</div>
<div class="penalty-header-actions">
<button type="button" class="btn btn-secondary" onclick="addPenaltyRound()">+ Добавить серию</button>
<button type="button" class="btn btn-secondary" onclick="deleteLastPenaltyRound()">Удалить последнюю серию</button>
<button type="button" class="btn btn-danger" onclick="clearPenaltyShootout()">Очистить</button>
</div>
</div>
<div class="penalty-scoreboard">
<div class="penalty-team-card home">
<div class="penalty-team-name">{{ session[14] }}</div>
<div class="penalty-team-score" id="penaltyHomeScore">0</div>
<div class="penalty-next-label" id="penaltyHomeNext">Следующий удар: 1</div>
<div class="penalty-fast-actions">
<button type="button" class="penalty-fast-btn scored" onclick="setNextPenaltyShot('home', 'scored')">Забил</button>
<button type="button" class="penalty-fast-btn missed" onclick="setNextPenaltyShot('home', 'missed')">Не забил</button>
</div>
</div>
<div class="penalty-team-card away">
<div class="penalty-team-name">{{ session[18] }}</div>
<div class="penalty-team-score" id="penaltyAwayScore">0</div>
<div class="penalty-next-label" id="penaltyAwayNext">Следующий удар: 1</div>
<div class="penalty-fast-actions">
<button type="button" class="penalty-fast-btn scored" onclick="setNextPenaltyShot('away', 'scored')">Забил</button>
<button type="button" class="penalty-fast-btn missed" onclick="setNextPenaltyShot('away', 'missed')">Не забил</button>
</div>
</div>
</div>
<div class="penalty-table-wrap">
<table class="penalty-table">
<thead>
<tr>
<th>Серия</th>
<th>{{ session[14] }}</th>
<th>{{ session[18] }}</th>
</tr>
</thead>
<tbody id="penaltyRoundsBody">
<tr>
<td colspan="3" class="empty-box">Загрузка серии пенальти…</td>
</tr>
</tbody>
</table>
</div>
<div class="penalty-footer">
<div class="penalty-help">Любой удар можно исправить прямо в таблице: нажми “Забил”, “Не забил” или “Очистить” в нужной ячейке. После 5 ударов продолжай 6-й, 7-й и дальше — для vMix они будут записываться в первые 5 строк нового блока.</div>
</div>
</div>
{% else %}
<div class="panel">
<div class="empty-box">Эта вкладка пока в разработке.</div>
</div>
{% endif %}
</div>
</div>
<script>
window.MATCH_DATA = {
teamNames: {
home: {{ session[14] | tojson }},
away: {{ session[18] | tojson }}
},
teamLogos: {
home: {{ session[15] | tojson }},
away: {{ session[19] | tojson }}
},
vmixTeamLogos: {
home: {{ session[16] | tojson }},
away: {{ session[20] | tojson }}
},
sessionToken: {{ session[3] | tojson }},
matchId: {{ session[1] | tojson }},
sourceKey: {{ source_key | tojson }},
isRussianCup: {{ is_russian_cup | tojson }},
homeFormations: {{ home_formations | tojson }},
awayFormations: {{ away_formations | tojson }},
homeStarting: {{ lineups.home_starting | tojson }},
awayStarting: {{ lineups.away_starting | tojson }},
homeBench: {{ lineups.home_bench | tojson }},
awayBench: {{ lineups.away_bench | tojson }},
homeCoaches: {{ coaches.home | tojson }},
awayCoaches: {{ coaches.away | tojson }},
referees: {{ referees | tojson }},
refereePool: {{ referee_pool | tojson }},
currentUser: {{ current_user | tojson }},
homeSquadPool: [],
awaySquadPool: [],
homeCoachPool: [],
awayCoachPool: []
};
</script>
<div class="formation-modal" id="clockEditorModal">
<div class="formation-modal-backdrop" onclick="closeClockEditor()"></div>
<div class="formation-modal-content clock-editor-modal-content">
<div class="formation-modal-header">
<div>
<div class="formation-modal-title">Редактировать время</div>
<div class="formation-modal-subtitle" id="clockEditorHint"></div>
</div>
<button type="button" class="modal-close-btn" onclick="closeClockEditor()">×</button>
</div>
<div class="formation-modal-body clock-editor-body">
<label class="clock-editor-label" for="clockEditorInput">Текущее матчевое время</label>
<input
type="text"
class="clock-editor-input"
id="clockEditorInput"
placeholder="12:30"
inputmode="numeric"
onkeydown="if (event.key === 'Enter') applyClockEditor(); if (event.key === 'Escape') closeClockEditor();"
/>
<div class="clock-editor-note">Можно ввести 12:30, 57:20 или 01:02:03.</div>
</div>
<div class="formation-modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeClockEditor()">Отмена</button>
<button type="button" class="btn btn-primary" onclick="applyClockEditor()">Применить в интерфейсе и vMix</button>
</div>
</div>
</div>
<div class="formation-modal" id="formationAssignModal">
<div
class="formation-modal-backdrop"
onclick="closeFormationAssignModal()"
></div>
<div class="formation-modal-content">
<div class="formation-modal-header">
<div>
<div class="formation-modal-title">Заполнение расстановки</div>
<div
class="formation-modal-subtitle"
id="formationAssignSubtitle"
></div>
</div>
<button
type="button"
class="modal-close-btn"
onclick="closeFormationAssignModal()"
>
</button>
</div>
<div class="formation-modal-body">
<div id="formationAssignTableWrap"></div>
</div>
<div class="formation-modal-footer">
<button
type="button"
class="btn btn-cancel"
onclick="closeFormationAssignModal()"
>
Отмена
</button>
<button
type="button"
class="btn btn-primary"
onclick="confirmFormationAssign()"
>
Применить игроков
</button>
</div>
</div>
</div>
<div class="formation-modal" id="goalEditorModal">
<div class="formation-modal-backdrop" onclick="closeGoalEditor()"></div>
<div class="formation-modal-content" style="width: min(560px, 96vw);">
<div class="formation-modal-header">
<div>
<div class="formation-modal-title">Редактирование гола</div>
<div class="formation-modal-subtitle" id="goalEditorTitle"></div>
</div>
<button
type="button"
class="btn btn-secondary"
onclick="closeGoalEditor()"
>
Закрыть
</button>
</div>
<div class="formation-modal-body">
<div id="goalEditorList" class="goal-editor-list"></div>
</div>
</div>
</div>
<div class="formation-modal captain-required-modal" id="captainRequiredModal">
<div class="formation-modal-backdrop" onclick="closeCaptainRequiredWarning()"></div>
<div class="formation-modal-content captain-required-modal-content">
<div class="captain-required-icon">!</div>
<div class="captain-required-copy">
<div class="formation-modal-title">Не выбран капитан</div>
<div class="captain-required-text" id="captainRequiredText"></div>
</div>
<div class="formation-modal-footer captain-required-actions">
<button type="button" class="btn btn-cancel" onclick="closeCaptainRequiredWarning()">
Закрыть
</button>
<button type="button" class="btn btn-confirm" onclick="openCaptainEditorFromWarning()">
Выбрать капитана
</button>
</div>
</div>
</div>
<div class="formation-modal" id="squadEditorModal">
<div class="formation-modal-backdrop" onclick="closeSquadEditor()"></div>
<div class="formation-modal-content squad-editor-modal-content">
<div class="formation-modal-header">
<div>
<div class="formation-modal-title">Редактирование состава</div>
<div class="formation-modal-subtitle">
Добавление, удаление игроков, капитаны и тренеры
</div>
</div>
<button
type="button"
class="modal-close-btn"
onclick="closeSquadEditor()"
>
</button>
</div>
<div class="formation-modal-body">
<div class="squad-editor-grid">
<div class="squad-editor-team">
<div class="squad-editor-team-title">{{ session[14] }}</div>
<div id="squadEditorHome"></div>
</div>
<div class="squad-editor-team">
<div class="squad-editor-team-title">{{ session[18] }}</div>
<div id="squadEditorAway"></div>
</div>
</div>
</div>
<div class="formation-modal-footer">
<button
type="button"
class="btn btn-cancel"
onclick="closeSquadEditor()"
>
Отмена
</button>
<button
type="button"
class="btn btn-confirm"
onclick="applySquadEditorChanges()"
>
Сохранить изменения
</button>
</div>
</div>
</div>
<script>
(function () {
const LOGIN_URL = "/login";
const LOGOUT_URL = "/logout";
const IDLE_LIMIT_MS = 2 * 60 * 60 * 1000;
const WARNING_BEFORE_MS = 60 * 1000;
const ACTIVITY_EVENTS = ["mousemove", "mousedown", "keydown", "scroll", "touchstart", "click"];
let lastActivityAt = Date.now();
let warned = false;
let redirected = false;
const banner = document.getElementById("sessionStatusBanner");
function showBanner(message) {
if (!banner) return;
banner.textContent = message;
banner.style.display = "block";
}
function markActivity() {
lastActivityAt = Date.now();
warned = false;
if (banner) banner.style.display = "none";
}
function redirectToLogin(reason) {
if (redirected) return;
redirected = true;
const url = new URL(LOGIN_URL, window.location.origin);
if (reason) url.searchParams.set("reason", reason);
window.location.replace(url.toString());
}
function isHtmlNavigationRequest(input, init) {
const method = (init && init.method ? init.method : "GET").toUpperCase();
if (method !== "GET") return false;
const headers = new Headers((init && init.headers) || {});
const accept = headers.get("Accept") || "";
return accept.includes("text/html");
}
const nativeFetch = window.fetch.bind(window);
window.fetch = async function (input, init) {
const response = await nativeFetch(input, init);
if ([401, 403, 419, 440].includes(response.status)) {
showBanner("Сессия истекла. Выполняется переход на страницу входа…");
redirectToLogin("expired");
}
if (response.redirected && response.url && response.url.includes("/login")) {
redirectToLogin("expired");
}
if (!response.ok && isHtmlNavigationRequest(input, init)) {
const clone = response.clone();
try {
const text = await clone.text();
if (text && text.toLowerCase().includes("name=\"username\"")) {
redirectToLogin("expired");
}
} catch (e) {}
}
return response;
};
document.addEventListener("submit", function (event) {
const form = event.target;
if (!(form instanceof HTMLFormElement)) return;
if (form.action && form.action.endsWith(LOGOUT_URL)) return;
markActivity();
}, true);
ACTIVITY_EVENTS.forEach((eventName) => {
window.addEventListener(eventName, markActivity, { passive: true });
});
setInterval(() => {
const idleFor = Date.now() - lastActivityAt;
const leftMs = IDLE_LIMIT_MS - idleFor;
if (!warned && leftMs <= WARNING_BEFORE_MS && leftMs > 0) {
warned = true;
const leftMinutes = Math.ceil(leftMs / 60000);
showBanner(`До выхода из системы из-за неактивности осталось около ${leftMinutes} мин.`);
}
if (idleFor >= IDLE_LIMIT_MS) {
showBanner("Сессия завершена из-за 2 часов неактивности.");
redirectToLogin("idle");
}
}, 15000);
})();
</script>
<script>
(function () {
const matchId = window.MATCH_DATA?.matchId;
const sessionToken = window.MATCH_DATA?.sessionToken;
const teamLogos = window.MATCH_DATA?.teamLogos || {};
const vmixTeamLogos = window.MATCH_DATA?.vmixTeamLogos || {};
const currentUserLogin = window.MATCH_DATA?.currentUser?.username || null;
if (!matchId || !sessionToken) {
return;
}
function formatCoachName(rawName) {
const parts = String(rawName || "").trim().split(/\s+/).filter(Boolean);
if (parts.length >= 2) {
return `${parts[1]} ${parts[0]}`;
}
return String(rawName || "").trim();
}
function formatPlayerName(firstName, lastName, fallbackName) {
const result = `${String(firstName || "").trim()} ${String(lastName || "").trim()}`.trim();
return result || String(fallbackName || "").trim();
}
async function sendNeutralPersonTitle(name, role, side) {
const inputName = "ИГРОК - НЕЙТРАЛЬНЫЙ";
const teamName = TEAM_NAMES?.[side] || "";
const logoPath = getVmixTeamLogo(teamName);
const commands = [
`/api/?Function=SetText&Input=${encodeURIComponent(inputName)}&SelectedName=${encodeURIComponent("Тренер.Text")}&Value=${encodeURIComponent(name || "")}`,
`/api/?Function=SetText&Input=${encodeURIComponent(inputName)}&SelectedName=${encodeURIComponent("Должность.Text")}&Value=${encodeURIComponent(role || "")}`,
`/api/?Function=SetImage&Input=${encodeURIComponent(inputName)}&SelectedName=${encodeURIComponent("Лого.Source")}&Value=${encodeURIComponent(logoPath || "")}`,
`/api/?Function=PreviewInput&Input=${encodeURIComponent(inputName)}`
];
try {
const response = await fetch("/api/vmix/publish-command", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Requested-With": "XMLHttpRequest"
},
credentials: "same-origin",
body: JSON.stringify({
match_id: matchId,
commands,
target_login: currentUserLogin,
meta: {
source: "person_click",
title_input: inputName,
person_name: name,
person_role: role,
side,
team_name: teamName,
logo_path: logoPath
}
})
});
if (!response.ok) {
console.error("Ошибка отправки титра в vMix:", response.status);
}
} catch (error) {
console.error("Не удалось отправить титр в vMix:", error);
}
}
document.addEventListener("click", function (event) {
const trigger = event.target.closest(".vmix-person-trigger");
if (!trigger) return;
const personType = String(trigger.dataset.vmixPersonType || "").trim();
const side = String(trigger.dataset.side || "").trim();
const rawName = String(trigger.dataset.name || "").trim();
const firstName = String(trigger.dataset.firstName || "").trim();
const lastName = String(trigger.dataset.lastName || "").trim();
const role = String(trigger.dataset.role || "").trim();
let formattedName = "";
if (personType === "coach") {
formattedName = formatCoachName(rawName);
} else {
formattedName = formatPlayerName(firstName, lastName, rawName);
}
if (!formattedName) return;
sendNeutralPersonTitle(formattedName, role, side);
});
})();
</script>
<script src="/static/script.js?v={{ static_version('script.js') }}"></script>
<div id="matrixLoader" class="matrix-loader" aria-hidden="true">
<canvas id="matrixCanvas" class="matrix-canvas"></canvas>
<div class="matrix-content">
<div class="matrix-title" id="matrixTitle">Загрузка данных матча</div>
<div class="matrix-status" id="matrixStatusText">
Подключение к сайту...
</div>
</div>
</div>
<script>
(function () {
const loadForm = document.getElementById("loadMatchDataForm");
const loader = document.getElementById("matrixLoader");
const titleEl = document.getElementById("matrixTitle");
const statusText = document.getElementById("matrixStatusText");
const canvas = document.getElementById("matrixCanvas");
const ctx = canvas ? canvas.getContext("2d") : null;
let matrixAnimationId = null;
let drops = [];
const fontSize = 18;
const chars = "アイウエオカキクケコサシスセソ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZЖФЛ<>+*#";
let columns = 0;
let lastTime = 0;
const fps = 18;
const interval = 1000 / fps;
function resizeCanvas() {
if (!canvas || !ctx) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
columns = Math.floor(canvas.width / fontSize);
drops = Array(columns).fill(1);
}
function drawMatrix(time = 0) {
if (!canvas || !ctx) return;
if (time - lastTime < interval) {
matrixAnimationId = requestAnimationFrame(drawMatrix);
return;
}
lastTime = time;
ctx.fillStyle = "rgba(0, 0, 0, 0.12)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#00ff66";
ctx.font = fontSize + "px monospace";
for (let i = 0; i < drops.length; i++) {
const text = chars[Math.floor(Math.random() * chars.length)];
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
if (drops[i] * fontSize > canvas.height && Math.random() > 0.985) {
drops[i] = 0;
}
// drops[i] += 0.65;
drops[i] ++;
}
matrixAnimationId = requestAnimationFrame(drawMatrix);
}
function startLoader(title, message) {
if (!loader) return;
loader.classList.add("active");
loader.classList.remove("is-error", "is-success");
if (titleEl) titleEl.textContent = title || "Загрузка данных матча";
if (statusText) statusText.textContent = message || "Подключение к сайту...";
document.body.style.overflow = "hidden";
resizeCanvas();
if (!matrixAnimationId) {
drawMatrix();
}
}
function setLoaderState(state, title, message) {
if (!loader) return;
loader.classList.remove("is-error", "is-success");
if (state) {
loader.classList.add(state);
}
if (titleEl) titleEl.textContent = title || "";
if (statusText) statusText.textContent = message || "";
}
window.addEventListener("resize", resizeCanvas);
if (!loadForm) return;
loadForm.addEventListener("submit", async function (event) {
event.preventDefault();
const submitButton = loadForm.querySelector('button[type="submit"]');
if (submitButton) {
submitButton.disabled = true;
}
startLoader("Загрузка данных матча", "Подключение к сайту...");
try {
const response = await fetch(loadForm.action, {
method: "POST",
headers: {
"X-Requested-With": "XMLHttpRequest",
"Accept": "application/json"
},
credentials: "same-origin"
});
let data = {};
try {
data = await response.json();
} catch (_) {}
if (!response.ok || !data.success) {
throw new Error(data.error || "Не удалось загрузить данные с сайта");
}
setLoaderState(
"is-success",
"Загрузка завершена",
(data.message || "Данные успешно загружены") + "\nПереход во вкладку «Игра»..."
);
setTimeout(() => {
window.location.href = data.redirect_url || "{{ '/admin/session/' ~ session[3] ~ '?tab=game' }}";
}, 1200);
} catch (error) {
setLoaderState(
"is-error",
"Ошибка загрузки",
(error.message || "Не удалось загрузить данные с сайта") + "\nВозврат во вкладку «Игра» через 3 секунды..."
);
setTimeout(() => {
window.location.href = "{{ '/admin/session/' ~ session[3] ~ '?tab=game' }}";
}, 3000);
} finally {
if (submitButton) {
submitButton.disabled = false;
}
}
});
})();
</script>
<div class="formation-modal" id="clearMatchModal">
<div
class="formation-modal-backdrop"
onclick="closeClearMatchModal()"
></div>
<div class="formation-modal-content clear-match-modal-content">
<div class="formation-modal-header">
<div>
<div class="formation-modal-title">
Очистить матч
</div>
<div class="formation-modal-subtitle">
Это действие удалит все события, счет и время матча
</div>
</div>
<button
type="button"
class="modal-close-btn"
onclick="closeClearMatchModal()"
>
×
</button>
</div>
<div class="formation-modal-body">
<div class="clear-match-warning">
Вы точно уверены, что хотите очистить данные матча?
</div>
</div>
<div class="formation-modal-footer">
<button
type="button"
class="btn btn-secondary"
onclick="closeClearMatchModal()"
>
Отмена
</button>
<button
type="button"
class="btn btn-danger"
onclick="confirmClearMatch()"
>
Очистить
</button>
</div>
</div>
</div>
</body>
</html>