mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(series): count every volume of series longer than the ladder cap
Send logical volume counts on each series summary, tallied from the member grouping the ladder already builds, and read them on the cards instead of counting the 60-slot ladder. Long series no longer stop at 60 or show as complete after their first 60 volumes are read. Closes #1987
This commit is contained in:
@@ -24,6 +24,9 @@ function makeSeries(overrides: Partial<SeriesPage['items'][number]> = {}): Serie
|
||||
lastAddedAt: null,
|
||||
libraryNames: [],
|
||||
expectedBookCount: null,
|
||||
ownedVolumeCount: 1,
|
||||
readVolumeCount: 0,
|
||||
readingVolumeCount: 0,
|
||||
volumes: [],
|
||||
volumesTruncated: false,
|
||||
gaps: [],
|
||||
|
||||
@@ -19,6 +19,9 @@ function summary(overrides: Partial<SeriesSummary> = {}): SeriesSummary {
|
||||
lastAddedAt: null,
|
||||
libraryNames: [],
|
||||
expectedBookCount: null,
|
||||
ownedVolumeCount: 1,
|
||||
readVolumeCount: 0,
|
||||
readingVolumeCount: 0,
|
||||
volumes: [],
|
||||
volumesTruncated: false,
|
||||
gaps: [],
|
||||
@@ -31,51 +34,40 @@ function summary(overrides: Partial<SeriesSummary> = {}): SeriesSummary {
|
||||
}
|
||||
|
||||
describe('seriesRowFacts', () => {
|
||||
it('counts volumes off the ladder, so two editions of one volume count once', () => {
|
||||
const facts = seriesRowFacts(
|
||||
summary({
|
||||
bookCount: 4,
|
||||
readCount: 1,
|
||||
volumes: [slot({ index: 1, bookId: 1, status: 'read' }), slot({ index: 2, bookId: 2, status: 'unread' })],
|
||||
}),
|
||||
)
|
||||
it('reports volumes rather than books, so two editions of one volume count once', () => {
|
||||
const facts = seriesRowFacts(summary({ bookCount: 4, readCount: 2, ownedVolumeCount: 2, readVolumeCount: 1 }))
|
||||
|
||||
expect(facts.ownedVolumes).toBe(2)
|
||||
expect(facts.readVolumes).toBe(1)
|
||||
expect(facts.percentRead).toBe(50)
|
||||
})
|
||||
|
||||
it('does not count a missing rung as something the library owns', () => {
|
||||
it('counts a series past the capped ladder instead of stopping at the cap', () => {
|
||||
const volumes = Array.from({ length: 60 }, (_, i) => slot({ index: i + 1, bookId: i + 1, status: 'read' }))
|
||||
const facts = seriesRowFacts(
|
||||
summary({
|
||||
bookCount: 2,
|
||||
volumes: [
|
||||
slot({ index: 1, bookId: 1, status: 'read' }),
|
||||
slot({ index: 2, bookId: null, title: null, status: 'missing' }),
|
||||
slot({ index: 3, bookId: 3, status: 'unread' }),
|
||||
],
|
||||
gaps: [2],
|
||||
gapCount: 1,
|
||||
}),
|
||||
summary({ bookCount: 300, readCount: 60, ownedVolumeCount: 300, readVolumeCount: 60, volumes, volumesTruncated: true }),
|
||||
)
|
||||
|
||||
expect(facts.ownedVolumes).toBe(2)
|
||||
expect(facts.hasGaps).toBe(true)
|
||||
expect(facts.ownedVolumes).toBe(300)
|
||||
expect(facts.readVolumes).toBe(60)
|
||||
expect(facts.percentRead).toBe(20)
|
||||
expect(facts.isComplete).toBe(false)
|
||||
})
|
||||
|
||||
it('falls back to the book counts when there is no ladder to count', () => {
|
||||
const facts = seriesRowFacts(summary({ bookCount: 9, readCount: 4, readingCount: 1, volumes: [], volumesTruncated: true }))
|
||||
it('reports the volumes in progress', () => {
|
||||
const facts = seriesRowFacts(summary({ ownedVolumeCount: 9, readVolumeCount: 4, readingVolumeCount: 1 }))
|
||||
|
||||
expect(facts.ownedVolumes).toBe(9)
|
||||
expect(facts.readVolumes).toBe(4)
|
||||
expect(facts.readingVolumes).toBe(1)
|
||||
expect(facts.isStarted).toBe(true)
|
||||
})
|
||||
|
||||
it('flags gaps from the gap count', () => {
|
||||
expect(seriesRowFacts(summary({ gaps: [2], gapCount: 1 })).hasGaps).toBe(true)
|
||||
})
|
||||
|
||||
it('is complete only when every owned volume is read', () => {
|
||||
const read = [slot({ index: 1, bookId: 1, status: 'read' }), slot({ index: 2, bookId: 2, status: 'read' })]
|
||||
expect(seriesRowFacts(summary({ volumes: read })).isComplete).toBe(true)
|
||||
expect(seriesRowFacts(summary({ volumes: [], bookCount: 0, readCount: 0 })).isComplete).toBe(false)
|
||||
expect(seriesRowFacts(summary({ ownedVolumeCount: 2, readVolumeCount: 2 })).isComplete).toBe(true)
|
||||
expect(seriesRowFacts(summary({ ownedVolumeCount: 0, readVolumeCount: 0, bookCount: 0 })).isComplete).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import type { SeriesSummary, SeriesVolumeSlot } from '@bookorbit/types'
|
||||
* What a row actually shows, derived once so the grid card and the list row can never
|
||||
* disagree about a series.
|
||||
*
|
||||
* The volume counts come from the ladder rather than from `bookCount`, because a library holding
|
||||
* the same volume twice - one ebook, one audiobook - owns two books and one volume, and "3 of 7"
|
||||
* only reads correctly when it counts volumes. When there is no ladder to count, the book counts
|
||||
* stand in.
|
||||
* The volume counts are the server's logical-volume counts rather than `bookCount`, because a
|
||||
* library holding the same volume twice - one ebook, one audiobook - owns two books and one
|
||||
* volume, and "3 of 7" only reads correctly when it counts volumes. They are not counted from
|
||||
* `volumes`, which is capped and would stop a long series at the cap.
|
||||
*/
|
||||
export type SeriesRowFacts = {
|
||||
ownedVolumes: number
|
||||
@@ -23,10 +23,9 @@ export type SeriesRowFacts = {
|
||||
}
|
||||
|
||||
export function seriesRowFacts(series: SeriesSummary): SeriesRowFacts {
|
||||
const slots = series.volumes
|
||||
const owned = slots.length > 0 ? slots.filter((slot) => slot.bookId !== null).length : series.bookCount
|
||||
const read = slots.length > 0 ? slots.filter((slot) => slot.status === 'read').length : series.readCount
|
||||
const reading = slots.length > 0 ? slots.filter((slot) => slot.status === 'reading').length : series.readingCount
|
||||
const owned = series.ownedVolumeCount
|
||||
const read = series.readVolumeCount
|
||||
const reading = series.readingVolumeCount
|
||||
|
||||
return {
|
||||
ownedVolumes: owned,
|
||||
@@ -37,7 +36,7 @@ export function seriesRowFacts(series: SeriesSummary): SeriesRowFacts {
|
||||
isStarted: read > 0 || reading > 0,
|
||||
isSingleVolume: owned === 1,
|
||||
hasGaps: series.gapCount > 0,
|
||||
isNumbered: slots.some((slot) => slot.index !== null),
|
||||
isNumbered: series.volumes.some((slot) => slot.index !== null),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,9 @@ function makeSeries(overrides: Partial<SeriesSummary> = {}): SeriesSummary {
|
||||
lastAddedAt: null,
|
||||
libraryNames: ['Novels'],
|
||||
expectedBookCount: null,
|
||||
ownedVolumeCount: 2,
|
||||
readVolumeCount: 1,
|
||||
readingVolumeCount: 0,
|
||||
volumes: [
|
||||
{ index: 1, bookId: 10, title: 'One', status: 'read' },
|
||||
{ index: 2, bookId: null, title: null, status: 'missing' },
|
||||
|
||||
Reference in New Issue
Block a user