@model SceneEditViewModel @{ var revisionStatus = Model.RevisionStatuses.FirstOrDefault(x => x.Value == Model.RevisionStatusID.ToString())?.Text ?? "Not set"; var primaryLocation = Model.LocationOptions.FirstOrDefault(x => x.Value == Model.PrimaryLocationID?.ToString())?.Text ?? "No primary location"; var povCharacter = Model.SceneCharacters.FirstOrDefault(x => x.RoleInSceneTypeName?.Contains("POV", StringComparison.OrdinalIgnoreCase) == true)?.CharacterName ?? Model.SceneCharacters.FirstOrDefault()?.CharacterName ?? "No POV"; var timeLabel = !string.IsNullOrWhiteSpace(Model.RelativeTimeText) ? Model.RelativeTimeText : Model.StartDateTime?.ToString("dd MMM yyyy HH:mm") ?? Model.StartDateTime?.ToString("dd MMM yyyy") ?? "Time not set"; var selectedPurposeLabels = Model.ScenePurposeTypes .Where(x => Model.SelectedPurposeTypeIds.Contains(x.ScenePurposeTypeID)) .Select(x => x.PurposeName) .ToList(); var dependencyCount = Model.DependenciesThisSceneNeeds.Count + Model.DependenciesNeedingThisScene.Count; var totalWarningCount = Model.Warnings.Count + Model.AgeContinuityWarnings.Count; var exactDateTimeModeId = Model.TimeModes.FirstOrDefault(x => string.Equals(x.Text, "Exact DateTime", StringComparison.OrdinalIgnoreCase))?.Value ?? string.Empty; var exactDateModeId = Model.TimeModes.FirstOrDefault(x => string.Equals(x.Text, "Exact Date", StringComparison.OrdinalIgnoreCase))?.Value ?? string.Empty; }

Working context

@(string.IsNullOrWhiteSpace(Model.SceneTitle) ? "Untitled scene" : Model.SceneTitle)

@if (Model.Chapter is not null) { Chapter @Model.Chapter.ChapterNumber } Scene @Model.SceneNumber @povCharacter @primaryLocation

@revisionStatus @timeLabel @totalWarningCount warning@(totalWarningCount == 1 ? "" : "s") @Model.SceneCharacters.Count character@(Model.SceneCharacters.Count == 1 ? "" : "s") @Model.AssetEvents.Count asset event@(Model.AssetEvents.Count == 1 ? "" : "s") @Model.ThreadEvents.Count thread event@(Model.ThreadEvents.Count == 1 ? "" : "s") @dependencyCount dependenc@(dependencyCount == 1 ? "y" : "ies")
@if (selectedPurposeLabels.Any()) {
@foreach (var purpose in selectedPurposeLabels) { @purpose }
} @if (!string.IsNullOrWhiteSpace(Model.SceneOutcomeNotes)) {

@Model.SceneOutcomeNotes

}

Timing & Structure

@if (Model.LocationConsistencyMessages.Any()) {

Location checks

    @foreach (var message in Model.LocationConsistencyMessages) {
  • @message
  • }
}

Time

Purposes

@foreach (var purpose in Model.ScenePurposeTypes) { }

Notes

Scene Metrics

@if (!Model.Metrics.Any()) {

This project has no scene metrics yet.

@if (Model.ReturnProjectID.HasValue) { Add default metrics }
} else {
@for (var index = 0; index < Model.Metrics.Count; index++) {
@Model.Metrics[index].Value
}
}
@if (Model.SceneID > 0 && Model.ReturnToTimeline) {
} @if (TempData["WriterWorkspaceError"] is string writerWorkspaceError) {
@writerWorkspaceError
}

Scene Notes

@if (!Model.Notes.Any()) {

No working notes yet. Capture drafting thoughts, research reminders, or revision questions as they come up.

} else {
@foreach (var note in Model.Notes) {
@note.TypeName @(string.IsNullOrWhiteSpace(note.NoteTitle) ? note.NoteText : note.NoteTitle) @if (note.IsPinned) { Pinned } @if (note.IsResolved) { Resolved }
}
}

Characters

@if (!Model.SceneCharacters.Any()) {

No characters attached to this scene yet. Add the people who appear, are mentioned, or meaningfully affect the scene.

} else {
@foreach (var character in Model.SceneCharacters) { var editId = $"scene-character-edit-{character.SceneCharacterID}"; var ageDisplay = CharacterAgeDisplayService.GetAgeForScene(character, Model.TimeModeName, Model.StartDateTime);
@character.CharacterName @if (!string.IsNullOrWhiteSpace(ageDisplay.DisplayText)) { @ageDisplay.Label: @ageDisplay.DisplayText } @((character.RoleInSceneTypeName ?? "Role not set")) • @((character.PresenceTypeName ?? "Presence not set")) Location: @(character.LocationName ?? "Not set") Entry: @(character.EntryLocationName ?? "Not set") Exit: @(character.ExitLocationName ?? "Not set") @if (!string.IsNullOrWhiteSpace(character.OutfitDescription) || !string.IsNullOrWhiteSpace(character.PhysicalCondition) || !string.IsNullOrWhiteSpace(character.EmotionalState)) { @character.OutfitDescription @character.PhysicalCondition @character.EmotionalState }
}
}

Add character to scene

@if (TempData["SceneCharacterAddError"] is string addCharacterError) {

@addCharacterError

}

Character Attributes

@if (Model.CharacterAttributeEvents.Any()) {
@foreach (var item in Model.CharacterAttributeEvents) {
@item.CharacterName: @item.AttributeName @item.AttributeValue @item.Description
}
} else {

Character attribute history for this scene will appear here when available.

}

Character Knowledge

@if (Model.CharacterKnowledge.Any()) {
@foreach (var item in Model.CharacterKnowledge) {
@item.CharacterName @item.KnowledgeStateName @(item.AssetName ?? item.ThreadTitle) @item.Description
}
}

Relationship Events

@if (Model.RelationshipEvents.Any()) {
@foreach (var item in Model.RelationshipEvents) {
@item.CharacterAName -> @item.CharacterBName @item.RelationshipStateName / intensity @item.Intensity @item.Description
}
}
@if (Model.RelationshipOptions.Count == 1) {
@Model.RelationshipOptions[0].Text
} else {
}

Plot Thread Events

@if (!Model.ThreadEvents.Any()) {

No plot thread events attached to this scene yet. Add clues, reveals, promises, payoffs, or reversals when the scene touches a thread.

} else {
@foreach (var threadEvent in Model.ThreadEvents) {
@threadEvent.MarkerText @threadEvent.EventTitle @threadEvent.PlotEventTypeName / @threadEvent.PlotLineName / @threadEvent.ThreadTitle
Edit
@if (TempData[$"ThreadEventEditError:{threadEvent.ThreadEventID}"] is string editThreadEventError) {

@editThreadEventError

}
Branch starts another plot line while this one continues. Split ends this plot line and starts two or more others. Merge ends this plot line by feeding into another.
}
}

Choose an existing thread, or choose a plot line to create one from the event title.

@if (TempData["ThreadEventAddError"] is string addThreadEventError) {

@addThreadEventError

}
Branch starts another plot line while this one continues. Split ends this plot line and starts two or more others. Merge ends this plot line by feeding into another.

Story Assets

@if (!Model.AssetEvents.Any()) {

No asset events attached to this scene yet. Add one when an object, clue, secret, or important item changes here.

} else {
@foreach (var assetEvent in Model.AssetEvents) {
@assetEvent.MarkerText @assetEvent.EventTitle @assetEvent.AssetName / @assetEvent.AssetEventTypeName @if (!string.IsNullOrWhiteSpace(assetEvent.ToStateName)) { State: @(assetEvent.FromStateName ?? "Any") -> @assetEvent.ToStateName }
}
}

Choose an existing asset, or enter a new asset name to create it from this scene.

Asset Locations

@if (!Model.SceneAssetLocations.Any()) {

No assets have been placed in a location for this scene yet.

} else {
@foreach (var assetLocation in Model.SceneAssetLocations) {
@assetLocation.AssetName @assetLocation.LocationName @assetLocation.Description
}
}

Asset Custody

@if (!Model.AssetCustodyEvents.Any()) {

No custody entries attached to this scene yet.

} else {
@foreach (var custody in Model.AssetCustodyEvents) {
@custody.CustodyEventTypeName @custody.AssetName @custody.CustodianSummary
}
}

Writer Workspace

Chapter workflow

Scene Checklist

@foreach (var item in Model.ChecklistItems) {
}

Attachments / Research

@if (Model.StorageUsage is not null) {

Storage used: @Model.StorageUsage.DisplayLabel

} @if (!Model.Attachments.Any()) {

No scene references yet. Add images, links, floorplans, research notes, or files that help you write this scene.

} else {
@foreach (var attachment in Model.Attachments) { var hasFile = !string.IsNullOrWhiteSpace(attachment.FilePath); var fileHref = hasFile && attachment.FilePath!.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? attachment.FilePath : hasFile && attachment.FilePath!.StartsWith("/") ? attachment.FilePath : hasFile && !System.IO.Path.IsPathRooted(attachment.FilePath!) ? "/" + attachment.FilePath!.TrimStart('/', '\\') : null; var fileExtension = hasFile ? System.IO.Path.GetExtension(attachment.FilePath!) : string.Empty; var isImageAttachment = !string.IsNullOrWhiteSpace(fileHref) && new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp" }.Contains(fileExtension, StringComparer.OrdinalIgnoreCase);
@(hasFile ? "File" : "Link") @attachment.TypeName @attachment.Title
@if (isImageAttachment) { @attachment.Title preview } else { }
@if (!string.IsNullOrWhiteSpace(attachment.Notes)) {

@attachment.Notes

} @if (hasFile) {

@System.IO.Path.GetFileName(attachment.FilePath)

}
@if (!string.IsNullOrWhiteSpace(fileHref)) { Open Download } @if (!string.IsNullOrWhiteSpace(attachment.ExternalUrl)) { Open link }
}
}
+ Add Attachment

Continuity Warnings

@if (Model.SceneID > 0 && Model.ReturnProjectID.HasValue) {
}
@if (!Model.Warnings.Any() && !Model.AgeContinuityWarnings.Any()) {

No active warnings for this scene. Run validation again after major story changes.

} else {
@foreach (var warning in Model.Warnings) {
@warning.SeverityName @warning.WarningTypeName

@warning.Message

} @foreach (var warning in Model.AgeContinuityWarnings) {
@warning.SeverityName @warning.WarningTypeName

@warning.Message

@if (!string.IsNullOrWhiteSpace(warning.Details)) {

@warning.Details

}
}
}

Scene Dependencies

This scene depends on

@if (!Model.DependenciesThisSceneNeeds.Any()) {

No dependencies yet. Add one when this scene needs another moment to happen first.

} else { @foreach (var dependency in Model.DependenciesThisSceneNeeds) {
Ch @dependency.SourceChapterNumber, Scene @dependency.SourceSceneNumber @dependency.SourceSceneTitle @dependency.SceneDependencyTypeName @(dependency.IsHardDependency ? "/ hard" : "/ soft")
} }

Scenes depending on this

@if (!Model.DependenciesNeedingThisScene.Any()) {

No dependent scenes yet. Later scenes that rely on this one will appear here.

} else { @foreach (var dependency in Model.DependenciesNeedingThisScene) {
Ch @dependency.TargetChapterNumber, Scene @dependency.TargetSceneNumber @dependency.TargetSceneTitle @dependency.SceneDependencyTypeName @(dependency.IsHardDependency ? "/ hard" : "/ soft")
} }

Move / Reorder