Skip to content

Commit 5fab4b1

Browse files
committed
feat(progress): adds collection & watched badge, adds eager data fetch
1 parent 55ab5f1 commit 5fab4b1

20 files changed

+351
-44
lines changed

src/components/common/list/ListItem.vue

+12
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ const props = defineProps({
8080
required: false,
8181
default: false,
8282
},
83+
showPlayed: {
84+
type: Boolean,
85+
required: false,
86+
default: false,
87+
},
88+
showCollected: {
89+
type: Boolean,
90+
required: false,
91+
default: false,
92+
},
8393
});
8494
8595
const emit = defineEmits<{
@@ -216,6 +226,8 @@ const onClick = () => emit('onItemClick', { item: item?.value });
216226
:hide-time="hideTime"
217227
:content-height="contentHeight"
218228
:show-progress="showProgress"
229+
:show-played="showPlayed"
230+
:show-collected="showCollected"
219231
>
220232
<slot :item="item" :loading="loading" />
221233
</ListItemPanel>

src/components/common/list/ListItemPanel.vue

+115-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { deCapitalise } from '@dvcol/common-utils/common/string';
44
import {
55
NEllipsis,
66
NFlex,
7+
NIcon,
78
NProgress,
89
NSkeleton,
910
NTag,
@@ -15,9 +16,12 @@ import { computed, defineProps, type PropType, ref, toRefs } from 'vue';
1516
import PosterPlaceholder from '~/assets/images/poster-placholder.webp';
1617
import TagLink from '~/components/common/buttons/TagLink.vue';
1718
import ProgressTooltip from '~/components/common/tooltip/ProgressTooltip.vue';
19+
import IconGrid from '~/components/icons/IconGrid.vue';
20+
import IconPlayFilled from '~/components/icons/IconPlayFilled.vue';
1821
import { type ListScrollItem, type ShowProgress } from '~/models/list-scroll.model';
1922
2023
import { ProgressType } from '~/models/progress-type.model';
24+
import { useMovieStore } from '~/stores/data/movie.store';
2125
import { useShowStore } from '~/stores/data/show.store';
2226
import { useExtensionSettingsStoreRefs } from '~/stores/settings/extension.store';
2327
import { useLinksStore } from '~/stores/settings/links.store';
@@ -57,9 +61,19 @@ const props = defineProps({
5761
required: false,
5862
default: false,
5963
},
64+
showPlayed: {
65+
type: Boolean,
66+
required: false,
67+
default: false,
68+
},
69+
showCollected: {
70+
type: Boolean,
71+
required: false,
72+
default: false,
73+
},
6074
});
6175
62-
const { item, hideDate } = toRefs(props);
76+
const { item, hideDate, showProgress, showPlayed, showCollected } = toRefs(props);
6377
6478
const type = computed(() =>
6579
item.value.type ? i18n(item.value.type, 'common', 'media', 'type') : item.value.type,
@@ -84,15 +98,61 @@ const tags = computed(
8498
}),
8599
);
86100
87-
const { getShowWatchedProgress } = useShowStore();
101+
const { getShowWatchedProgress, getShowCollectionProgress } = useShowStore();
88102
89103
const progress = computed<ShowProgress | undefined>(() => {
104+
if (!showProgress.value && !showPlayed.value && !showCollected.value) return;
90105
if (item?.value?.progress) return item.value?.progress;
91106
if (item?.value?.progressRef) return item.value?.progressRef.value;
92107
if (!item?.value?.getProgressQuery) return;
93-
const { id, cacheOptions } = item.value?.getProgressQuery() ?? {};
108+
const { id, cacheOptions, noFetch } = item.value?.getProgressQuery() ?? {};
94109
if (!id) return;
95-
return getShowWatchedProgress(id, cacheOptions).value;
110+
return getShowWatchedProgress(id, cacheOptions, noFetch).value;
111+
});
112+
113+
const collection = computed<ShowProgress | undefined>(() => {
114+
if (!showCollected.value) return;
115+
if (!item?.value?.getProgressQuery) return;
116+
const { id, noFetch } = item.value?.getProgressQuery() ?? {};
117+
if (!id) return;
118+
return getShowCollectionProgress(id, noFetch).value;
119+
});
120+
121+
const { getMovieWatched, getMovieCollected } = useMovieStore();
122+
123+
const played = computed(() => {
124+
if (!showPlayed.value) return false;
125+
const _item = item?.value;
126+
if (_item?.type === 'movie' && _item?.meta?.ids?.movie?.trakt) {
127+
return getMovieWatched(_item.meta.ids.movie.trakt)?.value;
128+
}
129+
if (_item?.type !== 'episode') return false;
130+
const _progress = progress.value;
131+
if (!_progress) return false;
132+
const _season = _item.meta?.number?.season;
133+
const _episode = _item.meta?.number?.episode;
134+
if (!_season || !_episode) return false;
135+
return _progress.seasons
136+
?.find(s => s.number === _season)
137+
?.episodes?.find(e => e.number === _episode)?.completed;
138+
});
139+
140+
const collected = computed(() => {
141+
if (!showCollected.value) return false;
142+
const _item = item?.value;
143+
if (_item?.type === 'movie' && _item?.meta?.ids?.movie?.trakt) {
144+
return getMovieCollected(_item.meta.ids.movie.trakt)?.value;
145+
}
146+
if (_item?.type !== 'episode') return false;
147+
const _collection = collection.value;
148+
149+
if (!_collection) return false;
150+
const _season = _item.meta?.number?.season;
151+
const _episode = _item.meta?.number?.episode;
152+
if (!_season || !_episode) return false;
153+
return _collection.seasons
154+
?.find(s => s.number === _season)
155+
?.episodes?.find(e => e.number === _episode)?.completed;
96156
});
97157
98158
const { progressType } = useExtensionSettingsStoreRefs();
@@ -144,17 +204,60 @@ const onTagClick = (url?: string) => {
144204
content
145205
}}</NEllipsis>
146206
</div>
147-
<NFlex v-if="(!hideTime && date) || tags?.length" size="medium" class="tags">
207+
<NFlex
208+
v-if="(!hideTime && date) || tags?.length || showCollected || showPlayed"
209+
size="medium"
210+
class="tags"
211+
>
148212
<template v-for="(tag, i) of tags" :key="`${i}-${tag.label}`">
149213
<NSkeleton v-if="loading" text style="width: 6%" />
150214
<TagLink :tag="tag" @on-click="onTagClick" />
151215
</template>
152216
<template v-if="!hideTime && date">
153-
<NSkeleton v-if="loading" text style="width: 6%" />
154-
<NTag v-else class="tag" size="small" type="default" :bordered="false">
217+
<NSkeleton v-if="loading" key="date-loader" text style="width: 6%" />
218+
<NTag
219+
v-else
220+
key="date"
221+
class="tag"
222+
size="small"
223+
type="default"
224+
:bordered="false"
225+
>
155226
{{ date }}
156227
</NTag>
157228
</template>
229+
<template v-if="showCollected && collected">
230+
<NSkeleton v-if="loading" key="collected-loader" text style="width: 3%" />
231+
<NTag
232+
v-else
233+
key="collected"
234+
class="tag badge"
235+
size="small"
236+
type="info"
237+
:bordered="false"
238+
:title="i18n('collected', 'common', 'tooltip')"
239+
>
240+
<template #icon>
241+
<NIcon :component="IconGrid" />
242+
</template>
243+
</NTag>
244+
</template>
245+
<template v-if="showPlayed && played">
246+
<NSkeleton v-if="loading" key="played-loader" text style="width: 3%" />
247+
<NTag
248+
v-else
249+
key="played"
250+
class="tag badge"
251+
size="small"
252+
type="primary"
253+
:bordered="false"
254+
:title="i18n('watched', 'common', 'tooltip')"
255+
>
256+
<template #icon>
257+
<NIcon :component="IconPlayFilled" />
258+
</template>
259+
</NTag>
260+
</template>
158261
</NFlex>
159262
<div v-if="showProgress && !loading" class="panel-progress">
160263
<ProgressTooltip :progress="progress" :to="innerContainer" placement="top-end">
@@ -222,4 +325,9 @@ const onTagClick = (url?: string) => {
222325
.panel-progress-tooltip.n-tooltip.n-tooltip {
223326
background: var(--bg-color-80);
224327
}
328+
329+
// stylelint-disable-next-line selector-class-pattern -- framework overriding
330+
.n-tag.n-tag--icon.tag.badge .n-tag__icon {
331+
margin: 0;
332+
}
225333
</style>

src/components/common/list/ListScroll.vue

+12
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ const props = defineProps({
7070
required: false,
7171
default: false,
7272
},
73+
showPlayed: {
74+
type: Boolean,
75+
required: false,
76+
default: false,
77+
},
78+
showCollected: {
79+
type: Boolean,
80+
required: false,
81+
default: false,
82+
},
7383
});
7484
7585
const emits = defineEmits<{
@@ -188,6 +198,8 @@ const onLoadMore = (payload: { page: number; pageCount: number; pageSize: number
188198
:hover="hoverDate === item.date?.current?.toDateString()"
189199
:scroll-into-view="scrollIntoView?.includes(item.id)"
190200
:show-progress="showProgress"
201+
:show-played="showPlayed"
202+
:show-collected="showCollected"
191203
@on-hover="onHover"
192204
@on-item-click="(...args) => $emit('onItemClick', ...args)"
193205
@on-scroll-into-view="(...args) => $emit('onScrollIntoView', ...args)"

src/components/common/list/use-list-scroll.ts

+62-16
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import { computed, ref } from 'vue';
44

55
import type { Ref } from 'vue';
66

7+
import type { ListScrollItem, ListScrollItemMeta, ListScrollItemTag, ListScrollSourceItem, OnScroll, OnUpdated } from '~/models/list-scroll.model';
8+
79
import type { ImageQuery } from '~/stores/data/image.store';
810

9-
import { type ListScrollItem, ListScrollItemType, type ListScrollSourceItem, type OnScroll, type OnUpdated } from '~/models/list-scroll.model';
11+
import { ListScrollItemType } from '~/models/list-scroll.model';
12+
1013
import { ResolveExternalLinks } from '~/settings/external.links';
1114
import { useI18n } from '~/utils/i18n.utils';
1215

@@ -80,13 +83,66 @@ export const getPosterQuery =
8083
} satisfies ImageQuery;
8184
};
8285

86+
export const getProgressQuery = (item: ListScrollSourceItem): ListScrollItem['getProgressQuery'] => {
87+
const showId = item.show?.ids.trakt;
88+
if (!showId) return;
89+
return () => ({
90+
id: showId,
91+
noFetch: true,
92+
});
93+
};
94+
8395
const i18n = useI18n('common');
8496

8597
const i18nEpisode = () => i18n('episode', 'common', 'tag');
8698
const i18nSeason = () => i18n('season', 'common', 'tag');
8799
const openInEpisode = () => i18n('open_episode_in_trakt', 'common', 'tooltip');
88100
const openInSeason = () => i18n('open_season_in_trakt', 'common', 'tooltip');
89101

102+
const getEpisodeTypeTag = (episode: ListScrollSourceItem['episode']): ListScrollItemTag | undefined => {
103+
if (!episode) return;
104+
const type = 'episode_type' in episode ? episode.episode_type : undefined;
105+
let premiere: TraktEpisodeTypes | null = null;
106+
let color: ListScrollItemTag['type'] = 'primary';
107+
// let finale: TraktEpisodeTypes;
108+
if (type === TraktEpisodeType.SeriesPremiere || (episode.season === 1 && episode.number === 1)) {
109+
premiere = TraktEpisodeType.SeriesPremiere;
110+
} else if (type === TraktEpisodeType.MidSeasonPremiere) {
111+
premiere = TraktEpisodeType.MidSeasonPremiere;
112+
color = 'info';
113+
} else if (type === TraktEpisodeType.SeasonPremiere || episode.number === 1) {
114+
premiere = TraktEpisodeType.SeasonPremiere;
115+
}
116+
117+
if (premiere) {
118+
return {
119+
label: premiere,
120+
i18n: ['common', 'tag'],
121+
type: color,
122+
bordered: true,
123+
};
124+
}
125+
if (!type) return;
126+
127+
let finale: TraktEpisodeTypes | null = null;
128+
if (type === TraktEpisodeType.SeriesFinale) {
129+
finale = TraktEpisodeType.SeriesFinale;
130+
} else if (type === TraktEpisodeType.MidSeasonFinale) {
131+
finale = TraktEpisodeType.MidSeasonFinale;
132+
color = 'info';
133+
} else if (type === TraktEpisodeType.SeasonFinale) {
134+
finale = TraktEpisodeType.SeasonFinale;
135+
}
136+
137+
if (!finale) return;
138+
return {
139+
label: finale,
140+
i18n: ['common', 'tag'],
141+
type: color,
142+
bordered: true,
143+
};
144+
};
145+
90146
export const getTags = (item: Pick<ListScrollSourceItem, 'episode' | 'season'>, type: ListScrollItem['type']): ListScrollItem['tags'] => {
91147
const tags: ListScrollItem['tags'] = [];
92148
if (type === 'episode' && item.episode) {
@@ -102,19 +158,8 @@ export const getTags = (item: Pick<ListScrollSourceItem, 'episode' | 'season'>,
102158
}),
103159
});
104160

105-
let premiere: TraktEpisodeTypes | null = null;
106-
// let finale: TraktEpisodeTypes;
107-
if (item.episode.season === 1 && item.episode.number === 1) premiere = TraktEpisodeType.SeriesPremiere;
108-
else if (item.episode.number === 1) premiere = TraktEpisodeType.SeasonPremiere;
109-
110-
if (premiere) {
111-
tags.push({
112-
label: premiere,
113-
i18n: ['common', 'tag'],
114-
type: premiere === 'season' ? 'info' : 'primary',
115-
bordered: true,
116-
});
117-
}
161+
const typeTag = getEpisodeTypeTag(item.episode);
162+
if (typeTag) tags.push(typeTag);
118163
} else if (type === 'season' && item.season) {
119164
tags.push({
120165
label: `${i18nSeason()} ${item.season.number.toString().padStart(2, '0')}`,
@@ -134,12 +179,12 @@ export const getTags = (item: Pick<ListScrollSourceItem, 'episode' | 'season'>,
134179
export const useListScroll = <T extends ListScrollSourceItemWithDate<D>, D extends string | never = never>(
135180
items: Ref<T[]>,
136181
dateFn?: D | ((item: T) => T[D]),
137-
) => {
182+
): Ref<ListScrollItem[]> => {
138183
return computed<ListScrollItem[]>(() => {
139184
const array = items.value;
140185
if (!array.length) return [];
141186
return array.map((item, index) => {
142-
const _item: ListScrollItem = {
187+
const _item: ListScrollItem<ListScrollItemMeta> = {
143188
...item,
144189
index,
145190
key: `${index}-${item.id}`,
@@ -151,6 +196,7 @@ export const useListScroll = <T extends ListScrollSourceItemWithDate<D>, D exten
151196
if (!_item.content) _item.content = getContent(item);
152197
if (!_item.posterRef) _item.posterRef = ref<string>();
153198
if (!_item.getPosterQuery) _item.getPosterQuery = getPosterQuery(item, _item.type);
199+
if (!_item.getProgressQuery) _item.getProgressQuery = getProgressQuery(item);
154200
if (!_item.tags) _item.tags = getTags(item, _item.type);
155201

156202
_item.date = getDate(item, array, index, dateFn);

src/components/views/calendar/CalendarComponent.vue

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ const { onItemClick } = usePanelItem();
5757
:items="list"
5858
:loading="loading"
5959
backdrop
60+
show-played
61+
show-collected
6062
:scroll-into-view="centerItem?.id ? [centerItem?.id] : []"
6163
@on-item-click="onItemClick"
6264
@on-scroll-into-view="e => onScrollIntoOutOfView(false, e.ref)"

src/components/views/history/HistoryComponent.vue

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const { onItemClick } = usePanelItem();
6767
:loading="loading"
6868
:pagination="pagination"
6969
:page-size="pageSize"
70+
show-collected
7071
@on-scroll="scrolled = true"
7172
@on-scroll-top="scrolled = false"
7273
@on-scroll-bottom="onScroll"

src/components/views/progress/ProgressComponent.vue

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const { onItemClick } = usePanelItem();
6868
backdrop
6969
hide-date
7070
show-progress
71+
show-collected
7172
@on-scroll="scrolled = true"
7273
@on-scroll-top="scrolled = false"
7374
@on-item-click="onItemClick"

src/components/views/search/SearchComponent.vue

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ const { onItemClick } = usePanelItem();
5252
hide-date
5353
:items="list"
5454
:loading="loading"
55+
show-played
56+
show-collected
5557
@on-scroll="scrolled = true"
5658
@on-scroll-top="scrolled = false"
5759
@on-scroll-bottom="onScroll"

0 commit comments

Comments
 (0)