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,
|
lastAddedAt: null,
|
||||||
libraryNames: [],
|
libraryNames: [],
|
||||||
expectedBookCount: null,
|
expectedBookCount: null,
|
||||||
|
ownedVolumeCount: 1,
|
||||||
|
readVolumeCount: 0,
|
||||||
|
readingVolumeCount: 0,
|
||||||
volumes: [],
|
volumes: [],
|
||||||
volumesTruncated: false,
|
volumesTruncated: false,
|
||||||
gaps: [],
|
gaps: [],
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ function summary(overrides: Partial<SeriesSummary> = {}): SeriesSummary {
|
|||||||
lastAddedAt: null,
|
lastAddedAt: null,
|
||||||
libraryNames: [],
|
libraryNames: [],
|
||||||
expectedBookCount: null,
|
expectedBookCount: null,
|
||||||
|
ownedVolumeCount: 1,
|
||||||
|
readVolumeCount: 0,
|
||||||
|
readingVolumeCount: 0,
|
||||||
volumes: [],
|
volumes: [],
|
||||||
volumesTruncated: false,
|
volumesTruncated: false,
|
||||||
gaps: [],
|
gaps: [],
|
||||||
@@ -31,51 +34,40 @@ function summary(overrides: Partial<SeriesSummary> = {}): SeriesSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('seriesRowFacts', () => {
|
describe('seriesRowFacts', () => {
|
||||||
it('counts volumes off the ladder, so two editions of one volume count once', () => {
|
it('reports volumes rather than books, so two editions of one volume count once', () => {
|
||||||
const facts = seriesRowFacts(
|
const facts = seriesRowFacts(summary({ bookCount: 4, readCount: 2, ownedVolumeCount: 2, readVolumeCount: 1 }))
|
||||||
summary({
|
|
||||||
bookCount: 4,
|
|
||||||
readCount: 1,
|
|
||||||
volumes: [slot({ index: 1, bookId: 1, status: 'read' }), slot({ index: 2, bookId: 2, status: 'unread' })],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(facts.ownedVolumes).toBe(2)
|
expect(facts.ownedVolumes).toBe(2)
|
||||||
expect(facts.readVolumes).toBe(1)
|
expect(facts.readVolumes).toBe(1)
|
||||||
expect(facts.percentRead).toBe(50)
|
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(
|
const facts = seriesRowFacts(
|
||||||
summary({
|
summary({ bookCount: 300, readCount: 60, ownedVolumeCount: 300, readVolumeCount: 60, volumes, volumesTruncated: true }),
|
||||||
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,
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(facts.ownedVolumes).toBe(2)
|
expect(facts.ownedVolumes).toBe(300)
|
||||||
expect(facts.hasGaps).toBe(true)
|
expect(facts.readVolumes).toBe(60)
|
||||||
|
expect(facts.percentRead).toBe(20)
|
||||||
expect(facts.isComplete).toBe(false)
|
expect(facts.isComplete).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('falls back to the book counts when there is no ladder to count', () => {
|
it('reports the volumes in progress', () => {
|
||||||
const facts = seriesRowFacts(summary({ bookCount: 9, readCount: 4, readingCount: 1, volumes: [], volumesTruncated: true }))
|
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.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', () => {
|
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({ ownedVolumeCount: 2, readVolumeCount: 2 })).isComplete).toBe(true)
|
||||||
expect(seriesRowFacts(summary({ volumes: read })).isComplete).toBe(true)
|
expect(seriesRowFacts(summary({ ownedVolumeCount: 0, readVolumeCount: 0, bookCount: 0 })).isComplete).toBe(false)
|
||||||
expect(seriesRowFacts(summary({ volumes: [], bookCount: 0, readCount: 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
|
* What a row actually shows, derived once so the grid card and the list row can never
|
||||||
* disagree about a series.
|
* disagree about a series.
|
||||||
*
|
*
|
||||||
* The volume counts come from the ladder rather than from `bookCount`, because a library holding
|
* The volume counts are the server's logical-volume counts rather than `bookCount`, because a
|
||||||
* the same volume twice - one ebook, one audiobook - owns two books and one volume, and "3 of 7"
|
* library holding the same volume twice - one ebook, one audiobook - owns two books and one
|
||||||
* only reads correctly when it counts volumes. When there is no ladder to count, the book counts
|
* volume, and "3 of 7" only reads correctly when it counts volumes. They are not counted from
|
||||||
* stand in.
|
* `volumes`, which is capped and would stop a long series at the cap.
|
||||||
*/
|
*/
|
||||||
export type SeriesRowFacts = {
|
export type SeriesRowFacts = {
|
||||||
ownedVolumes: number
|
ownedVolumes: number
|
||||||
@@ -23,10 +23,9 @@ export type SeriesRowFacts = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function seriesRowFacts(series: SeriesSummary): SeriesRowFacts {
|
export function seriesRowFacts(series: SeriesSummary): SeriesRowFacts {
|
||||||
const slots = series.volumes
|
const owned = series.ownedVolumeCount
|
||||||
const owned = slots.length > 0 ? slots.filter((slot) => slot.bookId !== null).length : series.bookCount
|
const read = series.readVolumeCount
|
||||||
const read = slots.length > 0 ? slots.filter((slot) => slot.status === 'read').length : series.readCount
|
const reading = series.readingVolumeCount
|
||||||
const reading = slots.length > 0 ? slots.filter((slot) => slot.status === 'reading').length : series.readingCount
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ownedVolumes: owned,
|
ownedVolumes: owned,
|
||||||
@@ -37,7 +36,7 @@ export function seriesRowFacts(series: SeriesSummary): SeriesRowFacts {
|
|||||||
isStarted: read > 0 || reading > 0,
|
isStarted: read > 0 || reading > 0,
|
||||||
isSingleVolume: owned === 1,
|
isSingleVolume: owned === 1,
|
||||||
hasGaps: series.gapCount > 0,
|
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,
|
lastAddedAt: null,
|
||||||
libraryNames: ['Novels'],
|
libraryNames: ['Novels'],
|
||||||
expectedBookCount: null,
|
expectedBookCount: null,
|
||||||
|
ownedVolumeCount: 2,
|
||||||
|
readVolumeCount: 1,
|
||||||
|
readingVolumeCount: 0,
|
||||||
volumes: [
|
volumes: [
|
||||||
{ index: 1, bookId: 10, title: 'One', status: 'read' },
|
{ index: 1, bookId: 10, title: 'One', status: 'read' },
|
||||||
{ index: 2, bookId: null, title: null, status: 'missing' },
|
{ index: 2, bookId: null, title: null, status: 'missing' },
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ export type SeriesSummary = {
|
|||||||
libraryNames: string[];
|
libraryNames: string[];
|
||||||
/** Total a metadata provider reports for the series, or null when no provider has said. */
|
/** Total a metadata provider reports for the series, or null when no provider has said. */
|
||||||
expectedBookCount: number | null;
|
expectedBookCount: number | null;
|
||||||
|
/**
|
||||||
|
* Logical volumes owned across the whole series, collapsing book records that share a
|
||||||
|
* whole-numbered position. Unlike `volumes`, never capped; falls back to `bookCount` when the
|
||||||
|
* members could not all be read.
|
||||||
|
*/
|
||||||
|
ownedVolumeCount: number;
|
||||||
|
/** Owned logical volumes whose furthest status is read. */
|
||||||
|
readVolumeCount: number;
|
||||||
|
/** Owned logical volumes whose furthest status is reading. */
|
||||||
|
readingVolumeCount: number;
|
||||||
volumes: SeriesVolumeSlot[];
|
volumes: SeriesVolumeSlot[];
|
||||||
volumesTruncated: boolean;
|
volumesTruncated: boolean;
|
||||||
/** Missing numbers, capped at {@link SERIES_GAP_PREVIEW_LIMIT}. */
|
/** Missing numbers, capped at {@link SERIES_GAP_PREVIEW_LIMIT}. */
|
||||||
|
|||||||
@@ -225,6 +225,36 @@ describe('SeriesService', () => {
|
|||||||
expect(item.volumesTruncated).toBe(true);
|
expect(item.volumesTruncated).toBe(true);
|
||||||
expect(item.gapCount).toBe(0);
|
expect(item.gapCount).toBe(0);
|
||||||
expect(item.nextBookId).toBe(31);
|
expect(item.nextBookId).toBe(31);
|
||||||
|
expect(item).toMatchObject({ ownedVolumeCount: 900, readVolumeCount: 0, readingVolumeCount: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports the true volume counts for a series longer than the ladder cap', async () => {
|
||||||
|
seriesRepo.findPage.mockResolvedValue({
|
||||||
|
items: [
|
||||||
|
summaryRow({
|
||||||
|
id: 10,
|
||||||
|
name: 'Hellblazer',
|
||||||
|
bookCount: 300,
|
||||||
|
readCount: 60,
|
||||||
|
members: Array.from({ length: 300 }, (_, i) => ({
|
||||||
|
bookId: 100 + i,
|
||||||
|
seriesIndex: String(i + 1),
|
||||||
|
title: `Issue ${i + 1}`,
|
||||||
|
status: i < 60 ? 'read' : null,
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
facets: { ...EMPTY_FACETS, all: 1, inProgress: 1 },
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = (await service.findAll(reqUser(), {})).items[0]!;
|
||||||
|
|
||||||
|
expect(item.volumes).toHaveLength(60);
|
||||||
|
expect(item.volumesTruncated).toBe(true);
|
||||||
|
expect(item).toMatchObject({ ownedVolumeCount: 300, readVolumeCount: 60, readingVolumeCount: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('scopes to specific library when libraryId provided', async () => {
|
it('scopes to specific library when libraryId provided', async () => {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export class SeriesService {
|
|||||||
bookCount: row.bookCount,
|
bookCount: row.bookCount,
|
||||||
expectedBookCount: row.expectedBookCount,
|
expectedBookCount: row.expectedBookCount,
|
||||||
});
|
});
|
||||||
|
const counts = ladder.counts ?? { ownedVolumeCount: row.bookCount, readVolumeCount: row.readCount, readingVolumeCount: row.readingCount };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -75,6 +76,9 @@ export class SeriesService {
|
|||||||
lastAddedAt: row.lastAddedAt ?? null,
|
lastAddedAt: row.lastAddedAt ?? null,
|
||||||
libraryNames: row.libraryNames,
|
libraryNames: row.libraryNames,
|
||||||
expectedBookCount: normalizeSeriesTotalBooks(row.expectedBookCount) ?? null,
|
expectedBookCount: normalizeSeriesTotalBooks(row.expectedBookCount) ?? null,
|
||||||
|
ownedVolumeCount: counts.ownedVolumeCount,
|
||||||
|
readVolumeCount: counts.readVolumeCount,
|
||||||
|
readingVolumeCount: counts.readingVolumeCount,
|
||||||
volumes: ladder.volumes,
|
volumes: ladder.volumes,
|
||||||
volumesTruncated: ladder.truncated,
|
volumesTruncated: ladder.truncated,
|
||||||
gaps: ladder.gaps.slice(0, SERIES_GAP_PREVIEW_LIMIT),
|
gaps: ladder.gaps.slice(0, SERIES_GAP_PREVIEW_LIMIT),
|
||||||
@@ -191,7 +195,8 @@ export class SeriesService {
|
|||||||
name: detail.name,
|
name: detail.name,
|
||||||
bookCount: detail.bookCount,
|
bookCount: detail.bookCount,
|
||||||
readCount: detail.readCount,
|
readCount: detail.readCount,
|
||||||
...volumeCounts,
|
ownedVolumeCount: volumeCounts.ownedVolumeCount,
|
||||||
|
readVolumeCount: volumeCounts.readVolumeCount,
|
||||||
authors: detail.authors,
|
authors: detail.authors,
|
||||||
possibleGaps,
|
possibleGaps,
|
||||||
expectedBookCount: detail.expectedBookCount ?? null,
|
expectedBookCount: detail.expectedBookCount ?? null,
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ describe('buildVolumeLadder', () => {
|
|||||||
expect(ladder.truncated).toBe(true);
|
expect(ladder.truncated).toBe(true);
|
||||||
expect(ladder.gaps).toEqual([]);
|
expect(ladder.gaps).toEqual([]);
|
||||||
expect(ladder.next?.bookId).toBe(1);
|
expect(ladder.next?.bookId).toBe(1);
|
||||||
|
expect(ladder.counts).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('caps a very long ladder and says so', () => {
|
it('caps a very long ladder and says so', () => {
|
||||||
@@ -100,6 +101,15 @@ describe('buildVolumeLadder', () => {
|
|||||||
expect(ladder.truncated).toBe(true);
|
expect(ladder.truncated).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('counts every volume of a capped ladder, not just the drawn rungs', () => {
|
||||||
|
const members = Array.from({ length: 80 }, (_, i) => member(i + 1, String(i + 1), i < 60 ? 'read' : i === 60 ? 'reading' : null));
|
||||||
|
members.push(member(81, '1'), member(82, null, 'read'));
|
||||||
|
const ladder = build(members);
|
||||||
|
|
||||||
|
expect(ladder.counts).toEqual({ ownedVolumeCount: 81, readVolumeCount: 61, readingVolumeCount: 1 });
|
||||||
|
expect(ladder.counts).toEqual(countSeriesVolumes({ members, bookCount: members.length, expectedBookCount: null }));
|
||||||
|
});
|
||||||
|
|
||||||
describe('next volume', () => {
|
describe('next volume', () => {
|
||||||
it('prefers the one already in progress over an earlier unread one', () => {
|
it('prefers the one already in progress over an earlier unread one', () => {
|
||||||
const ladder = build([member(1, '1'), member(2, '2', 'reading')]);
|
const ladder = build([member(1, '1'), member(2, '2', 'reading')]);
|
||||||
@@ -130,6 +140,7 @@ describe('countSeriesVolumes', () => {
|
|||||||
expect(countSeriesVolumes({ members, bookCount: 4, expectedBookCount: 7 })).toEqual({
|
expect(countSeriesVolumes({ members, bookCount: 4, expectedBookCount: 7 })).toEqual({
|
||||||
ownedVolumeCount: 2,
|
ownedVolumeCount: 2,
|
||||||
readVolumeCount: 1,
|
readVolumeCount: 1,
|
||||||
|
readingVolumeCount: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -139,6 +150,7 @@ describe('countSeriesVolumes', () => {
|
|||||||
expect(countSeriesVolumes({ members, bookCount: 5, expectedBookCount: null })).toEqual({
|
expect(countSeriesVolumes({ members, bookCount: 5, expectedBookCount: null })).toEqual({
|
||||||
ownedVolumeCount: 3,
|
ownedVolumeCount: 3,
|
||||||
readVolumeCount: 2,
|
readVolumeCount: 2,
|
||||||
|
readingVolumeCount: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,6 +160,7 @@ describe('countSeriesVolumes', () => {
|
|||||||
expect(countSeriesVolumes({ members, bookCount: 5, expectedBookCount: null })).toEqual({
|
expect(countSeriesVolumes({ members, bookCount: 5, expectedBookCount: null })).toEqual({
|
||||||
ownedVolumeCount: 5,
|
ownedVolumeCount: 5,
|
||||||
readVolumeCount: 3,
|
readVolumeCount: 3,
|
||||||
|
readingVolumeCount: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,6 +170,7 @@ describe('countSeriesVolumes', () => {
|
|||||||
expect(countSeriesVolumes({ members, bookCount: 3, expectedBookCount: null })).toEqual({
|
expect(countSeriesVolumes({ members, bookCount: 3, expectedBookCount: null })).toEqual({
|
||||||
ownedVolumeCount: 3,
|
ownedVolumeCount: 3,
|
||||||
readVolumeCount: 2,
|
readVolumeCount: 2,
|
||||||
|
readingVolumeCount: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -164,6 +178,7 @@ describe('countSeriesVolumes', () => {
|
|||||||
expect(countSeriesVolumes({ members: [], bookCount: 0, expectedBookCount: null })).toEqual({
|
expect(countSeriesVolumes({ members: [], bookCount: 0, expectedBookCount: null })).toEqual({
|
||||||
ownedVolumeCount: 0,
|
ownedVolumeCount: 0,
|
||||||
readVolumeCount: 0,
|
readVolumeCount: 0,
|
||||||
|
readingVolumeCount: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ import { SERIES_VOLUME_SLOT_LIMIT } from '@bookorbit/types';
|
|||||||
import type { SeriesMemberRow, SeriesVolumeMember } from '../series.repository';
|
import type { SeriesMemberRow, SeriesVolumeMember } from '../series.repository';
|
||||||
import { computeSeriesGaps, resolveLadderRange } from './series-gaps.utils';
|
import { computeSeriesGaps, resolveLadderRange } from './series-gaps.utils';
|
||||||
|
|
||||||
|
export type SeriesVolumeCounts = {
|
||||||
|
ownedVolumeCount: number;
|
||||||
|
readVolumeCount: number;
|
||||||
|
readingVolumeCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type SeriesLadder = {
|
export type SeriesLadder = {
|
||||||
volumes: SeriesVolumeSlot[];
|
volumes: SeriesVolumeSlot[];
|
||||||
truncated: boolean;
|
truncated: boolean;
|
||||||
gaps: number[];
|
gaps: number[];
|
||||||
next: { bookId: number; index: SeriesIndex | null; title: string | null } | null;
|
next: { bookId: number; index: SeriesIndex | null; title: string | null } | null;
|
||||||
};
|
/** Logical volumes across the whole series, not just the capped slots; null when the members could not all be read. */
|
||||||
|
counts: SeriesVolumeCounts | null;
|
||||||
export type SeriesVolumeCounts = {
|
|
||||||
ownedVolumeCount: number;
|
|
||||||
readVolumeCount: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_RANK: Record<SeriesVolumeStatus, number> = { read: 3, reading: 2, unread: 1, missing: 0 };
|
const STATUS_RANK: Record<SeriesVolumeStatus, number> = { read: 3, reading: 2, unread: 1, missing: 0 };
|
||||||
@@ -62,6 +65,16 @@ function groupSeriesMembers<T extends SeriesVolumeMember>(params: {
|
|||||||
return { gaps, range, byIndex, unnumbered };
|
return { gaps, range, byIndex, unnumbered };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tallyVolumes(grouped: { byIndex: ReadonlyMap<number, SeriesVolumeMember>; unnumbered: readonly SeriesVolumeMember[] }): SeriesVolumeCounts {
|
||||||
|
const statuses = [...grouped.byIndex.values(), ...grouped.unnumbered].map((member) => memberStatus(member.status));
|
||||||
|
|
||||||
|
return {
|
||||||
|
ownedVolumeCount: statuses.length,
|
||||||
|
readVolumeCount: statuses.filter((status) => status === 'read').length,
|
||||||
|
readingVolumeCount: statuses.filter((status) => status === 'reading').length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counts logical volumes rather than book records. Numbered copies share one volume and keep the
|
* Counts logical volumes rather than book records. Numbered copies share one volume and keep the
|
||||||
* furthest reading state, while books without a usable whole-numbered position stay independent.
|
* furthest reading state, while books without a usable whole-numbered position stay independent.
|
||||||
@@ -71,13 +84,7 @@ export function countSeriesVolumes(params: {
|
|||||||
bookCount: number;
|
bookCount: number;
|
||||||
expectedBookCount: number | null;
|
expectedBookCount: number | null;
|
||||||
}): SeriesVolumeCounts {
|
}): SeriesVolumeCounts {
|
||||||
const grouped = groupSeriesMembers(params);
|
return tallyVolumes(groupSeriesMembers(params));
|
||||||
const owned = [...grouped.byIndex.values(), ...grouped.unnumbered];
|
|
||||||
|
|
||||||
return {
|
|
||||||
ownedVolumeCount: owned.length,
|
|
||||||
readVolumeCount: owned.filter((member) => memberStatus(member.status) === 'read').length,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,16 +104,17 @@ export function buildVolumeLadder(params: {
|
|||||||
const { members, bookCount, expectedBookCount } = params;
|
const { members, bookCount, expectedBookCount } = params;
|
||||||
|
|
||||||
if (members.length === 0) {
|
if (members.length === 0) {
|
||||||
return { volumes: [], truncated: params.truncated, gaps: [], next: null };
|
return { volumes: [], truncated: params.truncated, gaps: [], next: null, counts: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// A series we could not read in full can still be counted, but naming a volume missing needs
|
// A series we could not read in full can still be counted, but naming a volume missing needs
|
||||||
// every sibling in hand, so a truncated one draws no ladder rather than a wrong one.
|
// every sibling in hand, so a truncated one draws no ladder rather than a wrong one.
|
||||||
if (params.truncated) {
|
if (params.truncated) {
|
||||||
return { volumes: [], truncated: true, gaps: [], next: firstOpenMember(members) };
|
return { volumes: [], truncated: true, gaps: [], next: firstOpenMember(members), counts: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { gaps, range, byIndex, unnumbered } = groupSeriesMembers({ members, bookCount, expectedBookCount });
|
const grouped = groupSeriesMembers({ members, bookCount, expectedBookCount });
|
||||||
|
const { gaps, range, byIndex, unnumbered } = grouped;
|
||||||
|
|
||||||
const volumes: SeriesVolumeSlot[] = [];
|
const volumes: SeriesVolumeSlot[] = [];
|
||||||
let truncated = false;
|
let truncated = false;
|
||||||
@@ -134,7 +142,7 @@ export function buildVolumeLadder(params: {
|
|||||||
volumes.push({ index: null, bookId: member.bookId, title: member.title, status: memberStatus(member.status) });
|
volumes.push({ index: null, bookId: member.bookId, title: member.title, status: memberStatus(member.status) });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { volumes, truncated, gaps, next: nextFromSlots(volumes, members) };
|
return { volumes, truncated, gaps, next: nextFromSlots(volumes, members), counts: tallyVolumes(grouped) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user