feat: add configurable operator appearance and layout
This commit is contained in:
@@ -983,7 +983,8 @@ public sealed partial class MainWindow
|
||||
LegacyDeleteSelectedNamedPlaylistIntent or
|
||||
LegacyChooseOperatorFolderIntent or
|
||||
LegacyResetOperatorFolderIntent or
|
||||
LegacySetOperatorNavigationExpandedIntent;
|
||||
LegacySetOperatorNavigationExpandedIntent or
|
||||
LegacySetOperatorAppearanceIntent;
|
||||
|
||||
private static bool IsFixedSectionBatchMutationIntent(LegacyUiIntent intent) => intent is
|
||||
LegacySetManualNetSellCellIntent or
|
||||
|
||||
@@ -14,20 +14,20 @@ public sealed partial class MainWindow
|
||||
private LegacyOperatorSettingsMessageKind _operatorSettingsMessageKind =
|
||||
LegacyOperatorSettingsMessageKind.Neutral;
|
||||
private LegacyOperatorFolderSnapshot _designFolderSnapshot = new(
|
||||
"앱 기본 디자인 폴더",
|
||||
"앱 기본값",
|
||||
IsCustom: false,
|
||||
IsValid: false,
|
||||
"확인 중");
|
||||
private LegacyOperatorFolderSnapshot _resourceFolderSnapshot = new(
|
||||
"앱 기본 설정 폴더",
|
||||
"앱 기본값",
|
||||
IsCustom: false,
|
||||
IsValid: false,
|
||||
"확인 중");
|
||||
private LegacyOperatorFolderSnapshot _backgroundFolderSnapshot = new(
|
||||
"장면 폴더 기준 자동 탐색",
|
||||
"자동",
|
||||
IsCustom: false,
|
||||
IsValid: true,
|
||||
"필요할 때 선택");
|
||||
"사용 가능");
|
||||
|
||||
private void InitializeOperatorSettingsStatus(string? warningMessage)
|
||||
{
|
||||
@@ -58,9 +58,17 @@ public sealed partial class MainWindow
|
||||
{
|
||||
NavigationExpanded = navigation.Expanded
|
||||
},
|
||||
navigation.Expanded
|
||||
? "왼쪽 메뉴를 펼쳐서 시작하도록 저장했습니다."
|
||||
: "왼쪽 메뉴를 접어서 시작하도록 저장했습니다.");
|
||||
"저장됨");
|
||||
break;
|
||||
case LegacySetOperatorAppearanceIntent appearance:
|
||||
SaveOperatorSettings(
|
||||
_operatorSettings with
|
||||
{
|
||||
ColorTheme = appearance.ColorTheme,
|
||||
ViewMode = appearance.ViewMode,
|
||||
StartWorkspace = appearance.StartWorkspace
|
||||
},
|
||||
"저장됨");
|
||||
break;
|
||||
default:
|
||||
SetOperatorSettingsMessage(
|
||||
@@ -90,9 +98,6 @@ public sealed partial class MainWindow
|
||||
var selected = await picker.PickSingleFolderAsync();
|
||||
if (selected is null)
|
||||
{
|
||||
SetOperatorSettingsMessage(
|
||||
"폴더 선택을 취소했습니다.",
|
||||
LegacyOperatorSettingsMessageKind.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,11 +142,7 @@ public sealed partial class MainWindow
|
||||
},
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
||||
};
|
||||
var savedMessage = kind == LegacyOperatorFolderKind.Design &&
|
||||
validated.MissingOptionalExternalAssetCount > 0
|
||||
? $"디자인 폴더를 저장했습니다. 외부 영상 {validated.MissingOptionalExternalAssetCount}개가 없어 관련 장면만 제한됩니다. 앱을 다시 시작하면 적용됩니다."
|
||||
: $"{OperatorFolderLabel(kind)} 폴더를 저장했습니다. 앱을 다시 시작하면 적용됩니다.";
|
||||
SaveOperatorSettings(next, savedMessage);
|
||||
SaveOperatorSettings(next, "저장됨 · 재시작 필요");
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -175,7 +176,7 @@ public sealed partial class MainWindow
|
||||
};
|
||||
SaveOperatorSettings(
|
||||
next,
|
||||
$"{OperatorFolderLabel(kind)} 폴더를 기본값으로 되돌렸습니다. 앱을 다시 시작하면 적용됩니다.");
|
||||
"초기화됨 · 재시작 필요");
|
||||
}
|
||||
|
||||
private void SaveOperatorSettings(
|
||||
@@ -252,31 +253,27 @@ public sealed partial class MainWindow
|
||||
_designFolderSnapshot = CreateOperatorFolderSnapshot(
|
||||
LegacyOperatorSettingsFolderKind.Scene,
|
||||
_operatorSettings.SceneDirectory,
|
||||
"앱 기본 디자인 폴더",
|
||||
"활성 장면·기본 자산 확인됨");
|
||||
"앱 기본값");
|
||||
_resourceFolderSnapshot = CreateOperatorFolderSnapshot(
|
||||
LegacyOperatorSettingsFolderKind.Resource,
|
||||
_operatorSettings.ResourceDirectory,
|
||||
"앱 기본 설정 폴더",
|
||||
"UI 설정 검증됨");
|
||||
"앱 기본값");
|
||||
_backgroundFolderSnapshot = _operatorSettings.BackgroundDirectory is null
|
||||
? new LegacyOperatorFolderSnapshot(
|
||||
"장면 폴더 기준 자동 탐색",
|
||||
"자동",
|
||||
IsCustom: false,
|
||||
IsValid: true,
|
||||
"필요할 때 선택")
|
||||
"사용 가능")
|
||||
: CreateOperatorFolderSnapshot(
|
||||
LegacyOperatorSettingsFolderKind.Background,
|
||||
_operatorSettings.BackgroundDirectory,
|
||||
"장면 폴더 기준 자동 탐색",
|
||||
"배경 폴더 확인됨");
|
||||
"자동");
|
||||
}
|
||||
|
||||
private static LegacyOperatorFolderSnapshot CreateOperatorFolderSnapshot(
|
||||
LegacyOperatorSettingsFolderKind kind,
|
||||
string? configuredPath,
|
||||
string defaultDisplay,
|
||||
string validStatus)
|
||||
string defaultDisplay)
|
||||
{
|
||||
if (configuredPath is null)
|
||||
{
|
||||
@@ -307,7 +304,7 @@ public sealed partial class MainWindow
|
||||
defaultDisplay,
|
||||
IsCustom: false,
|
||||
IsValid: true,
|
||||
FolderStatus(kind, defaultFolder, validStatus));
|
||||
"사용 가능");
|
||||
}
|
||||
|
||||
var validation = LegacyOperatorFolderValidator.Validate(kind, configuredPath);
|
||||
@@ -320,32 +317,22 @@ public sealed partial class MainWindow
|
||||
"폴더 확인 필요");
|
||||
}
|
||||
|
||||
var status = kind == LegacyOperatorSettingsFolderKind.Resource &&
|
||||
validation.Folder.DatabaseIniDetected
|
||||
? "UI·DB 설정 검증됨"
|
||||
: FolderStatus(kind, validation.Folder, validStatus);
|
||||
return new LegacyOperatorFolderSnapshot(
|
||||
validation.Folder.AbbreviatedPath,
|
||||
IsCustom: true,
|
||||
IsValid: true,
|
||||
status);
|
||||
"사용 가능");
|
||||
}
|
||||
|
||||
private static string FolderStatus(
|
||||
LegacyOperatorSettingsFolderKind kind,
|
||||
LegacyOperatorValidatedFolder folder,
|
||||
string validStatus) =>
|
||||
kind == LegacyOperatorSettingsFolderKind.Scene &&
|
||||
folder.MissingOptionalExternalAssetCount > 0
|
||||
? $"핵심 자산 확인됨 · 외부 영상 {folder.MissingOptionalExternalAssetCount}개 제외"
|
||||
: validStatus;
|
||||
|
||||
private LegacyOperatorSettingsSnapshot CreateOperatorSettingsSnapshot() => new(
|
||||
Volatile.Read(ref _operatorSettingsRevision),
|
||||
_designFolderSnapshot,
|
||||
_resourceFolderSnapshot,
|
||||
_backgroundFolderSnapshot,
|
||||
_operatorSettings.NavigationExpanded,
|
||||
_operatorSettings.ColorTheme,
|
||||
_operatorSettings.ViewMode,
|
||||
_operatorSettings.StartWorkspace,
|
||||
RestartRequired: !OperatorFolderSettingsEqual(
|
||||
_operatorSettings,
|
||||
_appliedOperatorSettings),
|
||||
@@ -408,11 +395,4 @@ public sealed partial class MainWindow
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
||||
};
|
||||
|
||||
private static string OperatorFolderLabel(LegacyOperatorFolderKind kind) => kind switch
|
||||
{
|
||||
LegacyOperatorFolderKind.Design => "디자인",
|
||||
LegacyOperatorFolderKind.Resource => "설정",
|
||||
LegacyOperatorFolderKind.Background => "운영 배경",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -831,7 +831,8 @@ public sealed partial class MainWindow : Window
|
||||
private static bool IsOperatorSettingsIntent(LegacyUiIntent intent) => intent is
|
||||
LegacyChooseOperatorFolderIntent or
|
||||
LegacyResetOperatorFolderIntent or
|
||||
LegacySetOperatorNavigationExpandedIntent;
|
||||
LegacySetOperatorNavigationExpandedIntent or
|
||||
LegacySetOperatorAppearanceIntent;
|
||||
|
||||
private static bool IsNamedPlaylistModalIntent(LegacyUiIntent intent) =>
|
||||
intent is LegacyRefreshNamedPlaylistsIntent or
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
const webview = window.chrome && window.chrome.webview;
|
||||
const searchInput = document.getElementById("stock-search");
|
||||
const searchButton = document.getElementById("stock-search-button");
|
||||
const stockSearchMeta = document.getElementById("stock-search-meta");
|
||||
const stockResults = document.getElementById("stock-results");
|
||||
const cutList = document.getElementById("cut-list");
|
||||
const cutDragFeedback = document.getElementById("cut-drag-feedback");
|
||||
@@ -24,6 +25,23 @@
|
||||
const operatorSettingsMessage = document.getElementById("operator-settings-message");
|
||||
const operatorSettingsRestart = document.getElementById("operator-settings-restart");
|
||||
const operatorNavigationExpanded = document.getElementById("operator-navigation-expanded");
|
||||
const operatorAppearanceControls = Object.freeze({
|
||||
colorTheme: Object.freeze({
|
||||
element: document.getElementById("operator-color-theme"),
|
||||
values: Object.freeze(["system", "light", "dark"]),
|
||||
fallback: "system"
|
||||
}),
|
||||
viewMode: Object.freeze({
|
||||
element: document.getElementById("operator-view-mode"),
|
||||
values: Object.freeze(["automatic", "compact", "cards"]),
|
||||
fallback: "automatic"
|
||||
}),
|
||||
startWorkspace: Object.freeze({
|
||||
element: document.getElementById("operator-start-workspace"),
|
||||
values: Object.freeze(["stockCut", "lastWorkspace"]),
|
||||
fallback: "stockCut"
|
||||
})
|
||||
});
|
||||
const operatorSettingsAppVersion = document.getElementById("operator-settings-app-version");
|
||||
const operatorSettingsPlayoutMode = document.getElementById("operator-settings-playout-mode");
|
||||
const operatorSettingsPlayoutConnection = document.getElementById(
|
||||
@@ -223,6 +241,11 @@
|
||||
let operatorCatalogSearchFocus = null;
|
||||
let operatorSettingsRenderRevision = null;
|
||||
let operatorSettingsRevisionInitialized = false;
|
||||
let operatorAppearanceSnapshot = Object.freeze({
|
||||
colorTheme: "system",
|
||||
viewMode: "automatic",
|
||||
startWorkspace: "stockCut"
|
||||
});
|
||||
let pendingOperatorSettingsFocusId = null;
|
||||
let pendingOperatorSettingsFallbackFocusId = null;
|
||||
let operatorSettingsWasBusy = false;
|
||||
@@ -1326,6 +1349,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderStockSearchMeta(state) {
|
||||
const results = Array.isArray(state.searchResults) ? state.searchResults : [];
|
||||
const selected = results.find(function (row) {
|
||||
return row.index === state.selectedStockIndex;
|
||||
});
|
||||
let label = "";
|
||||
|
||||
if (state.isBusy === true) {
|
||||
label = "검색 중";
|
||||
} else if (results.length > 0 || String(state.searchText || "").trim().length > 0) {
|
||||
label = String(results.length) + "건";
|
||||
if (selected && selected.displayName) {
|
||||
label += " · " + selected.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
stockSearchMeta.textContent = label;
|
||||
stockSearchMeta.hidden = label.length === 0;
|
||||
if (state.isBusy !== true && results.length === 0) {
|
||||
stockResults.dataset.emptyLabel = "결과 없음";
|
||||
} else {
|
||||
delete stockResults.dataset.emptyLabel;
|
||||
}
|
||||
}
|
||||
|
||||
function pointerPosition(event) {
|
||||
const bounds = cutList.getBoundingClientRect();
|
||||
return {
|
||||
@@ -1573,6 +1621,14 @@
|
||||
if (cutDragGesture && !cutList.contains(cutDragGesture.element)) {
|
||||
clearCutDragGesture();
|
||||
}
|
||||
|
||||
if (state.cutRows.length === 0) {
|
||||
cutList.dataset.emptyLabel = Number.isInteger(state.selectedStockIndex)
|
||||
? "컷 없음"
|
||||
: "종목을 선택하세요";
|
||||
} else {
|
||||
delete cutList.dataset.emptyLabel;
|
||||
}
|
||||
}
|
||||
|
||||
cutList.addEventListener("pointerdown", function (event) {
|
||||
@@ -5028,11 +5084,13 @@
|
||||
const controls = operatorFolderControls[kind];
|
||||
const current = folder && typeof folder === "object" ? folder : {};
|
||||
const defaultNames = {
|
||||
design: "앱 기본 디자인 폴더",
|
||||
resource: "앱 기본 설정 폴더",
|
||||
background: "앱 기본 배경 폴더"
|
||||
design: "앱 기본값",
|
||||
resource: "앱 기본값",
|
||||
background: "자동"
|
||||
};
|
||||
const displayName = operatorSettingsText(current.displayName, defaultNames[kind]);
|
||||
const displayName = current.isCustom === true
|
||||
? operatorSettingsText(current.displayName, defaultNames[kind])
|
||||
: defaultNames[kind];
|
||||
if (controls.name.textContent !== displayName) controls.name.textContent = displayName;
|
||||
controls.name.title = displayName;
|
||||
|
||||
@@ -5050,6 +5108,55 @@
|
||||
controls.reset.disabled = isBusy || current.isCustom !== true;
|
||||
}
|
||||
|
||||
function normalizeOperatorAppearanceValue(key, value) {
|
||||
const definition = operatorAppearanceControls[key];
|
||||
return typeof value === "string" && definition.values.includes(value)
|
||||
? value
|
||||
: definition.fallback;
|
||||
}
|
||||
|
||||
function operatorAppearanceFromSettings(settings) {
|
||||
return {
|
||||
colorTheme: normalizeOperatorAppearanceValue(
|
||||
"colorTheme",
|
||||
settings && settings.colorTheme),
|
||||
viewMode: normalizeOperatorAppearanceValue("viewMode", settings && settings.viewMode),
|
||||
startWorkspace: normalizeOperatorAppearanceValue(
|
||||
"startWorkspace",
|
||||
settings && settings.startWorkspace)
|
||||
};
|
||||
}
|
||||
|
||||
function operatorAppearanceFromControls() {
|
||||
return {
|
||||
colorTheme: normalizeOperatorAppearanceValue(
|
||||
"colorTheme",
|
||||
operatorAppearanceControls.colorTheme.element.value),
|
||||
viewMode: normalizeOperatorAppearanceValue(
|
||||
"viewMode",
|
||||
operatorAppearanceControls.viewMode.element.value),
|
||||
startWorkspace: normalizeOperatorAppearanceValue(
|
||||
"startWorkspace",
|
||||
operatorAppearanceControls.startWorkspace.element.value)
|
||||
};
|
||||
}
|
||||
|
||||
function syncOperatorAppearanceControls(appearance) {
|
||||
Object.keys(operatorAppearanceControls).forEach(function (key) {
|
||||
const control = operatorAppearanceControls[key].element;
|
||||
const nextValue = appearance[key];
|
||||
if (control.value !== nextValue) control.value = nextValue;
|
||||
});
|
||||
}
|
||||
|
||||
function applyOperatorAppearance(appearance) {
|
||||
[document.documentElement, document.body].forEach(function (element) {
|
||||
element.dataset.colorTheme = appearance.colorTheme;
|
||||
element.dataset.viewMode = appearance.viewMode;
|
||||
element.dataset.startWorkspace = appearance.startWorkspace;
|
||||
});
|
||||
}
|
||||
|
||||
function renderOperatorSettings(state) {
|
||||
const settings = state && state.operatorSettings;
|
||||
const isBusy = !settings || state.isBusy === true;
|
||||
@@ -5060,11 +5167,18 @@
|
||||
renderOperatorFolder("background", settings && settings.background, isBusy);
|
||||
|
||||
operatorNavigationExpanded.disabled = isBusy;
|
||||
Object.keys(operatorAppearanceControls).forEach(function (key) {
|
||||
operatorAppearanceControls[key].element.disabled = isBusy;
|
||||
});
|
||||
if (settings) {
|
||||
const revision = settings.revision === undefined ? null : settings.revision;
|
||||
const appearance = operatorAppearanceFromSettings(settings);
|
||||
revisionChanged = !operatorSettingsRevisionInitialized ||
|
||||
revision !== operatorSettingsRenderRevision;
|
||||
if (revisionChanged) {
|
||||
operatorAppearanceSnapshot = Object.freeze(appearance);
|
||||
syncOperatorAppearanceControls(appearance);
|
||||
applyOperatorAppearance(appearance);
|
||||
const expanded = settings.navigationExpanded !== false;
|
||||
operatorNavigationExpanded.checked = expanded;
|
||||
if (window.LegacyWorkspaceNavigation &&
|
||||
@@ -5074,6 +5188,10 @@
|
||||
operatorSettingsRenderRevision = revision;
|
||||
operatorSettingsRevisionInitialized = true;
|
||||
}
|
||||
if (window.LegacyWorkspaceNavigation &&
|
||||
typeof window.LegacyWorkspaceNavigation.applyStartPreference === "function") {
|
||||
window.LegacyWorkspaceNavigation.applyStartPreference(appearance.startWorkspace);
|
||||
}
|
||||
}
|
||||
|
||||
const message = operatorSettingsText(settings && settings.message, "");
|
||||
@@ -5147,6 +5265,7 @@
|
||||
updateDoubleClickTime(state);
|
||||
updateCutDragSize(state);
|
||||
renderStockResults(state);
|
||||
renderStockSearchMeta(state);
|
||||
renderCuts(state);
|
||||
renderTabs(state);
|
||||
movingAverage5.checked = state.movingAverages ? state.movingAverages.ma5 : true;
|
||||
@@ -5170,7 +5289,7 @@
|
||||
renderNamedPlaylist(state);
|
||||
renderDialog(state);
|
||||
syncModalFocus();
|
||||
status.textContent = isBusy ? "종목을 검색하고 있습니다." : (state.statusMessage || "");
|
||||
status.textContent = isBusy ? "처리 중" : (state.statusMessage || "");
|
||||
}
|
||||
|
||||
Object.keys(operatorFolderControls).forEach(function (kind) {
|
||||
@@ -5209,6 +5328,26 @@
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(operatorAppearanceControls).forEach(function (key) {
|
||||
const control = operatorAppearanceControls[key].element;
|
||||
control.addEventListener("change", function () {
|
||||
const appearance = operatorAppearanceFromControls();
|
||||
pendingOperatorSettingsFocusId = control.id;
|
||||
pendingOperatorSettingsFallbackFocusId = null;
|
||||
applyOperatorAppearance(appearance);
|
||||
if (!send("set-operator-appearance", {
|
||||
colorTheme: appearance.colorTheme,
|
||||
viewMode: appearance.viewMode,
|
||||
startWorkspace: appearance.startWorkspace
|
||||
})) {
|
||||
pendingOperatorSettingsFocusId = null;
|
||||
pendingOperatorSettingsFallbackFocusId = null;
|
||||
syncOperatorAppearanceControls(operatorAppearanceSnapshot);
|
||||
applyOperatorAppearance(operatorAppearanceSnapshot);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("input", function (event) {
|
||||
rememberSearchDraft(event.target);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<html lang="ko" data-color-theme="system" data-view-mode="automatic"
|
||||
data-start-workspace="stockCut">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>V-Stock 증권정보송출시스템</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<body data-color-theme="system" data-view-mode="automatic"
|
||||
data-start-workspace="stockCut">
|
||||
<main class="operator-shell" aria-label="V-Stock 증권정보송출시스템">
|
||||
<section id="workspace-region" class="workspace-region" aria-label="작업 영역">
|
||||
<aside id="workspace-navigation" class="workspace-navigation" role="navigation"
|
||||
@@ -152,15 +154,10 @@
|
||||
</svg>
|
||||
</span>
|
||||
<div class="workspace-heading-copy">
|
||||
<small>현재 작업</small>
|
||||
<h1 id="workspace-title">종목·컷</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="playout-connection-summary">
|
||||
<span class="playout-connection-copy" aria-hidden="true">
|
||||
<small>송출 연결</small>
|
||||
<strong>Tornado2</strong>
|
||||
</span>
|
||||
<div class="connection-state" id="playout-connection-state" role="status"
|
||||
aria-label="Tornado2 연결 상태" aria-live="polite"
|
||||
aria-atomic="true">미연결</div>
|
||||
@@ -180,6 +177,8 @@
|
||||
</svg>
|
||||
검색
|
||||
</button>
|
||||
<span id="stock-search-meta" class="search-meta" role="status"
|
||||
aria-live="polite" aria-atomic="true" hidden></span>
|
||||
</div>
|
||||
|
||||
<div id="stock-results" class="stock-results" role="listbox" aria-label="종목명"></div>
|
||||
@@ -204,26 +203,12 @@
|
||||
|
||||
<section id="catalog-workspace" class="catalog-panel workspace-view"
|
||||
aria-label="분류별 그래픽" hidden inert>
|
||||
<label class="expand-line"><input id="catalog-expand-all" type="checkbox" checked> Expand</label>
|
||||
<div class="category-title">Category</div>
|
||||
<label class="expand-line"><input id="catalog-expand-all" type="checkbox" checked> 전체 펼치기</label>
|
||||
<div id="category-tree" class="category-tree" aria-label="원본 카테고리"></div>
|
||||
</section>
|
||||
|
||||
<section id="settings-workspace" class="settings-panel workspace-view"
|
||||
aria-label="설정" hidden inert>
|
||||
<div class="settings-intro">
|
||||
<div class="settings-intro-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" focusable="false">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19 12a7 7 0 0 0-.1-1l2-1.5-2-3.4-2.4 1A8 8 0 0 0 15 6.2L14.7 4h-4l-.3 2.2a8 8 0 0 0-1.5.9l-2.4-1-2 3.4 2 1.5a7 7 0 0 0 0 2l-2 1.5 2 3.4 2.4-1a8 8 0 0 0 1.5.9l.3 2.2h4l.3-2.2a8 8 0 0 0 1.5-.9l2.4 1 2-3.4-2-1.5a7 7 0 0 0 .1-1Z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2>운영 설정</h2>
|
||||
<p>이 PC에서 사용할 디자인·설정·배경 폴더와 시작 화면 환경을 관리합니다. 폴더는 Windows 선택 창에서만 지정할 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="operator-settings-message" class="settings-message" role="status"
|
||||
aria-live="polite" aria-atomic="true" hidden></div>
|
||||
<div id="operator-settings-restart" class="settings-restart-banner" role="status"
|
||||
@@ -232,19 +217,14 @@
|
||||
<path d="M20 11a8 8 0 1 0-2.3 5.7M20 5v6h-6"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<strong>앱을 다시 시작하면 새 폴더가 적용됩니다.</strong>
|
||||
<span>현재 송출 작업은 그대로 유지되며, 다음 실행부터 변경된 위치를 사용합니다.</span>
|
||||
<strong>재시작하면 적용됩니다.</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-grid">
|
||||
<article class="settings-card settings-card-wide settings-folder-card">
|
||||
<div class="settings-card-heading">
|
||||
<div>
|
||||
<h3>운영 폴더</h3>
|
||||
<p>경로를 직접 입력하지 않고 폴더 선택 창으로 안전하게 지정합니다.</p>
|
||||
</div>
|
||||
<span class="settings-card-label">로컬 PC</span>
|
||||
<h3>폴더</h3>
|
||||
</div>
|
||||
|
||||
<div class="settings-folder-list">
|
||||
@@ -261,17 +241,16 @@
|
||||
<h4 id="operator-design-folder-title">디자인 폴더</h4>
|
||||
<span class="settings-folder-kind">Cuts</span>
|
||||
</div>
|
||||
<p>송출 장면(.t2s)과 장면별 디자인 자산의 기준 위치입니다.</p>
|
||||
<div class="settings-folder-value">
|
||||
<strong id="operator-design-folder-name">앱 기본 디자인 폴더</strong>
|
||||
<strong id="operator-design-folder-name">앱 기본값</strong>
|
||||
<span id="operator-design-folder-status" class="settings-status-badge neutral">확인 중</span>
|
||||
<span id="operator-design-folder-source" class="settings-folder-source">앱 기본값</span>
|
||||
<span id="operator-design-folder-source" class="settings-folder-source" hidden>앱 기본값</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-folder-actions">
|
||||
<button id="operator-design-folder-select" class="settings-primary-action"
|
||||
type="button">폴더 선택</button>
|
||||
<button id="operator-design-folder-reset" type="button" disabled>기본값</button>
|
||||
type="button">변경</button>
|
||||
<button id="operator-design-folder-reset" type="button" disabled>초기화</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -288,17 +267,16 @@
|
||||
<h4 id="operator-resource-folder-title">설정 폴더</h4>
|
||||
<span class="settings-folder-kind">Res</span>
|
||||
</div>
|
||||
<p>종목 목록 등 UI 설정과, MmoneyCoder.ini가 있으면 다음 시작부터 적용할 DB 프로필의 위치입니다.</p>
|
||||
<div class="settings-folder-value">
|
||||
<strong id="operator-resource-folder-name">앱 기본 설정 폴더</strong>
|
||||
<strong id="operator-resource-folder-name">앱 기본값</strong>
|
||||
<span id="operator-resource-folder-status" class="settings-status-badge neutral">확인 중</span>
|
||||
<span id="operator-resource-folder-source" class="settings-folder-source">앱 기본값</span>
|
||||
<span id="operator-resource-folder-source" class="settings-folder-source" hidden>앱 기본값</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-folder-actions">
|
||||
<button id="operator-resource-folder-select" class="settings-primary-action"
|
||||
type="button">폴더 선택</button>
|
||||
<button id="operator-resource-folder-reset" type="button" disabled>기본값</button>
|
||||
type="button">변경</button>
|
||||
<button id="operator-resource-folder-reset" type="button" disabled>초기화</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -312,36 +290,62 @@
|
||||
</div>
|
||||
<div class="settings-folder-copy">
|
||||
<div class="settings-folder-title-line">
|
||||
<h4 id="operator-background-folder-title">운영 배경 폴더</h4>
|
||||
<h4 id="operator-background-folder-title">배경 폴더</h4>
|
||||
<span class="settings-folder-kind">VRV/JPG/PNG</span>
|
||||
</div>
|
||||
<p>F2에서 선택할 수 있는 검증된 VRV/JPG/PNG 배경의 기준·허용 위치입니다.</p>
|
||||
<div class="settings-folder-value">
|
||||
<strong id="operator-background-folder-name">앱 기본 배경 폴더</strong>
|
||||
<strong id="operator-background-folder-name">자동</strong>
|
||||
<span id="operator-background-folder-status" class="settings-status-badge neutral">확인 중</span>
|
||||
<span id="operator-background-folder-source" class="settings-folder-source">앱 기본값</span>
|
||||
<span id="operator-background-folder-source" class="settings-folder-source" hidden>앱 기본값</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-folder-actions">
|
||||
<button id="operator-background-folder-select" class="settings-primary-action"
|
||||
type="button">폴더 선택</button>
|
||||
<button id="operator-background-folder-reset" type="button" disabled>기본값</button>
|
||||
type="button">변경</button>
|
||||
<button id="operator-background-folder-reset" type="button" disabled>초기화</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="settings-card settings-appearance-card">
|
||||
<div class="settings-card-heading">
|
||||
<h3>화면</h3>
|
||||
</div>
|
||||
<div class="settings-choice-list">
|
||||
<label class="settings-choice-row" for="operator-color-theme">
|
||||
<strong>테마</strong>
|
||||
<select id="operator-color-theme">
|
||||
<option value="system">자동</option>
|
||||
<option value="light">라이트</option>
|
||||
<option value="dark">다크</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="settings-choice-row" for="operator-view-mode">
|
||||
<strong>보기</strong>
|
||||
<select id="operator-view-mode">
|
||||
<option value="automatic">자동</option>
|
||||
<option value="compact">컴팩트</option>
|
||||
<option value="cards">카드</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="settings-choice-row" for="operator-start-workspace">
|
||||
<strong>시작</strong>
|
||||
<select id="operator-start-workspace">
|
||||
<option value="stockCut">종목·컷</option>
|
||||
<option value="lastWorkspace">마지막 화면</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="settings-card">
|
||||
<div class="settings-card-heading">
|
||||
<div>
|
||||
<h3>시작 화면</h3>
|
||||
<p>앱을 열었을 때의 작업 공간 표시 방식을 정합니다.</p>
|
||||
</div>
|
||||
<h3>메뉴</h3>
|
||||
</div>
|
||||
<label class="settings-switch-row" for="operator-navigation-expanded">
|
||||
<span>
|
||||
<strong>왼쪽 메뉴 펼쳐서 시작</strong>
|
||||
<small>끄면 아이콘만 보이는 간결한 메뉴로 시작합니다.</small>
|
||||
<strong>메뉴 펼치기</strong>
|
||||
</span>
|
||||
<input id="operator-navigation-expanded" type="checkbox" checked>
|
||||
<span class="settings-switch" aria-hidden="true"></span>
|
||||
@@ -350,11 +354,7 @@
|
||||
|
||||
<article class="settings-card settings-diagnostics-card">
|
||||
<div class="settings-card-heading">
|
||||
<div>
|
||||
<h3>현재 상태</h3>
|
||||
<p>문제 확인에 필요한 읽기 전용 정보입니다.</p>
|
||||
</div>
|
||||
<span class="settings-card-label settings-card-label-readonly">읽기 전용</span>
|
||||
<h3>상태</h3>
|
||||
</div>
|
||||
<dl class="settings-diagnostics">
|
||||
<div><dt>앱 버전</dt><dd id="operator-settings-app-version">확인 중</dd></div>
|
||||
@@ -366,24 +366,17 @@
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="settings-card settings-card-wide">
|
||||
<div class="settings-protected-copy">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<rect x="5" y="10" width="14" height="10" rx="2"></rect>
|
||||
<path d="M8 10V7a4 4 0 0 1 8 0v3M12 14v2"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3>보호되는 운영 설정</h3>
|
||||
<p>DB 자격 증명의 내용은 이 화면에 표시하거나 직접 편집할 수 없습니다. 다만 선택한 설정(Res) 폴더에 검증된 MmoneyCoder.ini가 있으면 일반 실행의 다음 시작부터 그 DB 프로필을 사용합니다. Gate A 검증 실행에서는 선택한 프로필 대신 검증 계획에 고정된 DB 구성을 사용합니다. Tornado2 송출 대상, Live/DryRun 권한과 장면 허용 목록은 변경할 수 없으며 기존 송출 안전 규칙이 계속 적용됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<div id="workspace-splitter" class="workspace-splitter" role="separator"
|
||||
aria-label="작업 영역과 송출 스케줄 너비 조절" aria-orientation="vertical"
|
||||
aria-controls="workspace-region" aria-valuemin="34" aria-valuemax="48"
|
||||
aria-valuenow="39" tabindex="0"></div>
|
||||
|
||||
<section class="playlist-panel" aria-label="플레이리스트">
|
||||
<header class="playlist-heading">
|
||||
<div class="playlist-heading-icon" aria-hidden="true">
|
||||
@@ -392,20 +385,33 @@
|
||||
<path d="M8 9h8M8 12h8M8 15h5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div><small>항상 표시</small><h2>송출 스케줄</h2></div>
|
||||
<div><h2>송출 스케줄</h2></div>
|
||||
<div class="playlist-state-legend" aria-label="스케줄 상태">
|
||||
<span class="legend-selected"><i aria-hidden="true"></i>선택</span>
|
||||
<span class="legend-next"><i aria-hidden="true"></i>다음</span>
|
||||
<span class="legend-on-air"><i aria-hidden="true"></i>ON AIR</span>
|
||||
</div>
|
||||
<div id="playlist-heading-count" class="playlist-heading-count"
|
||||
role="status" aria-live="polite" aria-atomic="true">총 0개 · 송출 0개</div>
|
||||
</header>
|
||||
<div class="playlist-toolbar">
|
||||
<label><input id="playlist-enable-all" type="checkbox" checked disabled> 전체</label>
|
||||
<label><input id="moving-average-5" type="checkbox" checked> 5일선</label>
|
||||
<label><input id="moving-average-20" type="checkbox" checked> 20일선</label>
|
||||
<button id="playlist-move-up" type="button" disabled aria-label="선택 행 위로 이동">↑</button>
|
||||
<button id="playlist-move-down" type="button" disabled aria-label="선택 행 아래로 이동">↓</button>
|
||||
<span class="toolbar-spacer"></span>
|
||||
<label><input id="playout-background-none" type="checkbox" disabled> 배경없음(F3)</label>
|
||||
<button id="playout-background-file" type="button" disabled>배경파일(F2)</button>
|
||||
<input id="playout-background-name" class="background-name" value="배경없음" readonly>
|
||||
<div class="playlist-toolbar-group" aria-label="표시">
|
||||
<label><input id="playlist-enable-all" type="checkbox" checked disabled> 전체</label>
|
||||
<label><input id="moving-average-5" type="checkbox" checked> 5일</label>
|
||||
<label><input id="moving-average-20" type="checkbox" checked> 20일</label>
|
||||
</div>
|
||||
<div class="playlist-toolbar-group playlist-toolbar-order" aria-label="순서">
|
||||
<button id="playlist-move-up" type="button" disabled aria-label="선택 행 위로 이동">↑</button>
|
||||
<button id="playlist-move-down" type="button" disabled aria-label="선택 행 아래로 이동">↓</button>
|
||||
</div>
|
||||
<div class="playlist-toolbar-group playlist-toolbar-background" aria-label="배경">
|
||||
<label><input id="playout-background-none" type="checkbox" disabled
|
||||
aria-keyshortcuts="F3"> 없음</label>
|
||||
<button id="playout-background-file" type="button" disabled
|
||||
aria-keyshortcuts="F2">배경 선택</button>
|
||||
<input id="playout-background-name" class="background-name" value="배경없음" readonly
|
||||
aria-label="선택한 배경">
|
||||
</div>
|
||||
<label class="fade-control" for="playout-fade-duration" hidden
|
||||
aria-hidden="true">DissolveTime</label>
|
||||
<select id="playout-fade-duration" disabled hidden aria-hidden="true" tabindex="-1"
|
||||
@@ -421,10 +427,15 @@
|
||||
<option value="16">17</option><option value="17">18</option>
|
||||
<option value="18">19</option><option value="19">20</option>
|
||||
</select>
|
||||
<button id="db-save-button" type="button" disabled>DB 저장</button>
|
||||
<button id="db-load-button" type="button" disabled>DB 불러오기</button>
|
||||
<button id="playlist-delete-selected" type="button" disabled>선택삭제(DEL)</button>
|
||||
<button id="playlist-delete-all" type="button" disabled>전체삭제</button>
|
||||
<div class="playlist-toolbar-group playlist-toolbar-db" aria-label="DB">
|
||||
<button id="db-load-button" type="button" disabled>불러오기</button>
|
||||
<button id="db-save-button" type="button" disabled>저장</button>
|
||||
</div>
|
||||
<div class="playlist-toolbar-group playlist-toolbar-danger" aria-label="삭제">
|
||||
<button id="playlist-delete-selected" type="button" disabled
|
||||
aria-keyshortcuts="Delete">선택 삭제</button>
|
||||
<button id="playlist-delete-all" type="button" disabled>전체 삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="playlist-drop-zone" class="playlist-grid">
|
||||
@@ -438,11 +449,11 @@
|
||||
<button id="playout-prepare" class="prepare" type="button" disabled hidden
|
||||
aria-hidden="true">PREPARE</button>
|
||||
<button id="playout-take-in" class="take-in" type="button" disabled
|
||||
aria-label="TAKE IN" aria-keyshortcuts="F8">● TAKE IN [F8]</button>
|
||||
aria-label="TAKE IN" aria-keyshortcuts="F8">● TAKE IN [F8]</button>
|
||||
<button id="playout-next" class="next" type="button" disabled
|
||||
aria-label="NEXT">▶ NEXT</button>
|
||||
<button id="playout-take-out" class="take-out" type="button" disabled
|
||||
aria-label="TAKE OUT" aria-keyshortcuts="Escape">✖ TAKE OUT[Esc]</button>
|
||||
aria-label="TAKE OUT" aria-keyshortcuts="Escape">✖ TAKE OUT [Esc]</button>
|
||||
</div>
|
||||
<div id="playout-status" class="playout-status" aria-live="polite">송출 상태를 확인하고 있습니다.</div>
|
||||
</section>
|
||||
|
||||
@@ -76,12 +76,30 @@
|
||||
playout.nextKind !== "none" && playout.nextKind !== "endOfPlaylist";
|
||||
}
|
||||
|
||||
function statusState(playout, busy) {
|
||||
if (!playout) return "error";
|
||||
if (playout.outcomeUnknown === true ||
|
||||
playout.connectionState === "outcomeUnknown") {
|
||||
return "outcome-unknown";
|
||||
}
|
||||
if (busy) return "busy";
|
||||
if (playout.mode === "disabled" ||
|
||||
playout.connectionState === "faulted" ||
|
||||
playout.connectionState === "disposed") {
|
||||
return "error";
|
||||
}
|
||||
if (playout.phase === "program") return "program";
|
||||
if (playout.phase === "prepared") return "prepared";
|
||||
return "idle";
|
||||
}
|
||||
|
||||
function render(state) {
|
||||
currentState = state || currentState;
|
||||
const playout = currentState && currentState.playout;
|
||||
const nativeBusy = !!(currentState && currentState.isBusy) ||
|
||||
!!(playout && playout.isBusy);
|
||||
const busy = localBusy || nativeBusy;
|
||||
status.dataset.state = statusState(playout, busy);
|
||||
if (!playout) {
|
||||
connection.textContent = "미연결";
|
||||
status.textContent = "송출 엔진 상태를 사용할 수 없습니다.";
|
||||
@@ -193,6 +211,7 @@
|
||||
});
|
||||
} else {
|
||||
setAllDisabled(true);
|
||||
status.dataset.state = "error";
|
||||
status.textContent = "WebView2 송출 브리지를 사용할 수 없습니다.";
|
||||
status.title = status.textContent;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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