обновил поиск следующей игры для титра и поправил commentary

This commit is contained in:
2025-12-10 15:07:36 +03:00
parent 5e42fd69cd
commit d75042ca7a

View File

@@ -1,6 +1,5 @@
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import Response, HTMLResponse, StreamingResponse, JSONResponse from fastapi.responses import Response, HTMLResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import requests, uvicorn, json import requests, uvicorn, json
import threading, queue import threading, queue
@@ -16,7 +15,7 @@ import nasio
import io, os, platform, time import io, os, platform, time
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import re import re
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw
from io import BytesIO from io import BytesIO
import warnings import warnings
@@ -2263,6 +2262,8 @@ async def team(who: str):
) )
+ ".svg" + ".svg"
), ),
"country": (item.get("countryName") or "").strip(),
"photo_site": (item.get("photo") or "").strip(),
"photoGFX": ( "photoGFX": (
os.path.join( os.path.join(
"D:\\Photos", "D:\\Photos",
@@ -3766,6 +3767,7 @@ async def last_5_games():
return wl_list[::-1] return wl_list[::-1]
# ищем СЛЕДУЮЩИЙ матч (ближайший в будущем) для команды
# ищем СЛЕДУЮЩИЙ матч (ближайший в будущем) для команды # ищем СЛЕДУЮЩИЙ матч (ближайший в будущем) для команды
def find_next_game(team_id: int): def find_next_game(team_id: int):
if not CALENDAR or "items" not in CALENDAR: if not CALENDAR or "items" not in CALENDAR:
@@ -3806,6 +3808,11 @@ async def last_5_games():
if team_id not in (t1.get("teamId"), t2.get("teamId")): if team_id not in (t1.get("teamId"), t2.get("teamId")):
continue continue
# 👇 пропускаем ТЕКУЩИЙ матч (тот, что в GAME_ID)
gid = game_info.get("id")
if gid is not None and GAME_ID is not None and str(gid) == str(GAME_ID):
continue
dt_str = game_info.get("defaultZoneDateTime") or "" dt_str = game_info.get("defaultZoneDateTime") or ""
try: try:
dt = datetime.fromisoformat(dt_str) dt = datetime.fromisoformat(dt_str)
@@ -3815,11 +3822,11 @@ async def last_5_games():
# убираем tzinfo, чтобы можно было сравнивать с now # убираем tzinfo, чтобы можно было сравнивать с now
if dt.tzinfo is not None: if dt.tzinfo is not None:
dt = dt.replace(tzinfo=None) dt = dt.replace(tzinfo=None)
# только будущие матчи
# только матчи ПОСЛЕ текущего момента
if dt <= now: if dt <= now:
print(dt, now)
continue continue
print(item)
# определяем соперника и место # определяем соперника и место
if team_id == t1.get("teamId"): if team_id == t1.get("teamId"):
opp_name = t2.get("name", "") opp_name = t2.get("name", "")
@@ -3852,7 +3859,7 @@ async def last_5_games():
"date": best["dt"].strftime("%Y-%m-%d %H:%M"), "date": best["dt"].strftime("%Y-%m-%d %H:%M"),
"place": best["place"], # "home" / "away" "place": best["place"], # "home" / "away"
"place_ru": place_ru, # "дома" / "в гостях" "place_ru": place_ru, # "дома" / "в гостях"
"formatted": formatted, # 🆕 "wednesday, march 26, at home against astana" "formatted": formatted, # "wednesday, march 26, at home against astana"
} }
# последние 5 игр и результаты # последние 5 игр и результаты
@@ -4031,6 +4038,8 @@ async def commentary():
<th>Игрок</th> <th>Игрок</th>
<th>PTS</th> <th>PTS</th>
<th>REB</th> <th>REB</th>
<th class="extra-col">OREB</th>
<th class="extra-col">DREB</th>
<th>AST</th> <th>AST</th>
<th>STL</th> <th>STL</th>
<th>BLK</th> <th>BLK</th>
@@ -4069,9 +4078,11 @@ async def commentary():
# REB: если нет 'reb', то dreb + oreb # REB: если нет 'reb', то dreb + oreb
reb = p.get("reb") reb = p.get("reb")
dreb_raw = p.get("dreb")
oreb_raw = p.get("oreb")
if reb in ("", None): if reb in ("", None):
dreb = p.get("dreb") or 0 dreb = dreb_raw or 0
oreb = p.get("oreb") or 0 oreb = oreb_raw or 0
reb_val = dreb + oreb reb_val = dreb + oreb
else: else:
reb_val = reb reb_val = reb
@@ -4090,6 +4101,10 @@ async def commentary():
foul_raw = p.get("foul") foul_raw = p.get("foul")
foul_str = "" if foul_raw is None else str(foul_raw) foul_str = "" if foul_raw is None else str(foul_raw)
# значения для отдельных колонок
oreb_val = "" if oreb_raw in (None, "") else oreb_raw
dreb_val = "" if dreb_raw in (None, "") else dreb_raw
plus_minus = p.get("plusMinus") or "" plus_minus = p.get("plusMinus") or ""
kpi = p.get("kpi") or "" kpi = p.get("kpi") or ""
@@ -4119,6 +4134,8 @@ async def commentary():
</td> </td>
<td>{pts}</td> <td>{pts}</td>
<td>{reb_val}</td> <td>{reb_val}</td>
<td class="extra-col">{oreb_val}</td>
<td class="extra-col">{dreb_val}</td>
<td>{ast}</td> <td>{ast}</td>
<td>{stl}</td> <td>{stl}</td>
<td>{blk}</td> <td>{blk}</td>
@@ -4139,7 +4156,7 @@ async def commentary():
# Если вдруг ни одного игрока не прошло фильтр покажем заглушку # Если вдруг ни одного игрока не прошло фильтр покажем заглушку
if not rows_html: if not rows_html:
rows_html.append( rows_html.append(
'<tr><td colspan="16" class="meta">Нет данных по игрокам.</td></tr>' '<tr><td colspan="18" class="meta">Нет данных по игрокам.</td></tr>'
) )
table_footer = """ table_footer = """
@@ -4324,6 +4341,19 @@ async def commentary():
cursor: pointer; cursor: pointer;
text-decoration: underline; text-decoration: underline;
}} }}
.player-name.selected-player-left,
.player-name.selected-player-right {{
text-decoration: none;
font-weight: 600;
background: rgba(59,130,246,0.12);
border-radius: 4px;
padding: 0 4px;
}}
.player-name.selected-player-right {{
background: rgba(16,185,129,0.12); /* можно оставить одинаковый, если не хочется различать */
}}
.extra-col {{ .extra-col {{
display: none; /* по умолчанию скрыто */ display: none; /* по умолчанию скрыто */
}} }}
@@ -4337,6 +4367,75 @@ async def commentary():
#player-details table {{ #player-details table {{
margin-top: 5px; margin-top: 5px;
}} }}
.compare-header {{
display: flex;
gap: 16px;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 10px;
}}
/* делаем блок игрока flex-контейнером: слева текст, справа фото */
.compare-player-block {{
flex: 1 1 0;
padding: 8px 10px;
border-radius: 10px;
background: #141414;
border: 1px solid #333;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
}}
/* левая часть — текст (имя, амплуа и т.п.) */
.compare-player-info {{
flex: 1 1 auto;
min-width: 0;
}}
/* правая часть — фото */
.compare-player-photo {{
flex: 0 0 auto;
width: 72px;
height: 72px;
border-radius: 8px;
overflow: hidden;
background: #111;
border: 1px solid #333;
}}
.compare-player-photo img {{
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}}
.compare-name-row {{
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}}
.compare-name {{
font-size: 16px;
font-weight: 600;
}}
.compare-flag {{
width: 20px;
height: 14px;
border-radius: 2px;
object-fit: cover;
box-shadow: 0 0 0 1px rgba(0,0,0,0.4);
}}
.compare-meta-list {{
list-style: none;
padding: 0;
margin: 0;
font-size: 12px;
color: #ccc;
line-height: 1.4;
}}
</style> </style>
</head> </head>
<body> <body>
@@ -4528,16 +4627,39 @@ async def commentary():
["team1_table", "team2_table"].forEach(function(tableId) {{ ["team1_table", "team2_table"].forEach(function(tableId) {{
var table = document.getElementById(tableId); var table = document.getElementById(tableId);
if (!table) return; if (!table) return;
var tbody = table.querySelector("tbody"); var tbody = table.querySelector("tbody");
if (!tbody) return; if (!tbody) return;
// Находим индексы нужных колонок по заголовкам
var headers = table.querySelectorAll("thead th");
var ptsIndex = -1;
var foulIndex = -1;
headers.forEach(function(th, idx) {{
var text = (th.innerText || th.textContent || "").trim().toLowerCase();
if (text === "pts") {{
ptsIndex = idx;
}}
// если вдруг подпишешь по-русски — можно добавить варианты
if (text === "foul" || text === "фол" || text === "фолы") {{
foulIndex = idx;
}}
}});
if (ptsIndex < 0 || foulIndex < 0) {{
// не нашли нужные колонки тихо выходим
return;
}}
Array.from(tbody.rows).forEach(function(row) {{ Array.from(tbody.rows).forEach(function(row) {{
var cells = row.cells; var cells = row.cells;
if (!cells || cells.length < 14) return; if (!cells) return;
if (cells.length <= ptsIndex || cells.length <= foulIndex) return;
// индексы: PTS = 2, Foul = 13 var ptsCell = cells[ptsIndex];
var ptsCell = cells[2]; var foulCell = cells[foulIndex];
var foulCell = cells[13];
if (!ptsCell || !foulCell) return; if (!ptsCell || !foulCell) return;
// --- 1) зелёная подсветка при изменении PTS / Foul --- // --- 1) зелёная подсветка при изменении PTS / Foul ---
@@ -4609,7 +4731,7 @@ async def commentary():
}}); }});
}})(); }})();
// === клик по игроку + компактные таблицы сезон/карьера === // === клик по игроку + компактные таблицы сезон/карьера + сравнение левого/правого ===
(function() {{ (function() {{
var details = document.getElementById("player-details"); var details = document.getElementById("player-details");
if (!details) return; if (!details) return;
@@ -4620,6 +4742,8 @@ async def commentary():
function buildMetrics(p) {{ function buildMetrics(p) {{
var seasonAvg = [ var seasonAvg = [
["Игры", p.TGameCount],
["В старте", p.TStartCount],
["Очки", p.AvgPoints], ["Очки", p.AvgPoints],
["Передачи", p.AvgAssist], ["Передачи", p.AvgAssist],
["Подборы", p.AvgRebound], ["Подборы", p.AvgRebound],
@@ -4635,6 +4759,8 @@ async def commentary():
["С игры, %", p.Shot23Percent] ["С игры, %", p.Shot23Percent]
]; ];
var seasonTot = [ var seasonTot = [
["Игры", p.TGameCount],
["В старте", p.TStartCount],
["Очки", p.TPoints], ["Очки", p.TPoints],
["Передачи", p.TAssist], ["Передачи", p.TAssist],
["Подборы", p.TRebound], ["Подборы", p.TRebound],
@@ -4650,6 +4776,8 @@ async def commentary():
["С игры (goal/shot)", p.TShots23] ["С игры (goal/shot)", p.TShots23]
]; ];
var careerAvg = [ var careerAvg = [
["Игры", p.Career_TGameCount],
["В старте", p.Career_TStartCount],
["Очки", p.Career_AvgPoints], ["Очки", p.Career_AvgPoints],
["Передачи", p.Career_AvgAssist], ["Передачи", p.Career_AvgAssist],
["Подборы", p.Career_AvgRebound], ["Подборы", p.Career_AvgRebound],
@@ -4665,6 +4793,8 @@ async def commentary():
["С игры, %", p.Career_Shot23Percent] ["С игры, %", p.Career_Shot23Percent]
]; ];
var careerTot = [ var careerTot = [
["Игры", p.Career_TGameCount],
["В старте", p.Career_TStartCount],
["Очки", p.Career_TPoints], ["Очки", p.Career_TPoints],
["Передачи", p.Career_TAssist], ["Передачи", p.Career_TAssist],
["Подборы", p.Career_TRebound], ["Подборы", p.Career_TRebound],
@@ -4694,12 +4824,32 @@ async def commentary():
return metrics; return metrics;
}} }}
// выбранные игроки слева/справа
var selectedLeft = null;
var selectedRight = null;
// одиночное отображение (как было раньше) — НЕ трогаем таблицу, тут всё ок
function renderPlayerDetails(p) {{ function renderPlayerDetails(p) {{
if (!p) return ""; if (!p) return "";
var metrics = buildMetrics(p); var metrics = buildMetrics(p);
var html = ""; var html = "";
html += "<h2>" + safe(p.NameGFX || p.name) + "</h2>";
var metaParts = [];
if (p.age) metaParts.push(p.age + " лет");
if (p.height) metaParts.push(p.height.height || p.height);
if (p.weight) metaParts.push(p.weight);
html += "<h2 style='display:flex;align-items:center;gap:6px;'>"
+ safe(p.NameGFX || p.name)
+ (p.flag ? ("<img src='" + p.flag + "' style='width:22px;height:16px;border-radius:3px;'>") : "")
+ "</h2>";
if (metaParts.length) {{
html += "<div class='player-meta'>" + safe(metaParts.join(" · ")) + "</div>";
}}
html += "<h3>Сезон / карьера — компактно</h3>"; html += "<h3>Сезон / карьера — компактно</h3>";
html += "<table><thead><tr>"; html += "<table><thead><tr>";
html += "<th></th>"; html += "<th></th>";
for (var j = 0; j < metrics.length; j++) {{ for (var j = 0; j < metrics.length; j++) {{
@@ -4735,6 +4885,156 @@ async def commentary():
return html; return html;
}} }}
// === Сравнение игроков: шапка + СТАРАЯ вертикальная таблица с отдельными колонками ===
function renderPlayersComparison(leftP, rightP) {{
// если вообще никого нет — ничего не показываем
if (!leftP && !rightP) {{
return "";
}}
// --- метрики ---
var LM = leftP ? buildMetrics(leftP) : null;
var RM = rightP ? buildMetrics(rightP) : null;
// базовый набор строк (под один из игроков)
var base = LM || RM || [];
// если одного игрока нет — делаем для него пустой набор метрик
function makeEmptyMetrics(from) {{
return from.map(function (m) {{
return {{
label: m.label,
sAvg: "",
sTot: "",
cAvg: "",
cTot: ""
}};
}});
}}
if (!LM) LM = makeEmptyMetrics(base);
if (!RM) RM = makeEmptyMetrics(base);
function metaList(p) {{
if (!p) return [];
var arr = [];
if (p.roleFull || p.role) arr.push("Амплуа: " + (p.roleFull || p.role));
if (p.age) arr.push("Возраст: " + p.age);
if (p.height) arr.push("Рост: " + p.height);
if (p.weight) arr.push("Вес: " + p.weight);
if (p.teamName) arr.push("Команда: " + p.teamName);
return arr;
}}
function renderPlayerHeader(p) {{
var html = "";
html += "<div class='compare-player-block'>";
// левая часть: текст
html += " <div class='compare-player-info'>";
html += " <div class='compare-name-row'>";
html += " <span class='compare-name'>" + safe((p && (p.NameGFX || p.name)) || "") + "</span>";
if (p && p.flag) {{
var title = safe(p.country || "").replace(/\"/g, "&quot;");
html += " <img src='" + p.flag + "' class='compare-flag' title='" + title + "'>";
}}
html += " </div>";
html += " <ul class='compare-meta-list'>";
metaList(p || {{}}).forEach(function (txt) {{
html += " <li>" + safe(txt) + "</li>";
}});
html += " </ul>";
html += " </div>";
// правая часть: фото из p.photo_site (если есть)
if (p && p.photo_site) {{
html += " <div class='compare-player-photo'>";
html += " <img src='" + safe(p.photo_site) + "' alt='" + safe((p && (p.NameGFX || p.name)) || \"\") + "'>";
html += " </div>";
}}
html += "</div>";
return html;
}}
var html = "";
html += "<h2>Сравнение игроков</h2>";
html += "<div class='compare-header'>";
html += renderPlayerHeader(leftP || {{}});
html += renderPlayerHeader(rightP || {{}});
html += "</div>";
// --- СТАРАЯ таблица: 4 колонки слева + 4 справа ---
html += "<table class='details-wide-table' style='width:100%;border-collapse:collapse;'>";
// шапка таблицы
html += "<thead>";
html += "<tr>";
html += "<th colspan='4' style='text-align:center;border-bottom:2px solid #333;'>Левый игрок</th>";
html += "<th></th>";
html += "<th colspan='4' style='text-align:center;border-bottom:2px solid #333;'>Правый игрок</th>";
html += "</tr>";
html += "<tr>";
html += "<th>Сезон (сред.)</th>";
html += "<th>Сезон (тотал)</th>";
html += "<th>Карьера (сред.)</th>";
html += "<th>Карьера (тотал)</th>";
html += "<th>Показатель</th>";
html += "<th>Сезон (сред.)</th>";
html += "<th>Сезон (тотал)</th>";
html += "<th>Карьера (сред.)</th>";
html += "<th>Карьера (тотал)</th>";
html += "</tr>";
html += "</thead>";
html += "<tbody>";
// строки таблицы — одна строка = один показатель
for (var i = 0; i < base.length; i++) {{
var L = LM[i] || {{}};
var R = RM[i] || {{}};
html += "<tr>";
// левый
html += "<td style='text-align:center;'>" + safe(L.sAvg) + "</td>";
html += "<td style='text-align:center;'>" + safe(L.sTot) + "</td>";
html += "<td style='text-align:center;'>" + safe(L.cAvg) + "</td>";
html += "<td style='text-align:center;'>" + safe(L.cTot) + "</td>";
// показатель
html += "<td style='text-align:center;font-weight:600;'>" + safe(base[i].label || "") + "</td>";
// правый
html += "<td style='text-align:center;'>" + safe(R.sAvg) + "</td>";
html += "<td style='text-align:center;'>" + safe(R.sTot) + "</td>";
html += "<td style='text-align:center;'>" + safe(R.cAvg) + "</td>";
html += "<td style='text-align:center;'>" + safe(R.cTot) + "</td>";
html += "</tr>";
}}
html += "</tbody></table>";
return html;
}}
// переотрисовка блока с деталями/сравнением
function rerenderDetails() {{
if (selectedLeft || selectedRight) {{
// Всегда рисуем вертикальную сравнительную таблицу.
// Если один из игроков не выбран — его половина будет пустой.
details.innerHTML = renderPlayersComparison(selectedLeft, selectedRight);
}} else {{
details.innerHTML = '<div class="meta">Кликни по фамилии игрока, чтобы показать сезон/карьеру.</div>';
}}
}}
// выбор/снятие выбора игрока
function selectPlayer(teamKey, pid, noScroll, noSave) {{ function selectPlayer(teamKey, pid, noScroll, noSave) {{
var data = window.PLAYER_DATA || {{}}; var data = window.PLAYER_DATA || {{}};
var list = data[teamKey] || []; var list = data[teamKey] || [];
@@ -4745,16 +5045,77 @@ async def commentary():
break; break;
}} }}
}} }}
// если игрок не найден в данных — просто выходим
if (!found) return; if (!found) return;
details.innerHTML = renderPlayerDetails(found);
var pidStr = String(pid || "");
if (teamKey === "team1") {{
// повторный клик по тому же игроку слева — снимаем выбор
if (selectedLeft && String(selectedLeft.id || "") === pidStr) {{
selectedLeft = null;
}} else {{
selectedLeft = found;
}}
}} else if (teamKey === "team2") {{
// повторный клик по тому же игроку справа — снимаем выбор
if (selectedRight && String(selectedRight.id || "") === pidStr) {{
selectedRight = null;
}} else {{
selectedRight = found;
}}
}} else {{
// запасной вариант — считаем как левую сторону
if (selectedLeft && String(selectedLeft.id || "") === pidStr) {{
selectedLeft = null;
}} else {{
selectedLeft = found;
}}
}}
// подсветка выбранных фамилий
var allCells = document.querySelectorAll(".player-name");
allCells.forEach(function (cell) {{
cell.classList.remove("selected-player-left", "selected-player-right");
var cPid = cell.getAttribute("data-player-id");
var cTeam = cell.getAttribute("data-team");
if (selectedLeft && cTeam === "team1" &&
String(selectedLeft.id || "") === String(cPid || "")) {{
cell.classList.add("selected-player-left");
}}
if (selectedRight && cTeam === "team2" &&
String(selectedRight.id || "") === String(cPid || "")) {{
cell.classList.add("selected-player-right");
}}
}});
rerenderDetails();
if (!noSave) {{ if (!noSave) {{
localStorage.setItem("commentary_last_player", JSON.stringify({{ team: teamKey, id: pid }})); try {{
if (selectedLeft || selectedRight) {{
// запоминаем последнего кликнутого
localStorage.setItem(
"commentary_last_player",
JSON.stringify({{ team: teamKey, id: pid }})
);
}} else {{
// если вообще никого не осталось выбранным — чистим
localStorage.removeItem("commentary_last_player");
}}
}} catch (e) {{}}
}} }}
if (!noScroll) {{ if (!noScroll) {{
details.scrollIntoView({{ behavior: "smooth", block: "start" }}); details.scrollIntoView({{ behavior: "smooth", block: "start" }});
}} }}
}} }}
window.selectPlayer = selectPlayer;
// клики по фамилиям // клики по фамилиям
var cells = document.querySelectorAll(".player-name"); var cells = document.querySelectorAll(".player-name");
for (var i = 0; i < cells.length; i++) {{ for (var i = 0; i < cells.length; i++) {{
@@ -4765,7 +5126,7 @@ async def commentary():
}}); }});
}} }}
// восстановить последнего выбранного // восстановить последнего выбранного (если был)
var saved = localStorage.getItem("commentary_last_player"); var saved = localStorage.getItem("commentary_last_player");
if (saved) {{ if (saved) {{
try {{ try {{
@@ -4788,6 +5149,7 @@ async def commentary():
return (v === undefined || v === null) ? "" : v; return (v === undefined || v === null) ? "" : v;
}} }}
// все ячейки с фамилиями для нужной команды
var cells = document.querySelectorAll('.player-name[data-team="' + teamKey + '"]'); var cells = document.querySelectorAll('.player-name[data-team="' + teamKey + '"]');
cells.forEach(function (cell) {{ cells.forEach(function (cell) {{
@@ -4798,42 +5160,64 @@ async def commentary():
var row = cell.parentElement; var row = cell.parentElement;
var tds = row.children; var tds = row.children;
// обновляем data-on-court // обновляем data-on-court (для раскраски строк)
var onCourt = !!newP.isOnCourt; var onCourt = !!newP.isOnCourt;
row.dataset.onCourt = onCourt ? "true" : "false"; row.dataset.onCourt = onCourt ? "true" : "false";
// Порядок колонок в таблице:
// 0 #
// 1 Игрок
// 2 PTS
// 3 REB
// 4 AST
// 5 STL
// 6 BLK
// 7 MIN
// 8 OREB (extra-col)
// 9 DREB (extra-col)
// 10 FG (extra-col)
// 11 2PT (extra-col)
// 12 3PT (extra-col)
// 13 1PT (extra-col)
// 14 TO (extra-col)
// 15 Foul
// 16 +/-
// 17 KPI
var fields = [ var fields = [
[2, "pts"], [2, "pts"],
[3, "reb"], [3, "reb"], // пересчёт ниже
[4, "ast"], [4, "oreb"],
[5, "stl"], [5, "dreb"],
[6, "blk"], [6, "ast"],
[7, "time"], [7, "stl"],
[8, "fg"], [8, "blk"],
[9, "pt-2"], [9, "time"],
[10, "pt-3"], [10, "fg"],
[11, "pt-1"], [11, "pt-2"],
[12, "to"], [12, "pt-3"],
[13, "foul"], [13, "pt-1"],
[14, "plusMinus"], [14, "to"],
[15, "kpi"] [15, "foul"],
[16, "plusMinus"],
[17, "kpi"]
]; ];
// REB: если нет newP.reb, считаем как dreb + oreb
var newReb = newP.reb; var newReb = newP.reb;
if (newReb === undefined) {{ if (newReb === undefined || newReb === null || newReb === "") {{
newReb = (newP.dreb || 0) + (newP.oreb || 0); var dreb = newP.dreb || 0;
var oreb = newP.oreb || 0;
newReb = dreb + oreb;
}} }}
function applyUpdate(td, newVal) {{ function applyUpdate(td, newVal) {{
newVal = safe(newVal); td.textContent = safe(newVal);
td.textContent = newVal;
}} }}
fields.forEach(function (item) {{ fields.forEach(function (item) {{
var index = item[0]; var index = item[0];
var key = item[1]; var key = item[1];
var td = tds[index]; var td = tds[index];
if (!td) return; if (!td) return;
if (key === "reb") {{ if (key === "reb") {{
@@ -4845,15 +5229,44 @@ async def commentary():
}}); }});
}} }}
function refreshSelected() {{ function refreshSelected() {{
var saved = localStorage.getItem("commentary_last_player"); // если никого не выбрали нечего обновлять
if (!saved) return; if (!selectedLeft && !selectedRight) {{
try {{ return;
var obj = JSON.parse(saved);
if (obj && obj.team && obj.id) {{
selectPlayer(obj.team, obj.id, true, true);
}} }}
}} catch (e) {{}}
var data = window.PLAYER_DATA || {{}};
var team1 = Array.isArray(data.team1) ? data.team1 : [];
var team2 = Array.isArray(data.team2) ? data.team2 : [];
// обновляем ссылку на объект для левого игрока (team1)
if (selectedLeft) {{
var leftId = String(selectedLeft.id || "");
var updatedLeft = team1.find(function (p) {{
return String(p.id || "") === leftId;
}});
if (updatedLeft) {{
selectedLeft = updatedLeft;
}}
}}
// обновляем ссылку на объект для правого игрока (team2)
if (selectedRight) {{
var rightId = String(selectedRight.id || "");
var updatedRight = team2.find(function (p) {{
return String(p.id || "") === rightId;
}});
if (updatedRight) {{
selectedRight = updatedRight;
}}
}}
// просто перерисовываем блок сравнения, без скролла и без записи в localStorage
rerenderDetails();
}} }}
setInterval(function() {{ setInterval(function() {{
@@ -4884,8 +5297,9 @@ async def commentary():
// 🔁 обновляем шапку и четверти // 🔁 обновляем шапку и четверти
updateHeaderFromLiveStatus(); // счёт + фолы из live_status updateHeaderFromLiveStatus(); // счёт + фолы из live_status
updateQuartersFromScores(); // счёт по четвертям из /scores updateQuartersFromScores(); // счёт по четвертям из /scores
updateTimeoutsFromTeamStats(); // тайм-ауты обновление
}}, 1000); }}, 1500);
updateTimeoutsFromTeamStats(); // тайм-ауты строго из team_stats.timeout_left updateTimeoutsFromTeamStats(); // тайм-ауты строго из team_stats.timeout_left
}})(); }})();
@@ -5002,7 +5416,7 @@ async def commentary():
}} }}
// запустим автообновление счёта и четвертей // запустим автообновление счёта и четвертей
updateScoreAndQuarters(); updateScoreAndQuarters();
setInterval(updateScoreAndQuarters, 1000); setInterval(updateScoreAndQuarters, 1500);
makeTableSortable("team1_table"); makeTableSortable("team1_table");
makeTableSortable("team2_table"); makeTableSortable("team2_table");
@@ -5010,7 +5424,7 @@ async def commentary():
// первый старт стилей сразу // первый старт стилей сразу
updateRowStyles(); updateRowStyles();
// и периодически обновляем (на случай любых внешних изменений) // и периодически обновляем (на случай любых внешних изменений)
setInterval(updateRowStyles, 1000); setInterval(updateRowStyles, 1500);
}}); }});
</script> </script>
</body> </body>
@@ -5563,6 +5977,18 @@ async def milestones_ui():
align-items: center; align-items: center;
gap: 6px; gap: 6px;
} }
.player-flag {
width: 22px;
height: 16px;
object-fit: cover;
border-radius: 3px;
}
.player-meta,
.player-meta-inline {
font-size: 11px;
color: var(--text-muted);
}
label { label {
font-size: 12px; font-size: 12px;
color: var(--text-muted); color: var(--text-muted);