build 118 - оверлеи

This commit is contained in:
2026-08-26 13:17:08 +03:00
parent 2c9b0d4fcc
commit c5b25c23e2
8 changed files with 31383 additions and 36 deletions

11
agent/README_UPDATE.txt Normal file
View File

@@ -0,0 +1,11 @@
Hockey vMix Agent 1.5.1
Changes:
- vMix 29 tally now reads Overlay channels 1 through 8.
- Existing agent_config.json is NOT included. Keep your current config next to agent.exe.
Update:
1. Close Agent.
2. Replace only agent.exe.
3. Keep the existing agent_config.json.
4. Start Agent again.

Binary file not shown.

31306
agent/agent.log.1 Normal file

File diff suppressed because it is too large Load Diff

2
app.py
View File

@@ -29,7 +29,7 @@ from ui_builder import install_ui_builder
from khl_site.khl_data_center import APP as khl_site_app from khl_site.khl_data_center import APP as khl_site_app
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
BUILD_VERSION = "2026.08.26.2" BUILD_VERSION = "2026.08.26.5"
# compatibility: BUILD_VERSION = "2026.08.26.1" # compatibility: BUILD_VERSION = "2026.08.26.1"
# compatibility: BUILD_VERSION = "2026.08.25.1" # compatibility: BUILD_VERSION = "2026.08.25.1"
# compatibility: BUILD_VERSION = "2026.08.24.15" # compatibility: BUILD_VERSION = "2026.08.24.15"

View File

@@ -277,7 +277,7 @@ class VmixAgentHub:
text = re.sub(r"\s+", "", str(function_name or "")) text = re.sub(r"\s+", "", str(function_name or ""))
if text.lower() == "overlayinputalloff": if text.lower() == "overlayinputalloff":
return ("all", "off") return ("all", "off")
match = re.search(r"OverlayInput([1-4])(In|Out|Off)?", text, flags=re.IGNORECASE) match = re.search(r"OverlayInput([1-8])(In|Out|Off)?", text, flags=re.IGNORECASE)
if not match: if not match:
return None return None
raw = str(match.group(2) or "toggle").lower() raw = str(match.group(2) or "toggle").lower()
@@ -333,7 +333,7 @@ class VmixAgentHub:
def _runtime_overlay_payload(self, device_id: str) -> dict[str, Any]: def _runtime_overlay_payload(self, device_id: str) -> dict[str, Any]:
source = self._runtime_overlay_state.get(device_id, {}) source = self._runtime_overlay_state.get(device_id, {})
overlays = {str(layer): dict(value) for layer, value in source.items() if str(layer) in {"1", "2", "3", "4"}} overlays = {str(layer): dict(value) for layer, value in source.items() if str(layer) in {"1", "2", "3", "4", "5", "6", "7", "8"}}
authoritative = bool(getattr(self, "_runtime_overlay_authoritative", {}).get(device_id, False)) authoritative = bool(getattr(self, "_runtime_overlay_authoritative", {}).get(device_id, False))
observed = str(getattr(self, "_runtime_overlay_observed_at", {}).get(device_id) or "") observed = str(getattr(self, "_runtime_overlay_observed_at", {}).get(device_id) or "")
return { return {
@@ -697,7 +697,7 @@ class VmixAgentHub:
async def receive_overlay_state(self, device_id: str, message: dict[str, Any]) -> None: async def receive_overlay_state(self, device_id: str, message: dict[str, Any]) -> None:
raw_overlays = message.get("overlays") if isinstance(message.get("overlays"), dict) else {} raw_overlays = message.get("overlays") if isinstance(message.get("overlays"), dict) else {}
next_state: dict[str, dict[str, str]] = {} next_state: dict[str, dict[str, str]] = {}
for layer in ("1", "2", "3", "4"): for layer in ("1", "2", "3", "4", "5", "6", "7", "8"):
raw = raw_overlays.get(layer) raw = raw_overlays.get(layer)
if raw is None: if raw is None:
raw = raw_overlays.get(int(layer)) raw = raw_overlays.get(int(layer))

View File

@@ -340,7 +340,7 @@ class HockeySettingsStore:
source = source_team_states.get(state_key) source = source_team_states.get(state_key)
source = source if isinstance(source, dict) else {} source = source if isinstance(source, dict) else {}
overlay = str(source.get("overlay") or defaults["overlay"]).strip() overlay = str(source.get("overlay") or defaults["overlay"]).strip()
if overlay not in {"1", "2", "3", "4"}: if overlay not in {"1", "2", "3", "4", "5", "6", "7", "8"}:
overlay = str(defaults["overlay"]) overlay = str(defaults["overlay"])
normalised_team_states[state_key] = { normalised_team_states[state_key] = {
"ru": str(source.get("ru", defaults["ru"]) or "").strip()[:48], "ru": str(source.get("ru", defaults["ru"]) or "").strip()[:48],

View File

@@ -653,14 +653,14 @@
function scoreboardTeamStatesMarkup(settings) { function scoreboardTeamStatesMarkup(settings) {
return SCOREBOARD_TEAM_STATE_ROWS.map((row) => { return SCOREBOARD_TEAM_STATE_ROWS.map((row) => {
const value = scoreboardTeamStateValue(settings, row.key); const value = scoreboardTeamStateValue(settings, row.key);
const overlay = ["1", "2", "3", "4"].includes(String(value.overlay || "")) ? String(value.overlay) : (row.key.includes("empty_net") ? "3" : "2"); const overlay = ["1", "2", "3", "4", "5", "6", "7", "8"].includes(String(value.overlay || "")) ? String(value.overlay) : (row.key.includes("empty_net") ? "3" : "2");
return ` return `
<div class="hockey-team-state-setting-row"> <div class="hockey-team-state-setting-row">
<div class="hockey-team-state-setting-kind"><b>${escapeHtml(row.side)}</b><strong>${escapeHtml(row.title)}</strong></div> <div class="hockey-team-state-setting-kind"><b>${escapeHtml(row.side)}</b><strong>${escapeHtml(row.title)}</strong></div>
<label><span>Русская надпись</span><input name="team_state_${row.key}_ru" maxlength="48" value="${escapeHtml(value.ru || row.defaultRu)}"></label> <label><span>Русская надпись</span><input name="team_state_${row.key}_ru" maxlength="48" value="${escapeHtml(value.ru || row.defaultRu)}"></label>
<label><span>English label</span><input name="team_state_${row.key}_en" maxlength="48" value="${escapeHtml(value.en || row.defaultEn)}"></label> <label><span>English label</span><input name="team_state_${row.key}_en" maxlength="48" value="${escapeHtml(value.en || row.defaultEn)}"></label>
<label><span>vMix Input</span><select name="team_state_${row.key}_input">${scoreboardInputOptions(value.input, value.input_title)}</select></label> <label><span>vMix Input</span><select name="team_state_${row.key}_input">${scoreboardInputOptions(value.input, value.input_title)}</select></label>
<label><span>Overlay</span><select name="team_state_${row.key}_overlay">${[1,2,3,4].map((item) => `<option value="${item}" ${String(item) === overlay ? "selected" : ""}>Overlay ${item}</option>`).join("")}</select></label> <label><span>Overlay</span><select name="team_state_${row.key}_overlay">${[1,2,3,4,5,6,7,8].map((item) => `<option value="${item}" ${String(item) === overlay ? "selected" : ""}>Overlay ${item}</option>`).join("")}</select></label>
<label class="hockey-rules-switch compact"><input name="team_state_${row.key}_enabled" type="checkbox" ${value.enabled !== false ? "checked" : ""}><i></i><span><strong>Использовать</strong></span></label> <label class="hockey-rules-switch compact"><input name="team_state_${row.key}_enabled" type="checkbox" ${value.enabled !== false ? "checked" : ""}><i></i><span><strong>Использовать</strong></span></label>
</div>`; </div>`;
}).join(""); }).join("");

View File

@@ -2357,7 +2357,7 @@ function startCustomTooltips() {
id: String(target?.id || `penalty-target-${index + 1}-${Math.random().toString(36).slice(2, 7)}`), id: String(target?.id || `penalty-target-${index + 1}-${Math.random().toString(36).slice(2, 7)}`),
input: String(target?.input || ""), input: String(target?.input || ""),
selected_name: String(target?.selected_name || target?.selectedName || ""), selected_name: String(target?.selected_name || target?.selectedName || ""),
overlay: ["1", "2", "3", "4"].includes(String(target?.overlay || "")) ? String(target.overlay) : "2", overlay: ["1", "2", "3", "4", "5", "6", "7", "8"].includes(String(target?.overlay || "")) ? String(target.overlay) : "2",
auto_hide_on_finish: target?.auto_hide_on_finish !== false, auto_hide_on_finish: target?.auto_hide_on_finish !== false,
})); }));
} }
@@ -2370,7 +2370,7 @@ function startCustomTooltips() {
enabled: action?.enabled !== false, enabled: action?.enabled !== false,
source: allowedSources.has(String(action?.source || "")) ? String(action.source) : "game", source: allowedSources.has(String(action?.source || "")) ? String(action.source) : "game",
input: String(action?.input || ""), input: String(action?.input || ""),
overlay: ["1", "2", "3", "4"].includes(String(action?.overlay || "")) ? String(action.overlay) : "1", overlay: ["1", "2", "3", "4", "5", "6", "7", "8"].includes(String(action?.overlay || "")) ? String(action.overlay) : "1",
duration_ms: clamp(Number(action?.duration_ms) || 3000, 100, 120000), duration_ms: clamp(Number(action?.duration_ms) || 3000, 100, 120000),
only_when_side_clear: action?.only_when_side_clear !== false, only_when_side_clear: action?.only_when_side_clear !== false,
})); }));
@@ -2537,6 +2537,15 @@ function startCustomTooltips() {
prevent_default: sequence.prevent_default !== false, prevent_default: sequence.prevent_default !== false,
allow_in_inputs: Boolean(sequence.allow_in_inputs), allow_in_inputs: Boolean(sequence.allow_in_inputs),
toggle_all_overlays_on_repeat: Boolean(sequence.toggle_all_overlays_on_repeat), toggle_all_overlays_on_repeat: Boolean(sequence.toggle_all_overlays_on_repeat),
// BUILD117: keep the legacy toggle flag for config compatibility, but let the
// operator choose exactly which vMix overlay channels are taken OUT on repeat.
// Existing scoreboard configs had no list, so the safe default remains 1/2/3.
// BUILD118 exposes vMix 29 Overlay channels 1..8 while leaving 4..8 untouched by default.
repeat_overlay_outs: (() => {
const raw = Array.isArray(sequence.repeat_overlay_outs) ? sequence.repeat_overlay_outs : [1, 2, 3];
const normalized = [...new Set(raw.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value >= 1 && value <= 8))];
return normalized.length ? normalized : [1, 2, 3];
})(),
sync_hockey_team_states: sequence.sync_hockey_team_states === undefined sync_hockey_team_states: sequence.sync_hockey_team_states === undefined
? Boolean(sequence.toggle_all_overlays_on_repeat) ? Boolean(sequence.toggle_all_overlays_on_repeat)
: Boolean(sequence.sync_hockey_team_states), : Boolean(sequence.sync_hockey_team_states),
@@ -4322,8 +4331,8 @@ function startCustomTooltips() {
// executes the command, but the strict BUILD58 parser did not recognise it, // executes the command, but the strict BUILD58 parser did not recognise it,
// so the quick-panel button never received the on-air state. // so the quick-panel button never received the on-air state.
const normalized = String(functionName || "").trim().replace(/\s+/g, ""); const normalized = String(functionName || "").trim().replace(/\s+/g, "");
const match = normalized.match(/(?:^|[^a-z0-9])OverlayInput([1-4])(In|Out|Off)?(?=$|[^a-z])/i) const match = normalized.match(/(?:^|[^a-z0-9])OverlayInput([1-8])(In|Out|Off)?(?=$|[^a-z])/i)
|| normalized.match(/^OverlayInput([1-4])(In|Out|Off)?/i); || normalized.match(/^OverlayInput([1-8])(In|Out|Off)?/i);
if (!match) return null; if (!match) return null;
const raw = String(match[2] || "toggle").toLowerCase(); const raw = String(match[2] || "toggle").toLowerCase();
const action = raw === "in" ? "In" : raw === "out" ? "Out" : raw === "off" ? "Off" : "toggle"; const action = raw === "in" ? "In" : raw === "out" ? "Out" : raw === "off" ? "Off" : "toggle";
@@ -4467,7 +4476,7 @@ function startCustomTooltips() {
// original PreviewInput GUID can now be matched to the real on-air Input. // original PreviewInput GUID can now be matched to the real on-air Input.
const previewTargets = shortcutSequencePreviewTargets(sequenceOrId); const previewTargets = shortcutSequencePreviewTargets(sequenceOrId);
if (!previewTargets.length) return false; if (!previewTargets.length) return false;
for (const layer of ["1", "2", "3", "4"]) { for (const layer of ["1", "2", "3", "4", "5", "6", "7", "8"]) {
const current = state.vmixOverlayRuntime.get(layer); const current = state.vmixOverlayRuntime.get(layer);
if (previewTargets.some((input) => runtimeOverlayMatchesInput(current, input))) return true; if (previewTargets.some((input) => runtimeOverlayMatchesInput(current, input))) return true;
} }
@@ -4523,7 +4532,7 @@ function startCustomTooltips() {
const previousServerIds = new Set(state.quickPanelServerOnAirSequences); const previousServerIds = new Set(state.quickPanelServerOnAirSequences);
const nextServerIds = new Set(); const nextServerIds = new Set();
for (const layer of ["1", "2", "3", "4"]) { for (const layer of ["1", "2", "3", "4", "5", "6", "7", "8"]) {
const raw = overlays[layer]; const raw = overlays[layer];
if (raw && typeof raw === "object") { if (raw && typeof raw === "object") {
const entry = { const entry = {
@@ -5203,7 +5212,7 @@ function startCustomTooltips() {
if (entry && target.input && target.selected_name) { if (entry && target.input && target.selected_name) {
const eventKey = penaltyMirrorKey(entry.component, entry.event); const eventKey = penaltyMirrorKey(entry.component, entry.event);
const sourceSide = String(entry.side || entry.event?.side || entry.event?.player?.side || side); const sourceSide = String(entry.side || entry.event?.side || entry.event?.player?.side || side);
const overlay = ["1", "2", "3", "4"].includes(String(target.overlay || "")) ? String(target.overlay) : "2"; const overlay = ["1", "2", "3", "4", "5", "6", "7", "8"].includes(String(target.overlay || "")) ? String(target.overlay) : "2";
const assignmentChanged = !previous const assignmentChanged = !previous
|| previous.eventKey !== eventKey || previous.eventKey !== eventKey
|| String(previous.input || "") !== String(target.input) || String(previous.input || "") !== String(target.input)
@@ -5261,7 +5270,7 @@ function startCustomTooltips() {
&& String(previous.overlay || overlay) === overlay; && String(previous.overlay || overlay) === overlay;
if (!targetWasVisible) { if (!targetWasVisible) {
if (previous?.input && (String(previous.input) !== String(target.input) || String(previous.overlay || overlay) !== overlay)) { if (previous?.input && (String(previous.input) !== String(target.input) || String(previous.overlay || overlay) !== overlay)) {
const previousOverlay = ["1", "2", "3", "4"].includes(String(previous.overlay || "")) ? String(previous.overlay) : overlay; const previousOverlay = ["1", "2", "3", "4", "5", "6", "7", "8"].includes(String(previous.overlay || "")) ? String(previous.overlay) : overlay;
outCommands.push({ Function: `OverlayInput${previousOverlay}Out`, Input: previous.input }); outCommands.push({ Function: `OverlayInput${previousOverlay}Out`, Input: previous.input });
} }
inCommands.push({ Function: `OverlayInput${overlay}In`, Input: target.input }); inCommands.push({ Function: `OverlayInput${overlay}In`, Input: target.input });
@@ -5272,7 +5281,7 @@ function startCustomTooltips() {
stopCommands.push({ Function: "StopCountdown", Input: previous.input, SelectedName: previous.selectedName || target.selected_name }); stopCommands.push({ Function: "StopCountdown", Input: previous.input, SelectedName: previous.selectedName || target.selected_name });
} }
if (previous && hideUnused && target.auto_hide_on_finish !== false && target.input) { if (previous && hideUnused && target.auto_hide_on_finish !== false && target.input) {
const overlay = ["1", "2", "3", "4"].includes(String(target.overlay || "")) ? String(target.overlay) : "2"; const overlay = ["1", "2", "3", "4", "5", "6", "7", "8"].includes(String(target.overlay || "")) ? String(target.overlay) : "2";
outCommands.push({ Function: `OverlayInput${overlay}Out`, Input: target.input }); outCommands.push({ Function: `OverlayInput${overlay}Out`, Input: target.input });
} }
state.vmixPenaltyTargetAssignments.delete(assignmentKey); state.vmixPenaltyTargetAssignments.delete(assignmentKey);
@@ -5285,7 +5294,7 @@ function startCustomTooltips() {
stopCommands.push({ Function: "StopCountdown", Input: previous.input, SelectedName: previous.selectedName || target.selected_name }); stopCommands.push({ Function: "StopCountdown", Input: previous.input, SelectedName: previous.selectedName || target.selected_name });
} }
if (previous && hideUnused && target.auto_hide_on_finish !== false && target.input) { if (previous && hideUnused && target.auto_hide_on_finish !== false && target.input) {
const overlay = ["1", "2", "3", "4"].includes(String(target.overlay || "")) ? String(target.overlay) : "2"; const overlay = ["1", "2", "3", "4", "5", "6", "7", "8"].includes(String(target.overlay || "")) ? String(target.overlay) : "2";
outCommands.push({ Function: `OverlayInput${overlay}Out`, Input: target.input }); outCommands.push({ Function: `OverlayInput${overlay}Out`, Input: target.input });
} }
state.vmixPenaltyTargetAssignments.delete(assignmentKey); state.vmixPenaltyTargetAssignments.delete(assignmentKey);
@@ -5314,7 +5323,7 @@ function startCustomTooltips() {
async function runVmixTimedFinishAction(action, meta = {}) { async function runVmixTimedFinishAction(action, meta = {}) {
const input = String(action?.input || "").trim(); const input = String(action?.input || "").trim();
if (!input || action?.enabled === false) return false; if (!input || action?.enabled === false) return false;
const overlay = ["1", "2", "3", "4"].includes(String(action.overlay || "")) ? String(action.overlay) : "1"; const overlay = ["1", "2", "3", "4", "5", "6", "7", "8"].includes(String(action.overlay || "")) ? String(action.overlay) : "1";
const durationMs = clamp(Number(action.duration_ms) || 3000, 100, 120000); const durationMs = clamp(Number(action.duration_ms) || 3000, 100, 120000);
const timerKey = `${action.id || input}:${overlay}:${input}`; const timerKey = `${action.id || input}:${overlay}:${input}`;
const previousTimer = state.vmixFinishOverlayTimers.get(timerKey); const previousTimer = state.vmixFinishOverlayTimers.get(timerKey);
@@ -5542,17 +5551,23 @@ function startCustomTooltips() {
const execution = { sequence_id: String(sequence.id || ""), sequence_name: String(sequence.name || ""), button_id: String(meta.button_id || "") }; const execution = { sequence_id: String(sequence.id || ""), sequence_name: String(sequence.name || ""), button_id: String(meta.button_id || "") };
window.dispatchEvent(new CustomEvent("ui-builder:shortcut-sequence-start", { detail: { sequence: clone(sequence), meta } })); window.dispatchEvent(new CustomEvent("ui-builder:shortcut-sequence-start", { detail: { sequence: clone(sequence), meta } }));
try { try {
if (sequence.toggle_all_overlays_on_repeat && state.shortcutSequenceOverlayState.get(sequence.id)) { // BUILD116: repeat-toggle must follow the same ON AIR decision as the UI.
// IMPORTANT: do not use OverlayInputAllOff here. vMix documents AllOff as an // With authoritative Agent/vMix tally, a scoreboard can be truly live even when
// immediate cut, which bypasses the configured overlay/GT transition-out. // the legacy local latch is false (for example after reload or external vMix IN).
// Explicit OverlayInputNOut commands preserve the exit animation while still // `wasOnAir` already resolves to the real tally when available, and falls back
// clearing every overlay channel controlled by vMix. // to the legacy latch when tally is unavailable.
await sendRuntimeVmixSequence([ if (sequence.toggle_all_overlays_on_repeat && wasOnAir) {
{ Function: "OverlayInput1Out" }, // BUILD118: repeat OUT is configurable across vMix Overlay channels 1..8. Do not use
{ Function: "OverlayInput2Out" }, // OverlayInputAllOff: explicit OverlayInputNOut preserves each configured
{ Function: "OverlayInput3Out" }, // transition-out and avoids touching unrelated channels that are not selected.
{ Function: "OverlayInput4Out" }, const repeatOverlayOuts = (Array.isArray(sequence.repeat_overlay_outs) ? sequence.repeat_overlay_outs : [1, 2, 3])
], execution); .map((value) => Number(value))
.filter((value, index, values) => Number.isInteger(value) && value >= 1 && value <= 8 && values.indexOf(value) === index);
const selectedOverlayOuts = repeatOverlayOuts.length ? repeatOverlayOuts : [1, 2, 3];
await sendRuntimeVmixSequence(
selectedOverlayOuts.map((number) => ({ Function: `OverlayInput${number}Out` })),
execution,
);
if (sequence.sync_hockey_team_states) hockeyClearTeamStateOverlayTracking(); if (sequence.sync_hockey_team_states) hockeyClearTeamStateOverlayTracking();
state.shortcutSequenceOverlayState.set(sequence.id, false); state.shortcutSequenceOverlayState.set(sequence.id, false);
state.quickPanelOnAirSequences.delete(String(sequence.id || "")); state.quickPanelOnAirSequences.delete(String(sequence.id || ""));
@@ -11918,7 +11933,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
label: String(value.label || value[language] || fallback), label: String(value.label || value[language] || fallback),
input: String(value.input || "").trim(), input: String(value.input || "").trim(),
input_title: String(value.input_title || "").trim(), input_title: String(value.input_title || "").trim(),
overlay: ["1", "2", "3", "4"].includes(String(value.overlay || "")) ? String(value.overlay) : (key.includes("empty_net") ? "3" : "2"), overlay: ["1", "2", "3", "4", "5", "6", "7", "8"].includes(String(value.overlay || "")) ? String(value.overlay) : (key.includes("empty_net") ? "3" : "2"),
enabled: value.enabled !== false, enabled: value.enabled !== false,
}; };
} }
@@ -13700,7 +13715,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
// Colour correction // Colour correction
"SetCCGainB","SetCCGainG","SetCCGainR","SetCCGainRGB","SetCCGainY","SetCCGammaB","SetCCGammaG","SetCCGammaR","SetCCGammaRGB","SetCCGammaY","SetCCHue","SetCCLiftB","SetCCLiftG","SetCCLiftR","SetCCLiftRGB","SetCCLiftY","SetCCSaturation", "SetCCGainB","SetCCGainG","SetCCGainR","SetCCGainRGB","SetCCGainY","SetCCGammaB","SetCCGammaG","SetCCGammaR","SetCCGammaRGB","SetCCGammaY","SetCCHue","SetCCLiftB","SetCCLiftG","SetCCLiftR","SetCCLiftRGB","SetCCLiftY","SetCCSaturation",
// Overlay // Overlay
"OverlayInput1","OverlayInput1In","OverlayInput1Out","OverlayInput2","OverlayInput2In","OverlayInput2Out","OverlayInput3","OverlayInput3In","OverlayInput3Out","OverlayInput4","OverlayInput4In","OverlayInput4Out", "OverlayInput1","OverlayInput1In","OverlayInput1Out","OverlayInput2","OverlayInput2In","OverlayInput2Out","OverlayInput3","OverlayInput3In","OverlayInput3Out","OverlayInput4","OverlayInput4In","OverlayInput4Out","OverlayInput5","OverlayInput5In","OverlayInput5Out","OverlayInput6","OverlayInput6In","OverlayInput6Out","OverlayInput7","OverlayInput7In","OverlayInput7Out","OverlayInput8","OverlayInput8In","OverlayInput8Out",
// Data sources / browser / scripts // Data sources / browser / scripts
"DataSourceAutoNext","DataSourceAutoNextOff","DataSourceAutoNextOn","DataSourceNextRow","DataSourcePreviousRow","DataSourceSelectRow","BrowserBack","BrowserForward","BrowserHome","BrowserNavigate","BrowserReload","BrowserSetURL","BrowserExecuteJavascript","ScriptStart","ScriptStop","ScriptStartDynamic", "DataSourceAutoNext","DataSourceAutoNextOff","DataSourceAutoNextOn","DataSourceNextRow","DataSourcePreviousRow","DataSourceSelectRow","BrowserBack","BrowserForward","BrowserHome","BrowserNavigate","BrowserReload","BrowserSetURL","BrowserExecuteJavascript","ScriptStart","ScriptStop","ScriptStartDynamic",
// NDI / OMT // NDI / OMT
@@ -13889,7 +13904,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
<span class="shortcut-target-index">${sideLabel} ${index + 1}</span> <span class="shortcut-target-index">${sideLabel} ${index + 1}</span>
<label>Input<select data-penalty-target-side="${side}" data-penalty-target-id="${escapeHtml(target.id)}" data-penalty-target-field="input">${vmixInputOptions(target.input)}</select></label> <label>Input<select data-penalty-target-side="${side}" data-penalty-target-id="${escapeHtml(target.id)}" data-penalty-target-field="input">${vmixInputOptions(target.input)}</select></label>
<label>Text / SelectedName<select data-penalty-target-side="${side}" data-penalty-target-id="${escapeHtml(target.id)}" data-penalty-target-field="selected_name">${vmixTextSelectedNameOptions(target.input, target.selected_name)}</select></label> <label>Text / SelectedName<select data-penalty-target-side="${side}" data-penalty-target-id="${escapeHtml(target.id)}" data-penalty-target-field="selected_name">${vmixTextSelectedNameOptions(target.input, target.selected_name)}</select></label>
<label>Overlay<select data-penalty-target-side="${side}" data-penalty-target-id="${escapeHtml(target.id)}" data-penalty-target-field="overlay">${triggerSelectOptions([["1","Overlay 1"],["2","Overlay 2"],["3","Overlay 3"],["4","Overlay 4"]], target.overlay)}</select></label> <label>Overlay<select data-penalty-target-side="${side}" data-penalty-target-id="${escapeHtml(target.id)}" data-penalty-target-field="overlay">${triggerSelectOptions([["1","Overlay 1"],["2","Overlay 2"],["3","Overlay 3"],["4","Overlay 4"],["5","Overlay 5"],["6","Overlay 6"],["7","Overlay 7"],["8","Overlay 8"]], target.overlay)}</select></label>
<label class="shortcut-inline-check"><input type="checkbox" data-penalty-target-side="${side}" data-penalty-target-id="${escapeHtml(target.id)}" data-penalty-target-field="auto_hide_on_finish" ${target.auto_hide_on_finish !== false ? "checked" : ""}> Снимать после последнего удаления</label> <label class="shortcut-inline-check"><input type="checkbox" data-penalty-target-side="${side}" data-penalty-target-id="${escapeHtml(target.id)}" data-penalty-target-field="auto_hide_on_finish" ${target.auto_hide_on_finish !== false ? "checked" : ""}> Снимать после последнего удаления</label>
<button type="button" class="icon-btn danger" data-penalty-target-delete="${escapeHtml(target.id)}" data-penalty-target-side="${side}" title="Удалить слот">×</button> <button type="button" class="icon-btn danger" data-penalty-target-delete="${escapeHtml(target.id)}" data-penalty-target-side="${side}" title="Удалить слот">×</button>
</div>`).join(""); </div>`).join("");
@@ -13911,7 +13926,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
["away_penalty","Удаление AWAY завершено"], ["away_penalty","Удаление AWAY завершено"],
], action.source)}</select></label> ], action.source)}</select></label>
<label>Показать Input<select data-finish-action-id="${escapeHtml(action.id)}" data-finish-action-field="input">${vmixInputOptions(action.input)}</select></label> <label>Показать Input<select data-finish-action-id="${escapeHtml(action.id)}" data-finish-action-field="input">${vmixInputOptions(action.input)}</select></label>
<label>Overlay<select data-finish-action-id="${escapeHtml(action.id)}" data-finish-action-field="overlay">${triggerSelectOptions([["1","Overlay 1"],["2","Overlay 2"],["3","Overlay 3"],["4","Overlay 4"]], action.overlay)}</select></label> <label>Overlay<select data-finish-action-id="${escapeHtml(action.id)}" data-finish-action-field="overlay">${triggerSelectOptions([["1","Overlay 1"],["2","Overlay 2"],["3","Overlay 3"],["4","Overlay 4"],["5","Overlay 5"],["6","Overlay 6"],["7","Overlay 7"],["8","Overlay 8"]], action.overlay)}</select></label>
<label>Показывать, мс<input type="number" min="100" max="120000" step="100" data-finish-action-id="${escapeHtml(action.id)}" data-finish-action-field="duration_ms" value="${Number(action.duration_ms) || 3000}"></label> <label>Показывать, мс<input type="number" min="100" max="120000" step="100" data-finish-action-id="${escapeHtml(action.id)}" data-finish-action-field="duration_ms" value="${Number(action.duration_ms) || 3000}"></label>
<label class="shortcut-inline-check"><input type="checkbox" data-finish-action-id="${escapeHtml(action.id)}" data-finish-action-field="only_when_side_clear" ${action.only_when_side_clear !== false ? "checked" : ""}> Только когда у команды больше нет удалений</label> <label class="shortcut-inline-check"><input type="checkbox" data-finish-action-id="${escapeHtml(action.id)}" data-finish-action-field="only_when_side_clear" ${action.only_when_side_clear !== false ? "checked" : ""}> Только когда у команды больше нет удалений</label>
<button type="button" class="icon-btn danger" data-finish-action-delete="${escapeHtml(action.id)}" title="Удалить действие">×</button> <button type="button" class="icon-btn danger" data-finish-action-delete="${escapeHtml(action.id)}" title="Удалить действие">×</button>
@@ -14221,7 +14236,11 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
<label><span>Где работает</span><select data-sequence-scope>${triggerSelectOptions([["runtime","Runtime и предпросмотр"],["all","Также в редакторе"]], sequence.scope)}</select></label> <label><span>Где работает</span><select data-sequence-scope>${triggerSelectOptions([["runtime","Runtime и предпросмотр"],["all","Также в редакторе"]], sequence.scope)}</select></label>
<label class="check-inline"><input type="checkbox" data-sequence-prevent ${sequence.prevent_default ? "checked" : ""}> блокировать действие браузера</label> <label class="check-inline"><input type="checkbox" data-sequence-prevent ${sequence.prevent_default ? "checked" : ""}> блокировать действие браузера</label>
<label class="check-inline"><input type="checkbox" data-sequence-inputs ${sequence.allow_in_inputs ? "checked" : ""}> во время ввода</label> <label class="check-inline"><input type="checkbox" data-sequence-inputs ${sequence.allow_in_inputs ? "checked" : ""}> во время ввода</label>
<label class="check-inline"><input type="checkbox" data-sequence-overlay-group ${sequence.toggle_all_overlays_on_repeat ? "checked" : ""}> повторное нажатие снимает все Overlay</label> <label class="check-inline"><input type="checkbox" data-sequence-overlay-group ${sequence.toggle_all_overlays_on_repeat ? "checked" : ""}> повторное нажатие снимает выбранные Overlay</label>
<span class="check-inline" data-sequence-overlay-outs-wrap>
снять:
${[1, 2, 3, 4, 5, 6, 7, 8].map((number) => `<label class="check-inline"><input type="checkbox" data-sequence-overlay-out="${number}" ${(sequence.repeat_overlay_outs || [1, 2, 3]).includes(number) ? "checked" : ""}> O${number}</label>`).join("")}
</span>
<label class="check-inline"><input type="checkbox" data-sequence-scoreboard ${sequence.is_scoreboard_sequence ? "checked" : ""}> этот сценарий — верхний счёт</label> <label class="check-inline"><input type="checkbox" data-sequence-scoreboard ${sequence.is_scoreboard_sequence ? "checked" : ""}> этот сценарий — верхний счёт</label>
<label class="check-inline"><input type="checkbox" data-sequence-team-states ${sequence.sync_hockey_team_states ? "checked" : ""}> добавлять состояния команд к верхнему счёту</label> <label class="check-inline"><input type="checkbox" data-sequence-team-states ${sequence.sync_hockey_team_states ? "checked" : ""}> добавлять состояния команд к верхнему счёту</label>
</div> </div>
@@ -14254,6 +14273,17 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
sequence.toggle_all_overlays_on_repeat = event.target.checked; sequence.toggle_all_overlays_on_repeat = event.target.checked;
if (!sequence.toggle_all_overlays_on_repeat) state.shortcutSequenceOverlayState.delete(sequence.id); if (!sequence.toggle_all_overlays_on_repeat) state.shortcutSequenceOverlayState.delete(sequence.id);
}); });
card.querySelectorAll("[data-sequence-overlay-out]").forEach((input) => input.addEventListener("change", (event) => {
const selected = [...card.querySelectorAll("[data-sequence-overlay-out]:checked")]
.map((item) => Number(item.dataset.sequenceOverlayOut))
.filter((value) => Number.isInteger(value) && value >= 1 && value <= 8);
if (!selected.length) {
event.target.checked = true;
toast("Для повторного снятия выберите хотя бы один Overlay");
return;
}
sequence.repeat_overlay_outs = selected;
}));
card.querySelector("[data-sequence-scoreboard]").addEventListener("change", (event) => { card.querySelector("[data-sequence-scoreboard]").addEventListener("change", (event) => {
sequence.is_scoreboard_sequence = event.target.checked; sequence.is_scoreboard_sequence = event.target.checked;
}); });
@@ -14345,8 +14375,8 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
const penaltyStep = createSequenceStep("vmix_command"); const penaltyStep = createSequenceStep("vmix_command");
Object.assign(penaltyStep, { function: "OverlayInput2In", condition: "has_penalties", label: "Если есть удаление — показать слой удаления" }); Object.assign(penaltyStep, { function: "OverlayInput2In", condition: "has_penalties", label: "Если есть удаление — показать слой удаления" });
const sequence = normalizeShortcutSequence({ const sequence = normalizeShortcutSequence({
id: uid(), name: "Счёт", description: "Показывает счёт; повторное нажатие снимает все Overlay.", id: uid(), name: "Счёт", description: "Показывает счёт; повторное нажатие снимает выбранные Overlay (по умолчанию 13).",
enabled: true, combo: "F1", prevent_default: true, toggle_all_overlays_on_repeat: true, sync_hockey_team_states: true, is_scoreboard_sequence: true, scope: "runtime", steps: [scoreStep, penaltyStep], enabled: true, combo: "F1", prevent_default: true, toggle_all_overlays_on_repeat: true, repeat_overlay_outs: [1, 2, 3], sync_hockey_team_states: true, is_scoreboard_sequence: true, scope: "runtime", steps: [scoreStep, penaltyStep],
}, state.config.shortcut_sequences.length); }, state.config.shortcut_sequences.length);
state.config.shortcut_sequences.push(sequence); state.config.shortcut_sequences.push(sequence);
state.shortcutSequenceOpenId = sequence.id; state.shortcutSequenceOpenId = sequence.id;