refactor(reader): serve pages as static files instead of base64

Replace the per-page API call (base64 payload) with static image URLs
served directly by Caddy from public/images/pages/{chapterId}/.

- LocalImageStorage now stores to public/images/ (was MANGA_DATA_PATH)
- LegacyChapterRepository returns /images/pages/{id}/{file} URLs,
  uses getimagesize() instead of loading file content into memory
- Delete GetChapterPage query/handler/response, ChapterPageResource,
  ChapterPageProvider, PageContent model
- Remove getPageContent() from ChapterRepositoryInterface
- Frontend: loadChapter() fetches chapter + all pages in parallel,
  ReaderPage uses URL instead of base64 data URI, InfiniteReader drops
  lazy-load observer side effect, readerStore drops loadedPages/preload
- GetChapterPagesTest: extract fixture images from CBZ at runtime,
  ignore tests/Fixtures/pages/ in .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2026-03-09 22:05:45 +01:00
parent 6875ad4222
commit 322c396165
19 changed files with 300 additions and 755 deletions

View File

@@ -13,7 +13,6 @@ export const useReaderStore = defineStore('reader', {
error: null,
pages: [],
totalPages: 0,
loadedPages: new Set(), // Garder une trace des pages déjà chargées
// Paramètres pour les doubles pages
doublePageSettings: {
@@ -32,7 +31,6 @@ export const useReaderStore = defineStore('reader', {
// Getters pour les doubles pages
effectiveDoublePageMode: (state) => {
// Si la détection automatique est désactivée, retourner 'normal'
if (!state.doublePageSettings.autoDetect) {
return 'normal';
}
@@ -55,28 +53,20 @@ export const useReaderStore = defineStore('reader', {
try {
const repository = new ApiChapterRepository();
// Charger les informations du chapitre
const chapterData = await repository.getChapter(chapterId);
const [chapterData, pagesData] = await Promise.all([
repository.getChapter(chapterId),
repository.getChapterPages(chapterId, 1, 9999),
]);
this.currentChapter = Chapter.create(chapterData);
// Charger la liste des pages
const pagesData = await repository.getChapterPages(chapterId);
// Initialiser le tableau avec des placeholders
this.pages = new Array(pagesData.totalItems).fill(null);
this.pages = pagesData.pages.map(p => ({
id: p.id,
pageNumber: p.pageNumber,
url: p.url,
dimensions: p.dimensions,
}));
this.totalPages = pagesData.totalItems;
this.loadedPages.clear();
// Charger la première page
if (this.totalPages > 0) {
this.currentPage = 0;
await this.loadPageData(0);
// En mode infini, précharger les premières pages
if (this.readingMode === 'infinite') {
await this.preloadNextPages(0);
}
}
this.currentPage = 0;
} catch (error) {
this.error = error.message;
} finally {
@@ -84,100 +74,28 @@ export const useReaderStore = defineStore('reader', {
}
},
async loadPageData(pageIndex) {
if (!this.currentChapter || pageIndex < 0 || pageIndex >= this.totalPages) {
return;
}
// Si la page est déjà chargée, ne rien faire
if (this.loadedPages.has(pageIndex)) {
return;
}
const pageNumber = pageIndex + 1; // Convertir en 1-based pour l'API
// Marquer la page comme en cours de chargement
const newPages = [...this.pages];
newPages[pageIndex] = { loading: true };
this.pages = newPages;
try {
const repository = new ApiChapterRepository();
const pageData = await repository.getChapterPage(this.currentChapter.id, pageNumber);
// Vérifier que les données sont valides
if (!pageData || !pageData.base64Content) {
throw new Error("Données de page invalides reçues de l'API");
}
// Mettre à jour la page
const updatedPages = [...this.pages];
updatedPages[pageIndex] = {
id: pageData.id,
pageNumber: pageData.pageNumber,
base64Content: pageData.base64Content,
mimeType: pageData.mimeType,
dimensions: pageData.dimensions
};
this.pages = updatedPages;
this.loadedPages.add(pageIndex);
} catch (error) {
console.error(`Erreur lors du chargement de la page ${pageNumber}:`, error);
// Marquer la page comme en erreur
const errorPages = [...this.pages];
errorPages[pageIndex] = { error: error.message };
this.pages = errorPages;
}
},
async preloadNextPages(startIndex, count = 3) {
const promises = [];
for (let i = 1; i <= count; i++) {
const pageIndex = startIndex + i;
if (pageIndex < this.totalPages) {
promises.push(this.loadPageData(pageIndex));
}
}
await Promise.all(promises);
},
async handlePageVisible(pageIndex) {
handlePageVisible(pageIndex) {
if (pageIndex !== this.currentPage) {
this.currentPage = pageIndex;
// Précharger les pages suivantes
if (this.readingMode === 'infinite') {
await this.preloadNextPages(pageIndex);
}
}
},
async nextPage() {
nextPage() {
if (!this.isLastPage) {
this.currentPage++;
await this.loadPageData(this.currentPage);
}
},
async previousPage() {
previousPage() {
if (!this.isFirstPage) {
this.currentPage--;
await this.loadPageData(this.currentPage);
}
},
async setReadingMode(mode) {
if (mode === this.readingMode) return;
this.readingMode = mode;
this.savePreferences();
// S'assurer que la page courante est chargée
await this.loadPageData(this.currentPage);
// Si on passe en mode infini, précharger les pages suivantes
if (mode === 'infinite') {
await this.preloadNextPages(this.currentPage);
}
},
setReadingDirection(direction) {
@@ -190,7 +108,6 @@ export const useReaderStore = defineStore('reader', {
this.savePreferences();
},
// Nouvelles actions pour les doubles pages
setDoublePageMode(mode) {
if (['rotate', 'scroll', 'normal'].includes(mode)) {
this.doublePageSettings.mobileMode = mode;
@@ -225,16 +142,10 @@ export const useReaderStore = defineStore('reader', {
async goToPreviousChapter() {
if (this.currentChapter?.navigation?.previousChapter) {
await this.loadChapter(this.currentChapter.navigation.previousChapter);
// Aller à la dernière page du chapitre précédent
this.currentPage = Math.max(0, this.totalPages - 1);
// S'assurer que la page est chargée
if (this.totalPages > 0) {
await this.loadPageData(this.currentPage);
}
}
},
// Gestion de la persistance des préférences
savePreferences() {
try {
const preferences = {
@@ -255,7 +166,6 @@ export const useReaderStore = defineStore('reader', {
if (stored) {
const preferences = JSON.parse(stored);
// Appliquer les préférences sauvegardées
if (preferences.readingMode) this.readingMode = preferences.readingMode;
if (preferences.readingDirection) this.readingDirection = preferences.readingDirection;
if (typeof preferences.zoom === 'number') this.zoom = preferences.zoom;
@@ -277,7 +187,6 @@ export const useReaderStore = defineStore('reader', {
}
},
// Réinitialiser les préférences
resetPreferences() {
this.readingMode = 'single';
this.readingDirection = 'ltr';