feat: add configurable operator appearance and layout
This commit is contained in:
@@ -13,6 +13,9 @@
|
||||
const settingsWorkspace = document.getElementById("settings-workspace");
|
||||
const marketTabs = document.getElementById("market-tabs");
|
||||
const title = document.getElementById("workspace-title");
|
||||
const shell = document.querySelector(".operator-shell");
|
||||
const splitter = document.getElementById("workspace-splitter");
|
||||
const playlistPanel = document.querySelector(".playlist-panel");
|
||||
|
||||
if (!region || !navigation || !navigationItems || !toggle || !toggleLabel ||
|
||||
!stockTab || !settingsTab || !stockWorkspace || !catalogWorkspace ||
|
||||
@@ -29,6 +32,51 @@
|
||||
let activeMarketTabId = marketTabs.querySelector("button.active[data-tab-id]")
|
||||
?.dataset.tabId || marketTabs.querySelector("button[data-tab-id]")?.dataset.tabId || null;
|
||||
let pendingKeyboardMarketTabId = null;
|
||||
let rememberWorkspaceChanges = false;
|
||||
let startPreferenceApplied = false;
|
||||
let splitterGesture = null;
|
||||
let scheduleWidthPercent = null;
|
||||
const lastWorkspaceStorageKey = "mbn-stock-webview.last-workspace.v1";
|
||||
const scheduleWidthStorageKey = "mbn-stock-webview.schedule-width.v1";
|
||||
const scheduleMinimumPercent = 34;
|
||||
const scheduleMaximumPercent = 48;
|
||||
const workspaceMinimumPixels = 780;
|
||||
const scheduleMinimumPixels = 600;
|
||||
const splitterPixels = 8;
|
||||
|
||||
function readStoredValue(key) {
|
||||
try {
|
||||
return window.localStorage && window.localStorage.getItem(key);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredValue(key, value) {
|
||||
try {
|
||||
if (window.localStorage) window.localStorage.setItem(key, value);
|
||||
} catch (_) {
|
||||
// Presentation preferences must never interrupt operator input.
|
||||
}
|
||||
}
|
||||
|
||||
function currentWorkspaceToken() {
|
||||
if (activeWorkspace !== "catalog") return activeWorkspace;
|
||||
return activeMarketTabId ? "catalog:" + activeMarketTabId : "stock";
|
||||
}
|
||||
|
||||
function rememberActiveWorkspace() {
|
||||
if (!rememberWorkspaceChanges) return;
|
||||
writeStoredValue(lastWorkspaceStorageKey, currentWorkspaceToken());
|
||||
}
|
||||
|
||||
function storedWorkspaceToken() {
|
||||
const token = readStoredValue(lastWorkspaceStorageKey);
|
||||
if (token === "stock" || token === "settings") return token;
|
||||
if (typeof token !== "string" || !token.startsWith("catalog:")) return null;
|
||||
const tabId = token.slice("catalog:".length);
|
||||
return marketButton(tabId) ? token : null;
|
||||
}
|
||||
|
||||
function marketButtons() {
|
||||
return Array.from(marketTabs.querySelectorAll("button[data-tab-id]"));
|
||||
@@ -98,9 +146,159 @@
|
||||
title.textContent = name === "catalog"
|
||||
? marketLabel(selectedMenuButton(name))
|
||||
: selected.title;
|
||||
rememberActiveWorkspace();
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyStartPreference(mode) {
|
||||
if (startPreferenceApplied) return true;
|
||||
const token = mode === "lastWorkspace" ? storedWorkspaceToken() : "stock";
|
||||
if (!token || token === "stock" || token === "settings") {
|
||||
startPreferenceApplied = true;
|
||||
return setWorkspace(token === "settings" ? "settings" : "stock");
|
||||
}
|
||||
|
||||
const tabId = token.slice("catalog:".length);
|
||||
const button = marketButton(tabId);
|
||||
if (!button || button.disabled) return false;
|
||||
startPreferenceApplied = true;
|
||||
if (activeMarketTabId === tabId) {
|
||||
syncMarket(tabId);
|
||||
return setWorkspace("catalog");
|
||||
}
|
||||
button.click();
|
||||
return true;
|
||||
}
|
||||
|
||||
function numericScheduleBounds() {
|
||||
if (!shell) {
|
||||
return { minimum: scheduleMinimumPercent, maximum: scheduleMaximumPercent };
|
||||
}
|
||||
const width = shell.getBoundingClientRect().width;
|
||||
if (!Number.isFinite(width) || width <= 0) {
|
||||
return { minimum: scheduleMinimumPercent, maximum: scheduleMaximumPercent };
|
||||
}
|
||||
const minimum = Math.max(
|
||||
scheduleMinimumPercent,
|
||||
scheduleMinimumPixels / width * 100);
|
||||
const maximum = Math.min(
|
||||
scheduleMaximumPercent,
|
||||
(width - workspaceMinimumPixels - splitterPixels) / width * 100);
|
||||
return {
|
||||
minimum: Math.min(minimum, maximum),
|
||||
maximum: Math.max(minimum, maximum)
|
||||
};
|
||||
}
|
||||
|
||||
function clampScheduleWidth(value) {
|
||||
const bounds = numericScheduleBounds();
|
||||
return Math.min(bounds.maximum, Math.max(bounds.minimum, value));
|
||||
}
|
||||
|
||||
function applyScheduleWidth(value, persist) {
|
||||
if (!shell || !splitter || !Number.isFinite(value)) return false;
|
||||
scheduleWidthPercent = clampScheduleWidth(value);
|
||||
const serialized = scheduleWidthPercent.toFixed(3) + "%";
|
||||
shell.style.setProperty("--schedule-pane-width", serialized);
|
||||
const bounds = numericScheduleBounds();
|
||||
const rounded = Math.round(scheduleWidthPercent);
|
||||
splitter.setAttribute("aria-valuemin", bounds.minimum.toFixed(1));
|
||||
splitter.setAttribute("aria-valuemax", bounds.maximum.toFixed(1));
|
||||
splitter.setAttribute("aria-valuenow", scheduleWidthPercent.toFixed(1));
|
||||
splitter.setAttribute("aria-valuetext", "송출 스케줄 " + rounded + "%");
|
||||
if (persist === true) {
|
||||
writeStoredValue(scheduleWidthStorageKey, scheduleWidthPercent.toFixed(3));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function schedulePercentFromPointer(clientX) {
|
||||
const bounds = shell.getBoundingClientRect();
|
||||
return (bounds.right - clientX) / bounds.width * 100;
|
||||
}
|
||||
|
||||
function finishSplitterGesture(restore) {
|
||||
if (!splitterGesture || !splitter) return;
|
||||
const gesture = splitterGesture;
|
||||
splitterGesture = null;
|
||||
if (splitter.hasPointerCapture?.(gesture.pointerId)) {
|
||||
splitter.releasePointerCapture(gesture.pointerId);
|
||||
}
|
||||
document.body.classList.remove("resizing-workspace");
|
||||
splitter.classList.remove("dragging");
|
||||
applyScheduleWidth(
|
||||
restore === true ? gesture.startPercent : scheduleWidthPercent,
|
||||
restore !== true);
|
||||
}
|
||||
|
||||
function initializeSplitter() {
|
||||
if (!shell || !splitter || !playlistPanel) return;
|
||||
const stored = Number.parseFloat(readStoredValue(scheduleWidthStorageKey));
|
||||
let initial = Number.isFinite(stored) ? stored : null;
|
||||
if (initial === null) {
|
||||
const shellBounds = shell.getBoundingClientRect();
|
||||
const scheduleBounds = playlistPanel.getBoundingClientRect();
|
||||
initial = shellBounds.width > 0
|
||||
? scheduleBounds.width / shellBounds.width * 100
|
||||
: 40;
|
||||
}
|
||||
applyScheduleWidth(initial, false);
|
||||
|
||||
splitter.addEventListener("pointerdown", function (event) {
|
||||
if (event.button !== 0 || event.isPrimary === false || splitterGesture) return;
|
||||
splitterGesture = {
|
||||
pointerId: event.pointerId,
|
||||
startPercent: scheduleWidthPercent
|
||||
};
|
||||
splitter.setPointerCapture?.(event.pointerId);
|
||||
splitter.classList.add("dragging");
|
||||
document.body.classList.add("resizing-workspace");
|
||||
splitter.focus({ preventScroll: true });
|
||||
event.preventDefault();
|
||||
});
|
||||
splitter.addEventListener("pointermove", function (event) {
|
||||
if (!splitterGesture || splitterGesture.pointerId !== event.pointerId ||
|
||||
(event.buttons & 1) === 0) return;
|
||||
applyScheduleWidth(schedulePercentFromPointer(event.clientX), false);
|
||||
event.preventDefault();
|
||||
});
|
||||
splitter.addEventListener("pointerup", function (event) {
|
||||
if (!splitterGesture || splitterGesture.pointerId !== event.pointerId) return;
|
||||
finishSplitterGesture(false);
|
||||
event.preventDefault();
|
||||
});
|
||||
splitter.addEventListener("pointercancel", function (event) {
|
||||
if (!splitterGesture || splitterGesture.pointerId !== event.pointerId) return;
|
||||
finishSplitterGesture(true);
|
||||
});
|
||||
splitter.addEventListener("lostpointercapture", function (event) {
|
||||
if (!splitterGesture || splitterGesture.pointerId !== event.pointerId) return;
|
||||
finishSplitterGesture(false);
|
||||
});
|
||||
splitter.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && splitterGesture) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
finishSplitterGesture(true);
|
||||
return;
|
||||
}
|
||||
let next = scheduleWidthPercent;
|
||||
if (event.key === "ArrowLeft") next += 2;
|
||||
else if (event.key === "ArrowRight") next -= 2;
|
||||
else if (event.key === "Home") next = numericScheduleBounds().minimum;
|
||||
else if (event.key === "End") next = numericScheduleBounds().maximum;
|
||||
else return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
applyScheduleWidth(next, true);
|
||||
});
|
||||
window.addEventListener("resize", function () {
|
||||
window.requestAnimationFrame(function () {
|
||||
applyScheduleWidth(scheduleWidthPercent, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncMarket(tabId, options) {
|
||||
const button = marketButton(tabId);
|
||||
if (!button) return false;
|
||||
@@ -166,8 +364,14 @@
|
||||
toggle.addEventListener("click", function () {
|
||||
setNavigationExpanded(toggle.getAttribute("aria-expanded") !== "true");
|
||||
});
|
||||
stockTab.addEventListener("click", function () { setWorkspace("stock"); });
|
||||
settingsTab.addEventListener("click", function () { setWorkspace("settings"); });
|
||||
stockTab.addEventListener("click", function () {
|
||||
startPreferenceApplied = true;
|
||||
setWorkspace("stock");
|
||||
});
|
||||
settingsTab.addEventListener("click", function () {
|
||||
startPreferenceApplied = true;
|
||||
setWorkspace("settings");
|
||||
});
|
||||
|
||||
navigation.addEventListener("keydown", function (event) {
|
||||
const menuButton = event.target.closest && event.target.closest(".workspace-nav-item");
|
||||
@@ -202,20 +406,25 @@
|
||||
marketTabs.addEventListener("click", function (event) {
|
||||
const button = event.target.closest("button[data-tab-id]");
|
||||
if (!button || !marketTabs.contains(button)) return;
|
||||
startPreferenceApplied = true;
|
||||
syncMarket(button.dataset.tabId);
|
||||
setWorkspace("catalog");
|
||||
}, true);
|
||||
|
||||
setNavigationExpanded(true);
|
||||
setWorkspace("stock");
|
||||
rememberWorkspaceChanges = true;
|
||||
initializeSplitter();
|
||||
|
||||
window.LegacyWorkspaceNavigation = Object.freeze({
|
||||
select: select,
|
||||
syncMarket: syncMarket,
|
||||
hasPendingKeyboardMarketFocus: hasPendingKeyboardMarketFocus,
|
||||
setExpanded: setNavigationExpanded,
|
||||
applyStartPreference: applyStartPreference,
|
||||
active: function () { return activeWorkspace; },
|
||||
activeMarket: function () { return activeMarketTabId; },
|
||||
expanded: function () { return toggle.getAttribute("aria-expanded") === "true"; }
|
||||
expanded: function () { return toggle.getAttribute("aria-expanded") === "true"; },
|
||||
scheduleWidth: function () { return scheduleWidthPercent; }
|
||||
});
|
||||
}());
|
||||
|
||||
Reference in New Issue
Block a user