Skip to content

Commit 020686f

Browse files
committed
feat(account): support multiple accounts logout/login
1 parent 44e18e0 commit 020686f

19 files changed

+680
-121
lines changed

src/components/AppComponent.vue

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import { Transition } from 'vue';
33
import { RouterView } from 'vue-router';
44
55
import { NavbarComponent } from '~/components/common';
6-
import { useAuthSettingsStoreRefs } from '~/stores/settings.store';
6+
7+
import { useAuthSettingsStoreRefs } from '~/stores/settings/auth.store';
78
89
const { isAuthenticated } = useAuthSettingsStoreRefs();
910
</script>
1011

1112
<template>
12-
<header v-if="isAuthenticated">
13-
<NavbarComponent />
13+
<header>
14+
<NavbarComponent v-if="isAuthenticated" />
1415
</header>
1516
<main>
1617
<RouterView v-slot="{ Component, route }">
@@ -45,6 +46,7 @@ main {
4546
display: flex;
4647
align-items: center;
4748
justify-content: center;
49+
height: calc(100vh - 2.75rem);
4850
padding: 0 2rem;
4951
}
5052

src/components/common/navbar/NavbarSettingsDopdown.vue

+109-16
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,41 @@
11
<script setup lang="ts">
22
import { NAvatar, NDropdown, NEllipsis, NIcon, NSpace } from 'naive-ui';
3+
34
import { computed, defineProps, h } from 'vue';
45
6+
import { useRouter } from 'vue-router';
7+
8+
import type { DropdownProps } from 'naive-ui';
9+
import type { Component } from 'vue';
10+
11+
import type { ArrayElement } from '~/utils/typescript.utils';
12+
513
import IconAccount from '~/components/icons/IconAccount.vue';
14+
import IconAccountAdd from '~/components/icons/IconAccountAdd.vue';
15+
import IconCog from '~/components/icons/IconCog.vue';
16+
import IconExternalLink from '~/components/icons/IconExternalLink.vue';
617
import IconLogOut from '~/components/icons/IconLogOut.vue';
718
819
import { Route } from '~/router';
9-
import { useUserSettingsStoreRefs } from '~/stores/settings.store';
20+
import { TraktService } from '~/services/trakt.service';
21+
import { ExternaLinks } from '~/settings/external.links';
22+
import { useAuthSettingsStore } from '~/stores/settings/auth.store';
23+
import {
24+
defaultUser,
25+
useUserSettingsStore,
26+
useUserSettingsStoreRefs,
27+
} from '~/stores/settings/user.store';
1028
import { Blurs, Colors } from '~/styles/colors.style';
1129
import { useI18n } from '~/utils';
1230
13-
const i18n = useI18n('route');
31+
import { createTab } from '~/utils/browser/browser.utils';
1432
15-
const { userSetting } = useUserSettingsStoreRefs();
33+
const i18n = useI18n('navbar_settings');
34+
const router = useRouter();
35+
36+
const { user, userSetting, userSettings } = useUserSettingsStoreRefs();
37+
const { setCurrentUser } = useUserSettingsStore();
38+
const { syncRestoreAuth } = useAuthSettingsStore();
1639
1740
const avatar = computed(() => userSetting.value?.user?.images?.avatar?.full);
1841
const username = computed(() => userSetting.value?.user?.username);
@@ -24,35 +47,105 @@ defineProps({
2447
},
2548
});
2649
27-
const options = [
28-
{
29-
label: 'Logout',
30-
key: 'logout',
31-
icon: () =>
32-
h(NIcon, null, {
33-
default: () => h(IconLogOut, { size: '1.5em' }),
34-
}),
35-
},
50+
const toOption = (
51+
key: string,
52+
icon: Component | string,
53+
label?: string,
54+
): ArrayElement<DropdownProps['options']> => {
55+
return {
56+
label: label ?? i18n(key),
57+
key,
58+
icon: () => {
59+
if (typeof icon === 'string')
60+
return h(NAvatar, { src: icon, size: 16, round: true });
61+
return h(NIcon, null, { default: () => h(icon) });
62+
},
63+
};
64+
};
65+
66+
const baseOptions: DropdownProps['options'] = [
67+
{ type: 'divider', key: 'account-divider' },
68+
toOption('settings', IconCog),
69+
{ type: 'divider', key: 'external-links' },
70+
toOption('trakt', IconExternalLink),
71+
{ type: 'divider', key: 'session-divider' },
72+
toOption('login', IconAccountAdd),
73+
toOption('logout', IconLogOut),
3674
];
75+
76+
const users = computed(() => {
77+
return Object.entries(userSettings.value).filter(
78+
([key, value]) => value && key !== username.value && key !== defaultUser,
79+
);
80+
});
81+
82+
const options = computed<DropdownProps['options']>(() => {
83+
if (users.value.length) {
84+
return [
85+
...users.value.map(([key, value]) =>
86+
toOption(`user-${key}`, value?.user?.images?.avatar?.full || IconAccount, key),
87+
),
88+
{ type: 'divider', key: 'users-divider' },
89+
...baseOptions,
90+
];
91+
}
92+
93+
return baseOptions;
94+
});
95+
96+
const login = async () => {
97+
const response = await TraktService.approve();
98+
return createTab({ url: response.url });
99+
};
100+
101+
const loadUser = async (account: string) => {
102+
const auth = await syncRestoreAuth(account);
103+
return TraktService.importAuthentication(auth);
104+
};
105+
106+
const onSelect: DropdownProps['onSelect'] = async (key: string, { label }) => {
107+
console.info('Selected:', key);
108+
switch (key) {
109+
case 'settings':
110+
return router.push(Route.Settings);
111+
case 'trakt':
112+
return createTab({
113+
url: ExternaLinks.trakt[TraktService.isStaging ? 'staging' : 'production'],
114+
});
115+
case 'login':
116+
return login();
117+
case 'logout':
118+
await TraktService.logout();
119+
await setCurrentUser();
120+
if (user.value !== defaultUser) return loadUser(user.value);
121+
return router.push(Route.Login);
122+
default:
123+
if (typeof label === 'string' && key.startsWith('user-')) {
124+
return loadUser(label);
125+
}
126+
console.error('Unknown key:', key);
127+
}
128+
};
37129
</script>
38130

39131
<template>
40132
<NDropdown
41133
trigger="hover"
42134
:options="options"
43135
:to="parentElement"
44-
placement="bottom-end"
136+
placement="bottom"
45137
size="small"
46138
class="dropdown"
47139
:style="{
48140
'margin-top': '0.75rem',
49141
'margin-right': '-0.25rem',
50142
'text-align': 'left',
51-
'min-width': 'calc(100vw / 6 - 3px)',
143+
'min-width': 'max(calc(100vw / 6), 9rem)',
144+
'max-width': '20rem',
52145
'background-color': Colors.bgBlurBlackHover,
53146
'backdrop-filter': Blurs.blur,
54147
}"
55-
@select="(...args: any[]) => console.info(args)"
148+
@select="onSelect"
56149
>
57150
<NSpace justify="space-around" align="center" :wrap="false">
58151
<NEllipsis
@@ -76,7 +169,7 @@ const options = [
76169
round
77170
size="small"
78171
color="transparent"
79-
style="position: absolute; top: 3px; right: 0.5rem"
172+
style="position: absolute; top: 0.125rem; right: 0.5rem; scale: 0.8"
80173
/>
81174
<NIcon v-else style="position: absolute; top: 0.4rem; right: 0.75rem" size="1.5em">
82175
<IconAccount />
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<template>
2+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
3+
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2">
4+
<path
5+
stroke-dasharray="20"
6+
stroke-dashoffset="20"
7+
d="M3 21V20C3 17.7909 4.79086 16 7 16H11C13.2091 16 15 17.7909 15 20V21"
8+
>
9+
<animate
10+
fill="freeze"
11+
attributeName="stroke-dashoffset"
12+
dur="0.4s"
13+
values="20;0"
14+
/>
15+
</path>
16+
<path
17+
stroke-dasharray="20"
18+
stroke-dashoffset="20"
19+
d="M9 13C7.34315 13 6 11.6569 6 10C6 8.34315 7.34315 7 9 7C10.6569 7 12 8.34315 12 10C12 11.6569 10.6569 13 9 13Z"
20+
>
21+
<animate
22+
fill="freeze"
23+
attributeName="stroke-dashoffset"
24+
begin="0.5s"
25+
dur="0.4s"
26+
values="20;0"
27+
/>
28+
</path>
29+
<path stroke-dasharray="8" stroke-dashoffset="8" d="M15 6H21">
30+
<animate
31+
fill="freeze"
32+
attributeName="stroke-dashoffset"
33+
begin="1s"
34+
dur="0.2s"
35+
values="8;0"
36+
/>
37+
</path>
38+
<path stroke-dasharray="8" stroke-dashoffset="8" d="M18 3V9">
39+
<animate
40+
fill="freeze"
41+
attributeName="stroke-dashoffset"
42+
begin="1.2s"
43+
dur="0.2s"
44+
values="8;0"
45+
/>
46+
</path>
47+
</g>
48+
</svg>
49+
</template>

src/components/icons/IconCog.vue

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<template>
2+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
3+
<defs>
4+
<symbol id="lineMdCogLoop0">
5+
<path
6+
fill="none"
7+
stroke-width="2"
8+
d="M15.24 6.37C15.65 6.6 16.04 6.88 16.38 7.2C16.6 7.4 16.8 7.61 16.99 7.83C17.46 8.4 17.85 9.05 18.11 9.77C18.2 10.03 18.28 10.31 18.35 10.59C18.45 11.04 18.5 11.52 18.5 12"
9+
>
10+
<animate
11+
fill="freeze"
12+
attributeName="d"
13+
begin="0.8s"
14+
dur="0.2s"
15+
values="M15.24 6.37C15.65 6.6 16.04 6.88 16.38 7.2C16.6 7.4 16.8 7.61 16.99 7.83C17.46 8.4 17.85 9.05 18.11 9.77C18.2 10.03 18.28 10.31 18.35 10.59C18.45 11.04 18.5 11.52 18.5 12;M15.24 6.37C15.65 6.6 16.04 6.88 16.38 7.2C16.38 7.2 19 6.12 19.01 6.14C19.01 6.14 20.57 8.84 20.57 8.84C20.58 8.87 18.35 10.59 18.35 10.59C18.45 11.04 18.5 11.52 18.5 12"
16+
/>
17+
</path>
18+
</symbol>
19+
</defs>
20+
<g fill="none" stroke="currentColor" stroke-width="2">
21+
<g stroke-linecap="round" stroke-linejoin="round">
22+
<path
23+
stroke-dasharray="42"
24+
stroke-dashoffset="42"
25+
d="M12 5.5C15.59 5.5 18.5 8.41 18.5 12C18.5 15.59 15.59 18.5 12 18.5C8.41 18.5 5.5 15.59 5.5 12C5.5 8.41 8.41 5.5 12 5.5z"
26+
opacity="0"
27+
>
28+
<animate
29+
fill="freeze"
30+
attributeName="stroke-dashoffset"
31+
begin="0.2s"
32+
dur="0.5s"
33+
values="42;0"
34+
/>
35+
<set attributeName="opacity" begin="0.2s" to="1" />
36+
<set attributeName="opacity" begin="0.7s" to="0" />
37+
</path>
38+
<path
39+
stroke-dasharray="20"
40+
stroke-dashoffset="20"
41+
d="M12 9C13.66 9 15 10.34 15 12C15 13.66 13.66 15 12 15C10.34 15 9 13.66 9 12C9 10.34 10.34 9 12 9z"
42+
>
43+
<animate
44+
fill="freeze"
45+
attributeName="stroke-dashoffset"
46+
dur="0.2s"
47+
values="20;0"
48+
/>
49+
</path>
50+
</g>
51+
<g opacity="0">
52+
<use href="#lineMdCogLoop0" />
53+
<use href="#lineMdCogLoop0" transform="rotate(60 12 12)" />
54+
<use href="#lineMdCogLoop0" transform="rotate(120 12 12)" />
55+
<use href="#lineMdCogLoop0" transform="rotate(180 12 12)" />
56+
<use href="#lineMdCogLoop0" transform="rotate(240 12 12)" />
57+
<use href="#lineMdCogLoop0" transform="rotate(300 12 12)" />
58+
<set attributeName="opacity" begin="0.7s" to="1" />
59+
<animateTransform
60+
attributeName="transform"
61+
dur="30s"
62+
repeatCount="indefinite"
63+
type="rotate"
64+
values="0 12 12;360 12 12"
65+
/>
66+
</g>
67+
</g>
68+
</svg>
69+
</template>
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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="2"
9+
>
10+
<path stroke-dasharray="42" stroke-dashoffset="42" d="M11 5H5V19H19V13">
11+
<animate
12+
fill="freeze"
13+
attributeName="stroke-dashoffset"
14+
dur="0.6s"
15+
values="42;0"
16+
/>
17+
</path>
18+
<path stroke-dasharray="12" stroke-dashoffset="12" d="M13 11L20 4">
19+
<animate
20+
fill="freeze"
21+
attributeName="stroke-dashoffset"
22+
begin="0.6s"
23+
dur="0.3s"
24+
values="12;0"
25+
/>
26+
</path>
27+
<path stroke-dasharray="8" stroke-dashoffset="8" d="M21 3H15M21 3V9">
28+
<animate
29+
fill="freeze"
30+
attributeName="stroke-dashoffset"
31+
begin="0.9s"
32+
dur="0.2s"
33+
values="8;0"
34+
/>
35+
</path>
36+
</g>
37+
</svg>
38+
</template>

0 commit comments

Comments
 (0)