From b0fe3ad5a4688986e0571ef5a82c9cfd9834fd54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A7=D0=B5=D1=80=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE?= Date: Mon, 22 Jun 2026 18:25:04 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 103 ++++++++++++++++++++++++-- templates/admin_db_index.html | 98 ++++++++++++++++++++++++- templates/info.html | 132 +++++++++++++++------------------- 3 files changed, 253 insertions(+), 80 deletions(-) diff --git a/app.py b/app.py index 5f49eee..1c5a915 100644 --- a/app.py +++ b/app.py @@ -1958,12 +1958,107 @@ def _format_update_date(value: str | None) -> str: return value -def _paragraphs_html(value: str | None) -> str: - lines = _split_lines(value) +def _inline_update_markup(value: str | None) -> str: + """Безопасное мини-форматирование для текста обновлений. + + Поддерживаем в админке: + - `F5` или `1` — выделенная клавиша/команда; + - **важный текст** — зелёное выделение. + Остальной текст обязательно экранируется, чтобы не сломать HTML. + """ + source = _clean_update_text(value) + if not source: + return "" + + token_re = re.compile(r"(`[^`]+`|\*\*.+?\*\*)") + result: list[str] = [] + last_pos = 0 + + for match in token_re.finditer(source): + result.append(html.escape(source[last_pos:match.start()], quote=True)) + token = match.group(0) + + if token.startswith("`") and token.endswith("`"): + inner = token[1:-1].strip() + if inner: + result.append(f'{html.escape(inner, quote=True)}') + elif token.startswith("**") and token.endswith("**"): + inner = token[2:-2].strip() + if inner: + result.append(f'{html.escape(inner, quote=True)}') + + last_pos = match.end() + + result.append(html.escape(source[last_pos:], quote=True)) + return "".join(result) + + +def _flush_update_list(buffer: list[str], list_type: str) -> str: + if not buffer: + return "" + + tag = "ol" if list_type == "numbered" else "ul" + items = "\n".join(f"
  • {item}
  • " for item in buffer) + return ( + f' <{tag} class="update-pretty-list update-pretty-list--{list_type}">\n' + f"{items}\n" + f" " + ) + + +def _rich_update_text_html(value: str | None) -> str: + """Текст обновления с красивыми списками и мини-выделениями. + + Поддерживаемые форматы в textarea: + 1. Первый пункт -> красивый нумерованный список + - Первый пункт -> красивый маркированный список + `F5` -> выделенная клавиша/команда + **важно** -> выделенное слово/фраза + """ + raw_lines = [line.strip() for line in (value or "").splitlines()] + lines = [line for line in raw_lines if line] if not lines: return "

    Описание не заполнено.

    " - return "\n".join(f"

    {_escape_text(line)}

    " for line in lines) + html_parts: list[str] = [] + list_buffer: list[str] = [] + current_list_type = "" + + def close_list() -> None: + nonlocal list_buffer, current_list_type + if list_buffer: + html_parts.append(_flush_update_list(list_buffer, current_list_type)) + list_buffer = [] + current_list_type = "" + + for line in lines: + numbered_match = re.match(r"^\d+[\.)]\s+(.+)$", line) + bullet_match = re.match(r"^[-*•]\s+(.+)$", line) + + if numbered_match: + if current_list_type and current_list_type != "numbered": + close_list() + current_list_type = "numbered" + list_buffer.append(_inline_update_markup(numbered_match.group(1))) + continue + + if bullet_match: + if current_list_type and current_list_type != "bullet": + close_list() + current_list_type = "bullet" + list_buffer.append(_inline_update_markup(bullet_match.group(1))) + continue + + close_list() + html_parts.append(f"

    {_inline_update_markup(line)}

    ") + + close_list() + return "\n".join(html_parts) + + +def _paragraphs_html(value: str | None) -> str: + # Оставлено как совместимый алиас: теперь описание умеет списки, клавиши и выделения. + return _rich_update_text_html(value) def _normalize_update_image_path(value: str | None) -> str: @@ -2012,7 +2107,7 @@ def _build_update_block( actions = _split_lines(operator_actions) if actions: - actions_html = "\n".join(f"
  • {_escape_text(action)}
  • " for action in actions) + actions_html = "\n".join(f"
  • {_inline_update_markup(action)}
  • " for action in actions) else: actions_html = "
  • Ознакомиться с обновлением перед работой с матчем.
  • " diff --git a/templates/admin_db_index.html b/templates/admin_db_index.html index 8842916..ecc55f0 100644 --- a/templates/admin_db_index.html +++ b/templates/admin_db_index.html @@ -271,6 +271,48 @@ 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; @@ -493,7 +535,14 @@ -
    Каждая строка станет отдельным пунктом списка.
    +
    Каждая строка станет отдельным пунктом списка. Можно выделять клавиши и важные слова.
    +
    + Формат: `F5` — клавиша/команда, **важно** — выделенное слово. +
    +
    + + +
    @@ -517,7 +566,21 @@
    - + +
    + Красивый список: 1. Первый пункт или - Первый пункт
    + Клавиши/команды: `F5`, `1`. Выделение слов: **важно**. +
    +
    + + + + +
    @@ -663,6 +726,37 @@ 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); + } + + 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); + }); + parserForms.forEach((form) => { form.addEventListener("submit", () => { const parserTitle = form.dataset.parserTitle || "Запуск парсера"; diff --git a/templates/info.html b/templates/info.html index 477ede1..cf44e77 100644 --- a/templates/info.html +++ b/templates/info.html @@ -527,72 +527,84 @@ margin-bottom: 0; } - .pretty-change-list { + .update-pretty-list { display: grid; gap: 10px; - margin: 12px 0 0; + margin: 10px 0 0; padding: 0; list-style: none; - counter-reset: change-step; + counter-reset: update-step; } - .pretty-change-list li { + .update-pretty-list li { position: relative; - counter-increment: change-step; - padding: 13px 14px 13px 54px; - border: 1px solid rgba(255, 209, 102, 0.28); + min-height: 44px; + padding: 11px 14px 11px 50px; + border: 1px solid var(--border-soft); border-radius: var(--radius-sm); background: - linear-gradient(135deg, rgba(255, 209, 102, 0.10), rgba(0, 255, 136, 0.045)), - rgba(0, 0, 0, 0.22); - box-shadow: inset 0 0 14px rgba(255, 209, 102, 0.04); + linear-gradient(135deg, rgba(0, 255, 136, 0.075), rgba(0, 0, 0, 0.20)); + box-shadow: inset 0 0 16px rgba(0, 255, 136, 0.035); } - .pretty-change-list li::before { - content: counter(change-step); + .update-pretty-list--numbered li::before { + counter-increment: update-step; + content: counter(update-step); position: absolute; - top: 13px; left: 14px; - display: inline-flex; - align-items: center; - justify-content: center; - width: 27px; - height: 27px; - color: var(--yellow); - font-size: 13px; + top: 11px; + width: 25px; + height: 25px; + display: grid; + place-items: center; + color: var(--green); + font-size: 12px; font-weight: 700; - border: 1px solid rgba(255, 209, 102, 0.55); + border: 1px solid rgba(0, 255, 136, 0.58); + border-radius: 8px; + background: rgba(0, 255, 136, 0.10); + box-shadow: 0 0 12px rgba(0, 255, 136, 0.18); + } + + .update-pretty-list--bullet li::before { + content: ""; + position: absolute; + left: 22px; + top: 22px; + width: 9px; + height: 9px; border-radius: 50%; - background: rgba(255, 209, 102, 0.08); - box-shadow: 0 0 12px rgba(255, 209, 102, 0.14); + background: var(--green); + box-shadow: 0 0 12px rgba(0, 255, 136, 0.62); } - .change-main { - display: block; - margin-bottom: 4px; - color: var(--yellow); - font-weight: 700; - } - - .change-desc { - display: block; - color: var(--text); - } - - .update-text kbd { + .update-key { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; - padding: 2px 7px; - color: var(--green); - font: inherit; - font-size: 13px; + min-height: 22px; + margin: 0 2px; + padding: 2px 8px; + color: #001a0f; + font-family: var(--font); + font-size: 12px; font-weight: 700; - border: 1px solid rgba(0, 255, 136, 0.45); + line-height: 1.2; + border: 1px solid rgba(0, 255, 136, 0.78); border-radius: 7px; - background: rgba(0, 255, 136, 0.08); - box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.34), 0 0 10px rgba(0, 255, 136, 0.08); + background: var(--green); + box-shadow: 0 0 14px rgba(0, 255, 136, 0.28); + } + + .update-highlight { + padding: 1px 6px; + color: var(--green); + font-weight: 700; + border: 1px solid rgba(0, 255, 136, 0.28); + border-radius: 999px; + background: rgba(0, 255, 136, 0.075); + text-shadow: 0 0 8px rgba(0, 255, 136, 0.22); } .update-badge { @@ -894,47 +906,19 @@
    -
    Новое
    Иконка на отсутствие фотографии на сервере
    - -

    - Теперь рядом с игроком отображается, если у него нет фотографии для расстановки. + Теперь рядом с игроком отображается у кого нет фотографии для расстановки.

    +
    Скриншот с примером отображения иконки
    - -
    - Важно - -
    -
    ЖФЛ_2026 NEW.config
    - -
    -
      -
    1. - Горячие клавиши таймера - - Добавлены шорткаты на основной клавиатуре: 1 уменьшает таймер на 1 секунду, 2 увеличивает таймер на 1 секунду. Это нужно для быстрой корректировки основного таймера. - -
    2. -
    3. - Добавленное время остаётся на счёте - - При снятии верхнего титра добавленное время больше не пропадает вместе с титром, а остаётся на верхнем счёте. - -
    4. -
    -
    -
    -
    -
    Важно @@ -954,7 +938,7 @@
    -
    +
    Версия 1.2