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
@@ -1,21 +1,185 @@
|
||||
<template>
|
||||
<div class="reader-settings">
|
||||
<button @click="onToggleReadingMode">
|
||||
{{ readingMode === 'single' ? 'Mode Infini' : 'Mode Simple' }}
|
||||
</button>
|
||||
<button @click="onToggleReadingDirection">
|
||||
{{ readingDirection === 'ltr' ? 'RTL' : 'LTR' }}
|
||||
</button>
|
||||
<div class="zoom-controls">
|
||||
<button @click="onZoomOut">-</button>
|
||||
<span>{{ Math.round(zoom * 100) }}%</span>
|
||||
<button @click="onZoomIn">+</button>
|
||||
</div>
|
||||
<!-- Bouton pour ouvrir/fermer les paramètres -->
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
leave-active-class="transition-all duration-300 ease-in"
|
||||
enter-from-class="opacity-0 translate-y-5 scale-75"
|
||||
enter-to-class="opacity-100 translate-y-0 scale-100"
|
||||
leave-from-class="opacity-100 translate-y-0 scale-100"
|
||||
leave-to-class="opacity-0 translate-y-5 scale-75"
|
||||
>
|
||||
<button
|
||||
v-show="visible"
|
||||
@click="toggleSettings"
|
||||
class="settings-toggle"
|
||||
:class="{ 'active': effectiveIsOpen }"
|
||||
:data-external-control="forceOpen !== null"
|
||||
title="Paramètres du lecteur"
|
||||
>
|
||||
<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="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
|
||||
</svg>
|
||||
</button>
|
||||
</Transition>
|
||||
|
||||
<!-- Panel des paramètres -->
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
leave-active-class="transition-all duration-300 ease-in"
|
||||
enter-from-class="opacity-0 translate-y-4 scale-95"
|
||||
enter-to-class="opacity-100 translate-y-0 scale-100"
|
||||
leave-from-class="opacity-100 translate-y-0 scale-100"
|
||||
leave-to-class="opacity-0 translate-y-4 scale-95"
|
||||
>
|
||||
<div v-show="effectiveIsOpen" class="settings-panel" :data-external-control="forceOpen !== null" ref="panelRef">
|
||||
<!-- Paramètres de base -->
|
||||
<div class="settings-section">
|
||||
<h3 class="section-title">Mode de lecture</h3>
|
||||
<div class="setting-group">
|
||||
<button
|
||||
@click="onToggleReadingMode"
|
||||
class="setting-button"
|
||||
:class="{ 'active': readingMode === 'infinite' }"
|
||||
>
|
||||
<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 6h16M4 12h16m-7 6h7" />
|
||||
</svg>
|
||||
{{ readingMode === 'single' ? 'Mode Infini' : 'Mode Simple' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="onToggleReadingDirection"
|
||||
class="setting-button"
|
||||
:class="{ 'active': readingDirection === 'rtl' }"
|
||||
>
|
||||
<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>
|
||||
{{ readingDirection === 'ltr' ? 'RTL' : 'LTR' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contrôles du zoom -->
|
||||
<div class="settings-section">
|
||||
<h3 class="section-title">Zoom</h3>
|
||||
<div class="zoom-controls">
|
||||
<button @click="onZoomOut" class="zoom-button" :disabled="zoom <= 0.5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
|
||||
</svg>
|
||||
</button>
|
||||
<span class="zoom-display">{{ Math.round(zoom * 100) }}%</span>
|
||||
<button @click="onZoomIn" class="zoom-button" :disabled="zoom >= 2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
:value="zoom"
|
||||
@input="onZoomChange($event.target.value)"
|
||||
min="0.5"
|
||||
max="2"
|
||||
step="0.1"
|
||||
class="zoom-slider"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Paramètres des doubles pages -->
|
||||
<div class="settings-section" v-if="isMobile">
|
||||
<h3 class="section-title">
|
||||
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Doubles pages (Mobile)
|
||||
</h3>
|
||||
|
||||
<!-- Activation/désactivation -->
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="doublePageSettings.autoDetect"
|
||||
@change="onDoublePageAutoDetectChange($event.target.checked)"
|
||||
class="setting-checkbox"
|
||||
/>
|
||||
<span>Détection automatique</span>
|
||||
</label>
|
||||
<p class="setting-description">
|
||||
Détecter et optimiser automatiquement l'affichage des doubles pages sur mobile
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Mode d'affichage (si la détection automatique est activée) -->
|
||||
<div v-if="doublePageSettings.autoDetect" class="setting-item">
|
||||
<label class="setting-label">Mode d'affichage</label>
|
||||
<select
|
||||
:value="doublePageMode"
|
||||
@change="onDoublePageModeChange($event.target.value)"
|
||||
class="setting-select"
|
||||
>
|
||||
<option value="rotate">Rotation suggérée</option>
|
||||
<option value="scroll">Défilement horizontal</option>
|
||||
<option value="normal">Affichage normal</option>
|
||||
</select>
|
||||
|
||||
<!-- Descriptions des modes -->
|
||||
<p class="setting-description">
|
||||
<span v-if="doublePageMode === 'rotate'">
|
||||
Suggère de tourner l'appareil pour une meilleure lecture
|
||||
</span>
|
||||
<span v-else-if="doublePageMode === 'scroll'">
|
||||
Permet le défilement horizontal pour naviguer dans la page (commence à droite)
|
||||
</span>
|
||||
<span v-else>
|
||||
Affichage standard sans optimisation spéciale
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Seuil de détection -->
|
||||
<div v-if="doublePageSettings.autoDetect" class="setting-item">
|
||||
<label class="setting-label">
|
||||
Sensibilité de détection: {{ doublePageSettings.detectionThreshold.toFixed(1) }}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
:value="doublePageSettings.detectionThreshold"
|
||||
@input="onDetectionThresholdChange($event.target.value)"
|
||||
min="1.0"
|
||||
max="2.5"
|
||||
step="0.1"
|
||||
class="setting-slider"
|
||||
/>
|
||||
<p class="setting-description">
|
||||
Plus la valeur est faible, plus la détection est sensible (1.4 recommandé)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="settings-section">
|
||||
<div class="setting-actions">
|
||||
<button @click="onResetPreferences" class="action-button reset">
|
||||
<svg class="w-4 h-4" 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>
|
||||
Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
readingMode: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -27,27 +191,302 @@
|
||||
zoom: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
doublePageMode: {
|
||||
type: String,
|
||||
default: 'rotate'
|
||||
},
|
||||
doublePageSettings: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
autoDetect: true,
|
||||
mobileOnly: true,
|
||||
detectionThreshold: 1.4
|
||||
})
|
||||
},
|
||||
// Visibilité contrôlée par le parent
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// Contrôle externe de l'ouverture (pour le bouton intégré)
|
||||
forceOpen: {
|
||||
type: Boolean,
|
||||
default: null // null = pas de contrôle externe, true/false = contrôle externe
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['toggleReadingMode', 'toggleReadingDirection', 'zoomIn', 'zoomOut']);
|
||||
const emit = defineEmits([
|
||||
'toggleReadingMode',
|
||||
'toggleReadingDirection',
|
||||
'zoomIn',
|
||||
'zoomOut',
|
||||
'zoomChange',
|
||||
'doublePageModeChange',
|
||||
'doublePageAutoDetectChange',
|
||||
'detectionThresholdChange',
|
||||
'resetPreferences',
|
||||
'buttonClick' // Signaler l'interaction au parent
|
||||
]);
|
||||
|
||||
const onToggleReadingMode = () => emit('toggleReadingMode');
|
||||
const onToggleReadingDirection = () => emit('toggleReadingDirection');
|
||||
const onZoomIn = () => emit('zoomIn');
|
||||
const onZoomOut = () => emit('zoomOut');
|
||||
const isOpen = ref(false);
|
||||
const isMobile = computed(() => window.innerWidth < 768);
|
||||
const panelRef = ref(null);
|
||||
|
||||
// Computed pour gérer l'état d'ouverture (interne ou externe)
|
||||
const effectiveIsOpen = computed(() => {
|
||||
// Si forceOpen est défini (true/false), on l'utilise
|
||||
if (props.forceOpen !== null) {
|
||||
return props.forceOpen;
|
||||
}
|
||||
// Sinon, on utilise l'état interne
|
||||
return isOpen.value;
|
||||
});
|
||||
|
||||
const toggleSettings = () => {
|
||||
// Si on est en contrôle externe, ne pas permettre le toggle via le bouton flottant
|
||||
if (props.forceOpen !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
isOpen.value = !isOpen.value;
|
||||
// Signaler l'interaction au parent
|
||||
emit('buttonClick');
|
||||
};
|
||||
|
||||
// Fonction pour fermer le panel (utilisée par les clics externes et internes)
|
||||
const closePanel = () => {
|
||||
if (props.forceOpen !== null) {
|
||||
// Mode externe : émettre l'événement pour que le parent gère la fermeture
|
||||
emit('buttonClick');
|
||||
} else {
|
||||
// Mode interne : fermer directement
|
||||
isOpen.value = false;
|
||||
emit('buttonClick');
|
||||
}
|
||||
};
|
||||
|
||||
// Gestion des clics en dehors du panel
|
||||
const handleClickOutside = (event) => {
|
||||
if (effectiveIsOpen.value && panelRef.value && !panelRef.value.contains(event.target)) {
|
||||
// Vérifier que le clic n'est pas sur le bouton de toggle
|
||||
const settingsButton = document.querySelector('.settings-toggle, .settings-button');
|
||||
if (settingsButton && settingsButton.contains(event.target)) {
|
||||
return; // Laisser le bouton gérer le toggle
|
||||
}
|
||||
|
||||
closePanel();
|
||||
}
|
||||
};
|
||||
|
||||
// Watcher pour empêcher la fermeture du bouton quand le panel est ouvert
|
||||
watch(
|
||||
() => effectiveIsOpen.value,
|
||||
(newIsOpen) => {
|
||||
if (newIsOpen || !newIsOpen) {
|
||||
// Signaler l'interaction à chaque changement
|
||||
emit('buttonClick');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Cycle de vie des event listeners
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside, true);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside, true);
|
||||
});
|
||||
|
||||
// Méthodes des événements (toutes signalent l'interaction)
|
||||
const onToggleReadingMode = () => {
|
||||
emit('toggleReadingMode');
|
||||
emit('buttonClick');
|
||||
};
|
||||
const onToggleReadingDirection = () => {
|
||||
emit('toggleReadingDirection');
|
||||
emit('buttonClick');
|
||||
};
|
||||
const onZoomIn = () => {
|
||||
emit('zoomIn');
|
||||
emit('buttonClick');
|
||||
};
|
||||
const onZoomOut = () => {
|
||||
emit('zoomOut');
|
||||
emit('buttonClick');
|
||||
};
|
||||
const onZoomChange = (value) => {
|
||||
emit('zoomChange', parseFloat(value));
|
||||
emit('buttonClick');
|
||||
};
|
||||
const onDoublePageModeChange = (mode) => {
|
||||
emit('doublePageModeChange', mode);
|
||||
emit('buttonClick');
|
||||
};
|
||||
const onDoublePageAutoDetectChange = (enabled) => {
|
||||
emit('doublePageAutoDetectChange', enabled);
|
||||
emit('buttonClick');
|
||||
};
|
||||
const onDetectionThresholdChange = (threshold) => {
|
||||
emit('detectionThresholdChange', parseFloat(threshold));
|
||||
emit('buttonClick');
|
||||
};
|
||||
const onResetPreferences = () => {
|
||||
emit('resetPreferences');
|
||||
emit('buttonClick');
|
||||
isOpen.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.reader-settings {
|
||||
@apply flex items-center justify-center gap-4 p-4 bg-gray-800;
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.settings-toggle {
|
||||
@apply fixed top-20 right-4 z-50 w-12 h-12 bg-gray-800 hover:bg-gray-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200;
|
||||
@apply focus:outline-none focus:ring-2 focus:ring-blue-500;
|
||||
}
|
||||
|
||||
/* Masquer le bouton flottant si on est en contrôle externe */
|
||||
.settings-toggle[data-external-control="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-toggle.active {
|
||||
@apply bg-blue-600 hover:bg-blue-700;
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
@apply fixed top-36 right-4 z-40 w-80 max-w-[calc(100vw-2rem)] bg-gray-800 rounded-lg shadow-xl border border-gray-700 max-h-[80vh] overflow-y-auto;
|
||||
}
|
||||
|
||||
/* Responsive pour settings-panel */
|
||||
@media (max-width: 480px) {
|
||||
.settings-panel {
|
||||
width: 90vw;
|
||||
max-width: calc(100vw - 1rem);
|
||||
right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Position adaptative pour le contrôle externe (bouton intégré) */
|
||||
.settings-panel[data-external-control="true"] {
|
||||
@apply top-32 left-1/2 right-auto;
|
||||
transform: translateX(-50%);
|
||||
/* S'assurer qu'il ne couvre pas les contrôles */
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
@apply p-4 border-b border-gray-700 last:border-b-0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-white font-semibold text-lg mb-3 flex items-center;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.setting-button {
|
||||
@apply flex items-center gap-2 px-3 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors duration-200 text-sm;
|
||||
}
|
||||
|
||||
.setting-button.active {
|
||||
@apply bg-blue-600 hover:bg-blue-700;
|
||||
}
|
||||
|
||||
/* Contrôles du zoom */
|
||||
.zoom-controls {
|
||||
@apply flex items-center gap-2;
|
||||
@apply flex items-center gap-3 mb-2;
|
||||
}
|
||||
|
||||
button {
|
||||
@apply px-4 py-2 bg-gray-700 rounded hover:bg-gray-600 transition-colors;
|
||||
.zoom-button {
|
||||
@apply w-8 h-8 bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 disabled:cursor-not-allowed text-white rounded flex items-center justify-center transition-colors;
|
||||
}
|
||||
|
||||
.zoom-display {
|
||||
@apply text-white font-mono text-sm min-w-[3rem] text-center;
|
||||
}
|
||||
|
||||
.zoom-slider {
|
||||
@apply w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer;
|
||||
}
|
||||
|
||||
.zoom-slider::-webkit-slider-thumb {
|
||||
@apply appearance-none w-4 h-4 bg-blue-600 rounded-full cursor-pointer;
|
||||
}
|
||||
|
||||
.zoom-slider::-moz-range-thumb {
|
||||
@apply w-4 h-4 bg-blue-600 rounded-full cursor-pointer border-none;
|
||||
}
|
||||
|
||||
/* Paramètres des doubles pages */
|
||||
.setting-item {
|
||||
@apply mb-4 last:mb-0;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
@apply flex items-center gap-2 text-white font-medium text-sm mb-2 cursor-pointer;
|
||||
}
|
||||
|
||||
.setting-checkbox {
|
||||
@apply w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2;
|
||||
}
|
||||
|
||||
.setting-select {
|
||||
@apply w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500;
|
||||
}
|
||||
|
||||
.setting-slider {
|
||||
@apply w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer mb-2;
|
||||
}
|
||||
|
||||
.setting-slider::-webkit-slider-thumb {
|
||||
@apply appearance-none w-4 h-4 bg-blue-600 rounded-full cursor-pointer;
|
||||
}
|
||||
|
||||
.setting-slider::-moz-range-thumb {
|
||||
@apply w-4 h-4 bg-blue-600 rounded-full cursor-pointer border-none;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
@apply text-gray-400 text-xs leading-relaxed;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.setting-actions {
|
||||
@apply flex gap-2;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
@apply flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors;
|
||||
}
|
||||
|
||||
.action-button.reset {
|
||||
@apply bg-red-600 hover:bg-red-700 text-white;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.settings-panel {
|
||||
@apply right-2 w-72;
|
||||
}
|
||||
|
||||
.settings-toggle {
|
||||
@apply right-2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pour les très petits écrans */
|
||||
@media (max-width: 480px) {
|
||||
.settings-toggle {
|
||||
right: 0.25rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user