Files
Mangarr/assets/vue/app/domain/reader/presentation/components/ChapterReader.vue

163 lines
4.7 KiB
Vue

<template>
<div class="chapter-reader" :class="{ rtl: store.readingDirection === 'rtl' }">
<div v-if="store.isLoading" class="loading">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
<div v-else-if="store.error" class="error">
{{ store.error }}
</div>
<div v-else class="reader-content">
<div class="reader-controls">
<button @click="store.previousPage" :disabled="store.isFirstPage">
<ChevronLeftIcon class="h-6 w-6" />
</button>
<div class="page-info"> {{ store.currentPage + 1 }} / {{ store.totalPages }} </div>
<button @click="store.nextPage" :disabled="store.isLastPage">
<ChevronRightIcon class="h-6 w-6" />
</button>
</div>
<div class="page-container" :style="{ transform: `scale(${store.zoom})` }">
<div v-if="!store.currentPageData" class="error"> Aucune donnée d'image disponible </div>
<div v-else-if="!store.currentPageData.base64Content" class="error"> Contenu de l'image manquant </div>
<img v-else :src="imageSource" :alt="`Page ${store.currentPage + 1}`" class="page-image" />
</div>
<div class="reader-settings">
<button @click="toggleReadingMode">
{{ store.readingMode === 'single' ? 'Mode Infini' : 'Mode Simple' }}
</button>
<button @click="toggleReadingDirection">
{{ store.readingDirection === 'ltr' ? 'RTL' : 'LTR' }}
</button>
<div class="zoom-controls">
<button @click="zoomOut">-</button>
<span>{{ Math.round(store.zoom * 100) }}%</span>
<button @click="zoomIn">+</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, computed, watch } from 'vue';
import { useReaderStore } from '../../application/store/readerStore';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/vue/24/outline';
const props = defineProps({
chapterId: {
type: String,
required: true
}
});
const store = useReaderStore();
const imageSource = computed(() => {
if (!store.currentPageData?.base64Content || !store.currentPageData?.mimeType) {
return '';
}
return `data:${store.currentPageData.mimeType};base64,${store.currentPageData.base64Content}`;
});
const toggleReadingMode = () => {
store.setReadingMode(store.readingMode === 'single' ? 'infinite' : 'single');
};
const toggleReadingDirection = () => {
store.setReadingDirection(store.readingDirection === 'ltr' ? 'rtl' : 'ltr');
};
const zoomIn = () => {
store.setZoom(Math.min(store.zoom + 0.1, 2));
};
const zoomOut = () => {
store.setZoom(Math.max(store.zoom - 0.1, 0.5));
};
const handleKeyPress = event => {
if (event.key === 'ArrowRight') {
store.nextPage();
} else if (event.key === 'ArrowLeft') {
store.previousPage();
}
};
watch(
() => props.chapterId,
newId => {
if (newId) {
store.loadChapter(newId);
}
},
{ immediate: true }
);
onMounted(() => {
window.addEventListener('keydown', handleKeyPress);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyPress);
});
</script>
<style lang="postcss" scoped>
.chapter-reader {
@apply w-full h-full flex flex-col items-center justify-center bg-gray-900 text-white;
}
.loading {
@apply flex items-center justify-center h-full;
}
.error {
@apply text-red-500 text-xl;
}
.reader-content {
@apply w-full h-full flex flex-col;
}
.reader-controls {
@apply flex items-center justify-between p-4 bg-gray-800;
}
.page-info {
@apply text-lg font-medium;
}
.page-container {
@apply flex-1 flex items-center justify-center overflow-hidden;
transform-origin: center;
}
.page-image {
@apply max-w-full max-h-full object-contain;
}
.reader-settings {
@apply flex items-center justify-center gap-4 p-4 bg-gray-800;
}
.zoom-controls {
@apply flex items-center gap-2;
}
button {
@apply px-4 py-2 bg-gray-700 rounded hover:bg-gray-600 transition-colors;
}
button:disabled {
@apply opacity-50 cursor-not-allowed;
}
.rtl {
direction: rtl;
}
</style>