feat(books): marquage des livres comme lus — statut lu/non lu par utilisateur sur progress (colonnes read_status/completed_at/read_status_source, migration idempotente avec backfill à 95 %), endpoint PUT /progress/:bookId/status, complétion automatique au fil de la progression, état lu exposé sur catalogue/recherche/fiche/séries, étagère « continuer » limitée aux lectures en cours, badge Lu sur cartes/fiche/accueil et bouton Marquer comme lu/non lu sur la fiche, avec tests api/shared/web — évolution #52
This commit is contained in:
@ -95,6 +95,9 @@ export class DatabaseService implements OnModuleDestroy {
|
||||
book_id INTEGER NOT NULL REFERENCES books(id) ON DELETE CASCADE,
|
||||
locator TEXT NOT NULL,
|
||||
percent INTEGER NOT NULL CHECK (percent >= 0 AND percent <= 100),
|
||||
read_status TEXT NOT NULL DEFAULT 'unread' CHECK (read_status IN ('unread','reading','read')),
|
||||
completed_at TEXT,
|
||||
read_status_source TEXT CHECK (read_status_source IN ('progress','manual')),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(user_id, book_id)
|
||||
@ -165,6 +168,7 @@ export class DatabaseService implements OnModuleDestroy {
|
||||
this.ensureBooksMetadataColumns();
|
||||
this.ensureSeriesModel();
|
||||
this.ensureReaderPreferencesTable();
|
||||
this.ensureProgressReadStatusColumns();
|
||||
this.ensureMetadataSourceConfigSupportsComicProviders();
|
||||
this.ensureMetadataSourceConfigColumns();
|
||||
this.ensureAutomationSettingsColumns();
|
||||
@ -375,6 +379,35 @@ export class DatabaseService implements OnModuleDestroy {
|
||||
`);
|
||||
}
|
||||
|
||||
private ensureProgressReadStatusColumns(): void {
|
||||
const names = this.columnNames("progress");
|
||||
if (!names.has("read_status")) {
|
||||
this.sqlite.exec("ALTER TABLE progress ADD COLUMN read_status TEXT NOT NULL DEFAULT 'unread'");
|
||||
}
|
||||
if (!names.has("completed_at")) {
|
||||
this.sqlite.exec("ALTER TABLE progress ADD COLUMN completed_at TEXT");
|
||||
}
|
||||
if (!names.has("read_status_source")) {
|
||||
this.sqlite.exec("ALTER TABLE progress ADD COLUMN read_status_source TEXT");
|
||||
}
|
||||
this.sqlite.exec(`
|
||||
UPDATE progress
|
||||
SET
|
||||
read_status = CASE
|
||||
WHEN percent >= 95 THEN 'read'
|
||||
WHEN percent > 0 THEN 'reading'
|
||||
ELSE 'unread'
|
||||
END,
|
||||
completed_at = CASE
|
||||
WHEN percent >= 95 THEN COALESCE(completed_at, updated_at)
|
||||
ELSE NULL
|
||||
END,
|
||||
read_status_source = COALESCE(read_status_source, 'progress')
|
||||
WHERE read_status_source IS NULL
|
||||
`);
|
||||
this.sqlite.exec("CREATE INDEX IF NOT EXISTS progress_user_status_idx ON progress(user_id, read_status)");
|
||||
}
|
||||
|
||||
private ensureMetadataSourceConfigColumns(): void {
|
||||
const names = this.columnNames("metadata_source_config");
|
||||
const now = sqlString(this.now());
|
||||
|
||||
Reference in New Issue
Block a user