- Corriger la troncature de la toolbar (max-height 4rem → 5rem) - Animer la toolbar en translateY pour un effet "bloc uni" avec le header - Corriger le bug d'auto-hide du header après switch simple → scroll - Augmenter la taille du titre de chapitre dans la toolbar (text-sm font-medium) - Harmoniser le bouton scroll-to-top avec le style des ToolbarButtons - Ajouter support de prop `class` sur les labels de ToolbarSection
234 lines
6.8 KiB
Vue
234 lines
6.8 KiB
Vue
<template>
|
|
<div class="single-mode-reader">
|
|
<!-- Zone cliquable pour navigation -->
|
|
<div class="page-navigation-wrapper" @click="handlePageClick">
|
|
<!-- Zone de navigation gauche (invisible) -->
|
|
<div
|
|
class="navigation-zone left-zone"
|
|
@click.stop="onLeftZoneClick"
|
|
@mouseenter="showLeftHint"
|
|
@mouseleave="hideLeftHint"
|
|
:title="isRtl ? 'Page suivante' : 'Page précédente'"
|
|
></div>
|
|
|
|
<!-- Page centrale -->
|
|
<div class="page-content">
|
|
<ReaderPage
|
|
:page-data="pageData"
|
|
:page-number="pageNumber"
|
|
:zoom="zoom"
|
|
:double-page-mode="doublePageMode"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Zone de navigation droite (invisible) -->
|
|
<div
|
|
class="navigation-zone right-zone"
|
|
@click.stop="onRightZoneClick"
|
|
@mouseenter="showRightHint"
|
|
@mouseleave="hideRightHint"
|
|
:title="isRtl ? 'Page précédente' : 'Page suivante'"
|
|
></div>
|
|
</div>
|
|
|
|
<!-- Indicateurs visuels de navigation -->
|
|
<div class="navigation-hints">
|
|
<div class="hint left-hint" v-if="canGoLeft && (showNavigationHints || showLeftHintHover)">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
</div>
|
|
<div class="hint right-hint" v-if="canGoRight && (showNavigationHints || showRightHintHover)">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue';
|
|
import { useReaderStore } from '../../application/store/readerStore';
|
|
import ReaderPage from './ReaderPage.vue';
|
|
|
|
const props = defineProps({
|
|
pageData: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
pageNumber: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
zoom: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
doublePageMode: {
|
|
type: String,
|
|
required: true
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['buttonClick']);
|
|
|
|
const store = useReaderStore();
|
|
|
|
// État pour afficher les indicateurs de navigation
|
|
const showNavigationHints = ref(false);
|
|
const showLeftHintHover = ref(false);
|
|
const showRightHintHover = ref(false);
|
|
let hintTimeout = null;
|
|
|
|
const isRtl = computed(() => store.readingDirection === 'rtl');
|
|
|
|
// Computed pour vérifier les possibilités de navigation
|
|
const canGoToPrevious = computed(() => !store.isFirstPage || store.hasPreviousChapter);
|
|
const canGoToNext = computed(() => !store.isLastPage || store.hasNextChapter);
|
|
|
|
// En RTL, le côté gauche avance dans l'histoire (page suivante) et le droit recule
|
|
const canGoLeft = computed(() => isRtl.value ? canGoToNext.value : canGoToPrevious.value);
|
|
const canGoRight = computed(() => isRtl.value ? canGoToPrevious.value : canGoToNext.value);
|
|
|
|
const onLeftZoneClick = () => isRtl.value ? goToNext() : goToPrevious();
|
|
const onRightZoneClick = () => isRtl.value ? goToPrevious() : goToNext();
|
|
|
|
// Navigation vers la page/chapitre précédent
|
|
const goToPrevious = async () => {
|
|
if (!store.isFirstPage) {
|
|
// Page précédente dans le même chapitre
|
|
await store.previousPage();
|
|
} else if (store.hasPreviousChapter) {
|
|
// Chapitre précédent (le store gère automatiquement la navigation vers la dernière page)
|
|
await store.goToPreviousChapter();
|
|
}
|
|
showNavigationHints.value = true;
|
|
emit('buttonClick'); // Signaler l'interaction pour afficher les boutons
|
|
clearTimeout(hintTimeout);
|
|
hintTimeout = setTimeout(() => {
|
|
showNavigationHints.value = false;
|
|
}, 1000);
|
|
};
|
|
|
|
// Navigation vers la page/chapitre suivant
|
|
const goToNext = async () => {
|
|
if (!store.isLastPage) {
|
|
// Page suivante dans le même chapitre
|
|
await store.nextPage();
|
|
} else if (store.hasNextChapter) {
|
|
// Première page du chapitre suivant
|
|
await store.goToNextChapter();
|
|
// Le store va charger le chapitre suivant et se positionner automatiquement à la première page
|
|
}
|
|
showNavigationHints.value = true;
|
|
emit('buttonClick'); // Signaler l'interaction pour afficher les boutons
|
|
clearTimeout(hintTimeout);
|
|
hintTimeout = setTimeout(() => {
|
|
showNavigationHints.value = false;
|
|
}, 1000);
|
|
};
|
|
|
|
// Gestion du clic général sur la page (fallback)
|
|
const handlePageClick = (event) => {
|
|
// Si le clic n'a pas été intercepté par les zones, on navigue vers la page suivante
|
|
goToNext();
|
|
};
|
|
|
|
// Gestion des hints au hover
|
|
const showLeftHint = () => {
|
|
showLeftHintHover.value = true;
|
|
};
|
|
|
|
const hideLeftHint = () => {
|
|
showLeftHintHover.value = false;
|
|
};
|
|
|
|
const showRightHint = () => {
|
|
showRightHintHover.value = true;
|
|
};
|
|
|
|
const hideRightHint = () => {
|
|
showRightHintHover.value = false;
|
|
};
|
|
</script>
|
|
|
|
<style lang="postcss" scoped>
|
|
.single-mode-reader {
|
|
@apply relative w-full flex-1 flex flex-col min-h-0 overflow-hidden;
|
|
@apply py-2;
|
|
}
|
|
|
|
.page-navigation-wrapper {
|
|
/* overflow-auto : scrollbars quand l'image zoomée déborde */
|
|
@apply relative w-full flex-1 min-h-0 overflow-auto cursor-pointer;
|
|
}
|
|
|
|
.page-content {
|
|
/* min-h-full : centre l'image quand elle est plus petite que le conteneur */
|
|
min-height: 100%;
|
|
@apply flex items-center justify-center;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.navigation-zone {
|
|
@apply absolute top-0 bottom-0 z-10;
|
|
width: 33%; /* 1/3 de la largeur pour chaque zone */
|
|
}
|
|
|
|
.left-zone {
|
|
@apply left-0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.right-zone {
|
|
@apply right-0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Indicateurs visuels de navigation */
|
|
.navigation-hints {
|
|
@apply absolute inset-0 pointer-events-none z-20;
|
|
}
|
|
|
|
.hint {
|
|
@apply absolute top-1/2 transform -translate-y-1/2;
|
|
@apply bg-black/50 text-white p-2 rounded-full;
|
|
@apply transition-all duration-300;
|
|
}
|
|
|
|
.left-hint {
|
|
@apply left-4;
|
|
animation: slideInLeft 0.3s ease-out;
|
|
}
|
|
|
|
.right-hint {
|
|
@apply right-4;
|
|
animation: slideInRight 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes slideInLeft {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-50%) translateX(-20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(-50%) translateX(0);
|
|
}
|
|
}
|
|
|
|
@keyframes slideInRight {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-50%) translateX(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(-50%) translateX(0);
|
|
}
|
|
}
|
|
|
|
/* Pas d'effet hover background - les flèches apparaissent à la place */
|
|
</style>
|