feat: complete legacy UI and playout parity migration

This commit is contained in:
2026-07-15 13:11:38 +09:00
parent f14800656b
commit 6e0c275fdd
108 changed files with 43738 additions and 560 deletions

View File

@@ -0,0 +1,172 @@
(function () {
"use strict";
const webview = window.chrome && window.chrome.webview;
const connection = document.getElementById("playout-connection-state");
const prepare = document.getElementById("playout-prepare");
const takeIn = document.getElementById("playout-take-in");
const next = document.getElementById("playout-next");
const takeOut = document.getElementById("playout-take-out");
const backgroundNone = document.getElementById("playout-background-none");
const backgroundFile = document.getElementById("playout-background-file");
const backgroundName = document.getElementById("playout-background-name");
const fadeDuration = document.getElementById("playout-fade-duration");
const status = document.getElementById("playout-status");
const controls = [prepare, takeIn, next, takeOut, backgroundNone,
backgroundFile, fadeDuration];
let currentState = null;
let localBusy = false;
function hasOpenDialog() {
return Array.from(document.querySelectorAll("[role='dialog'], [role='alertdialog']"))
.some(function (dialog) {
const container = dialog.closest("[hidden]");
return !container && !dialog.hidden;
});
}
function post(type, payload) {
if (!webview || localBusy || hasOpenDialog()) return;
localBusy = true;
render(currentState);
webview.postMessage({ type: type, payload: payload || {} });
}
function setAllDisabled(disabled) {
controls.forEach(function (control) {
if (control) control.disabled = disabled;
});
}
function phaseLabel(phase) {
if (phase === "prepared") return "PREPARED";
if (phase === "program") return "PROGRAM";
return "IDLE";
}
function connectionLabel(playout) {
if (!playout) return "미연결";
if (playout.outcomeUnknown) return "결과 불명확";
if (playout.mode === "disabled") return "송출 비활성";
if (playout.mode === "dryRun") return "DRY RUN";
if (playout.isConnected) return "Tornado2 연결됨";
if (playout.isProcessRunning) return "Tornado2 감지됨";
return "미연결";
}
function pageLabel(playout) {
if (!playout || playout.pageCount <= 0) return "";
return " · Page " + String(playout.pageIndexZeroBased + 1) +
"/" + String(playout.pageCount);
}
function nextLabel(playout) {
if (!playout || playout.nextKind === "none") return "";
return playout.nextKind === "pageNext" ? " · 다음 페이지" : " · 다음 항목";
}
function render(state) {
currentState = state || currentState;
const playout = currentState && currentState.playout;
const nativeBusy = !!(currentState && currentState.isBusy) ||
!!(playout && playout.isBusy);
const busy = localBusy || nativeBusy;
if (!playout) {
connection.textContent = "미연결";
status.textContent = "송출 엔진 상태를 사용할 수 없습니다.";
setAllDisabled(true);
return;
}
const commandAvailable = playout.isCommandAvailable === true &&
playout.outcomeUnknown !== true;
const hasPlaylist = Array.isArray(currentState.playlist) &&
currentState.playlist.some(function (row) { return row.isEnabled === true; });
const liveTakeInAllowed = playout.mode !== "live" ||
playout.liveTakeInAllowed === true;
const canPrepareToggle = playout.phase === "idle" ? hasPlaylist :
playout.phase === "prepared" || playout.phase === "program";
connection.textContent = connectionLabel(playout);
connection.classList.toggle("connected", playout.isConnected === true);
connection.classList.toggle("outcome-unknown", playout.outcomeUnknown === true);
prepare.classList.toggle("active", playout.phase !== "idle");
prepare.disabled = busy || !commandAvailable || !canPrepareToggle ||
playout.isPlayCompletionPending === true;
const canTakeIn = playout.phase === "prepared" ||
(playout.phase === "idle" && hasPlaylist);
takeIn.disabled = busy || !commandAvailable || !liveTakeInAllowed ||
!canTakeIn || playout.isPlayCompletionPending === true;
next.disabled = busy || !commandAvailable ||
playout.phase !== "program" || playout.nextKind === "none" ||
playout.isPlayCompletionPending === true;
takeOut.disabled = busy || !commandAvailable ||
(playout.phase !== "prepared" && playout.phase !== "program");
backgroundNone.disabled = busy || playout.canChangeBackground !== true;
backgroundFile.disabled = busy || playout.canChangeBackground !== true;
fadeDuration.disabled = busy || playout.canChangeBackground !== true;
backgroundNone.checked = playout.backgroundEnabled !== true;
backgroundName.value = playout.backgroundEnabled === true
? (playout.backgroundFileName || "선택한 배경") : "배경없음";
fadeDuration.value = String(playout.fadeDuration);
const scene = playout.onAirCode || playout.preparedCode;
const sceneLabel = scene ? " · " + scene : "";
const refresh = playout.refreshActive
? " · 자동 갱신 " + String(playout.refreshCompletedCount) + "회" : "";
status.textContent = phaseLabel(playout.phase) + sceneLabel +
pageLabel(playout) + nextLabel(playout) + refresh +
(playout.message ? " · " + playout.message : "");
}
prepare.addEventListener("click", function () {
post("prepare-playout", {});
});
takeIn.addEventListener("click", function () {
post("take-in", {});
});
next.addEventListener("click", function () {
post("next-playout", {});
});
takeOut.addEventListener("click", function () {
post("take-out", {});
});
backgroundFile.addEventListener("click", function () {
post("choose-background", {});
});
backgroundNone.addEventListener("change", function () {
post("toggle-background", {});
});
fadeDuration.addEventListener("change", function () {
post("set-fade-duration", { duration: Number(fadeDuration.value) });
});
document.addEventListener("keydown", function (event) {
if (event.repeat || hasOpenDialog()) return;
if (event.key === "F2") {
event.preventDefault();
if (!backgroundFile.disabled) backgroundFile.click();
} else if (event.key === "F3") {
event.preventDefault();
if (!backgroundNone.disabled) backgroundNone.click();
} else if (event.key === "F8") {
event.preventDefault();
if (!takeIn.disabled) takeIn.click();
} else if (event.key === "Escape") {
event.preventDefault();
if (!takeOut.disabled) takeOut.click();
}
});
if (webview) {
webview.addEventListener("message", function (event) {
if (event.data && event.data.type === "state" && event.data.payload) {
localBusy = false;
render(event.data.payload);
}
});
} else {
setAllDisabled(true);
status.textContent = "WebView2 송출 브리지를 사용할 수 없습니다.";
}
}());