Label chapter chips by kind, not raw number

Beats, tags, locations, open questions, and character/arc-stage
responses that reference a chapter now carry a ChapterLabel/DisplayNumber
alongside the raw Number, computed via the new
ChapterDisplayNumberLookup. Front/back matter chips show their title;
body chapters show "Chapter N: Title".
This commit is contained in:
James Wampler
2026-08-19 18:02:53 -07:00
parent ef5260a111
commit 7f56c79b20
18 changed files with 407 additions and 88 deletions
@@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore;
using Novelly.Api.Data;
namespace Novelly.Api.Chapters;
public class ChapterDisplayNumberLookup(INovelDbContext db)
{
public async Task<IReadOnlyDictionary<Guid, int>> ForNovelAsync(Guid novelId, CancellationToken ct = default)
{
var chapters = await db.Chapters.AsNoTracking().Where(c => c.NovelId == novelId).ToListAsync(ct);
return ChapterNumbering.DisplayNumbers(chapters);
}
public async Task<int?> ForChapterAsync(Chapter chapter, CancellationToken ct = default)
{
if (chapter.Kind != ChapterKind.Body)
return null;
return await db.Chapters.CountAsync(
c => c.NovelId == chapter.NovelId && c.Kind == ChapterKind.Body && c.Number <= chapter.Number, ct);
}
public async Task<IReadOnlyDictionary<Guid, int>> ForChaptersAsync(IEnumerable<Chapter> chapters, CancellationToken ct = default)
{
var displayNumbers = new Dictionary<Guid, int>();
foreach (var chapter in chapters.DistinctBy(c => c.Id))
{
if (await ForChapterAsync(chapter, ct) is { } number)
displayNumbers[chapter.Id] = number;
}
return displayNumbers;
}
public string LabelFor(Chapter chapter, IReadOnlyDictionary<Guid, int> displayNumbers) =>
ChapterNumbering.Label(chapter.Kind, displayNumbers.TryGetValue(chapter.Id, out var n) ? n : null, chapter.Title);
}
+3 -8
View File
@@ -14,6 +14,7 @@ public class ChapterService(
NovelAccessService access,
TagService tags,
LocationService locations,
ChapterDisplayNumberLookup displayNumbers,
ActivityLog activity,
ILogger<ChapterService> logger,
IModelValidator<CreateChapterRequest> createValidator,
@@ -168,14 +169,8 @@ public class ChapterService(
return true;
}
public async Task<int?> DisplayNumberAsync(Chapter chapter, CancellationToken ct = default)
{
if (chapter.Kind != ChapterKind.Body)
return null;
return await db.Chapters.CountAsync(
c => c.NovelId == chapter.NovelId && c.Kind == ChapterKind.Body && c.Number <= chapter.Number, ct);
}
public Task<int?> DisplayNumberAsync(Chapter chapter, CancellationToken ct = default) =>
displayNumbers.ForChapterAsync(chapter, ct);
private async Task<int> NextChapterNumberAsync(Guid novelId, CancellationToken ct)
{