выделение удалений, и заготовки
This commit is contained in:
@@ -107,6 +107,17 @@
|
||||
shortcutInventory: { device_id: "", device_name: "", online: false, vmix_connected: false, inventory: { inputs: [] }, devices: [] },
|
||||
shortcutInventoryLoading: false,
|
||||
shortcutEditorScrollTop: 0,
|
||||
preparedTitles: [],
|
||||
preparedTitleInventory: { device_id: "", device_name: "", inventory: { inputs: [] } },
|
||||
preparedTitlesLoading: false,
|
||||
preparedTitlesLoadedKey: "",
|
||||
preparedTitleSearch: "",
|
||||
preparedTitleSourceKey: "",
|
||||
preparedTitleFieldValues: {},
|
||||
preparedTitleName: "",
|
||||
preparedTitlePanelId: "",
|
||||
preparedTitleMappingSources: [],
|
||||
preparedTitleSnapshotLoading: false,
|
||||
modalLocked: false,
|
||||
timerQuickEditorInterval: null,
|
||||
timers: {},
|
||||
@@ -2550,6 +2561,13 @@ function startCustomTooltips() {
|
||||
state.config.tabs = (Array.isArray(state.config.tabs) ? state.config.tabs : []).filter((tab) => tab?.id !== "prematch");
|
||||
state.config.components = components.filter((component) => component?.type !== "hockey_prematch_panel" && component?.action_id !== "hockey_prematch_panel");
|
||||
if (!state.config.tabs.length) state.config.tabs = [{ id: "main", label: "Игра" }];
|
||||
// BUILD84: "Заготовки" is a built-in runtime workspace rather than a canvas
|
||||
// component. Keep it near the game tab and recreate it if an older config
|
||||
// does not contain it yet.
|
||||
if (!state.config.tabs.some((tab) => tab?.id === "prepared_titles")) {
|
||||
const mainIndex = Math.max(0, state.config.tabs.findIndex((tab) => tab?.id === "main"));
|
||||
state.config.tabs.splice(mainIndex + 1, 0, { id: "prepared_titles", label: "Заготовки" });
|
||||
}
|
||||
if (state.activeTab === "prematch") state.activeTab = state.config.tabs.find((tab) => tab.id === "main")?.id || state.config.tabs[0].id;
|
||||
}
|
||||
|
||||
@@ -4665,14 +4683,14 @@ function startCustomTooltips() {
|
||||
const home = hockeyPenaltySideMappingDetail(sortedPenaltyEntries("home")[0] || null, "home");
|
||||
const away = hockeyPenaltySideMappingDetail(sortedPenaltyEntries("away")[0] || null, "away");
|
||||
const values = {
|
||||
selected_home_penalty_id: home?.penalty_id || "",
|
||||
selected_home_penalty_player_id: home?.player_id || "",
|
||||
selected_home_penalty_player_db_id: home?.player_db_id || "",
|
||||
selected_home_penalty_team_penalty: home ? (home.team_penalty ? "1" : "0") : "",
|
||||
selected_away_penalty_id: away?.penalty_id || "",
|
||||
selected_away_penalty_player_id: away?.player_id || "",
|
||||
selected_away_penalty_player_db_id: away?.player_db_id || "",
|
||||
selected_away_penalty_team_penalty: away ? (away.team_penalty ? "1" : "0") : "",
|
||||
active_home_penalty_id: home?.penalty_id || "",
|
||||
active_home_penalty_player_id: home?.player_id || "",
|
||||
active_home_penalty_player_db_id: home?.player_db_id || "",
|
||||
active_home_penalty_team_penalty: home ? (home.team_penalty ? "1" : "0") : "",
|
||||
active_away_penalty_id: away?.penalty_id || "",
|
||||
active_away_penalty_player_id: away?.player_id || "",
|
||||
active_away_penalty_player_db_id: away?.player_db_id || "",
|
||||
active_away_penalty_team_penalty: away ? (away.team_penalty ? "1" : "0") : "",
|
||||
};
|
||||
const signature = JSON.stringify([gameId, values]);
|
||||
if (!force && signature === state.hockeyPenaltyMappingContextSignature) return false;
|
||||
@@ -6715,8 +6733,11 @@ function openTimerQuickEditor(focusActionId = "") {
|
||||
board.penalties = board.penalties.filter((item) => item.id !== event.id);
|
||||
const side = String(event.player?.side || event.side || "").toLowerCase();
|
||||
const remainingOnSide = board.penalties.filter((item) => !item.finished && hockeyEventReady(item) && String(item.player?.side || item.side || "").toLowerCase() === side).length;
|
||||
if (board.selectedEventId === event.id) board.selectedEventId = null;
|
||||
if (board.selectedPreviewEventIds?.[side] === event.id) board.selectedPreviewEventIds[side] = "";
|
||||
const clearCommonSelection = board.selectedEventId === event.id;
|
||||
const clearSideSelection = board.selectedPreviewEventIds?.[side] === event.id;
|
||||
if (clearCommonSelection) board.selectedEventId = null;
|
||||
if (clearSideSelection) board.selectedPreviewEventIds[side] = "";
|
||||
if (clearSideSelection) hockeyClearSelectedPenaltyMappingContext(side, { clearCommon: clearCommonSelection }).catch(() => {});
|
||||
persistHockeyBoard(component, board, true);
|
||||
refreshHockeyBoardNodes(component);
|
||||
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||
@@ -6735,10 +6756,13 @@ function openTimerQuickEditor(focusActionId = "") {
|
||||
emitHockeyEventUpdated(component, event, "event-time");
|
||||
} else if (command === "remove") {
|
||||
board.penalties = board.penalties.filter((item) => item.id !== eventId);
|
||||
if (board.selectedEventId === eventId) board.selectedEventId = null;
|
||||
const clearCommonSelection = board.selectedEventId === eventId;
|
||||
if (clearCommonSelection) board.selectedEventId = null;
|
||||
{
|
||||
const removedSide = String(event.player?.side || event.side || "").toLowerCase();
|
||||
if (board.selectedPreviewEventIds?.[removedSide] === eventId) board.selectedPreviewEventIds[removedSide] = "";
|
||||
const clearSideSelection = board.selectedPreviewEventIds?.[removedSide] === eventId;
|
||||
if (clearSideSelection) board.selectedPreviewEventIds[removedSide] = "";
|
||||
if (clearSideSelection) hockeyClearSelectedPenaltyMappingContext(removedSide, { clearCommon: clearCommonSelection }).catch(() => {});
|
||||
}
|
||||
addHockeyHistory(
|
||||
component,
|
||||
@@ -7342,6 +7366,57 @@ function hockeyPenaltyPlayerIdentifiers(component, event) {
|
||||
return { externalId, dbId };
|
||||
}
|
||||
|
||||
async function hockeyClearSelectedPenaltyMappingContext(side, { clearCommon = false } = {}) {
|
||||
const gameId = hockeyTimerSelectedGameId();
|
||||
const cleanSide = ["home", "away"].includes(String(side || "").toLowerCase()) ? String(side).toLowerCase() : "";
|
||||
if (!gameId || !cleanSide) return false;
|
||||
const values = cleanSide === "home"
|
||||
? {
|
||||
selected_home_penalty_id: "",
|
||||
selected_home_penalty_player_id: "",
|
||||
selected_home_penalty_player_db_id: "",
|
||||
selected_home_penalty_team_penalty: "",
|
||||
}
|
||||
: {
|
||||
selected_away_penalty_id: "",
|
||||
selected_away_penalty_player_id: "",
|
||||
selected_away_penalty_player_db_id: "",
|
||||
selected_away_penalty_team_penalty: "",
|
||||
};
|
||||
if (clearCommon) Object.assign(values, {
|
||||
selected_penalty_id: "",
|
||||
selected_penalty_player_id: "",
|
||||
selected_penalty_player_db_id: "",
|
||||
selected_penalty_team_penalty: "",
|
||||
selected_penalty_team_id: "",
|
||||
selected_penalty_infraction_id: "",
|
||||
selected_penalty_preset_id: "",
|
||||
selected_penalty_side: "",
|
||||
selected_penalty_event_time: "",
|
||||
selected_penalty_status: "",
|
||||
selected_penalty_remaining_ms: "",
|
||||
selected_penalty_duration_ms: "",
|
||||
selected_event_id: "",
|
||||
});
|
||||
try {
|
||||
const response = await fetch("/api/hockey/context/batch", {
|
||||
method: "POST", cache: "no-store", credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
values,
|
||||
context: {
|
||||
game_id: gameId,
|
||||
device_id: currentRuntimeVmixDeviceId(),
|
||||
session_token: currentRuntimeHockeySessionToken(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
return response.ok;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hockeyPenaltyIsPreviewSelected(board, event) {
|
||||
const side = String(event.player?.side || event.side || "").toLowerCase();
|
||||
return ["home", "away"].includes(side)
|
||||
@@ -7363,6 +7438,7 @@ async function hockeySelectPenaltyForPreview(component, event) {
|
||||
: side === "away"
|
||||
? String(getByPath(state.data, "hockey.selected_game.away.id") || getByPath(state.data, "hockey.selected_game.away.external_id") || "")
|
||||
: "";
|
||||
const playerIds = hockeyPenaltyPlayerIdentifiers(component, event);
|
||||
const detail = {
|
||||
item_id: String(event.external_id || event.id || ""),
|
||||
value: String(event.external_id || event.id || ""),
|
||||
@@ -7461,7 +7537,6 @@ function hockeyPenaltyCard(component, event, runtime) {
|
||||
const side = event.player?.side || event.side || "neutral";
|
||||
const card = div(`hpd-team-penalty-card team-${side}`);
|
||||
card.dataset.penaltyId = event.id;
|
||||
card.classList.toggle("is-selected", board.selectedEventId === event.id);
|
||||
card.classList.toggle("is-preview-selected", hockeyPenaltyIsPreviewSelected(board, event));
|
||||
card.classList.toggle("is-running", event.running);
|
||||
card.classList.toggle("is-finished", event.finished);
|
||||
@@ -7817,8 +7892,8 @@ function openHockeyPrematchButtonsEditor() {
|
||||
<label>ID для Mapping<input type="text" maxlength="48" data-player-panel-field="id" data-player-panel-id="${escapeHtml(panel.id)}" value="${escapeHtml(panel.id)}"></label>
|
||||
<label>Слотов<input type="number" min="1" max="6" data-player-panel-field="slots" data-player-panel-id="${escapeHtml(panel.id)}" value="${panel.slots}"></label>
|
||||
<label>Правило<select data-player-panel-field="rule" data-player-panel-id="${escapeHtml(panel.id)}">${triggerSelectOptions([["any","Любые игроки"],["home","Только левая команда"],["away","Только правая команда"],["different_teams","Разные команды"]], panel.rule)}</select></label>
|
||||
<label class="shortcut-inline-check"><input type="checkbox" data-player-panel-field="sync_selected_player" data-player-panel-id="${escapeHtml(panel.id)}" ${panel.sync_selected_player ? "checked" : ""}> Главный выбранный игрок</label>
|
||||
<label class="shortcut-inline-check"><input type="checkbox" data-player-panel-field="collapsed_default" data-player-panel-id="${escapeHtml(panel.id)}" ${panel.collapsed_default ? "checked" : ""}> По умолчанию свёрнут</label>
|
||||
<label class="shortcut-inline-check" title="Использовать первый слот как общий selected_player"><input type="checkbox" data-player-panel-field="sync_selected_player" data-player-panel-id="${escapeHtml(panel.id)}" ${panel.sync_selected_player ? "checked" : ""}> Главный</label>
|
||||
<label class="shortcut-inline-check" title="Сворачивать блок при первом открытии"><input type="checkbox" data-player-panel-field="collapsed_default" data-player-panel-id="${escapeHtml(panel.id)}" ${panel.collapsed_default ? "checked" : ""}> Свёрнут</label>
|
||||
<label class="player-selection-editor-description">Описание<input type="text" maxlength="300" data-player-panel-field="description" data-player-panel-id="${escapeHtml(panel.id)}" value="${escapeHtml(panel.description)}"></label>
|
||||
<div class="player-selection-editor-order"><button type="button" class="mini-btn" data-player-panel-up="${escapeHtml(panel.id)}" ${index <= 0 ? "disabled" : ""}>↑</button><button type="button" class="mini-btn" data-player-panel-down="${escapeHtml(panel.id)}" ${index >= draftPlayerPanels.length - 1 ? "disabled" : ""}>↓</button></div>
|
||||
<button type="button" class="icon-btn danger" data-player-panel-delete="${escapeHtml(panel.id)}" title="Удалить блок">×</button>
|
||||
@@ -8189,6 +8264,7 @@ async function hockeySetMatchValues(patch, { refreshMapping = true } = {}) {
|
||||
if (!Object.keys(values).length) return null;
|
||||
const language = hockeyGameControlLanguage();
|
||||
const payload = await hockeyUpdateGameControl(gameId, "/control/values", { method: "PUT", body: JSON.stringify({ values, language }) });
|
||||
hockeyBackupPlayerSelectionValues(gameId, payload?.values || values);
|
||||
if (refreshMapping) await hockeyRefreshQuickPanelMapping();
|
||||
return payload;
|
||||
}
|
||||
@@ -8469,6 +8545,43 @@ function hockeyEventCategoryLabels(language) {
|
||||
: {all:"Все",goal:"Гол",penalty:"Удаление",shot:"Бросок",shootout:"Буллит",period:"Период",timeout:"Тайм-аут",goalie:"Вратарь",comment:"Комментарий",info:"Событие"};
|
||||
}
|
||||
|
||||
function hockeyPlayerSelectionStorageKey(gameId) {
|
||||
return `hockey:player-selections:${String(gameId || "").trim()}`;
|
||||
}
|
||||
|
||||
function hockeyPlayerSelectionValuesOnly(values) {
|
||||
const result = {};
|
||||
Object.entries(values && typeof values === "object" ? values : {}).forEach(([key, value]) => {
|
||||
if (String(key).startsWith("player_select.")) result[String(key)] = String(value ?? "");
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function hockeyBackupPlayerSelectionValues(gameId, values) {
|
||||
const id = String(gameId || "").trim();
|
||||
if (!id) return false;
|
||||
const selected = hockeyPlayerSelectionValuesOnly(values);
|
||||
if (!Object.keys(selected).length) return false;
|
||||
try {
|
||||
localStorage.setItem(hockeyPlayerSelectionStorageKey(id), JSON.stringify({ game_id: id, saved_at: Date.now(), values: selected }));
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hockeyStoredPlayerSelectionValues(gameId) {
|
||||
const id = String(gameId || "").trim();
|
||||
if (!id) return {};
|
||||
try {
|
||||
const payload = JSON.parse(localStorage.getItem(hockeyPlayerSelectionStorageKey(id)) || "{}");
|
||||
if (String(payload?.game_id || "") !== id || !payload?.values || typeof payload.values !== "object") return {};
|
||||
return hockeyPlayerSelectionValuesOnly(payload.values);
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function hockeyPlayerSelectionPanels() {
|
||||
return normalizePlayerSelectionPanels(state.config.player_selection_panels).filter((panel) => panel.enabled !== false);
|
||||
}
|
||||
@@ -8616,8 +8729,17 @@ async function hockeyEnsurePlayerSelectionRuntimeValues() {
|
||||
const gameId = hockeyTimerSelectedGameId();
|
||||
const panels = hockeyPlayerSelectionPanels();
|
||||
if (!gameId || !panels.length || state.hockeyPlayerPanelSeedPending) return false;
|
||||
// Do not seed empty values before the saved GameControlState has arrived.
|
||||
// On a hard refresh that race used to erase an already prepared comparison.
|
||||
const control = getByPath(state.data, "hockey.game_control");
|
||||
if (!control || String(control.game_id || "") !== String(gameId)) return false;
|
||||
const current = hockeyMatchRuntimeValues();
|
||||
const patch = {};
|
||||
const hasServerSelectionKeys = Object.keys(current).some((key) => String(key).startsWith("player_select."));
|
||||
if (!hasServerSelectionKeys) {
|
||||
const backup = hockeyStoredPlayerSelectionValues(gameId);
|
||||
Object.entries(backup).forEach(([key, value]) => { patch[key] = value; });
|
||||
}
|
||||
for (const panel of panels) {
|
||||
const meta = {
|
||||
[`player_select.${panel.id}._label`]: panel.label,
|
||||
@@ -8715,7 +8837,7 @@ function renderHockeyPlayerSelectionWindows() {
|
||||
</div>`;
|
||||
}).join("")}
|
||||
</div>
|
||||
${filled ? `<button type="button" class="hockey-player-select-clear" data-player-panel-clear-all="${escapeHtml(panel.id)}">Очистить блок</button>` : ""}
|
||||
${filled ? `<div class="hockey-player-select-footer"><button type="button" class="hockey-player-select-prepared" data-player-panel-prepared="${escapeHtml(panel.id)}">+ В заготовки</button><button type="button" class="hockey-player-select-clear" data-player-panel-clear-all="${escapeHtml(panel.id)}">Очистить блок</button></div>` : ""}
|
||||
</div>`;
|
||||
stack.appendChild(node);
|
||||
node.querySelector("[data-player-panel-collapse]")?.addEventListener("click", () => {
|
||||
@@ -8773,6 +8895,7 @@ function renderHockeyPlayerSelectionWindows() {
|
||||
event.preventDefault(); event.stopPropagation();
|
||||
await hockeyClearPlayerSelectionSlot(panel, Number(button.dataset.playerPanelClearSlot || 1));
|
||||
}));
|
||||
node.querySelector("[data-player-panel-prepared]")?.addEventListener("click", () => hockeyOpenPreparedFromPlayerPanel(panel));
|
||||
node.querySelector("[data-player-panel-clear-all]")?.addEventListener("click", async () => hockeyClearPlayerSelectionPanel(panel));
|
||||
}
|
||||
setTimeout(() => hockeyEnsurePlayerSelectionRuntimeValues().catch(() => {}), 0);
|
||||
@@ -8789,6 +8912,389 @@ function renderHockeyRuntimeSideWindows() {
|
||||
return Boolean(players || pbp);
|
||||
}
|
||||
|
||||
|
||||
function hockeyPreparedNumber(value, fallback = 999999) {
|
||||
const raw = String(value ?? "").trim();
|
||||
return /^\d+$/.test(raw) ? Number(raw) : fallback;
|
||||
}
|
||||
|
||||
function hockeyPreparedInventoryInputs() {
|
||||
const inventory = state.preparedTitleInventory?.inventory || {};
|
||||
return (Array.isArray(inventory.inputs) ? inventory.inputs : [])
|
||||
.filter((item) => item && typeof item === "object" && Array.isArray(item.fields) && item.fields.length)
|
||||
.slice()
|
||||
.sort((a, b) => hockeyPreparedNumber(a.number) - hockeyPreparedNumber(b.number) || String(a.title || "").localeCompare(String(b.title || ""), "ru", { numeric: true, sensitivity: "base" }));
|
||||
}
|
||||
|
||||
function hockeyPreparedSourceIdentity(input) {
|
||||
if (!input) return "";
|
||||
return String(input.key || input.number || input.title || "").trim();
|
||||
}
|
||||
|
||||
function hockeyPreparedSelectedInput() {
|
||||
const identity = String(state.preparedTitleSourceKey || "").trim();
|
||||
if (!identity) return null;
|
||||
return hockeyPreparedInventoryInputs().find((item) => hockeyPreparedSourceIdentity(item) === identity || String(item.key || "") === identity || String(item.number || "") === identity) || null;
|
||||
}
|
||||
|
||||
function hockeyPreparedFieldType(field) {
|
||||
const explicit = String(field?.type || "").toLowerCase();
|
||||
if (["color", "colour"].includes(explicit) || /\.Color$/i.test(String(field?.name || ""))) return "color";
|
||||
if (["image", "source"].includes(explicit) || /\.Source$/i.test(String(field?.name || ""))) return "image";
|
||||
return "text";
|
||||
}
|
||||
|
||||
function hockeyPreparedFieldValuesForInput(input, { preserve = true } = {}) {
|
||||
const next = {};
|
||||
(Array.isArray(input?.fields) ? input.fields : []).forEach((field) => {
|
||||
const name = String(field?.name || "").trim();
|
||||
if (!name) return;
|
||||
const type = hockeyPreparedFieldType(field);
|
||||
const previous = state.preparedTitleFieldValues?.[name];
|
||||
next[name] = {
|
||||
type,
|
||||
value: preserve && previous && typeof previous === "object" ? String(previous.value ?? "") : "",
|
||||
touched: Boolean(preserve && previous && typeof previous === "object" && previous.touched),
|
||||
};
|
||||
});
|
||||
state.preparedTitleFieldValues = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
function hockeyPreparedWorkspaceKey() {
|
||||
return [currentRuntimeVmixDeviceId(), hockeyTimerSelectedGameId()].join("|");
|
||||
}
|
||||
|
||||
async function hockeyLoadPreparedMappingSources(panelId = state.preparedTitlePanelId, { render = false } = {}) {
|
||||
const clean = String(panelId || "").trim();
|
||||
if (!clean) {
|
||||
state.preparedTitleMappingSources = [];
|
||||
if (render && state.activeTab === "prepared_titles") renderRuntime();
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
const deviceId = currentRuntimeVmixDeviceId();
|
||||
if (deviceId) params.set("device_id", deviceId);
|
||||
params.set("panel_id", clean);
|
||||
const payload = await api(`/api/hockey/prepared-titles/mapping-sources?${params.toString()}`);
|
||||
state.preparedTitleMappingSources = Array.isArray(payload?.items) ? payload.items : [];
|
||||
if (!state.preparedTitleSourceKey && state.preparedTitleMappingSources.length === 1) {
|
||||
const source = state.preparedTitleMappingSources[0];
|
||||
const inventoryMatch = hockeyPreparedInventoryInputs().find((item) =>
|
||||
(source.key && String(item.key || "") === String(source.key)) ||
|
||||
(source.number && String(item.number || "") === String(source.number)) ||
|
||||
(source.title && String(item.title || "") === String(source.title))
|
||||
);
|
||||
if (inventoryMatch) {
|
||||
state.preparedTitleSourceKey = hockeyPreparedSourceIdentity(inventoryMatch);
|
||||
hockeyPreparedFieldValuesForInput(inventoryMatch, { preserve: false });
|
||||
}
|
||||
}
|
||||
if (render && state.activeTab === "prepared_titles") renderRuntime();
|
||||
return state.preparedTitleMappingSources;
|
||||
} catch (error) {
|
||||
console.error("Prepared title mapping sources error", error);
|
||||
state.preparedTitleMappingSources = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function hockeyLoadPreparedTitlesWorkspace({ force = false, render = true } = {}) {
|
||||
if (state.preparedTitlesLoading) return false;
|
||||
const key = hockeyPreparedWorkspaceKey();
|
||||
if (!force && state.preparedTitlesLoadedKey === key && state.preparedTitleInventory?.inventory) {
|
||||
if (state.preparedTitlePanelId && !state.preparedTitleMappingSources.length) await hockeyLoadPreparedMappingSources(state.preparedTitlePanelId);
|
||||
return true;
|
||||
}
|
||||
state.preparedTitlesLoading = true;
|
||||
if (render && state.activeTab === "prepared_titles") renderRuntime();
|
||||
try {
|
||||
const deviceId = currentRuntimeVmixDeviceId();
|
||||
const gameId = hockeyTimerSelectedGameId();
|
||||
const invParams = new URLSearchParams();
|
||||
if (deviceId) invParams.set("device_id", deviceId);
|
||||
const listParams = new URLSearchParams();
|
||||
if (deviceId) listParams.set("device_id", deviceId);
|
||||
if (gameId) listParams.set("game_id", gameId);
|
||||
const [inventoryPayload, preparedPayload] = await Promise.all([
|
||||
api(`/api/hockey/agents/vmix-inventory${invParams.toString() ? `?${invParams}` : ""}`),
|
||||
api(`/api/hockey/prepared-titles${listParams.toString() ? `?${listParams}` : ""}`),
|
||||
]);
|
||||
state.preparedTitleInventory = inventoryPayload || { device_id: "", device_name: "", inventory: { inputs: [] } };
|
||||
state.preparedTitles = Array.isArray(preparedPayload?.items) ? preparedPayload.items : [];
|
||||
state.preparedTitlesLoadedKey = key;
|
||||
const selected = hockeyPreparedSelectedInput();
|
||||
if (state.preparedTitleSourceKey && !selected) {
|
||||
state.preparedTitleSourceKey = "";
|
||||
state.preparedTitleFieldValues = {};
|
||||
} else if (selected && !Object.keys(state.preparedTitleFieldValues || {}).length) {
|
||||
hockeyPreparedFieldValuesForInput(selected, { preserve: false });
|
||||
}
|
||||
if (state.preparedTitlePanelId) await hockeyLoadPreparedMappingSources(state.preparedTitlePanelId);
|
||||
if (state.preparedTitleAutoSnapshotPending && state.preparedTitleSourceKey) {
|
||||
state.preparedTitleAutoSnapshotPending = false;
|
||||
setTimeout(() => hockeyApplyPreparedMappingSnapshot().catch(() => {}), 0);
|
||||
}
|
||||
} catch (error) {
|
||||
toast(`Заготовки: ${error.message}`, true);
|
||||
console.error(error);
|
||||
} finally {
|
||||
state.preparedTitlesLoading = false;
|
||||
if (render && state.activeTab === "prepared_titles") renderRuntime();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function hockeySelectPreparedSource(input, { clearPanel = false } = {}) {
|
||||
if (!input) return false;
|
||||
state.preparedTitleSourceKey = hockeyPreparedSourceIdentity(input);
|
||||
hockeyPreparedFieldValuesForInput(input, { preserve: false });
|
||||
if (!state.preparedTitleName) state.preparedTitleName = `${String(input.title || `Input ${input.number || ""}`).trim()} · заготовка`;
|
||||
if (clearPanel) {
|
||||
state.preparedTitlePanelId = "";
|
||||
state.preparedTitleMappingSources = [];
|
||||
}
|
||||
renderRuntime();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function hockeyApplyPreparedMappingSnapshot() {
|
||||
const input = hockeyPreparedSelectedInput();
|
||||
if (!input) {
|
||||
toast("Сначала выберите vMix Input", true);
|
||||
return false;
|
||||
}
|
||||
state.preparedTitleSnapshotLoading = true;
|
||||
renderRuntime();
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
const deviceId = currentRuntimeVmixDeviceId();
|
||||
if (deviceId) params.set("device_id", deviceId);
|
||||
if (input.key) params.set("input_key", input.key);
|
||||
if (input.number) params.set("input_number", input.number);
|
||||
if (input.title) params.set("input_title", input.title);
|
||||
if (state.preparedTitlePanelId) params.set("panel_id", state.preparedTitlePanelId);
|
||||
const payload = await api(`/api/hockey/prepared-titles/mapping-snapshot?${params.toString()}`);
|
||||
const fields = Array.isArray(payload?.fields) ? payload.fields : [];
|
||||
if (!fields.length) {
|
||||
toast(state.preparedTitlePanelId ? "Для этого блока и Input нет Mapping-связей" : "Для этого Input нет Mapping-связей", true);
|
||||
return false;
|
||||
}
|
||||
const next = { ...(state.preparedTitleFieldValues || {}) };
|
||||
fields.forEach((field) => {
|
||||
const name = String(field?.name || "");
|
||||
if (!name || !Object.prototype.hasOwnProperty.call(next, name)) return;
|
||||
next[name] = { type: hockeyPreparedFieldType(field), value: String(field?.value ?? ""), touched: true };
|
||||
});
|
||||
state.preparedTitleFieldValues = next;
|
||||
toast(`Подставлено из Mapping: ${fields.length}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
toast(`Mapping → заготовка: ${error.message}`, true);
|
||||
return false;
|
||||
} finally {
|
||||
state.preparedTitleSnapshotLoading = false;
|
||||
renderRuntime();
|
||||
}
|
||||
}
|
||||
|
||||
async function hockeyCreatePreparedTitle() {
|
||||
const input = hockeyPreparedSelectedInput();
|
||||
if (!input) {
|
||||
toast("Сначала выберите исходный vMix Input", true);
|
||||
return false;
|
||||
}
|
||||
const name = String(state.preparedTitleName || "").trim() || `${input.title || `Input ${input.number || ""}`} · заготовка`;
|
||||
const fieldValues = {};
|
||||
Object.entries(state.preparedTitleFieldValues || {}).forEach(([fieldName, entry]) => {
|
||||
if (!entry?.touched) return;
|
||||
fieldValues[fieldName] = {
|
||||
type: String(entry?.type || "text"),
|
||||
value: String(entry?.value ?? ""),
|
||||
};
|
||||
});
|
||||
state.preparedTitlesLoading = true;
|
||||
renderRuntime();
|
||||
try {
|
||||
const payload = await api("/api/hockey/prepared-titles", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
device_id: currentRuntimeVmixDeviceId(),
|
||||
session_token: currentRuntimeHockeySessionToken(),
|
||||
source_input_key: String(input.key || ""),
|
||||
source_input_number: String(input.number || ""),
|
||||
source_input_title: String(input.title || ""),
|
||||
source_kind: state.preparedTitlePanelId ? "player_selection" : "manual",
|
||||
source_ref: state.preparedTitlePanelId || "",
|
||||
field_values: fieldValues,
|
||||
}),
|
||||
});
|
||||
toast(`Заготовка сохранена${payload?.clone_input?.number ? ` · Input #${payload.clone_input.number}` : ""}`);
|
||||
state.preparedTitleName = "";
|
||||
state.preparedTitlePanelId = "";
|
||||
state.preparedTitleMappingSources = [];
|
||||
state.preparedTitlesLoadedKey = "";
|
||||
await hockeyLoadPreparedTitlesWorkspace({ force: true, render: false });
|
||||
return true;
|
||||
} catch (error) {
|
||||
toast(`Не удалось создать заготовку: ${error.message}`, true);
|
||||
return false;
|
||||
} finally {
|
||||
state.preparedTitlesLoading = false;
|
||||
renderRuntime();
|
||||
}
|
||||
}
|
||||
|
||||
async function hockeyPreviewPreparedTitle(id) {
|
||||
try {
|
||||
await api(`/api/hockey/prepared-titles/${encodeURIComponent(id)}/preview`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ device_id: currentRuntimeVmixDeviceId(), session_token: currentRuntimeHockeySessionToken() }),
|
||||
});
|
||||
toast("Заготовка отправлена в Preview");
|
||||
return true;
|
||||
} catch (error) {
|
||||
toast(`Preview: ${error.message}`, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function hockeyDeletePreparedTitle(id) {
|
||||
if (!confirm("Убрать заготовку из списка? Сам vMix Input останется в проекте.")) return false;
|
||||
try {
|
||||
await api(`/api/hockey/prepared-titles/${encodeURIComponent(id)}`, { method: "DELETE" });
|
||||
state.preparedTitles = state.preparedTitles.filter((item) => String(item.id) !== String(id));
|
||||
renderRuntime();
|
||||
return true;
|
||||
} catch (error) {
|
||||
toast(`Удаление заготовки: ${error.message}`, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hockeyOpenPreparedFromPlayerPanel(panel) {
|
||||
if (!panel) return false;
|
||||
state.preparedTitlePanelId = String(panel.id || "");
|
||||
state.preparedTitleName = `${String(panel.label || "Выбор игроков")} · заготовка`;
|
||||
state.preparedTitleSourceKey = "";
|
||||
state.preparedTitleFieldValues = {};
|
||||
state.preparedTitleMappingSources = [];
|
||||
state.preparedTitleAutoSnapshotPending = true;
|
||||
state.preparedTitlesLoadedKey = "";
|
||||
activateRuntimeTab("prepared_titles", { source: "player-selection" });
|
||||
setTimeout(() => hockeyLoadPreparedTitlesWorkspace({ force: true }).catch(() => {}), 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderHockeyPreparedTitlesWorkspace() {
|
||||
const root = document.createElement("section");
|
||||
root.className = "hockey-prepared-workspace";
|
||||
const inputs = hockeyPreparedInventoryInputs();
|
||||
const search = String(state.preparedTitleSearch || "").trim().toLowerCase();
|
||||
const filtered = inputs.filter((item) => !search || `${item.number || ""} ${item.title || ""} ${item.type || ""}`.toLowerCase().includes(search));
|
||||
const selected = hockeyPreparedSelectedInput();
|
||||
const fields = selected && Array.isArray(selected.fields)
|
||||
? selected.fields.slice().sort((a, b) => String(a?.name || "").localeCompare(String(b?.name || ""), "ru", { numeric: true, sensitivity: "base" }))
|
||||
: [];
|
||||
const panel = state.preparedTitlePanelId ? hockeyPlayerSelectionPanels().find((item) => item.id === state.preparedTitlePanelId) : null;
|
||||
const mappingSources = Array.isArray(state.preparedTitleMappingSources) ? state.preparedTitleMappingSources : [];
|
||||
|
||||
root.innerHTML = `
|
||||
<div class="hockey-prepared-head">
|
||||
<div><span>VMIX PRESETS</span><strong>Заготовки</strong><small>${escapeHtml(state.preparedTitleInventory?.device_name || "Agent не выбран")} · матч ${escapeHtml(hockeyTimerSelectedGameId() || "—")}</small></div>
|
||||
<button type="button" data-prepared-refresh ${state.preparedTitlesLoading ? "disabled" : ""}>↻ Обновить vMix</button>
|
||||
</div>
|
||||
${panel ? `<div class="hockey-prepared-context"><div><span>БЛОК ИГРОКОВ</span><strong>${escapeHtml(panel.label)}</strong><small>Выберите титр, связанный с <code>player_select.${escapeHtml(panel.id)}.*</code>, затем подставьте текущие данные.</small></div><button type="button" data-prepared-clear-context>×</button></div>` : ""}
|
||||
<div class="hockey-prepared-layout">
|
||||
<aside class="hockey-prepared-inputs">
|
||||
<label class="hockey-prepared-search"><span>Поиск Input</span><input type="search" value="${escapeHtml(state.preparedTitleSearch)}" placeholder="номер или название"></label>
|
||||
${mappingSources.length ? `<div class="hockey-prepared-recommended"><span>Подходят для ${escapeHtml(panel?.label || "блока")}</span>${mappingSources.map((item) => `<button type="button" data-prepared-mapping-source="${escapeHtml(String(item.key || item.number || item.title || ""))}"><b>#${escapeHtml(item.number || "—")}</b><span>${escapeHtml(item.title || "Input")}</span><small>${escapeHtml(item.mapped_fields || 0)} связей</small></button>`).join("")}</div>` : ""}
|
||||
<div class="hockey-prepared-input-list">${filtered.map((item) => {
|
||||
const identity = hockeyPreparedSourceIdentity(item);
|
||||
const active = selected && hockeyPreparedSourceIdentity(selected) === identity;
|
||||
return `<button type="button" class="${active ? "active" : ""}" data-prepared-input="${escapeHtml(identity)}"><b>#${escapeHtml(item.number || "—")}</b><span>${escapeHtml(item.title || "Без названия")}</span><small>${escapeHtml(item.type || "")}${Array.isArray(item.fields) ? ` · ${item.fields.length}` : ""}</small></button>`;
|
||||
}).join("") || `<div class="hockey-prepared-empty">Нет Inputs с редактируемыми элементами</div>`}</div>
|
||||
</aside>
|
||||
<main class="hockey-prepared-editor">
|
||||
${selected ? `
|
||||
<div class="hockey-prepared-source"><div><span>ИСТОЧНИК</span><strong>#${escapeHtml(selected.number || "—")} · ${escapeHtml(selected.title || "Input")}</strong><small>${escapeHtml(selected.type || "")} · элементов ${fields.length}</small></div>${panel ? `<button type="button" data-prepared-apply-mapping ${state.preparedTitleSnapshotLoading ? "disabled" : ""}>${state.preparedTitleSnapshotLoading ? "Подставляю…" : "Подставить из Mapping"}</button>` : `<button type="button" data-prepared-apply-mapping ${state.preparedTitleSnapshotLoading ? "disabled" : ""}>Из Mapping</button>`}</div>
|
||||
<label class="hockey-prepared-name"><span>Название заготовки</span><input type="text" data-prepared-name value="${escapeHtml(state.preparedTitleName)}" placeholder="Например: Сравнение вратарей · студия"></label>
|
||||
<div class="hockey-prepared-fields">${fields.map((field) => {
|
||||
const name = String(field?.name || "");
|
||||
const type = hockeyPreparedFieldType(field);
|
||||
const value = String(state.preparedTitleFieldValues?.[name]?.value ?? "");
|
||||
const color = /^#[0-9A-Fa-f]{6}$/.test(value) ? value : "#ffffff";
|
||||
return `<label class="hockey-prepared-field" data-field-type="${escapeHtml(type)}"><span><b>${escapeHtml(name)}</b><small>${escapeHtml(type)}</small></span><div>${type === "color" ? `<input type="color" data-prepared-color="${escapeHtml(name)}" value="${escapeHtml(color)}">` : ""}<input type="text" data-prepared-field="${escapeHtml(name)}" data-prepared-field-type="${escapeHtml(type)}" value="${escapeHtml(value)}" placeholder="${type === "image" ? "путь к изображению" : type === "color" ? "#RRGGBB" : "текст"}"></div></label>`;
|
||||
}).join("") || `<div class="hockey-prepared-empty">У Input нет доступных элементов</div>`}</div>
|
||||
<div class="hockey-prepared-savebar"><small>При сохранении vMix создаст виртуальную копию этого Input в конце проекта и заполнит её текущими значениями.</small><button type="button" data-prepared-save ${state.preparedTitlesLoading ? "disabled" : ""}>${state.preparedTitlesLoading ? "Сохраняю…" : "+ Сохранить новым Input"}</button></div>
|
||||
` : `<div class="hockey-prepared-editor-empty"><b>Выберите vMix Input</b><span>Справа появятся все его .Text / .Source / .Color элементы для ручной заготовки.</span></div>`}
|
||||
</main>
|
||||
<aside class="hockey-prepared-saved">
|
||||
<div class="hockey-prepared-saved-head"><span>СОХРАНЁННЫЕ</span><b>${state.preparedTitles.length}</b></div>
|
||||
<div class="hockey-prepared-saved-list">${state.preparedTitles.map((item) => `<article><div><span>${escapeHtml(item.source_kind === "player_selection" ? "Блок игроков" : "Ручная")}</span><strong>${escapeHtml(item.name || "Заготовка")}</strong><small>Input #${escapeHtml(item.clone_input?.number || "—")} · из #${escapeHtml(item.source_input?.number || "—")} ${escapeHtml(item.source_input?.title || "")}</small></div><div><button type="button" data-prepared-preview="${escapeHtml(item.id)}">▶ Preview</button><button type="button" class="danger" data-prepared-delete="${escapeHtml(item.id)}" title="Убрать из списка, не удаляя Input в vMix">×</button></div></article>`).join("") || `<div class="hockey-prepared-empty">Для этого матча заготовок пока нет</div>`}</div>
|
||||
</aside>
|
||||
</div>`;
|
||||
|
||||
root.querySelector("[data-prepared-refresh]")?.addEventListener("click", () => {
|
||||
state.preparedTitlesLoadedKey = "";
|
||||
hockeyLoadPreparedTitlesWorkspace({ force: true }).catch(() => {});
|
||||
});
|
||||
root.querySelector(".hockey-prepared-search input")?.addEventListener("input", (event) => {
|
||||
state.preparedTitleSearch = event.target.value || "";
|
||||
renderRuntime();
|
||||
setTimeout(() => el.runtimeStage.querySelector(".hockey-prepared-search input")?.focus({ preventScroll: true }), 0);
|
||||
});
|
||||
root.querySelectorAll("[data-prepared-input]").forEach((button) => button.addEventListener("click", () => {
|
||||
const identity = button.dataset.preparedInput || "";
|
||||
const input = inputs.find((item) => hockeyPreparedSourceIdentity(item) === identity);
|
||||
if (input) hockeySelectPreparedSource(input);
|
||||
}));
|
||||
root.querySelectorAll("[data-prepared-mapping-source]").forEach((button) => button.addEventListener("click", async () => {
|
||||
const identity = button.dataset.preparedMappingSource || "";
|
||||
const source = mappingSources.find((item) => String(item.key || item.number || item.title || "") === identity);
|
||||
const input = source ? inputs.find((item) =>
|
||||
(source.key && String(item.key || "") === String(source.key)) ||
|
||||
(source.number && String(item.number || "") === String(source.number)) ||
|
||||
(source.title && String(item.title || "") === String(source.title))
|
||||
) : null;
|
||||
if (!input) return;
|
||||
state.preparedTitleSourceKey = hockeyPreparedSourceIdentity(input);
|
||||
hockeyPreparedFieldValuesForInput(input, { preserve: false });
|
||||
renderRuntime();
|
||||
await hockeyApplyPreparedMappingSnapshot();
|
||||
}));
|
||||
root.querySelector("[data-prepared-clear-context]")?.addEventListener("click", () => {
|
||||
state.preparedTitlePanelId = "";
|
||||
state.preparedTitleMappingSources = [];
|
||||
renderRuntime();
|
||||
});
|
||||
root.querySelector("[data-prepared-apply-mapping]")?.addEventListener("click", () => hockeyApplyPreparedMappingSnapshot());
|
||||
root.querySelector("[data-prepared-name]")?.addEventListener("input", (event) => { state.preparedTitleName = event.target.value || ""; });
|
||||
root.querySelectorAll("[data-prepared-field]").forEach((inputNode) => inputNode.addEventListener("input", () => {
|
||||
const name = inputNode.dataset.preparedField || "";
|
||||
if (!name) return;
|
||||
state.preparedTitleFieldValues[name] = { type: inputNode.dataset.preparedFieldType || "text", value: inputNode.value || "", touched: true };
|
||||
if ((inputNode.dataset.preparedFieldType || "") === "color") {
|
||||
const picker = root.querySelector(`[data-prepared-color="${CSS.escape(name)}"]`);
|
||||
if (picker && /^#[0-9A-Fa-f]{6}$/.test(inputNode.value || "")) picker.value = inputNode.value;
|
||||
}
|
||||
}));
|
||||
root.querySelectorAll("[data-prepared-color]").forEach((picker) => picker.addEventListener("input", () => {
|
||||
const name = picker.dataset.preparedColor || "";
|
||||
const text = root.querySelector(`[data-prepared-field="${CSS.escape(name)}"]`);
|
||||
if (text) text.value = picker.value;
|
||||
state.preparedTitleFieldValues[name] = { type: "color", value: picker.value, touched: true };
|
||||
}));
|
||||
root.querySelector("[data-prepared-save]")?.addEventListener("click", () => hockeyCreatePreparedTitle());
|
||||
root.querySelectorAll("[data-prepared-preview]").forEach((button) => button.addEventListener("click", () => hockeyPreviewPreparedTitle(button.dataset.preparedPreview)));
|
||||
root.querySelectorAll("[data-prepared-delete]").forEach((button) => button.addEventListener("click", () => hockeyDeletePreparedTitle(button.dataset.preparedDelete)));
|
||||
return root;
|
||||
}
|
||||
|
||||
function hockeyEventCategory(item) {
|
||||
const declared = String(item?.category || "").trim().toLowerCase();
|
||||
// Prefer the explicit Stat2TV code (`pn`, `go`, ...). `type` can contain a
|
||||
@@ -10588,6 +11094,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
{ hockey: { game_control: payload } },
|
||||
{ render }
|
||||
);
|
||||
hockeyBackupPlayerSelectionValues(gameId, payload?.values || {});
|
||||
if (!previousControl || hockeyStrengthMappingSignature(previousControl) !== hockeyStrengthMappingSignature(payload)) {
|
||||
hockeyRefreshVmixMappingForStrength(gameId, previousControl, payload).catch(() => {});
|
||||
}
|
||||
@@ -11832,6 +12339,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
}
|
||||
|
||||
function runtimeHasFixedHockeyCanvas() {
|
||||
if (state.activeTab === "prepared_titles") return true;
|
||||
return state.config.components.some((component) =>
|
||||
[
|
||||
"hockey_penalty_dashboard",
|
||||
@@ -11936,11 +12444,18 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
el.runtimeStage.innerHTML = "";
|
||||
state.timerNodes = new Map();
|
||||
state.hockeyPenaltyBoardNodes = new Map();
|
||||
state.config.components.filter((component) => !component.hidden && component.props?.runtimeHidden !== true && state.runtimeVisibility[component.action_id] !== false && isEffectivelyOnActiveTab(component)).sort((a, b) => Number(a.z) - Number(b.z)).forEach((component) => {
|
||||
const wrapper = document.createElement("div"); wrapper.className = "runtime-component"; Object.assign(wrapper.style, { left: `${component.x}px`, top: `${component.y}px`, width: `${component.w}px`, height: `${component.h}px`, zIndex: String(component.z) }); wrapper.appendChild(renderComponent(component, true)); el.runtimeStage.appendChild(wrapper);
|
||||
});
|
||||
renderHockeyRuntimeSideWindows();
|
||||
renderHockeyQuickCommandDock();
|
||||
if (state.activeTab === "prepared_titles") {
|
||||
el.runtimeStage.appendChild(renderHockeyPreparedTitlesWorkspace());
|
||||
if (state.preparedTitlesLoadedKey !== hockeyPreparedWorkspaceKey() && !state.preparedTitlesLoading) {
|
||||
setTimeout(() => hockeyLoadPreparedTitlesWorkspace({ force: true }).catch(() => {}), 0);
|
||||
}
|
||||
} else {
|
||||
state.config.components.filter((component) => !component.hidden && component.props?.runtimeHidden !== true && state.runtimeVisibility[component.action_id] !== false && isEffectivelyOnActiveTab(component)).sort((a, b) => Number(a.z) - Number(b.z)).forEach((component) => {
|
||||
const wrapper = document.createElement("div"); wrapper.className = "runtime-component"; Object.assign(wrapper.style, { left: `${component.x}px`, top: `${component.y}px`, width: `${component.w}px`, height: `${component.h}px`, zIndex: String(component.z) }); wrapper.appendChild(renderComponent(component, true)); el.runtimeStage.appendChild(wrapper);
|
||||
});
|
||||
renderHockeyRuntimeSideWindows();
|
||||
renderHockeyQuickCommandDock();
|
||||
}
|
||||
Object.entries(shootoutRosterScroll).forEach(([side, top]) => {
|
||||
const list = el.runtimeStage.querySelector(`.hso-roster.side-${side} .hso-player-list`);
|
||||
if (list) list.scrollTop = top;
|
||||
|
||||
@@ -7223,6 +7223,15 @@ body.hockey-navigation-open .runtime-viewport.has-hockey-pbp { gap: 14px !import
|
||||
.hpd-team-penalty-card.is-preview-selected {
|
||||
outline: 2px solid rgba(255, 255, 255, .72);
|
||||
outline-offset: -2px;
|
||||
box-shadow: 0 0 0 3px rgba(255,255,255,.06);
|
||||
}
|
||||
.hpd-team-penalty-card.team-home.is-preview-selected {
|
||||
outline-color: color-mix(in srgb,var(--hpd-home) 82%,#fff);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb,var(--hpd-home) 18%,transparent);
|
||||
}
|
||||
.hpd-team-penalty-card.team-away.is-preview-selected {
|
||||
outline-color: color-mix(in srgb,var(--hpd-away) 82%,#fff);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb,var(--hpd-away) 18%,transparent);
|
||||
}
|
||||
.hpd-team-card-identifiers {
|
||||
display: grid;
|
||||
@@ -7433,3 +7442,242 @@ body.hockey-navigation-open .runtime-viewport.has-hockey-pbp { gap: 14px !import
|
||||
user-select:none;
|
||||
}
|
||||
.hockey-player-select-slot:hover .hockey-player-select-drag { color:#9ab2c6; }
|
||||
|
||||
/* BUILD84 — compact, single-line player-block editor. */
|
||||
.player-selection-editor.prematch-editor-section .player-selection-editor-list { gap:4px; }
|
||||
.player-selection-editor-row {
|
||||
grid-template-columns:24px 66px minmax(150px,1fr) minmax(135px,.9fr) 52px minmax(135px,.9fr) 84px 82px minmax(150px,1fr) 56px 30px;
|
||||
gap:5px;
|
||||
align-items:center;
|
||||
min-height:48px;
|
||||
padding:4px 5px;
|
||||
border-radius:8px;
|
||||
}
|
||||
.player-selection-editor-row label { gap:2px; font-size:6.5px; line-height:1.05; }
|
||||
.player-selection-editor-row input,
|
||||
.player-selection-editor-row select { height:26px; padding-top:3px; padding-bottom:3px; font-size:9px; }
|
||||
.player-selection-editor-row .shortcut-inline-check {
|
||||
align-self:center;
|
||||
justify-self:start;
|
||||
gap:4px;
|
||||
white-space:nowrap;
|
||||
font-size:7px;
|
||||
}
|
||||
.player-selection-editor-row .shortcut-inline-check input { width:16px; height:16px; padding:0; }
|
||||
.player-selection-editor-description { min-width:120px; }
|
||||
.player-selection-editor-order { align-items:center; }
|
||||
.player-selection-editor-order .mini-btn,
|
||||
.player-selection-editor-row > .icon-btn { width:26px; min-width:26px; height:26px; padding:0; }
|
||||
@media (max-width:1450px) {
|
||||
.player-selection-editor-row {
|
||||
grid-template-columns:24px 64px minmax(130px,1fr) minmax(120px,.9fr) 50px minmax(120px,.9fr) 80px 78px minmax(130px,1fr) 54px 28px;
|
||||
}
|
||||
}
|
||||
|
||||
/* BUILD84 — prepared vMix title workspace + player block handoff. */
|
||||
.hockey-player-select-footer {
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
gap:6px;
|
||||
margin-top:7px;
|
||||
}
|
||||
.hockey-player-select-prepared {
|
||||
padding:5px 8px;
|
||||
color:#071a1d;
|
||||
border:1px solid #54e5c4;
|
||||
border-radius:7px;
|
||||
background:#48dfbd;
|
||||
font-size:7px;
|
||||
font-weight:950;
|
||||
cursor:pointer;
|
||||
}
|
||||
.hockey-player-select-prepared:hover { filter:brightness(1.08); }
|
||||
.hockey-player-select-footer .hockey-player-select-clear { margin-left:auto; }
|
||||
|
||||
.hockey-prepared-workspace {
|
||||
position:absolute;
|
||||
inset:0;
|
||||
display:grid;
|
||||
grid-template-rows:auto auto minmax(0,1fr);
|
||||
gap:9px;
|
||||
padding:14px;
|
||||
box-sizing:border-box;
|
||||
color:#dcecff;
|
||||
background:linear-gradient(180deg,#0a1522 0%,#09131f 100%);
|
||||
overflow:hidden;
|
||||
}
|
||||
.hockey-prepared-head,
|
||||
.hockey-prepared-context,
|
||||
.hockey-prepared-source,
|
||||
.hockey-prepared-savebar,
|
||||
.hockey-prepared-saved-head {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
}
|
||||
.hockey-prepared-head {
|
||||
min-height:52px;
|
||||
padding:0 12px;
|
||||
border:1px solid #263f56;
|
||||
border-radius:11px;
|
||||
background:#0c1b2a;
|
||||
}
|
||||
.hockey-prepared-head > div,
|
||||
.hockey-prepared-context > div,
|
||||
.hockey-prepared-source > div { min-width:0; display:grid; gap:2px; }
|
||||
.hockey-prepared-head span,
|
||||
.hockey-prepared-context span,
|
||||
.hockey-prepared-source span,
|
||||
.hockey-prepared-saved-head span {
|
||||
color:#56e3c1;
|
||||
font-size:7px;
|
||||
font-weight:950;
|
||||
letter-spacing:.08em;
|
||||
}
|
||||
.hockey-prepared-head strong { font-size:16px; line-height:1; }
|
||||
.hockey-prepared-head small,
|
||||
.hockey-prepared-context small,
|
||||
.hockey-prepared-source small { color:#7891a8; font-size:8px; }
|
||||
.hockey-prepared-head button,
|
||||
.hockey-prepared-context button,
|
||||
.hockey-prepared-source button,
|
||||
.hockey-prepared-savebar button,
|
||||
.hockey-prepared-saved article button {
|
||||
min-height:29px;
|
||||
padding:0 10px;
|
||||
border:1px solid #31516a;
|
||||
border-radius:7px;
|
||||
color:#cfe3f3;
|
||||
background:#10263a;
|
||||
font-size:8px;
|
||||
font-weight:900;
|
||||
cursor:pointer;
|
||||
}
|
||||
.hockey-prepared-head button:hover,
|
||||
.hockey-prepared-source button:hover,
|
||||
.hockey-prepared-saved article button:hover { border-color:#54dcbf; color:#fff; }
|
||||
.hockey-prepared-head button:disabled,
|
||||
.hockey-prepared-source button:disabled,
|
||||
.hockey-prepared-savebar button:disabled { opacity:.5; cursor:default; }
|
||||
.hockey-prepared-context {
|
||||
min-height:42px;
|
||||
padding:7px 10px;
|
||||
border:1px solid #31586a;
|
||||
border-radius:10px;
|
||||
background:#0d2431;
|
||||
}
|
||||
.hockey-prepared-context code { color:#86dfca; }
|
||||
.hockey-prepared-context > button { width:29px; padding:0; color:#8ba3b8; }
|
||||
.hockey-prepared-layout {
|
||||
min-height:0;
|
||||
display:grid;
|
||||
grid-template-columns:310px minmax(0,1fr) 310px;
|
||||
gap:9px;
|
||||
}
|
||||
.hockey-prepared-inputs,
|
||||
.hockey-prepared-editor,
|
||||
.hockey-prepared-saved {
|
||||
min-width:0;
|
||||
min-height:0;
|
||||
border:1px solid #263f56;
|
||||
border-radius:11px;
|
||||
background:#0b1927;
|
||||
overflow:hidden;
|
||||
}
|
||||
.hockey-prepared-inputs { display:grid; grid-template-rows:auto auto minmax(0,1fr); padding:9px; gap:7px; }
|
||||
.hockey-prepared-search { display:grid; gap:4px; color:#7f98af; font-size:7px; font-weight:900; }
|
||||
.hockey-prepared-search input,
|
||||
.hockey-prepared-name input,
|
||||
.hockey-prepared-field input[type="text"] {
|
||||
width:100%;
|
||||
min-width:0;
|
||||
height:30px;
|
||||
box-sizing:border-box;
|
||||
padding:0 9px;
|
||||
border:1px solid #2b465e;
|
||||
border-radius:7px;
|
||||
outline:0;
|
||||
color:#e5f2ff;
|
||||
background:#0c2133;
|
||||
font-size:9px;
|
||||
font-weight:700;
|
||||
font-family:inherit;
|
||||
}
|
||||
.hockey-prepared-search input:focus,
|
||||
.hockey-prepared-name input:focus,
|
||||
.hockey-prepared-field input[type="text"]:focus { border-color:#4ed9bd; box-shadow:0 0 0 1px rgba(78,217,189,.18); }
|
||||
.hockey-prepared-input-list,
|
||||
.hockey-prepared-saved-list { min-height:0; overflow:auto; display:grid; align-content:start; gap:4px; padding-right:2px; }
|
||||
.hockey-prepared-input-list > button,
|
||||
.hockey-prepared-recommended button {
|
||||
width:100%;
|
||||
display:grid;
|
||||
grid-template-columns:42px minmax(0,1fr) auto;
|
||||
align-items:center;
|
||||
gap:7px;
|
||||
min-height:37px;
|
||||
padding:5px 7px;
|
||||
text-align:left;
|
||||
border:1px solid #243d53;
|
||||
border-radius:7px;
|
||||
color:#a8bdd0;
|
||||
background:#0d2031;
|
||||
cursor:pointer;
|
||||
}
|
||||
.hockey-prepared-input-list > button:hover,
|
||||
.hockey-prepared-input-list > button.active,
|
||||
.hockey-prepared-recommended button:hover { border-color:#3e826f; background:#102b3c; }
|
||||
.hockey-prepared-input-list > button.active { box-shadow:inset 3px 0 0 #4dddbc; }
|
||||
.hockey-prepared-input-list b,
|
||||
.hockey-prepared-recommended b { color:#61e0c2; font:900 8px "Roboto Mono",Consolas,monospace; }
|
||||
.hockey-prepared-input-list span,
|
||||
.hockey-prepared-recommended span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:9px; font-weight:900; color:#e0effb; }
|
||||
.hockey-prepared-input-list small,
|
||||
.hockey-prepared-recommended small { color:#6f879d; font-size:7px; white-space:nowrap; }
|
||||
.hockey-prepared-recommended { display:grid; gap:4px; padding:6px; border:1px solid #2d554d; border-radius:8px; background:#0d2728; }
|
||||
.hockey-prepared-recommended > span { color:#70bea9; font-size:7px; font-weight:900; }
|
||||
.hockey-prepared-editor { display:grid; grid-template-rows:auto auto minmax(0,1fr) auto; gap:8px; padding:9px; }
|
||||
.hockey-prepared-source { padding:7px 8px; border:1px solid #29475e; border-radius:8px; background:#0d2031; }
|
||||
.hockey-prepared-source strong { font-size:10px; }
|
||||
.hockey-prepared-name { display:grid; gap:4px; color:#8099ae; font-size:7px; font-weight:900; }
|
||||
.hockey-prepared-fields { min-height:0; overflow:auto; display:grid; align-content:start; gap:4px; padding-right:3px; }
|
||||
.hockey-prepared-field {
|
||||
min-height:42px;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(160px,.8fr) minmax(220px,1.2fr);
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
padding:5px 7px;
|
||||
border:1px solid #223b51;
|
||||
border-radius:7px;
|
||||
background:#0c1d2d;
|
||||
}
|
||||
.hockey-prepared-field > span { min-width:0; display:flex; align-items:baseline; gap:7px; }
|
||||
.hockey-prepared-field > span b { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font:850 8px "Roboto Mono",Consolas,monospace; color:#cfe0ee; }
|
||||
.hockey-prepared-field > span small { color:#52768e; font-size:6px; text-transform:uppercase; }
|
||||
.hockey-prepared-field > div { min-width:0; display:flex; align-items:center; gap:5px; }
|
||||
.hockey-prepared-field input[type="color"] { width:32px; min-width:32px; height:30px; padding:2px; border:1px solid #2b465e; border-radius:6px; background:#0c2133; }
|
||||
.hockey-prepared-savebar { padding-top:7px; border-top:1px solid #21394d; }
|
||||
.hockey-prepared-savebar small { max-width:520px; color:#70899f; font-size:7px; line-height:1.35; }
|
||||
.hockey-prepared-savebar button { min-width:155px; color:#071a1d; border-color:#54e5c4; background:#48dfbd; }
|
||||
.hockey-prepared-saved { display:grid; grid-template-rows:auto minmax(0,1fr); padding:9px; gap:7px; }
|
||||
.hockey-prepared-saved-head { min-height:28px; padding:0 2px; }
|
||||
.hockey-prepared-saved-head b { min-width:22px; height:18px; display:grid; place-items:center; border-radius:9px; color:#9ec1d8; background:#1b3850; font:900 8px "Roboto Mono",Consolas,monospace; }
|
||||
.hockey-prepared-saved article { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:6px; align-items:center; padding:7px; border:1px solid #243e54; border-radius:8px; background:#0d2031; }
|
||||
.hockey-prepared-saved article > div:first-child { min-width:0; display:grid; gap:2px; }
|
||||
.hockey-prepared-saved article span { color:#5c8ca6; font-size:6px; font-weight:950; text-transform:uppercase; }
|
||||
.hockey-prepared-saved article strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:9px; color:#e1eef8; }
|
||||
.hockey-prepared-saved article small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#71899e; font-size:7px; }
|
||||
.hockey-prepared-saved article > div:last-child { display:flex; gap:4px; }
|
||||
.hockey-prepared-saved article button { min-height:25px; padding:0 7px; font-size:7px; }
|
||||
.hockey-prepared-saved article button.danger { width:25px; min-width:25px; padding:0; color:#ff7d91; border-color:#593646; }
|
||||
.hockey-prepared-empty,
|
||||
.hockey-prepared-editor-empty { display:grid; place-items:center; align-content:center; gap:5px; min-height:90px; padding:16px; text-align:center; color:#637e94; font-size:8px; }
|
||||
.hockey-prepared-editor-empty { height:100%; }
|
||||
.hockey-prepared-editor-empty b { color:#b9cfdf; font-size:13px; }
|
||||
@media (max-width:1100px) {
|
||||
.hockey-prepared-layout { grid-template-columns:250px minmax(0,1fr) 250px; }
|
||||
.hockey-prepared-field { grid-template-columns:1fr; gap:4px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user