Skip to content

Commit 1b93f2b

Browse files
committed
feat(progress): adds progress bar
1 parent 7de4968 commit 1b93f2b

14 files changed

+258
-40
lines changed

src/components/common/list/ListItem.vue

+11-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ const props = defineProps({
6767
type: Boolean,
6868
required: false,
6969
},
70+
showProgress: {
71+
type: Boolean,
72+
required: false,
73+
default: false,
74+
},
7075
});
7176
7277
const emit = defineEmits<{
@@ -261,7 +266,12 @@ const itemHeight = computed(() => (height?.value ? `${height.value}px` : undefin
261266
:src="PosterPlaceholder"
262267
:fallback-src="PosterPlaceholder"
263268
/>
264-
<ListItemPanel :item="item" :loading="loading" :hide-date="hideDate">
269+
<ListItemPanel
270+
:item="item"
271+
:loading="loading"
272+
:hide-date="hideDate"
273+
:show-progress="showProgress"
274+
>
265275
<slot :item="item" :loading="loading" />
266276
</ListItemPanel>
267277
</NFlex>

src/components/common/list/ListItemPanel.vue

+40-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
<script setup lang="ts">
2-
import { NEllipsis, NFlex, NSkeleton, NTag } from 'naive-ui';
2+
import { NEllipsis, NFlex, NProgress, NSkeleton, NTag } from 'naive-ui';
33
44
import { computed, defineProps, type PropType, toRefs } from 'vue';
55
66
import PosterPlaceholder from '~/assets/images/poster-placholder.webp';
77
import { type ListScrollItem } from '~/models/list-scroll.model';
88
9+
import { useShowStore } from '~/stores/data/show.store';
910
import { useI18n } from '~/utils';
11+
import { deCapitalise } from '~/utils/string.utils';
1012
1113
const i18n = useI18n('list-item-panel');
1214
@@ -28,6 +30,11 @@ const props = defineProps({
2830
type: Boolean,
2931
required: false,
3032
},
33+
showProgress: {
34+
type: Boolean,
35+
required: false,
36+
default: false,
37+
},
3138
});
3239
3340
const { item, hideDate } = toRefs(props);
@@ -36,9 +43,9 @@ const type = computed(() =>
3643
item.value.type ? i18n(item.value.type, 'common', 'media', 'type') : item.value.type,
3744
);
3845
39-
const title = computed(() => item.value.title);
46+
const title = computed(() => deCapitalise(item.value.title));
4047
41-
const content = computed(() => item.value.content);
48+
const content = computed(() => deCapitalise(item.value.content));
4249
4350
const date = computed(() => {
4451
if (hideDate.value) return;
@@ -56,6 +63,17 @@ const tags = computed(
5663
return { ...tag, label: i18n(tag.label, ...tag.i18n) };
5764
}),
5865
);
66+
67+
const { getShowProgress } = useShowStore();
68+
69+
const progress = computed(() => {
70+
if (item?.value?.progress) return item.value.progress;
71+
if (item?.value?.progressRef) return item.value.progressRef.value;
72+
if (!item?.value?.getProgressQuery) return;
73+
const id = item.value.getProgressQuery();
74+
if (!id) return;
75+
return getShowProgress(id).value;
76+
});
5977
</script>
6078

6179
<template>
@@ -99,6 +117,19 @@ const tags = computed(
99117
</NTag>
100118
</template>
101119
</NFlex>
120+
<NProgress
121+
v-if="showProgress"
122+
:data-percentage="progress?.percentage"
123+
:data-last="progress?.last_episode"
124+
:data-next="progress?.next_episode"
125+
class="progress"
126+
:theme-overrides="{
127+
railHeight: 'var(--rail-height)',
128+
}"
129+
:percentage="progress?.percentage ?? 0"
130+
:show-indicator="false"
131+
color="var(--trakt-red-dark)"
132+
/>
102133
</NFlex>
103134
</template>
104135

@@ -136,5 +167,11 @@ const tags = computed(
136167
width: fit-content;
137168
}
138169
}
170+
171+
.progress {
172+
margin-top: 0.75rem;
173+
174+
--rail-height: 4px;
175+
}
139176
}
140177
</style>

src/components/common/list/ListScroll.vue

+6
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ const props = defineProps({
5757
required: false,
5858
default: 300,
5959
},
60+
showProgress: {
61+
type: Boolean,
62+
required: false,
63+
default: false,
64+
},
6065
});
6166
6267
const emits = defineEmits<{
@@ -171,6 +176,7 @@ const onLoadMore = (payload: { page: number; pageCount: number; pageSize: number
171176
:episode="episode"
172177
:hover="hoverDate === item.date?.current?.toDateString()"
173178
:scroll-into-view="scrollIntoView?.includes(item.id)"
179+
:show-progress="showProgress"
174180
@on-hover="onHover"
175181
@on-scroll-into-view="(...args) => $emit('onScrollIntoView', ...args)"
176182
@on-scroll-out-of-view="(...args) => $emit('onScrollOutOfView', ...args)"

src/components/views/progress/ProgressComponent.vue

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const { scrolled, listRef, onClick } = useBackToTop();
4444
:items="progress"
4545
episode
4646
hide-date
47+
show-progress
4748
@on-scroll="scrolled = true"
4849
@on-scroll-top="scrolled = false"
4950
>

src/models/list-scroll.model.ts

+25
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
55
import type { TraktList } from '~/models/trakt/trakt-list.model';
66
import type { TraktMovie } from '~/models/trakt/trakt-movie.model';
77
import type { TraktPerson } from '~/models/trakt/trakt-people.model';
8+
import type { BaseTraktProgress, BaseTraktProgressEpisode, BaseTraktProgressSeason } from '~/models/trakt/trakt-progress.model';
89
import type { TraktSeason } from '~/models/trakt/trakt-season.model';
910
import type { TraktShow } from '~/models/trakt/trakt-show.model';
1011
import type { ImageQuery, ImageStoreMedias } from '~/stores/data/image.store';
@@ -42,6 +43,26 @@ export type ListScrollItemTag = {
4243
meta?: string;
4344
};
4445

46+
export type ListScrollItemProgressEpisode = BaseTraktProgressEpisode & {
47+
date: Date;
48+
};
49+
50+
export type ListScrollItemProgressSeason = BaseTraktProgressSeason & {
51+
episodes: ListScrollItemProgressEpisode[];
52+
};
53+
54+
export const ListScrollItemProgressType = {
55+
collection: 'collection',
56+
watched: 'watched',
57+
} as const;
58+
59+
export type ListScrollItemProgress = BaseTraktProgress & {
60+
type: (typeof ListScrollItemProgressType)[keyof typeof ListScrollItemProgressType];
61+
date: Date;
62+
seasons: ListScrollItemProgressSeason[];
63+
percentage: number;
64+
};
65+
4566
export const ListScrollItemType = {
4667
movie: 'movie',
4768
show: 'show',
@@ -64,6 +85,10 @@ export type ListScrollItem<T = Record<string, any>> = {
6485
content?: string;
6586
tags?: ListScrollItemTag[];
6687

88+
progress?: ListScrollItemProgress;
89+
progressRef?: Ref<ListScrollItemProgress | undefined>;
90+
getProgressQuery?: () => string | number | undefined;
91+
6792
poster?: string;
6893
posterRef?: Ref<ImageStoreMedias | undefined>;
6994
getPosterQuery?: () => ImageQuery | undefined;

src/models/trakt/trakt-progress.model.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
22
import type { TraktSeason } from '~/models/trakt/trakt-season.model';
33

4-
type BaseTraktProgressEpisode = {
4+
export type BaseTraktProgressEpisode = {
55
number: number;
66
completed: boolean;
77
};
@@ -11,7 +11,7 @@ export type TraktCollectionProgressEpisode = BaseTraktProgressEpisode & {
1111
collected_at: string;
1212
};
1313

14-
type BaseTraktProgressSeason = {
14+
export type BaseTraktProgressSeason = {
1515
number: number;
1616
title: string;
1717
aired: number;
@@ -22,7 +22,7 @@ export type TraktCollectionProgressSeason = BaseTraktProgressSeason & {
2222
episodes: TraktCollectionProgressEpisode[];
2323
};
2424

25-
type BaseTraktProgress = {
25+
export type BaseTraktProgress = {
2626
aired: number;
2727
completed: number;
2828
hidden_seasons: TraktSeason[];
@@ -38,7 +38,7 @@ export type TraktCollectionProgress = BaseTraktProgress & {
3838

3939
export type TraktWatchedProgressEpisode = BaseTraktProgressEpisode & {
4040
/** Timestamp in ISO 8601 GMT format (YYYY-MM-DD'T'hh:mm:ss.sssZ) */
41-
collected_at: string;
41+
last_watched_at: string;
4242
};
4343

4444
export type TraktWatchedProgressSeason = BaseTraktProgressSeason & {

src/services/trakt-client/api/endpoints/shows.endpoint.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -509,12 +509,12 @@ export const shows = {
509509
*
510510
* @auth required
511511
*
512-
* @see [get-show-watched-progress]{@link https://trakt.docs.apiary.io/#reference/shows/collection-progress/get-show-watched-progress}
512+
* @see [get-show-watched-progress]{@link https://trakt.docs.apiary.io/#reference/shows/watched-progress/get-show-watched-progress}
513513
*/
514514
watched: new TraktClientEndpoint<
515515
{
516516
/** Trakt ID, Trakt slug, or IMDB ID */
517-
id: string;
517+
id: number | string;
518518
/** include any hidden seasons */
519519
hidden?: boolean;
520520
/** include specials as season 0 */

src/services/trakt.service.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,16 @@ export class TraktService {
299299
},
300300
);
301301

302-
static async progress() {
303-
const response = await this.cachedProgress();
304-
if (!response.ok) throw response;
305-
return response.json();
306-
}
302+
static progress = {
303+
async onDeck() {
304+
const response = await TraktService.cachedProgress();
305+
if (!response.ok) throw response;
306+
return response.json();
307+
},
308+
309+
async show(showId: string | number) {
310+
const response = await TraktService.traktClient.shows.progress.watched.cached({ id: showId });
311+
return response.json();
312+
},
313+
};
307314
}

src/stores/data/image.store.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const useImageStore = defineStore('data.image', () => {
5454
const tmdbConfig = ref<TmdbConfiguration>();
5555
const images = reactive<ImageStore>(EmptyImageStore);
5656

57-
const syncSaveImageStore = debounce(
57+
const saveState = debounce(
5858
(_images = images) =>
5959
Promise.all([
6060
storage.local.set(`data.image-store.movie`, _images.movie),
@@ -66,7 +66,7 @@ export const useImageStore = defineStore('data.image', () => {
6666
1000,
6767
);
6868

69-
const syncRestoreImageStore = async (seed?: Partial<ImageStore>) => {
69+
const restoreState = async (seed?: Partial<ImageStore>) => {
7070
const [movie, show, season, episode, person] = await Promise.all([
7171
storage.local.get<Record<string, string>>(`data.image-store.movie`),
7272
storage.local.get<Record<string, string>>(`data.image-store.show`),
@@ -87,7 +87,7 @@ export const useImageStore = defineStore('data.image', () => {
8787
const initImageStore = async (config?: TmdbConfiguration) => {
8888
if (!config) config = await TraktService.tmdbConfiguration();
8989
tmdbConfig.value = config;
90-
return syncRestoreImageStore();
90+
return restoreState();
9191
};
9292

9393
const imageSizes = computed(() => ({
@@ -145,7 +145,7 @@ export const useImageStore = defineStore('data.image', () => {
145145
if (!image.poster && !image.backdrop) return;
146146
images[type][key] = image;
147147

148-
syncSaveImageStore().catch(err => console.error('Failed to save image store', err));
148+
saveState().catch(err => console.error('Failed to save image store', err));
149149
return { image, key, type };
150150
}
151151

@@ -160,7 +160,7 @@ export const useImageStore = defineStore('data.image', () => {
160160
const image = arrayMax(fetchedImages, 'vote_average', i => !!i.file_path)?.file_path;
161161
if (!image) return;
162162
images[type][key] = image;
163-
syncSaveImageStore().catch(err => console.error('Failed to save image store', err));
163+
saveState().catch(err => console.error('Failed to save image store', err));
164164
return { image, key, type };
165165
};
166166

src/stores/data/progress.store.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { defineStore, storeToRefs } from 'pinia';
22
import { ref } from 'vue';
33

4-
import type { ListScrollItem, ListScrollSourceItem } from '~/models/list-scroll.model';
5-
64
import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
75
import type { TraktShow } from '~/models/trakt/trakt-show.model';
86

97
import { getContent, getTags, getTitle } from '~/components/common/list/use-list-scroll';
8+
import { type ListScrollItem, type ListScrollSourceItem } from '~/models/list-scroll.model';
109
import { type ProgressItem } from '~/models/progress.model';
1110
import { TraktService } from '~/services/trakt.service';
1211
import { debounceLoading, useLoadingPlaceholder } from '~/utils/store.utils';
1312

14-
type ProgressListItem = Omit<ListScrollItem, 'posterRef'>;
13+
type ProgressListItem = Omit<ListScrollItem, 'posterRef' | 'progressRef'>;
1514

1615
const titleRegex = /(.*)\s\d+x\d+\s"([^"]+)"/;
16+
1717
export const progressToListItem = (progress: ProgressItem, index: number): ProgressListItem => {
1818
const match = titleRegex.exec(progress.fullTitle);
1919

@@ -41,6 +41,7 @@ export const progressToListItem = (progress: ProgressItem, index: number): Progr
4141
title: getTitle({ show, episode }),
4242
content: getContent({ show, episode }),
4343
poster,
44+
getProgressQuery: () => show?.ids?.trakt,
4445
date: {
4546
current: new Date(progress.firstAired),
4647
},
@@ -78,22 +79,23 @@ export const useProgressStore = defineStore('data.progress', () => {
7879
}
7980
if (firstLoad.value) firstLoad.value = false;
8081

81-
console.info('Fetching progress', progress);
82+
console.info('Fetching progress');
8283
loading.value = true;
8384
const timeout = debounceLoading(progress, loadingPlaceholder, true);
8485
try {
85-
const items = await TraktService.progress();
86+
const items = await TraktService.progress.onDeck();
8687
progress.value = items.map(progressToListItem);
8788
} catch (error) {
89+
progress.value = [];
90+
loading.value = false;
91+
8892
if (error instanceof Response && error.status === 401) {
8993
console.warn('User is not logged in', error);
9094
loggedOut.value = true;
91-
progress.value = [];
92-
loading.value = false;
9395
return;
9496
}
97+
9598
console.error(error);
96-
progress.value = [];
9799
throw error;
98100
} finally {
99101
clearTimeout(timeout);

0 commit comments

Comments
 (0)