Skip to content

Commit b96676e

Browse files
committed
feat(list): connect store to scroll list
1 parent a24775b commit b96676e

File tree

15 files changed

+364
-182
lines changed

15 files changed

+364
-182
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ref } from 'vue';
2+
3+
import type { VirtualListRef } from '~/components/common/list/ListScroll.model';
4+
5+
export const useBackToTop = () => {
6+
const listRef = ref<{ list: VirtualListRef }>();
7+
const scrolled = ref(false);
8+
9+
const onClick = () => {
10+
listRef.value?.list?.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
11+
scrolled.value = false;
12+
};
13+
14+
return { listRef, scrolled, onClick };
15+
};

src/components/common/list/ListItem.vue

+6-4
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,17 @@ const emit = defineEmits<{
6161
(e: 'onHover', event: { index: number; item: ListScrollItem; hover: boolean }): void;
6262
}>();
6363
64-
const { item, size, index, noHeader, nextHasHeader, poster } = toRefs(props);
64+
const { item, size, index, noHeader, nextHasHeader, poster, hideDate } = toRefs(props);
6565
6666
const onHover = (_hover: boolean) => {
6767
emit('onHover', { index: index?.value, item: item?.value, hover: _hover });
6868
};
6969
70-
const noHead = computed(() => noHeader.value || item?.value?.date?.sameDayAsPrevious);
70+
const noHead = computed(
71+
() => !hideDate.value && (noHeader.value || item?.value?.date?.sameDayAsPrevious),
72+
);
7173
const nextHasHead = computed(
72-
() => nextHasHeader.value || !item?.value?.date?.sameDayAsNext,
74+
() => hideDate.value || nextHasHeader.value || !item?.value?.date?.sameDayAsNext,
7375
);
7476
const date = computed(() => item?.value?.date?.current);
7577
@@ -175,7 +177,7 @@ watch(
175177
:src="PosterPlaceholder"
176178
:fallback-src="PosterPlaceholder"
177179
/>
178-
<ListItemPanel :item="item" :loading="loading">
180+
<ListItemPanel :item="item" :loading="loading" :hide-date="hideDate">
179181
<slot :item="item" :index="index" :loading="loading" />
180182
</ListItemPanel>
181183
</NFlex>

src/components/common/list/ListItemPanel.vue

+5-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const props = defineProps({
2424
required: false,
2525
default: PosterPlaceholder,
2626
},
27+
hideDate: {
28+
type: Boolean,
29+
required: false,
30+
},
2731
});
2832
2933
const { item } = toRefs(props);
@@ -71,7 +75,7 @@ const date = computed(() => currentDate.value?.toLocaleTimeString());
7175
<NSkeleton v-if="loading" text style="width: 60%" round />
7276
<NEllipsis v-else :line-clamp="2">{{ content }}</NEllipsis>
7377
</div>
74-
<div class="meta time">
78+
<div v-if="!hideDate" class="meta time">
7579
<NSkeleton v-if="loading" text style="width: 20%" round />
7680
<NEllipsis v-else :line-clamp="1">{{ date }}</NEllipsis>
7781
</div>

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

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { NVirtualList, VirtualListInst } from 'naive-ui';
22
import type { Ref } from 'vue';
33
import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
44
import type { TraktMovie } from '~/models/trakt/trakt-movie.model';
5+
import type { TraktPerson } from '~/models/trakt/trakt-people.model';
56
import type { TraktSeason } from '~/models/trakt/trakt-season.model';
67
import type { TraktShow } from '~/models/trakt/trakt-show.model';
78

@@ -24,6 +25,7 @@ export type ListScrollSourceItem = {
2425
show?: TraktShow<'short'>;
2526
season?: TraktSeason<'short'>;
2627
episode?: TraktEpisode<'short'>;
28+
person?: TraktPerson<'short'>;
2729
};
2830

2931
export type ListScrollItem = ListScrollSourceItem & {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { computed } from 'vue';
2+
3+
import type { Ref } from 'vue';
4+
5+
import type { ListScrollItem, ListScrollSourceItem, OnScroll, OnUpdated } from '~/components/common/list/ListScroll.model';
6+
import type { TraktClientPagination } from '~/models/trakt/trakt-client.model';
7+
8+
export type ListScrollSourceItemWithDate<T extends string> = ListScrollSourceItem & Partial<Record<T, string | number | Date>>;
9+
10+
export const useListScroll = <D extends string, T extends ListScrollSourceItemWithDate<D>>(
11+
items: Ref<T[]>,
12+
dateFn?: D | ((item: T) => ListScrollSourceItemWithDate<D>[D]),
13+
) =>
14+
computed<ListScrollItem[]>(() => {
15+
const array = items.value;
16+
if (!array.length) return [];
17+
return array.map((item, index) => {
18+
const _item: ListScrollItem = { ...item, index, loading: typeof item.id === 'number' && item.id < 0 };
19+
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+
25+
if (!_item || !dateFn) return _item;
26+
const _date = typeof dateFn === 'function' ? dateFn(item) : item[dateFn];
27+
if (!_date) return _item;
28+
29+
const date: ListScrollItem['date'] = { current: new Date(_date!) };
30+
const previous = typeof dateFn === 'function' ? dateFn(array[index - 1]) : array[index - 1]?.[dateFn];
31+
if (index > 0 && previous) date.previous = new Date(previous);
32+
const next = typeof dateFn === 'function' ? dateFn(array[index + 1]) : array[index + 1]?.[dateFn];
33+
if (next) date.next = new Date(next);
34+
date.sameDayAsPrevious = date.previous?.toLocaleDateString() === date.current?.toLocaleDateString();
35+
date.sameDayAsNext = date.next?.toLocaleDateString() === date.current?.toLocaleDateString();
36+
37+
return { ..._item, date };
38+
});
39+
});
40+
41+
export const useListScrollEvents = (
42+
callback: (query: { page: number }) => Promise<unknown>,
43+
{
44+
data,
45+
pagination,
46+
loading,
47+
belowThreshold,
48+
}: { data: Ref<ListScrollItem[]>; pagination: Ref<TraktClientPagination | undefined>; loading: Ref<boolean>; belowThreshold: Ref<boolean> },
49+
) => {
50+
const onScroll: OnScroll = async listRef => {
51+
const key = data.value[data.value.length - 1].id;
52+
await callback({
53+
page: pagination.value?.page ? pagination.value.page + 1 : 0,
54+
});
55+
listRef.value?.scrollTo({ key, debounce: true });
56+
};
57+
58+
const onLoadMore = async () =>
59+
callback({
60+
page: pagination.value?.page ? pagination.value.page + 1 : 0,
61+
});
62+
63+
/**
64+
* This is a workaround for the onUpdated lifecycle hook not triggering when wrapped in transition.
65+
*/
66+
const onUpdated: OnUpdated = listRef => {
67+
const { scrollHeight, clientHeight } = listRef.value?.$el?.firstElementChild ?? {};
68+
if (scrollHeight !== clientHeight || !belowThreshold.value || loading.value) return;
69+
70+
return onLoadMore();
71+
};
72+
73+
return { onScroll, onUpdated, onLoadMore };
74+
};
75+
76+
export const addLoadMore = (
77+
items: Ref<ListScrollItem[]>,
78+
pagination: Ref<TraktClientPagination | undefined>,
79+
search: Ref<string>,
80+
): Ref<ListScrollItem[]> => {
81+
return computed(() => {
82+
const array = items.value;
83+
if (!array.length) return array;
84+
if (!search.value) return array;
85+
if (!pagination.value?.page) return array;
86+
if (!pagination.value?.pageCount) return array;
87+
if (pagination.value.page === pagination.value.pageCount) return array;
88+
if (array.length && array[array.length - 1].id === 'load-more') return array;
89+
const loadMore: ListScrollItem = { id: 'load-more', index: items.value.length };
90+
return [...array, loadMore];
91+
});
92+
};

src/components/common/list/useListScroll.ts

-54
This file was deleted.

src/components/views/history/HistoryComponent.vue

+14-40
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
<script lang="ts" setup>
2-
import { ref } from 'vue';
3-
4-
import type {
5-
OnScroll,
6-
OnUpdated,
7-
VirtualListRef,
8-
} from '~/components/common/list/ListScroll.model';
9-
102
import FloatingButton from '~/components/common/buttons/FloatingButton.vue';
3+
import { useBackToTop } from '~/components/common/buttons/use-back-to-top';
114
import ListScroll from '~/components/common/list/ListScroll.vue';
12-
import { addLoadMore, useListScroll } from '~/components/common/list/useListScroll';
5+
import {
6+
addLoadMore,
7+
useListScroll,
8+
useListScrollEvents,
9+
} from '~/components/common/list/use-list-scroll';
1310
import { useHistoryStore, useHistoryStoreRefs } from '~/stores/data/history.store';
1411
import { useI18n } from '~/utils';
1512
import { watchUserChange } from '~/utils/store.utils';
@@ -26,37 +23,14 @@ const list = useListScroll(filteredHistory, 'watched_at');
2623
2724
const history = addLoadMore(list, pagination, searchHistory);
2825
29-
const onScroll: OnScroll = async listRef => {
30-
const key = history.value[history.value.length - 1].id;
31-
await fetchHistory({
32-
page: pagination.value?.page ? pagination.value.page + 1 : 0,
33-
});
34-
listRef.value?.scrollTo({ key, debounce: true });
35-
};
36-
37-
const onLoadMore = async () =>
38-
fetchHistory({
39-
page: pagination.value?.page ? pagination.value.page + 1 : 0,
40-
});
41-
42-
/**
43-
* This is a workaround for the onUpdated lifecycle hook not triggering when wrapped in transition.
44-
*/
45-
const onUpdated: OnUpdated = listRef => {
46-
const { scrollHeight, clientHeight } = listRef.value?.$el?.firstElementChild ?? {};
47-
if (scrollHeight !== clientHeight || !belowThreshold.value || loading.value) return;
48-
49-
return onLoadMore();
50-
};
51-
52-
const listRef = ref<{ list: VirtualListRef }>();
53-
54-
const scrolled = ref(false);
26+
const { onScroll, onUpdated, onLoadMore } = useListScrollEvents(fetchHistory, {
27+
data: history,
28+
pagination,
29+
loading,
30+
belowThreshold,
31+
});
5532
56-
const onClick = () => {
57-
listRef.value?.list?.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
58-
scrolled.value = false;
59-
};
33+
const { scrolled, listRef, onClick } = useBackToTop();
6034
</script>
6135

6236
<template>
@@ -79,7 +53,7 @@ const onClick = () => {
7953
</template>
8054
</ListScroll>
8155
<FloatingButton :show="scrolled" @on-click="onClick">
82-
{{ i18n('button_top') }}
56+
{{ i18n('back_to_top', 'common', 'button') }}
8357
</FloatingButton>
8458
</div>
8559
</template>

0 commit comments

Comments
 (0)