493 lines
18 KiB
Vue
493 lines
18 KiB
Vue
<template>
|
|
<div class="reader-settings">
|
|
<!-- 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>
|
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
|
|
const props = defineProps({
|
|
readingMode: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
readingDirection: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
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',
|
|
'zoomChange',
|
|
'doublePageModeChange',
|
|
'doublePageAutoDetectChange',
|
|
'detectionThresholdChange',
|
|
'resetPreferences',
|
|
'buttonClick' // Signaler l'interaction au parent
|
|
]);
|
|
|
|
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 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-3 mb-2;
|
|
}
|
|
|
|
.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>
|