fix(web,api): lecteur — worker pdf.js dédié, shell commun et préférences par livre

Régression worker PDF : le worker pdf.js est désormais instancié une
seule fois via un port dédié (?worker&url) et reconfiguré à chaque
montage, au lieu d'un workerSrc recalculé qui cassait le rendu.

- ReaderShell : chrome commun aux lecteurs (toolbar, zones de tap,
  statut) et contrat ReaderControls pour EPUB/PDF/CBZ
- préférences de lecture par livre (mode horizontal/vertical, fit) :
  table reader_preferences + migrations idempotentes, module API,
  DTO partagés, client web avec fallback localStorage hors-ligne

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Git Agent
2026-08-23 18:06:22 +02:00
parent 8024cab11c
commit 5de46a6f6d
19 changed files with 790 additions and 89 deletions

View File

@ -143,6 +143,7 @@ export class DatabaseService implements OnModuleDestroy {
`);
this.ensureBooksSupportsComicArchives();
this.ensureBooksMetadataColumns();
this.ensureReaderPreferencesTable();
this.ensureMetadataSourceConfigColumns();
this.ensureAutomationSettingsColumns();
this.ensureMetadataDefaults();
@ -235,6 +236,22 @@ export class DatabaseService implements OnModuleDestroy {
this.sqlite.exec("CREATE INDEX IF NOT EXISTS books_isbn13_idx ON books(isbn13)");
}
private ensureReaderPreferencesTable(): void {
this.sqlite.exec(`
CREATE TABLE IF NOT EXISTS reader_preferences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
book_id INTEGER NOT NULL REFERENCES books(id) ON DELETE CASCADE,
mode TEXT NOT NULL CHECK (mode IN ('paged','scrolled','horizontal','vertical')),
fit TEXT CHECK (fit IN ('page','width','height','auto')),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(user_id, book_id)
);
CREATE INDEX IF NOT EXISTS reader_preferences_user_idx ON reader_preferences(user_id);
`);
}
private ensureMetadataSourceConfigColumns(): void {
const names = this.columnNames("metadata_source_config");
const now = sqlString(this.now());

View File

@ -50,6 +50,24 @@ export const books = sqliteTable(
(table) => ({ filePathIdx: uniqueIndex("books_file_path_unique").on(table.filePath) })
);
export const readerPreferences = sqliteTable(
"reader_preferences",
{
id: integer("id").primaryKey({ autoIncrement: true }),
userId: integer("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
bookId: integer("book_id")
.notNull()
.references(() => books.id, { onDelete: "cascade" }),
mode: text("mode", { enum: ["paged", "scrolled", "horizontal", "vertical"] }).notNull(),
fit: text("fit", { enum: ["page", "width", "height", "auto"] }),
createdAt: text("created_at").notNull(),
updatedAt: text("updated_at").notNull()
},
(table) => ({ userBookIdx: uniqueIndex("reader_preferences_user_book_unique").on(table.userId, table.bookId) })
);
export const progress = sqliteTable(
"progress",
{