mirror of
https://github.com/jellyfin/jellyfin-vue.git
synced 2026-09-03 05:10:26 +03:00
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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user