222 lines
10 KiB
JavaScript
222 lines
10 KiB
JavaScript
(() => {
|
|
const root = document.querySelector("[data-writing-workspace-launcher]");
|
|
const workspace = window.PlotDirectorWritingWorkspace;
|
|
if (!root || !workspace) {
|
|
return;
|
|
}
|
|
|
|
const statusNodes = {
|
|
book: root.querySelector("[data-writing-workspace-book]"),
|
|
word: root.querySelector("[data-writing-workspace-word]"),
|
|
context: root.querySelector("[data-writing-workspace-context]"),
|
|
intent: root.querySelector("[data-writing-workspace-intent]"),
|
|
layout: root.querySelector("[data-writing-workspace-layout]"),
|
|
message: root.querySelector("[data-writing-workspace-message]")
|
|
};
|
|
const startButton = root.querySelector("[data-start-writing-workspace]");
|
|
const detectDisplaysButton = root.querySelector("[data-detect-writing-displays]");
|
|
const contextDisplaySelect = root.querySelector("[data-writing-context-display]");
|
|
const intentDisplaySelect = root.querySelector("[data-writing-intent-display]");
|
|
const contextButton = root.querySelector("[data-open-writing-context]");
|
|
const intentButton = root.querySelector("[data-open-writing-intent]");
|
|
const focusContextButton = root.querySelector("[data-focus-writing-context]");
|
|
const focusIntentButton = root.querySelector("[data-focus-writing-intent]");
|
|
const fallbackScope = workspace.scopeFrom({
|
|
projectID: root.dataset.projectId,
|
|
bookID: root.dataset.bookId
|
|
});
|
|
let activeScope = { ...fallbackScope };
|
|
let latestStatus = null;
|
|
let latestScreenInfo = null;
|
|
|
|
const text = (node, value) => {
|
|
if (node) node.textContent = value;
|
|
};
|
|
|
|
const statusText = (status, role) => {
|
|
const match = (status?.windows || []).find((windowPresence) =>
|
|
String(windowPresence.writingRole || windowPresence.WritingRole || "").toLowerCase() === role.toLowerCase()
|
|
&& (!activeScope.bookID || Number(windowPresence.bookID || windowPresence.BookID) === activeScope.bookID));
|
|
if (!match) return "Not open";
|
|
const id = match.browserWindowID || match.BrowserWindowID || "";
|
|
return id === workspace.getBrowserWindowId() ? "This window" : "Open";
|
|
};
|
|
|
|
const setMessage = (message) => text(statusNodes.message, message || "");
|
|
|
|
const selectedDisplayPreferences = () => ({
|
|
contextDisplay: contextDisplaySelect?.value || "left",
|
|
intentDisplay: intentDisplaySelect?.value || "right"
|
|
});
|
|
|
|
const displayLabel = (display, fallback) => {
|
|
if (!display) return fallback;
|
|
const position = Number.isFinite(display.left) ? `${display.left}, ${display.top}` : "";
|
|
return `${display.label || fallback}${position ? ` (${position})` : ""}`;
|
|
};
|
|
|
|
const updateDisplayOptions = (screenInfo) => {
|
|
latestScreenInfo = screenInfo;
|
|
const screens = screenInfo?.screens || [];
|
|
if (!contextDisplaySelect || !intentDisplaySelect || !screens.length) {
|
|
return;
|
|
}
|
|
|
|
const currentContext = contextDisplaySelect.value;
|
|
const currentIntent = intentDisplaySelect.value;
|
|
const baseContext = [
|
|
["left", "Left display"],
|
|
["primary", "Primary display"],
|
|
["right", "Right display"]
|
|
];
|
|
const baseIntent = [
|
|
["right", "Right display"],
|
|
["primary", "Primary display"],
|
|
["left", "Left display"]
|
|
];
|
|
const physicalOptions = screens.map((screen, index) => [
|
|
screen.key,
|
|
`${index + 1}: ${displayLabel(screen, "Display")}`
|
|
]);
|
|
const fill = (select, options, previous) => {
|
|
select.replaceChildren();
|
|
[...options, ...physicalOptions].forEach(([value, label]) => {
|
|
const option = document.createElement("option");
|
|
option.value = value;
|
|
option.textContent = label;
|
|
select.append(option);
|
|
});
|
|
if ([...select.options].some((option) => option.value === previous)) {
|
|
select.value = previous;
|
|
}
|
|
};
|
|
fill(contextDisplaySelect, baseContext, currentContext);
|
|
fill(intentDisplaySelect, baseIntent, currentIntent);
|
|
};
|
|
|
|
const render = (status = latestStatus) => {
|
|
latestStatus = status;
|
|
activeScope = workspace.statusScope(status, activeScope);
|
|
text(statusNodes.book, activeScope.bookID ? `Book ${activeScope.bookID}` : "Waiting for book");
|
|
text(statusNodes.word, status?.wordCompanionConnected || status?.WordCompanionConnected ? "Connected" : "Not connected");
|
|
text(statusNodes.context, statusText(status, "Context"));
|
|
text(statusNodes.intent, statusText(status, "Intent"));
|
|
const contextOpen = statusText(status, "Context") !== "Not open";
|
|
const intentOpen = statusText(status, "Intent") !== "Not open";
|
|
if (focusContextButton) focusContextButton.hidden = !contextOpen;
|
|
if (focusIntentButton) focusIntentButton.hidden = !intentOpen;
|
|
if (contextButton) contextButton.textContent = contextOpen ? "Focus Context" : "Open Context";
|
|
if (intentButton) intentButton.textContent = intentOpen ? "Focus Intent" : "Open Intent";
|
|
};
|
|
|
|
const refresh = async () => {
|
|
try {
|
|
render(await workspace.watchWorkspace(activeScope));
|
|
} catch (error) {
|
|
setMessage("Writing workspace presence is unavailable.");
|
|
console.debug("Writing workspace presence unavailable.", error);
|
|
}
|
|
};
|
|
|
|
const openOrFocus = async (role) => {
|
|
const scope = workspace.statusScope(latestStatus, activeScope);
|
|
if (!scope.bookID) {
|
|
setMessage("Open a linked Word manuscript or focus a book before opening writing windows.");
|
|
return;
|
|
}
|
|
|
|
const screenInfo = latestScreenInfo || await workspace.getScreenLayout(false, selectedDisplayPreferences());
|
|
updateDisplayOptions(screenInfo);
|
|
const targetDisplay = role === workspace.roles.context
|
|
? screenInfo.layout?.contextDisplay
|
|
: screenInfo.layout?.intentDisplay;
|
|
await workspace.focusRole(role, scope, targetDisplay);
|
|
setMessage(`${role} requested on ${displayLabel(targetDisplay, "the selected display")}. If it is behind Word, use the taskbar once; future clicks should reuse it.`);
|
|
await refresh();
|
|
};
|
|
|
|
detectDisplaysButton?.addEventListener("click", async () => {
|
|
detectDisplaysButton.disabled = true;
|
|
setMessage("Detecting displays...");
|
|
try {
|
|
const screenInfo = await workspace.getScreenLayout(true, selectedDisplayPreferences());
|
|
updateDisplayOptions(screenInfo);
|
|
const count = screenInfo.screens?.length || 0;
|
|
text(statusNodes.layout, screenInfo.layout?.mode || "single-screen");
|
|
if (count > 0) {
|
|
setMessage(`Detected ${count} display${count === 1 ? "" : "s"}. Choose target displays, then start or reapply the workspace.`);
|
|
} else if (screenInfo.permission === "denied") {
|
|
setMessage("Display permission was denied. PlotDirector will reuse named windows, but your browser may open them on the current monitor.");
|
|
} else {
|
|
setMessage("Display detection is unavailable in this browser. PlotDirector will reuse named windows without monitor placement.");
|
|
}
|
|
} catch (error) {
|
|
console.debug("Display detection failed.", error);
|
|
setMessage("Display detection failed. The workspace can still open using named windows.");
|
|
} finally {
|
|
detectDisplaysButton.disabled = false;
|
|
}
|
|
});
|
|
|
|
startButton?.addEventListener("click", async () => {
|
|
startButton.disabled = true;
|
|
setMessage("Starting writing workspace...");
|
|
try {
|
|
const result = await workspace.startWorkspace(activeScope, {
|
|
requestScreens: true,
|
|
displayPreferences: selectedDisplayPreferences()
|
|
});
|
|
updateDisplayOptions(result.screenInfo);
|
|
if (!result.ok) {
|
|
setMessage(result.message || `Pop-up blocked for ${result.blocked?.join(", ") || "a writing window"}. Use the explicit open buttons.`);
|
|
} else {
|
|
const contextDisplay = displayLabel(result.screenInfo?.layout?.contextDisplay, "selected display");
|
|
const intentDisplay = displayLabel(result.screenInfo?.layout?.intentDisplay, "selected display");
|
|
setMessage(`Workspace requested: Context on ${contextDisplay}, Intent on ${intentDisplay}. If your browser ignores placement, use Focus instead of opening more copies.`);
|
|
}
|
|
text(statusNodes.layout, result.screenInfo?.layout?.mode || "single-screen");
|
|
await refresh();
|
|
} catch (error) {
|
|
console.debug("Unable to start writing workspace.", error);
|
|
setMessage("Writing workspace could not be started. Use the direct open buttons.");
|
|
} finally {
|
|
startButton.disabled = false;
|
|
}
|
|
});
|
|
|
|
contextButton?.addEventListener("click", () => openOrFocus(workspace.roles.context));
|
|
intentButton?.addEventListener("click", () => openOrFocus(workspace.roles.intent));
|
|
focusContextButton?.addEventListener("click", () => openOrFocus(workspace.roles.context));
|
|
focusIntentButton?.addEventListener("click", () => openOrFocus(workspace.roles.intent));
|
|
contextDisplaySelect?.addEventListener("change", () => {
|
|
setMessage("Context display preference updated. Click Start / Reapply Workspace to use it.");
|
|
});
|
|
intentDisplaySelect?.addEventListener("change", () => {
|
|
setMessage("Intent display preference updated. Click Start / Reapply Workspace to use it.");
|
|
});
|
|
|
|
document.addEventListener("plotdirector:writing-workspace-status", (event) => render(event.detail));
|
|
document.addEventListener(window.PlotDirectorStoryPosition?.eventName || "plotdirector:story-position-changed", (event) => {
|
|
const position = window.PlotDirectorStoryPosition?.normalise?.(event.detail, event.detail?.source || event.detail?.Source) || event.detail;
|
|
if (position?.projectId && position?.bookId) {
|
|
activeScope = { projectID: position.projectId, bookID: position.bookId };
|
|
void workspace.registerWindow({
|
|
...activeScope,
|
|
writingRole: workspace.roles.launcher,
|
|
displayLabel: "Writing Workspace"
|
|
}).then(render).catch(() => null);
|
|
}
|
|
});
|
|
|
|
void workspace.registerWindow({
|
|
...activeScope,
|
|
writingRole: workspace.roles.launcher,
|
|
displayLabel: "Writing Workspace"
|
|
}).then(async (status) => {
|
|
render(status);
|
|
const screenInfo = await workspace.getScreenLayout(false, selectedDisplayPreferences());
|
|
updateDisplayOptions(screenInfo);
|
|
text(statusNodes.layout, screenInfo.layout?.mode || "adaptive");
|
|
}).catch(() => null);
|
|
})();
|