build 118 - оверлеи
This commit is contained in:
@@ -2357,7 +2357,7 @@ function startCustomTooltips() {
|
||||
id: String(target?.id || `penalty-target-${index + 1}-${Math.random().toString(36).slice(2, 7)}`),
|
||||
input: String(target?.input || ""),
|
||||
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,
|
||||
}));
|
||||
}
|
||||
@@ -2370,7 +2370,7 @@ function startCustomTooltips() {
|
||||
enabled: action?.enabled !== false,
|
||||
source: allowedSources.has(String(action?.source || "")) ? String(action.source) : "game",
|
||||
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),
|
||||
only_when_side_clear: action?.only_when_side_clear !== false,
|
||||
}));
|
||||
@@ -2537,6 +2537,15 @@ function startCustomTooltips() {
|
||||
prevent_default: sequence.prevent_default !== false,
|
||||
allow_in_inputs: Boolean(sequence.allow_in_inputs),
|
||||
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
|
||||
? Boolean(sequence.toggle_all_overlays_on_repeat)
|
||||
: Boolean(sequence.sync_hockey_team_states),
|
||||
@@ -4322,8 +4331,8 @@ function startCustomTooltips() {
|
||||
// executes the command, but the strict BUILD58 parser did not recognise it,
|
||||
// so the quick-panel button never received the on-air state.
|
||||
const normalized = String(functionName || "").trim().replace(/\s+/g, "");
|
||||
const match = normalized.match(/(?:^|[^a-z0-9])OverlayInput([1-4])(In|Out|Off)?(?=$|[^a-z])/i)
|
||||
|| normalized.match(/^OverlayInput([1-4])(In|Out|Off)?/i);
|
||||
const match = normalized.match(/(?:^|[^a-z0-9])OverlayInput([1-8])(In|Out|Off)?(?=$|[^a-z])/i)
|
||||
|| normalized.match(/^OverlayInput([1-8])(In|Out|Off)?/i);
|
||||
if (!match) return null;
|
||||
const raw = String(match[2] || "toggle").toLowerCase();
|
||||
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.
|
||||
const previewTargets = shortcutSequencePreviewTargets(sequenceOrId);
|
||||
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);
|
||||
if (previewTargets.some((input) => runtimeOverlayMatchesInput(current, input))) return true;
|
||||
}
|
||||
@@ -4523,7 +4532,7 @@ function startCustomTooltips() {
|
||||
const previousServerIds = new Set(state.quickPanelServerOnAirSequences);
|
||||
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];
|
||||
if (raw && typeof raw === "object") {
|
||||
const entry = {
|
||||
@@ -5203,7 +5212,7 @@ function startCustomTooltips() {
|
||||
if (entry && target.input && target.selected_name) {
|
||||
const eventKey = penaltyMirrorKey(entry.component, entry.event);
|
||||
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
|
||||
|| previous.eventKey !== eventKey
|
||||
|| String(previous.input || "") !== String(target.input)
|
||||
@@ -5261,7 +5270,7 @@ function startCustomTooltips() {
|
||||
&& String(previous.overlay || overlay) === overlay;
|
||||
if (!targetWasVisible) {
|
||||
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 });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
state.vmixPenaltyTargetAssignments.delete(assignmentKey);
|
||||
@@ -5285,7 +5294,7 @@ function startCustomTooltips() {
|
||||
stopCommands.push({ Function: "StopCountdown", Input: previous.input, SelectedName: previous.selectedName || target.selected_name });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
state.vmixPenaltyTargetAssignments.delete(assignmentKey);
|
||||
@@ -5314,7 +5323,7 @@ function startCustomTooltips() {
|
||||
async function runVmixTimedFinishAction(action, meta = {}) {
|
||||
const input = String(action?.input || "").trim();
|
||||
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 timerKey = `${action.id || input}:${overlay}:${input}`;
|
||||
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 || "") };
|
||||
window.dispatchEvent(new CustomEvent("ui-builder:shortcut-sequence-start", { detail: { sequence: clone(sequence), meta } }));
|
||||
try {
|
||||
if (sequence.toggle_all_overlays_on_repeat && state.shortcutSequenceOverlayState.get(sequence.id)) {
|
||||
// IMPORTANT: do not use OverlayInputAllOff here. vMix documents AllOff as an
|
||||
// immediate cut, which bypasses the configured overlay/GT transition-out.
|
||||
// Explicit OverlayInputNOut commands preserve the exit animation while still
|
||||
// clearing every overlay channel controlled by vMix.
|
||||
await sendRuntimeVmixSequence([
|
||||
{ Function: "OverlayInput1Out" },
|
||||
{ Function: "OverlayInput2Out" },
|
||||
{ Function: "OverlayInput3Out" },
|
||||
{ Function: "OverlayInput4Out" },
|
||||
], execution);
|
||||
// BUILD116: repeat-toggle must follow the same ON AIR decision as the UI.
|
||||
// With authoritative Agent/vMix tally, a scoreboard can be truly live even when
|
||||
// the legacy local latch is false (for example after reload or external vMix IN).
|
||||
// `wasOnAir` already resolves to the real tally when available, and falls back
|
||||
// to the legacy latch when tally is unavailable.
|
||||
if (sequence.toggle_all_overlays_on_repeat && wasOnAir) {
|
||||
// BUILD118: repeat OUT is configurable across vMix Overlay channels 1..8. Do not use
|
||||
// OverlayInputAllOff: explicit OverlayInputNOut preserves each configured
|
||||
// transition-out and avoids touching unrelated channels that are not selected.
|
||||
const repeatOverlayOuts = (Array.isArray(sequence.repeat_overlay_outs) ? sequence.repeat_overlay_outs : [1, 2, 3])
|
||||
.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();
|
||||
state.shortcutSequenceOverlayState.set(sequence.id, false);
|
||||
state.quickPanelOnAirSequences.delete(String(sequence.id || ""));
|
||||
@@ -11918,7 +11933,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
label: String(value.label || value[language] || fallback),
|
||||
input: String(value.input || "").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,
|
||||
};
|
||||
}
|
||||
@@ -13700,7 +13715,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
// Colour correction
|
||||
"SetCCGainB","SetCCGainG","SetCCGainR","SetCCGainRGB","SetCCGainY","SetCCGammaB","SetCCGammaG","SetCCGammaR","SetCCGammaRGB","SetCCGammaY","SetCCHue","SetCCLiftB","SetCCLiftG","SetCCLiftR","SetCCLiftRGB","SetCCLiftY","SetCCSaturation",
|
||||
// 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
|
||||
"DataSourceAutoNext","DataSourceAutoNextOff","DataSourceAutoNextOn","DataSourceNextRow","DataSourcePreviousRow","DataSourceSelectRow","BrowserBack","BrowserForward","BrowserHome","BrowserNavigate","BrowserReload","BrowserSetURL","BrowserExecuteJavascript","ScriptStart","ScriptStop","ScriptStartDynamic",
|
||||
// NDI / OMT
|
||||
@@ -13889,7 +13904,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
<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>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>
|
||||
<button type="button" class="icon-btn danger" data-penalty-target-delete="${escapeHtml(target.id)}" data-penalty-target-side="${side}" title="Удалить слот">×</button>
|
||||
</div>`).join("");
|
||||
@@ -13911,7 +13926,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
["away_penalty","Удаление AWAY завершено"],
|
||||
], 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>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 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>
|
||||
@@ -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 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-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-team-states ${sequence.sync_hockey_team_states ? "checked" : ""}> добавлять состояния команд к верхнему счёту</label>
|
||||
</div>
|
||||
@@ -14254,6 +14273,17 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
sequence.toggle_all_overlays_on_repeat = event.target.checked;
|
||||
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) => {
|
||||
sequence.is_scoreboard_sequence = event.target.checked;
|
||||
});
|
||||
@@ -14345,8 +14375,8 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
const penaltyStep = createSequenceStep("vmix_command");
|
||||
Object.assign(penaltyStep, { function: "OverlayInput2In", condition: "has_penalties", label: "Если есть удаление — показать слой удаления" });
|
||||
const sequence = normalizeShortcutSequence({
|
||||
id: uid(), name: "Счёт", description: "Показывает счёт; повторное нажатие снимает все Overlay.",
|
||||
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],
|
||||
id: uid(), name: "Счёт", description: "Показывает счёт; повторное нажатие снимает выбранные Overlay (по умолчанию 1–3).",
|
||||
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.push(sequence);
|
||||
state.shortcutSequenceOpenId = sequence.id;
|
||||
|
||||
Reference in New Issue
Block a user