diff --git a/PlotLine/Controllers/BooksController.cs b/PlotLine/Controllers/BooksController.cs index 5a6d3c8..1339ecc 100644 --- a/PlotLine/Controllers/BooksController.cs +++ b/PlotLine/Controllers/BooksController.cs @@ -7,7 +7,7 @@ using PlotLine.ViewModels; namespace PlotLine.Controllers; [Authorize] -public sealed class BooksController(IBookService books) : Controller +public sealed class BooksController(IBookService books, IOnboardingService onboarding) : Controller { public async Task Details(int id) { @@ -31,6 +31,11 @@ public sealed class BooksController(IBookService books) : Controller [ValidateAntiForgeryToken] public async Task Save(BookEditViewModel model) { + if (model.BeginMode is not (BookBeginModes.Manual or BookBeginModes.Import)) + { + model.BeginMode = BookBeginModes.Manual; + } + if (!BookStatuses.IsValid(model.Status)) { ModelState.AddModelError(nameof(model.Status), "Select a valid book status."); @@ -68,6 +73,7 @@ public sealed class BooksController(IBookService books) : Controller createModel.TargetCompletionDate = model.TargetCompletionDate; createModel.PublicationDate = model.PublicationDate; createModel.UsesExplicitScenes = model.UsesExplicitScenes; + createModel.BeginMode = model.BeginMode; await books.PopulateEditContextAsync(createModel); return View("Edit", createModel); } @@ -80,6 +86,12 @@ public sealed class BooksController(IBookService books) : Controller return View("Edit", model); } + if (model.BookID == 0 && string.Equals(model.BeginMode, BookBeginModes.Import, StringComparison.Ordinal)) + { + await onboarding.BeginManuscriptImportForBookAsync(model.ProjectID, result.BookID); + return RedirectToAction("Index", "Onboarding"); + } + return RedirectToAction(nameof(Details), new { id = result.BookID }); } diff --git a/PlotLine/Controllers/OnboardingController.cs b/PlotLine/Controllers/OnboardingController.cs index aab02d2..f6a9578 100644 --- a/PlotLine/Controllers/OnboardingController.cs +++ b/PlotLine/Controllers/OnboardingController.cs @@ -17,6 +17,22 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return View(model); } + [HttpPost("start-manual")] + [ValidateAntiForgeryToken] + public async Task StartManual() + { + await onboarding.StartManualSetupAsync(); + return RedirectToAction(nameof(Index)); + } + + [HttpPost("start-import")] + [ValidateAntiForgeryToken] + public async Task StartImport() + { + await onboarding.StartManuscriptImportSetupAsync(); + return RedirectToAction(nameof(Index)); + } + [HttpGet("scan-review")] public async Task ScanReview(Guid? previewId) { diff --git a/PlotLine/Services/ManuscriptScanPreviewStore.cs b/PlotLine/Services/ManuscriptScanPreviewStore.cs index 6eb9746..23de1c8 100644 --- a/PlotLine/Services/ManuscriptScanPreviewStore.cs +++ b/PlotLine/Services/ManuscriptScanPreviewStore.cs @@ -6,6 +6,7 @@ namespace PlotLine.Services; public interface IManuscriptScanPreviewStore { Task StartAsync(int userId, int onboardingId, int projectId, int bookId); + Task ClearAsync(int userId, int onboardingId); Task ProgressAsync(int userId, ManuscriptScanProgress progress); Task CompleteAsync(int userId, ManuscriptScanPreview preview); Task FailAsync(int userId, int onboardingId, string message); @@ -45,6 +46,12 @@ public sealed class ManuscriptScanPreviewStore : IManuscriptScanPreviewStore return Task.FromResult(ToState(session)); } + public Task ClearAsync(int userId, int onboardingId) + { + sessions.TryRemove((userId, onboardingId), out _); + return Task.CompletedTask; + } + public Task ProgressAsync(int userId, ManuscriptScanProgress progress) { var key = (userId, progress.OnboardingID); diff --git a/PlotLine/Services/OnboardingService.cs b/PlotLine/Services/OnboardingService.cs index 1bbe598..10a37ac 100644 --- a/PlotLine/Services/OnboardingService.cs +++ b/PlotLine/Services/OnboardingService.cs @@ -16,6 +16,9 @@ public interface IOnboardingService Task GetDashboardNudgeAsync(); Task ShouldShowWordCompanionHeaderAsync(); Task MoveToNextStepAsync(); + Task StartManualSetupAsync(); + Task StartManuscriptImportSetupAsync(); + Task BeginManuscriptImportForBookAsync(int projectId, int bookId); Task SetCurrentStepAsync(string currentStep); Task SaveWritingJourneyAsync(string writingJourney); Task SaveWritingSoftwareAsync(string writingSoftware); @@ -572,6 +575,42 @@ public sealed class OnboardingService( await onboarding.SetCurrentStepAsync(RequireUserId(), nextStep); } + public async Task StartManualSetupAsync() + { + var userId = RequireUserId(); + await onboarding.SaveWritingJourneyAsync(userId, WritingJourneyValues.PlanningNewStory); + var state = await onboarding.SaveWritingSoftwareAsync(userId, WritingSoftwareValues.Other); + await scanPreviews.ClearAsync(userId, state.UserOnboardingStateID); + await onboarding.SetCurrentStepAsync(userId, OnboardingSteps.Project); + } + + public async Task StartManuscriptImportSetupAsync() + { + var userId = RequireUserId(); + await onboarding.SaveWritingJourneyAsync(userId, WritingJourneyValues.ManuscriptMostlyComplete); + var state = await onboarding.SaveWritingSoftwareAsync(userId, WritingSoftwareValues.MicrosoftWord); + await scanPreviews.ClearAsync(userId, state.UserOnboardingStateID); + await onboarding.SetCurrentStepAsync(userId, OnboardingSteps.Project); + } + + public async Task BeginManuscriptImportForBookAsync(int projectId, int bookId) + { + var userId = RequireUserId(); + var project = await projects.GetForUserAsync(projectId, userId); + var book = await books.GetAsync(bookId); + if (project is null || !project.IsOwner || project.IsArchived || book is null || book.ProjectID != project.ProjectID || book.IsArchived) + { + throw new InvalidOperationException("Choose one of your active books before importing a manuscript."); + } + + await onboarding.SaveWritingJourneyAsync(userId, WritingJourneyValues.ManuscriptMostlyComplete); + await onboarding.SaveWritingSoftwareAsync(userId, WritingSoftwareValues.MicrosoftWord); + await onboarding.SaveProjectAsync(userId, project.ProjectID); + var state = await onboarding.SaveBookAsync(userId, book.BookID); + await scanPreviews.ClearAsync(userId, state.UserOnboardingStateID); + await onboarding.SetCurrentStepAsync(userId, OnboardingSteps.NextPathPreview); + } + public async Task SetCurrentStepAsync(string currentStep) { if (currentStep is not (OnboardingSteps.Welcome diff --git a/PlotLine/ViewModels/CoreViewModels.cs b/PlotLine/ViewModels/CoreViewModels.cs index 4a66b63..7c84c29 100644 --- a/PlotLine/ViewModels/CoreViewModels.cs +++ b/PlotLine/ViewModels/CoreViewModels.cs @@ -28,6 +28,7 @@ public sealed class ProjectIndexViewModel public TrialVisibilityViewModel? Trial { get; init; } public OnboardingNudgeViewModel OnboardingNudge { get; init; } = new(); public StoryIntelligenceDashboardViewModel StoryIntelligence { get; init; } = new(); + public bool HasAnyProjects => ActiveProjects.Any() || ArchivedProjects.Any(); } public sealed class ProjectIndexCardViewModel @@ -228,6 +229,13 @@ public sealed class BookEditViewModel public string? Description { get; set; } public Project? Project { get; set; } public IReadOnlyList StatusOptions { get; set; } = []; + public string BeginMode { get; set; } = BookBeginModes.Manual; +} + +public static class BookBeginModes +{ + public const string Manual = "Manual"; + public const string Import = "Import"; } public sealed class BookDetailViewModel diff --git a/PlotLine/Views/Books/Edit.cshtml b/PlotLine/Views/Books/Edit.cshtml index 9ce0b23..a37f77b 100644 --- a/PlotLine/Views/Books/Edit.cshtml +++ b/PlotLine/Views/Books/Edit.cshtml @@ -14,7 +14,38 @@
+ @if (Model.BookID != 0) + { + + }
+ @if (Model.BookID == 0) + { +
+

New book

+

How would you like to begin?

+
+
+ +
+
+ +
+
+
+ }
diff --git a/PlotLine/Views/Projects/Index.cshtml b/PlotLine/Views/Projects/Index.cshtml index 96cc8b1..b6a37ad 100644 --- a/PlotLine/Views/Projects/Index.cshtml +++ b/PlotLine/Views/Projects/Index.cshtml @@ -2,6 +2,7 @@ @{ ViewData["Title"] = "Projects"; var trial = Model.Trial; + var hasAnyProjects = Model.HasAnyProjects; }
@@ -33,33 +34,6 @@ } -@if (Model.OnboardingNudge.ShouldShow && !Model.StoryIntelligence.ShouldShow) -{ -
-
-

First setup

-

@Model.OnboardingNudge.Title

-

@Model.OnboardingNudge.Description

-
- @Model.OnboardingNudge.ButtonText -
-} - -@if (Model.StoryIntelligence.ShouldShow) -{ -
-
-

Manuscript import

-

@Model.StoryIntelligence.Title

-

@Model.StoryIntelligence.Description

-
- @Model.StoryIntelligence.ButtonText -
-} - @if (TempData["ArchiveMessage"] is string archiveMessage) {
@archiveMessage
@@ -73,12 +47,19 @@
@projectExportMessage
} -@if (!Model.ActiveProjects.Any() && !Model.ArchivedProjects.Any()) +@if (!hasAnyProjects) {
-

Start with a series or standalone novel

-

Create a project, then add books, chapters and scenes in narrative order.

- Create your first project +

Welcome to PlotDirector

+

Let's create your first project.

+
+ + + +
+ +
+
} else