561 lines
36 KiB
Plaintext
561 lines
36 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 })
|
|
})
|
|
};
|
|
var hasActiveFilter = Model.MatchingSceneIDs.Count != Model.OrderedScenes.Count || !string.IsNullOrWhiteSpace(Model.FocusLabel);
|
|
}
|
|
|
|
<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>
|
|
|
|
<section class="timeline-control-panel" data-timeline-controls>
|
|
<details class="timeline-filter-drawer" @(hasActiveFilter ? "open" : "")>
|
|
<summary>Filters and focus</summary>
|
|
<form method="get" class="timeline-filter-grid">
|
|
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
|
|
<div>
|
|
<label class="form-label">Book</label>
|
|
<select name="BookID" asp-items="Model.BookOptions" class="form-select form-select-sm">
|
|
<option value="">All books</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">From chapter</label>
|
|
<select name="ChapterFromID" asp-items="Model.ChapterFromOptions" class="form-select form-select-sm">
|
|
<option value="">Start</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">To chapter</label>
|
|
<select name="ChapterToID" asp-items="Model.ChapterToOptions" class="form-select form-select-sm">
|
|
<option value="">End</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">From scene</label>
|
|
<select name="SceneFromID" asp-items="Model.SceneFromOptions" class="form-select form-select-sm">
|
|
<option value="">Start</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">To scene</label>
|
|
<select name="SceneToID" asp-items="Model.SceneToOptions" class="form-select form-select-sm">
|
|
<option value="">End</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Plot line</label>
|
|
<select name="PlotLineID" asp-items="Model.PlotLineOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Plot thread</label>
|
|
<select name="PlotThreadID" asp-items="Model.PlotThreadOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Story asset</label>
|
|
<select name="StoryAssetID" asp-items="Model.AssetOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Character</label>
|
|
<select name="CharacterID" asp-items="Model.CharacterOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Location</label>
|
|
<select name="LocationID" asp-items="Model.LocationOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Warning severity</label>
|
|
<select name="WarningSeverityID" asp-items="Model.WarningSeverityOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Warning type</label>
|
|
<select name="WarningTypeID" asp-items="Model.WarningTypeOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Revision status</label>
|
|
<select name="RevisionStatusID" asp-items="Model.RevisionStatusOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Scene purpose</label>
|
|
<select name="ScenePurposeTypeID" asp-items="Model.ScenePurposeOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">POV character</label>
|
|
<select name="POVCharacterID" asp-items="Model.PovCharacterOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Focus type</label>
|
|
<select name="FocusType" class="form-select form-select-sm">
|
|
<option value="">No focus</option>
|
|
<option value="character" selected="@(Model.Filter.FocusType == "character")">Character</option>
|
|
<option value="asset" selected="@(Model.Filter.FocusType == "asset")">Story asset</option>
|
|
<option value="plotline" selected="@(Model.Filter.FocusType == "plotline")">Plot line</option>
|
|
<option value="plotthread" selected="@(Model.Filter.FocusType == "plotthread")">Plot thread</option>
|
|
<option value="location" selected="@(Model.Filter.FocusType == "location")">Location</option>
|
|
<option value="scene" selected="@(Model.Filter.FocusType == "scene")">Scene</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Focus ID</label>
|
|
<input name="FocusID" value="@Model.Filter.FocusID" class="form-control form-control-sm" placeholder="Entity ID" />
|
|
</div>
|
|
<div class="timeline-filter-checks">
|
|
<label><input type="checkbox" name="ShowWarningsOnly" value="true" checked="@Model.Filter.ShowWarningsOnly" /> Warnings only</label>
|
|
<label><input type="checkbox" name="IncludeDismissedWarnings" value="true" checked="@Model.Filter.IncludeDismissedWarnings" /> Include dismissed</label>
|
|
<label><input type="checkbox" name="IncludeIntentionalWarnings" value="true" checked="@Model.Filter.IncludeIntentionalWarnings" /> Include intentional</label>
|
|
</div>
|
|
<div class="timeline-filter-actions">
|
|
<button class="btn btn-outline-primary btn-sm" type="submit">Apply filters</button>
|
|
<a class="btn btn-outline-secondary btn-sm" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Clear filters</a>
|
|
</div>
|
|
</form>
|
|
</details>
|
|
|
|
<div class="timeline-visibility-panel">
|
|
<span class="eyebrow">Visibility</span>
|
|
<label><input type="checkbox" data-timeline-toggle="scene-cards" checked /> Scene cards</label>
|
|
<label><input type="checkbox" data-timeline-toggle="plot-lines" checked /> Plot lines</label>
|
|
<label><input type="checkbox" data-timeline-toggle="assets" checked /> Story assets</label>
|
|
<label><input type="checkbox" data-timeline-toggle="characters" checked /> Characters</label>
|
|
<label><input type="checkbox" data-timeline-toggle="locations" checked /> Locations</label>
|
|
<label><input type="checkbox" data-timeline-toggle="warnings" checked /> Warnings</label>
|
|
<label><input type="checkbox" data-timeline-toggle="metrics" checked /> Metrics</label>
|
|
</div>
|
|
|
|
<div class="timeline-preset-panel">
|
|
<form asp-action="SavePreset" method="post" data-timeline-preset-form>
|
|
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
|
|
<input type="hidden" name="BookID" value="@Model.SelectedBookID" />
|
|
<input type="hidden" name="VisibilityJson" value="{}" data-visibility-json />
|
|
<input type="hidden" name="Filter.ProjectID" value="@Model.Project.ProjectID" />
|
|
<input type="hidden" name="Filter.BookID" value="@Model.Filter.BookID" />
|
|
<input type="hidden" name="Filter.SelectedSceneID" value="@Model.Filter.SelectedSceneID" />
|
|
<input type="hidden" name="Filter.ChapterFromID" value="@Model.Filter.ChapterFromID" />
|
|
<input type="hidden" name="Filter.ChapterToID" value="@Model.Filter.ChapterToID" />
|
|
<input type="hidden" name="Filter.SceneFromID" value="@Model.Filter.SceneFromID" />
|
|
<input type="hidden" name="Filter.SceneToID" value="@Model.Filter.SceneToID" />
|
|
<input type="hidden" name="Filter.PlotLineID" value="@Model.Filter.PlotLineID" />
|
|
<input type="hidden" name="Filter.PlotThreadID" value="@Model.Filter.PlotThreadID" />
|
|
<input type="hidden" name="Filter.StoryAssetID" value="@Model.Filter.StoryAssetID" />
|
|
<input type="hidden" name="Filter.CharacterID" value="@Model.Filter.CharacterID" />
|
|
<input type="hidden" name="Filter.LocationID" value="@Model.Filter.LocationID" />
|
|
<input type="hidden" name="Filter.WarningSeverityID" value="@Model.Filter.WarningSeverityID" />
|
|
<input type="hidden" name="Filter.WarningTypeID" value="@Model.Filter.WarningTypeID" />
|
|
<input type="hidden" name="Filter.RevisionStatusID" value="@Model.Filter.RevisionStatusID" />
|
|
<input type="hidden" name="Filter.ScenePurposeTypeID" value="@Model.Filter.ScenePurposeTypeID" />
|
|
<input type="hidden" name="Filter.POVCharacterID" value="@Model.Filter.POVCharacterID" />
|
|
<input type="hidden" name="Filter.FocusType" value="@Model.Filter.FocusType" />
|
|
<input type="hidden" name="Filter.FocusID" value="@Model.Filter.FocusID" />
|
|
<input type="hidden" name="Filter.ShowWarningsOnly" value="@Model.Filter.ShowWarningsOnly.ToString().ToLowerInvariant()" />
|
|
<input type="hidden" name="Filter.IncludeDismissedWarnings" value="@Model.Filter.IncludeDismissedWarnings.ToString().ToLowerInvariant()" />
|
|
<input type="hidden" name="Filter.IncludeIntentionalWarnings" value="@Model.Filter.IncludeIntentionalWarnings.ToString().ToLowerInvariant()" />
|
|
<input class="form-control form-control-sm" name="PresetName" placeholder="View name" />
|
|
<button class="btn btn-outline-primary btn-sm" type="submit">Save view</button>
|
|
</form>
|
|
@if (Model.ViewPresets.Any())
|
|
{
|
|
<div class="preset-chip-row">
|
|
@foreach (var preset in Model.ViewPresets)
|
|
{
|
|
<span class="preset-chip">
|
|
<a asp-action="LoadPreset" asp-route-id="@preset.TimelineViewPresetID">@preset.PresetName</a>
|
|
<form asp-action="DeletePreset" method="post">
|
|
<input type="hidden" name="id" value="@preset.TimelineViewPresetID" />
|
|
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
|
<input type="hidden" name="bookId" value="@Model.SelectedBookID" />
|
|
<button type="submit" title="Delete view">x</button>
|
|
</form>
|
|
</span>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</section>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(Model.FocusLabel))
|
|
{
|
|
<section class="timeline-focus-banner">
|
|
<strong>Focus: @Model.FocusLabel</strong>
|
|
<span>@Model.FocusSceneIDs.Count matching scene@(Model.FocusSceneIDs.Count == 1 ? "" : "s")</span>
|
|
<a class="btn btn-outline-secondary btn-sm" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Clear focus</a>
|
|
</section>
|
|
}
|
|
|
|
@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 timeline-filtered-root" data-timeline-root>
|
|
<main class="timeline-workspace">
|
|
<section class="timeline-shell workspace-timeline" data-timeline-section="scene-cards">
|
|
@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" : "";
|
|
var matchClass = Model.MatchingSceneIDs.Contains(scene.SceneID) ? "filter-match" : "filter-dim";
|
|
var focusClass = Model.FocusSceneIDs.Contains(scene.SceneID) ? "focus-match" : (Model.FocusSceneIDs.Any() ? "focus-dim" : "");
|
|
<a class="timeline-scene-card @selectedClass @matchClass @focusClass"
|
|
data-scene-id="@scene.SceneID"
|
|
asp-controller="Timeline"
|
|
asp-action="Index"
|
|
asp-route-ProjectID="@Model.Project.ProjectID"
|
|
asp-route-BookID="@Model.SelectedBookID"
|
|
asp-route-SelectedSceneID="@scene.SceneID"
|
|
asp-route-FocusType="scene"
|
|
asp-route-FocusID="@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" data-warning-marker>@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" data-location-marker 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" data-timeline-section="plot-lines" 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)
|
|
{
|
|
var laneFocusClass = Model.Filter.FocusType == "plotline" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.PlotLine.PlotLineID ? "lane-dim" : "";
|
|
<div class="plot-lane-row @laneFocusClass" 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"
|
|
asp-route-FocusType="plotthread"
|
|
asp-route-FocusID="@threadEvent.PlotThreadID"
|
|
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" data-timeline-section="assets" 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)
|
|
{
|
|
var assetFocusClass = Model.Filter.FocusType == "asset" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.Asset.StoryAssetID ? "lane-dim" : "";
|
|
<div class="asset-lane-row @assetFocusClass">
|
|
<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="Timeline"
|
|
asp-action="Index"
|
|
asp-route-ProjectID="@Model.Project.ProjectID"
|
|
asp-route-BookID="@Model.SelectedBookID"
|
|
asp-route-SelectedSceneID="@slot.Scene.SceneID"
|
|
asp-route-FocusType="asset"
|
|
asp-route-FocusID="@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" data-timeline-section="characters" 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)
|
|
{
|
|
var characterFocusClass = Model.Filter.FocusType == "character" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.Character.CharacterID ? "lane-dim" : "";
|
|
<div class="character-lane-row @characterFocusClass">
|
|
<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"
|
|
asp-route-FocusType="character"
|
|
asp-route-FocusID="@appearance.CharacterID"
|
|
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" data-timeline-section="metrics">
|
|
<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" />
|
|
}
|