343 lines
17 KiB
Plaintext
343 lines
17 KiB
Plaintext
@model ContinuityExplorerViewModel
|
|
@{
|
|
ViewData["Title"] = $"Continuity Explorer - {Model.Project.ProjectName}";
|
|
ViewData["ProjectSection"] = "Continuity Explorer";
|
|
}
|
|
|
|
@functions {
|
|
private static string StateBadgeClass(string state) => state switch
|
|
{
|
|
"Explicit" => "bg-success",
|
|
"Inferred" => "bg-secondary",
|
|
_ => "bg-warning text-dark"
|
|
};
|
|
|
|
private bool IsEntitySelected(string entityType) =>
|
|
Model.Filter.EntityTypes.Contains(entityType, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
|
<a asp-controller="Projects" asp-action="Index">Projects</a>
|
|
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
|
|
<span>Continuity Explorer</span>
|
|
</nav>
|
|
|
|
<partial name="_ProjectSectionNav" model="Model.Project" />
|
|
|
|
<div class="page-heading">
|
|
<div>
|
|
<p class="eyebrow">Continuity</p>
|
|
<h1>Continuity Explorer</h1>
|
|
<p class="lead-text">Investigate where characters, assets, and locations stand across the story.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<section class="card mb-3">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<strong>Filters</strong>
|
|
<button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#continuity-filter-panel" aria-expanded="true" aria-controls="continuity-filter-panel">Toggle</button>
|
|
</div>
|
|
<div class="collapse show" id="continuity-filter-panel">
|
|
<div class="card-body">
|
|
<form asp-action="ApplyFilters" method="post">
|
|
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
|
|
|
|
<div class="row g-3">
|
|
<div class="col-lg-3">
|
|
<label class="form-label">Entity types</label>
|
|
<div class="d-grid gap-1">
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="EntityTypes" value="Characters" checked="@IsEntitySelected("Characters")" />
|
|
<span class="form-check-label">Characters</span>
|
|
</label>
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="EntityTypes" value="Assets" checked="@IsEntitySelected("Assets")" />
|
|
<span class="form-check-label">Assets</span>
|
|
</label>
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="EntityTypes" value="Locations" checked="@IsEntitySelected("Locations")" />
|
|
<span class="form-check-label">Locations</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-9">
|
|
<div class="row g-2">
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="CharacterIDs">Characters</label>
|
|
<select class="form-select form-select-sm" id="CharacterIDs" name="CharacterIDs" asp-items="Model.CharacterOptions" multiple size="6"></select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="AssetIDs">Assets</label>
|
|
<select class="form-select form-select-sm" id="AssetIDs" name="AssetIDs" asp-items="Model.AssetOptions" multiple size="6"></select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="LocationIDs">Locations</label>
|
|
<select class="form-select form-select-sm" id="LocationIDs" name="LocationIDs" asp-items="Model.LocationOptions" multiple size="6"></select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-4">
|
|
<label class="form-label">Scene range</label>
|
|
<div class="d-grid gap-1">
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="radio" name="SceneRangeMode" value="EntireProject" checked="@(Model.Filter.SceneRangeMode == "EntireProject")" />
|
|
<span class="form-check-label">Entire Project</span>
|
|
</label>
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="radio" name="SceneRangeMode" value="Book" checked="@(Model.Filter.SceneRangeMode == "Book")" />
|
|
<span class="form-check-label">Book</span>
|
|
</label>
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="radio" name="SceneRangeMode" value="Chapter" checked="@(Model.Filter.SceneRangeMode == "Chapter")" />
|
|
<span class="form-check-label">Chapter</span>
|
|
</label>
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="radio" name="SceneRangeMode" value="Custom" checked="@(Model.Filter.SceneRangeMode == "Custom")" />
|
|
<span class="form-check-label">Custom Scene Range</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-8">
|
|
<div class="row g-2">
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="BookID">Book</label>
|
|
<select class="form-select form-select-sm" id="BookID" name="BookID" asp-items="Model.BookOptions">
|
|
<option value="">Any book</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="ChapterID">Chapter</label>
|
|
<select class="form-select form-select-sm" id="ChapterID" name="ChapterID" asp-items="Model.ChapterOptions">
|
|
<option value="">Any chapter</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="StartSceneID">Start scene</label>
|
|
<select class="form-select form-select-sm" id="StartSceneID" name="StartSceneID" asp-items="Model.SceneOptions">
|
|
<option value="">First scene</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="EndSceneID">End scene</label>
|
|
<select class="form-select form-select-sm" id="EndSceneID" name="EndSceneID" asp-items="Model.SceneOptions">
|
|
<option value="">Last scene</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-4">
|
|
<label class="form-label">Display mode</label>
|
|
<div class="d-grid gap-1">
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="radio" name="DisplayMode" value="StoryState" checked="@(Model.Filter.DisplayMode == "StoryState")" />
|
|
<span class="form-check-label">Story State</span>
|
|
</label>
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="radio" name="DisplayMode" value="Journey" checked="@(Model.Filter.DisplayMode == "Journey")" />
|
|
<span class="form-check-label">Journey</span>
|
|
</label>
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="radio" name="DisplayMode" value="LocationActivity" checked="@(Model.Filter.DisplayMode == "LocationActivity")" />
|
|
<span class="form-check-label">Location Activity</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-8">
|
|
<label class="form-label" for="SelectedSceneID">Story State scene</label>
|
|
<select class="form-select form-select-sm" id="SelectedSceneID" name="SelectedSceneID" asp-items="Model.SceneOptions"></select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="button-row mt-3">
|
|
<button class="btn btn-primary" type="submit">Apply filters</button>
|
|
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Reset</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
@if (Model.Filter.DisplayMode == "StoryState")
|
|
{
|
|
<section class="story-bible-section">
|
|
<h2>Story State</h2>
|
|
@if (!Model.StoryState.Characters.Any() && !Model.StoryState.Assets.Any() && !Model.StoryState.Locations.Any())
|
|
{
|
|
<p class="muted">No matching continuity information found.</p>
|
|
}
|
|
else
|
|
{
|
|
@if (Model.Filter.IncludeCharacters)
|
|
{
|
|
<h3>Characters</h3>
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm align-middle">
|
|
<thead><tr><th>Character</th><th>Current Location</th><th>State</th><th>Last Confirmed</th></tr></thead>
|
|
<tbody>
|
|
@foreach (var row in Model.StoryState.Characters)
|
|
{
|
|
<tr><td>@row.CharacterName</td><td>@row.CurrentLocation</td><td><span class="badge @StateBadgeClass(row.State)">@row.State</span></td><td>@row.LastConfirmed</td></tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
|
|
@if (Model.Filter.IncludeAssets)
|
|
{
|
|
<h3>Assets</h3>
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm align-middle">
|
|
<thead><tr><th>Asset</th><th>Holder / Location</th><th>State</th><th>Last Confirmed</th></tr></thead>
|
|
<tbody>
|
|
@foreach (var row in Model.StoryState.Assets)
|
|
{
|
|
<tr><td>@row.AssetName</td><td>@row.CurrentHolderOrLocation</td><td><span class="badge @StateBadgeClass(row.State)">@row.State</span></td><td>@row.LastConfirmed</td></tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
|
|
@if (Model.Filter.IncludeLocations)
|
|
{
|
|
<h3>Locations</h3>
|
|
@if (!Model.StoryState.Locations.Any())
|
|
{
|
|
<p class="muted">No matching location presence found.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="row g-3">
|
|
@foreach (var location in Model.StoryState.Locations)
|
|
{
|
|
<div class="col-lg-6">
|
|
<article class="story-bible-card h-100">
|
|
<h3>@location.LocationPath</h3>
|
|
<p><strong>Characters:</strong> @(location.CharactersPresent.Any() ? string.Join(", ", location.CharactersPresent) : "None")</p>
|
|
<p><strong>Assets:</strong> @(location.AssetsPresent.Any() ? string.Join(", ", location.AssetsPresent) : "None")</p>
|
|
</article>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
</section>
|
|
}
|
|
else if (Model.Filter.DisplayMode == "Journey")
|
|
{
|
|
<section class="story-bible-section">
|
|
<h2>Journey</h2>
|
|
@if (!Model.CharacterJourney.Any() && !Model.AssetJourney.Any())
|
|
{
|
|
<p class="muted">No movement recorded.</p>
|
|
}
|
|
else
|
|
{
|
|
@foreach (var group in Model.CharacterJourney.GroupBy(x => new { x.CharacterID, x.CharacterName }))
|
|
{
|
|
<h3>@group.Key.CharacterName</h3>
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm align-middle">
|
|
<thead><tr><th>Scene</th><th>Location</th></tr></thead>
|
|
<tbody>
|
|
@foreach (var row in group)
|
|
{
|
|
<tr><td>@row.SceneLabel</td><td>@row.Location</td></tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
|
|
@foreach (var group in Model.AssetJourney.GroupBy(x => new { x.StoryAssetID, x.AssetName }))
|
|
{
|
|
<h3>@group.Key.AssetName</h3>
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm align-middle">
|
|
<thead><tr><th>Scene</th><th>Holder / Location</th></tr></thead>
|
|
<tbody>
|
|
@foreach (var row in group)
|
|
{
|
|
<tr><td>@row.SceneLabel</td><td>@row.HolderOrLocation</td></tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
}
|
|
</section>
|
|
}
|
|
else
|
|
{
|
|
<section class="story-bible-section">
|
|
<h2>Location Activity</h2>
|
|
@if (!Model.LocationActivity.Any())
|
|
{
|
|
<p class="muted">No matching continuity information found.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="table-responsive">
|
|
<table class="table table-sm align-middle">
|
|
<thead><tr><th>Location</th><th>Scene</th><th>Characters Present</th><th>Assets Present</th></tr></thead>
|
|
<tbody>
|
|
@foreach (var row in Model.LocationActivity)
|
|
{
|
|
<tr><td>@row.LocationName</td><td>@row.SceneLabel</td><td>@row.CharactersPresent</td><td>@row.AssetsPresent</td></tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</section>
|
|
}
|
|
|
|
<script>
|
|
(() => {
|
|
const setDisabled = (id, disabled) => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.disabled = disabled;
|
|
}
|
|
};
|
|
|
|
const selectedRadioValue = name => {
|
|
const selected = document.querySelector(`input[name="${name}"]:checked`);
|
|
return selected ? selected.value : "";
|
|
};
|
|
|
|
const isEntityChecked = value => {
|
|
const checkbox = document.querySelector(`input[name="EntityTypes"][value="${value}"]`);
|
|
return checkbox ? checkbox.checked : false;
|
|
};
|
|
|
|
const updateContinuityFilterControls = () => {
|
|
setDisabled("CharacterIDs", !isEntityChecked("Characters"));
|
|
setDisabled("AssetIDs", !isEntityChecked("Assets"));
|
|
setDisabled("LocationIDs", !isEntityChecked("Locations"));
|
|
|
|
const sceneRangeMode = selectedRadioValue("SceneRangeMode");
|
|
setDisabled("BookID", sceneRangeMode === "EntireProject");
|
|
setDisabled("ChapterID", sceneRangeMode !== "Chapter");
|
|
setDisabled("StartSceneID", sceneRangeMode !== "Custom");
|
|
setDisabled("EndSceneID", sceneRangeMode !== "Custom");
|
|
|
|
setDisabled("SelectedSceneID", selectedRadioValue("DisplayMode") !== "StoryState");
|
|
};
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
document.querySelectorAll('input[name="EntityTypes"], input[name="SceneRangeMode"], input[name="DisplayMode"]')
|
|
.forEach(input => input.addEventListener("change", updateContinuityFilterControls));
|
|
|
|
updateContinuityFilterControls();
|
|
});
|
|
})();
|
|
</script>
|