feat: ajout de la gestion de l'auto-hide du header et amélioration de la réactivité des composants en fonction de la taille de la fenêtre, ainsi que des optimisations CSS pour une meilleure expérience utilisateur sur mobile.
This commit is contained in:
parent
4848a1736f
commit
ebcca466a9
@@ -93,7 +93,7 @@ export class ApiMangaRepository {
|
|||||||
|
|
||||||
async searchMangaDex(query) {
|
async searchMangaDex(query) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://localhost/api/mangadex-search?title=${encodeURIComponent(query)}`);
|
const response = await fetch(`/api/mangadex-search?title=${encodeURIComponent(query)}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to search MangaDex');
|
throw new Error('Failed to search MangaDex');
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ export class ApiMangaRepository {
|
|||||||
|
|
||||||
async createFromMangaDex(externalId) {
|
async createFromMangaDex(externalId) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('https://localhost/api/mangas/create-from-mangadex', {
|
const response = await fetch('/api/mangas/create-from-mangadex', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -125,7 +125,7 @@ export class ApiMangaRepository {
|
|||||||
|
|
||||||
async searchChapter(chapterId) {
|
async searchChapter(chapterId) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('https://localhost/api/scraping/chapters', {
|
const response = await fetch('/api/scraping/chapters', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, onUnmounted, watch } from 'vue';
|
import { onMounted, onUnmounted, watch } from 'vue';
|
||||||
|
import { useHeaderStore } from '../../../../shared/stores/headerStore';
|
||||||
import { useReaderStore } from '../../application/store/readerStore';
|
import { useReaderStore } from '../../application/store/readerStore';
|
||||||
import InfiniteReader from './InfiniteReader.vue';
|
import InfiniteReader from './InfiniteReader.vue';
|
||||||
import ReaderControls from './ReaderControls.vue';
|
import ReaderControls from './ReaderControls.vue';
|
||||||
@@ -62,9 +63,16 @@ import SingleModeReader from './SingleModeReader.vue';
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = useReaderStore();
|
const store = useReaderStore();
|
||||||
|
const headerStore = useHeaderStore();
|
||||||
|
|
||||||
const toggleReadingMode = () => {
|
const toggleReadingMode = () => {
|
||||||
store.setReadingMode(store.readingMode === 'single' ? 'infinite' : 'single');
|
const newMode = store.readingMode === 'single' ? 'infinite' : 'single';
|
||||||
|
store.setReadingMode(newMode);
|
||||||
|
|
||||||
|
// Désactiver l'auto-hide si on passe en mode single
|
||||||
|
if (newMode === 'single') {
|
||||||
|
headerStore.disableAutoHide();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleReadingDirection = () => {
|
const toggleReadingDirection = () => {
|
||||||
@@ -111,12 +119,15 @@ import SingleModeReader from './SingleModeReader.vue';
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('keydown', handleKeyPress);
|
window.removeEventListener('keydown', handleKeyPress);
|
||||||
|
// S'assurer que l'auto-hide est désactivé en quittant le lecteur
|
||||||
|
headerStore.disableAutoHide();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
.chapter-reader {
|
.chapter-reader {
|
||||||
@apply w-full h-full flex flex-col items-center justify-center bg-gray-900 text-white;
|
@apply w-full h-full flex flex-col items-center justify-center bg-gray-900 text-white;
|
||||||
|
@apply p-0 sm:p-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
@@ -129,6 +140,7 @@ import SingleModeReader from './SingleModeReader.vue';
|
|||||||
|
|
||||||
.reader-content {
|
.reader-content {
|
||||||
@apply w-full h-full flex flex-col;
|
@apply w-full h-full flex flex-col;
|
||||||
|
@apply p-0 sm:p-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rtl {
|
.rtl {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
import { useHeaderStore } from '../../../../shared/stores/headerStore';
|
||||||
import ChapterNavigation from './ChapterNavigation.vue';
|
import ChapterNavigation from './ChapterNavigation.vue';
|
||||||
import ReaderPage from './ReaderPage.vue';
|
import ReaderPage from './ReaderPage.vue';
|
||||||
|
|
||||||
@@ -62,8 +63,10 @@ import ReaderPage from './ReaderPage.vue';
|
|||||||
|
|
||||||
const emit = defineEmits(['pageVisible']);
|
const emit = defineEmits(['pageVisible']);
|
||||||
|
|
||||||
|
const headerStore = useHeaderStore();
|
||||||
const containerRef = ref(null);
|
const containerRef = ref(null);
|
||||||
const observer = ref(null);
|
const observer = ref(null);
|
||||||
|
const windowWidth = ref(window.innerWidth);
|
||||||
|
|
||||||
// État pour le bouton scroll to top
|
// État pour le bouton scroll to top
|
||||||
const showScrollToTop = ref(false);
|
const showScrollToTop = ref(false);
|
||||||
@@ -102,7 +105,7 @@ import ReaderPage from './ReaderPage.vue';
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gestion du scroll pour le bouton "revenir en haut"
|
// Gestion du scroll pour le bouton "revenir en haut" et le header
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
let scrollTop = 0;
|
let scrollTop = 0;
|
||||||
|
|
||||||
@@ -133,6 +136,11 @@ import ReaderPage from './ReaderPage.vue';
|
|||||||
scrollDirection = 'up';
|
scrollDirection = 'up';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gestion du header auto-hide (seulement si largeur < 1200px)
|
||||||
|
if (windowWidth.value < 1200) {
|
||||||
|
headerStore.updateScrollDirection(scrollTop);
|
||||||
|
}
|
||||||
|
|
||||||
// Mise à jour de la visibilité du bouton
|
// Mise à jour de la visibilité du bouton
|
||||||
// Afficher si on scroll vers le bas et qu'on est à plus de 300px
|
// Afficher si on scroll vers le bas et qu'on est à plus de 300px
|
||||||
// Masquer si on scroll vers le haut ou qu'on est en haut de page
|
// Masquer si on scroll vers le haut ou qu'on est en haut de page
|
||||||
@@ -195,9 +203,27 @@ import ReaderPage from './ReaderPage.vue';
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Gestion du redimensionnement de la fenêtre
|
||||||
|
const handleResize = () => {
|
||||||
|
const newWidth = window.innerWidth;
|
||||||
|
windowWidth.value = newWidth;
|
||||||
|
|
||||||
|
// Activer/désactiver l'auto-hide selon la largeur
|
||||||
|
if (newWidth < 1200) {
|
||||||
|
headerStore.enableAutoHide();
|
||||||
|
} else {
|
||||||
|
headerStore.disableAutoHide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setupIntersectionObserver();
|
setupIntersectionObserver();
|
||||||
|
|
||||||
|
// Activer l'auto-hide du header si la largeur < 1200px
|
||||||
|
if (windowWidth.value < 1200) {
|
||||||
|
headerStore.enableAutoHide();
|
||||||
|
}
|
||||||
|
|
||||||
// Ajouter l'écouteur de scroll sur le conteneur
|
// Ajouter l'écouteur de scroll sur le conteneur
|
||||||
if (containerRef.value) {
|
if (containerRef.value) {
|
||||||
containerRef.value.addEventListener('scroll', handleScroll, { passive: true });
|
containerRef.value.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
@@ -205,6 +231,9 @@ import ReaderPage from './ReaderPage.vue';
|
|||||||
|
|
||||||
// Ajouter l'écouteur de scroll sur la fenêtre
|
// Ajouter l'écouteur de scroll sur la fenêtre
|
||||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
|
||||||
|
// Ajouter l'écouteur de redimensionnement
|
||||||
|
window.addEventListener('resize', handleResize, { passive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -212,6 +241,9 @@ import ReaderPage from './ReaderPage.vue';
|
|||||||
observer.value.disconnect();
|
observer.value.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Désactiver l'auto-hide du header en quittant
|
||||||
|
headerStore.disableAutoHide();
|
||||||
|
|
||||||
// Nettoyer l'écouteur de scroll du conteneur
|
// Nettoyer l'écouteur de scroll du conteneur
|
||||||
if (containerRef.value) {
|
if (containerRef.value) {
|
||||||
containerRef.value.removeEventListener('scroll', handleScroll);
|
containerRef.value.removeEventListener('scroll', handleScroll);
|
||||||
@@ -219,23 +251,46 @@ import ReaderPage from './ReaderPage.vue';
|
|||||||
|
|
||||||
// Nettoyer l'écouteur de scroll de la fenêtre
|
// Nettoyer l'écouteur de scroll de la fenêtre
|
||||||
window.removeEventListener('scroll', handleScroll);
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
// Nettoyer l'écouteur de redimensionnement
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
.infinite-reader {
|
.infinite-reader {
|
||||||
@apply flex-1 flex flex-col items-center overflow-y-auto py-8 relative;
|
@apply flex-1 flex flex-col items-center overflow-y-auto relative;
|
||||||
|
/* Réduction du padding sur mobile */
|
||||||
|
@apply py-2 sm:py-8;
|
||||||
height: calc(100vh - 8rem);
|
height: calc(100vh - 8rem);
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-wrapper {
|
.page-wrapper {
|
||||||
@apply w-full flex justify-center min-h-[200px] mb-4;
|
@apply w-full flex justify-center min-h-[200px];
|
||||||
|
/* Réduction des marges sur mobile */
|
||||||
|
@apply mb-2 sm:mb-4 px-1 sm:px-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading,
|
.loading,
|
||||||
.error {
|
.error {
|
||||||
@apply flex items-center justify-center w-[70vw] min-h-[400px];
|
@apply flex items-center justify-center min-h-[400px];
|
||||||
|
/* Largeur adaptative selon la taille d'écran */
|
||||||
|
width: 95vw; /* Mobile : 95% de la largeur */
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen sm {
|
||||||
|
.loading,
|
||||||
|
.error {
|
||||||
|
width: 80vw; /* Tablette : 80% de la largeur */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen lg {
|
||||||
|
.loading,
|
||||||
|
.error {
|
||||||
|
width: 70vw; /* Desktop : 70% de la largeur */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, onMounted } from 'vue';
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
pageData: {
|
pageData: {
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
const imageRef = ref(null);
|
const imageRef = ref(null);
|
||||||
const naturalWidth = ref(0);
|
const naturalWidth = ref(0);
|
||||||
const naturalHeight = ref(0);
|
const naturalHeight = ref(0);
|
||||||
|
const windowWidth = ref(window.innerWidth);
|
||||||
|
|
||||||
const imageSource = computed(() => {
|
const imageSource = computed(() => {
|
||||||
if (!props.pageData?.base64Content || !props.pageData?.mimeType) {
|
if (!props.pageData?.base64Content || !props.pageData?.mimeType) {
|
||||||
@@ -49,19 +50,19 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculer la largeur maximale en fonction de la hauteur de la fenêtre
|
// Calculer la largeur maximale en fonction de la largeur disponible
|
||||||
const maxWidth = computed(() => {
|
const maxWidth = computed(() => {
|
||||||
if (!naturalWidth.value || !naturalHeight.value) return null;
|
if (!naturalWidth.value || !naturalHeight.value) return null;
|
||||||
|
|
||||||
// On prend 70% de la largeur de la fenêtre comme largeur maximale
|
const availableWidth = windowWidth.value;
|
||||||
const maxWidthPx = window.innerWidth * 0.7;
|
|
||||||
|
|
||||||
// Si l'image est plus petite que la largeur maximale, on garde sa taille naturelle
|
// Si la largeur disponible est < 1200px : utiliser 95% de la largeur
|
||||||
if (naturalWidth.value <= maxWidthPx) {
|
if (availableWidth < 1200) {
|
||||||
return naturalWidth.value;
|
return Math.min(naturalWidth.value, availableWidth * 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
return maxWidthPx;
|
// Si la largeur disponible est >= 1200px : limiter à 1200px maximum
|
||||||
|
return Math.min(naturalWidth.value, 1200);
|
||||||
});
|
});
|
||||||
|
|
||||||
const imageStyle = computed(() => {
|
const imageStyle = computed(() => {
|
||||||
@@ -74,10 +75,20 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Gestion du redimensionnement de la fenêtre
|
||||||
|
const handleResize = () => {
|
||||||
|
windowWidth.value = window.innerWidth;
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (imageRef.value && imageRef.value.complete) {
|
if (imageRef.value && imageRef.value.complete) {
|
||||||
handleImageLoad();
|
handleImageLoad();
|
||||||
}
|
}
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -85,13 +96,20 @@
|
|||||||
.page-container {
|
.page-container {
|
||||||
@apply flex-1 flex items-center justify-center overflow-hidden;
|
@apply flex-1 flex items-center justify-center overflow-hidden;
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
|
/* Réduction des marges sur mobile */
|
||||||
|
@apply p-0 sm:p-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-image {
|
.page-image {
|
||||||
@apply max-w-full max-h-full object-contain;
|
@apply object-contain;
|
||||||
|
/* La largeur est gérée par le JavaScript, on garde juste les contraintes max */
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
@apply text-red-500 text-xl;
|
@apply text-red-500 text-xl;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,8 @@ const hideRightHint = () => {
|
|||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
.single-mode-reader {
|
.single-mode-reader {
|
||||||
@apply relative w-full h-full flex items-center justify-center;
|
@apply relative w-full h-full flex items-center justify-center;
|
||||||
|
/* Suppression des marges sur mobile */
|
||||||
|
@apply p-0 sm:p-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-navigation-wrapper {
|
.page-navigation-wrapper {
|
||||||
@@ -152,6 +154,8 @@ const hideRightHint = () => {
|
|||||||
.page-content {
|
.page-content {
|
||||||
@apply flex-1 h-full flex items-center justify-center;
|
@apply flex-1 h-full flex items-center justify-center;
|
||||||
pointer-events: none; /* Empêche les clics sur l'image elle-même */
|
pointer-events: none; /* Empêche les clics sur l'image elle-même */
|
||||||
|
/* Optimisation pour mobile */
|
||||||
|
@apply p-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-zone {
|
.navigation-zone {
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chapter-page">
|
<div class="chapter-page">
|
||||||
<div class="chapter-header">
|
<div class="chapter-header">
|
||||||
<h1 class="text-2xl font-bold">
|
<!-- Bouton de retour -->
|
||||||
{{ currentChapter?.title || 'Chargement...' }}
|
<div class="flex items-center gap-4 mb-4">
|
||||||
</h1>
|
<button
|
||||||
<div class="chapter-info">
|
@click="goBackToManga"
|
||||||
<span class="text-gray-400"> Chapitre {{ currentChapter?.number }} </span>
|
class="flex items-center gap-2 px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-white transition-colors duration-200"
|
||||||
|
:disabled="!currentChapter?.mangaId"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon class="h-5 w-5" />
|
||||||
|
<span class="text-sm font-medium">Retour au manga</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Titre du chapitre amélioré -->
|
||||||
|
<div class="chapter-title-section">
|
||||||
|
<h1 class="text-3xl md:text-4xl font-bold text-white leading-tight">
|
||||||
|
{{ currentChapter?.title || 'Chargement...' }}
|
||||||
|
</h1>
|
||||||
|
<div class="chapter-meta mt-3">
|
||||||
|
<span class="inline-flex items-center px-3 py-1 bg-blue-600 text-white text-sm font-semibold rounded-full">
|
||||||
|
Chapitre {{ currentChapter?.number }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -16,16 +33,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { ArrowLeftIcon } from '@heroicons/vue/24/outline';
|
||||||
import { useRoute } from 'vue-router';
|
import { computed } from 'vue';
|
||||||
import { useReaderStore } from '../../application/store/readerStore';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import ChapterReader from '../components/ChapterReader.vue';
|
import { useReaderStore } from '../../application/store/readerStore';
|
||||||
|
import ChapterReader from '../components/ChapterReader.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const store = useReaderStore();
|
const store = useReaderStore();
|
||||||
|
|
||||||
const chapterId = computed(() => route.params.chapterId);
|
const chapterId = computed(() => route.params.chapterId);
|
||||||
const currentChapter = computed(() => store.currentChapter);
|
const currentChapter = computed(() => store.currentChapter);
|
||||||
|
|
||||||
|
const goBackToManga = () => {
|
||||||
|
if (currentChapter.value?.mangaId) {
|
||||||
|
router.push({ name: 'manga-details', params: { id: currentChapter.value.mangaId } });
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
@@ -34,11 +59,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chapter-header {
|
.chapter-header {
|
||||||
@apply p-4 bg-gray-800 border-b border-gray-700;
|
@apply p-6 bg-gradient-to-b from-gray-800 to-gray-900 border-b border-gray-700 shadow-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chapter-info {
|
.chapter-title-section {
|
||||||
@apply mt-2;
|
@apply space-y-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter-meta {
|
||||||
|
@apply flex flex-wrap items-center gap-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reader-container {
|
.reader-container {
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="bg-green-600 h-16 flex items-center fixed w-full z-50">
|
<header
|
||||||
|
:class="[
|
||||||
|
'bg-green-600 h-16 flex items-center fixed w-full z-50 transition-transform duration-300 ease-in-out',
|
||||||
|
headerStore.shouldShowHeader ? 'translate-y-0' : '-translate-y-full'
|
||||||
|
]"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
@click="$emit('menu-click')"
|
@click="$emit('menu-click')"
|
||||||
class="ml-4 text-white p-2 md:hidden"
|
:class="[
|
||||||
|
'ml-4 text-white p-2',
|
||||||
|
showMenuButton ? '' : 'md:hidden'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<Bars3Icon class="h-6 w-6" />
|
<Bars3Icon class="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
@@ -17,7 +25,17 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Bars3Icon } from '@heroicons/vue/24/outline';
|
import { Bars3Icon } from '@heroicons/vue/24/outline';
|
||||||
|
import { useHeaderStore } from '../../stores/headerStore';
|
||||||
import SearchBar from './SearchBar.vue';
|
import SearchBar from './SearchBar.vue';
|
||||||
|
|
||||||
|
const headerStore = useHeaderStore();
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
showMenuButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
defineEmits(['menu-click']);
|
defineEmits(['menu-click']);
|
||||||
</script>
|
</script>
|
||||||
@@ -1,24 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50 flex">
|
<div class="min-h-screen bg-gray-50 flex">
|
||||||
<Header
|
<Header
|
||||||
|
:show-menu-button="isReaderMode"
|
||||||
@menu-click="toggleSidebar"
|
@menu-click="toggleSidebar"
|
||||||
@manga-click="$emit('manga-click', $event)"
|
@manga-click="$emit('manga-click', $event)"
|
||||||
@add-manga-click="$emit('add-manga-click', $event)" />
|
@add-manga-click="$emit('add-manga-click', $event)" />
|
||||||
<Sidebar :is-open="isSidebarOpen" @close="closeSidebar" @add-manga-click="$emit('add-manga-click', $event)" />
|
<Sidebar
|
||||||
|
:is-open="isSidebarOpen"
|
||||||
|
:force-mobile-behavior="isReaderMode"
|
||||||
|
@close="closeSidebar"
|
||||||
|
@add-manga-click="$emit('add-manga-click', $event)" />
|
||||||
|
|
||||||
<main class="flex-1 pt-16 md:ml-60">
|
<main :class="[
|
||||||
|
'flex-1 pt-16',
|
||||||
|
isReaderMode ? '' : 'md:ml-60'
|
||||||
|
]">
|
||||||
<RouterView></RouterView>
|
<RouterView></RouterView>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import Header from './Header.vue';
|
import { useRoute } from 'vue-router';
|
||||||
import Sidebar from './Sidebar.vue';
|
import Header from './Header.vue';
|
||||||
|
import Sidebar from './Sidebar.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const isSidebarOpen = ref(false);
|
const isSidebarOpen = ref(false);
|
||||||
|
|
||||||
|
// Détecte si on est en mode Reader
|
||||||
|
const isReaderMode = computed(() => {
|
||||||
|
return route.name === 'reader';
|
||||||
|
});
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
isSidebarOpen.value = !isSidebarOpen.value;
|
isSidebarOpen.value = !isSidebarOpen.value;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
<aside
|
<aside
|
||||||
:class="[
|
:class="[
|
||||||
'fixed top-16 left-0 w-60 bg-gray-600 text-white transform transition-transform duration-300 ease-in-out z-40 h-full',
|
'fixed top-16 left-0 w-60 bg-gray-600 text-white transform transition-transform duration-300 ease-in-out z-40 h-full',
|
||||||
isOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'
|
isOpen ? 'translate-x-0' : '-translate-x-full',
|
||||||
|
!forceMobileBehavior ? 'md:translate-x-0' : ''
|
||||||
]"
|
]"
|
||||||
role="navigation"
|
role="navigation"
|
||||||
aria-label="Menu principal">
|
aria-label="Menu principal">
|
||||||
@@ -23,22 +24,26 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
BookOpenIcon,
|
ArrowDownTrayIcon,
|
||||||
PlusIcon,
|
ArrowsRightLeftIcon,
|
||||||
ArrowDownTrayIcon,
|
BookOpenIcon,
|
||||||
GlobeAltIcon,
|
CalendarIcon,
|
||||||
ArrowsRightLeftIcon,
|
ClockIcon,
|
||||||
CalendarIcon,
|
Cog6ToothIcon,
|
||||||
ClockIcon,
|
ComputerDesktopIcon,
|
||||||
Cog6ToothIcon,
|
GlobeAltIcon,
|
||||||
ComputerDesktopIcon
|
PlusIcon
|
||||||
} from '@heroicons/vue/24/solid';
|
} from '@heroicons/vue/24/solid';
|
||||||
import MenuGroup from './sidebar/MenuGroup.vue';
|
import MenuGroup from './sidebar/MenuGroup.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isOpen: {
|
isOpen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
forceMobileBehavior: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
73
assets/vue/app/shared/stores/headerStore.js
Normal file
73
assets/vue/app/shared/stores/headerStore.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export const useHeaderStore = defineStore('header', {
|
||||||
|
state: () => ({
|
||||||
|
isHeaderVisible: true,
|
||||||
|
isAutoHideEnabled: false,
|
||||||
|
lastScrollY: 0,
|
||||||
|
scrollDirection: 'up'
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
shouldShowHeader: (state) => {
|
||||||
|
// Si l'auto-hide n'est pas activé, toujours afficher le header
|
||||||
|
if (!state.isAutoHideEnabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si l'auto-hide est activé, suivre la visibilité
|
||||||
|
return state.isHeaderVisible;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
enableAutoHide() {
|
||||||
|
this.isAutoHideEnabled = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
disableAutoHide() {
|
||||||
|
this.isAutoHideEnabled = false;
|
||||||
|
this.isHeaderVisible = true; // Toujours visible quand désactivé
|
||||||
|
},
|
||||||
|
|
||||||
|
updateScrollDirection(scrollY) {
|
||||||
|
// Éviter les calculs inutiles si pas d'auto-hide
|
||||||
|
if (!this.isAutoHideEnabled) {
|
||||||
|
this.lastScrollY = scrollY;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Détecter la direction du scroll avec un seuil pour éviter les micro-mouvements
|
||||||
|
const scrollDifference = Math.abs(scrollY - this.lastScrollY);
|
||||||
|
|
||||||
|
if (scrollDifference < 5) {
|
||||||
|
// Mouvement trop petit, on ignore
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollY > this.lastScrollY && scrollY > 100) {
|
||||||
|
// Scroll vers le bas et suffisamment de scroll
|
||||||
|
if (this.scrollDirection !== 'down') {
|
||||||
|
this.scrollDirection = 'down';
|
||||||
|
this.isHeaderVisible = false;
|
||||||
|
}
|
||||||
|
} else if (scrollY < this.lastScrollY) {
|
||||||
|
// Scroll vers le haut
|
||||||
|
if (this.scrollDirection !== 'up') {
|
||||||
|
this.scrollDirection = 'up';
|
||||||
|
this.isHeaderVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastScrollY = scrollY;
|
||||||
|
},
|
||||||
|
|
||||||
|
showHeader() {
|
||||||
|
this.isHeaderVisible = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
hideHeader() {
|
||||||
|
this.isHeaderVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user