Skip to content

Commit a8c43d0

Browse files
committed
feat(list): adds show link for list items
1 parent d109336 commit a8c43d0

12 files changed

+111
-38
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
<script lang="ts" setup>
22
import { NH2 } from 'naive-ui';
3-
4-
import { type Component, type PropType, useAttrs } from 'vue';
3+
import { type Component, type PropType, toRefs, useAttrs } from 'vue';
54
65
import { useLinksStore } from '~/stores/settings/links.store';
76
8-
defineProps({
7+
const props = defineProps({
98
component: {
109
type: Object as PropType<Component>,
1110
required: false,
@@ -15,13 +14,26 @@ defineProps({
1514
type: String,
1615
required: false,
1716
},
17+
text: {
18+
type: Boolean,
19+
required: false,
20+
},
21+
onClick: {
22+
type: Function as PropType<
23+
(e: MouseEvent, link: { url?: string; label?: string }) => void
24+
>,
25+
required: false,
26+
},
1827
});
1928
29+
const { onClick, label } = toRefs(props);
30+
2031
const attrs = useAttrs() as Record<keyof HTMLAnchorElement, string> | undefined;
2132
2233
const { openTab } = useLinksStore();
2334
2435
const onTitleClick = (e: MouseEvent) => {
36+
if (onClick?.value) return onClick.value(e, { url: attrs?.href, label: label?.value });
2537
e.preventDefault();
2638
e.stopPropagation();
2739
openTab(attrs?.href);
@@ -30,15 +42,22 @@ const onTitleClick = (e: MouseEvent) => {
3042

3143
<template>
3244
<a
45+
v-if="$attrs?.href || onClick"
3346
class="anchor-link"
34-
:class="{ 'has-link': !!$attrs.href }"
3547
:title="label"
3648
@click="onTitleClick"
3749
>
38-
<component :is="component" class="content" :class="{ 'hover-link': !!$attrs.href }">
50+
<component :is="component" v-if="!text && component" class="content hover-link">
3951
<slot />
4052
</component>
53+
<slot v-else />
4154
</a>
55+
<template v-else>
56+
<component :is="component" v-if="!text && component" class="content">
57+
<slot />
58+
</component>
59+
<slot v-else />
60+
</template>
4261
</template>
4362

4463
<style lang="scss" scoped>
@@ -49,31 +68,34 @@ const onTitleClick = (e: MouseEvent) => {
4968
color: inherit;
5069
text-decoration: none;
5170
outline: none;
71+
cursor: pointer;
5272
transition: color 0.3s var(--n-bezier);
5373
will-change: color;
5474
55-
&:focus-visible:not(.has-link) {
56-
text-decoration: underline;
57-
text-underline-offset: 0.2rem;
58-
}
59-
6075
.hover-link {
6176
transition: color 0.3s var(--n-bezier);
6277
will-change: color;
6378
6479
&:active,
6580
&:focus-within,
66-
&:hover {
81+
&:hover,
82+
&:focus-visible {
6783
color: var(--trakt-red);
6884
}
6985
}
7086
71-
&:focus-visible.has-link .hover-link {
87+
&:hover,
88+
&:focus-visible .hover-link {
7289
color: var(--trakt-red);
7390
}
7491
7592
.content {
7693
margin: 0;
7794
}
95+
96+
.content:not(.hover-link) {
97+
text-decoration: underline;
98+
text-underline-offset: 0.2rem;
99+
}
78100
}
79101
</style>

src/components/common/list/ListItemPanel.vue

+15-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { computed, defineProps, type PropType, ref, toRefs } from 'vue';
1515
1616
import PosterPlaceholder from '~/assets/images/poster-placholder.webp';
17+
import AnchorLink from '~/components/common/buttons/AnchorLink.vue';
1718
import TagLink from '~/components/common/buttons/TagLink.vue';
1819
import ProgressTooltip from '~/components/common/tooltip/ProgressTooltip.vue';
1920
import IconGrid from '~/components/icons/IconGrid.vue';
@@ -255,20 +256,26 @@ const onTagClick = (url?: string) => {
255256
>
256257
<div ref="innerContainer">
257258
<div class="meta type">
258-
<NSkeleton v-if="loading" text style="width: 10%" round />
259-
<NEllipsis v-else :line-clamp="1" :tooltip="tooltipOptions">{{ type }}</NEllipsis>
259+
<NSkeleton v-if="loading" style="width: 10%" round />
260+
<AnchorLink v-else text v-bind="item?.typeLink">
261+
<NEllipsis :line-clamp="1" :tooltip="tooltipOptions">{{ type }}</NEllipsis>
262+
</AnchorLink>
260263
</div>
261264
<div class="title">
262265
<NSkeleton v-if="loading" text style="width: 70%" round />
263-
<NEllipsis v-else :line-clamp="2" :tooltip="tooltipOptions">{{
264-
title
265-
}}</NEllipsis>
266+
<AnchorLink v-else text v-bind="item?.titleLink">
267+
<NEllipsis :line-clamp="2" :tooltip="tooltipOptions">
268+
{{ title }}
269+
</NEllipsis>
270+
</AnchorLink>
266271
</div>
267272
<div class="content">
268273
<NSkeleton v-if="loading" text style="width: 60%" round />
269-
<NEllipsis v-else :line-clamp="contentHeight" :tooltip="tooltipOptions">{{
270-
content
271-
}}</NEllipsis>
274+
<AnchorLink v-else text v-bind="item?.contentLink">
275+
<NEllipsis :line-clamp="contentHeight" :tooltip="tooltipOptions">
276+
{{ content }}
277+
</NEllipsis>
278+
</AnchorLink>
272279
</div>
273280
<NFlex
274281
v-if="(!hideTime && date) || tags?.length || showCollected || showPlayed"

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

+15
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import type { ListScrollItem, ListScrollItemMeta, ListScrollItemTag, ListScrollS
88

99
import type { ImageQuery } from '~/stores/data/image.store';
1010

11+
import { usePanelItem } from '~/components/views/panel/use-panel-item';
1112
import { ListScrollItemType } from '~/models/list-scroll.model';
1213

14+
import { RouterService } from '~/services/router.service';
1315
import { ResolveExternalLinks } from '~/settings/external.links';
1416
import { useI18n } from '~/utils/i18n.utils';
1517

@@ -181,6 +183,18 @@ export const getTags = (item: Pick<ListScrollSourceItem, 'episode' | 'season'>,
181183
return tags;
182184
};
183185

186+
const getContentLink = (item: ListScrollSourceItem): ListScrollItem['contentLink'] => {
187+
if (!item?.type || !item.show?.ids?.trakt) return;
188+
if (![ListScrollItemType.show, ListScrollItemType.episode].map(String).includes(item.type)) return;
189+
return {
190+
onClick: event => {
191+
event.preventDefault();
192+
event.stopPropagation();
193+
usePanelItem(RouterService.router).onItemClick({ type: 'show', id: item.show!.ids.trakt });
194+
},
195+
};
196+
};
197+
184198
export const computeNewArray = <T extends ListScrollSourceItemWithDate<D>, D extends string | never = never>(
185199
array: T[],
186200
dateFn?: D | ((item: T) => T[D]),
@@ -196,6 +210,7 @@ export const computeNewArray = <T extends ListScrollSourceItemWithDate<D>, D ext
196210
if (!_item.type) _item.type = getType(item);
197211
if (!_item.title) _item.title = getTitle(item);
198212
if (!_item.content) _item.content = getContent(item);
213+
if (!_item.contentLink) _item.contentLink = getContentLink(_item);
199214
if (!_item.posterRef) _item.posterRef = ref<string>();
200215
if (!_item.getPosterQuery) _item.getPosterQuery = getPosterQuery(item, _item.type);
201216
if (!_item.getProgressQuery) _item.getProgressQuery = getProgressQuery(item);

src/components/views/panel/MoviePanel.vue

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { deCapitalise } from '@dvcol/common-utils/common/string';
33
import { NFlex, NSkeleton } from 'naive-ui';
44
import { computed, onMounted, toRefs, watch } from 'vue';
55
6-
import TitleLink from '~/components/common/buttons/TitleLink.vue';
6+
import AnchorLink from '~/components/common/buttons/AnchorLink.vue';
77
import MoviePanelButtons from '~/components/views/panel/MoviePanelButtons.vue';
88
import MoviePanelDetails from '~/components/views/panel/MoviePanelDetails.vue';
99
import MoviePanelOverview from '~/components/views/panel/MoviePanelOverview.vue';
@@ -269,14 +269,14 @@ onMounted(() => {
269269

270270
<template>
271271
<NFlex class="panel-container" justify="center" align="center" vertical>
272-
<TitleLink
272+
<AnchorLink
273273
v-if="title"
274274
class="show-title"
275275
:href="titleUrl"
276276
:title="i18n('open_in_trakt', 'common', 'tooltip')"
277277
>
278278
{{ title }}
279-
</TitleLink>
279+
</AnchorLink>
280280
<NSkeleton
281281
v-else
282282
class="show-title-skeleton"

src/components/views/panel/PanelOverview.vue

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import { NFlex, NH4, NSkeleton } from 'naive-ui';
33
4-
import TitleLink from '~/components/common/buttons/TitleLink.vue';
4+
import AnchorLink from '~/components/common/buttons/AnchorLink.vue';
55
66
defineProps({
77
title: {
@@ -25,9 +25,9 @@ defineProps({
2525

2626
<template>
2727
<NFlex justify="center" align="center" vertical class="overview">
28-
<TitleLink v-if="title" class="title" :href="url" :title="label" :component="NH4">
28+
<AnchorLink v-if="title" class="title" :href="url" :title="label" :component="NH4">
2929
{{ title }}
30-
</TitleLink>
30+
</AnchorLink>
3131
<NSkeleton v-else class="title-skeleton" style="width: var(--height-40-dvh)" round />
3232

3333
<div v-if="overview">{{ overview }}</div>

src/components/views/panel/PersonPanel.vue

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { computed, onMounted, onUnmounted, ref, toRefs, watch } from 'vue';
55
66
import type { TraktPersonExtended } from '@dvcol/trakt-http-client/models';
77
8-
import TitleLink from '~/components/common/buttons/TitleLink.vue';
8+
import AnchorLink from '~/components/common/buttons/AnchorLink.vue';
99
import PanelPoster from '~/components/views/panel/PanelPoster.vue';
1010
import PersonPanelDetails from '~/components/views/panel/PersonPanelDetails.vue';
1111
import PersonPanelOverview from '~/components/views/panel/PersonPanelOverview.vue';
@@ -65,14 +65,14 @@ const titleUrl = computed(() => {
6565

6666
<template>
6767
<NFlex class="panel-container" justify="center" align="center" vertical>
68-
<TitleLink
68+
<AnchorLink
6969
v-if="title"
7070
class="show-title"
7171
:href="titleUrl"
7272
:title="i18n('open_in_trakt', 'common', 'tooltip')"
7373
>
7474
{{ title }}
75-
</TitleLink>
75+
</AnchorLink>
7676
<NSkeleton
7777
v-else
7878
class="show-title-skeleton"

src/components/views/panel/ShowPanel.vue

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { NFlex, NSkeleton } from 'naive-ui';
44
55
import { computed, onMounted, toRefs, watch } from 'vue';
66
7-
import TitleLink from '~/components/common/buttons/TitleLink.vue';
7+
import AnchorLink from '~/components/common/buttons/AnchorLink.vue';
88
import PanelPoster from '~/components/views/panel/PanelPoster.vue';
99
import PanelShowStatistics from '~/components/views/panel/PanelShowStatistics.vue';
1010
import ShowPanelButtons from '~/components/views/panel/ShowPanelButtons.vue';
@@ -406,14 +406,14 @@ onMounted(() => {
406406

407407
<template>
408408
<NFlex class="panel-container" justify="center" align="center" vertical>
409-
<TitleLink
409+
<AnchorLink
410410
v-if="title"
411411
class="show-title"
412412
:href="titleUrl"
413413
:title="i18n('open_show_in_trakt', 'common', 'tooltip')"
414414
>
415415
{{ title }}
416-
</TitleLink>
416+
</AnchorLink>
417417
<NSkeleton
418418
v-else
419419
class="show-title-skeleton"

src/components/views/panel/use-panel-item.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { useRouter } from 'vue-router';
22

33
import type { ListScrollItem } from '~/models/list-scroll.model';
44

5-
export const usePanelItem = () => {
6-
const { push, currentRoute } = useRouter();
7-
5+
export const usePanelItem = ({ push, currentRoute } = useRouter()) => {
86
const onItemClick = ({
97
type,
108
id,

src/models/list-scroll.model.ts

+9
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,24 @@ export type ListScrollItemMeta<T = { [key: string]: unknown }> = {
103103
};
104104
} & T;
105105

106+
export type AnchorLinkUrl = {
107+
href?: string;
108+
label?: string;
109+
onClick?: (event: MouseEvent, link: Omit<AnchorLinkUrl, 'onClick'>) => void;
110+
};
111+
106112
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- meta is intentionally weakly typed
107113
export type ListScrollItem<T extends Record<string, any> = ListScrollItemMeta> = Omit<PosterItem, 'type'> & {
108114
id: string | number;
109115
index: number;
110116
key: string;
111117

112118
type?: (typeof ListScrollItemType)[keyof typeof ListScrollItemType];
119+
typeLink?: AnchorLinkUrl;
113120
title?: string;
121+
titleLink?: AnchorLinkUrl;
114122
content?: string;
123+
contentLink?: AnchorLinkUrl;
115124
tags?: ListScrollItemTag[];
116125

117126
progress?: ShowProgress;

src/router/create-router.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { chromeRuntimeId } from '@dvcol/web-extension-utils/chrome/runtime';
22
import { watch } from 'vue';
3+
34
import { createRouter as createVueRouter, createWebHashHistory } from 'vue-router';
45

6+
import type { Router } from 'vue-router';
7+
58
import { Route } from '~/models/router.model';
69
import { routes } from '~/router/routes';
710
import { useAppStateStoreRefs } from '~/stores/app-state.store';
@@ -10,7 +13,7 @@ import { useAuthSettingsStoreRefs } from '~/stores/settings/auth.store';
1013
import { useExtensionSettingsStore, useExtensionSettingsStoreRefs } from '~/stores/settings/extension.store';
1114

1215
export type RouterOptions = { baseName?: string; baseUrl?: string };
13-
export const createRouter = ({ baseName = '', baseUrl = import.meta.env.BASE_URL }: RouterOptions) => {
16+
export const createRouter = ({ baseName = '', baseUrl = import.meta.env.BASE_URL }: RouterOptions): Router => {
1417
const { restoreRoute, restorePanel, defaultTab } = useExtensionSettingsStoreRefs();
1518
const { initExtensionSettingsStore } = useExtensionSettingsStore();
1619

src/services/router.service.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Router } from 'vue-router';
2+
import type { RouterOptions } from '~/router';
3+
4+
import { createRouter } from '~/router';
5+
6+
export class RouterService {
7+
private static instance: Router;
8+
9+
static get router() {
10+
if (!RouterService.instance) throw new Error('Router not initialized');
11+
return RouterService.instance;
12+
}
13+
14+
static init(options: RouterOptions) {
15+
if (RouterService.instance) return this.instance;
16+
RouterService.instance = createRouter(options);
17+
return RouterService.instance;
18+
}
19+
}

src/web/init-vue-app.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { App, Component } from 'vue';
66

77
import type { RouterOptions } from '~/router';
88

9-
import { createRouter } from '~/router';
9+
import { RouterService } from '~/services/router.service';
1010
import { initServices } from '~/web/init-services';
1111

1212
export type InitVueAppOption = RouterOptions & { view?: { option?: boolean; popup?: boolean; web?: boolean } };
@@ -19,7 +19,7 @@ export const initVueApp = (component: Component, options: InitVueAppOption = {})
1919
app.use(pinia);
2020

2121
let router = getCurrentInstance()?.appContext?.config?.globalProperties?.$router;
22-
if (!router) router = createRouter(options);
22+
if (!router) router = RouterService.init(options);
2323
app.use(router);
2424

2525
initServices(options.view).catch(error => console.error('Failed to initialized services.', error));

0 commit comments

Comments
 (0)