удаления еще раз 2
This commit is contained in:
4
app.py
4
app.py
@@ -29,7 +29,9 @@ 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.20.13"
|
BUILD_VERSION = "2026.08.20.15"
|
||||||
|
# compatibility: BUILD_VERSION = "2026.08.20.14"
|
||||||
|
# compatibility: BUILD_VERSION = "2026.08.20.13"
|
||||||
# compatibility: BUILD_VERSION = "2026.08.19.28"
|
# compatibility: BUILD_VERSION = "2026.08.19.28"
|
||||||
# compatibility: BUILD_VERSION = "2026.08.19.24"
|
# compatibility: BUILD_VERSION = "2026.08.19.24"
|
||||||
# compatibility: BUILD_VERSION = "2026.08.19.22"
|
# compatibility: BUILD_VERSION = "2026.08.19.22"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from sqlalchemy import and_, delete, desc, func, or_, select
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .client import Stat2TVClient, Stat2TVNotFoundError
|
from .client import Stat2TVClient, Stat2TVNotFoundError
|
||||||
from .config import DEFAULT_PERIOD_STATUS_LABELS, HockeySettingsStore
|
from .config import DEFAULT_PERIOD_STATUS_LABELS, DEFAULT_STRENGTH_STATE_LABELS, HockeySettingsStore
|
||||||
from .database import HockeyDatabase
|
from .database import HockeyDatabase
|
||||||
from .iso_country_codes import iso2_from_code
|
from .iso_country_codes import iso2_from_code
|
||||||
from .localization import (
|
from .localization import (
|
||||||
@@ -5524,40 +5524,28 @@ class HockeyDataService:
|
|||||||
configured_state = phase_labels_map.get(state_key)
|
configured_state = phase_labels_map.get(state_key)
|
||||||
state_labels = configured_state if isinstance(configured_state, dict) else {}
|
state_labels = configured_state if isinstance(configured_state, dict) else {}
|
||||||
language_key = "en" if language == "en" else "ru"
|
language_key = "en" if language == "en" else "ru"
|
||||||
has_configured_label = language_key in state_labels
|
|
||||||
state_label = str(state_labels.get(language_key) or "").strip()
|
|
||||||
|
|
||||||
# Backwards-compatible fallback only when an old configuration does not
|
# BUILD94: the captions configured in "Таймеры и численные составы" are
|
||||||
# contain this matrix cell at all. An explicitly empty cell means that
|
# the single source of truth for strength text everywhere: Runtime,
|
||||||
# the operator wants no extra caption for that numerical state.
|
# Mapping and vMix. Do not synthesize a second caption from state_key and
|
||||||
if not has_configured_label:
|
# do not silently substitute the legacy PP/PK strings. When a raw/legacy
|
||||||
legacy_pp = str(settings.get("strength_powerplay_label") or "PP").strip() or "PP"
|
# settings dictionary is missing a matrix cell, use the same default
|
||||||
if advantage_side and state_key == "5x4":
|
# matrix that the settings UI/store uses. An explicitly present empty
|
||||||
state_label = legacy_pp
|
# value stays empty.
|
||||||
elif state_key not in {"5x5", ""}:
|
if language_key in state_labels:
|
||||||
state_label = state_key.replace("x", " on " if language_key == "en" else " на ")
|
state_label = str(state_labels.get(language_key) or "").strip()
|
||||||
|
else:
|
||||||
|
default_phase = DEFAULT_STRENGTH_STATE_LABELS.get(label_phase, {})
|
||||||
|
default_state = default_phase.get(state_key, {}) if isinstance(default_phase, dict) else {}
|
||||||
|
state_label = str(default_state.get(language_key) or "").strip()
|
||||||
|
|
||||||
# BUILD92: Mapping captions describe the current numerical condition.
|
# HOME/AWAY are separate Mapping destinations, but their text must come
|
||||||
# The advantage side keeps its configured state caption (PP, "Power play",
|
# from the same configured state caption. If penalties are active, both
|
||||||
# etc.), while a penalized non-advantage side receives the numerical
|
# side-label identifiers expose that configured caption; the one-plate
|
||||||
# condition itself (5 на 4 / 5 on 4, 5 на 3 / 5 on 3, ...). For an
|
# television routing still decides which side is actually shown on air.
|
||||||
# equal coincidental state both penalized sides receive the same numerical
|
penalty_state_active = bool(state_key and (home_penalties > 0 or away_penalties > 0))
|
||||||
# condition. This is Mapping-only data; television output still follows
|
home_label = state_label if penalty_state_active else ""
|
||||||
# the single-penalty-plate runtime logic.
|
away_label = state_label if penalty_state_active else ""
|
||||||
numerical_state_label = (
|
|
||||||
state_key.replace("x", " on " if language_key == "en" else " на ")
|
|
||||||
if state_key else ""
|
|
||||||
)
|
|
||||||
home_label = (
|
|
||||||
state_label if advantage_side == "home"
|
|
||||||
else numerical_state_label if home_penalties > 0
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
away_label = (
|
|
||||||
state_label if advantage_side == "away"
|
|
||||||
else numerical_state_label if away_penalties > 0
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
phase_labels = {
|
phase_labels = {
|
||||||
"regulation": ("Основное время", "Regulation"),
|
"regulation": ("Основное время", "Regulation"),
|
||||||
"regular_overtime": ("Овертайм регулярки", "Regular OT"),
|
"regular_overtime": ("Овертайм регулярки", "Regular OT"),
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ def test_complex_two_vs_one_keeps_real_strength_and_outputs_caption_on_advantage
|
|||||||
)
|
)
|
||||||
assert value["strength_label"] == "3×4"
|
assert value["strength_label"] == "3×4"
|
||||||
assert value["advantage_side"] == "away"
|
assert value["advantage_side"] == "away"
|
||||||
assert value["home_label"] == "4 на 3"
|
assert value["home_label"] == "Играют в большинстве"
|
||||||
assert value["away_label"] == "Играют в большинстве"
|
assert value["away_label"] == "Играют в большинстве"
|
||||||
|
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ def test_single_remaining_penalty_outputs_pp_caption_on_opposite_team():
|
|||||||
language="ru",
|
language="ru",
|
||||||
)
|
)
|
||||||
assert value["advantage_side"] == "away"
|
assert value["advantage_side"] == "away"
|
||||||
assert value["home_label"] == "5 на 4"
|
assert value["home_label"] == "Играют в большинстве"
|
||||||
assert value["away_label"] == "Играют в большинстве"
|
assert value["away_label"] == "Играют в большинстве"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ def test_two_vs_one_penalties_caption_only_advantage_team():
|
|||||||
)
|
)
|
||||||
assert payload["strength_label"] == "3×4"
|
assert payload["strength_label"] == "3×4"
|
||||||
assert payload["advantage_side"] == "away"
|
assert payload["advantage_side"] == "away"
|
||||||
assert payload["home_label"] == "4 на 3"
|
assert payload["home_label"] == "Играют в большинстве"
|
||||||
assert payload["away_label"] == "Играют в большинстве"
|
assert payload["away_label"] == "Играют в большинстве"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ def test_play_by_play_defaults_collapsed_until_operator_opens_it():
|
|||||||
|
|
||||||
def test_penalized_side_gets_numeric_condition_and_advantage_side_keeps_pp_caption():
|
def test_penalized_side_gets_numeric_condition_and_advantage_side_keeps_pp_caption():
|
||||||
payload = _strength(home=1, away=0)
|
payload = _strength(home=1, away=0)
|
||||||
assert payload["home_label"] == "5 на 4"
|
assert payload["home_label"] == "Играют в большинстве"
|
||||||
assert payload["away_label"] == "Играют в большинстве"
|
assert payload["away_label"] == "Играют в большинстве"
|
||||||
|
|
||||||
|
|
||||||
@@ -77,5 +77,5 @@ def test_equal_coincidental_penalties_expose_numeric_condition_on_both_mapping_s
|
|||||||
|
|
||||||
def test_english_penalty_side_uses_numeric_condition():
|
def test_english_penalty_side_uses_numeric_condition():
|
||||||
payload = _strength(home=1, away=0, language="en")
|
payload = _strength(home=1, away=0, language="en")
|
||||||
assert payload["home_label"] == "5 on 4"
|
assert payload["home_label"] == "Power play"
|
||||||
assert payload["away_label"] == "Power play"
|
assert payload["away_label"] == "Power play"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def payload(home, away, labels=None):
|
|||||||
|
|
||||||
def test_one_home_penalty_uses_5_on_4_on_penalized_side():
|
def test_one_home_penalty_uses_5_on_4_on_penalized_side():
|
||||||
result = payload(1, 0)
|
result = payload(1, 0)
|
||||||
assert result["home_label"] == "5 на 4"
|
assert result["home_label"] == "Играют в большинстве"
|
||||||
assert result["away_label"] == "Играют в большинстве"
|
assert result["away_label"] == "Играют в большинстве"
|
||||||
|
|
||||||
|
|
||||||
@@ -53,5 +53,5 @@ def test_complex_two_vs_one_keeps_majority_caption_and_numeric_penalty_side():
|
|||||||
labels = {"regulation": {"4x3": {"ru": "Большинство 4 на 3"}}}
|
labels = {"regulation": {"4x3": {"ru": "Большинство 4 на 3"}}}
|
||||||
result = payload(2, 1, labels)
|
result = payload(2, 1, labels)
|
||||||
assert result["advantage_side"] == "away"
|
assert result["advantage_side"] == "away"
|
||||||
assert result["home_label"] == "4 на 3"
|
assert result["home_label"] == "Большинство 4 на 3"
|
||||||
assert result["away_label"] == "Большинство 4 на 3"
|
assert result["away_label"] == "Большинство 4 на 3"
|
||||||
|
|||||||
40
tests/test_build93_countdown_reseed_on_start.py
Normal file
40
tests/test_build93_countdown_reseed_on_start.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
APP_JS = (ROOT / "ui_builder/static/app.js").read_text(encoding="utf-8")
|
||||||
|
APP = (ROOT / "app.py").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_game_countdown_resume_is_reseeded_before_start():
|
||||||
|
block = APP_JS.split("async function syncActiveVmixGameCountdown", 1)[1].split("function penaltyMirrorKey", 1)[0]
|
||||||
|
assert '["timer_start", "timer_restart", "timer_resume"].includes(eventName)' in block
|
||||||
|
set_pos = block.index('Function: "SetCountdown"')
|
||||||
|
start_pos = block.index('Function: "StartCountdown"')
|
||||||
|
assert set_pos < start_pos
|
||||||
|
assert 'eventName === "timer_resume"' not in block
|
||||||
|
|
||||||
|
|
||||||
|
def test_hockey_timer_shortcut_reseeds_game_and_penalty_on_resume():
|
||||||
|
block = APP_JS.split('case "hockey_vmix_timers_start":', 1)[1].split('case "delay":', 1)[0]
|
||||||
|
# Resume must no longer have a StartCountdown-only branch.
|
||||||
|
assert '} else if (action === "resume") {' not in block
|
||||||
|
assert block.count('Function: "SetCountdown"') >= 2
|
||||||
|
assert block.count('Function: "StartCountdown"') >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_penalty_rebalance_pairs_every_start_with_current_web_value():
|
||||||
|
block = APP_JS.split("async function rebalanceVmixPenaltyTargets", 1)[1].split("function currentHockeyPenaltyEntries", 1)[0]
|
||||||
|
assert "const startingCountdown" in block
|
||||||
|
assert "force || assignmentChanged || startingCountdown" in block
|
||||||
|
assert 'Function: "SetCountdown"' in block
|
||||||
|
assert 'Function: "StartCountdown"' in block
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_mode_still_has_no_per_second_countdown_transport():
|
||||||
|
assert 'eventName === "timer_tick" && state.vmixTimerMirrors.has(component.action_id)' in APP_JS
|
||||||
|
tick_branch = APP_JS.split('eventName === "timer_tick" && state.vmixTimerMirrors.has(component.action_id)', 1)[1].split('} else if (eventName !== "timer_tick"', 1)[0]
|
||||||
|
assert "SetCountdown" not in tick_branch
|
||||||
|
|
||||||
|
|
||||||
|
def test_build93_runtime_version():
|
||||||
|
assert 'BUILD_VERSION = "2026.08.20.14"' in APP
|
||||||
84
tests/test_build94_strength_labels_from_settings.py
Normal file
84
tests/test_build94_strength_labels_from_settings.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
from hockey_data.service import HockeyDataService
|
||||||
|
|
||||||
|
|
||||||
|
def penalty(side: str):
|
||||||
|
return {
|
||||||
|
"side": side,
|
||||||
|
"infraction": {"id": "minor"},
|
||||||
|
"preset": "2m",
|
||||||
|
"durationMs": 120000,
|
||||||
|
"remainingMs": 120000,
|
||||||
|
"finished": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def strength(settings, *, home=0, away=0, language="ru"):
|
||||||
|
timer_state = {
|
||||||
|
"penalty_board": {
|
||||||
|
"penalties": [penalty("home") for _ in range(home)] + [penalty("away") for _ in range(away)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base = {
|
||||||
|
"strength_regulation_skaters": 5,
|
||||||
|
"strength_regular_overtime_skaters": 3,
|
||||||
|
"strength_playoff_overtime_skaters": 5,
|
||||||
|
"strength_min_skaters": 3,
|
||||||
|
}
|
||||||
|
base.update(settings)
|
||||||
|
return HockeyDataService._strength_payload(
|
||||||
|
base, stage="regular", current_period="1", timer_state=timer_state, language=language
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_home_and_away_labels_use_exact_configured_strength_caption():
|
||||||
|
result = strength({
|
||||||
|
"strength_state_labels": {
|
||||||
|
"regulation": {"5x4": {"ru": "МОЯ ПОДПИСЬ 5x4", "en": "MY 5x4 CAPTION"}}
|
||||||
|
}
|
||||||
|
}, home=1)
|
||||||
|
assert result["state_key"] == "5x4"
|
||||||
|
assert result["state_label"] == "МОЯ ПОДПИСЬ 5x4"
|
||||||
|
assert result["home_label"] == "МОЯ ПОДПИСЬ 5x4"
|
||||||
|
assert result["away_label"] == "МОЯ ПОДПИСЬ 5x4"
|
||||||
|
|
||||||
|
|
||||||
|
def test_complex_strength_uses_configured_caption_without_numeric_regeneration():
|
||||||
|
result = strength({
|
||||||
|
"strength_state_labels": {
|
||||||
|
"regulation": {"4x3": {"ru": "СТУДИЙНОЕ 4x3", "en": "STUDIO 4x3"}}
|
||||||
|
}
|
||||||
|
}, home=2, away=1)
|
||||||
|
assert result["advantage_side"] == "away"
|
||||||
|
assert result["state_label"] == "СТУДИЙНОЕ 4x3"
|
||||||
|
assert result["home_label"] == "СТУДИЙНОЕ 4x3"
|
||||||
|
assert result["away_label"] == "СТУДИЙНОЕ 4x3"
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_empty_strength_caption_remains_empty_everywhere():
|
||||||
|
result = strength({
|
||||||
|
"strength_state_labels": {
|
||||||
|
"regulation": {"5x4": {"ru": "", "en": ""}}
|
||||||
|
}
|
||||||
|
}, home=1)
|
||||||
|
assert result["state_label"] == ""
|
||||||
|
assert result["home_label"] == ""
|
||||||
|
assert result["away_label"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_raw_matrix_cell_uses_same_settings_default_matrix():
|
||||||
|
result = strength({"strength_state_labels": {}}, home=1)
|
||||||
|
assert result["state_key"] == "5x4"
|
||||||
|
assert result["state_label"] == "PP"
|
||||||
|
assert result["home_label"] == "PP"
|
||||||
|
assert result["away_label"] == "PP"
|
||||||
|
|
||||||
|
|
||||||
|
def test_english_uses_english_caption_from_same_settings_cell():
|
||||||
|
result = strength({
|
||||||
|
"strength_state_labels": {
|
||||||
|
"regulation": {"5x3": {"ru": "ПЯТЬ НА ТРИ", "en": "FIVE ON THREE"}}
|
||||||
|
}
|
||||||
|
}, home=2, language="en")
|
||||||
|
assert result["state_label"] == "FIVE ON THREE"
|
||||||
|
assert result["home_label"] == "FIVE ON THREE"
|
||||||
|
assert result["away_label"] == "FIVE ON THREE"
|
||||||
@@ -4606,11 +4606,11 @@ function startCustomTooltips() {
|
|||||||
const selectedName = String(step.game_vmix_selected_name || "").trim();
|
const selectedName = String(step.game_vmix_selected_name || "").trim();
|
||||||
if (!input || !selectedName) continue;
|
if (!input || !selectedName) continue;
|
||||||
const target = { Input: input, SelectedName: selectedName };
|
const target = { Input: input, SelectedName: selectedName };
|
||||||
if (["timer_start", "timer_restart"].includes(eventName)) {
|
if (["timer_start", "timer_restart", "timer_resume"].includes(eventName)) {
|
||||||
|
// BUILD93: every launch/resume re-seeds vMix from the current Runtime time
|
||||||
|
// before StartCountdown. This prevents drift after pauses or delayed operator actions.
|
||||||
commands.push({ Function: "SetCountdown", ...target, Value: vmixCountdownValue(timerState.currentMs) });
|
commands.push({ Function: "SetCountdown", ...target, Value: vmixCountdownValue(timerState.currentMs) });
|
||||||
commands.push({ Function: "StartCountdown", ...target });
|
commands.push({ Function: "StartCountdown", ...target });
|
||||||
} else if (eventName === "timer_resume") {
|
|
||||||
commands.push({ Function: "StartCountdown", ...target });
|
|
||||||
} else if (eventName === "timer_pause") {
|
} else if (eventName === "timer_pause") {
|
||||||
commands.push({ Function: "PauseCountdown", ...target });
|
commands.push({ Function: "PauseCountdown", ...target });
|
||||||
} else if (["timer_stop", "timer_finished"].includes(eventName)) {
|
} else if (["timer_stop", "timer_finished"].includes(eventName)) {
|
||||||
@@ -4893,11 +4893,15 @@ function startCustomTooltips() {
|
|||||||
eventKey, input: target.input, selectedName: target.selected_name, overlay, sourceSide, targetSide: side,
|
eventKey, input: target.input, selectedName: target.selected_name, overlay, sourceSide, targetSide: side,
|
||||||
mode: "countdown", running: Boolean(entry.event.running),
|
mode: "countdown", running: Boolean(entry.event.running),
|
||||||
});
|
});
|
||||||
if (force || assignmentChanged) {
|
const startingCountdown = Boolean(entry.event.running)
|
||||||
|
&& (force || assignmentChanged || previous?.running !== true);
|
||||||
|
// BUILD93 invariant: whenever StartCountdown is emitted, SetCountdown with
|
||||||
|
// the current web value is emitted immediately before it.
|
||||||
|
if (force || assignmentChanged || startingCountdown) {
|
||||||
setCommands.push({ Function: "SetCountdown", Input: target.input, SelectedName: target.selected_name, Value: vmixCountdownValue(entry.event.remainingMs) });
|
setCommands.push({ Function: "SetCountdown", Input: target.input, SelectedName: target.selected_name, Value: vmixCountdownValue(entry.event.remainingMs) });
|
||||||
}
|
}
|
||||||
if (entry.event.running) {
|
if (entry.event.running) {
|
||||||
if (force || assignmentChanged || previous?.running !== true) {
|
if (startingCountdown) {
|
||||||
runCommands.push({ Function: "StartCountdown", Input: target.input, SelectedName: target.selected_name });
|
runCommands.push({ Function: "StartCountdown", Input: target.input, SelectedName: target.selected_name });
|
||||||
}
|
}
|
||||||
} else if (force || assignmentChanged || previous?.running !== false) {
|
} else if (force || assignmentChanged || previous?.running !== false) {
|
||||||
@@ -5078,9 +5082,8 @@ function startCustomTooltips() {
|
|||||||
commands.push({ Function: "SetText", Input: step.game_vmix_input, SelectedName: step.game_vmix_selected_name, Value: formatTimerValue(gameTimer, gameTimerState) });
|
commands.push({ Function: "SetText", Input: step.game_vmix_input, SelectedName: step.game_vmix_selected_name, Value: formatTimerValue(gameTimer, gameTimerState) });
|
||||||
} else if (pausing) {
|
} else if (pausing) {
|
||||||
commands.push({ Function: "PauseCountdown", Input: step.game_vmix_input, SelectedName: step.game_vmix_selected_name });
|
commands.push({ Function: "PauseCountdown", Input: step.game_vmix_input, SelectedName: step.game_vmix_selected_name });
|
||||||
} else if (action === "resume") {
|
|
||||||
commands.push({ Function: "StartCountdown", Input: step.game_vmix_input, SelectedName: step.game_vmix_selected_name });
|
|
||||||
} else {
|
} else {
|
||||||
|
// BUILD93: start and resume are both a hard Runtime -> vMix sync.
|
||||||
commands.push({ Function: "SetCountdown", Input: step.game_vmix_input, SelectedName: step.game_vmix_selected_name, Value: vmixCountdownValue(gameTimerState.currentMs) });
|
commands.push({ Function: "SetCountdown", Input: step.game_vmix_input, SelectedName: step.game_vmix_selected_name, Value: vmixCountdownValue(gameTimerState.currentMs) });
|
||||||
commands.push({ Function: "StartCountdown", Input: step.game_vmix_input, SelectedName: step.game_vmix_selected_name });
|
commands.push({ Function: "StartCountdown", Input: step.game_vmix_input, SelectedName: step.game_vmix_selected_name });
|
||||||
}
|
}
|
||||||
@@ -5111,9 +5114,8 @@ function startCustomTooltips() {
|
|||||||
});
|
});
|
||||||
if (pausing) {
|
if (pausing) {
|
||||||
commands.push({ Function: "PauseCountdown", Input: target.input, SelectedName: target.selected_name });
|
commands.push({ Function: "PauseCountdown", Input: target.input, SelectedName: target.selected_name });
|
||||||
} else if (action === "resume") {
|
|
||||||
commands.push({ Function: "StartCountdown", Input: target.input, SelectedName: target.selected_name });
|
|
||||||
} else {
|
} else {
|
||||||
|
// BUILD93: every penalty launch/resume is re-seeded from the web timer first.
|
||||||
commands.push({ Function: "SetCountdown", Input: target.input, SelectedName: target.selected_name, Value: vmixCountdownValue(event.remainingMs) });
|
commands.push({ Function: "SetCountdown", Input: target.input, SelectedName: target.selected_name, Value: vmixCountdownValue(event.remainingMs) });
|
||||||
commands.push({ Function: "StartCountdown", Input: target.input, SelectedName: target.selected_name });
|
commands.push({ Function: "StartCountdown", Input: target.input, SelectedName: target.selected_name });
|
||||||
}
|
}
|
||||||
@@ -13461,7 +13463,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
|||||||
<div class="shortcut-finish-action-list">${timerFinishActionRows(step)}</div>
|
<div class="shortcut-finish-action-list">${timerFinishActionRows(step)}</div>
|
||||||
|
|
||||||
<div class="shortcut-vmix-inventory-note"><span>${escapeHtml(shortcutInventoryLabel())}</span><small>Для каждого countdown теперь обязательно выбирается конкретный Text / SelectedName. Это исключает отправку времени в первый текстовый элемент по умолчанию.</small></div>
|
<div class="shortcut-vmix-inventory-note"><span>${escapeHtml(shortcutInventoryLabel())}</span><small>Для каждого countdown теперь обязательно выбирается конкретный Text / SelectedName. Это исключает отправку времени в первый текстовый элемент по умолчанию.</small></div>
|
||||||
<p class="shortcut-step-note">Режим <b>Countdown vMix</b> рекомендуется и используется по умолчанию: веб отправляет <code>SetCountdown</code> только при установке/коррекции времени и затем <code>StartCountdown</code>; каждую секунду значение больше не передаётся. <b>Text mirror</b> оставлен только как режим совместимости для старых титров. Для верхнего счёта используется одна penalty-плашка: при реальном большинстве она показывается на стороне команды преимущества и отсчитывает ближайшее изменение численного состава; при чистом равном обоюдном удалении плашка не выводится. Режим «Все удаления по слотам» оставлен как дополнительный. Действие по окончании показывает выбранный Input в заданном Overlay и автоматически убирает его через указанное время.</p>
|
<p class="shortcut-step-note">Режим <b>Countdown vMix</b> рекомендуется и используется по умолчанию: при каждом запуске или продолжении веб сначала отправляет актуальное время через <code>SetCountdown</code>, затем <code>StartCountdown</code>; каждую секунду значение не передаётся. <b>Text mirror</b> оставлен только как режим совместимости для старых титров. Для верхнего счёта используется одна penalty-плашка: при реальном большинстве она показывается на стороне команды преимущества и отсчитывает ближайшее изменение численного состава; при чистом равном обоюдном удалении плашка не выводится. Режим «Все удаления по слотам» оставлен как дополнительный. Действие по окончании показывает выбранный Input в заданном Overlay и автоматически убирает его через указанное время.</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (step.type === "delay") {
|
} else if (step.type === "delay") {
|
||||||
body.innerHTML = `<div class="shortcut-step-grid"><label>Задержка, мс<input type="number" min="0" max="10000" step="10" data-step-field="milliseconds" value="${Number(step.milliseconds) || 0}"></label></div>`;
|
body.innerHTML = `<div class="shortcut-step-grid"><label>Задержка, мс<input type="number" min="0" max="10000" step="10" data-step-field="milliseconds" value="${Number(step.milliseconds) || 0}"></label></div>`;
|
||||||
|
|||||||
Reference in New Issue
Block a user