Files
WFL/templates/admin_db_index.html

1109 lines
43 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">
<title>База</title>
<link rel="icon" href="/static/smith.ico" type="image/x-icon">
<style>
:root {
--bg: #0f1115;
--panel: #171a21;
--panel-2: #1d222b;
--border: #2b3240;
--text: #e8ecf3;
--muted: #9aa4b2;
--accent: #00ff88;
--accent-hover: #00d976;
--success-bg: rgba(46, 204, 113, 0.12);
--success-border: rgba(46, 204, 113, 0.35);
--danger-bg: rgba(255, 99, 99, 0.12);
--danger-border: rgba(255, 99, 99, 0.35);
--shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
--radius: 16px;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: Arial, sans-serif;
}
.page {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 20px;
}
.title {
font-size: 24px;
font-weight: 700;
margin-bottom: 18px;
}
.tabs,
.parser-actions,
.account-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.parser-actions,
.account-actions {
margin-top: 18px;
padding-top: 18px;
border-top: 1px solid var(--border);
}
.tab-link,
.action-btn,
.account-toggle-btn,
.submit-btn,
.cancel-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 0 16px;
border-radius: 999px;
background: var(--panel-2);
border: 1px solid var(--border);
color: var(--text);
text-decoration: none;
font-weight: 600;
cursor: pointer;
font-size: 14px;
transition: 0.2s ease;
}
.tab-link:hover,
.action-btn:hover,
.account-toggle-btn:hover,
.submit-btn:hover {
background: var(--accent);
border-color: var(--accent);
color: #04110b;
}
.cancel-btn:hover {
background: #293140;
border-color: #3b4658;
}
.action-form,
.account-create-form {
margin: 0;
}
.parser-import-card {
width: 100%;
padding: 18px;
border: 1px solid rgba(0, 255, 136, 0.18);
border-radius: 16px;
background: rgba(255, 255, 255, 0.025);
}
.parser-import-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 6px;
}
.parser-import-subtitle {
color: var(--muted);
font-size: 13px;
line-height: 1.45;
margin-bottom: 16px;
}
.parser-import-grid {
display: grid;
grid-template-columns: minmax(260px, 360px) minmax(0, 1fr);
gap: 16px;
align-items: start;
}
.parser-checkboxes {
display: grid;
grid-template-columns: repeat(4, minmax(120px, 1fr));
gap: 10px;
}
.parser-checkbox {
display: flex;
align-items: center;
gap: 8px;
min-height: 44px;
padding: 0 12px;
border-radius: 12px;
border: 1px solid var(--border);
background: #0f141d;
color: var(--text);
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.parser-checkbox input {
accent-color: var(--accent);
}
.parser-submit-row {
margin-top: 16px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.parser-mode-note {
color: var(--muted);
font-size: 12px;
line-height: 1.45;
}
@media (max-width: 900px) {
.parser-import-grid,
.parser-checkboxes {
grid-template-columns: 1fr;
}
}
.log-block {
margin-top: 20px;
border: 1px solid var(--border);
border-radius: 14px;
padding: 16px;
background: var(--panel-2);
}
.log-block.success {
background: var(--success-bg);
border-color: var(--success-border);
}
.log-block.error {
background: var(--danger-bg);
border-color: var(--danger-border);
}
.log-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 10px;
}
.log-status {
color: var(--muted);
margin-bottom: 10px;
}
.log-output {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
line-height: 1.5;
}
.action-form.is-loading .action-btn {
opacity: 0.75;
pointer-events: none;
}
.account-modal,
.update-modal,
.settings-modal {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(3, 8, 15, 0.84);
z-index: 9998;
padding: 20px;
}
.account-modal.active,
.update-modal.active,
.settings-modal.active {
display: flex;
}
.account-modal-card,
.update-modal-card,
.settings-modal-card {
width: min(100%, 460px);
background: rgba(12, 17, 26, 0.98);
border: 1px solid rgba(0, 255, 136, 0.28);
border-radius: 18px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
padding: 24px;
}
.update-modal-card,
.settings-modal-card {
width: min(100%, 760px);
max-height: 92vh;
overflow-y: auto;
}
.account-modal-title,
.update-modal-title,
.settings-modal-title {
font-size: 22px;
font-weight: 700;
margin-bottom: 8px;
}
.account-modal-subtitle,
.update-modal-subtitle,
.settings-modal-subtitle {
color: var(--muted);
margin-bottom: 18px;
line-height: 1.5;
}
.form-grid {
display: grid;
gap: 14px;
}
.env-groups {
display: grid;
gap: 16px;
}
.env-group {
border: 1px solid var(--border);
border-radius: 14px;
padding: 14px;
background: rgba(255, 255, 255, 0.025);
}
.env-group-title {
font-size: 16px;
font-weight: 700;
margin-bottom: 4px;
}
.env-group-subtitle {
color: var(--muted);
font-size: 12px;
margin-bottom: 12px;
}
.env-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.env-field-wide {
grid-column: 1 / -1;
}
@media (max-width: 720px) {
.env-grid {
grid-template-columns: 1fr;
}
}
.field-label {
display: block;
font-size: 13px;
font-weight: 700;
color: var(--muted);
margin-bottom: 6px;
}
.field-input,
.field-select,
.field-textarea {
width: 100%;
min-height: 44px;
border-radius: 12px;
border: 1px solid var(--border);
background: #0f141d;
color: var(--text);
padding: 0 14px;
font-size: 14px;
outline: none;
}
.field-textarea {
min-height: 96px;
padding: 12px 14px;
resize: vertical;
line-height: 1.45;
font-family: Arial, sans-serif;
}
.field-input:focus,
.field-select:focus,
.field-textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.12);
}
.update-form-grid {
display: grid;
grid-template-columns: 1fr 180px;
gap: 14px;
}
.update-items-editor {
display: grid;
gap: 14px;
}
.update-item-form {
display: grid;
grid-template-columns: 160px minmax(0, 1fr);
gap: 12px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 14px;
background: rgba(255, 255, 255, 0.025);
}
.update-item-text,
.update-item-image {
grid-column: 1 / -1;
}
.helper-text {
color: var(--muted);
font-size: 12px;
line-height: 1.45;
}
.format-guide {
margin-top: 8px;
padding: 10px 12px;
border: 1px solid rgba(0, 255, 136, 0.18);
border-radius: 12px;
background: rgba(0, 255, 136, 0.045);
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
.format-guide code {
color: #00ff88;
background: rgba(0, 255, 136, 0.10);
border: 1px solid rgba(0, 255, 136, 0.22);
border-radius: 7px;
padding: 1px 6px;
}
.format-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.format-btn {
min-height: 30px;
padding: 0 10px;
border: 1px solid rgba(0, 255, 136, 0.28);
border-radius: 999px;
color: #9cf3c8;
background: rgba(0, 255, 136, 0.055);
cursor: pointer;
font-size: 12px;
}
.format-btn:hover {
color: #00ff88;
background: rgba(0, 255, 136, 0.12);
}
.add-row-btn {
min-height: 38px;
padding: 0 14px;
border-radius: 999px;
border: 1px dashed rgba(0, 255, 136, 0.45);
background: rgba(0, 255, 136, 0.06);
color: var(--accent);
font-weight: 700;
cursor: pointer;
}
.add-row-btn:hover {
background: rgba(0, 255, 136, 0.12);
}
@media (max-width: 720px) {
.update-form-grid,
.update-item-form {
grid-template-columns: 1fr;
}
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 18px;
flex-wrap: wrap;
}
.matrix-loader {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(3, 8, 15, 0.94);
z-index: 9999;
overflow: hidden;
}
.matrix-loader.active {
display: flex;
}
.matrix-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.matrix-content {
position: relative;
z-index: 1;
width: min(90vw, 560px);
padding: 28px 24px;
border-radius: 18px;
border: 1px solid rgba(0, 255, 136, 0.35);
background: rgba(12, 17, 26, 0.88);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
text-align: center;
backdrop-filter: blur(6px);
}
.matrix-title {
font-size: 28px;
font-weight: 700;
color: #dcffef;
margin-bottom: 12px;
}
.matrix-status {
font-size: 16px;
color: #9cf3c8;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="page">
<div class="panel">
<div class="title">Редактирование базы</div>
<div class="tabs">
{% if current_user and current_user.role == "admin" %}
<a class="tab-link" href="/admin/db/create?entity=player"> Создать запись</a>
{% endif %}
<a class="tab-link" href="/admin/db/players">Игроки</a>
<a class="tab-link" href="/admin/db/referees">Судьи</a>
<a class="tab-link" href="/admin/db/teams">Команды</a>
<a class="tab-link" href="/admin/db/coaches">Тренеры</a>
<a class="tab-link" href="/admin/db/stadiums">Стадионы</a>
<a class="tab-link" href="/admin/matches">Назад к матчам</a>
</div>
<div class="parser-actions">
<form method="post" action="/admin/db/run-parser" class="action-form parser-import-card" data-parser-title="Выбранные данные" data-parser-status="Загрузка выбранных данных из выбранного турнира...">
<div class="parser-import-title">Импорт данных</div>
<div class="parser-import-subtitle">
Выберите источник турнира и отметьте, какие данные нужно заграбить. Существующие команды, игроки и матчи будут обновлены через UPSERT, без полной очистки таблиц.
</div>
<div class="parser-import-grid">
<div>
<label class="field-label" for="parserSource">Источник / турнир</label>
{% set sources = parser_sources.values() if parser_sources is mapping else parser_sources|default([]) %}
<select class="field-select" id="parserSource" name="parser_source" required>
{% if sources %}
{% for source in sources %}
<option value="{{ source.key }}" data-logo-base-path="{{ source.logo_base_path or '' }}" data-photo-base-path="{{ source.photo_base_path or '' }}" {% if source.key == default_parser_source_key %}selected{% endif %}>
{{ source.title }}
</option>
{% endfor %}
{% else %}
<option value="SUPERLEAGUE" data-logo-base-path="D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Teams Logos" data-photo-base-path="D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo" selected>Суперлига 2026</option>
<option value="RUSSIAN_CUP" data-logo-base-path="D:\Графика\ФУТБОЛ\Кубок России 2026\Teams Logos" data-photo-base-path="D:\Графика\ФУТБОЛ\Кубок России 2026\Photo">Кубок России 2026</option>
{% endif %}
</select>
<div class="helper-text">Список источников хранится в <b>базе данных</b> и редактируется в настройках проекта.</div>
<div class="helper-text">Путь к логотипам: <b id="selectedLogoBasePath"></b></div>
<div class="helper-text">Путь к фотографиям: <b id="selectedPhotoBasePath"></b></div>
</div>
<div>
<label class="field-label">Что парсить</label>
<div class="parser-checkboxes">
<label class="parser-checkbox">
<input type="checkbox" name="parser_names" value="teams" checked>
<span>Команды</span>
</label>
<label class="parser-checkbox">
<input type="checkbox" name="parser_names" value="players" checked>
<span>Игроки</span>
</label>
<label class="parser-checkbox">
<input type="checkbox" name="parser_names" value="schedule" checked>
<span>Расписание</span>
</label>
<label class="parser-checkbox">
<input type="checkbox" name="parser_names" value="standings">
<span>Турнирка</span>
</label>
</div>
</div>
</div>
<div class="parser-submit-row">
<button type="submit" class="action-btn">🗄️ Заграбить выбранное</button>
<div class="parser-mode-note">Безопасный режим: добавляет новые записи и обновляет существующие. Турнирка по-прежнему пересобирается только за выбранный сезон.</div>
</div>
</form>
</div>
<div class="account-actions">
<button type="button" class="account-toggle-btn" id="openCreateAccountBtn"> Создать аккаунт</button>
<button type="button" class="account-toggle-btn" id="openSettingsBtn">⚙️ Настройки проекта</button>
<button type="button" class="account-toggle-btn" id="openAddUpdateBtn">📝 Добавить обновление</button>
</div>
{% if account_output %}
<div class="log-block {% if account_success %}success{% else %}error{% endif %}">
<div class="log-title">Создание аккаунта</div>
<div class="log-status">
Статус:
{% if account_success %}
успешно
{% else %}
ошибка
{% endif %}
</div>
<pre class="log-output">{{ account_output }}</pre>
</div>
{% endif %}
{% if parser_output %}
<div class="log-block {% if parser_success %}success{% else %}error{% endif %}">
<div class="log-title">Итог парсинга{% if parser_name %}: {{ parser_name }}{% endif %}</div>
<div class="log-status">
Статус:
{% if parser_success %}
успешно
{% else %}
ошибка
{% endif %}
</div>
<pre class="log-output">{{ parser_output }}</pre>
</div>
{% endif %}
{% if settings_output %}
<div class="log-block {% if settings_success %}success{% else %}error{% endif %}">
<div class="log-title">Сохранение настроек проекта</div>
<div class="log-status">
Статус:
{% if settings_success %}
успешно
{% else %}
ошибка
{% endif %}
</div>
<pre class="log-output">{{ settings_output }}</pre>
</div>
{% endif %}
{% if update_output %}
<div class="log-block {% if update_success %}success{% else %}error{% endif %}">
<div class="log-title">Добавление обновления</div>
<div class="log-status">
Статус:
{% if update_success %}
успешно
{% else %}
ошибка
{% endif %}
</div>
<pre class="log-output">{{ update_output }}</pre>
</div>
{% endif %}
</div>
</div>
<div id="accountModal" class="account-modal" aria-hidden="true">
<div class="account-modal-card">
<div class="account-modal-title">Создать аккаунт</div>
<div class="account-modal-subtitle">Заполните данные нового пользователя и выберите роль.</div>
<form method="post" action="/admin/db/create-account" class="account-create-form" id="createAccountForm">
<div class="form-grid">
<div>
<label class="field-label" for="accountUsername">Логин</label>
<input class="field-input" id="accountUsername" type="text" name="username" required>
</div>
<div>
<label class="field-label" for="accountPassword">Пароль</label>
<input class="field-input" id="accountPassword" type="password" name="password" required>
</div>
<div>
<label class="field-label" for="accountRole">Роль</label>
<select class="field-select" id="accountRole" name="role" required>
<option value="admin">admin</option>
<option value="operator">operator</option>
</select>
</div>
</div>
<div class="modal-actions">
<button type="button" class="cancel-btn" id="closeCreateAccountBtn">Отмена</button>
<button type="submit" class="submit-btn">Создать</button>
</div>
</form>
</div>
</div>
<div id="settingsModal" class="settings-modal" aria-hidden="true">
<div class="settings-modal-card">
<div class="settings-modal-title">Настройки проекта</div>
<div class="settings-modal-subtitle">
Здесь редактируются источники парсинга: ссылки, сезон, тип календаря, папки логотипов и фотографий. Эти данные хранятся в базе данных.
<br>.env оставляем только для NAS и подключения к базе.
</div>
<form method="post" action="/admin/db/project-settings" id="settingsForm">
{% if project_settings and project_settings.groups %}
<div class="env-groups">
{% for group in project_settings.groups %}
<div class="env-group">
<div class="env-group-title">{{ group.title }}</div>
{% if group.subtitle %}
<div class="env-group-subtitle">{{ group.subtitle }}</div>
{% endif %}
<div class="env-grid">
{% for field in group.fields %}
<div class="{% if field.wide %}env-field-wide{% endif %}">
<label class="field-label" for="env_{{ field.key }}">{{ field.label }}</label>
{% if field.type == 'select' %}
<select class="field-select" id="env_{{ field.key }}" name="{{ field.key }}">
{% for option in field.options %}
<option value="{{ option.value }}" {% if option.value == field.value %}selected{% endif %}>{{ option.label }}</option>
{% endfor %}
</select>
{% else %}
<input class="field-input" id="env_{{ field.key }}" type="text" name="{{ field.key }}" value="{{ field.value }}">
{% endif %}
{% if field.help %}
<div class="helper-text">{{ field.help }}</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="helper-text">Не удалось загрузить список настроек проекта из базы данных.</div>
{% endif %}
<div class="modal-actions">
<button type="button" class="cancel-btn" id="closeSettingsBtn">Отмена</button>
<button type="submit" class="submit-btn">Сохранить настройки</button>
</div>
</form>
</div>
</div>
<div id="updateModal" class="update-modal" aria-hidden="true">
<div class="update-modal-card">
<div class="update-modal-title">Добавить обновление</div>
<div class="update-modal-subtitle">
Заполните версию, дату, действия для оператора и пункты изменений. При необходимости добавьте путь к картинке. После сохранения новая версия появится сверху на странице /info.
</div>
<form method="post" action="/admin/db/add-update" id="addUpdateForm">
<div class="form-grid">
<div class="update-form-grid">
<div>
<label class="field-label" for="updateVersion">Версия</label>
<input class="field-input" id="updateVersion" type="text" name="version" placeholder="1.4" required>
</div>
<div>
<label class="field-label" for="updateDate">Дата</label>
<input class="field-input" id="updateDate" type="date" name="date" required>
</div>
</div>
<div>
<label class="field-label" for="operatorActions">Что сделать оператору</label>
<textarea class="field-textarea" id="operatorActions" name="operator_actions" placeholder="Каждое действие с новой строки">Скачать новый проект vMix перед работой с матчем.
Полностью заменить все файлы проекта.</textarea>
<div class="helper-text">Каждая строка станет отдельным пунктом списка. Можно выделять клавиши и важные слова.</div>
<div class="format-guide">
Формат: <code>`F5`</code> — клавиша/команда, <code>**важно**</code> — выделенное слово.
</div>
<div class="format-toolbar" data-format-for="operatorActions">
<button type="button" class="format-btn" data-snippet="`F5`">⌨️ Клавиша</button>
<button type="button" class="format-btn" data-snippet="**важно**">✨ Выделить слово</button>
</div>
</div>
<div>
<label class="field-label">Пункты обновления</label>
<div class="update-items-editor" id="updateItemsEditor">
<div class="update-item-form">
<div>
<label class="field-label">Тип</label>
<select class="field-select" name="item_badge">
<option value="new">Новое</option>
<option value="fix">Исправлено</option>
<option value="improve">Улучшено</option>
<option value="important">Важно</option>
</select>
</div>
<div>
<label class="field-label">Заголовок</label>
<input class="field-input" type="text" name="item_title" placeholder="Например: Верхний счёт" required>
</div>
<div class="update-item-text">
<label class="field-label">Описание</label>
<textarea class="field-textarea" name="item_text" placeholder="Пример:
1. Нажмите `1`, чтобы уменьшить таймер на секунду.
2. Нажмите `2`, чтобы увеличить таймер на секунду.
Можно выделить **важный текст**." required></textarea>
<div class="format-guide">
Красивый список: <code>1. Первый пункт</code> или <code>- Первый пункт</code><br>
Клавиши/команды: <code>`F5`</code>, <code>`1`</code>. Выделение слов: <code>**важно**</code>.
</div>
<div class="format-toolbar">
<button type="button" class="format-btn" data-snippet="1. Первый пункт&#10;2. Второй пункт">🔢 Список</button>
<button type="button" class="format-btn" data-snippet="- Первый пункт&#10;- Второй пункт">• Маркеры</button>
<button type="button" class="format-btn" data-snippet="`F5`">⌨️ Клавиша</button>
<button type="button" class="format-btn" data-snippet="**важно**">✨ Выделить слово</button>
</div>
</div>
<div class="update-item-image">
<label class="field-label">Картинка / скриншот</label>
<input class="field-input" type="text" name="item_image" placeholder="Например: /static/docs/no photo.png или просто no photo.png">
<div class="helper-text">Картинку нужно положить в static/docs. Если указать только имя файла, система сама подставит /static/docs/.</div>
</div>
</div>
</div>
<button type="button" class="add-row-btn" id="addUpdateItemBtn"> Добавить ещё пункт</button>
</div>
</div>
<div class="modal-actions">
<button type="button" class="cancel-btn" id="closeAddUpdateBtn">Отмена</button>
<button type="submit" class="submit-btn">Сохранить обновление</button>
</div>
</form>
</div>
</div>
<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>
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;
const parserForms = document.querySelectorAll(".action-form");
const parserSourceSelect = document.getElementById("parserSource");
const selectedLogoBasePath = document.getElementById("selectedLogoBasePath");
const selectedPhotoBasePath = document.getElementById("selectedPhotoBasePath");
const openCreateAccountBtn = document.getElementById("openCreateAccountBtn");
const closeCreateAccountBtn = document.getElementById("closeCreateAccountBtn");
const accountModal = document.getElementById("accountModal");
const openSettingsBtn = document.getElementById("openSettingsBtn");
const closeSettingsBtn = document.getElementById("closeSettingsBtn");
const settingsModal = document.getElementById("settingsModal");
const openAddUpdateBtn = document.getElementById("openAddUpdateBtn");
const closeAddUpdateBtn = document.getElementById("closeAddUpdateBtn");
const updateModal = document.getElementById("updateModal");
const updateDate = document.getElementById("updateDate");
const addUpdateItemBtn = document.getElementById("addUpdateItemBtn");
const updateItemsEditor = document.getElementById("updateItemsEditor");
let drops = [];
let fontSize = 18;
let columns = 0;
let matrixAnimationId = null;
let lastFrameTime = 0;
const frameInterval = 70;
const chars = "01アイウエオカキクケコサシスセソタチツテトABCDEFGHIJKLMNOPQRSTUVWXYZ";
function resizeCanvas() {
if (!canvas || !ctx) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
columns = Math.max(1, Math.floor(canvas.width / fontSize));
drops = Array.from({ length: columns }, () => Math.random() * canvas.height / fontSize);
}
function drawMatrix(timestamp = 0) {
if (!canvas || !ctx) return;
if (timestamp - lastFrameTime < frameInterval) {
matrixAnimationId = requestAnimationFrame(drawMatrix);
return;
}
lastFrameTime = timestamp;
ctx.fillStyle = "rgba(3, 8, 15, 0.08)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#00ff88";
ctx.font = `${fontSize}px monospace`;
for (let i = 0; i < drops.length; i++) {
const text = chars[Math.floor(Math.random() * chars.length)];
const x = i * fontSize;
const y = drops[i] * fontSize;
ctx.fillText(text, x, y);
if (y > canvas.height && Math.random() > 0.985) {
drops[i] = 0;
}
drops[i] += 0.65;
}
matrixAnimationId = requestAnimationFrame(drawMatrix);
}
function startLoader(title, message) {
if (!loader) return;
titleEl.textContent = title;
statusText.textContent = message;
loader.classList.add("active");
loader.setAttribute("aria-hidden", "false");
if (!matrixAnimationId) {
resizeCanvas();
lastFrameTime = 0;
drawMatrix();
}
}
function openAccountModal() {
if (!accountModal) return;
accountModal.classList.add("active");
accountModal.setAttribute("aria-hidden", "false");
}
function closeAccountModal() {
if (!accountModal) return;
accountModal.classList.remove("active");
accountModal.setAttribute("aria-hidden", "true");
}
function openSettingsModal() {
if (!settingsModal) return;
settingsModal.classList.add("active");
settingsModal.setAttribute("aria-hidden", "false");
}
function closeSettingsModal() {
if (!settingsModal) return;
settingsModal.classList.remove("active");
settingsModal.setAttribute("aria-hidden", "true");
}
function openUpdateModal() {
if (!updateModal) return;
if (updateDate && !updateDate.value) {
updateDate.value = new Date().toISOString().slice(0, 10);
}
updateModal.classList.add("active");
updateModal.setAttribute("aria-hidden", "false");
}
function closeUpdateModal() {
if (!updateModal) return;
updateModal.classList.remove("active");
updateModal.setAttribute("aria-hidden", "true");
}
function addUpdateItem() {
if (!updateItemsEditor) return;
const firstItem = updateItemsEditor.querySelector(".update-item-form");
if (!firstItem) return;
const clone = firstItem.cloneNode(true);
clone.querySelectorAll("input, textarea").forEach((field) => {
field.value = "";
});
clone.querySelectorAll("select").forEach((field) => {
field.selectedIndex = 0;
});
updateItemsEditor.appendChild(clone);
}
function insertSnippet(textarea, snippet) {
if (!textarea || !snippet) return;
const start = textarea.selectionStart ?? textarea.value.length;
const end = textarea.selectionEnd ?? textarea.value.length;
const before = textarea.value.slice(0, start);
const after = textarea.value.slice(end);
const spacerBefore = before && !before.endsWith("\n") ? "\n" : "";
const spacerAfter = after && !snippet.endsWith("\n") ? "\n" : "";
const insertion = spacerBefore + snippet + spacerAfter;
textarea.value = before + insertion + after;
const cursorPosition = before.length + insertion.length;
textarea.focus();
textarea.setSelectionRange(cursorPosition, cursorPosition);
}
function updateSelectedSourcePaths() {
if (!parserSourceSelect) return;
const option = parserSourceSelect.options[parserSourceSelect.selectedIndex];
if (selectedLogoBasePath) {
selectedLogoBasePath.textContent = option?.dataset?.logoBasePath || "—";
}
if (selectedPhotoBasePath) {
selectedPhotoBasePath.textContent = option?.dataset?.photoBasePath || "—";
}
}
document.addEventListener("click", (event) => {
const button = event.target.closest(".format-btn");
if (!button) return;
const snippet = button.dataset.snippet || "";
const explicitTargetId = button.closest(".format-toolbar")?.dataset.formatFor;
let textarea = explicitTargetId ? document.getElementById(explicitTargetId) : null;
if (!textarea) {
textarea = button.closest(".update-item-text")?.querySelector("textarea");
}
insertSnippet(textarea, snippet);
});
updateSelectedSourcePaths();
parserSourceSelect?.addEventListener("change", updateSelectedSourcePaths);
parserForms.forEach((form) => {
form.addEventListener("submit", () => {
const parserTitle = form.dataset.parserTitle || "Запуск парсера";
const parserStatus = form.dataset.parserStatus || "Обработка данных...";
document.querySelectorAll(".action-btn").forEach((btn) => {
btn.disabled = true;
});
form.classList.add("is-loading");
startLoader(`Парсинг: ${parserTitle}`, parserStatus);
});
});
if (openCreateAccountBtn) {
openCreateAccountBtn.addEventListener("click", openAccountModal);
}
if (closeCreateAccountBtn) {
closeCreateAccountBtn.addEventListener("click", closeAccountModal);
}
if (accountModal) {
accountModal.addEventListener("click", (event) => {
if (event.target === accountModal) {
closeAccountModal();
}
});
}
if (openSettingsBtn) {
openSettingsBtn.addEventListener("click", openSettingsModal);
}
if (closeSettingsBtn) {
closeSettingsBtn.addEventListener("click", closeSettingsModal);
}
if (settingsModal) {
settingsModal.addEventListener("click", (event) => {
if (event.target === settingsModal) {
closeSettingsModal();
}
});
}
if (openAddUpdateBtn) {
openAddUpdateBtn.addEventListener("click", openUpdateModal);
}
if (closeAddUpdateBtn) {
closeAddUpdateBtn.addEventListener("click", closeUpdateModal);
}
if (updateModal) {
updateModal.addEventListener("click", (event) => {
if (event.target === updateModal) {
closeUpdateModal();
}
});
}
if (addUpdateItemBtn) {
addUpdateItemBtn.addEventListener("click", addUpdateItem);
}
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeAccountModal();
closeSettingsModal();
closeUpdateModal();
}
});
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.get("open_update") === "1") {
openUpdateModal();
}
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
</script>
</body>
</html>