Skip to content

Commit 24a11d3

Browse files
committed
feat(calendar): adds basic support for calendar
1 parent 589f469 commit 24a11d3

17 files changed

+557
-172
lines changed

src/components/common/list/ListItem.vue

+18-23
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import { NFlex, NImage, NSkeleton, NTime, NTimelineItem } from 'naive-ui';
33
4-
import { computed, type PropType, toRefs, watch } from 'vue';
4+
import { computed, type PropType, toRefs } from 'vue';
55
66
import type { ListScrollItem } from '~/components/common/list/ListScroll.model';
77
@@ -20,15 +20,15 @@ const props = defineProps({
2020
type: Number,
2121
required: true,
2222
},
23-
size: {
24-
type: Number,
25-
required: true,
26-
},
2723
poster: {
2824
type: String,
2925
required: false,
3026
default: PosterPlaceholder,
3127
},
28+
episode: {
29+
type: Boolean,
30+
required: false,
31+
},
3232
color: {
3333
type: String,
3434
required: false,
@@ -61,7 +61,7 @@ const emit = defineEmits<{
6161
(e: 'onHover', event: { index: number; item: ListScrollItem; hover: boolean }): void;
6262
}>();
6363
64-
const { item, size, index, noHeader, nextHasHeader, poster, hideDate } = toRefs(props);
64+
const { item, index, noHeader, nextHasHeader, poster, episode, hideDate } = toRefs(props);
6565
6666
const onHover = (_hover: boolean) => {
6767
emit('onHover', { index: index?.value, item: item?.value, hover: _hover });
@@ -79,8 +79,6 @@ const year = new Date().getFullYear();
7979
const sameYear = computed(() => date.value?.getFullYear() === year);
8080
const loading = computed(() => item?.value?.loading);
8181
82-
const resolvedPoster = computed(() => item?.value?.poster?.value || poster.value);
83-
8482
const { getImageUrl } = useImageStore();
8583
const { imageSizes } = useImageStoreRefs();
8684
@@ -90,22 +88,19 @@ const imageSize = computed(() =>
9088
: 'original',
9189
);
9290
93-
watch(
94-
item,
95-
_item => {
96-
if (_item?.poster) return;
97-
if (!_item?.type) return;
98-
99-
const type = ['show', 'episode', 'season'].includes(_item.type) ? 'show' : _item.type;
100-
if (!_item?.[type]?.ids?.tmdb) {
101-
console.warn('No tmdb id found', JSON.parse(JSON.stringify(_item?.[_item?.type])));
102-
return;
103-
}
91+
const itemPoster = computed(() => {
92+
const media = item.value;
93+
if (media.poster) return media.poster;
94+
const query = media.getPosterQuery?.();
95+
if (query)
96+
return getImageUrl(
97+
episode.value ? query : { ...query, episode: undefined },
98+
imageSize.value,
99+
).value;
100+
return null;
101+
});
104102
105-
_item.poster = getImageUrl({ id: _item[type]!.ids.tmdb, type }, imageSize.value);
106-
},
107-
{ immediate: true },
108-
);
103+
const resolvedPoster = computed(() => itemPoster.value || poster.value);
109104
</script>
110105

111106
<template>

src/components/common/list/ListItemPanel.vue

+47-24
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { NEllipsis, NFlex, NSkeleton } from 'naive-ui';
2+
import { NEllipsis, NFlex, NSkeleton, NTag } from 'naive-ui';
33
44
import { computed, type PropType, toRefs } from 'vue';
55
@@ -30,30 +30,32 @@ const props = defineProps({
3030
},
3131
});
3232
33-
const { item } = toRefs(props);
33+
const { item, hideDate } = toRefs(props);
3434
3535
const type = computed(() =>
3636
item.value.type ? i18n(item.value.type, 'common', 'media', 'type') : item.value.type,
3737
);
3838
39-
const title = computed(() => {
40-
const media = item.value;
41-
if (media.person) return media.person.name;
42-
if (media.movie) return media.movie.title;
43-
if (!media.episode) return media.show?.title;
44-
const number = media.episode.number?.toString().padStart(2, '0');
45-
return `${media.episode.season}x${number} - ${media.episode.title}`;
46-
});
39+
const title = computed(() => item.value.title);
40+
41+
const content = computed(() => item.value.content);
4742
48-
const content = computed(() => {
49-
const media = item.value;
50-
if (media.movie) return media.movie.year;
51-
if (!media.episode) return media.show?.year;
52-
return media.show?.title;
43+
const date = computed(() => {
44+
if (hideDate.value) return;
45+
return item.value.date?.current?.toLocaleTimeString(navigator.language, {
46+
hour: '2-digit',
47+
minute: '2-digit',
48+
});
5349
});
5450
55-
const currentDate = computed(() => item.value.date?.current);
56-
const date = computed(() => currentDate.value?.toLocaleTimeString());
51+
const tags = computed(
52+
() =>
53+
item.value.tags?.map(tag => {
54+
if (!tag.i18n) return tag;
55+
if (typeof tag.i18n === 'boolean') return { ...tag, label: i18n(tag.label) };
56+
return { ...tag, label: i18n(tag.label, ...tag.i18n) };
57+
}),
58+
);
5759
</script>
5860

5961
<template>
@@ -76,10 +78,25 @@ const date = computed(() => currentDate.value?.toLocaleTimeString());
7678
<NSkeleton v-if="loading" text style="width: 60%" round />
7779
<NEllipsis v-else :line-clamp="2">{{ content }}</NEllipsis>
7880
</div>
79-
<div v-if="!hideDate" class="meta time">
80-
<NSkeleton v-if="loading" text style="width: 20%" round />
81-
<NEllipsis v-else :line-clamp="1">{{ date }}</NEllipsis>
82-
</div>
81+
<NFlex v-if="date || tags?.length" size="medium" class="tags">
82+
<NTag v-if="date" class="tag meta" size="small">
83+
<NSkeleton v-if="loading" text style="width: 20%" round />
84+
<span>{{ date }} </span>
85+
</NTag>
86+
<NTag
87+
v-for="tag of tags"
88+
:key="tag.label"
89+
class="tag"
90+
:class="{
91+
meta: tag.meta,
92+
}"
93+
size="small"
94+
:type="tag.type"
95+
:bordered="tag.bordered ?? true"
96+
>
97+
{{ tag.label }}
98+
</NTag>
99+
</NFlex>
83100
</NFlex>
84101
</template>
85102

@@ -89,11 +106,12 @@ const date = computed(() => currentDate.value?.toLocaleTimeString());
89106
margin: 0.25rem 0;
90107
91108
.title {
92-
font-variant-numeric: tabular-nums;
109+
margin-top: 0.1rem;
93110
color: var(--trakt-red);
94111
font-weight: var(--n-title-font-weight);
95112
font-size: var(--n-title-font-size);
96113
transition: color 0.3s var(--n-bezier);
114+
font-variant-numeric: tabular-nums;
97115
}
98116
99117
.content {
@@ -108,8 +126,13 @@ const date = computed(() => currentDate.value?.toLocaleTimeString());
108126
transition: color 0.3s var(--n-bezier);
109127
}
110128
111-
.time {
112-
margin-top: 0.25rem;
129+
.tags {
130+
gap: 0.5rem !important;
131+
margin-top: 0.3rem;
132+
133+
.tag {
134+
width: fit-content;
135+
}
113136
}
114137
}
115138
</style>

src/components/common/list/ListScroll.model.ts

+28-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import type { NVirtualList, VirtualListInst } from 'naive-ui';
1+
import type { NVirtualList, TagProps, VirtualListInst } from 'naive-ui';
2+
23
import type { Ref } from 'vue';
34
import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
45
import type { TraktMovie } from '~/models/trakt/trakt-movie.model';
56
import type { TraktPerson } from '~/models/trakt/trakt-people.model';
67
import type { TraktSeason } from '~/models/trakt/trakt-season.model';
78
import type { TraktShow } from '~/models/trakt/trakt-show.model';
9+
import type { ImageQuery } from '~/stores/data/image.store';
810

911
export type VirtualListRef = VirtualListInst & InstanceType<typeof NVirtualList>;
1012
export type VirtualListProps = {
@@ -28,13 +30,34 @@ export type ListScrollSourceItem = {
2830
person?: TraktPerson<'short'>;
2931
};
3032

31-
export type ListScrollItem = ListScrollSourceItem & {
32-
id: string | number | 'load-more';
33+
export type ListScrollItemTag = {
34+
label: string;
35+
i18n?: boolean | string[];
36+
type?: TagProps['type'];
37+
bordered?: boolean;
38+
meta?: string;
39+
};
40+
41+
export const ListScrollItemType = {
42+
movie: 'movie',
43+
show: 'show',
44+
season: 'season',
45+
episode: 'episode',
46+
person: 'person',
47+
placeholder: 'placeholder',
48+
} as const;
49+
50+
export type ListScrollItem = {
51+
id: string | number | 'load-more' | 'empty';
3352
index: number;
3453

35-
type?: 'movie' | 'show' | 'season' | 'episode' | 'person';
54+
type?: keyof typeof ListScrollItemType;
55+
title?: string;
56+
content?: string;
57+
tags?: ListScrollItemTag[];
3658

37-
poster?: Ref<string | undefined>;
59+
poster?: string;
60+
getPosterQuery?: () => ImageQuery | undefined;
3861

3962
loading?: boolean;
4063
date?: {

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

+88-22
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,108 @@ import { computed } from 'vue';
22

33
import type { Ref } from 'vue';
44

5-
import type { ListScrollItem, ListScrollSourceItem, OnScroll, OnUpdated } from '~/components/common/list/ListScroll.model';
65
import type { TraktClientPagination } from '~/models/trakt/trakt-client.model';
76

7+
import type { ImageQuery } from '~/stores/data/image.store';
8+
9+
import {
10+
type ListScrollItem,
11+
ListScrollItemType,
12+
type ListScrollSourceItem,
13+
type OnScroll,
14+
type OnUpdated,
15+
} from '~/components/common/list/ListScroll.model';
16+
817
export type ListScrollSourceItemWithDate<T extends string> = ListScrollSourceItem & Partial<Record<T, string | number | Date>>;
918

19+
export const getTitle = (media: ListScrollSourceItem): ListScrollItem['title'] => {
20+
if (!media) return;
21+
if (media.person) return media.person.name;
22+
if (media.movie) return media.movie.title;
23+
if (!media.episode) return media.show?.title;
24+
const number = media.episode.number?.toString().padStart(2, '0');
25+
return `${media.episode.season}x${number} - ${media.episode.title}`;
26+
};
27+
28+
export const getContent = (media: ListScrollSourceItem): ListScrollItem['content'] => {
29+
if (!media) return;
30+
if (media.movie) return media.movie.year?.toString();
31+
if (!media.episode) return media.show?.year?.toString();
32+
return media.show?.title;
33+
};
34+
35+
export const getType = (media: ListScrollSourceItem): ListScrollItem['type'] => {
36+
if (!media) return;
37+
if ('movie' in media) return ListScrollItemType.movie;
38+
if ('episode' in media) return ListScrollItemType.episode;
39+
if ('season' in media) return ListScrollItemType.season;
40+
if ('show' in media) return ListScrollItemType.show;
41+
if ('person' in media) return ListScrollItemType.person;
42+
};
43+
44+
export const getDate = <D extends string, T extends ListScrollSourceItemWithDate<D>>(
45+
media: T,
46+
array: T[],
47+
index: number,
48+
dateFn?: D | ((item: T) => ListScrollSourceItemWithDate<D>[D]),
49+
): ListScrollItem['date'] => {
50+
if (!media || !dateFn) return;
51+
const _date = typeof dateFn === 'function' ? dateFn(media) : media[dateFn];
52+
if (!_date) return;
53+
const date: ListScrollItem['date'] = { current: new Date(_date) };
54+
const previous = typeof dateFn === 'function' ? dateFn(array[index - 1]) : array[index - 1]?.[dateFn];
55+
if (index > 0 && previous) date.previous = new Date(previous);
56+
const next = typeof dateFn === 'function' ? dateFn(array[index + 1]) : array[index + 1]?.[dateFn];
57+
if (next) date.next = new Date(next);
58+
date.sameDayAsPrevious = date.previous?.toLocaleDateString() === date.current?.toLocaleDateString();
59+
date.sameDayAsNext = date.next?.toLocaleDateString() === date.current?.toLocaleDateString();
60+
return date;
61+
};
62+
63+
const isMediaType = (type: ListScrollItem['type']): type is 'movie' | 'show' | 'season' | 'episode' | 'person' =>
64+
type === ListScrollItemType.movie ||
65+
type === ListScrollItemType.show ||
66+
type === ListScrollItemType.season ||
67+
type === ListScrollItemType.episode ||
68+
type === ListScrollItemType.person;
69+
70+
export const getPosterQuery =
71+
(item: ListScrollSourceItem, type: ListScrollItem['type']): ListScrollItem['getPosterQuery'] =>
72+
() => {
73+
if (!item || !type) return;
74+
if (type === ListScrollItemType.placeholder) return;
75+
if (!isMediaType(type)) return;
76+
const _type = ['show', 'episode', 'season'].includes(type) ? 'show' : type;
77+
const media = item[_type];
78+
if (!media?.ids.tmdb) return;
79+
return {
80+
type,
81+
id: media.ids.tmdb,
82+
season: item?.episode?.season ?? item.season?.number,
83+
episode: item?.episode?.number,
84+
} satisfies ImageQuery;
85+
};
86+
1087
export const useListScroll = <D extends string, T extends ListScrollSourceItemWithDate<D>>(
1188
items: Ref<T[]>,
1289
dateFn?: D | ((item: T) => ListScrollSourceItemWithDate<D>[D]),
13-
) =>
14-
computed<ListScrollItem[]>(() => {
90+
) => {
91+
return computed<ListScrollItem[]>(() => {
1592
const array = items.value;
1693
if (!array.length) return [];
1794
return array.map((item, index) => {
1895
const _item: ListScrollItem = { ...item, index, loading: typeof item.id === 'number' && item.id < 0 };
1996

20-
if ('movie' in _item) _item.type = 'movie';
21-
else if ('episode' in _item) _item.type = 'episode';
22-
else if ('season' in _item) _item.type = 'season';
23-
else if ('show' in _item) _item.type = 'show';
24-
else if ('person' in _item) _item.type = 'person';
25-
26-
if (!_item || !dateFn) return _item;
27-
const _date = typeof dateFn === 'function' ? dateFn(item) : item[dateFn];
28-
if (!_date) return _item;
29-
30-
const date: ListScrollItem['date'] = { current: new Date(_date!) };
31-
const previous = typeof dateFn === 'function' ? dateFn(array[index - 1]) : array[index - 1]?.[dateFn];
32-
if (index > 0 && previous) date.previous = new Date(previous);
33-
const next = typeof dateFn === 'function' ? dateFn(array[index + 1]) : array[index + 1]?.[dateFn];
34-
if (next) date.next = new Date(next);
35-
date.sameDayAsPrevious = date.previous?.toLocaleDateString() === date.current?.toLocaleDateString();
36-
date.sameDayAsNext = date.next?.toLocaleDateString() === date.current?.toLocaleDateString();
37-
38-
return { ..._item, date };
97+
if (!_item.type) _item.type = getType(item);
98+
if (!_item.title) _item.title = getTitle(item);
99+
if (!_item.content) _item.content = getContent(item);
100+
if (!_item.poster && !_item.getPosterQuery) _item.getPosterQuery = getPosterQuery(item, _item.type);
101+
_item.date = getDate(item, array, index, dateFn);
102+
103+
return _item;
39104
});
40105
});
106+
};
41107

42108
export const useListScrollEvents = (
43109
callback: (query: { page: number }) => Promise<unknown>,

0 commit comments

Comments
 (0)