mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-16 05:13:51 +02:00
replace el-carousel with Carousel component
This commit is contained in:
870
package-lock.json
generated
870
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -62,6 +62,7 @@
|
||||
"electron": "^39.2.7",
|
||||
"electron-builder": "^26.4.0",
|
||||
"element-plus": "^2.13.1",
|
||||
"embla-carousel-vue": "^8.6.0",
|
||||
"esbuild-jest": "^0.5.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
@@ -76,6 +77,7 @@
|
||||
"prettier": "^3.7.4",
|
||||
"reka-ui": "^2.7.0",
|
||||
"remixicon": "^4.8.0",
|
||||
"sass-embedded": "^1.97.2",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
|
||||
@@ -354,45 +354,24 @@
|
||||
:indeterminate="true"
|
||||
:percentage="100"
|
||||
:stroke-width="3"
|
||||
style="margin: 10px 0; max-width: 240px" />
|
||||
<el-carousel
|
||||
v-if="avatarDialog.galleryImages.length"
|
||||
type="card"
|
||||
:autoplay="false"
|
||||
height="200px">
|
||||
<el-carousel-item v-for="imageUrl in avatarDialog.galleryImages" :key="imageUrl">
|
||||
<img
|
||||
:src="imageUrl"
|
||||
style="width: 100%; height: 100%; object-fit: contain"
|
||||
@click="showFullscreenImageDialog(imageUrl)"
|
||||
loading="lazy" />
|
||||
<div
|
||||
v-if="avatarDialog.ref.authorId === currentUser.id"
|
||||
style="position: absolute; bottom: 35px; left: 38%">
|
||||
<el-button
|
||||
size="small"
|
||||
:icon="Back"
|
||||
circle
|
||||
class="x-link"
|
||||
style="margin-left: 0"
|
||||
@click.stop="reorderAvatarGalleryImage(imageUrl, -1)"></el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:icon="Right"
|
||||
circle
|
||||
class="x-link"
|
||||
style="margin-left: 0"
|
||||
@click.stop="reorderAvatarGalleryImage(imageUrl, 1)"></el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
circle
|
||||
class="x-link"
|
||||
style="margin-left: 0"
|
||||
@click.stop="deleteAvatarGalleryImage(imageUrl)"></el-button>
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
style="margin: 10px 0" />
|
||||
<div class="mt-2 w-[80%] ml-20">
|
||||
<Carousel v-if="avatarDialog.galleryImages.length" class="w-full">
|
||||
<CarouselContent class="h-50">
|
||||
<CarouselItem v-for="imageUrl in avatarDialog.galleryImages" :key="imageUrl">
|
||||
<div class="relative h-50 w-full">
|
||||
<img
|
||||
:src="imageUrl"
|
||||
style="width: 100%; height: 100%; object-fit: contain"
|
||||
@click="showFullscreenImageDialog(imageUrl)"
|
||||
loading="lazy" />
|
||||
</div>
|
||||
</CarouselItem>
|
||||
</CarouselContent>
|
||||
<CarouselPrevious />
|
||||
<CarouselNext />
|
||||
</Carousel>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="avatarDialog.ref.publishedListings?.length">
|
||||
<span class="name">{{ t('dialog.avatar.info.listings') }}</span>
|
||||
@@ -560,6 +539,7 @@
|
||||
User,
|
||||
Warning
|
||||
} from '@element-plus/icons-vue';
|
||||
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel';
|
||||
import { CircleCheck, Ellipsis, RefreshCcw, Star, Trash2 } from 'lucide-vue-next';
|
||||
import { computed, defineAsyncComponent, nextTick, ref, watch } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
56
src/components/ui/carousel/Carousel.vue
Normal file
56
src/components/ui/carousel/Carousel.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { useProvideCarousel } from './useCarousel';
|
||||
|
||||
const props = defineProps({
|
||||
opts: { type: Object, required: false },
|
||||
plugins: { type: null, required: false },
|
||||
orientation: { type: String, required: false, default: 'horizontal' },
|
||||
class: { type: null, required: false }
|
||||
});
|
||||
|
||||
const emits = defineEmits(['init-api']);
|
||||
|
||||
const { canScrollNext, canScrollPrev, carouselApi, carouselRef, orientation, scrollNext, scrollPrev } =
|
||||
useProvideCarousel(props, emits);
|
||||
|
||||
defineExpose({
|
||||
canScrollNext,
|
||||
canScrollPrev,
|
||||
carouselApi,
|
||||
carouselRef,
|
||||
orientation,
|
||||
scrollNext,
|
||||
scrollPrev
|
||||
});
|
||||
|
||||
function onKeyDown(event) {
|
||||
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
|
||||
const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
|
||||
|
||||
if (event.key === prevKey) {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === nextKey) {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="carousel"
|
||||
:class="cn('relative', props.class)"
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
tabindex="0"
|
||||
@keydown="onKeyDown">
|
||||
<slot :can-scroll-next :can-scroll-prev :carousel-api :carousel-ref :orientation :scroll-next :scroll-prev />
|
||||
</div>
|
||||
</template>
|
||||
25
src/components/ui/carousel/CarouselContent.vue
Normal file
25
src/components/ui/carousel/CarouselContent.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { useCarousel } from './useCarousel';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false }
|
||||
});
|
||||
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="carouselRef" data-slot="carousel-content" class="overflow-hidden">
|
||||
<div
|
||||
:class="cn('flex', orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col', props.class)"
|
||||
v-bind="$attrs">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
21
src/components/ui/carousel/CarouselItem.vue
Normal file
21
src/components/ui/carousel/CarouselItem.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { useCarousel } from './useCarousel';
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false }
|
||||
});
|
||||
|
||||
const { orientation } = useCarousel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="carousel-item"
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
:class="cn('min-w-0 shrink-0 grow-0 basis-full', orientation === 'horizontal' ? 'pl-4' : 'pt-4', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
38
src/components/ui/carousel/CarouselNext.vue
Normal file
38
src/components/ui/carousel/CarouselNext.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import { ArrowRight } from 'lucide-vue-next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { useCarousel } from './useCarousel';
|
||||
|
||||
const props = defineProps({
|
||||
variant: { type: String, required: false, default: 'outline' },
|
||||
size: { type: String, required: false, default: 'icon' },
|
||||
class: { type: null, required: false }
|
||||
});
|
||||
|
||||
const { orientation, canScrollNext, scrollNext } = useCarousel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
data-slot="carousel-next"
|
||||
:disabled="!canScrollNext"
|
||||
:class="
|
||||
cn(
|
||||
'absolute size-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? 'top-1/2 -right-12 -translate-y-1/2'
|
||||
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
:variant="variant"
|
||||
:size="size"
|
||||
@click="scrollNext">
|
||||
<slot>
|
||||
<ArrowRight />
|
||||
<span class="sr-only">Next Slide</span>
|
||||
</slot>
|
||||
</Button>
|
||||
</template>
|
||||
38
src/components/ui/carousel/CarouselPrevious.vue
Normal file
38
src/components/ui/carousel/CarouselPrevious.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import { ArrowLeft } from 'lucide-vue-next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { useCarousel } from './useCarousel';
|
||||
|
||||
const props = defineProps({
|
||||
variant: { type: String, required: false, default: 'outline' },
|
||||
size: { type: String, required: false, default: 'icon' },
|
||||
class: { type: null, required: false }
|
||||
});
|
||||
|
||||
const { orientation, canScrollPrev, scrollPrev } = useCarousel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
data-slot="carousel-previous"
|
||||
:disabled="!canScrollPrev"
|
||||
:class="
|
||||
cn(
|
||||
'absolute size-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? 'top-1/2 -left-12 -translate-y-1/2'
|
||||
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
:variant="variant"
|
||||
:size="size"
|
||||
@click="scrollPrev">
|
||||
<slot>
|
||||
<ArrowLeft />
|
||||
<span class="sr-only">Previous Slide</span>
|
||||
</slot>
|
||||
</Button>
|
||||
</template>
|
||||
7
src/components/ui/carousel/index.js
Normal file
7
src/components/ui/carousel/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export { default as Carousel } from './Carousel.vue';
|
||||
export { default as CarouselContent } from './CarouselContent.vue';
|
||||
export { default as CarouselItem } from './CarouselItem.vue';
|
||||
export { default as CarouselNext } from './CarouselNext.vue';
|
||||
export { default as CarouselPrevious } from './CarouselPrevious.vue';
|
||||
|
||||
export { useCarousel } from './useCarousel';
|
||||
63
src/components/ui/carousel/useCarousel.js
Normal file
63
src/components/ui/carousel/useCarousel.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { createInjectionState } from '@vueuse/core';
|
||||
|
||||
import emblaCarouselVue from 'embla-carousel-vue';
|
||||
|
||||
const [useProvideCarousel, useInjectCarousel] = createInjectionState(
|
||||
(props, emits) => {
|
||||
const { opts, orientation, plugins } = props;
|
||||
const [emblaNode, emblaApi] = emblaCarouselVue(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === 'horizontal' ? 'x' : 'y'
|
||||
},
|
||||
plugins
|
||||
);
|
||||
|
||||
function scrollPrev() {
|
||||
emblaApi.value?.scrollPrev();
|
||||
}
|
||||
function scrollNext() {
|
||||
emblaApi.value?.scrollNext();
|
||||
}
|
||||
|
||||
const canScrollNext = ref(false);
|
||||
const canScrollPrev = ref(false);
|
||||
|
||||
function onSelect(api) {
|
||||
canScrollNext.value = api?.canScrollNext() || false;
|
||||
canScrollPrev.value = api?.canScrollPrev() || false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!emblaApi.value) return;
|
||||
|
||||
emblaApi.value?.on('init', onSelect);
|
||||
emblaApi.value?.on('reInit', onSelect);
|
||||
emblaApi.value?.on('select', onSelect);
|
||||
|
||||
emits('init-api', emblaApi.value);
|
||||
});
|
||||
|
||||
return {
|
||||
carouselRef: emblaNode,
|
||||
carouselApi: emblaApi,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
orientation
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
function useCarousel() {
|
||||
const carouselState = useInjectCarousel();
|
||||
|
||||
if (!carouselState)
|
||||
throw new Error('useCarousel must be used within a <Carousel />');
|
||||
|
||||
return carouselState;
|
||||
}
|
||||
|
||||
export { useCarousel, useProvideCarousel };
|
||||
@@ -111,36 +111,39 @@
|
||||
:hint="screenshotMetadataDialog.metadata.author.displayName"
|
||||
style="color: var(--el-text-color-secondary); font-family: monospace" />
|
||||
<br />
|
||||
<el-carousel
|
||||
ref="screenshotMetadataCarouselRef"
|
||||
:interval="0"
|
||||
:initial-index="1"
|
||||
indicator-position="none"
|
||||
arrow="always"
|
||||
height="600px"
|
||||
style="margin-top: 10px"
|
||||
@change="screenshotMetadataCarouselChange">
|
||||
<el-carousel-item>
|
||||
<img
|
||||
class="x-link"
|
||||
:src="screenshotMetadataDialog.metadata.previousFilePath"
|
||||
style="width: 100%; height: 100%; object-fit: contain" />
|
||||
</el-carousel-item>
|
||||
<el-carousel-item>
|
||||
<img
|
||||
class="x-link"
|
||||
:src="screenshotMetadataDialog.metadata.filePath"
|
||||
style="width: 100%; height: 100%; object-fit: contain"
|
||||
@click="showFullscreenImageDialog(screenshotMetadataDialog.metadata.filePath)" />
|
||||
</el-carousel-item>
|
||||
<el-carousel-item>
|
||||
<img
|
||||
class="x-link"
|
||||
:src="screenshotMetadataDialog.metadata.nextFilePath"
|
||||
style="width: 100%; height: 100%; object-fit: contain" />
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<br />
|
||||
<div class="my-2 w-[95%] ml-6.5">
|
||||
<Carousel :opts="{ loop: false }" @init-api="handleScreenshotMetadataCarouselInit">
|
||||
<CarouselContent class="h-150">
|
||||
<CarouselItem>
|
||||
<div class="h-150 w-full">
|
||||
<img
|
||||
class="x-link"
|
||||
:src="screenshotMetadataDialog.metadata.previousFilePath"
|
||||
style="width: 100%; height: 100%; object-fit: contain" />
|
||||
</div>
|
||||
</CarouselItem>
|
||||
<CarouselItem>
|
||||
<div class="h-150 w-full">
|
||||
<img
|
||||
class="x-link"
|
||||
:src="screenshotMetadataDialog.metadata.filePath"
|
||||
style="width: 100%; height: 100%; object-fit: contain"
|
||||
@click="showFullscreenImageDialog(screenshotMetadataDialog.metadata.filePath)" />
|
||||
</div>
|
||||
</CarouselItem>
|
||||
<CarouselItem>
|
||||
<div class="h-150 w-full">
|
||||
<img
|
||||
class="x-link"
|
||||
:src="screenshotMetadataDialog.metadata.nextFilePath"
|
||||
style="width: 100%; height: 100%; object-fit: contain" />
|
||||
</div>
|
||||
</CarouselItem>
|
||||
</CarouselContent>
|
||||
<CarouselPrevious />
|
||||
<CarouselNext />
|
||||
</Carousel>
|
||||
</div>
|
||||
<template v-if="screenshotMetadataDialog.metadata.error">
|
||||
<pre
|
||||
style="white-space: pre-wrap; font-size: 12px"
|
||||
@@ -161,6 +164,7 @@
|
||||
|
||||
<script setup>
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { storeToRefs } from 'pinia';
|
||||
@@ -219,11 +223,20 @@
|
||||
});
|
||||
|
||||
const screenshotMetadataSearchInputs = ref(0);
|
||||
const screenshotMetadataCarouselRef = ref(null);
|
||||
const screenshotMetadataCarouselApi = ref(null);
|
||||
const ignoreCarouselSelect = ref(false);
|
||||
|
||||
const handleComponentKeyup = (event) => {
|
||||
const carouselNavigation = { ArrowLeft: 0, ArrowRight: 2 }[event.key];
|
||||
if (typeof carouselNavigation !== 'undefined' && props.isScreenshotMetadataDialogVisible) {
|
||||
if (screenshotMetadataCarouselApi.value) {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
screenshotMetadataCarouselApi.value.scrollPrev();
|
||||
} else {
|
||||
screenshotMetadataCarouselApi.value.scrollNext();
|
||||
}
|
||||
return;
|
||||
}
|
||||
screenshotMetadataCarouselChange(carouselNavigation);
|
||||
}
|
||||
};
|
||||
@@ -407,9 +420,7 @@
|
||||
getAndDisplayScreenshot(D.metadata.filePath);
|
||||
}
|
||||
}
|
||||
if (typeof screenshotMetadataCarouselRef.value !== 'undefined') {
|
||||
screenshotMetadataCarouselRef.value.setActiveItem(1);
|
||||
}
|
||||
resetCarouselIndex();
|
||||
|
||||
if (fullscreenImageDialog.value.visible) {
|
||||
// TODO
|
||||
@@ -451,13 +462,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof screenshotMetadataCarouselRef.value !== 'undefined') {
|
||||
screenshotMetadataCarouselRef.value.setActiveItem(1);
|
||||
}
|
||||
resetCarouselIndex();
|
||||
|
||||
D.searchIndex = searchIndex;
|
||||
}
|
||||
|
||||
function handleScreenshotMetadataCarouselInit(api) {
|
||||
screenshotMetadataCarouselApi.value = api;
|
||||
api.on('select', handleCarouselSelect);
|
||||
api.on('reInit', handleCarouselSelect);
|
||||
resetCarouselIndex();
|
||||
}
|
||||
|
||||
function handleCarouselSelect() {
|
||||
if (ignoreCarouselSelect.value || !screenshotMetadataCarouselApi.value) {
|
||||
return;
|
||||
}
|
||||
const index = screenshotMetadataCarouselApi.value.selectedScrollSnap();
|
||||
screenshotMetadataCarouselChange(index);
|
||||
}
|
||||
|
||||
function resetCarouselIndex() {
|
||||
const api = screenshotMetadataCarouselApi.value;
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
ignoreCarouselSelect.value = true;
|
||||
api.scrollTo(1, true);
|
||||
setTimeout(() => {
|
||||
ignoreCarouselSelect.value = false;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async function getAndDisplayScreenshot(path, needsCarouselFiles = true) {
|
||||
const metadata = await AppApi.GetScreenshotMetadata(path);
|
||||
displayScreenshotMetadata(metadata, needsCarouselFiles);
|
||||
|
||||
Reference in New Issue
Block a user