feat: ajout de la gestion des doubles pages pour le lecteur, incluant des paramètres de détection automatique, des modes d'affichage et des préférences sauvegardées. Amélioration de l'interface utilisateur pour intégrer ces nouvelles fonctionnalités.
This commit is contained in:
parent
a6ca8a2c9a
commit
5a5569cf2c
@@ -2,6 +2,47 @@
|
||||
<div class="page-container" :style="{ transform: `scale(${zoom})` }">
|
||||
<div v-if="!pageData" class="error">Aucune donnée d'image disponible</div>
|
||||
<div v-else-if="!pageData.base64Content" class="error">Contenu de l'image manquant</div>
|
||||
|
||||
<!-- Affichage spécial pour les doubles pages sur mobile -->
|
||||
<div v-else-if="isDoublePage && isMobile && doublePageMode !== 'normal'" class="double-page-mobile">
|
||||
<!-- Mode rotation automatique -->
|
||||
<div v-if="doublePageMode === 'rotate'" class="double-page-rotated">
|
||||
<img
|
||||
:src="imageSource"
|
||||
:alt="`Page ${pageNumber} (Double page)`"
|
||||
class="page-image rotated"
|
||||
:style="doublePageRotatedStyle"
|
||||
@load="handleImageLoad"
|
||||
ref="imageRef" />
|
||||
<div class="rotation-hint">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>Tournez votre appareil pour une meilleure lecture</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode défilement horizontal -->
|
||||
<div v-else-if="doublePageMode === 'scroll'" class="double-page-scroll">
|
||||
<div class="scroll-container" ref="scrollContainerRef">
|
||||
<img
|
||||
:src="imageSource"
|
||||
:alt="`Page ${pageNumber} (Double page)`"
|
||||
class="page-image scrollable"
|
||||
:style="doublePageScrollStyle"
|
||||
@load="handleImageLoad"
|
||||
ref="imageRef" />
|
||||
</div>
|
||||
<div class="scroll-hint">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16l-4-4m0 0l4-4m-4 4h18" />
|
||||
</svg>
|
||||
<span>Glissez horizontalement (commence par la droite)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Affichage normal pour les pages simples, sur desktop, ou mode normal forcé -->
|
||||
<img
|
||||
v-else
|
||||
:src="imageSource"
|
||||
@@ -14,7 +55,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useReaderStore } from '../../application/store/readerStore';
|
||||
|
||||
const props = defineProps({
|
||||
pageData: {
|
||||
@@ -28,13 +70,22 @@
|
||||
zoom: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
doublePageMode: {
|
||||
type: String,
|
||||
default: 'rotate', // 'rotate', 'scroll', 'normal'
|
||||
validator: (value) => ['rotate', 'scroll', 'normal'].includes(value)
|
||||
}
|
||||
});
|
||||
|
||||
const store = useReaderStore();
|
||||
const imageRef = ref(null);
|
||||
const scrollContainerRef = ref(null);
|
||||
const naturalWidth = ref(0);
|
||||
const naturalHeight = ref(0);
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
const isMobile = computed(() => windowWidth.value < 768);
|
||||
const imageLoaded = ref(false);
|
||||
|
||||
const imageSource = computed(() => {
|
||||
if (!props.pageData?.base64Content || !props.pageData?.mimeType) {
|
||||
@@ -43,26 +94,99 @@
|
||||
return `data:${props.pageData.mimeType};base64,${props.pageData.base64Content}`;
|
||||
});
|
||||
|
||||
// Détection des doubles pages basée sur le ratio largeur/hauteur et les dimensions API
|
||||
const isDoublePage = computed(() => {
|
||||
// Ne pas détecter si la détection automatique est désactivée
|
||||
if (!store.doublePageSettings.autoDetect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const threshold = store.doublePageSettings.detectionThreshold || 1.2;
|
||||
|
||||
// Utiliser d'abord les dimensions de l'API si disponibles
|
||||
if (props.pageData?.dimensions?.width && props.pageData?.dimensions?.height) {
|
||||
const ratio = props.pageData.dimensions.width / props.pageData.dimensions.height;
|
||||
const isDouble = ratio > threshold;
|
||||
console.log(`API Dimensions - Page ${props.pageNumber}: ${props.pageData.dimensions.width}x${props.pageData.dimensions.height}, ratio: ${ratio.toFixed(2)}, isDouble: ${isDouble}`);
|
||||
return isDouble;
|
||||
}
|
||||
|
||||
// Fallback sur les dimensions naturelles de l'image (seulement si l'image est chargée)
|
||||
if (imageLoaded.value && naturalWidth.value && naturalHeight.value) {
|
||||
const ratio = naturalWidth.value / naturalHeight.value;
|
||||
const isDouble = ratio > threshold;
|
||||
console.log(`Natural Dimensions - Page ${props.pageNumber}: ${naturalWidth.value}x${naturalHeight.value}, ratio: ${ratio.toFixed(2)}, isDouble: ${isDouble}`);
|
||||
return isDouble;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const handleImageLoad = () => {
|
||||
if (imageRef.value) {
|
||||
naturalWidth.value = imageRef.value.naturalWidth;
|
||||
naturalHeight.value = imageRef.value.naturalHeight;
|
||||
imageLoaded.value = true;
|
||||
console.log(`Image loaded - Page ${props.pageNumber}: ${naturalWidth.value}x${naturalHeight.value}`);
|
||||
|
||||
// Positionner le scroll à droite si c'est le mode scroll
|
||||
if (props.doublePageMode === 'scroll' && scrollContainerRef.value) {
|
||||
nextTick(() => {
|
||||
scrollContainerRef.value.scrollLeft = scrollContainerRef.value.scrollWidth - scrollContainerRef.value.clientWidth;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Calculer la largeur maximale en fonction de la largeur disponible
|
||||
// Réinitialiser les dimensions quand on change de page
|
||||
const resetDimensions = () => {
|
||||
naturalWidth.value = 0;
|
||||
naturalHeight.value = 0;
|
||||
imageLoaded.value = false;
|
||||
};
|
||||
|
||||
// Watcher pour détecter les changements de page
|
||||
watch(
|
||||
() => props.pageData,
|
||||
(newPageData, oldPageData) => {
|
||||
// Réinitialiser les dimensions si c'est une nouvelle page
|
||||
if (newPageData?.id !== oldPageData?.id) {
|
||||
resetDimensions();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Watcher pour détecter les changements de numéro de page
|
||||
watch(
|
||||
() => props.pageNumber,
|
||||
() => {
|
||||
resetDimensions();
|
||||
}
|
||||
);
|
||||
|
||||
// Calculer la largeur maximale en fonction de la largeur disponible
|
||||
const maxWidth = computed(() => {
|
||||
if (!naturalWidth.value || !naturalHeight.value) return null;
|
||||
// Utiliser les dimensions API en priorité
|
||||
let width = naturalWidth.value;
|
||||
let height = naturalHeight.value;
|
||||
|
||||
if (props.pageData?.dimensions?.width && props.pageData?.dimensions?.height) {
|
||||
width = props.pageData.dimensions.width;
|
||||
height = props.pageData.dimensions.height;
|
||||
}
|
||||
|
||||
if (!width || !height) return null;
|
||||
|
||||
const availableWidth = windowWidth.value;
|
||||
|
||||
// Si la largeur disponible est < 1200px : utiliser 95% de la largeur
|
||||
if (availableWidth < 1200) {
|
||||
return Math.min(naturalWidth.value, availableWidth * 0.95);
|
||||
return Math.min(width, availableWidth * 0.95);
|
||||
}
|
||||
|
||||
// Si la largeur disponible est >= 1200px : limiter à 1200px maximum
|
||||
return Math.min(naturalWidth.value, 1200);
|
||||
return Math.min(width, 1200);
|
||||
});
|
||||
|
||||
const imageStyle = computed(() => {
|
||||
@@ -75,6 +199,70 @@
|
||||
};
|
||||
});
|
||||
|
||||
// Styles spéciaux pour les doubles pages
|
||||
const doublePageRotatedStyle = computed(() => {
|
||||
let width = naturalWidth.value;
|
||||
let height = naturalHeight.value;
|
||||
|
||||
// Utiliser les dimensions API si disponibles
|
||||
if (props.pageData?.dimensions?.width && props.pageData?.dimensions?.height) {
|
||||
width = props.pageData.dimensions.width;
|
||||
height = props.pageData.dimensions.height;
|
||||
}
|
||||
|
||||
if (!width || !height) return {};
|
||||
|
||||
// En mode rotation : maximiser l'utilisation de l'espace
|
||||
const availableWidth = windowWidth.value;
|
||||
const availableHeight = window.innerHeight - 100; // Laisser un peu d'espace pour les contrôles
|
||||
|
||||
// Après rotation, la largeur originale devient la hauteur affichée
|
||||
// et la hauteur originale devient la largeur affichée
|
||||
const rotatedWidth = height; // Hauteur de l'image devient largeur après rotation
|
||||
const rotatedHeight = width; // Largeur de l'image devient hauteur après rotation
|
||||
|
||||
// Calculer le facteur d'échelle pour remplir l'écran
|
||||
const scaleByWidth = availableWidth * 0.98 / rotatedWidth;
|
||||
const scaleByHeight = availableHeight * 0.95 / rotatedHeight;
|
||||
const scaleFactor = Math.min(scaleByWidth, scaleByHeight);
|
||||
|
||||
return {
|
||||
width: `${width * scaleFactor}px`,
|
||||
height: `${height * scaleFactor}px`,
|
||||
maxWidth: 'none',
|
||||
maxHeight: 'none',
|
||||
transform: 'rotate(90deg)',
|
||||
transformOrigin: 'center center'
|
||||
};
|
||||
});
|
||||
|
||||
const doublePageScrollStyle = computed(() => {
|
||||
let width = naturalWidth.value;
|
||||
let height = naturalHeight.value;
|
||||
|
||||
// Utiliser les dimensions API si disponibles
|
||||
if (props.pageData?.dimensions?.width && props.pageData?.dimensions?.height) {
|
||||
width = props.pageData.dimensions.width;
|
||||
height = props.pageData.dimensions.height;
|
||||
}
|
||||
|
||||
if (!width || !height) return {};
|
||||
|
||||
// Mode scroll : remplir la hauteur, permettre le défilement horizontal
|
||||
const availableHeight = window.innerHeight - 80; // Espace pour les contrôles
|
||||
|
||||
// Échelle basée sur la hauteur pour remplir l'écran verticalement
|
||||
const scaleFactor = (availableHeight * 0.95) / height;
|
||||
|
||||
return {
|
||||
width: `${width * scaleFactor}px`,
|
||||
height: `${height * scaleFactor}px`,
|
||||
maxWidth: 'none',
|
||||
maxHeight: 'none',
|
||||
minWidth: '150vw' // Assurer un débordement horizontal pour le scroll
|
||||
};
|
||||
});
|
||||
|
||||
// Gestion du redimensionnement de la fenêtre
|
||||
const handleResize = () => {
|
||||
windowWidth.value = window.innerWidth;
|
||||
@@ -107,9 +295,119 @@
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
/* Styles pour les doubles pages sur mobile */
|
||||
.double-page-mobile {
|
||||
@apply w-full h-full flex flex-col items-center justify-center relative;
|
||||
/* Utiliser tout l'espace disponible */
|
||||
min-height: 100vh;
|
||||
min-width: 100vw;
|
||||
}
|
||||
|
||||
.double-page-rotated {
|
||||
@apply relative flex items-center justify-center;
|
||||
/* Espace pour la rotation - utiliser tout l'espace */
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.double-page-rotated .page-image.rotated {
|
||||
@apply origin-center;
|
||||
/* Animation fluide pour la rotation */
|
||||
transition: transform 0.3s ease-in-out;
|
||||
/* Permettre le débordement contrôlé */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.double-page-scroll {
|
||||
@apply w-full h-full flex flex-col items-center;
|
||||
/* Utiliser toute la hauteur disponible */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
@apply overflow-x-auto overflow-y-hidden w-full flex items-center justify-start;
|
||||
/* Utiliser toute la hauteur et largeur */
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
/* Barres de défilement personnalisées */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #4F46E5 #E5E7EB;
|
||||
/* Défilement fluide */
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar {
|
||||
height: 12px; /* Plus visible sur mobile */
|
||||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar-track {
|
||||
@apply bg-gray-200 rounded;
|
||||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar-thumb {
|
||||
@apply bg-blue-600 rounded hover:bg-blue-700;
|
||||
}
|
||||
|
||||
.double-page-scroll .page-image.scrollable {
|
||||
@apply flex-shrink-0;
|
||||
/* Centrer verticalement */
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
/* Hints pour guider l'utilisateur - ajuster la position */
|
||||
.rotation-hint,
|
||||
.scroll-hint {
|
||||
@apply absolute top-8 left-4 bg-black bg-opacity-70 text-white px-3 py-2 rounded-lg text-sm flex items-center gap-2 z-10;
|
||||
/* Animation de disparition */
|
||||
animation: fadeOut 4s ease-in-out 2s forwards;
|
||||
/* S'assurer qu'ils sont visibles par-dessus tout */
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0%, 50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
@apply text-red-500 text-xl;
|
||||
}
|
||||
|
||||
/* Responsive ajustements */
|
||||
@media (orientation: landscape) and (max-width: 768px) {
|
||||
.double-page-rotated .page-image.rotated {
|
||||
/* En mode paysage mobile, ajuster la rotation pour optimiser l'espace */
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.rotation-hint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* En paysage, utiliser le mode adaptatif automatiquement */
|
||||
.double-page-rotated {
|
||||
@apply flex items-center justify-center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* Spécifique pour les écrans très petits */
|
||||
@media (max-width: 480px) {
|
||||
.rotation-hint,
|
||||
.scroll-hint {
|
||||
@apply text-xs px-2 py-1;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user