Skip to content

Commit deca6c5

Browse files
committed
feat(progress): adds initial progress view
1 parent 4c92c54 commit deca6c5

14 files changed

+163
-34
lines changed

src/components/common/buttons/use-back-to-top.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ref } from 'vue';
22

3-
import type { VirtualListRef } from '~/components/common/list/ListScroll.model';
3+
import type { VirtualListRef } from '~/models/list-scroll.model';
44

55
export const useBackToTop = () => {
66
const listRef = ref<{ list: VirtualListRef }>();

src/components/common/list/ListItem.vue

+6-8
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ import {
1414
1515
import PosterPlaceholder from '~/assets/images/poster-placholder.webp';
1616
import ListItemPanel from '~/components/common/list/ListItemPanel.vue';
17-
import {
18-
type ListScrollItem,
19-
ListScrollItemType,
20-
} from '~/components/common/list/ListScroll.model';
17+
import { type ListScrollItem, ListScrollItemType } from '~/models/list-scroll.model';
2118
2219
import { useImageStore } from '~/stores/data/image.store';
2320
import { Colors } from '~/styles/colors.style';
@@ -111,7 +108,8 @@ const { getImageUrl } = useImageStore();
111108
112109
const resolvedPoster = computed(() => {
113110
if (poster?.value) return poster.value;
114-
const image = item.value.poster?.value;
111+
if (item.value.poster) return item.value.poster;
112+
const image = item.value.posterRef?.value;
115113
if (!image) return;
116114
if (typeof image === 'string') return image;
117115
if (episode.value && 'backdrop' in image) return image.backdrop;
@@ -130,15 +128,15 @@ const onLoad = () => {
130128
131129
const getPosters = (_item: ListScrollItem) => {
132130
imgLoaded.value = false;
133-
if (_item.poster?.value) return;
134-
if (!_item.poster) return;
131+
if (_item.posterRef?.value) return;
132+
if (!_item.posterRef) return;
135133
const query = _item.getPosterQuery?.();
136134
if (!query) return;
137135
if (!episode.value && _item.type === 'episode') {
138136
query.type = 'show';
139137
delete query.episode;
140138
}
141-
getImageUrl(query, 300, _item.poster);
139+
getImageUrl(query, 300, _item.posterRef);
142140
};
143141
144142
watch(item, getPosters, { immediate: true, flush: 'post' });

src/components/common/list/ListItemPanel.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { NEllipsis, NFlex, NSkeleton, NTag } from 'naive-ui';
44
import { computed, defineProps, type PropType, toRefs } from 'vue';
55
66
import PosterPlaceholder from '~/assets/images/poster-placholder.webp';
7-
import { type ListScrollItem } from '~/components/common/list/ListScroll.model';
7+
import { type ListScrollItem } from '~/models/list-scroll.model';
88
99
import { useI18n } from '~/utils';
1010

src/components/common/list/ListScroll.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
ListScrollItemType,
1616
type VirtualListProps,
1717
type VirtualListRef,
18-
} from '~/components/common/list/ListScroll.model';
18+
} from '~/models/list-scroll.model';
1919
2020
const listRef = ref<VirtualListRef>();
2121

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

+4-10
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,11 @@ import type { TraktClientPagination } from '~/models/trakt/trakt-client.model';
66

77
import type { ImageQuery } from '~/stores/data/image.store';
88

9-
import {
10-
type ListScrollItem,
11-
ListScrollItemType,
12-
type ListScrollSourceItem,
13-
type OnScroll,
14-
type OnUpdated,
15-
} from '~/components/common/list/ListScroll.model';
9+
import { type ListScrollItem, ListScrollItemType, type ListScrollSourceItem, type OnScroll, type OnUpdated } from '~/models/list-scroll.model';
1610

1711
export type ListScrollSourceItemWithDate<T extends string> = ListScrollSourceItem & Partial<Record<T, string | number | Date>>;
1812

19-
export const getTitle = (media: ListScrollSourceItem): ListScrollItem['title'] => {
13+
export const getTitle = (media: Pick<ListScrollSourceItem, 'person' | 'movie' | 'episode' | 'show'>): ListScrollItem['title'] => {
2014
if (!media) return;
2115
if (media.person) return media.person.name;
2216
if (media.movie) return media.movie.title;
@@ -25,7 +19,7 @@ export const getTitle = (media: ListScrollSourceItem): ListScrollItem['title'] =
2519
return `${media.episode.season}x${number} - ${media.episode.title}`;
2620
};
2721

28-
export const getContent = (media: ListScrollSourceItem): ListScrollItem['content'] => {
22+
export const getContent = (media: Pick<ListScrollSourceItem, 'movie' | 'episode' | 'show'>): ListScrollItem['content'] => {
2923
if (!media) return;
3024
if (media.movie) return media.movie.year?.toString();
3125
if (!media.episode) return media.show?.year?.toString();
@@ -98,7 +92,7 @@ export const useListScroll = <T extends ListScrollSourceItemWithDate<D>, D exten
9892
if (!_item.type) _item.type = getType(item);
9993
if (!_item.title) _item.title = getTitle(item);
10094
if (!_item.content) _item.content = getContent(item);
101-
if (!_item.poster) _item.poster = ref<string | undefined>(undefined);
95+
if (!_item.posterRef) _item.posterRef = ref<string | undefined>(undefined);
10296
if (!_item.getPosterQuery) _item.getPosterQuery = getPosterQuery(item, _item.type);
10397
_item.date = getDate(item, array, index, dateFn);
10498
_item.meta = {

src/components/views/calendar/CalendarComponent.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { computed, ref, watch } from 'vue';
44
import type {
55
VirtualListRef,
66
VirtualListScrollToOptions,
7-
} from '~/components/common/list/ListScroll.model';
7+
} from '~/models/list-scroll.model';
88
99
import FloatingButton from '~/components/common/buttons/FloatingButton.vue';
1010
import ListScroll from '~/components/common/list/ListScroll.vue';
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,59 @@
11
<script lang="ts" setup>
2-
// TODO
2+
import { onMounted, ref } from 'vue';
3+
4+
import type { ListScrollItem } from '~/models/list-scroll.model';
5+
6+
import FloatingButton from '~/components/common/buttons/FloatingButton.vue';
7+
import { useBackToTop } from '~/components/common/buttons/use-back-to-top';
8+
import ListScroll from '~/components/common/list/ListScroll.vue';
9+
import { progressToListItem } from '~/models/progress.model';
10+
import { TraktService } from '~/services/trakt.service';
11+
import { useI18n } from '~/utils';
12+
13+
const i18n = useI18n('progress');
14+
15+
const loading = ref(true);
16+
17+
const fetchProgress = async (): Promise<ListScrollItem[]> => {
18+
loading.value = true;
19+
try {
20+
const items = await TraktService.progress();
21+
return items.map((item, index) => ({ ...progressToListItem(item), index }));
22+
} catch (error) {
23+
console.error(error);
24+
throw error;
25+
} finally {
26+
loading.value = false;
27+
}
28+
};
29+
30+
const list = ref<ListScrollItem[]>([]);
31+
32+
onMounted(async () => {
33+
list.value = await fetchProgress();
34+
console.info('list', list.value);
35+
});
36+
37+
const { scrolled, listRef, onClick } = useBackToTop();
338
</script>
439

540
<template>
6-
<span>This is a progress component</span>
41+
<div class="container">
42+
<ListScroll ref="listRef" :loading="loading" :items="list" episode hide-date>
43+
<template #default>
44+
<!-- TODO buttons here-->
45+
</template>
46+
</ListScroll>
47+
48+
<FloatingButton :show="scrolled" @on-click="onClick">
49+
{{ i18n('back_to_top', 'common', 'button') }}
50+
</FloatingButton>
51+
</div>
752
</template>
853

954
<style lang="scss" scoped>
10-
// TODO
55+
.container {
56+
width: 100%;
57+
height: 100%;
58+
}
1159
</style>

src/components/views/search/SearchComponent.vue

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script lang="ts" setup>
2+
import { onActivated } from 'vue';
3+
24
import FloatingButton from '~/components/common/buttons/FloatingButton.vue';
35
import { useBackToTop } from '~/components/common/buttons/use-back-to-top';
46
import ListScroll from '~/components/common/list/ListScroll.vue';
@@ -13,16 +15,14 @@ import {
1315
useSearchStoreRefs,
1416
} from '~/stores/data/search.store';
1517
import { useI18n } from '~/utils';
16-
import { watchUserChange } from '~/utils/store.utils';
1718
1819
const i18n = useI18n('search');
1920
2021
const { searchResults, loading, pagination } = useSearchStoreRefs();
21-
const { fetchSearchResults, clearState } = useSearchStore();
22+
const { fetchSearchResults } = useSearchStore();
2223
23-
watchUserChange({
24-
fetch: fetchSearchResults,
25-
clear: clearState,
24+
onActivated(async () => {
25+
await fetchSearchResults();
2626
});
2727
2828
const list = useListScroll<SearchResult>(searchResults);

src/components/views/search/SearchNavbar.vue

+8-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
type SelectOption,
1010
} from 'naive-ui';
1111
12-
import { type Component, computed, defineProps, h, ref } from 'vue';
12+
import { type Component, computed, defineProps, h, onActivated, ref } from 'vue';
1313
1414
import type { TraktSearchType } from '~/models/trakt/trakt-search.model';
1515
@@ -127,6 +127,12 @@ const hideTooltip = () => {
127127
tooltipHover.value = false;
128128
clearTimeout(timeout.value);
129129
};
130+
131+
const inputRef = ref();
132+
133+
onActivated(() => {
134+
inputRef.value?.focus();
135+
});
130136
</script>
131137

132138
<template>
@@ -169,6 +175,7 @@ const hideTooltip = () => {
169175
</NFlex>
170176
<template #trigger>
171177
<NInput
178+
ref="inputRef"
172179
v-model:value="debouncedSearch"
173180
class="search-input"
174181
:loading="loading"

src/components/common/list/ListScroll.model.ts src/models/list-scroll.model.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ export type ListScrollItem = {
6363
content?: string;
6464
tags?: ListScrollItemTag[];
6565

66-
poster?: Ref<ImageStoreMedias | undefined>;
66+
poster?: ImageStoreMedias;
67+
posterRef?: Ref<ImageStoreMedias | undefined>;
6768
getPosterQuery?: () => ImageQuery | undefined;
6869

6970
meta?: Record<string, unknown>;

src/models/progress.model.ts

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { ListScrollItem, ListScrollSourceItem } from '~/models/list-scroll.model';
2+
import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
3+
import type { TraktShow } from '~/models/trakt/trakt-show.model';
4+
5+
import { getContent, getTitle } from '~/components/common/list/use-list-scroll';
6+
7+
export type ProgressItem = {
8+
episodeId: string;
9+
episodeNumber: string;
10+
episodeTitle: string;
11+
fanart: string;
12+
firstAired: string;
13+
fullTitle: string;
14+
logo: string;
15+
runtime: string;
16+
safe: string;
17+
screenshot: string;
18+
seasonId: string;
19+
seasonNumber: string;
20+
showId: string;
21+
topTitle: string;
22+
totalRuntime: string;
23+
type: 'show' | 'season' | 'episode';
24+
url: string;
25+
};
26+
27+
const titleRegex = /(.*)\s\d+x\d+\s"([^"]+)"/;
28+
export const progressToListItem = (progress: ProgressItem): Omit<ListScrollItem, 'index'> => {
29+
const match = titleRegex.exec(progress.fullTitle);
30+
31+
const episode: ListScrollSourceItem['episode'] = {
32+
ids: {
33+
trakt: Number(progress.episodeId),
34+
} as TraktEpisode['ids'],
35+
title: match ? match[2] : progress.fullTitle,
36+
season: Number(progress.seasonNumber),
37+
number: Number(progress.episodeNumber),
38+
};
39+
40+
const show: ListScrollSourceItem['show'] = {
41+
ids: {
42+
trakt: Number(progress.showId),
43+
} as TraktShow['ids'],
44+
title: match ? match[1] : progress.fullTitle,
45+
} as ListScrollSourceItem['show'];
46+
47+
const poster = progress.fanart ?? progress.screenshot;
48+
49+
return {
50+
id: Number(progress.episodeId ?? progress.seasonId ?? progress.showId),
51+
title: getTitle({ show, episode }),
52+
content: getContent({ show, episode }),
53+
poster,
54+
date: {
55+
current: new Date(progress.firstAired),
56+
},
57+
type: progress.type,
58+
meta: {
59+
ids: {
60+
show: progress.showId ? Number(progress.showId) : undefined,
61+
season: progress.seasonId ? Number(progress.seasonId) : undefined,
62+
episode: progress.episodeId ? Number(progress.episodeId) : undefined,
63+
},
64+
show,
65+
episode,
66+
source: progress,
67+
},
68+
};
69+
};

src/services/trakt.service.ts

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ProgressItem } from '~/models/progress.model';
12
import type { TmdbApiResponse } from '~/models/tmdb/tmdb-client.model';
23
import type { TraktAuthenticationApprove } from '~/models/trakt/trakt-authentication.model';
34
import type { TraktCalendarQuery } from '~/models/trakt/trakt-calendar.model';
@@ -264,4 +265,15 @@ export class TraktService {
264265
const response = await this.traktClient.search.text.cached(query, undefined, { retention: CacheRetention.Day });
265266
return { data: await response.json(), pagination: response.pagination };
266267
}
268+
269+
static async progress() {
270+
const response = await fetch('https://trakt.tv/dashboard/on_deck', {
271+
credentials: 'include',
272+
});
273+
274+
const htmlString = await response.text();
275+
const htmlDoc = new DOMParser().parseFromString(htmlString, 'text/html');
276+
277+
return Array.from(htmlDoc.querySelectorAll<HTMLAnchorElement>('a[class="watch"]')).map(a => ({ ...a.dataset }) as unknown as ProgressItem);
278+
}
267279
}

src/stores/data/calendar.store.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { computed, ref } from 'vue';
33

44
import type { TraktCalendarMovie, TraktCalendarShow } from '~/models/trakt/trakt-calendar.model';
55

6-
import { type ListScrollItem, type ListScrollItemTag, ListScrollItemType } from '~/components/common/list/ListScroll.model';
6+
import { type ListScrollItem, type ListScrollItemTag, ListScrollItemType } from '~/models/list-scroll.model';
77

88
import { TraktService } from '~/services/trakt.service';
99
import { storage } from '~/utils/browser/browser-storage.utils';

src/stores/data/search.store.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { defineStore, storeToRefs } from 'pinia';
22

33
import { ref, watch } from 'vue';
44

5-
import type { ListScrollItem } from '~/components/common/list/ListScroll.model';
5+
import type { ListScrollItem } from '~/models/list-scroll.model';
66
import type { TraktClientPagination } from '~/models/trakt/trakt-client.model';
77
import type { TraktSearchResult, TraktSearchType } from '~/models/trakt/trakt-search.model';
88

0 commit comments

Comments
 (0)