Skip to content

Commit 8e00a0a

Browse files
committed
feat(list): adds quick action buttons - watched
1 parent a749993 commit 8e00a0a

23 files changed

+503
-291
lines changed

src/components/common/list/ListButton.vue

+5-5
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const onClick = (e: MouseEvent) => {
3636

3737
<style scoped lang="scss">
3838
.list-button {
39-
--color: var(--bg-black-80);
39+
--color: var(--bg-black-60);
4040
--progress: 100%;
4141
4242
display: flex;
@@ -63,13 +63,13 @@ const onClick = (e: MouseEvent) => {
6363
&:not(.n-button--disabled) {
6464
&:focus-visible,
6565
&:hover {
66-
--color: var(--bg-black-90);
67-
--progress: 70%;
66+
--color: var(--n-text-color);
67+
--progress: 600%;
6868
}
6969
7070
&:active {
71-
--color: var(--bg-black);
72-
--progress: 50%;
71+
--color: var(--n-text-color);
72+
--progress: 300%;
7373
}
7474
}
7575
}

src/components/common/list/ListCheckinButton.vue src/components/common/list/ListButtonCheckin.vue

+21-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
<script setup lang="ts">
22
import { computed } from 'vue';
33
4-
import type { ListScrollItem } from '~/models/list-scroll.model';
5-
64
import ListButton from '~/components/common/list/ListButton.vue';
5+
import IconCancel from '~/components/icons/IconCancel.vue';
76
import IconConfirmCircle from '~/components/icons/IconConfirmCircle.vue';
7+
import { isEpisodeOrMovie, type ListScrollItem } from '~/models/list-scroll.model';
8+
import { Logger } from '~/services/logger.service';
9+
import { NotificationService } from '~/services/notification.service';
10+
import { useCancelWatching } from '~/stores/composable/use-watching';
811
import { useWatchingStore, useWatchingStoreRefs } from '~/stores/data/watching.store';
912
import { useI18n } from '~/utils/i18n.utils';
10-
import { useCancelWatching } from '~/utils/watching.utils';
1113
1214
const { disabled, item } = defineProps<{
1315
disabled?: boolean;
@@ -25,12 +27,22 @@ const watching = computed(() => {
2527
const { cancel, checkin } = useCancelWatching();
2628
const onClick = async () => {
2729
if (!item?.type) return;
28-
if (!['movie', 'episode'].includes(item.type)) return;
29-
const type: 'movie' | 'episode' = item.type as 'movie' | 'episode';
30-
const ids = item.meta?.ids?.[type];
30+
if (!isEpisodeOrMovie(item.type)) return;
31+
const ids = item.meta?.ids?.[item.type];
3132
if (!ids?.trakt) return;
32-
if (watching.value) await cancel();
33-
else await checkin({ type, ids: { trakt: ids.trakt } });
33+
const query = {
34+
type: item.type,
35+
ids: { trakt: ids.trakt },
36+
showId: item.meta?.ids?.show?.trakt,
37+
};
38+
39+
try {
40+
if (watching.value) await cancel(query);
41+
else await checkin(query);
42+
} catch (error) {
43+
Logger.error('Failed to checkin', { query, error });
44+
NotificationService.error(i18n('checkin_failed', 'watching'), error);
45+
}
3446
};
3547
3648
const { loading } = useWatchingStoreRefs();
@@ -41,7 +53,7 @@ const { loading } = useWatchingStoreRefs();
4153
:disabled="loading || disabled"
4254
:loading="loading"
4355
:button-props="{ type: watching ? 'warning' : 'error' }"
44-
:icon-props="{ component: IconConfirmCircle }"
56+
:icon-props="{ component: watching ? IconCancel : IconConfirmCircle }"
4557
@on-click="onClick"
4658
>
4759
<span>{{ i18n(watching ? 'cancel' : 'checkin', 'common', 'button') }}</span>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<script setup lang="ts">
2+
import { computed, type PropType, toRefs } from 'vue';
3+
4+
import ListButton from '~/components/common/list/ListButton.vue';
5+
import IconPlay from '~/components/icons/IconPlay.vue';
6+
import IconRestore from '~/components/icons/IconRestore.vue';
7+
import { type ListScrollItem } from '~/models/list-scroll.model';
8+
import { isListItemType, ListType } from '~/models/list.model';
9+
import { useItemPlayed } from '~/stores/composable/use-item-played';
10+
import { useWatchedUpdates } from '~/stores/composable/use-list-update';
11+
import { useListStore } from '~/stores/data/list.store';
12+
import { useI18n } from '~/utils/i18n.utils';
13+
14+
const i18n = useI18n('list', 'watched');
15+
16+
const props = defineProps({
17+
item: {
18+
type: Object as PropType<ListScrollItem>,
19+
required: true,
20+
},
21+
disabled: {
22+
type: Boolean,
23+
required: false,
24+
},
25+
});
26+
27+
const { disabled, item } = toRefs(props);
28+
29+
const { played, date: datePlayed } = useItemPlayed(item);
30+
31+
const { isItemListLoading } = useListStore();
32+
33+
const isLoading = computed(() => {
34+
if (!item.value?.id) return;
35+
if (!isListItemType(item.value.type)) return;
36+
return isItemListLoading({
37+
listType: ListType.History,
38+
itemType: item.value.type,
39+
itemId: item.value?.id,
40+
}).value;
41+
});
42+
43+
const { addOrRemovePlayed } = useWatchedUpdates();
44+
45+
const onClick = () => {
46+
if (!isListItemType(item.value.type)) return;
47+
const trakt = item.value?.meta?.ids?.[item.value.type]?.trakt;
48+
if (!trakt) return;
49+
return addOrRemovePlayed({
50+
itemIds: { trakt },
51+
itemType: item.value.type,
52+
remove: played.value,
53+
showId: item.value?.meta?.ids?.show?.trakt,
54+
});
55+
};
56+
</script>
57+
58+
<template>
59+
<ListButton
60+
:disabled="isLoading || disabled"
61+
:loading="isLoading"
62+
:button-props="{ type: played ? 'error' : 'success' }"
63+
:icon-props="{ component: played ? IconRestore : IconPlay }"
64+
:title="i18n('watched', 'common', 'tooltip') + (datePlayed ? `: ${ datePlayed }` : '')"
65+
@on-click="onClick"
66+
>
67+
<span>{{ i18n(played ? 'remove' : 'watched', 'common', 'button') }}</span>
68+
</ListButton>
69+
</template>

src/components/common/list/ListItemPanel.vue

+8-126
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@ import ProgressTooltip from '~/components/common/tooltip/ProgressTooltip.vue';
2020
import IconGrid from '~/components/icons/IconGrid.vue';
2121
import IconPlayFilled from '~/components/icons/IconPlayFilled.vue';
2222
import { getCustomLinkIcon } from '~/models/link.model';
23-
import { type ListScrollItem, type ShowProgress } from '~/models/list-scroll.model';
23+
import { type ListScrollItem } from '~/models/list-scroll.model';
2424
2525
import { ProgressType } from '~/models/progress-type.model';
26-
import { useHistoryStore } from '~/stores/data/history.store';
27-
import { useMovieStore } from '~/stores/data/movie.store';
28-
import { useShowStore } from '~/stores/data/show.store';
26+
import { useItemCollected, useItemPlayed } from '~/stores/composable/use-item-played';
2927
import { useExtensionSettingsStoreRefs } from '~/stores/settings/extension.store';
3028
import { useLinksStore } from '~/stores/settings/links.store';
3129
import { useI18n } from '~/utils/i18n.utils';
@@ -113,127 +111,12 @@ const tags = computed(
113111
}),
114112
);
115113
116-
const { getShowWatchedProgress, getShowCollectionProgress } = useShowStore();
117-
118-
const progress = computed<ShowProgress | undefined>(() => {
119-
if (!showProgress.value && !showPlayed.value && !showCollected.value) return;
120-
if (item?.value?.progress) return item.value?.progress;
121-
if (item?.value?.progressRef) return item.value?.progressRef.value;
122-
if (!item?.value?.getProgressQuery) return;
123-
const { id, cacheOptions, noFetch } = item.value?.getProgressQuery() ?? {};
124-
if (!id) return;
125-
return getShowWatchedProgress(id, cacheOptions, noFetch).value;
126-
});
127-
128-
const collection = computed<ShowProgress | undefined>(() => {
129-
if (!showCollected.value) return;
130-
if (!item?.value?.getProgressQuery) return;
131-
const { id, noFetch } = item.value?.getProgressQuery() ?? {};
132-
if (!id) return;
133-
return getShowCollectionProgress(id, noFetch).value;
134-
});
135-
136-
const { getMovieWatched, getMovieCollected } = useMovieStore();
137-
const { getMovieHistory, getEpisodeHistory } = useHistoryStore();
138-
139-
const movieHistory = computed(() => {
140-
if (!showPlayed.value) return;
141-
const _item = item?.value;
142-
if (_item?.type !== 'movie') return;
143-
if (!_item?.meta?.ids?.movie?.trakt) return;
144-
return getMovieHistory(_item.meta.ids.movie.trakt)?.value;
145-
});
146-
147-
const episodeHistory = computed(() => {
148-
if (!showPlayed.value) return;
149-
const _item = item?.value;
150-
if (_item?.type !== 'episode') return;
151-
if (!_item?.meta?.ids?.episode?.trakt) return;
152-
return getEpisodeHistory(_item.meta.ids.episode.trakt)?.value;
153-
});
154-
155-
const episodeProgress = computed(() => {
156-
if (!showPlayed.value) return;
157-
const _item = item?.value;
158-
if (_item?.type !== 'episode') return;
159-
const _progress = progress.value;
160-
if (!_progress) return;
161-
const _season = _item.meta?.number?.season;
162-
const _episode = _item.meta?.number?.episode;
163-
if (!_season || !_episode) return;
164-
return _progress.seasons
165-
?.find(s => s.number === _season)
166-
?.episodes?.find(e => e.number === _episode);
167-
});
168-
169-
const movieWatched = computed(() => {
170-
if (!showPlayed.value) return;
171-
const _item = item?.value;
172-
if (_item?.type !== 'movie') return;
173-
if (!_item?.meta?.ids?.movie?.trakt) return;
174-
return getMovieWatched(_item.meta.ids.movie.trakt)?.value;
175-
});
176-
177-
const moviePlayed = computed(() => {
178-
if (!showPlayed.value) return;
179-
if (movieWatched.value !== undefined) return movieWatched.value?.last_watched_at;
180-
return movieHistory.value?.watched_at;
181-
});
182-
183-
const episodePlayed = computed(() => {
184-
if (!showPlayed.value) return;
185-
if (episodeProgress.value !== undefined) {
186-
return {
187-
date: episodeProgress.value?.date,
188-
completed: episodeProgress.value?.completed,
189-
};
190-
}
191-
return {
192-
date: episodeHistory.value?.watched_at,
193-
completed: !!episodeHistory.value,
194-
};
195-
});
196-
197-
const played = computed(() => {
198-
if (!showPlayed.value) return false;
199-
if (item?.value?.type === 'movie') return !!moviePlayed.value;
200-
if (item?.value?.type !== 'episode') return false;
201-
return episodePlayed.value?.completed;
202-
});
203-
204-
const playedDate = computed(() => {
205-
if (!played.value) return;
206-
if (item?.value?.type === 'movie') {
207-
if (!moviePlayed.value) return;
208-
return new Date(moviePlayed.value).toLocaleString();
209-
}
210-
if (!episodePlayed.value?.date) return;
211-
return new Date(episodePlayed.value?.date).toLocaleString();
212-
});
213-
214-
const collected = computed(() => {
215-
if (!showCollected.value) return false;
216-
const _item = item?.value;
217-
if (_item?.type === 'movie' && _item?.meta?.ids?.movie?.trakt) {
218-
return getMovieCollected(_item.meta.ids.movie.trakt)?.value?.collected_at;
219-
}
220-
if (_item?.type !== 'episode') return false;
221-
const _collection = collection.value;
222-
223-
if (!_collection) return false;
224-
const _season = _item.meta?.number?.season;
225-
const _episode = _item.meta?.number?.episode;
226-
if (!_season || !_episode) return false;
227-
return _collection.seasons
228-
?.find(s => s.number === _season)
229-
?.episodes?.find(e => e.number === _episode)?.date;
230-
});
231-
232-
const collectedDate = computed(() => {
233-
if (!collected.value) return;
234-
if (typeof collected.value !== 'string') return;
235-
return new Date(collected.value).toLocaleString();
236-
});
114+
const {
115+
progress,
116+
played,
117+
date: playedDate,
118+
} = useItemPlayed(item, { showPlayed, showProgress });
119+
const { collected, date: collectedDate } = useItemCollected(item, showCollected);
237120
238121
const { progressType } = useExtensionSettingsStoreRefs();
239122
@@ -461,7 +344,6 @@ const onTagClick = (url?: string) => {
461344
gap: 0.5rem !important;
462345
max-height: 3.325rem;
463346
margin-top: 0.3rem;
464-
overflow: scroll;
465347
466348
&.show-progress {
467349
max-height: 1.5rem;

src/components/common/list/ListScroll.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ const listPaddingBottom = computed(() => {
255255
:on-scroll="onScrollHandler"
256256
@vue:updated="onUpdatedHandler"
257257
>
258-
<template #default="{ item }">
258+
<template #default="{ item }: { item: ListScrollItem }">
259259
<slot v-if="item.type === ListScrollItemType.LoadMore" name="load-more">
260260
<NFlex
261261
:key="item.key"
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<template>
2+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
3+
<g
4+
fill="none"
5+
stroke="currentColor"
6+
stroke-linecap="round"
7+
stroke-linejoin="round"
8+
stroke-width="1.5"
9+
>
10+
<path
11+
stroke-dasharray="64"
12+
stroke-dashoffset="64"
13+
d="M3 12c0 -4.97 4.03 -9 9 -9c4.97 0 9 4.03 9 9c0 4.97 -4.03 9 -9 9c-4.97 0 -9 -4.03 -9 -9Z"
14+
>
15+
<animate
16+
fill="freeze"
17+
attributeName="stroke-dashoffset"
18+
dur="0.6s"
19+
values="64;0"
20+
/>
21+
</path>
22+
<path stroke-dasharray="12" stroke-dashoffset="12" d="M7 12h10">
23+
<animate
24+
fill="freeze"
25+
attributeName="stroke-dashoffset"
26+
begin="0.6s"
27+
dur="0.2s"
28+
values="12;0"
29+
/>
30+
</path>
31+
</g>
32+
</svg>
33+
</template>

src/components/icons/IconRemove.vue

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<template>
2+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
3+
<g
4+
fill="none"
5+
stroke="currentColor"
6+
stroke-dasharray="24"
7+
stroke-dashoffset="24"
8+
stroke-linecap="round"
9+
stroke-linejoin="round"
10+
stroke-width="1.5"
11+
>
12+
<path d="M5 5l14 14">
13+
<animate
14+
fill="freeze"
15+
attributeName="stroke-dashoffset"
16+
dur="0.4s"
17+
values="24;0"
18+
/>
19+
</path>
20+
<path d="M19 5l-14 14">
21+
<animate
22+
fill="freeze"
23+
attributeName="stroke-dashoffset"
24+
begin="0.4s"
25+
dur="0.4s"
26+
values="24;0"
27+
/>
28+
</path>
29+
</g>
30+
</svg>
31+
</template>

0 commit comments

Comments
 (0)