371 lines
22 KiB
Plaintext
371 lines
22 KiB
Plaintext
@model TimelineViewModel
|
|
@{
|
|
ViewData["Title"] = "Timeline";
|
|
ViewData["ShellClass"] = "plotline-shell-wide";
|
|
var selectedBook = Model.Books.FirstOrDefault(x => x.Book.BookID == Model.SelectedBookID)?.Book;
|
|
var timelineMetricScenes = Model.Books
|
|
.SelectMany(book => book.Chapters.SelectMany(chapter => chapter.Scenes.Select(scene => new
|
|
{
|
|
sceneId = scene.SceneID,
|
|
axisLabel = $"{chapter.Chapter.ChapterNumber:g}.{scene.SceneNumber:g}",
|
|
chapterLabel = $"Chapter {chapter.Chapter.ChapterNumber:g}",
|
|
sceneNumber = scene.SceneNumber.ToString("g"),
|
|
sceneTitle = scene.SceneTitle,
|
|
bookTitle = book.Book.BookTitle,
|
|
timelineUrl = Url.Action("Index", "Timeline", new { projectId = Model.Project.ProjectID, bookId = book.Book.BookID, selectedSceneId = scene.SceneID })
|
|
})))
|
|
.ToList();
|
|
var timelineMetricShapePayload = new
|
|
{
|
|
scenes = timelineMetricScenes,
|
|
metrics = Model.MetricGraphs.Select(graph => new
|
|
{
|
|
metricName = graph.MetricName,
|
|
points = graph.Points.Select(point => new { sceneId = point.SceneID, value = point.Value })
|
|
})
|
|
};
|
|
}
|
|
|
|
<section class="context-header">
|
|
<div class="context-title">
|
|
<span class="eyebrow">Workspace context</span>
|
|
<strong>@Model.Project.ProjectName@if (selectedBook is not null) { <span> / @selectedBook.BookTitle</span> }</strong>
|
|
</div>
|
|
<nav class="context-nav" aria-label="Workspace navigation">
|
|
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Project</a>
|
|
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Timeline</a>
|
|
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Story health</a>
|
|
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Warnings</a>
|
|
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
|
|
<a asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
|
|
<a asp-controller="Locations" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
|
|
<a asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Threads</a>
|
|
</nav>
|
|
</section>
|
|
|
|
<div class="workspace-heading">
|
|
<div>
|
|
<p class="eyebrow">Story timeline</p>
|
|
<h1>@Model.Project.ProjectName</h1>
|
|
</div>
|
|
<form method="get" class="timeline-filter">
|
|
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
|
<select name="bookId" asp-items="Model.BookOptions" class="form-select">
|
|
<option value="">All books</option>
|
|
</select>
|
|
<button class="btn btn-outline-primary" type="submit">View</button>
|
|
</form>
|
|
<div class="button-row">
|
|
<form asp-controller="Warnings" asp-action="ValidateProject" method="post">
|
|
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
|
<input type="hidden" name="bookId" value="@Model.SelectedBookID" />
|
|
<button class="btn btn-outline-primary" type="submit">Validate project</button>
|
|
</form>
|
|
@if (Model.SelectedBookID.HasValue)
|
|
{
|
|
<form asp-controller="Warnings" asp-action="ValidateBook" method="post">
|
|
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
|
<input type="hidden" name="bookId" value="@Model.SelectedBookID" />
|
|
<button class="btn btn-outline-primary" type="submit">Validate book</button>
|
|
</form>
|
|
}
|
|
<a class="btn btn-outline-secondary" asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Warnings</a>
|
|
<a class="btn btn-outline-secondary" asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Story health</a>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!Model.Books.Any())
|
|
{
|
|
<section class="empty-panel">
|
|
<h2>No timeline yet</h2>
|
|
<p>Add books, chapters and scenes to see them arranged here in narrative order.</p>
|
|
</section>
|
|
}
|
|
else
|
|
{
|
|
<div class="workspace-layout">
|
|
<main class="timeline-workspace">
|
|
<section class="timeline-shell workspace-timeline">
|
|
@foreach (var book in Model.Books)
|
|
{
|
|
<div class="timeline-book">
|
|
<div class="timeline-book-row">
|
|
<div class="timeline-book-title">Book @book.Book.BookNumber: @book.Book.BookTitle</div>
|
|
<div class="timeline-chapters">
|
|
@foreach (var chapter in book.Chapters)
|
|
{
|
|
<div class="timeline-chapter" style="--chapter-scene-count:@Math.Max(chapter.Scenes.Count, 1)">
|
|
<div class="timeline-chapter-title">
|
|
<span>Chapter @chapter.Chapter.ChapterNumber</span>
|
|
<strong>@chapter.Chapter.ChapterTitle</strong>
|
|
</div>
|
|
<div class="timeline-scenes">
|
|
@if (!chapter.Scenes.Any())
|
|
{
|
|
<div class="timeline-empty-scene">No scenes</div>
|
|
}
|
|
else
|
|
{
|
|
@foreach (var scene in chapter.Scenes)
|
|
{
|
|
var selectedClass = scene.SceneID == Model.SelectedSceneID ? "selected" : "";
|
|
<a class="timeline-scene-card @selectedClass"
|
|
asp-controller="Timeline"
|
|
asp-action="Index"
|
|
asp-route-projectId="@Model.Project.ProjectID"
|
|
asp-route-bookId="@Model.SelectedBookID"
|
|
asp-route-selectedSceneId="@scene.SceneID">
|
|
<span class="scene-number">Scene @scene.SceneNumber</span>
|
|
<strong>@scene.SceneTitle</strong>
|
|
<div class="scene-badge-row">
|
|
<span class="status-pill">@scene.RevisionStatusName</span>
|
|
@if (scene.WarningCount > 0)
|
|
{
|
|
<span class="warning-placeholder active">@scene.WarningCount warning@(scene.WarningCount == 1 ? "" : "s")</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="warning-placeholder">Warnings 0</span>
|
|
}
|
|
@if (scene.DependencyCount > 0)
|
|
{
|
|
<span class="dependency-placeholder">@scene.DependencyCount dep@(scene.DependencyCount == 1 ? "" : "s")</span>
|
|
}
|
|
</div>
|
|
<span class="time-label">@scene.TimeLabel</span>
|
|
@if (!string.IsNullOrWhiteSpace(scene.PrimaryLocationName))
|
|
{
|
|
<span class="location-pill" title="@scene.PrimaryLocationPath">@scene.PrimaryLocationName</span>
|
|
}
|
|
@if (scene.PurposeLabels.Any())
|
|
{
|
|
<div class="purpose-chip-row">
|
|
@foreach (var purpose in scene.PurposeLabels.Take(3))
|
|
{
|
|
<span class="purpose-chip">@purpose.PurposeName</span>
|
|
}
|
|
</div>
|
|
}
|
|
<span class="scene-summary">@scene.Summary</span>
|
|
</a>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
@if (Model.PlotLanes.Any() && Model.OrderedScenes.Any())
|
|
{
|
|
<div class="plot-lane-board" style="--scene-count:@Model.OrderedScenes.Count">
|
|
<div class="plot-lane-board-heading">
|
|
<span>Plot line lanes</span>
|
|
<small>Thread event markers by scene</small>
|
|
</div>
|
|
@foreach (var lane in Model.PlotLanes)
|
|
{
|
|
<div class="plot-lane-row" style="--plot-colour:@lane.PlotLine.Colour">
|
|
<a class="plot-lane-label" asp-controller="PlotLines" asp-action="Edit" asp-route-id="@lane.PlotLine.PlotLineID">
|
|
<span class="colour-swatch" style="background:@lane.PlotLine.Colour"></span>
|
|
<strong>@lane.PlotLine.PlotLineName</strong>
|
|
</a>
|
|
<div class="plot-lane-slots">
|
|
@foreach (var slot in lane.SceneSlots)
|
|
{
|
|
<div class="plot-lane-slot" title="Scene @slot.Scene.SceneNumber: @slot.Scene.SceneTitle">
|
|
@foreach (var threadEvent in slot.Events)
|
|
{
|
|
var markerClass = threadEvent.EventTypeName is "Resolved" or "Payoff"
|
|
? "resolved"
|
|
: threadEvent.EventTypeName is "Contradicted"
|
|
? "contradicted"
|
|
: "";
|
|
<a class="thread-marker @markerClass"
|
|
asp-controller="Timeline"
|
|
asp-action="Index"
|
|
asp-route-projectId="@Model.Project.ProjectID"
|
|
asp-route-bookId="@Model.SelectedBookID"
|
|
asp-route-selectedSceneId="@slot.Scene.SceneID"
|
|
title="@threadEvent.ThreadTitle: @threadEvent.EventTitle">
|
|
@threadEvent.MarkerText
|
|
</a>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="future-lane-placeholder">
|
|
Add visible plot lines and thread events to see narrative lanes here.
|
|
</div>
|
|
}
|
|
@if (Model.AssetLanes.Any() && Model.OrderedScenes.Any())
|
|
{
|
|
<div class="asset-lane-board" style="--scene-count:@Model.OrderedScenes.Count">
|
|
<div class="plot-lane-board-heading">
|
|
<span>Story asset lanes</span>
|
|
<small>Asset event markers by scene</small>
|
|
</div>
|
|
@foreach (var lane in Model.AssetLanes)
|
|
{
|
|
<div class="asset-lane-row">
|
|
<a class="asset-lane-label" asp-controller="StoryAssets" asp-action="Details" asp-route-id="@lane.Asset.StoryAssetID">
|
|
<strong>@lane.Asset.AssetName</strong>
|
|
<span>@lane.Asset.KindName / @lane.Asset.Importance</span>
|
|
</a>
|
|
<div class="asset-lane-slots">
|
|
@foreach (var slot in lane.SceneSlots)
|
|
{
|
|
<div class="asset-lane-slot" title="Scene @slot.Scene.SceneNumber: @slot.Scene.SceneTitle">
|
|
@foreach (var assetEvent in slot.Events)
|
|
{
|
|
<a class="asset-marker"
|
|
asp-controller="StoryAssets"
|
|
asp-action="Details"
|
|
asp-route-id="@assetEvent.StoryAssetID"
|
|
title="@assetEvent.AssetName: @assetEvent.EventTitle">
|
|
@assetEvent.MarkerText
|
|
</a>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="future-lane-placeholder">
|
|
Add story assets and asset events to see asset lanes here.
|
|
</div>
|
|
}
|
|
@if (Model.CharacterLanes.Any() && Model.OrderedScenes.Any())
|
|
{
|
|
<div class="character-lane-board" style="--scene-count:@Model.OrderedScenes.Count">
|
|
<div class="plot-lane-board-heading">
|
|
<span>Character appearances</span>
|
|
<small>First twelve project characters</small>
|
|
</div>
|
|
@foreach (var lane in Model.CharacterLanes)
|
|
{
|
|
<div class="character-lane-row">
|
|
<a class="character-lane-label" asp-controller="Characters" asp-action="Details" asp-route-id="@lane.Character.CharacterID">
|
|
<strong>@lane.Character.CharacterName</strong>
|
|
<span>@(lane.Character.ShortName ?? "Character")</span>
|
|
</a>
|
|
<div class="character-lane-slots">
|
|
@foreach (var slot in lane.SceneSlots)
|
|
{
|
|
<div class="character-lane-slot" title="Scene @slot.Scene.SceneNumber: @slot.Scene.SceneTitle">
|
|
@foreach (var appearance in slot.Appearances)
|
|
{
|
|
<a class="character-marker"
|
|
asp-controller="Timeline"
|
|
asp-action="Index"
|
|
asp-route-projectId="@Model.Project.ProjectID"
|
|
asp-route-bookId="@Model.SelectedBookID"
|
|
asp-route-selectedSceneId="@slot.Scene.SceneID"
|
|
title="@appearance.CharacterName: @appearance.RoleInSceneTypeName / @appearance.PresenceTypeName">
|
|
@(!string.IsNullOrWhiteSpace(appearance.PresenceTypeName) ? appearance.PresenceTypeName.Substring(0, 1) : "C")
|
|
</a>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="future-lane-placeholder">
|
|
Add characters to scenes to see character appearance lanes here.
|
|
</div>
|
|
}
|
|
</section>
|
|
|
|
<section class="pacing-panel">
|
|
<div class="pacing-heading">
|
|
<div>
|
|
<p class="eyebrow">Pacing</p>
|
|
<h2>Metric shape</h2>
|
|
</div>
|
|
<span class="muted">@Model.OrderedScenes.Count scene@(Model.OrderedScenes.Count == 1 ? "" : "s")</span>
|
|
</div>
|
|
@if (!Model.MetricGraphs.Any() || !Model.OrderedScenes.Any())
|
|
{
|
|
<p class="muted">Add scenes to begin seeing emotional and pacing shape.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="metric-shape-chart" data-metric-shape-chart>
|
|
<p class="metric-shape-intro">Compare selected metrics across the same scene timeline. Click a point to focus that scene.</p>
|
|
<div class="metric-toggle-row" data-metric-toggles></div>
|
|
<div class="metric-chart-frame">
|
|
<canvas aria-label="Comparative scene metric line graph"></canvas>
|
|
<p class="muted d-none" data-chart-fallback>Metric chart could not load. The raw metric rows are still available below.</p>
|
|
</div>
|
|
<script type="application/json">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(timelineMetricShapePayload))</script>
|
|
</div>
|
|
<details class="metric-raw-details">
|
|
<summary>Show raw metric rows</summary>
|
|
<div class="metric-graph-stack mt-2">
|
|
@foreach (var graph in Model.MetricGraphs)
|
|
{
|
|
<div class="metric-graph-row">
|
|
<div class="metric-graph-label">@graph.MetricName</div>
|
|
<div class="metric-graph-points" style="--scene-count:@Math.Max(graph.Points.Count, 1)">
|
|
@foreach (var point in graph.Points)
|
|
{
|
|
<a asp-controller="Timeline"
|
|
asp-action="Index"
|
|
asp-route-projectId="@Model.Project.ProjectID"
|
|
asp-route-bookId="@Model.SelectedBookID"
|
|
asp-route-selectedSceneId="@point.SceneID"
|
|
class="metric-point"
|
|
style="--metric-height:@point.Percent%"
|
|
title="Scene @point.SceneNumber: @point.SceneTitle - @point.Value">
|
|
<span>@point.Value</span>
|
|
</a>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</details>
|
|
}
|
|
</section>
|
|
</main>
|
|
|
|
<aside class="scene-inspector-panel">
|
|
@if (Model.SelectedScene is null)
|
|
{
|
|
<div class="inspector-empty">
|
|
<p class="eyebrow">Scene inspector</p>
|
|
<h2>Select a scene</h2>
|
|
<p>Click a scene card to edit its summary, timing, purposes, outcomes and metric sliders without leaving the timeline.</p>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="inspector-header">
|
|
<p class="eyebrow">Scene inspector</p>
|
|
<h2>@Model.SelectedScene.SceneTitle</h2>
|
|
</div>
|
|
<partial name="~/Views/Scenes/_SceneInspector.cshtml" model="Model.SelectedScene" />
|
|
}
|
|
</aside>
|
|
</div>
|
|
}
|
|
|
|
@section Scripts {
|
|
<partial name="_ValidationScriptsPartial" />
|
|
}
|