пауза данных пока находишься в настройках
This commit is contained in:
@@ -64,6 +64,7 @@
|
|||||||
drawerRefreshTimer: null,
|
drawerRefreshTimer: null,
|
||||||
drawerRefreshBusy: false,
|
drawerRefreshBusy: false,
|
||||||
operatorSettingsPaused: false,
|
operatorSettingsPaused: false,
|
||||||
|
operatorSettingsPauseGeneration: 0,
|
||||||
settingsPauseObserver: null,
|
settingsPauseObserver: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1706,6 +1707,7 @@ function gameCard(game) {
|
|||||||
// BUILD152: absolutely no Stat2TV data polling while any operator settings
|
// BUILD152: absolutely no Stat2TV data polling while any operator settings
|
||||||
// window is open. ONLINE stays logically enabled and resumes after close.
|
// window is open. ONLINE stays logically enabled and resumes after close.
|
||||||
if (operatorSettingsEditingActive()) return null;
|
if (operatorSettingsEditingActive()) return null;
|
||||||
|
const refreshGeneration = operatorDataRefreshGeneration();
|
||||||
state.scheduleOnlineBusy = true;
|
state.scheduleOnlineBusy = true;
|
||||||
try {
|
try {
|
||||||
const result = await request(scheduleOnlineEndpoint({
|
const result = await request(scheduleOnlineEndpoint({
|
||||||
@@ -1715,9 +1717,9 @@ function gameCard(game) {
|
|||||||
}), { method: "POST", timeoutMs: 45000 });
|
}), { method: "POST", timeoutMs: 45000 });
|
||||||
if (!scheduleOnlineContextMatches()) return result;
|
if (!scheduleOnlineContextMatches()) return result;
|
||||||
// A settings window may have opened while the HTTP request was in flight.
|
// A settings window may have opened while the HTTP request was in flight.
|
||||||
// Do not rebuild Runtime under the operator in that case; resume will
|
// Reject any response from the pre-settings generation even if Settings
|
||||||
// perform a fresh discover pass after settings are closed.
|
// has already closed again by the time the request finishes.
|
||||||
if (operatorSettingsEditingActive()) return result;
|
if (!operatorDataRefreshStillCurrent(refreshGeneration)) return result;
|
||||||
state.scheduleLive = result?.schedule || state.scheduleLive;
|
state.scheduleLive = result?.schedule || state.scheduleLive;
|
||||||
applyGamesData();
|
applyGamesData();
|
||||||
return result;
|
return result;
|
||||||
@@ -2243,8 +2245,18 @@ function startScheduleOnlineTimerOnly() {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function operatorDataRefreshGeneration() {
|
||||||
|
return Number(state.operatorSettingsPauseGeneration || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function operatorDataRefreshStillCurrent(generation) {
|
||||||
|
return !operatorSettingsEditingActive()
|
||||||
|
&& Number(generation) === operatorDataRefreshGeneration();
|
||||||
|
}
|
||||||
|
|
||||||
function pauseOperatorDataRefreshes() {
|
function pauseOperatorDataRefreshes() {
|
||||||
if (state.operatorSettingsPaused) return;
|
if (state.operatorSettingsPaused) return;
|
||||||
|
state.operatorSettingsPauseGeneration = operatorDataRefreshGeneration() + 1;
|
||||||
state.operatorSettingsPaused = true;
|
state.operatorSettingsPaused = true;
|
||||||
stopDrawerRefresh();
|
stopDrawerRefresh();
|
||||||
stopSelectedGamePolling();
|
stopSelectedGamePolling();
|
||||||
@@ -2337,6 +2349,7 @@ async function pollSelectedGame() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshGeneration = operatorDataRefreshGeneration();
|
||||||
state.gamePollBusy = true;
|
state.gamePollBusy = true;
|
||||||
try {
|
try {
|
||||||
const live = await request(
|
const live = await request(
|
||||||
@@ -2345,6 +2358,10 @@ async function pollSelectedGame() {
|
|||||||
+ `&language=${encodeURIComponent(state.language)}`,
|
+ `&language=${encodeURIComponent(state.language)}`,
|
||||||
{ method: "POST", timeoutMs: 8000 }
|
{ method: "POST", timeoutMs: 8000 }
|
||||||
);
|
);
|
||||||
|
// BUILD160: the request may have started just before Settings opened.
|
||||||
|
// Discard that response completely so neither state nor DOM changes during
|
||||||
|
// the editing session. Resume performs a fresh poll after Settings closes.
|
||||||
|
if (!operatorDataRefreshStillCurrent(refreshGeneration)) return;
|
||||||
const updated = live.game;
|
const updated = live.game;
|
||||||
const previous = state.selectedGame || {};
|
const previous = state.selectedGame || {};
|
||||||
if (live.changed && updated) {
|
if (live.changed && updated) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// BUILD159_COUNTDOWN_TRANSITION_FIXES
|
// BUILD159_COUNTDOWN_TRANSITION_FIXES
|
||||||
// BUILD155: all hockey game/penalty timers use native vMix Countdown; no per-second SetText mirroring.
|
// BUILD155: all hockey game/penalty timers use native vMix Countdown; no per-second SetText mirroring.
|
||||||
|
// BUILD160: strict settings freeze; background vMix inventory refresh never rebuilds an open settings form.
|
||||||
// BUILD153: settings freeze all data-driven DOM rendering and keep dropdown popovers open.
|
// BUILD153: settings freeze all data-driven DOM rendering and keep dropdown popovers open.
|
||||||
// BUILD152: all browser-side hockey data polling pauses while operator settings are open.
|
// BUILD152: all browser-side hockey data polling pauses while operator settings are open.
|
||||||
// BUILD151: Schedule ONLINE is a toggle; one-second live polling uses isolated schedule_live.
|
// BUILD151: Schedule ONLINE is a toggle; one-second live polling uses isolated schedule_live.
|
||||||
@@ -15369,6 +15370,118 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shortcutSequenceStepById(stepId) {
|
||||||
|
const wanted = String(stepId || "").trim();
|
||||||
|
if (!wanted) return null;
|
||||||
|
for (const sequence of state.config.shortcut_sequences || []) {
|
||||||
|
const step = (sequence.steps || []).find((item) => String(item?.id || "") === wanted);
|
||||||
|
if (step) return step;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortcutInventorySelectIsBeingUsed(select) {
|
||||||
|
if (!select) return false;
|
||||||
|
const shell = select.closest?.(".css-select-shell");
|
||||||
|
const trigger = shell?.querySelector?.(".css-select-trigger");
|
||||||
|
if (activeControlPopover?.anchor === trigger) return true;
|
||||||
|
const focused = document.activeElement;
|
||||||
|
return Boolean(focused && (focused === select || shell?.contains?.(focused)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshShortcutInventorySelect(select, optionsHtml, value) {
|
||||||
|
if (!select || shortcutInventorySelectIsBeingUsed(select)) return false;
|
||||||
|
const selectedValue = String(value || "");
|
||||||
|
select.innerHTML = optionsHtml;
|
||||||
|
select.value = selectedValue;
|
||||||
|
refreshEnhancedControl(select);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshShortcutEditorInventoryControls(editor) {
|
||||||
|
if (!editor?.isConnected) return;
|
||||||
|
|
||||||
|
const status = editor.querySelector("[data-vmix-inventory-status]");
|
||||||
|
if (status) status.textContent = shortcutInventoryLabel();
|
||||||
|
editor.querySelectorAll(".shortcut-vmix-inventory-note > span").forEach((node) => {
|
||||||
|
node.textContent = shortcutInventoryLabel();
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.querySelectorAll("[data-step-id]").forEach((card) => {
|
||||||
|
const step = shortcutSequenceStepById(card.dataset.stepId);
|
||||||
|
if (!step) return;
|
||||||
|
|
||||||
|
if (step.type === "vmix_command") {
|
||||||
|
refreshShortcutInventorySelect(
|
||||||
|
card.querySelector('[data-step-field="input"]'),
|
||||||
|
vmixInputOptions(step.input),
|
||||||
|
step.input
|
||||||
|
);
|
||||||
|
refreshShortcutInventorySelect(
|
||||||
|
card.querySelector('[data-step-field="selected_name"]'),
|
||||||
|
vmixSelectedNameOptions(step.input, step.selected_name),
|
||||||
|
step.selected_name
|
||||||
|
);
|
||||||
|
refreshShortcutInventorySelect(
|
||||||
|
card.querySelector('[data-step-field="scoreboard_alternate_input"]'),
|
||||||
|
vmixInputOptions(step.scoreboard_alternate_input),
|
||||||
|
step.scoreboard_alternate_input
|
||||||
|
);
|
||||||
|
refreshShortcutInventorySelect(
|
||||||
|
card.querySelector('[data-step-field="scoreboard_alternate_selected_name"]'),
|
||||||
|
vmixSelectedNameOptions(step.scoreboard_alternate_input, step.scoreboard_alternate_selected_name),
|
||||||
|
step.scoreboard_alternate_selected_name
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step.type !== "hockey_vmix_timers_start") return;
|
||||||
|
|
||||||
|
refreshShortcutInventorySelect(
|
||||||
|
card.querySelector('[data-step-field="game_vmix_input"]'),
|
||||||
|
vmixInputOptions(step.game_vmix_input),
|
||||||
|
step.game_vmix_input
|
||||||
|
);
|
||||||
|
refreshShortcutInventorySelect(
|
||||||
|
card.querySelector('[data-step-field="game_vmix_selected_name"]'),
|
||||||
|
vmixTextSelectedNameOptions(step.game_vmix_input, step.game_vmix_selected_name),
|
||||||
|
step.game_vmix_selected_name
|
||||||
|
);
|
||||||
|
|
||||||
|
const homeTargets = sequencePenaltyTargets(step, "home");
|
||||||
|
const awayTargets = sequencePenaltyTargets(step, "away");
|
||||||
|
card.querySelectorAll("[data-penalty-target-row]").forEach((row) => {
|
||||||
|
const targetId = String(row.dataset.penaltyTargetRow || "");
|
||||||
|
const side = row.querySelector("[data-penalty-target-side]")?.dataset?.penaltyTargetSide || "";
|
||||||
|
const targets = side === "away" ? awayTargets : homeTargets;
|
||||||
|
const target = targets.find((item) => String(item?.id || "") === targetId);
|
||||||
|
if (!target) return;
|
||||||
|
refreshShortcutInventorySelect(
|
||||||
|
row.querySelector('[data-penalty-target-field="input"]'),
|
||||||
|
vmixInputOptions(target.input),
|
||||||
|
target.input
|
||||||
|
);
|
||||||
|
refreshShortcutInventorySelect(
|
||||||
|
row.querySelector('[data-penalty-target-field="selected_name"]'),
|
||||||
|
vmixTextSelectedNameOptions(target.input, target.selected_name),
|
||||||
|
target.selected_name
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const finishActions = normalizeTimerFinishActions(step.timer_finish_actions);
|
||||||
|
card.querySelectorAll("[data-finish-action-row]").forEach((row) => {
|
||||||
|
const actionId = String(row.dataset.finishActionRow || "");
|
||||||
|
const action = finishActions.find((item) => String(item?.id || "") === actionId);
|
||||||
|
if (!action) return;
|
||||||
|
refreshShortcutInventorySelect(
|
||||||
|
row.querySelector('[data-finish-action-field="input"]'),
|
||||||
|
vmixInputOptions(action.input),
|
||||||
|
action.input
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function showShortcutsEditor() {
|
function showShortcutsEditor() {
|
||||||
state.shortcutSequenceOpenId = "";
|
state.shortcutSequenceOpenId = "";
|
||||||
showSettingsModal("Шорткаты и сценарии", `<div class="shortcut-sequence-editor">
|
showSettingsModal("Шорткаты и сценарии", `<div class="shortcut-sequence-editor">
|
||||||
@@ -15446,11 +15559,19 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
|||||||
render({ focusId: sequence.id });
|
render({ focusId: sequence.id });
|
||||||
});
|
});
|
||||||
editor.querySelector("[data-open-shortcut-reference]").addEventListener("click", showShortcutsReference);
|
editor.querySelector("[data-open-shortcut-reference]").addEventListener("click", showShortcutsReference);
|
||||||
editor.querySelector("[data-refresh-vmix-inventory]").addEventListener("click", async () => { await loadShortcutInventory(); render({ focusId: state.shortcutSequenceOpenId }); });
|
editor.querySelector("[data-refresh-vmix-inventory]").addEventListener("click", async () => {
|
||||||
|
await loadShortcutInventory();
|
||||||
|
refreshShortcutEditorInventoryControls(editor);
|
||||||
|
});
|
||||||
editor.querySelector("[data-save-shortcuts]").addEventListener("click", () => saveConfig());
|
editor.querySelector("[data-save-shortcuts]").addEventListener("click", () => saveConfig());
|
||||||
editor.querySelector("[data-close-shortcuts]").addEventListener("click", closeModal);
|
editor.querySelector("[data-close-shortcuts]").addEventListener("click", closeModal);
|
||||||
render();
|
render();
|
||||||
loadShortcutInventory({ silent: true }).then(() => render({ focusId: state.shortcutSequenceOpenId }));
|
loadShortcutInventory({ silent: true }).then(() => {
|
||||||
|
// BUILD160: do not replace the whole settings DOM after the async vMix
|
||||||
|
// inventory request completes. Replacing it can close an already-open
|
||||||
|
// dropdown and visually restore defaults while the operator is editing.
|
||||||
|
refreshShortcutEditorInventoryControls(editor);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortcutReferenceItems() {
|
function shortcutReferenceItems() {
|
||||||
|
|||||||
Reference in New Issue
Block a user