Обновил добавление обновлений
This commit is contained in:
103
app.py
103
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'<kbd class="update-key">{html.escape(inner, quote=True)}</kbd>')
|
||||
elif token.startswith("**") and token.endswith("**"):
|
||||
inner = token[2:-2].strip()
|
||||
if inner:
|
||||
result.append(f'<span class="update-highlight">{html.escape(inner, quote=True)}</span>')
|
||||
|
||||
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" <li>{item}</li>" for item in buffer)
|
||||
return (
|
||||
f' <{tag} class="update-pretty-list update-pretty-list--{list_type}">\n'
|
||||
f"{items}\n"
|
||||
f" </{tag}>"
|
||||
)
|
||||
|
||||
|
||||
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 "<p>Описание не заполнено.</p>"
|
||||
|
||||
return "\n".join(f"<p>{_escape_text(line)}</p>" 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"<p>{_inline_update_markup(line)}</p>")
|
||||
|
||||
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"<li>{_escape_text(action)}</li>" for action in actions)
|
||||
actions_html = "\n".join(f"<li>{_inline_update_markup(action)}</li>" for action in actions)
|
||||
else:
|
||||
actions_html = "<li>Ознакомиться с обновлением перед работой с матчем.</li>"
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
<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="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>
|
||||
@@ -517,7 +566,21 @@
|
||||
|
||||
<div class="update-item-text">
|
||||
<label class="field-label">Описание</label>
|
||||
<textarea class="field-textarea" name="item_text" placeholder="Опишите изменение. Каждая строка станет отдельным абзацем." required></textarea>
|
||||
<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. Первый пункт 2. Второй пункт">🔢 Список</button>
|
||||
<button type="button" class="format-btn" data-snippet="- Первый пункт - Второй пункт">• Маркеры</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">
|
||||
@@ -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 || "Запуск парсера";
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="update-item">
|
||||
<span class="update-badge new">Новое</span>
|
||||
|
||||
<div class="update-content">
|
||||
<div class="update-title">Иконка на отсутствие фотографии на сервере</div>
|
||||
|
||||
<div class="update-text">
|
||||
<p>
|
||||
Теперь рядом с игроком отображается, если у него нет фотографии для расстановки.
|
||||
Теперь рядом с игроком отображается у кого нет фотографии для расстановки.
|
||||
</p>
|
||||
<div class="update-text">
|
||||
<img src="/static/docs/no photo.png" alt="Скриншот с примером отображения иконки">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="update-item">
|
||||
<span class="update-badge important">Важно</span>
|
||||
|
||||
<div class="update-content">
|
||||
<div class="update-title">ЖФЛ_2026 NEW.config</div>
|
||||
|
||||
<div class="update-text">
|
||||
<ol class="pretty-change-list">
|
||||
<li>
|
||||
<span class="change-main">Горячие клавиши таймера</span>
|
||||
<span class="change-desc">
|
||||
Добавлены шорткаты на основной клавиатуре: <kbd>1</kbd> уменьшает таймер на 1 секунду, <kbd>2</kbd> увеличивает таймер на 1 секунду. Это нужно для быстрой корректировки основного таймера.
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="change-main">Добавленное время остаётся на счёте</span>
|
||||
<span class="change-desc">
|
||||
При снятии верхнего титра добавленное время больше не пропадает вместе с титром, а остаётся на верхнем счёте.
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="update-item">
|
||||
<span class="update-badge important">Важно</span>
|
||||
|
||||
@@ -954,7 +938,7 @@
|
||||
|
||||
<details class="update-details">
|
||||
<summary>
|
||||
<article class="update-card">
|
||||
|
||||
<div class="update-header">
|
||||
<div>
|
||||
<div class="update-version-big update-date">Версия 1.2</div>
|
||||
|
||||
Reference in New Issue
Block a user