Skip to content

Commit 81ddf7f

Browse files
committed
feat(poster): refactor poster in dedicated component
1 parent 5bddb01 commit 81ddf7f

File tree

5 files changed

+171
-83
lines changed

5 files changed

+171
-83
lines changed

src/components/common/list/ListItem.vue

+8-76
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { NEmpty, NFlex, NImage, NSkeleton, NTime, NTimelineItem } from 'naive-ui';
2+
import { NEmpty, NFlex, NSkeleton, NTime, NTimelineItem } from 'naive-ui';
33
44
import {
55
computed,
@@ -9,14 +9,13 @@ import {
99
type PropType,
1010
ref,
1111
toRefs,
12-
watch,
1312
} from 'vue';
1413
15-
import PosterPlaceholder from '~/assets/images/poster-placholder.webp';
14+
import type { PosterItem } from '~/models/poster.model';
15+
1616
import ListItemPanel from '~/components/common/list/ListItemPanel.vue';
17+
import PosterComponent from '~/components/common/poster/PosterComponent.vue';
1718
import { type ListScrollItem, ListScrollItemType } from '~/models/list-scroll.model';
18-
19-
import { useImageStore } from '~/stores/data/image.store';
2019
import { Colors } from '~/styles/colors.style';
2120
import { useI18n } from '~/utils';
2221
@@ -110,50 +109,6 @@ const year = new Date().getFullYear();
110109
const sameYear = computed(() => date.value?.getFullYear() === year);
111110
const loading = computed(() => item?.value?.loading);
112111
113-
const { getImageUrl } = useImageStore();
114-
115-
const resolvedPoster = computed(() => {
116-
if (poster?.value) return poster.value;
117-
if (item.value.poster) return item.value.poster;
118-
const image = item.value.posterRef?.value;
119-
if (!image) return;
120-
if (typeof image === 'string') return image;
121-
if (episode.value && 'backdrop' in image) return image.backdrop;
122-
return image.poster;
123-
});
124-
125-
const objectFit = computed(() =>
126-
resolvedPoster.value === PosterPlaceholder ? 'contain' : 'cover',
127-
);
128-
129-
const imgLoaded = ref(true);
130-
131-
const onLoad = () => {
132-
imgLoaded.value = true;
133-
};
134-
135-
const transition = ref(false);
136-
const timeout = ref();
137-
138-
const getPosters = (_item: ListScrollItem) => {
139-
imgLoaded.value = false;
140-
if (_item.posterRef?.value) return;
141-
if (!_item.posterRef) return;
142-
const query = _item.getPosterQuery?.();
143-
if (!query) return;
144-
if (!episode.value && _item.type === 'episode') {
145-
query.type = 'show';
146-
delete query.episode;
147-
}
148-
setTimeout(() => {
149-
if (resolvedPoster.value) return;
150-
transition.value = true;
151-
}, 100);
152-
getImageUrl(query, 300, _item.posterRef);
153-
};
154-
155-
watch(item, getPosters, { immediate: true, flush: 'post' });
156-
157112
const itemRef = ref<HTMLElement & InstanceType<typeof NTimelineItem>>();
158113
159114
onMounted(() => {
@@ -165,7 +120,6 @@ onMounted(() => {
165120
});
166121
167122
onBeforeUnmount(() => {
168-
clearTimeout(timeout.value);
169123
if (!scrollIntoView.value) return;
170124
emit('onScrollOutOfView', {
171125
item: item?.value,
@@ -243,32 +197,10 @@ const onClick = () => emit('onItemClick', { item: item?.value });
243197
</NFlex>
244198
</slot>
245199
<NFlex v-else class="tile" :wrap="false">
246-
<NImage
247-
alt="poster-image"
248-
class="poster"
249-
:class="{
250-
episode,
251-
loading: !imgLoaded,
252-
transition,
253-
}"
254-
:object-fit="objectFit"
255-
width="100%"
256-
lazy
257-
preview-disabled
258-
:src="resolvedPoster"
259-
:fallback-src="PosterPlaceholder"
260-
:on-load="onLoad"
261-
/>
262-
<NImage
263-
alt="poster-image-fallback"
264-
class="poster placeholder"
265-
:class="{ episode }"
266-
object-fit="contain"
267-
width="100%"
268-
lazy
269-
preview-disabled
270-
:src="PosterPlaceholder"
271-
:fallback-src="PosterPlaceholder"
200+
<PosterComponent
201+
:item="item as PosterItem"
202+
:poster="poster"
203+
:episode="episode"
272204
/>
273205
<ListItemPanel
274206
:item="item"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<script lang="ts" setup>
2+
import { NImage } from 'naive-ui';
3+
4+
import { computed, onBeforeUnmount, type PropType, ref, toRefs, watch } from 'vue';
5+
6+
import type { PosterItem } from '~/models/poster.model';
7+
8+
import PosterPlaceholder from '~/assets/images/poster-placholder.webp';
9+
import { useImageStore } from '~/stores/data/image.store';
10+
11+
const props = defineProps({
12+
item: {
13+
type: Object as PropType<PosterItem>,
14+
required: true,
15+
},
16+
poster: {
17+
type: String,
18+
required: false,
19+
},
20+
episode: {
21+
type: Boolean,
22+
required: false,
23+
default: false,
24+
},
25+
size: {
26+
type: Number,
27+
required: false,
28+
default: 300,
29+
},
30+
});
31+
32+
const { episode, poster, item, size } = toRefs(props);
33+
34+
const imgLoaded = ref(true);
35+
36+
const onLoad = () => {
37+
imgLoaded.value = true;
38+
};
39+
40+
const resolvedPoster = computed(() => {
41+
if (poster?.value) return poster.value;
42+
if (item.value.poster) return item.value.poster;
43+
const image = item.value.posterRef?.value;
44+
if (!image) return;
45+
if (typeof image === 'string') return image;
46+
if (episode.value && 'backdrop' in image) return image.backdrop;
47+
return image.poster;
48+
});
49+
50+
const objectFit = computed(() =>
51+
resolvedPoster.value === PosterPlaceholder ? 'contain' : 'cover',
52+
);
53+
54+
const transition = ref(false);
55+
const timeout = ref();
56+
57+
const { getImageUrl } = useImageStore();
58+
59+
const getPosters = (_item: PosterItem) => {
60+
imgLoaded.value = false;
61+
if (_item.posterRef?.value) return;
62+
if (!_item.posterRef) return;
63+
const query = _item.getPosterQuery?.();
64+
if (!query) return;
65+
if (!episode.value && _item.type === 'episode') {
66+
query.type = 'show';
67+
delete query.episode;
68+
}
69+
setTimeout(() => {
70+
if (imgLoaded.value) return;
71+
transition.value = true;
72+
}, 100);
73+
getImageUrl(query, size.value, _item.posterRef);
74+
};
75+
76+
watch(item, getPosters, { immediate: true, flush: 'post' });
77+
78+
onBeforeUnmount(() => {
79+
clearTimeout(timeout.value);
80+
});
81+
</script>
82+
83+
<template>
84+
<NImage
85+
alt="poster-image"
86+
class="poster"
87+
:class="{
88+
episode,
89+
loading: !imgLoaded,
90+
transition,
91+
}"
92+
:object-fit="objectFit"
93+
width="100%"
94+
lazy
95+
preview-disabled
96+
:src="resolvedPoster"
97+
:fallback-src="PosterPlaceholder"
98+
:on-load="onLoad"
99+
/>
100+
<NImage
101+
alt="poster-image-fallback"
102+
class="poster placeholder"
103+
:class="{ episode }"
104+
object-fit="contain"
105+
width="100%"
106+
lazy
107+
preview-disabled
108+
:src="PosterPlaceholder"
109+
:fallback-src="PosterPlaceholder"
110+
/>
111+
</template>
112+
113+
<style lang="scss" scoped>
114+
@use '~/styles/z-index' as layers;
115+
116+
.poster {
117+
flex: 0 0 var(--poster-width, 5.3125rem);
118+
justify-content: center;
119+
width: var(--poster-width, 5.3125rem);
120+
height: var(--poster-height, 8rem);
121+
opacity: 1;
122+
will-change: opacity;
123+
124+
&.loading {
125+
opacity: 0;
126+
}
127+
128+
&.transition {
129+
transition: opacity 0.5s var(--n-bezier);
130+
131+
&.loading {
132+
transition: opacity 0.1s;
133+
}
134+
}
135+
136+
&.episode {
137+
flex: 0 0 var(--poster-width, 14.23rem);
138+
width: var(--poster-width, 14.23rem);
139+
}
140+
141+
&.placeholder {
142+
position: absolute;
143+
background-color: #111;
144+
}
145+
146+
&:not(.placeholder) {
147+
z-index: layers.$in-front;
148+
}
149+
}
150+
</style>

src/models/list-scroll.model.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { NVirtualList, TagProps, VirtualListInst } from 'naive-ui';
22

33
import type { Ref } from 'vue';
4+
import type { PosterItem } from '~/models/poster.model';
45
import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
56
import type { TraktList } from '~/models/trakt/trakt-list.model';
67
import type { TraktMovie } from '~/models/trakt/trakt-movie.model';
78
import type { TraktPerson } from '~/models/trakt/trakt-people.model';
89
import type { BaseTraktProgress, BaseTraktProgressEpisode, BaseTraktProgressSeason } from '~/models/trakt/trakt-progress.model';
910
import type { TraktSeason } from '~/models/trakt/trakt-season.model';
1011
import type { TraktShow } from '~/models/trakt/trakt-show.model';
11-
import type { ImageQuery, ImageStoreMedias } from '~/stores/data/image.store';
1212

1313
export type VirtualListRef = VirtualListInst & InstanceType<typeof NVirtualList>;
1414
export type VirtualListProps = {
@@ -78,7 +78,7 @@ export const ListScrollItemType = {
7878
} as const;
7979

8080
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- meta is intentionally weakly typed
81-
export type ListScrollItem<T = Record<string, any>> = {
81+
export type ListScrollItem<T = Record<string, any>> = Omit<PosterItem, 'type'> & {
8282
id: string | number;
8383
index: number;
8484

@@ -91,10 +91,6 @@ export type ListScrollItem<T = Record<string, any>> = {
9191
progressRef?: Ref<ListScrollItemProgress | undefined>;
9292
getProgressQuery?: () => string | number | undefined;
9393

94-
poster?: string;
95-
posterRef?: Ref<ImageStoreMedias | undefined>;
96-
getPosterQuery?: () => ImageQuery | undefined;
97-
9894
meta?: T;
9995

10096
loading?: boolean;

src/models/poster.model.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Ref } from 'vue';
2+
import type { ImageQuery, ImageStoreMedias, ImageStoreTypes } from '~/stores/data/image.store';
3+
4+
export type PosterItem = {
5+
type?: ImageStoreTypes;
6+
7+
poster?: string;
8+
posterRef?: Ref<ImageStoreMedias | undefined>;
9+
getPosterQuery?: () => ImageQuery | undefined;
10+
};

src/stores/data/image.store.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type ImageStore = {
2424
person: Record<string, string>;
2525
};
2626

27-
type ImageStoreTypes = keyof ImageStore;
27+
export type ImageStoreTypes = keyof ImageStore;
2828

2929
export type ImageStoreMedias = ImageStoreMedia | string;
3030

0 commit comments

Comments
 (0)