- Windowing côté rendu : seules les pages dans une fenêtre de ±3 autour de la page visible sont montées en tant que ReaderPage ; les autres sont remplacées par des placeholders dimensionnés via aspect-ratio CSS pour maintenir la hauteur de scroll sans saut - IntersectionObserver utilise le minimum des indices intersectants pour éviter que les entrées simultanées au chargement ne décalent la fenêtre - Prop initialPage passé depuis ChapterReader pour ancrer la fenêtre sur la page courante dès le montage - loading="eager" sur les ReaderPage montés (le windowing est le mécanisme de lazy-loading, pas l'attribut HTML natif) - Prop loading bindé sur les 3 balises <img> de ReaderPage
181 lines
5.6 KiB
Vue
181 lines
5.6 KiB
Vue
<template>
|
|
<div class="chapter-reader" :class="{ rtl: store.readingDirection === 'rtl' }">
|
|
<div v-if="store.isLoading" class="loading">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
|
|
<div v-else-if="store.error" class="error">
|
|
{{ store.error }}
|
|
</div>
|
|
|
|
<div v-else class="reader-content">
|
|
<template v-if="store.readingMode === 'single'">
|
|
<SingleModeReader
|
|
:page-data="store.currentPageData"
|
|
:page-number="store.currentPage + 1"
|
|
:zoom="store.zoom"
|
|
:double-page-mode="store.effectiveDoublePageMode"
|
|
@button-click="showButtonsWithTimer" />
|
|
</template>
|
|
<template v-else>
|
|
<InfiniteReader
|
|
:pages="store.pages"
|
|
:zoom="store.zoom"
|
|
:double-page-mode="store.effectiveDoublePageMode"
|
|
:initial-page="store.currentPage"
|
|
@page-visible="store.handlePageVisible"
|
|
ref="infiniteReaderRef" />
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
import { useHeaderStore } from '../../../../shared/stores/headerStore';
|
|
import { useUserPreferencesStore } from '../../../../domain/setting/application/store/userPreferencesStore';
|
|
import { useReaderStore } from '../../application/store/readerStore';
|
|
import InfiniteReader from './InfiniteReader.vue';
|
|
import SingleModeReader from './SingleModeReader.vue';
|
|
|
|
const props = defineProps({
|
|
chapterId: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
availableChapters: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
});
|
|
|
|
const store = useReaderStore();
|
|
const headerStore = useHeaderStore();
|
|
const prefs = useUserPreferencesStore();
|
|
|
|
const infiniteReaderRef = ref(null);
|
|
|
|
// Actions de l'interface lecteur
|
|
const toggleReadingMode = () => {
|
|
const newMode = store.readingMode === 'single' ? 'infinite' : 'single';
|
|
store.setReadingMode(newMode);
|
|
prefs.setReadingMode(newMode === 'infinite' ? 'scroll' : 'single');
|
|
|
|
if (newMode === 'single') {
|
|
headerStore.disableAutoHide();
|
|
headerStore.disableReaderToolbarAutoHide();
|
|
} else {
|
|
headerStore.enableReaderToolbarAutoHide();
|
|
headerStore.enableAutoHide();
|
|
showButtonsWithTimer();
|
|
}
|
|
};
|
|
|
|
const toggleReadingDirection = () => {
|
|
const newDir = store.readingDirection === 'ltr' ? 'rtl' : 'ltr';
|
|
store.setReadingDirection(newDir);
|
|
prefs.setReadingDirection(newDir);
|
|
};
|
|
|
|
const zoomIn = () => store.setZoom(Math.min(store.zoom + 0.1, 2));
|
|
const zoomOut = () => store.setZoom(Math.max(store.zoom - 0.1, 0.5));
|
|
|
|
const handleZoomChange = (zoom) => store.setZoom(zoom);
|
|
|
|
const handleDoublePageModeChange = (mode) => store.setDoublePageMode(mode);
|
|
const handleDoublePageAutoDetectChange = (enabled) => store.setDoublePageAutoDetect(enabled);
|
|
const handleDetectionThresholdChange = (threshold) => store.setDoublePageDetectionThreshold(threshold);
|
|
const handleResetPreferences = () => store.resetPreferences();
|
|
|
|
const showButtonsWithTimer = () => {
|
|
if (store.readingMode === 'infinite' && infiniteReaderRef.value) {
|
|
infiniteReaderRef.value.showButtonsWithTimer();
|
|
}
|
|
};
|
|
|
|
const resetButtonsTimer = () => {
|
|
if (store.readingMode === 'infinite' && infiniteReaderRef.value) {
|
|
infiniteReaderRef.value.resetButtonsTimer();
|
|
}
|
|
};
|
|
|
|
const handleKeyPress = event => {
|
|
if (store.readingMode === 'single') {
|
|
if (event.key === 'ArrowRight') {
|
|
store.nextPage();
|
|
} else if (event.key === 'ArrowLeft') {
|
|
store.previousPage();
|
|
}
|
|
}
|
|
};
|
|
|
|
watch(
|
|
() => props.chapterId,
|
|
newId => {
|
|
if (newId) {
|
|
store.loadChapter(newId);
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
onMounted(() => {
|
|
store.loadPreferences();
|
|
window.addEventListener('keydown', handleKeyPress);
|
|
|
|
if (prefs.autoHideHeaderReader) {
|
|
headerStore.enableAutoHide();
|
|
}
|
|
|
|
if (store.readingMode === 'infinite') {
|
|
headerStore.enableReaderToolbarAutoHide();
|
|
}
|
|
|
|
if (prefs.autoFullscreen && document.documentElement.requestFullscreen) {
|
|
document.documentElement.requestFullscreen().catch(() => {});
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('keydown', handleKeyPress);
|
|
headerStore.disableAutoHide();
|
|
headerStore.disableReaderToolbarAutoHide();
|
|
});
|
|
|
|
defineExpose({
|
|
toggleReadingMode,
|
|
toggleReadingDirection,
|
|
zoomIn,
|
|
zoomOut,
|
|
handleZoomChange,
|
|
handleDoublePageModeChange,
|
|
handleDoublePageAutoDetectChange,
|
|
handleDetectionThresholdChange,
|
|
handleResetPreferences,
|
|
resetButtonsTimer,
|
|
showButtonsWithTimer,
|
|
});
|
|
</script>
|
|
|
|
<style lang="postcss" scoped>
|
|
.chapter-reader {
|
|
@apply w-full h-full flex flex-col bg-gray-900 text-white;
|
|
}
|
|
|
|
.loading {
|
|
@apply flex items-center justify-center h-full;
|
|
}
|
|
|
|
.error {
|
|
@apply text-red-500 text-xl;
|
|
}
|
|
|
|
.reader-content {
|
|
@apply w-full flex-1 flex flex-col min-h-0;
|
|
}
|
|
|
|
.rtl {
|
|
direction: rtl;
|
|
}
|
|
</style>
|