feat(album): add thumbnails and AlbumViewer.vue
This commit is contained in:
195
frontend/src/views/creators/AlbumViewer.vue
Normal file
195
frontend/src/views/creators/AlbumViewer.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
fullscreen
|
||||
:scrim="true"
|
||||
transition="dialog-bottom-transition"
|
||||
@click:outside="closeViewer"
|
||||
>
|
||||
<div class="album-viewer" @click.self="closeViewer">
|
||||
<!-- Main image container -->
|
||||
<div class="image-container">
|
||||
<img
|
||||
:src="currentImage"
|
||||
:alt="t('viewer.imageAlt', { index: currentIndex + 1 })"
|
||||
class="main-image"
|
||||
/>
|
||||
|
||||
<!-- Navigation buttons -->
|
||||
<button
|
||||
class="nav-btn left-btn"
|
||||
@click.stop="previousImage"
|
||||
:disabled="currentIndex === 0"
|
||||
:title="t('viewer.previous')"
|
||||
>
|
||||
<v-icon size="large" color="white">mdi-chevron-left</v-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="nav-btn right-btn"
|
||||
@click.stop="nextImage"
|
||||
:disabled="currentIndex === images.length - 1"
|
||||
:title="t('viewer.next')"
|
||||
>
|
||||
<v-icon size="large" color="white">mdi-chevron-right</v-icon>
|
||||
</button>
|
||||
|
||||
<!-- Close button -->
|
||||
<button
|
||||
class="close-btn"
|
||||
@click.stop="closeViewer"
|
||||
:title="t('viewer.close')"
|
||||
>
|
||||
<v-icon size="large" color="white">mdi-close</v-icon>
|
||||
</button>
|
||||
|
||||
<!-- Image counter -->
|
||||
<div class="image-counter">
|
||||
{{ currentIndex + 1 }} / {{ images.length }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
images: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
startIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const dialog = ref(false);
|
||||
const currentIndex = ref(0);
|
||||
|
||||
const currentImage = computed(() => props.images[currentIndex.value]);
|
||||
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
dialog.value = newVal;
|
||||
if (newVal) {
|
||||
currentIndex.value = props.startIndex;
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => dialog.value, (newVal) => {
|
||||
emit('update:modelValue', newVal);
|
||||
});
|
||||
|
||||
function nextImage() {
|
||||
if (currentIndex.value < props.images.length - 1) {
|
||||
currentIndex.value++;
|
||||
}
|
||||
}
|
||||
|
||||
function previousImage() {
|
||||
if (currentIndex.value > 0) {
|
||||
currentIndex.value--;
|
||||
}
|
||||
}
|
||||
|
||||
function closeViewer() {
|
||||
dialog.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.album-viewer {
|
||||
@apply fixed inset-0;
|
||||
@apply flex items-center justify-center;
|
||||
@apply bg-black bg-opacity-90;
|
||||
@apply z-50;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
@apply relative;
|
||||
@apply max-w-[90vw];
|
||||
@apply max-h-[90vh];
|
||||
}
|
||||
|
||||
.main-image {
|
||||
@apply max-w-full;
|
||||
@apply max-h-[90vh];
|
||||
@apply object-contain;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
@apply absolute top-1/2 -translate-y-1/2;
|
||||
@apply p-4;
|
||||
@apply rounded-full;
|
||||
@apply bg-black bg-opacity-50;
|
||||
@apply transition-all duration-200;
|
||||
@apply hover:bg-opacity-75;
|
||||
@apply disabled:opacity-30 disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.left-btn {
|
||||
@apply left-4;
|
||||
}
|
||||
|
||||
.right-btn {
|
||||
@apply right-4;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
@apply absolute top-4 right-4;
|
||||
@apply p-2;
|
||||
@apply rounded-full;
|
||||
@apply bg-black bg-opacity-50;
|
||||
@apply transition-all duration-200;
|
||||
@apply hover:bg-opacity-75;
|
||||
}
|
||||
|
||||
.image-counter {
|
||||
@apply absolute bottom-4 left-1/2 -translate-x-1/2;
|
||||
@apply px-4 py-2;
|
||||
@apply bg-black bg-opacity-50;
|
||||
@apply text-white;
|
||||
@apply rounded-full;
|
||||
@apply text-sm;
|
||||
}
|
||||
</style>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"en": {
|
||||
"viewer": {
|
||||
"previous": "Previous image",
|
||||
"next": "Next image",
|
||||
"close": "Close viewer",
|
||||
"imageAlt": "Image {index}"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"viewer": {
|
||||
"previous": "Image précédente",
|
||||
"next": "Image suivante",
|
||||
"close": "Fermer",
|
||||
"imageAlt": "Image {index}"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"viewer": {
|
||||
"previous": "Imagen anterior",
|
||||
"next": "Imagen siguiente",
|
||||
"close": "Cerrar",
|
||||
"imageAlt": "Imagen {index}"
|
||||
}
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
Reference in New Issue
Block a user