Skip to content

Commit e17611f

Browse files
committed
feat(panel): adds list state warning & active list
1 parent 761167d commit e17611f

10 files changed

+142
-33
lines changed

src/components/common/tooltip/ProgressTooltip.vue

+28-21
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const percentage = computed(() => {
4040
const remaining = computed(() => {
4141
if (!progress?.value) return;
4242
if (!('aired' in progress.value)) return;
43+
if (!progress.value.completed) return;
4344
return progress.value.aired - progress.value.completed;
4445
});
4546
@@ -54,27 +55,33 @@ const i18n = useI18n('common', 'tooltip', 'progress');
5455
</script>
5556

5657
<template>
57-
<NTooltip class="progress-tooltip" :disabled="disabled || !progress" :delay="100">
58-
<NFlex v-if="progress && isCount" vertical align="flex-end">
59-
<div v-if="'aired' in progress">
60-
<span class="metric">{{ progress?.completed ?? '-' }}</span>
61-
<span> / </span>
62-
<span class="metric">{{ progress?.aired ?? '-' }}</span>
63-
<span>&nbsp;</span>
64-
<span>{{ i18n('episodes') }}</span>
65-
</div>
66-
<div v-if="percentage">
67-
<span class="metric">{{ percentage }}</span>
68-
<span>%</span>
69-
<span>&nbsp;</span>
70-
<span>{{ i18n(type) }}</span>
71-
</div>
72-
<div v-if="remaining">
73-
<span class="metric">{{ remaining }}</span>
74-
<span>&nbsp;</span>
75-
<span>{{ i18n('remaining') }}</span>
76-
</div>
77-
</NFlex>
58+
<NTooltip
59+
class="progress-tooltip"
60+
:disabled="disabled || (!$slots.label && !progress)"
61+
:delay="100"
62+
>
63+
<slot name="label">
64+
<NFlex v-if="progress && isCount" vertical align="flex-end">
65+
<div v-if="'aired' in progress">
66+
<span class="metric">{{ progress?.completed ?? '-' }}</span>
67+
<span> / </span>
68+
<span class="metric">{{ progress?.aired ?? '-' }}</span>
69+
<span>&nbsp;</span>
70+
<span>{{ i18n('episodes') }}</span>
71+
</div>
72+
<div>
73+
<span class="metric">{{ percentage }}</span>
74+
<span>%</span>
75+
<span>&nbsp;</span>
76+
<span>{{ i18n(type) }}</span>
77+
</div>
78+
<div v-if="remaining">
79+
<span class="metric">{{ remaining }}</span>
80+
<span>&nbsp;</span>
81+
<span>{{ i18n('remaining') }}</span>
82+
</div>
83+
</NFlex>
84+
</slot>
7885
<template #trigger>
7986
<slot />
8087
</template>

src/components/views/panel/MoviePanel.vue

+18
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import MoviePanelOverview from '~/components/views/panel/MoviePanelOverview.vue'
1111
import PanelPoster from '~/components/views/panel/PanelPoster.vue';
1212
1313
import { ResolveExternalLinks } from '~/settings/external.links';
14+
import { useListsStoreRefs, useListStore } from '~/stores/data/list.store';
1415
import { useMovieStore, useMovieStoreRefs } from '~/stores/data/movie.store';
1516
import { useExtensionSettingsStore } from '~/stores/settings/extension.store';
1617
import { useI18n } from '~/utils';
@@ -67,6 +68,21 @@ onUnmounted(() => {
6768
movie.value = undefined;
6869
});
6970
71+
const { lists } = useListsStoreRefs();
72+
const { isListLoading, isItemInList } = useListStore();
73+
74+
const listLoading = computed(() => {
75+
if (!movieId?.value) return;
76+
return isListLoading(movieId.value).value;
77+
});
78+
79+
const activeLists = computed(() => {
80+
if (!movieId?.value) return;
81+
return lists.value
82+
?.filter(list => isItemInList(list.id, movieId.value).value)
83+
.map(list => list.id);
84+
});
85+
7086
const i18n = useI18n('movie', 'panel');
7187
7288
const title = computed(() => {
@@ -108,6 +124,8 @@ const { openTab } = useExtensionSettingsStore();
108124
:watched-loading="loadingWatched"
109125
:collected="collected"
110126
:collected-loading="loadingCollected"
127+
:active-loading="listLoading"
128+
:active-lists="activeLists"
111129
/>
112130

113131
<MoviePanelOverview :movie="movie" />

src/components/views/panel/MoviePanelButtons.vue

+9
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,20 @@ onMounted(() => {
100100
multiple: true,
101101
scrollable: true,
102102
}"
103+
:tooltip="{
104+
delay: 500,
105+
}"
103106
:icon="activeLists?.length ? IconCheckedList : IconListEmpty"
104107
:filled="!!activeLists?.length"
105108
:disabled="listsLoading"
106109
type="warning"
107110
>
111+
<template #tooltip>
112+
<NFlex vertical size="small" align="center" justify="center">
113+
<div>{{ i18n('list_de_sync', 'common', 'tooltip') }}</div>
114+
<div>{{ i18n('list_click_to_refresh', 'common', 'tooltip') }}</div>
115+
</NFlex>
116+
</template>
108117
{{ i18n(`label__list__${ activeLists?.length ? 'update' : 'add' }`) }}
109118
</PanelButtonProgress>
110119
</NFlex>

src/components/views/panel/PanelButtonProgress.vue

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import type {
1919
} from '~/models/list-scroll.model';
2020
2121
import ProgressTooltip from '~/components/common/tooltip/ProgressTooltip.vue';
22-
import { useI18n } from '~/utils';
2322
2423
const props = defineProps({
2524
select: {
@@ -76,8 +75,6 @@ const renderLabel = (option: SelectOption & { icon: Component }) => [
7675
option.label?.toString(),
7776
];
7877
79-
const i18n = useI18n('panel', 'buttons');
80-
8178
const root = ref();
8279
const trigger = ref();
8380
</script>
@@ -101,6 +98,9 @@ const trigger = ref();
10198
}"
10299
v-bind="tooltip"
103100
>
101+
<template v-if="$slots.tooltip" #label>
102+
<slot name="tooltip" />
103+
</template>
104104
<NPopselect
105105
:style="{
106106
'--custom-bg-color': `var(--bg-color-${type}-80)`,

src/components/views/panel/ShowPanel.vue

+19
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import ShowPanelDetails from '~/components/views/panel/ShowPanelDetails.vue';
1515
import ShowPanelOverview from '~/components/views/panel/ShowPanelOverview.vue';
1616
import ShowPanelPicker from '~/components/views/panel/ShowPanelPicker.vue';
1717
import { ResolveExternalLinks } from '~/settings/external.links';
18+
import { useListsStoreRefs, useListStore } from '~/stores/data/list.store';
1819
import { type ShowSeasons, useShowStore } from '~/stores/data/show.store';
1920
import { useExtensionSettingsStore } from '~/stores/settings/extension.store';
2021
import { useI18n } from '~/utils';
@@ -132,6 +133,21 @@ const collectionProgressEntity = computed(() => {
132133
return collectionProgress.value;
133134
});
134135
136+
const { lists } = useListsStoreRefs();
137+
const { isListLoading, isItemInList } = useListStore();
138+
139+
const listLoading = computed(() => {
140+
if (!showId?.value) return;
141+
return isListLoading(showId.value).value;
142+
});
143+
144+
const activeLists = computed(() => {
145+
if (!showId?.value) return;
146+
return lists.value
147+
?.filter(list => isItemInList(list.id, showId.value).value)
148+
.map(list => list.id);
149+
});
150+
135151
const title = computed(() => {
136152
if (!show.value?.title) return;
137153
return deCapitalise(show.value.title);
@@ -245,13 +261,16 @@ const { openTab } = useExtensionSettingsStore();
245261
:watched-loading="watchedLoading"
246262
:collection-progress="collectionProgressEntity"
247263
:collection-loading="collectionLoading"
264+
:active-loading="listLoading"
265+
:active-lists="activeLists"
248266
/>
249267

250268
<ShowPanelPicker
251269
:mode="panelType"
252270
:seasons="seasons"
253271
:episodes="episodes"
254272
:progress="watchedProgress"
273+
:collection="collectionProgress"
255274
/>
256275

257276
<ShowPanelOverview

src/components/views/panel/ShowPanelButtons.vue

+9
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,20 @@ onMounted(() => {
143143
multiple: true,
144144
scrollable: true,
145145
}"
146+
:tooltip="{
147+
delay: 500,
148+
}"
146149
:icon="activeLists?.length ? IconCheckedList : IconListEmpty"
147150
:filled="!!activeLists?.length"
148151
:disabled="listsLoading"
149152
type="warning"
150153
>
154+
<template #tooltip>
155+
<NFlex vertical size="small" align="center" justify="center">
156+
<div>{{ i18n('list_de_sync', 'common', 'tooltip') }}</div>
157+
<div>{{ i18n('list_click_to_refresh', 'common', 'tooltip') }}</div>
158+
</NFlex>
159+
</template>
151160
{{ i18n(`label__list__${ activeLists?.length ? 'update' : 'add' }`) }}
152161
</PanelButtonProgress>
153162
</NFlex>

src/components/views/panel/ShowPanelPicker.vue

+13-5
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ const props = defineProps({
3030
type: Object as PropType<ShowProgress>,
3131
required: false,
3232
},
33+
collection: {
34+
type: Object as PropType<ShowProgress>,
35+
required: false,
36+
},
3337
});
3438
35-
const { seasons, episodes, progress } = toRefs(props);
39+
const { seasons, episodes, progress, collection } = toRefs(props);
3640
3741
const { meta } = useRoute();
3842
@@ -44,6 +48,7 @@ const seasonsLinks = computed(() => {
4448
number,
4549
link: { name: `${meta.base}-season`, params: { seasonNumber: _number } },
4650
finished: progress?.value?.seasons?.find(s => s.number === number)?.finished,
51+
collected: collection?.value?.seasons?.find(s => s.number === number)?.finished,
4752
};
4853
});
4954
});
@@ -60,6 +65,9 @@ const episodeLinks = computed(() => {
6065
finished: progress?.value?.seasons
6166
?.find(s => s.number === _episode.season)
6267
?.episodes?.find(e => e.number === _episode.number)?.completed,
68+
collected: collection?.value?.seasons
69+
?.find(s => s.number === _episode.season)
70+
?.episodes?.find(e => e.number === _episode.number)?.completed,
6371
}));
6472
});
6573
@@ -74,10 +82,10 @@ const i18n = useI18n('panel', 'picker');
7482
<NFlex class="numbers" size="small">
7583
<template v-if="seasonsLinks?.length">
7684
<ButtonLink
77-
v-for="{ link, number, finished } in seasonsLinks"
85+
v-for="{ link, number, finished, collected } in seasonsLinks"
7886
:key="`season-${number}`"
7987
:link="{ to: link }"
80-
:button="{ type: finished ? 'primary' : undefined }"
88+
:button="{ type: finished ? 'primary' : collected ? 'info' : undefined }"
8189
>
8290
{{ number }}
8391
</ButtonLink>
@@ -93,11 +101,11 @@ const i18n = useI18n('panel', 'picker');
93101
<NFlex class="numbers episodes" size="small">
94102
<template v-if="episodeLinks?.length">
95103
<ButtonLink
96-
v-for="{ link, number, finished } in episodeLinks"
104+
v-for="{ link, number, finished, collected } in episodeLinks"
97105
:key="`episode-${ number }`"
98106
v-slot="{ isActive }"
99107
:link="{ to: link }"
100-
:button="{ type: finished ? 'primary' : undefined }"
108+
:button="{ type: finished ? 'primary' : collected ? 'info' : undefined }"
101109
class="link"
102110
>
103111
<span class="label" :class="{ active: isActive }">{{ number }}</span>

src/i18n/en/common/tooltip.json

+8
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,13 @@
3434
"common__tooltip__open_season_in_trakt": {
3535
"message": "Open season in Trakt.tv",
3636
"description": "Label for opening a trakt.tv season link in a new tab."
37+
},
38+
"common__tooltip__list_de_sync": {
39+
"message": "List state might be de-synchronised.",
40+
"description": "Warning message that the list state might be de-synchronised."
41+
},
42+
"common__tooltip__list_click_to_refresh": {
43+
"message": "Click un-checked list to refresh.",
44+
"description": "Label for a link that refreshes the list state."
3745
}
3846
}

src/stores/data/list.store.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineStore, storeToRefs } from 'pinia';
2-
import { ref, watch } from 'vue';
2+
import { computed, reactive, ref, watch } from 'vue';
33

44
import type { TraktClientPagination } from '~/models/trakt/trakt-client.model';
55

@@ -168,6 +168,9 @@ export const useListsStore = defineStore('data.lists', () => {
168168

169169
export const useListsStoreRefs = () => storeToRefs(useListsStore());
170170

171+
type ListDictionary = Record<string, Record<string, boolean>>;
172+
type ListDictionaryLoading = Record<string, boolean>;
173+
171174
export const useListStore = defineStore('data.list', () => {
172175
const firstLoad = ref(true);
173176
const loading = ref(true);
@@ -178,6 +181,9 @@ export const useListStore = defineStore('data.list', () => {
178181
const searchList = ref('');
179182
const threshold = ref(10);
180183

184+
const listDictionary = reactive<ListDictionary>({});
185+
const listDictionaryLoading = reactive<ListDictionaryLoading>({});
186+
181187
const saveState = async () => storage.local.set('data.list.page-size', pageSize.value);
182188
const restoreState = async () => {
183189
const restored = await storage.local.get<number>('data.list.page-size');
@@ -189,6 +195,23 @@ export const useListStore = defineStore('data.list', () => {
189195
listItems.value = [];
190196
pagination.value = undefined;
191197
searchList.value = '';
198+
199+
Object.assign(listDictionary, {});
200+
Object.assign(listDictionaryLoading, {});
201+
};
202+
203+
const addToDictionary = (list: ListEntity, item: AnyList) => {
204+
if (![ListType.List, ListType.Watchlist].map(String).includes(list.type)) return;
205+
if (!listDictionary[list.id]) listDictionary[list.id] = {};
206+
if ('movie' in item && item.movie?.ids?.trakt) {
207+
listDictionary[list.id][item.movie.ids.trakt] = true;
208+
} else if ('show' in item && item.show?.ids?.trakt) {
209+
listDictionary[list.id][item.show.ids.trakt] = true;
210+
} else if ('season' in item && item.season?.ids?.trakt) {
211+
listDictionary[list.id][item.season.ids.trakt] = true;
212+
} else if ('episode' in item && item.episode?.ids?.trakt) {
213+
listDictionary[list.id][item.episode.ids.trakt] = true;
214+
}
192215
};
193216

194217
const { activeList } = useListsStoreRefs();
@@ -212,6 +235,7 @@ export const useListStore = defineStore('data.list', () => {
212235

213236
console.info('Fetching List', list);
214237
loading.value = true;
238+
listDictionaryLoading[list.id] = true;
215239
const timeout = debounceLoading(listItems, loadingPlaceholder, !page);
216240
try {
217241
const query = {
@@ -238,6 +262,7 @@ export const useListStore = defineStore('data.list', () => {
238262
throw new Error('Invalid list type');
239263
}
240264
const newData = response.data.map((item, index) => {
265+
addToDictionary(list, item as AnyList);
241266
if ('id' in item) return item;
242267
return { ...item, id: `${page}-${index}` };
243268
});
@@ -251,9 +276,13 @@ export const useListStore = defineStore('data.list', () => {
251276
} finally {
252277
clearTimeout(timeout);
253278
loading.value = false;
279+
listDictionaryLoading[list.id] = false;
254280
}
255281
};
256282

283+
const isListLoading = (listId: ListEntity['id']) => computed(() => listDictionaryLoading[listId]);
284+
const isItemInList = (listId: ListEntity['id'], itemId: string | number) => computed(() => listDictionary[listId]?.[itemId]);
285+
257286
const initListStore = async () => {
258287
await restoreState();
259288

@@ -281,6 +310,8 @@ export const useListStore = defineStore('data.list', () => {
281310
fetchListItems,
282311
filteredListItems,
283312
initListStore,
313+
isListLoading,
314+
isItemInList,
284315
};
285316
});
286317

0 commit comments

Comments
 (0)