using Microsoft.AspNetCore.Mvc; using PlotLine.Models; using PlotLine.Services; using PlotLine.ViewModels; namespace PlotLine.Controllers; public sealed class TimelineController(ITimelineService timeline) : Controller { public async Task Index([FromQuery] TimelineFilterViewModel filter) { if (filter.ProjectID == 0) { return BadRequest("ProjectID is required."); } var model = await timeline.GetAsync(filter); return model is null ? NotFound() : View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task SavePreset(TimelinePresetSaveViewModel model) { await timeline.SavePresetAsync(model); return RedirectToAction(nameof(Index), new { projectId = model.ProjectID, bookId = model.BookID }); } public async Task LoadPreset(int id) { var preset = await timeline.GetPresetAsync(id); if (preset is null) { return NotFound(); } var filter = System.Text.Json.JsonSerializer.Deserialize(preset.FilterJson) ?? new TimelineFilterViewModel(); filter.ProjectID = preset.ProjectID; filter.BookID = preset.BookID; filter.FocusType = preset.FocusType; filter.FocusID = preset.FocusID; return RedirectToAction(nameof(Index), filter); } [HttpPost] [ValidateAntiForgeryToken] public async Task DeletePreset(int id, int projectId, int? bookId) { await timeline.ArchivePresetAsync(id); return RedirectToAction(nameof(Index), new { projectId, bookId }); } [HttpPost] [ValidateAntiForgeryToken] public async Task DropMoveScene(TimelineSceneDropMoveViewModel model) { var summary = await timeline.MoveSceneByDropAsync(model); if (summary is not null) { TempData["TimelineDragMessage"] = $"Scene moved. Validation found {summary.WarningCount} warning{(summary.WarningCount == 1 ? "" : "s")}."; } return RedirectToAction(nameof(Index), new { projectId = model.ProjectID, bookId = model.BookID, selectedSceneId = model.SceneID }); } [HttpPost] [ValidateAntiForgeryToken] public async Task DropCharacter(TimelineSceneCharacterDropViewModel model) { await RunDropActionAsync(() => timeline.AddCharacterByDropAsync(model), "Character added to scene."); return RedirectToAction(nameof(Index), new { projectId = model.ReturnProjectID, bookId = model.ReturnBookID, selectedSceneId = model.SceneID }); } [HttpPost] [ValidateAntiForgeryToken] public async Task DropAssetEvent(TimelineAssetEventDropViewModel model) { await RunDropActionAsync(() => timeline.AddAssetEventByDropAsync(model), "Asset event added to scene."); return RedirectToAction(nameof(Index), new { projectId = model.ReturnProjectID, bookId = model.ReturnBookID, selectedSceneId = model.SceneID }); } [HttpPost] [ValidateAntiForgeryToken] public async Task DropAssetDependency(TimelineAssetDependencyDropViewModel model) { await RunDropActionAsync(() => timeline.AddAssetDependencyByDropAsync(model), "Asset dependency saved."); return RedirectToAction(nameof(Index), new { projectId = model.ReturnProjectID, bookId = model.ReturnBookID }); } [HttpPost] [ValidateAntiForgeryToken] public async Task DropLocationRelationship(TimelineLocationRelationshipDropViewModel model) { await RunDropActionAsync(() => timeline.AddLocationRelationshipByDropAsync(model), "Location relationship saved."); return RedirectToAction(nameof(Index), new { projectId = model.ProjectID, bookId = model.ReturnBookID }); } private async Task RunDropActionAsync(Func action, string successMessage) { try { await action(); TempData["TimelineDragMessage"] = successMessage; } catch (InvalidOperationException ex) { TempData["TimelineDragMessage"] = ex.Message; } } }