PlotDirector Phase 17B - Scene Core Focused Editors
This commit is contained in:
parent
3d217d72c2
commit
4ae45ab8f9
@ -1,5 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
using PlotLine.Services;
|
using PlotLine.Services;
|
||||||
using PlotLine.ViewModels;
|
using PlotLine.ViewModels;
|
||||||
|
|
||||||
@ -14,7 +17,8 @@ public sealed class ScenesController(
|
|||||||
ILocationService locations,
|
ILocationService locations,
|
||||||
IWriterWorkspaceService writerWorkspace,
|
IWriterWorkspaceService writerWorkspace,
|
||||||
IStorageService storage,
|
IStorageService storage,
|
||||||
IUploadStorageService uploadStorage) : Controller
|
IUploadStorageService uploadStorage,
|
||||||
|
ICompositeViewEngine viewEngine) : Controller
|
||||||
{
|
{
|
||||||
private const long MaxAttachmentBytes = 25 * 1024 * 1024;
|
private const long MaxAttachmentBytes = 25 * 1024 * 1024;
|
||||||
|
|
||||||
@ -84,6 +88,160 @@ public sealed class ScenesController(
|
|||||||
return RedirectToAction(nameof(Edit), new { id = sceneId });
|
return RedirectToAction(nameof(Edit), new { id = sceneId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> SceneDetailsEditor(int sceneId)
|
||||||
|
{
|
||||||
|
var model = await scenes.GetEditAsync(sceneId);
|
||||||
|
return model is null
|
||||||
|
? NotFound()
|
||||||
|
: PartialView("~/Views/Shared/SceneInspectorV2/_SceneDetailsEditor.cshtml", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> ScenePurposeEditor(int sceneId)
|
||||||
|
{
|
||||||
|
var model = await scenes.GetEditAsync(sceneId);
|
||||||
|
return model is null
|
||||||
|
? NotFound()
|
||||||
|
: PartialView("~/Views/Shared/SceneInspectorV2/_ScenePurposeEditor.cshtml", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> SceneMetricsEditor(int sceneId)
|
||||||
|
{
|
||||||
|
var model = await scenes.GetEditAsync(sceneId);
|
||||||
|
return model is null
|
||||||
|
? NotFound()
|
||||||
|
: PartialView("~/Views/Shared/SceneInspectorV2/_SceneMetricsEditor.cshtml", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> SaveSceneDetails(SceneDetailsSaveViewModel model)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(new { success = false, message = FirstModelStateError() });
|
||||||
|
}
|
||||||
|
|
||||||
|
var current = await scenes.GetEditAsync(model.SceneID);
|
||||||
|
if (current is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
current.SceneNumber = model.SceneNumber;
|
||||||
|
current.SceneTitle = model.SceneTitle;
|
||||||
|
current.RevisionStatusID = model.RevisionStatusID;
|
||||||
|
current.PrimaryLocationID = model.PrimaryLocationID;
|
||||||
|
current.Summary = model.Summary;
|
||||||
|
current.TimeModeID = model.TimeModeID;
|
||||||
|
current.TimeConfidenceID = model.TimeConfidenceID;
|
||||||
|
current.StartDateTime = model.StartDateTime;
|
||||||
|
current.EndDateTime = model.EndDateTime;
|
||||||
|
current.DurationAmount = model.DurationAmount;
|
||||||
|
current.DurationUnitID = model.DurationUnitID;
|
||||||
|
current.RelativeTimeText = model.RelativeTimeText;
|
||||||
|
|
||||||
|
await scenes.SaveAsync(current);
|
||||||
|
await scenes.UpdatePovCharacterAsync(model.SceneID, model.POVCharacterID);
|
||||||
|
|
||||||
|
var updated = await scenes.GetEditAsync(model.SceneID);
|
||||||
|
if (updated is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
html = new
|
||||||
|
{
|
||||||
|
health = await RenderPartialViewToStringAsync("~/Views/Shared/SceneInspectorV2/_SceneInspectorHealthStrip.cshtml", updated),
|
||||||
|
overview = await RenderPartialViewToStringAsync("~/Views/Shared/SceneInspectorV2/_SceneOverviewCard.cshtml", updated)
|
||||||
|
},
|
||||||
|
scene = new
|
||||||
|
{
|
||||||
|
updated.SceneID,
|
||||||
|
updated.SceneTitle,
|
||||||
|
updated.SceneNumber,
|
||||||
|
RevisionStatusName = updated.RevisionStatuses.FirstOrDefault(x => x.Value == updated.RevisionStatusID.ToString())?.Text,
|
||||||
|
PovLabel = updated.CharacterOptions.FirstOrDefault(x => x.Value == updated.POVCharacterID?.ToString())?.Text ?? "No POV",
|
||||||
|
PrimaryLocationName = updated.LocationOptions.FirstOrDefault(x => x.Value == updated.PrimaryLocationID?.ToString())?.Text?.Trim()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> SaveScenePurpose(ScenePurposeSaveViewModel model)
|
||||||
|
{
|
||||||
|
var current = await scenes.GetEditAsync(model.SceneID);
|
||||||
|
if (current is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
current.SelectedPurposeTypeIds = model.SelectedPurposeTypeIds;
|
||||||
|
current.ScenePurposeNotes = model.ScenePurposeNotes;
|
||||||
|
current.SceneOutcomeNotes = model.SceneOutcomeNotes;
|
||||||
|
|
||||||
|
await scenes.SaveAsync(current);
|
||||||
|
|
||||||
|
var updated = await scenes.GetEditAsync(model.SceneID);
|
||||||
|
if (updated is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
html = new
|
||||||
|
{
|
||||||
|
story = await RenderPartialViewToStringAsync("~/Views/Shared/SceneInspectorV2/_SceneStoryCard.cshtml", updated)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> SaveSceneMetrics(SceneMetricsSaveViewModel model)
|
||||||
|
{
|
||||||
|
var current = await scenes.GetEditAsync(model.SceneID);
|
||||||
|
if (current is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var metric in current.Metrics)
|
||||||
|
{
|
||||||
|
var postedMetric = model.Metrics.FirstOrDefault(x => x.MetricTypeID == metric.MetricTypeID);
|
||||||
|
if (postedMetric is not null)
|
||||||
|
{
|
||||||
|
metric.Value = postedMetric.Value;
|
||||||
|
metric.Notes = postedMetric.Notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await scenes.SaveAsync(current);
|
||||||
|
|
||||||
|
var updated = await scenes.GetEditAsync(model.SceneID);
|
||||||
|
if (updated is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
html = new
|
||||||
|
{
|
||||||
|
story = await RenderPartialViewToStringAsync("~/Views/Shared/SceneInspectorV2/_SceneStoryCard.cshtml", updated)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> Archive(int id, int chapterId)
|
public async Task<IActionResult> Archive(int id, int chapterId)
|
||||||
@ -713,6 +871,43 @@ public sealed class ScenesController(
|
|||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string FirstModelStateError() =>
|
||||||
|
ModelState.Values
|
||||||
|
.SelectMany(x => x.Errors)
|
||||||
|
.Select(x => x.ErrorMessage)
|
||||||
|
.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
?? "Please check the highlighted fields and try again.";
|
||||||
|
|
||||||
|
private async Task<string> RenderPartialViewToStringAsync(string viewName, object model)
|
||||||
|
{
|
||||||
|
var viewResult = viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: false);
|
||||||
|
if (!viewResult.Success)
|
||||||
|
{
|
||||||
|
viewResult = viewEngine.FindView(ControllerContext, viewName, isMainPage: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!viewResult.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Partial view '{viewName}' was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var writer = new StringWriter();
|
||||||
|
var viewData = new ViewDataDictionary(ViewData)
|
||||||
|
{
|
||||||
|
Model = model
|
||||||
|
};
|
||||||
|
var viewContext = new ViewContext(
|
||||||
|
ControllerContext,
|
||||||
|
viewResult.View,
|
||||||
|
viewData,
|
||||||
|
TempData,
|
||||||
|
writer,
|
||||||
|
new HtmlHelperOptions());
|
||||||
|
|
||||||
|
await viewResult.View.RenderAsync(viewContext);
|
||||||
|
return writer.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<string> SaveUploadedAttachmentAsync(int sceneId, IFormFile uploadedFile)
|
private async Task<string> SaveUploadedAttachmentAsync(int sceneId, IFormFile uploadedFile)
|
||||||
{
|
{
|
||||||
var uploadRoot = uploadStorage.GetDirectory("scenes", sceneId.ToString());
|
var uploadRoot = uploadStorage.GetDirectory("scenes", sceneId.ToString());
|
||||||
|
|||||||
@ -84,6 +84,7 @@ public interface ISceneRepository
|
|||||||
Task<IReadOnlyList<Scene>> ListByFloorPlanAsync(int floorPlanId);
|
Task<IReadOnlyList<Scene>> ListByFloorPlanAsync(int floorPlanId);
|
||||||
Task<Scene?> GetAsync(int sceneId);
|
Task<Scene?> GetAsync(int sceneId);
|
||||||
Task<int> SaveAsync(Scene scene);
|
Task<int> SaveAsync(Scene scene);
|
||||||
|
Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId);
|
||||||
Task SaveFloorPlanLinkAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId);
|
Task SaveFloorPlanLinkAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId);
|
||||||
Task SavePurposesAsync(int sceneId, IEnumerable<int> purposeIds);
|
Task SavePurposesAsync(int sceneId, IEnumerable<int> purposeIds);
|
||||||
Task SaveMetricValuesAsync(int sceneId, IEnumerable<SceneMetricValue> metricValues);
|
Task SaveMetricValuesAsync(int sceneId, IEnumerable<SceneMetricValue> metricValues);
|
||||||
@ -2474,6 +2475,15 @@ public sealed class SceneRepository(ISqlConnectionFactory connectionFactory) : I
|
|||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
"dbo.Scene_UpdatePovCharacter",
|
||||||
|
new { SceneID = sceneId, POVCharacterID = povCharacterId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SaveFloorPlanLinkAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId)
|
public async Task SaveFloorPlanLinkAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
|||||||
@ -121,6 +121,7 @@ public interface ISceneService
|
|||||||
Task<SceneEditViewModel?> GetCreateAsync(int chapterId);
|
Task<SceneEditViewModel?> GetCreateAsync(int chapterId);
|
||||||
Task<SceneEditViewModel?> GetEditAsync(int sceneId);
|
Task<SceneEditViewModel?> GetEditAsync(int sceneId);
|
||||||
Task<int> SaveAsync(SceneEditViewModel model);
|
Task<int> SaveAsync(SceneEditViewModel model);
|
||||||
|
Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId);
|
||||||
Task<SceneOccupancyEditorViewModel?> GetOccupancyEditorAsync(int sceneId, int? floorPlanId = null);
|
Task<SceneOccupancyEditorViewModel?> GetOccupancyEditorAsync(int sceneId, int? floorPlanId = null);
|
||||||
Task SaveOccupancyAsync(SceneOccupancySaveViewModel model);
|
Task SaveOccupancyAsync(SceneOccupancySaveViewModel model);
|
||||||
Task ArchiveAsync(int sceneId);
|
Task ArchiveAsync(int sceneId);
|
||||||
@ -1517,6 +1518,7 @@ public sealed class SceneService(
|
|||||||
SceneNumber = scene.SceneNumber,
|
SceneNumber = scene.SceneNumber,
|
||||||
SceneTitle = scene.SceneTitle,
|
SceneTitle = scene.SceneTitle,
|
||||||
Summary = scene.Summary,
|
Summary = scene.Summary,
|
||||||
|
POVCharacterID = scene.POVCharacterID,
|
||||||
TimeModeID = scene.TimeModeID,
|
TimeModeID = scene.TimeModeID,
|
||||||
TimeModeName = scene.TimeModeName,
|
TimeModeName = scene.TimeModeName,
|
||||||
StartDateTime = scene.StartDateTime,
|
StartDateTime = scene.StartDateTime,
|
||||||
@ -1601,6 +1603,9 @@ public sealed class SceneService(
|
|||||||
return sceneId;
|
return sceneId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId) =>
|
||||||
|
scenes.UpdatePovCharacterAsync(sceneId, povCharacterId);
|
||||||
|
|
||||||
public async Task ArchiveAsync(int sceneId)
|
public async Task ArchiveAsync(int sceneId)
|
||||||
{
|
{
|
||||||
var scene = await scenes.GetAsync(sceneId);
|
var scene = await scenes.GetAsync(sceneId);
|
||||||
|
|||||||
@ -285,6 +285,9 @@ public sealed class SceneEditViewModel
|
|||||||
|
|
||||||
public string? Summary { get; set; }
|
public string? Summary { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "POV character")]
|
||||||
|
public int? POVCharacterID { get; set; }
|
||||||
|
|
||||||
[Display(Name = "Time mode")]
|
[Display(Name = "Time mode")]
|
||||||
public int TimeModeID { get; set; }
|
public int TimeModeID { get; set; }
|
||||||
|
|
||||||
@ -411,6 +414,69 @@ public sealed class SceneEditViewModel
|
|||||||
public IReadOnlyList<ScenePurposeType> ScenePurposeTypes { get; set; } = [];
|
public IReadOnlyList<ScenePurposeType> ScenePurposeTypes { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class SceneDetailsSaveViewModel
|
||||||
|
{
|
||||||
|
public int SceneID { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Scene number")]
|
||||||
|
public decimal SceneNumber { get; set; }
|
||||||
|
|
||||||
|
[Required, StringLength(200)]
|
||||||
|
[Display(Name = "Scene title")]
|
||||||
|
public string SceneTitle { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Display(Name = "Revision status")]
|
||||||
|
public int RevisionStatusID { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "POV character")]
|
||||||
|
public int? POVCharacterID { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Primary location")]
|
||||||
|
public int? PrimaryLocationID { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Synopsis")]
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Time mode")]
|
||||||
|
public int TimeModeID { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Time confidence")]
|
||||||
|
public int TimeConfidenceID { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Start date/time")]
|
||||||
|
public DateTime? StartDateTime { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "End date/time")]
|
||||||
|
public DateTime? EndDateTime { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Duration")]
|
||||||
|
public decimal? DurationAmount { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Duration unit")]
|
||||||
|
public int? DurationUnitID { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Relative time")]
|
||||||
|
public string? RelativeTimeText { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ScenePurposeSaveViewModel
|
||||||
|
{
|
||||||
|
public int SceneID { get; set; }
|
||||||
|
public List<int> SelectedPurposeTypeIds { get; set; } = [];
|
||||||
|
|
||||||
|
[Display(Name = "Purpose notes")]
|
||||||
|
public string? ScenePurposeNotes { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Outcome notes")]
|
||||||
|
public string? SceneOutcomeNotes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SceneMetricsSaveViewModel
|
||||||
|
{
|
||||||
|
public int SceneID { get; set; }
|
||||||
|
public List<SceneMetricInputViewModel> Metrics { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class SceneOccupancyEditorViewModel
|
public sealed class SceneOccupancyEditorViewModel
|
||||||
{
|
{
|
||||||
public int SceneID { get; set; }
|
public int SceneID { get; set; }
|
||||||
|
|||||||
@ -0,0 +1,96 @@
|
|||||||
|
@model SceneEditViewModel
|
||||||
|
@{
|
||||||
|
var startValue = Model.StartDateTime?.ToString("yyyy-MM-ddTHH:mm");
|
||||||
|
var endValue = Model.EndDateTime?.ToString("yyyy-MM-ddTHH:mm");
|
||||||
|
}
|
||||||
|
|
||||||
|
<form asp-controller="Scenes" asp-action="SaveSceneDetails" method="post" class="scene-inspector-editor-form" data-scene-editor-form>
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" asp-for="SceneID" />
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-error" data-scene-editor-error hidden></div>
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-fields">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="SceneTitle"></label>
|
||||||
|
<input asp-for="SceneTitle" class="form-control" />
|
||||||
|
<span asp-validation-for="SceneTitle" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="SceneNumber"></label>
|
||||||
|
<input asp-for="SceneNumber" class="form-control" step="0.01" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="RevisionStatusID"></label>
|
||||||
|
<select asp-for="RevisionStatusID" asp-items="Model.RevisionStatuses" class="form-control"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="POVCharacterID"></label>
|
||||||
|
<select asp-for="POVCharacterID" asp-items="Model.CharacterOptions" class="form-control">
|
||||||
|
<option value="">No POV character</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="PrimaryLocationID"></label>
|
||||||
|
<select asp-for="PrimaryLocationID" asp-items="Model.LocationOptions" class="form-control">
|
||||||
|
<option value="">No primary location</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Summary">Synopsis</label>
|
||||||
|
<textarea asp-for="Summary" class="form-control" rows="5"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="TimeModeID"></label>
|
||||||
|
<select asp-for="TimeModeID" asp-items="Model.TimeModes" class="form-control"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="TimeConfidenceID"></label>
|
||||||
|
<select asp-for="TimeConfidenceID" asp-items="Model.TimeConfidences" class="form-control"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="StartDateTime"></label>
|
||||||
|
<input type="datetime-local" name="StartDateTime" value="@startValue" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="EndDateTime"></label>
|
||||||
|
<input type="datetime-local" name="EndDateTime" value="@endValue" class="form-control" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="DurationAmount"></label>
|
||||||
|
<input asp-for="DurationAmount" class="form-control" step="0.01" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="DurationUnitID"></label>
|
||||||
|
<select asp-for="DurationUnitID" asp-items="Model.DurationUnits" class="form-control">
|
||||||
|
<option value="">No duration unit</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="RelativeTimeText"></label>
|
||||||
|
<input asp-for="RelativeTimeText" class="form-control" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-footer">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-scene-editor-close>Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary" data-scene-editor-save>Save details</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@ -12,14 +12,5 @@
|
|||||||
<partial name="~/Views/Shared/SceneInspectorV2/_SceneContinuityCard.cshtml" model="Model" />
|
<partial name="~/Views/Shared/SceneInspectorV2/_SceneContinuityCard.cshtml" model="Model" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="scene-inspector-editor-overlay" data-scene-editor-overlay hidden aria-hidden="true">
|
<partial name="~/Views/Shared/SceneInspectorV2/_SceneInspectorEditorHost.cshtml" />
|
||||||
<div class="scene-inspector-editor-shell">
|
|
||||||
<div>
|
|
||||||
<p class="eyebrow">Editor placeholder</p>
|
|
||||||
<h3 data-scene-editor-title>Focused editor</h3>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" data-scene-editor-close>Cancel</button>
|
|
||||||
</div>
|
|
||||||
<div class="scene-inspector-editor-empty" data-scene-editor-body></div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
<section class="scene-inspector-editor-overlay" data-scene-editor-overlay hidden aria-hidden="true">
|
||||||
|
<div class="scene-inspector-editor-shell">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Focused editor</p>
|
||||||
|
<h3 data-scene-editor-title>Focused editor</h3>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-scene-editor-close>Cancel</button>
|
||||||
|
</div>
|
||||||
|
<div class="scene-inspector-editor-empty" data-scene-editor-body></div>
|
||||||
|
</section>
|
||||||
@ -1,7 +1,8 @@
|
|||||||
@model SceneEditViewModel
|
@model SceneEditViewModel
|
||||||
@{
|
@{
|
||||||
var status = Model.RevisionStatuses.FirstOrDefault(x => x.Value == Model.RevisionStatusID.ToString())?.Text ?? "Not set";
|
var status = Model.RevisionStatuses.FirstOrDefault(x => x.Value == Model.RevisionStatusID.ToString())?.Text ?? "Not set";
|
||||||
var pov = Model.SceneCharacters.FirstOrDefault(x => x.RoleInSceneTypeName?.Contains("POV", StringComparison.OrdinalIgnoreCase) == true)?.CharacterName
|
var pov = Model.CharacterOptions.FirstOrDefault(x => x.Value == Model.POVCharacterID?.ToString())?.Text
|
||||||
|
?? Model.SceneCharacters.FirstOrDefault(x => x.RoleInSceneTypeName?.Contains("POV", StringComparison.OrdinalIgnoreCase) == true)?.CharacterName
|
||||||
?? "No POV";
|
?? "No POV";
|
||||||
var wordCount = Model.Workflow.ActualWordCount ?? Model.Workflow.EstimatedWordCount ?? 0;
|
var wordCount = Model.Workflow.ActualWordCount ?? Model.Workflow.EstimatedWordCount ?? 0;
|
||||||
var locationCount = Model.SceneCharacters.Where(x => x.LocationID.HasValue).Select(x => x.LocationID!.Value)
|
var locationCount = Model.SceneCharacters.Where(x => x.LocationID.HasValue).Select(x => x.LocationID!.Value)
|
||||||
@ -11,7 +12,7 @@
|
|||||||
var warningCount = Model.Warnings.Count + Model.AgeContinuityWarnings.Count;
|
var warningCount = Model.Warnings.Count + Model.AgeContinuityWarnings.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
<section class="scene-inspector-v2-health" aria-label="Scene health">
|
<section class="scene-inspector-v2-health" aria-label="Scene health" data-scene-summary-part="health">
|
||||||
<div>
|
<div>
|
||||||
<span>Status</span>
|
<span>Status</span>
|
||||||
<strong>@status</strong>
|
<strong>@status</strong>
|
||||||
|
|||||||
@ -12,14 +12,5 @@
|
|||||||
<partial name="~/Views/Shared/SceneInspectorV2/_SceneContinuityCard.cshtml" model="Model" />
|
<partial name="~/Views/Shared/SceneInspectorV2/_SceneContinuityCard.cshtml" model="Model" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="scene-inspector-editor-overlay" data-scene-editor-overlay hidden aria-hidden="true">
|
<partial name="~/Views/Shared/SceneInspectorV2/_SceneInspectorEditorHost.cshtml" />
|
||||||
<div class="scene-inspector-editor-shell">
|
|
||||||
<div>
|
|
||||||
<p class="eyebrow">Editor placeholder</p>
|
|
||||||
<h3 data-scene-editor-title>Focused editor</h3>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" data-scene-editor-close>Cancel</button>
|
|
||||||
</div>
|
|
||||||
<div class="scene-inspector-editor-empty" data-scene-editor-body></div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
@model SceneEditViewModel
|
||||||
|
|
||||||
|
<form asp-controller="Scenes" asp-action="SaveSceneMetrics" method="post" class="scene-inspector-editor-form" data-scene-editor-form>
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" asp-for="SceneID" />
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-error" data-scene-editor-error hidden></div>
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-fields">
|
||||||
|
@if (!Model.Metrics.Any())
|
||||||
|
{
|
||||||
|
<p class="muted">No active scene metrics are configured.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@for (var index = 0; index < Model.Metrics.Count; index++)
|
||||||
|
{
|
||||||
|
var metric = Model.Metrics[index];
|
||||||
|
<section class="scene-inspector-editor-metric">
|
||||||
|
<input type="hidden" name="Metrics[@index].MetricTypeID" value="@metric.MetricTypeID" />
|
||||||
|
<div class="scene-inspector-editor-metric-heading">
|
||||||
|
<div>
|
||||||
|
<h4>@metric.MetricName</h4>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(metric.Description))
|
||||||
|
{
|
||||||
|
<p>@metric.Description</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<output>@metric.Value</output>
|
||||||
|
</div>
|
||||||
|
<input type="range"
|
||||||
|
name="Metrics[@index].Value"
|
||||||
|
value="@metric.Value"
|
||||||
|
min="@metric.MinValue"
|
||||||
|
max="@metric.MaxValue"
|
||||||
|
class="form-range"
|
||||||
|
data-scene-metric-range />
|
||||||
|
<div class="scene-inspector-editor-range-labels">
|
||||||
|
<span>@metric.MinValue</span>
|
||||||
|
<span>@metric.MaxValue</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="metric-notes-@metric.MetricTypeID">Notes</label>
|
||||||
|
<textarea id="metric-notes-@metric.MetricTypeID" name="Metrics[@index].Notes" class="form-control" rows="3">@metric.Notes</textarea>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-footer">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-scene-editor-close>Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary" data-scene-editor-save>Save metrics</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@ -1,7 +1,8 @@
|
|||||||
@model SceneEditViewModel
|
@model SceneEditViewModel
|
||||||
@{
|
@{
|
||||||
var status = Model.RevisionStatuses.FirstOrDefault(x => x.Value == Model.RevisionStatusID.ToString())?.Text ?? "Not set";
|
var status = Model.RevisionStatuses.FirstOrDefault(x => x.Value == Model.RevisionStatusID.ToString())?.Text ?? "Not set";
|
||||||
var pov = Model.SceneCharacters.FirstOrDefault(x => x.RoleInSceneTypeName?.Contains("POV", StringComparison.OrdinalIgnoreCase) == true)?.CharacterName
|
var pov = Model.CharacterOptions.FirstOrDefault(x => x.Value == Model.POVCharacterID?.ToString())?.Text
|
||||||
|
?? Model.SceneCharacters.FirstOrDefault(x => x.RoleInSceneTypeName?.Contains("POV", StringComparison.OrdinalIgnoreCase) == true)?.CharacterName
|
||||||
?? "No POV character";
|
?? "No POV character";
|
||||||
var purposeLabels = Model.ScenePurposeTypes
|
var purposeLabels = Model.ScenePurposeTypes
|
||||||
.Where(x => Model.SelectedPurposeTypeIds.Contains(x.ScenePurposeTypeID))
|
.Where(x => Model.SelectedPurposeTypeIds.Contains(x.ScenePurposeTypeID))
|
||||||
@ -15,13 +16,13 @@
|
|||||||
?? "Time not set";
|
?? "Time not set";
|
||||||
}
|
}
|
||||||
|
|
||||||
<article class="scene-inspector-v2-card scene-overview-v2-card">
|
<article class="scene-inspector-v2-card scene-overview-v2-card" data-scene-summary-part="overview">
|
||||||
<div class="scene-inspector-v2-card-header">
|
<div class="scene-inspector-v2-card-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Overview</p>
|
<p class="eyebrow">Overview</p>
|
||||||
<h3>@(string.IsNullOrWhiteSpace(Model.SceneTitle) ? "Untitled scene" : Model.SceneTitle)</h3>
|
<h3>@(string.IsNullOrWhiteSpace(Model.SceneTitle) ? "Untitled scene" : Model.SceneTitle)</h3>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="scene-details" data-scene-id="@Model.SceneID">Manage scene details</button>
|
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="scene-details" data-scene-id="@Model.SceneID" data-scene-editor-url="@Url.Action("SceneDetailsEditor", "Scenes", new { sceneId = Model.SceneID })">Manage scene details</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dl class="scene-inspector-v2-facts">
|
<dl class="scene-inspector-v2-facts">
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
@model SceneEditViewModel
|
||||||
|
|
||||||
|
<form asp-controller="Scenes" asp-action="SaveScenePurpose" method="post" class="scene-inspector-editor-form" data-scene-editor-form>
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" asp-for="SceneID" />
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-error" data-scene-editor-error hidden></div>
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-fields">
|
||||||
|
<fieldset class="scene-inspector-editor-fieldset">
|
||||||
|
<legend>Selected purposes</legend>
|
||||||
|
<div class="scene-inspector-editor-checks">
|
||||||
|
@foreach (var purpose in Model.ScenePurposeTypes)
|
||||||
|
{
|
||||||
|
var checkedAttribute = Model.SelectedPurposeTypeIds.Contains(purpose.ScenePurposeTypeID) ? "checked" : null;
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="SelectedPurposeTypeIds" value="@purpose.ScenePurposeTypeID" checked="@checkedAttribute" />
|
||||||
|
<span>@purpose.PurposeName</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="ScenePurposeNotes"></label>
|
||||||
|
<textarea asp-for="ScenePurposeNotes" class="form-control" rows="5"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="SceneOutcomeNotes"></label>
|
||||||
|
<textarea asp-for="SceneOutcomeNotes" class="form-control" rows="5"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-inspector-editor-footer">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-scene-editor-close>Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary" data-scene-editor-save>Save purpose</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@ -7,7 +7,7 @@
|
|||||||
var threadCount = Model.ThreadEvents.Count;
|
var threadCount = Model.ThreadEvents.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
<article class="scene-inspector-v2-card">
|
<article class="scene-inspector-v2-card" data-scene-summary-part="story">
|
||||||
<div class="scene-inspector-v2-card-header">
|
<div class="scene-inspector-v2-card-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Story</p>
|
<p class="eyebrow">Story</p>
|
||||||
@ -67,8 +67,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scene-inspector-v2-actions">
|
<div class="scene-inspector-v2-actions">
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="purpose" data-scene-id="@Model.SceneID">Manage purpose</button>
|
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="purpose" data-scene-id="@Model.SceneID" data-scene-editor-url="@Url.Action("ScenePurposeEditor", "Scenes", new { sceneId = Model.SceneID })">Manage purpose</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="metrics" data-scene-id="@Model.SceneID">Manage metrics</button>
|
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="metrics" data-scene-id="@Model.SceneID" data-scene-editor-url="@Url.Action("SceneMetricsEditor", "Scenes", new { sceneId = Model.SceneID })">Manage metrics</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="threads" data-scene-id="@Model.SceneID">Manage threads</button>
|
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="threads" data-scene-id="@Model.SceneID">Manage threads</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@ -4653,6 +4653,12 @@ body.dragging-location [data-drag-type="location"] {
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scene-inspector-v2-timeline.editor-overlay-open {
|
||||||
|
height: clamp(380px, calc(100vh - 190px), 680px);
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.scene-inspector-v2-grid {
|
.scene-inspector-v2-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@ -4846,21 +4852,35 @@ body.dragging-location [data-drag-type="location"] {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
|
max-height: 100%;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
|
background: var(--plotline-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-inspector-editor-overlay[hidden] {
|
.scene-inspector-editor-overlay[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scene-inspector-v2-edit .scene-inspector-editor-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 96px 24px 24px;
|
||||||
|
z-index: 1030;
|
||||||
|
}
|
||||||
|
|
||||||
.scene-inspector-editor-shell {
|
.scene-inspector-editor-shell {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
border-bottom: 1px solid var(--plotline-line);
|
||||||
|
background: var(--plotline-panel);
|
||||||
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-inspector-editor-shell h3 {
|
.scene-inspector-editor-shell h3 {
|
||||||
@ -4869,10 +4889,145 @@ body.dragging-location [data-drag-type="location"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.scene-inspector-editor-empty {
|
.scene-inspector-editor-empty {
|
||||||
min-height: 180px;
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: minmax(0, 1fr) auto;
|
||||||
|
min-height: 100%;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-fields {
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
gap: 14px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-fieldset {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid var(--plotline-line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-fieldset legend {
|
||||||
|
float: none;
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
color: var(--plotline-muted);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-checks {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-checks label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid rgba(47, 111, 99, 0.12);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(47, 111, 99, 0.05);
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-checks span {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
border-top: 1px solid var(--plotline-line);
|
||||||
|
background: var(--plotline-panel);
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-error {
|
||||||
|
border: 1px solid rgba(179, 95, 59, 0.28);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(179, 95, 59, 0.1);
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: var(--plotline-danger, #b35f3b);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-loading,
|
||||||
|
.scene-inspector-editor-pending {
|
||||||
border: 1px dashed rgba(47, 111, 99, 0.24);
|
border: 1px dashed rgba(47, 111, 99, 0.24);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: rgba(47, 111, 99, 0.04);
|
background: rgba(47, 111, 99, 0.04);
|
||||||
|
padding: 16px;
|
||||||
|
color: var(--plotline-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-metric {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
border: 1px solid var(--plotline-line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(47, 111, 99, 0.04);
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-metric-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-metric-heading h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-metric-heading p {
|
||||||
|
margin: 3px 0 0;
|
||||||
|
color: var(--plotline-muted);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-metric-heading output {
|
||||||
|
min-width: 42px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--plotline-accent);
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-range-labels {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: var(--plotline-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-inspector-v2-timeline .scene-inspector-v2-health {
|
.scene-inspector-v2-timeline .scene-inspector-v2-health {
|
||||||
@ -4914,6 +5069,11 @@ body.dragging-location [data-drag-type="location"] {
|
|||||||
.scene-inspector-v2-card-header {
|
.scene-inspector-v2-card-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scene-inspector-editor-grid,
|
||||||
|
.scene-inspector-editor-checks {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.floor-plan-toolbar-floor-group {
|
.floor-plan-toolbar-floor-group {
|
||||||
|
|||||||
2
PlotLine/wwwroot/css/site.min.css
vendored
2
PlotLine/wwwroot/css/site.min.css
vendored
File diff suppressed because one or more lines are too long
@ -16,7 +16,114 @@
|
|||||||
warnings: "Review warnings"
|
warnings: "Review warnings"
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("click", event => {
|
function openOverlay(root, overlay, key) {
|
||||||
|
const title = overlay.querySelector("[data-scene-editor-title]");
|
||||||
|
if (title) {
|
||||||
|
title.textContent = titles[key] || "Focused editor";
|
||||||
|
}
|
||||||
|
overlay.hidden = false;
|
||||||
|
overlay.setAttribute("aria-hidden", "false");
|
||||||
|
root.classList.add("editor-overlay-open");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeOverlay(overlay) {
|
||||||
|
const root = overlay?.closest("[data-scene-inspector-v2]");
|
||||||
|
if (overlay) {
|
||||||
|
overlay.hidden = true;
|
||||||
|
overlay.setAttribute("aria-hidden", "true");
|
||||||
|
}
|
||||||
|
root?.classList.remove("editor-overlay-open");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEditorBody(overlay, html) {
|
||||||
|
const body = overlay.querySelector("[data-scene-editor-body]");
|
||||||
|
if (body) {
|
||||||
|
body.innerHTML = html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEditorError(form, message) {
|
||||||
|
const error = form.querySelector("[data-scene-editor-error]");
|
||||||
|
if (!error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error.textContent = message || "The editor could not be saved.";
|
||||||
|
error.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceSummaryParts(root, html) {
|
||||||
|
if (!html) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(html).forEach(([part, markup]) => {
|
||||||
|
if (!markup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = root.querySelector(`[data-scene-summary-part="${part}"]`);
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = document.createElement("template");
|
||||||
|
template.innerHTML = markup.trim();
|
||||||
|
const replacement = template.content.firstElementChild;
|
||||||
|
if (replacement) {
|
||||||
|
current.replaceWith(replacement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimelineSceneCard(scene) {
|
||||||
|
if (!scene?.sceneID && !scene?.SceneID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sceneId = scene.sceneID || scene.SceneID;
|
||||||
|
const card = document.querySelector(`.timeline-scene-card[data-scene-id="${sceneId}"]`);
|
||||||
|
if (!card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = scene.sceneTitle || scene.SceneTitle;
|
||||||
|
const sceneNumber = scene.sceneNumber || scene.SceneNumber;
|
||||||
|
const status = scene.revisionStatusName || scene.RevisionStatusName;
|
||||||
|
const pov = scene.povLabel || scene.PovLabel;
|
||||||
|
const location = scene.primaryLocationName || scene.PrimaryLocationName;
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
const titleElement = card.querySelector("strong");
|
||||||
|
if (titleElement) {
|
||||||
|
titleElement.textContent = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sceneNumber) {
|
||||||
|
const numberElement = card.querySelector(".scene-number");
|
||||||
|
if (numberElement) {
|
||||||
|
numberElement.textContent = `Scene ${sceneNumber}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
const statusElement = card.querySelector(".status-pill");
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.textContent = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pov) {
|
||||||
|
const povElement = card.querySelector(".scene-pov-label");
|
||||||
|
if (povElement) {
|
||||||
|
povElement.textContent = pov;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const locationElement = card.querySelector(".location-pill");
|
||||||
|
if (locationElement && location) {
|
||||||
|
locationElement.textContent = location;
|
||||||
|
locationElement.title = location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", async event => {
|
||||||
const openButton = event.target.closest("[data-scene-editor]");
|
const openButton = event.target.closest("[data-scene-editor]");
|
||||||
if (openButton) {
|
if (openButton) {
|
||||||
const root = openButton.closest("[data-scene-inspector-v2]");
|
const root = openButton.closest("[data-scene-inspector-v2]");
|
||||||
@ -26,30 +133,99 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const key = openButton.dataset.sceneEditor;
|
const key = openButton.dataset.sceneEditor;
|
||||||
const title = overlay.querySelector("[data-scene-editor-title]");
|
const url = openButton.dataset.sceneEditorUrl;
|
||||||
const body = overlay.querySelector("[data-scene-editor-body]");
|
openOverlay(root, overlay, key);
|
||||||
if (title) {
|
|
||||||
title.textContent = titles[key] || "Focused editor";
|
if (!url) {
|
||||||
}
|
setEditorBody(overlay, '<div class="scene-inspector-editor-pending">This focused editor has not been migrated yet.</div>');
|
||||||
if (body) {
|
|
||||||
body.textContent = "";
|
|
||||||
}
|
|
||||||
overlay.hidden = false;
|
|
||||||
overlay.setAttribute("aria-hidden", "false");
|
|
||||||
root.classList.add("editor-overlay-open");
|
|
||||||
overlay.querySelector("[data-scene-editor-close]")?.focus();
|
overlay.querySelector("[data-scene-editor-close]")?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEditorBody(overlay, '<div class="scene-inspector-editor-loading">Loading editor...</div>');
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||||
|
credentials: "same-origin"
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Editor load failed");
|
||||||
|
}
|
||||||
|
setEditorBody(overlay, await response.text());
|
||||||
|
overlay.querySelector("input, select, textarea, button")?.focus();
|
||||||
|
} catch {
|
||||||
|
setEditorBody(overlay, '<div class="scene-inspector-editor-error">The editor could not be loaded.</div>');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const closeButton = event.target.closest("[data-scene-editor-close]");
|
const closeButton = event.target.closest("[data-scene-editor-close]");
|
||||||
if (closeButton) {
|
if (closeButton) {
|
||||||
const overlay = closeButton.closest("[data-scene-editor-overlay]");
|
closeOverlay(closeButton.closest("[data-scene-editor-overlay]"));
|
||||||
const root = closeButton.closest("[data-scene-inspector-v2]");
|
}
|
||||||
if (overlay) {
|
});
|
||||||
overlay.hidden = true;
|
|
||||||
overlay.setAttribute("aria-hidden", "true");
|
document.addEventListener("input", event => {
|
||||||
|
const range = event.target.closest("[data-scene-metric-range]");
|
||||||
|
if (!range) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = range.closest(".scene-inspector-editor-metric")?.querySelector("output");
|
||||||
|
if (output) {
|
||||||
|
output.textContent = range.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("submit", async event => {
|
||||||
|
const form = event.target.closest("[data-scene-editor-form]");
|
||||||
|
if (!form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const overlay = form.closest("[data-scene-editor-overlay]");
|
||||||
|
const root = form.closest("[data-scene-inspector-v2]");
|
||||||
|
const saveButton = form.querySelector("[data-scene-editor-save]");
|
||||||
|
const originalText = saveButton?.textContent;
|
||||||
|
|
||||||
|
if (saveButton) {
|
||||||
|
saveButton.disabled = true;
|
||||||
|
saveButton.textContent = "Saving...";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(form.action, {
|
||||||
|
method: form.method || "POST",
|
||||||
|
body: new FormData(form),
|
||||||
|
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||||
|
credentials: "same-origin"
|
||||||
|
});
|
||||||
|
const contentType = response.headers.get("content-type") || "";
|
||||||
|
const payload = contentType.includes("application/json") ? await response.json() : null;
|
||||||
|
|
||||||
|
if (!response.ok || !payload?.success) {
|
||||||
|
setEditorError(form, payload?.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceSummaryParts(root, payload.html);
|
||||||
|
updateTimelineSceneCard(payload.scene);
|
||||||
|
|
||||||
|
const timelineHeader = document.querySelector(".scene-inspector-panel .inspector-header h2");
|
||||||
|
const title = payload.scene?.sceneTitle || payload.scene?.SceneTitle;
|
||||||
|
if (timelineHeader && title) {
|
||||||
|
timelineHeader.textContent = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeOverlay(overlay);
|
||||||
|
} catch {
|
||||||
|
setEditorError(form, "The editor could not be saved.");
|
||||||
|
} finally {
|
||||||
|
if (saveButton) {
|
||||||
|
saveButton.disabled = false;
|
||||||
|
saveButton.textContent = originalText;
|
||||||
}
|
}
|
||||||
root?.classList.remove("editor-overlay-open");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user