Skip to content

Commit 1b4bb1a

Browse files
committed
feat(panel): adds person & movie basic panels
1 parent 0581017 commit 1b4bb1a

File tree

9 files changed

+289
-17
lines changed

9 files changed

+289
-17
lines changed
+71-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,84 @@
11
<script setup lang="ts">
2-
import { onActivated } from 'vue';
2+
import { NFlex, NSkeleton } from 'naive-ui';
3+
import { computed, onMounted, onUnmounted, ref, toRefs, watch } from 'vue';
4+
5+
import type { TraktMovieExtended } from '~/models/trakt/trakt-movie.model';
6+
7+
import TitleLink from '~/components/common/buttons/TitleLink.vue';
8+
import PanelPoster from '~/components/views/panel/PanelPoster.vue';
9+
import { ResolveExternalLinks } from '~/settings/external.links';
10+
import { useMovieStore } from '~/stores/data/movie.store';
11+
import { useExtensionSettingsStore } from '~/stores/settings/extension.store';
12+
import { deCapitalise } from '~/utils/string.utils';
313
414
const props = defineProps({
515
movieId: {
616
type: String,
7-
required: false,
17+
required: true,
818
},
919
});
1020
11-
onActivated(() => {
12-
console.info('MovieDrawer activated', props.movieId);
21+
const { movieId } = toRefs(props);
22+
23+
const movie = ref<TraktMovieExtended>();
24+
25+
const { getMovieRef } = useMovieStore();
26+
27+
const unsub = ref<() => void>();
28+
29+
onMounted(() =>
30+
watch(
31+
movieId,
32+
async id => {
33+
unsub.value?.();
34+
if (!id) return;
35+
unsub.value = getMovieRef(id, movie).unsub;
36+
},
37+
{ immediate: true },
38+
),
39+
);
40+
41+
onUnmounted(() => {
42+
unsub.value?.();
43+
movie.value = undefined;
1344
});
45+
46+
const title = computed(() => {
47+
if (!movie.value?.title) return;
48+
return deCapitalise(movie.value.title);
49+
});
50+
51+
const titleUrl = computed(() => {
52+
if (!movie.value?.ids?.trakt) return;
53+
return ResolveExternalLinks.search({
54+
type: 'movie',
55+
source: 'trakt',
56+
id: movie.value.ids.trakt,
57+
});
58+
});
59+
60+
const { openTab } = useExtensionSettingsStore();
1461
</script>
1562

1663
<template>
17-
<div>movie {{ movieId }}</div>
64+
<NFlex justify="center" align="center" vertical>
65+
<TitleLink v-if="title" class="show-title" :href="titleUrl" @on-click="openTab">
66+
{{ title }}
67+
</TitleLink>
68+
<NSkeleton v-else class="show-title-skeleton" style="width: 50dvh" round />
69+
70+
<PanelPoster :tmdb="movie?.ids.tmdb" mode="movie" />
71+
</NFlex>
1872
</template>
73+
74+
<style lang="scss" scoped>
75+
.show-title:deep(h2),
76+
.show-title-skeleton {
77+
margin-bottom: 1rem;
78+
}
79+
80+
.show-title-skeleton {
81+
height: 1.5rem;
82+
margin-top: 0.625rem;
83+
}
84+
</style>

src/components/views/panel/PanelPoster.vue

+6-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ const props = defineProps({
1616
type: String as PropType<ImageStoreTypes>,
1717
required: true,
1818
},
19+
portait: {
20+
type: Boolean,
21+
required: false,
22+
},
1923
seasonNumber: {
2024
type: Number,
2125
required: false,
@@ -61,8 +65,8 @@ const key = computed(() => {
6165

6266
<template>
6367
<Transition v-if="posterItem" name="scale" mode="out-in">
64-
<NFlex :key="key" class="poster-container" :class="{ landscape: mode !== 'season' }">
65-
<PosterComponent :item="posterItem" :size="size" backdrop />
68+
<NFlex :key="key" class="poster-container" :class="{ landscape: !portait }">
69+
<PosterComponent :item="posterItem" :size="size" :backdrop="!portait" />
6670
</NFlex>
6771
</Transition>
6872
<NSkeleton
+72-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,85 @@
11
<script setup lang="ts">
2-
import { onActivated } from 'vue';
2+
import { NFlex, NSkeleton } from 'naive-ui';
3+
import { computed, onMounted, onUnmounted, ref, toRefs, watch } from 'vue';
4+
5+
import type { TraktPersonExtended } from '~/models/trakt/trakt-people.model';
6+
7+
import TitleLink from '~/components/common/buttons/TitleLink.vue';
8+
import PanelPoster from '~/components/views/panel/PanelPoster.vue';
9+
10+
import { ResolveExternalLinks } from '~/settings/external.links';
11+
import { usePersonStore } from '~/stores/data/person.store';
12+
import { useExtensionSettingsStore } from '~/stores/settings/extension.store';
13+
import { deCapitalise } from '~/utils/string.utils';
314
415
const props = defineProps({
516
personId: {
617
type: String,
7-
required: false,
18+
required: true,
819
},
920
});
1021
11-
onActivated(() => {
12-
console.info('PersonPanel activated', props.personId);
22+
const { personId } = toRefs(props);
23+
24+
const person = ref<TraktPersonExtended>();
25+
26+
const { getPersonRef } = usePersonStore();
27+
28+
const unsub = ref<() => void>();
29+
30+
onMounted(() =>
31+
watch(
32+
personId,
33+
async id => {
34+
unsub.value?.();
35+
if (!id) return;
36+
unsub.value = getPersonRef(id, person).unsub;
37+
},
38+
{ immediate: true },
39+
),
40+
);
41+
42+
onUnmounted(() => {
43+
unsub.value?.();
44+
person.value = undefined;
45+
});
46+
47+
const title = computed(() => {
48+
if (!person.value?.name) return;
49+
return deCapitalise(person.value?.name);
50+
});
51+
52+
const titleUrl = computed(() => {
53+
if (!person.value?.ids?.trakt) return;
54+
return ResolveExternalLinks.search({
55+
type: 'person',
56+
source: 'trakt',
57+
id: person.value.ids.trakt,
58+
});
1359
});
60+
61+
const { openTab } = useExtensionSettingsStore();
1462
</script>
1563

1664
<template>
17-
<div>person {{ personId }}</div>
65+
<NFlex justify="center" align="center" vertical>
66+
<TitleLink v-if="title" class="show-title" :href="titleUrl" @on-click="openTab">
67+
{{ title }}
68+
</TitleLink>
69+
<NSkeleton v-else class="show-title-skeleton" style="width: 50dvh" round />
70+
71+
<PanelPoster :tmdb="person?.ids.tmdb" mode="person" portait />
72+
</NFlex>
1873
</template>
74+
75+
<style lang="scss" scoped>
76+
.show-title:deep(h2),
77+
.show-title-skeleton {
78+
margin-bottom: 1rem;
79+
}
80+
81+
.show-title-skeleton {
82+
height: 1.5rem;
83+
margin-top: 0.625rem;
84+
}
85+
</style>

src/components/views/panel/ShowPanel.vue

+1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ const { openTab } = useExtensionSettingsStore();
154154
<PanelPoster
155155
:tmdb="show?.ids.tmdb"
156156
:mode="panelType"
157+
:portait="panelType === 'season'"
157158
:season-number="seasonNb"
158159
:episode-number="episodeNb"
159160
/>

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ export const movies = {
294294
summary: new TraktClientEndpoint<
295295
{
296296
/** Trakt ID, Trakt slug, or IMDB ID */
297-
id: string;
297+
id: string | number;
298298
} & TraktApiParamsExtended<typeof TraktApiExtended.Full>,
299299
TraktMovie<'any'>
300300
>({

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export const people = {
9999
summary: new TraktClientEndpoint<
100100
{
101101
/** Trakt ID, Trakt slug, or IMDB ID */
102-
id: string;
102+
id: string | number;
103103
} & TraktApiParamsExtended<typeof TraktApiExtended.Full>,
104104
TraktPerson<'any'>
105105
>({

src/services/trakt.service.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type { TraktEpisodeExtended, TraktEpisodeShort } from '~/models/trakt/tra
88
import type { TraktFavoriteGetQuery } from '~/models/trakt/trakt-favorite.model';
99
import type { TraktHistoryGetQuery } from '~/models/trakt/trakt-history.model';
1010
import type { TraktList, TraktListItemsGetQuery } from '~/models/trakt/trakt-list.model';
11+
import type { TraktMovieExtended } from '~/models/trakt/trakt-movie.model';
12+
import type { TraktPersonExtended } from '~/models/trakt/trakt-people.model';
1113
import type { TraktSearch } from '~/models/trakt/trakt-search.model';
1214
import type { TraktSeasonExtended } from '~/models/trakt/trakt-season.model';
1315
import type { TraktShowExtended } from '~/models/trakt/trakt-show.model';
@@ -342,14 +344,26 @@ export class TraktService {
342344
return response.json();
343345
}
344346

347+
static async movie(id: string | number) {
348+
const response = await this.traktClient.movies.summary.cached({ id, extended: 'full' });
349+
return response.json() as Promise<TraktMovieExtended>;
350+
}
351+
352+
static async person(id: string | number) {
353+
const response = await this.traktClient.people.summary.cached({ id, extended: 'full' });
354+
return response.json() as Promise<TraktPersonExtended>;
355+
}
356+
345357
static evict = {
346-
tmdb: TraktService.caches.tmdb.clear,
347-
trakt: TraktService.caches.trakt.clear,
348-
tvdb: TraktService.caches.tvdb.clear,
358+
tmdb: () => TraktService.tmdbClient.clearCache(),
359+
trakt: () => TraktService.traktClient.clearCache(),
360+
tvdb: () => TraktService.tvdbClient.clearCache(),
349361
history: TraktService.traktClient.sync.history.get.cached.evict,
350362
watchlist: TraktService.traktClient.sync.watchlist.get.cached.evict,
351363
favorites: TraktService.traktClient.sync.favorites.get.cached.evict,
352364
collection: TraktService.traktClient.sync.collection.get.cached.evict,
365+
movies: TraktService.traktClient.movies.summary.cached.evict,
366+
people: TraktService.traktClient.people.summary.cached.evict,
353367
shows: TraktService.traktClient.shows.summary.cached.evict,
354368
seasons: () => Promise.all([TraktService.traktClient.seasons.summary.cached.evict(), TraktService.traktClient.seasons.season.cached.evict()]),
355369
episodes: TraktService.traktClient.episodes.summary.cached.evict,

src/stores/data/movie.store.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { defineStore, storeToRefs } from 'pinia';
2+
3+
import { computed, reactive, ref } from 'vue';
4+
5+
import type { TraktMovieExtended } from '~/models/trakt/trakt-movie.model';
6+
7+
import { NotificationService } from '~/services/notification.service';
8+
import { TraktService } from '~/services/trakt.service';
9+
import { asyncRefGetter } from '~/utils/vue.utils';
10+
11+
type MovieDictionary = Record<string, TraktMovieExtended>;
12+
type LoadingDictionary = Record<string, boolean>;
13+
14+
export const useMovieStore = defineStore('data.movie', () => {
15+
const movies = reactive<MovieDictionary>({});
16+
const loading = reactive<LoadingDictionary>({});
17+
18+
const clearState = () => {
19+
Object.assign(movies, {});
20+
Object.assign(loading, {});
21+
};
22+
23+
const fetchMovie = async (id: string | number) => {
24+
if (loading[id]) {
25+
console.warn('Already fetching movie', id);
26+
}
27+
28+
console.info('Fetching movie', id);
29+
30+
loading[id] = true;
31+
32+
try {
33+
movies[id] = await TraktService.movie(id);
34+
} catch (error) {
35+
console.error('Failed to fetch movie', id);
36+
NotificationService.error(`Failed to fetch movie '${id}'.`, error);
37+
throw error;
38+
} finally {
39+
loading[id] = false;
40+
}
41+
};
42+
43+
const getMovieLoading = (id: string | number) => computed(() => loading[id.toString()]);
44+
const getMovie = async (id: string | number) => {
45+
if (!movies[id.toString()] && !loading[id.toString()]) await fetchMovie(id);
46+
return movies[id.toString()];
47+
};
48+
const getMovieRef = (id: string | number, response = ref<TraktMovieExtended>()) =>
49+
asyncRefGetter(() => getMovie(id), getMovieLoading(id), response);
50+
51+
return {
52+
clearState,
53+
fetchMovie,
54+
getMovie,
55+
getMovieRef,
56+
getMovieLoading,
57+
};
58+
});
59+
60+
export const useMovieStoreRefs = () => storeToRefs(useMovieStore());

src/stores/data/person.store.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { defineStore, storeToRefs } from 'pinia';
2+
3+
import { computed, reactive, ref } from 'vue';
4+
5+
import type { TraktPersonExtended } from '~/models/trakt/trakt-people.model';
6+
7+
import { NotificationService } from '~/services/notification.service';
8+
import { TraktService } from '~/services/trakt.service';
9+
import { asyncRefGetter } from '~/utils/vue.utils';
10+
11+
type PersonDictionary = Record<string, TraktPersonExtended>;
12+
type LoadingDictionary = Record<string, boolean>;
13+
14+
export const usePersonStore = defineStore('data.person', () => {
15+
const people = reactive<PersonDictionary>({});
16+
const loading = reactive<LoadingDictionary>({});
17+
18+
const clearState = () => {
19+
Object.assign(people, {});
20+
Object.assign(loading, {});
21+
};
22+
23+
const fetchPerson = async (id: string | number) => {
24+
if (loading[id]) {
25+
console.warn('Already fetching person', id);
26+
}
27+
28+
console.info('Fetching person', id);
29+
30+
loading[id] = true;
31+
32+
try {
33+
people[id] = await TraktService.person(id);
34+
} catch (error) {
35+
console.error('Failed to fetch person', id);
36+
NotificationService.error(`Failed to fetch person '${id}'.`, error);
37+
throw error;
38+
} finally {
39+
loading[id] = false;
40+
}
41+
};
42+
43+
const getPersonLoading = (id: string | number) => computed(() => loading[id.toString()]);
44+
const getPerson = async (id: string | number) => {
45+
if (!people[id.toString()] && !loading[id.toString()]) await fetchPerson(id);
46+
return people[id.toString()];
47+
};
48+
const getPersonRef = (id: string | number, response = ref<TraktPersonExtended>()) =>
49+
asyncRefGetter(() => getPerson(id), getPersonLoading(id), response);
50+
51+
return {
52+
clearState,
53+
fetchPerson,
54+
getPerson,
55+
getPersonRef,
56+
getPersonLoading,
57+
};
58+
});
59+
60+
export const usePersonStoreRefs = () => storeToRefs(usePersonStore());

0 commit comments

Comments
 (0)