обновил поиск следующей игры для титра и поправил commentary
This commit is contained in:
548
get_data.py
548
get_data.py
@@ -1,6 +1,5 @@
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.responses import Response, HTMLResponse, StreamingResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import Response, HTMLResponse, StreamingResponse
|
||||
from contextlib import asynccontextmanager
|
||||
import requests, uvicorn, json
|
||||
import threading, queue
|
||||
@@ -16,7 +15,7 @@ import nasio
|
||||
import io, os, platform, time
|
||||
import xml.etree.ElementTree as ET
|
||||
import re
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL import Image, ImageDraw
|
||||
from io import BytesIO
|
||||
import warnings
|
||||
|
||||
@@ -2263,6 +2262,8 @@ async def team(who: str):
|
||||
)
|
||||
+ ".svg"
|
||||
),
|
||||
"country": (item.get("countryName") or "").strip(),
|
||||
"photo_site": (item.get("photo") or "").strip(),
|
||||
"photoGFX": (
|
||||
os.path.join(
|
||||
"D:\\Photos",
|
||||
@@ -3766,6 +3767,7 @@ async def last_5_games():
|
||||
|
||||
return wl_list[::-1]
|
||||
|
||||
# ищем СЛЕДУЮЩИЙ матч (ближайший в будущем) для команды
|
||||
# ищем СЛЕДУЮЩИЙ матч (ближайший в будущем) для команды
|
||||
def find_next_game(team_id: int):
|
||||
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")):
|
||||
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 ""
|
||||
try:
|
||||
dt = datetime.fromisoformat(dt_str)
|
||||
@@ -3815,11 +3822,11 @@ async def last_5_games():
|
||||
# убираем tzinfo, чтобы можно было сравнивать с now
|
||||
if dt.tzinfo is not None:
|
||||
dt = dt.replace(tzinfo=None)
|
||||
# только будущие матчи
|
||||
|
||||
# только матчи ПОСЛЕ текущего момента
|
||||
if dt <= now:
|
||||
print(dt, now)
|
||||
continue
|
||||
print(item)
|
||||
|
||||
# определяем соперника и место
|
||||
if team_id == t1.get("teamId"):
|
||||
opp_name = t2.get("name", "")
|
||||
@@ -3839,8 +3846,8 @@ async def last_5_games():
|
||||
# 🆕 формируем английскую строку
|
||||
dt = best["dt"]
|
||||
weekday_en = WEEKDAYS_EN[dt.weekday()] # monday..sunday
|
||||
month_en = MONTHS_EN[dt.month - 1] # january..december
|
||||
day = dt.day # 1..31
|
||||
month_en = MONTHS_EN[dt.month - 1] # january..december
|
||||
day = dt.day # 1..31
|
||||
place_en = "home" if best["place"] == "home" else "away"
|
||||
|
||||
formatted = (
|
||||
@@ -3850,9 +3857,9 @@ async def last_5_games():
|
||||
return {
|
||||
"opponent": best["opp"],
|
||||
"date": best["dt"].strftime("%Y-%m-%d %H:%M"),
|
||||
"place": best["place"], # "home" / "away"
|
||||
"place_ru": place_ru, # "дома" / "в гостях"
|
||||
"formatted": formatted, # 🆕 "wednesday, march 26, at home against astana"
|
||||
"place": best["place"], # "home" / "away"
|
||||
"place_ru": place_ru, # "дома" / "в гостях"
|
||||
"formatted": formatted, # "wednesday, march 26, at home against astana"
|
||||
}
|
||||
|
||||
# последние 5 игр и результаты
|
||||
@@ -4031,6 +4038,8 @@ async def commentary():
|
||||
<th>Игрок</th>
|
||||
<th>PTS</th>
|
||||
<th>REB</th>
|
||||
<th class="extra-col">OREB</th>
|
||||
<th class="extra-col">DREB</th>
|
||||
<th>AST</th>
|
||||
<th>STL</th>
|
||||
<th>BLK</th>
|
||||
@@ -4069,9 +4078,11 @@ async def commentary():
|
||||
|
||||
# REB: если нет 'reb', то dreb + oreb
|
||||
reb = p.get("reb")
|
||||
dreb_raw = p.get("dreb")
|
||||
oreb_raw = p.get("oreb")
|
||||
if reb in ("", None):
|
||||
dreb = p.get("dreb") or 0
|
||||
oreb = p.get("oreb") or 0
|
||||
dreb = dreb_raw or 0
|
||||
oreb = oreb_raw or 0
|
||||
reb_val = dreb + oreb
|
||||
else:
|
||||
reb_val = reb
|
||||
@@ -4089,6 +4100,10 @@ async def commentary():
|
||||
|
||||
foul_raw = p.get("foul")
|
||||
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 ""
|
||||
kpi = p.get("kpi") or ""
|
||||
@@ -4119,6 +4134,8 @@ async def commentary():
|
||||
</td>
|
||||
<td>{pts}</td>
|
||||
<td>{reb_val}</td>
|
||||
<td class="extra-col">{oreb_val}</td>
|
||||
<td class="extra-col">{dreb_val}</td>
|
||||
<td>{ast}</td>
|
||||
<td>{stl}</td>
|
||||
<td>{blk}</td>
|
||||
@@ -4139,7 +4156,7 @@ async def commentary():
|
||||
# Если вдруг ни одного игрока не прошло фильтр – покажем заглушку
|
||||
if not rows_html:
|
||||
rows_html.append(
|
||||
'<tr><td colspan="16" class="meta">Нет данных по игрокам.</td></tr>'
|
||||
'<tr><td colspan="18" class="meta">Нет данных по игрокам.</td></tr>'
|
||||
)
|
||||
|
||||
table_footer = """
|
||||
@@ -4324,6 +4341,19 @@ async def commentary():
|
||||
cursor: pointer;
|
||||
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 {{
|
||||
display: none; /* по умолчанию скрыто */
|
||||
}}
|
||||
@@ -4337,6 +4367,75 @@ async def commentary():
|
||||
#player-details table {{
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
@@ -4528,16 +4627,39 @@ async def commentary():
|
||||
["team1_table", "team2_table"].forEach(function(tableId) {{
|
||||
var table = document.getElementById(tableId);
|
||||
if (!table) return;
|
||||
|
||||
var tbody = table.querySelector("tbody");
|
||||
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) {{
|
||||
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[2];
|
||||
var foulCell = cells[13];
|
||||
var ptsCell = cells[ptsIndex];
|
||||
var foulCell = cells[foulIndex];
|
||||
if (!ptsCell || !foulCell) return;
|
||||
|
||||
// --- 1) зелёная подсветка при изменении PTS / Foul ---
|
||||
@@ -4609,7 +4731,7 @@ async def commentary():
|
||||
}});
|
||||
}})();
|
||||
|
||||
// === клик по игроку + компактные таблицы сезон/карьера ===
|
||||
// === клик по игроку + компактные таблицы сезон/карьера + сравнение левого/правого ===
|
||||
(function() {{
|
||||
var details = document.getElementById("player-details");
|
||||
if (!details) return;
|
||||
@@ -4620,6 +4742,8 @@ async def commentary():
|
||||
|
||||
function buildMetrics(p) {{
|
||||
var seasonAvg = [
|
||||
["Игры", p.TGameCount],
|
||||
["В старте", p.TStartCount],
|
||||
["Очки", p.AvgPoints],
|
||||
["Передачи", p.AvgAssist],
|
||||
["Подборы", p.AvgRebound],
|
||||
@@ -4635,6 +4759,8 @@ async def commentary():
|
||||
["С игры, %", p.Shot23Percent]
|
||||
];
|
||||
var seasonTot = [
|
||||
["Игры", p.TGameCount],
|
||||
["В старте", p.TStartCount],
|
||||
["Очки", p.TPoints],
|
||||
["Передачи", p.TAssist],
|
||||
["Подборы", p.TRebound],
|
||||
@@ -4650,6 +4776,8 @@ async def commentary():
|
||||
["С игры (goal/shot)", p.TShots23]
|
||||
];
|
||||
var careerAvg = [
|
||||
["Игры", p.Career_TGameCount],
|
||||
["В старте", p.Career_TStartCount],
|
||||
["Очки", p.Career_AvgPoints],
|
||||
["Передачи", p.Career_AvgAssist],
|
||||
["Подборы", p.Career_AvgRebound],
|
||||
@@ -4665,6 +4793,8 @@ async def commentary():
|
||||
["С игры, %", p.Career_Shot23Percent]
|
||||
];
|
||||
var careerTot = [
|
||||
["Игры", p.Career_TGameCount],
|
||||
["В старте", p.Career_TStartCount],
|
||||
["Очки", p.Career_TPoints],
|
||||
["Передачи", p.Career_TAssist],
|
||||
["Подборы", p.Career_TRebound],
|
||||
@@ -4694,12 +4824,32 @@ async def commentary():
|
||||
return metrics;
|
||||
}}
|
||||
|
||||
// выбранные игроки слева/справа
|
||||
var selectedLeft = null;
|
||||
var selectedRight = null;
|
||||
|
||||
// одиночное отображение (как было раньше) — НЕ трогаем таблицу, тут всё ок
|
||||
function renderPlayerDetails(p) {{
|
||||
if (!p) return "";
|
||||
var metrics = buildMetrics(p);
|
||||
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 += "<table><thead><tr>";
|
||||
html += "<th></th>";
|
||||
for (var j = 0; j < metrics.length; j++) {{
|
||||
@@ -4735,6 +4885,156 @@ async def commentary():
|
||||
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, """);
|
||||
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) {{
|
||||
var data = window.PLAYER_DATA || {{}};
|
||||
var list = data[teamKey] || [];
|
||||
@@ -4745,16 +5045,77 @@ async def commentary():
|
||||
break;
|
||||
}}
|
||||
}}
|
||||
|
||||
// если игрок не найден в данных — просто выходим
|
||||
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) {{
|
||||
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) {{
|
||||
details.scrollIntoView({{ behavior: "smooth", block: "start" }});
|
||||
}}
|
||||
}}
|
||||
|
||||
window.selectPlayer = selectPlayer;
|
||||
|
||||
// клики по фамилиям
|
||||
var cells = document.querySelectorAll(".player-name");
|
||||
for (var i = 0; i < cells.length; i++) {{
|
||||
@@ -4765,7 +5126,7 @@ async def commentary():
|
||||
}});
|
||||
}}
|
||||
|
||||
// восстановить последнего выбранного
|
||||
// восстановить последнего выбранного (если был)
|
||||
var saved = localStorage.getItem("commentary_last_player");
|
||||
if (saved) {{
|
||||
try {{
|
||||
@@ -4775,7 +5136,7 @@ async def commentary():
|
||||
}}
|
||||
}} catch (e) {{}}
|
||||
}}
|
||||
|
||||
|
||||
// === автообновление статистики без перезагрузки страницы ===
|
||||
function updateTeamTable(teamKey, newPlayers) {{
|
||||
var map = {{}};
|
||||
@@ -4788,9 +5149,10 @@ async def commentary():
|
||||
return (v === undefined || v === null) ? "" : v;
|
||||
}}
|
||||
|
||||
// все ячейки с фамилиями для нужной команды
|
||||
var cells = document.querySelectorAll('.player-name[data-team="' + teamKey + '"]');
|
||||
|
||||
cells.forEach(function(cell) {{
|
||||
cells.forEach(function (cell) {{
|
||||
var pid = cell.getAttribute("data-player-id");
|
||||
var newP = map[String(pid)];
|
||||
if (!newP) return;
|
||||
@@ -4798,42 +5160,64 @@ async def commentary():
|
||||
var row = cell.parentElement;
|
||||
var tds = row.children;
|
||||
|
||||
// обновляем data-on-court
|
||||
// обновляем data-on-court (для раскраски строк)
|
||||
var onCourt = !!newP.isOnCourt;
|
||||
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 = [
|
||||
[2, "pts"],
|
||||
[3, "reb"],
|
||||
[4, "ast"],
|
||||
[5, "stl"],
|
||||
[6, "blk"],
|
||||
[7, "time"],
|
||||
[8, "fg"],
|
||||
[9, "pt-2"],
|
||||
[10, "pt-3"],
|
||||
[11, "pt-1"],
|
||||
[12, "to"],
|
||||
[13, "foul"],
|
||||
[14, "plusMinus"],
|
||||
[15, "kpi"]
|
||||
[2, "pts"],
|
||||
[3, "reb"], // пересчёт ниже
|
||||
[4, "oreb"],
|
||||
[5, "dreb"],
|
||||
[6, "ast"],
|
||||
[7, "stl"],
|
||||
[8, "blk"],
|
||||
[9, "time"],
|
||||
[10, "fg"],
|
||||
[11, "pt-2"],
|
||||
[12, "pt-3"],
|
||||
[13, "pt-1"],
|
||||
[14, "to"],
|
||||
[15, "foul"],
|
||||
[16, "plusMinus"],
|
||||
[17, "kpi"]
|
||||
];
|
||||
|
||||
// REB: если нет newP.reb, считаем как dreb + oreb
|
||||
var newReb = newP.reb;
|
||||
if (newReb === undefined) {{
|
||||
newReb = (newP.dreb || 0) + (newP.oreb || 0);
|
||||
if (newReb === undefined || newReb === null || newReb === "") {{
|
||||
var dreb = newP.dreb || 0;
|
||||
var oreb = newP.oreb || 0;
|
||||
newReb = dreb + oreb;
|
||||
}}
|
||||
|
||||
function applyUpdate(td, newVal) {{
|
||||
newVal = safe(newVal);
|
||||
td.textContent = newVal;
|
||||
td.textContent = safe(newVal);
|
||||
}}
|
||||
|
||||
fields.forEach(function(item) {{
|
||||
fields.forEach(function (item) {{
|
||||
var index = item[0];
|
||||
var key = item[1];
|
||||
var td = tds[index];
|
||||
|
||||
var key = item[1];
|
||||
var td = tds[index];
|
||||
if (!td) return;
|
||||
|
||||
if (key === "reb") {{
|
||||
@@ -4845,16 +5229,45 @@ async def commentary():
|
||||
}});
|
||||
}}
|
||||
|
||||
function refreshSelected() {{
|
||||
var saved = localStorage.getItem("commentary_last_player");
|
||||
if (!saved) return;
|
||||
try {{
|
||||
var obj = JSON.parse(saved);
|
||||
if (obj && obj.team && obj.id) {{
|
||||
selectPlayer(obj.team, obj.id, true, true);
|
||||
}}
|
||||
}} catch (e) {{}}
|
||||
}}
|
||||
|
||||
|
||||
|
||||
|
||||
function refreshSelected() {{
|
||||
// если никого не выбрали – нечего обновлять
|
||||
if (!selectedLeft && !selectedRight) {{
|
||||
return;
|
||||
}}
|
||||
|
||||
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() {{
|
||||
fetch('/team1').then(function(r) {{
|
||||
@@ -4884,8 +5297,9 @@ async def commentary():
|
||||
// 🔁 обновляем шапку и четверти
|
||||
updateHeaderFromLiveStatus(); // счёт + фолы из live_status
|
||||
updateQuartersFromScores(); // счёт по четвертям из /scores
|
||||
updateTimeoutsFromTeamStats(); // тайм-ауты обновление
|
||||
|
||||
}}, 1000);
|
||||
}}, 1500);
|
||||
updateTimeoutsFromTeamStats(); // тайм-ауты строго из team_stats.timeout_left
|
||||
|
||||
}})();
|
||||
@@ -5002,7 +5416,7 @@ async def commentary():
|
||||
}}
|
||||
// запустим автообновление счёта и четвертей
|
||||
updateScoreAndQuarters();
|
||||
setInterval(updateScoreAndQuarters, 1000);
|
||||
setInterval(updateScoreAndQuarters, 1500);
|
||||
|
||||
makeTableSortable("team1_table");
|
||||
makeTableSortable("team2_table");
|
||||
@@ -5010,7 +5424,7 @@ async def commentary():
|
||||
// первый старт стилей сразу
|
||||
updateRowStyles();
|
||||
// и периодически обновляем (на случай любых внешних изменений)
|
||||
setInterval(updateRowStyles, 1000);
|
||||
setInterval(updateRowStyles, 1500);
|
||||
}});
|
||||
</script>
|
||||
</body>
|
||||
@@ -5563,6 +5977,18 @@ async def milestones_ui():
|
||||
align-items: center;
|
||||
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 {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
|
||||
Reference in New Issue
Block a user