Unify browser story position follow events

This commit is contained in:
Nick Beckley 2026-08-17 14:17:29 +00:00
parent 9f0fdbc366
commit 90307757b4
6 changed files with 92 additions and 55 deletions

View File

@ -136,7 +136,19 @@ public sealed class WordCompanionController(
response.BookId,
response.ChapterId,
response.SceneId,
response.SceneUrl
response.SceneUrl,
response.Position
});
await BroadcastToCurrentUserAsync("storyPositionChanged", new
{
Source = "WordCompanion",
response.ProjectId,
response.BookId,
response.ChapterId,
response.SceneId,
CanonicalUrl = response.SceneUrl,
response.Position
});
return Ok(response);

View File

@ -245,6 +245,7 @@ public sealed class WordCompanionRuntimeCurrentSceneResponse
public int ChapterId { get; init; }
public int SceneId { get; init; }
public string SceneUrl { get; init; } = string.Empty;
public StoryPosition? Position => StoryPosition.FromRuntimeCurrentScene(this);
}
public sealed class WordCompanionRuntimeCharacterAliasDto

View File

@ -1,52 +0,0 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Runtime_CurrentScene_Validate
@UserID int,
@DocumentGuid uniqueidentifier,
@ProjectID int,
@BookID int,
@ChapterID int,
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
IF @DocumentGuid IS NULL
OR @DocumentGuid = '00000000-0000-0000-0000-000000000000'
OR @ProjectID <= 0
OR @BookID <= 0
OR @ChapterID <= 0
OR @SceneID <= 0
RETURN;
SELECT p.ProjectID AS ProjectId,
b.BookID AS BookId,
c.ChapterID AS ChapterId,
s.SceneID AS SceneId,
CONCAT(N'/Scenes/Details/', s.SceneID) AS SceneUrl
FROM dbo.Projects p
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
INNER JOIN dbo.Books b ON b.ProjectID = p.ProjectID
INNER JOIN dbo.Chapters c ON c.BookID = b.BookID
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID
INNER JOIN dbo.ManuscriptDocuments md ON md.ProjectID = p.ProjectID
AND md.BookID = b.BookID
WHERE p.ProjectID = @ProjectID
AND b.BookID = @BookID
AND c.ChapterID = @ChapterID
AND s.SceneID = @SceneID
AND md.DocumentGuid = @DocumentGuid
AND md.IsActive = 1
AND s.IsLinkedToManuscript = 1
AND s.ManuscriptDocumentID = md.ManuscriptDocumentID
AND p.IsArchived = 0
AND b.IsArchived = 0
AND c.IsArchived = 0
AND s.IsArchived = 0
AND pua.UserID = @UserID
AND pua.IsActive = 1;
END;
GO

View File

@ -1170,6 +1170,23 @@
refreshOccupancyTrays();
}
function applyStoryPosition(position) {
if (!occupancySceneSelect || !position?.sceneId) {
return;
}
const nextSceneId = String(position.sceneId);
const option = [...occupancySceneSelect.options].find(item => item.value === nextSceneId);
if (!option) {
return;
}
occupancySceneSelect.value = nextSceneId;
setFloorPlanMode("occupancy");
applyOccupancySceneFloor();
refreshOccupancyTrays();
}
function applyOccupancySceneFloor() {
const option = occupancySceneSelect?.selectedOptions?.[0];
const initialFloorId = option?.dataset.initialFloorId;
@ -2429,6 +2446,10 @@
toolboxTabs.forEach(tab => tab.addEventListener("click", () => setToolboxTab(tab.dataset.toolboxTab)));
modeButtons.forEach(button => button.addEventListener("click", () => setFloorPlanMode(button.dataset.floorPlanMode)));
occupancySceneSelect?.addEventListener("change", () => {
window.PlotDirectorStoryPosition?.publishChanged({
projectId: @Model.Project.ProjectID,
sceneId: selectedOccupancySceneId()
}, "FloorPlan");
applyOccupancySceneFloor();
refreshOccupancyTrays();
});
@ -2467,6 +2488,9 @@
});
addBlockForm?.addEventListener("submit", addBlockAsync);
editor.addEventListener("floor-plan-restore-state", event => restoreWorkspaceState(event.detail || {}));
document.addEventListener(window.PlotDirectorStoryPosition?.eventName || "plotdirector:story-position-changed", event => {
applyStoryPosition(event.detail);
});
workspace?.addEventListener("submit", async event => {
if (event.defaultPrevented) return;
const form = event.target;

View File

@ -63,6 +63,52 @@
});
})();
(() => {
const storyPositionChangedEventName = "plotdirector:story-position-changed";
const normaliseStoryPositionPayload = (payload, source) => {
const position = payload?.position || payload?.Position || payload || {};
const readNumber = (...values) => {
for (const value of values) {
const parsed = Number.parseInt(value || "", 10);
if (Number.isInteger(parsed) && parsed > 0) {
return parsed;
}
}
return null;
};
return {
source: source || payload?.source || payload?.Source || "Navigation",
projectId: readNumber(position.projectId, position.ProjectId, payload?.projectId, payload?.ProjectId),
bookId: readNumber(position.bookId, position.BookId, payload?.bookId, payload?.BookId),
chapterId: readNumber(position.chapterId, position.ChapterId, payload?.chapterId, payload?.ChapterId),
sceneId: readNumber(position.sceneId, position.SceneId, payload?.sceneId, payload?.SceneId),
canonicalUrl: payload?.canonicalUrl || payload?.CanonicalUrl || payload?.sceneUrl || payload?.SceneUrl || ""
};
};
const publishStoryPositionChanged = (payload, source) => {
const detail = normaliseStoryPositionPayload(payload, source);
if (!detail.projectId) {
return detail;
}
document.dispatchEvent(new CustomEvent(storyPositionChangedEventName, {
bubbles: true,
detail
}));
return detail;
};
window.PlotDirectorStoryPosition = {
eventName: storyPositionChangedEventName,
normalise: normaliseStoryPositionPayload,
publishChanged: publishStoryPositionChanged
};
})();
(() => {
const toggle = document.querySelector("[data-word-companion-follow-toggle]");
if (!toggle || !window.signalR) {
@ -160,7 +206,9 @@
.build();
connection.on("wordCompanionSceneChanged", (payload) => {
const sceneId = Number.parseInt(payload?.sceneId || payload?.SceneId || "", 10);
const position = window.PlotDirectorStoryPosition?.publishChanged(payload, "WordCompanion")
|| { sceneId: Number.parseInt(payload?.sceneId || payload?.SceneId || "", 10) };
const sceneId = position.sceneId;
if (!Number.isInteger(sceneId) || sceneId <= 0 || sceneId === lastSceneId) {
return;
}
@ -170,6 +218,10 @@
navigateIfAllowed(sceneUrl);
});
connection.on("storyPositionChanged", (payload) => {
window.PlotDirectorStoryPosition?.publishChanged(payload, payload?.source || payload?.Source || "WordCompanion");
});
connection.on("wordCompanionCharactersCreated", (payload) => {
const projectId = Number.parseInt(payload?.projectId || payload?.ProjectId || "", 10);
if (!Number.isInteger(projectId) || projectId <= 0 || projectId === lastCharactersProjectId) {

File diff suppressed because one or more lines are too long