using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using PlotLine.Services; using PlotLine.ViewModels; namespace PlotLine.Controllers; [Authorize] public sealed class CharactersController(ICharacterService characters) : Controller { public async Task Index(int projectId) { var model = await characters.GetCharactersAsync(projectId); return model is null ? NotFound() : View(model); } public async Task Create(int projectId) { var model = await characters.GetCreateCharacterAsync(projectId); return model is null ? NotFound() : View("Edit", model); } public async Task Edit(int id) { var model = await characters.GetEditCharacterAsync(id); return model is null ? NotFound() : View(model); } public async Task Details(int id) { var model = await characters.GetCharacterDetailAsync(id); return model is null ? NotFound() : View(model); } public async Task CropAvatar(int imageId) { var model = await characters.GetAvatarCropAsync(imageId); return model is null ? NotFound() : View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task Save(CharacterEditViewModel model) { ValidateSexWorkflow(model); if (!ModelState.IsValid) { var editModel = model.CharacterID == 0 ? await characters.GetCreateCharacterAsync(model.ProjectID) : await characters.GetEditCharacterAsync(model.CharacterID); if (editModel is null) { return NotFound(); } CopyCharacterFormValues(model, editModel); return View("Edit", editModel); } CharacterSaveResult saveResult; try { saveResult = await characters.SaveCharacterAsync(model); } catch (SubscriptionLimitException ex) when (model.CharacterID == 0) { ModelState.AddModelError(string.Empty, ex.Message); var createModel = await characters.GetCreateCharacterAsync(model.ProjectID); if (createModel is null) { return NotFound(); } CopyCharacterFormValues(model, createModel); return View("Edit", createModel); } catch (EntityImageUploadException ex) { TempData["ImageError"] = ex.Message; return RedirectToAction(nameof(Edit), new { id = ex.EntityId }); } catch (InvalidOperationException ex) { ModelState.AddModelError(string.Empty, ex.Message); var editModel = model.CharacterID == 0 ? await characters.GetCreateCharacterAsync(model.ProjectID) : await characters.GetEditCharacterAsync(model.CharacterID); if (editModel is null) { return NotFound(); } CopyCharacterFormValues(model, editModel); return View("Edit", editModel); } return saveResult.UploadedCharacterImageID.HasValue ? RedirectToAction(nameof(CropAvatar), new { imageId = saveResult.UploadedCharacterImageID.Value }) : RedirectToAction(nameof(Details), new { id = saveResult.CharacterID }); } private void ValidateSexWorkflow(CharacterEditViewModel model) { if (model.SexValueID == CharacterEditViewModel.CustomSexValue) { if (string.IsNullOrWhiteSpace(model.CustomSex)) { ModelState.AddModelError(nameof(model.CustomSex), "Enter a custom sex value."); } } else { ModelState.Remove(nameof(model.CustomSex)); model.CustomSex = null; } } private static void CopyCharacterFormValues(CharacterEditViewModel source, CharacterEditViewModel target) { target.CharacterName = source.CharacterName; target.ShortName = source.ShortName; target.SexValueID = source.SexValueID; target.Sex = source.Sex; target.CustomSex = source.CustomSex; target.BirthDate = source.BirthDate; target.AgeAtSeriesStart = source.AgeAtSeriesStart; target.Height = source.Height; target.EyeColour = source.EyeColour; target.CharacterImportance = source.CharacterImportance; target.ShowInQuickAddBar = source.ShowInQuickAddBar; target.DefaultDescription = source.DefaultDescription; foreach (var option in target.SexOptions) { option.Selected = option.Value == source.SexValueID?.ToString(); } } [HttpPost] [ValidateAntiForgeryToken] public async Task UploadImage(CharacterImageUploadViewModel model) { var result = await characters.UploadCharacterImageAsync(model); TempData[result.Succeeded ? "ImageMessage" : "ImageError"] = result.Succeeded ? "Image added to gallery." : result.ErrorMessage ?? "Character image could not be uploaded."; return RedirectToAction(nameof(Details), new { id = model.CharacterID }); } [HttpPost] [ValidateAntiForgeryToken] public async Task SaveAvatar(CharacterAvatarCropViewModel model) { var result = await characters.SaveCharacterAvatarAsync(model); TempData[result.Succeeded ? "ImageMessage" : "ImageError"] = result.Succeeded ? "Avatar updated." : result.ErrorMessage ?? "Avatar could not be saved."; return result.Succeeded ? RedirectToAction(nameof(Details), new { id = model.CharacterID }) : RedirectToAction(nameof(CropAvatar), new { imageId = model.CharacterImageID }); } [HttpPost] [ValidateAntiForgeryToken] public async Task RemoveAvatar(int id) { await characters.RemoveCharacterAvatarAsync(id); TempData["ImageMessage"] = "Avatar removed."; return RedirectToAction(nameof(Details), new { id }); } [HttpPost] [ValidateAntiForgeryToken] public async Task DeleteImage(int imageId, int characterId) { await characters.DeleteCharacterImageAsync(imageId); TempData["ImageMessage"] = "Image removed from gallery."; return RedirectToAction(nameof(Details), new { id = characterId }); } [HttpPost] [ValidateAntiForgeryToken] public async Task Archive(int id, int projectId) { await characters.ArchiveCharacterAsync(id); TempData["ArchiveMessage"] = "Character archived. You can restore it from Archived Items."; return RedirectToAction(nameof(Index), new { projectId }); } [HttpPost] [ValidateAntiForgeryToken] public async Task SaveInitialRelationship(InitialRelationshipEditViewModel model) { await characters.SaveInitialRelationshipAsync(model); TempData["RelationshipMessage"] = model.CharacterRelationshipID == 0 ? "Initial relationship added." : "Initial relationship updated."; return RedirectToAction(nameof(Details), new { id = model.ReturnCharacterID ?? model.CharacterID }); } [HttpPost] [ValidateAntiForgeryToken] public async Task ArchiveRelationship(int id, int characterId) { await characters.ArchiveRelationshipAsync(id); TempData["ArchiveMessage"] = "Relationship archived. You can restore it from Archived Items."; return RedirectToAction(nameof(Details), new { id = characterId }); } }