PlotDirector/PlotLine/Controllers/TimelineController.cs
2026-06-06 20:36:54 +01:00

160 lines
6.1 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using PlotLine.Models;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
[Authorize]
public sealed class TimelineController(ITimelineService timeline) : Controller
{
public async Task<IActionResult> 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);
}
public async Task<IActionResult> Settings(int projectId)
{
var model = await timeline.GetSettingsAsync(projectId);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Settings(TimelineSettingsViewModel model)
{
if (model.ShowMetricShape && !model.SelectedMetricTypeIDs.Any())
{
ModelState.AddModelError(nameof(model.SelectedMetricTypeIDs), "Select at least one metric or disable Metric Shape.");
}
if (!ModelState.IsValid)
{
var current = await timeline.GetSettingsAsync(model.ProjectID);
if (current is null)
{
return NotFound();
}
current.ShowSceneCards = model.ShowSceneCards;
current.ShowMetricShape = model.ShowMetricShape;
current.ShowPlotLines = model.ShowPlotLines;
current.ShowStoryAssets = model.ShowStoryAssets;
current.ShowCharacterAppearances = model.ShowCharacterAppearances;
current.ShowWarnings = model.ShowWarnings;
current.SelectedMetricTypeIDs = model.SelectedMetricTypeIDs;
current.MetricOptions = current.MetricOptions
.Select(x => new TimelineMetricSettingOptionViewModel
{
MetricTypeID = x.MetricTypeID,
MetricName = x.MetricName,
Description = x.Description,
SortOrder = x.SortOrder,
IsSelected = model.SelectedMetricTypeIDs.Contains(x.MetricTypeID)
})
.ToList();
return View(current);
}
await timeline.SaveSettingsAsync(model);
TempData["TimelineSettingsMessage"] = "Timeline settings saved.";
return RedirectToAction(nameof(Settings), new { projectId = model.ProjectID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SavePreset(TimelinePresetSaveViewModel model)
{
await timeline.SavePresetAsync(model);
return RedirectToAction(nameof(Index), new { projectId = model.ProjectID, bookId = model.BookID });
}
public async Task<IActionResult> LoadPreset(int id)
{
var preset = await timeline.GetPresetAsync(id);
if (preset is null)
{
return NotFound();
}
var filter = System.Text.Json.JsonSerializer.Deserialize<TimelineFilterViewModel>(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<IActionResult> DeletePreset(int id, int projectId, int? bookId)
{
await timeline.ArchivePresetAsync(id);
return RedirectToAction(nameof(Index), new { projectId, bookId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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.ReturnSelectedSceneID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<Task> action, string successMessage)
{
try
{
await action();
TempData["TimelineDragMessage"] = successMessage;
}
catch (InvalidOperationException ex)
{
TempData["TimelineDragMessage"] = ex.Message;
}
}
}