refactor(vue3): migrate the remaining Carousel components to Composition API

With the improvements on loading, items of the carousel would change after the component is instantiated.
However, there were some issues preventing the components from being reactive.
By migrating to Composition API, those reactivity issues are fixed.
This commit is contained in:
Fernando Fernández
2023-04-03 12:52:57 +02:00
parent f2a28346cb
commit 0916e4e9b8
5 changed files with 103 additions and 222 deletions
@@ -8,8 +8,8 @@
:paused="isPaused"
:class="useResponsiveClasses('px-2 px-sm-4 progress-bar')"
hoverable
@on-animation-end="onAnimationEnd"
@on-progress-clicked="onProgressClicked" />
@animation-end="onAnimationEnd"
@progress-clicked="onProgressClicked" />
<swiper
:modules="modules"
:class="useResponsiveClasses('swiper')"
@@ -35,8 +35,8 @@
:paused="isPaused"
:class="useResponsiveClasses('px-2 px-sm-4 progress-bar')"
hoverable
@on-animation-end="onAnimationEnd"
@on-progress-clicked="onProgressClicked" />
@animation-end="onAnimationEnd"
@progress-clicked="onProgressClicked" />
</div>
</template>
@@ -4,148 +4,65 @@
v-for="i in pages"
:key="`progress-key-${i}`"
:class="useResponsiveClasses('progress-bar')"
@click.capture="onProgressClicked(i)">
@click.capture="emit('progressClicked', i - 1)">
<div
ref="progress"
:class="
expand
? useResponsiveClasses(
'progress d-flex align-center justify-center expand'
)
: useResponsiveClasses(
'progress d-flex align-center justify-center'
)
" />
:class="useResponsiveClasses(barClasses[i - 1])"
@animationend="emit('animationEnd')" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import { computed } from 'vue';
import { useDisplay } from 'vuetify';
import { useResponsiveClasses } from '@/composables';
export default defineComponent({
props: {
pages: {
type: Number,
required: true
},
currentIndex: {
type: Number,
required: true
},
duration: {
type: Number,
required: true
},
paused: {
type: Boolean,
required: true,
default: false
},
hoverable: {
type: Boolean,
required: false,
default: false
const props = withDefaults(
defineProps<{
pages: number;
currentIndex: number;
duration: number;
paused: boolean;
hoverable?: boolean;
}>(),
{ hoverable: false }
);
const emit = defineEmits<{
(e: 'animationEnd'): void;
(e: 'progressClicked', index: number): void;
}>();
const display = useDisplay();
const defaultBarClasses = Object.freeze([
'progress',
'd-flex',
'align-center',
'justify-center'
]);
const expand = computed(() => props.hoverable && !display.mobile.value);
const animDuration = computed(() => (props.duration / 1000).toString() + 's');
const barClasses = computed(() =>
Array.from({ length: props.pages }).map((_, i) => {
const classes = [...defaultBarClasses];
if (expand.value) {
classes.push('expand');
}
},
setup() {
return { useResponsiveClasses };
},
data() {
return {
bars: [] as HTMLElement[]
};
},
computed: {
expand(): boolean {
return this.hoverable && !this.$vuetify.display.mobile;
if (i === props.currentIndex) {
classes.push('active');
if (props.paused) {
classes.push('paused');
}
} else if (i < props.currentIndex) {
classes.push('passed');
}
},
watch: {
currentIndex(): void {
this.$nextTick(() => {
window.requestAnimationFrame(this.updateBars);
});
},
paused(): void {
this.$nextTick(() => {
window.requestAnimationFrame(this.togglePause);
});
},
duration(): void {
this.$nextTick(() => {
window.requestAnimationFrame(this.setAnimationDuration);
});
}
},
mounted() {
this.$nextTick(() => {
this.bars = this.$refs.progress as Array<HTMLElement>;
window.requestAnimationFrame(this.setAnimationDuration);
window.requestAnimationFrame(this.updateBars);
});
},
unmounted() {
const animEndFunction = this.onAnimationEnd;
this.bars.forEach((element: HTMLElement) => {
element.removeEventListener('animationend', animEndFunction);
});
},
methods: {
updateBars(): void {
const followingBars = this.bars.slice(this.currentIndex + 1);
const previousBars = this.bars.slice(0, this.currentIndex);
const activeBar = this.bars[this.currentIndex];
const animEndFunction = this.onAnimationEnd;
if (activeBar) {
activeBar.classList.add('active');
activeBar.addEventListener('animationend', animEndFunction);
}
if (previousBars) {
window.requestAnimationFrame(() => {
previousBars.forEach((element: HTMLElement) => {
element.classList.remove('active', 'paused');
element.removeEventListener('animationend', animEndFunction);
element.classList.add('passed');
});
});
}
if (followingBars) {
window.requestAnimationFrame(() => {
followingBars.forEach((element: HTMLElement) => {
element.classList.remove('active', 'passed', 'paused');
element.removeEventListener('animationend', animEndFunction);
});
});
}
},
onAnimationEnd(): void {
this.$emit('on-animation-end');
},
onProgressClicked(index: number): void {
this.$emit('on-progress-clicked', index - 1);
},
togglePause(): void {
if (this.paused) {
this.bars[this.currentIndex].classList.add('paused');
} else {
this.bars[this.currentIndex].classList.remove('paused');
}
},
setAnimationDuration(): void {
const newDuration = (this.duration / 1000).toString() + 's';
this.bars.forEach((element: HTMLElement) => {
element.style.animationDuration = newDuration;
});
}
}
});
return classes.join(' ');
})
);
</script>
<style lang="scss" scoped>
@@ -196,6 +113,7 @@ export default defineComponent({
animation-timing-function: linear;
animation-delay: 0s;
animation-fill-mode: forwards;
animation-duration: v-bind(animDuration);
}
.progress.active {
@@ -20,7 +20,6 @@
</div>
<div :class="useResponsiveClasses('slide-content')">
<v-container
fill-height
class="mx-md-10 mt-md-5 py-md-4 align-end align-sm-center align-md-start">
<v-row>
<v-col cols="12" sm="8" md="6" xl="5" class="py-0 py-md-4">
@@ -31,96 +31,59 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import { getLogo, ImageUrlInfo } from '@/utils/images';
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client';
import { getLogo } from '@/utils/images';
import { getItemDetailsLink } from '@/utils/items';
export default defineComponent({
props: {
item: {
type: Object as () => BaseItemDto,
required: true
}
},
data() {
return {
itemLink: '',
titleString: '',
logoLink: '',
subtitle: ''
};
},
computed: {
logo(): ImageUrlInfo {
return getLogo(this.item);
}
},
watch: {
item: {
immediate: true,
handler(): void {
switch (this.item.Type) {
case 'MusicAlbum': {
if (this.item.AlbumArtists?.length) {
this.logoLink = getItemDetailsLink(
this.item.AlbumArtists[0],
'MusicArtist'
);
}
const props = defineProps<{ item: BaseItemDto }>();
const { t } = useI18n();
if (this.item.AlbumArtist) {
this.titleString = this.item.AlbumArtist;
}
const logo = computed(() => getLogo(props.item));
const itemLink = computed(() => getItemDetailsLink(props.item));
const titleString = computed(() => {
if (props.item.Type === BaseItemKind.MusicAlbum && props.item.AlbumArtist) {
return props.item.AlbumArtist;
} else if (
props.item.Type === BaseItemKind.Episode &&
props.item.SeriesName
) {
return props.item.SeriesName;
} else {
return props.item.Name;
}
});
if (this.item.Name) {
this.subtitle = this.item.Name;
}
const logoLink = computed(() => {
if (
props.item.Type === BaseItemKind.MusicAlbum &&
props.item.AlbumArtists?.length
) {
return getItemDetailsLink(
props.item.AlbumArtists[0],
BaseItemKind.MusicArtist
);
} else if (props.item.Type === BaseItemKind.Episode && props.item.SeriesId) {
return getItemDetailsLink({ Id: props.item.SeriesId }, BaseItemKind.Series);
}
});
break;
}
case 'Episode': {
if (this.item.SeriesId) {
this.logoLink = getItemDetailsLink(
{ Id: this.item.SeriesId },
'Series'
);
}
const subtitle = computed(() => {
if (props.item.Type === BaseItemKind.MusicAlbum) {
return props.item.Name;
} else if (
props.item.Type === BaseItemKind.Episode &&
props.item.SeasonName &&
props.item.IndexNumber &&
props.item.Name
) {
const episodeString = t('episodeNumber', {
episodeNumber: props.item.IndexNumber
});
if (
this.item.SeasonName &&
this.item.IndexNumber &&
this.item.Name
) {
const episodeString = this.$t('episodeNumber', {
episodeNumber: this.item.IndexNumber
});
this.subtitle = `${this.item.SeasonName} - ${episodeString}\n${this.item.Name}`;
}
if (this.item.SeriesName) {
this.titleString = this.item.SeriesName;
}
break;
}
}
/**
* Instead of using 'default', we need this additional extra check
* in case an Album doesn't have artists, for example.
*/
if (this.itemLink === '') {
this.itemLink = getItemDetailsLink(this.item);
}
if (this.titleString === '' && this.item.Name) {
this.titleString = this.item.Name;
}
}
}
return `${props.item.SeasonName} - ${episodeString}\n${props.item.Name}`;
}
});
</script>
+2 -1
View File
@@ -3,6 +3,7 @@
*/
import {
BaseItemDto,
BaseItemKind,
BaseItemPerson,
MediaStream
} from '@jellyfin/sdk/lib/generated-client';
@@ -255,7 +256,7 @@ export function canMarkWatched(item: BaseItemDto): boolean {
*/
export function getItemDetailsLink(
item: BaseItemDto | BaseItemPerson,
overrideType?: string
overrideType?: BaseItemKind
): string {
const router = useRouter();
let routeName: string;