feat: basic mutli-accounts support

zio/stable
Anthony Fu 2022-11-23 11:48:01 +08:00
parent 24c573ccf0
commit 241b28241c
15 changed files with 170 additions and 34 deletions

View File

@ -1,10 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { Account } from 'masto'
const { link = true } = defineProps<{ const { account, link = true, fullServer = false } = defineProps<{
account: Account account: Account
link?: boolean link?: boolean
fullServer?: boolean
}>() }>()
const id = computed(() => fullServer && !account.acct.includes('@') ? `@${account.acct}@${account.url.match(UserLinkRE)?.[1]}` : `@${account.acct}`)
</script> </script>
<template> <template>
@ -17,7 +20,7 @@ const { link = true } = defineProps<{
<NuxtLink flex flex-col :to="link ? `/@${account.acct}` : null"> <NuxtLink flex flex-col :to="link ? `/@${account.acct}` : null">
<CommonRichContent font-bold :content="getDisplayName(account)" :emojis="account.emojis" /> <CommonRichContent font-bold :content="getDisplayName(account)" :emojis="account.emojis" />
<p op35 text-sm> <p op35 text-sm>
@{{ account.acct }} {{ id }}
</p> </p>
<slot name="bottom" /> <slot name="bottom" />
</NuxtLink> </NuxtLink>

View File

@ -1,15 +0,0 @@
<script setup lang="ts">
const account = $computed(() => currentUser.value?.account)
</script>
<template>
<div flex flex-col gap-4 p4>
<!-- TODO: multiple account switcher -->
<template v-if="account">
<AccountInfo :account="account" />
<PublishWidget draft-key="home" />
</template>
<!-- TODO: dialog for select server -->
<a v-else href="/api/mas.to/login" px2 py1 bg-teal6 text-white m2 rounded>Login</a>
</div>
</template>

View File

@ -4,25 +4,21 @@ import { DEFAULT_SERVER } from '~/constants'
const server = ref<string>() const server = ref<string>()
async function oauth() { async function oauth() {
const a = document.createElement('a') location.href = `/api/${server.value || DEFAULT_SERVER}/login`
a.href = `/api/${server.value || DEFAULT_SERVER}/login`
a.target = '_blank'
a.click()
} }
</script> </script>
<template> <template>
<div h-full text-center justify-center flex="~ col items-center gap2"> <div h-full text-center justify-center flex="~ col items-center gap2">
<div text-4xl mb-10> <div text-3xl mb2>
Nuxtodon Sign in
</div> </div>
<div>Mastodon Server Name</div>
<div>Mastodon Server</div>
<div flex bg-gray:10 px2 py1 mxa rounded border="~ border" w-80 text-xl items-center> <div flex bg-gray:10 px2 py1 mxa rounded border="~ border" w-80 text-xl items-center>
<span op35 mr1 text-sm>https://</span> <span op35 mr1 text-sm>https://</span>
<input v-model="server" :placeholder="DEFAULT_SERVER" outline-none bg-transparent> <input v-model="server" :placeholder="DEFAULT_SERVER" outline-none bg-transparent>
</div> </div>
<button btn-solid mxa @click="oauth()"> <button btn-solid mxa mt2 @click="oauth()">
Sign in Sign in
</button> </button>
</div> </div>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
const accounts = useAccounts()
</script>
<template>
<div flex flex-col gap-4 p4>
<AccountInfo :account="currentUser?.account" :link="false" @click="openAccountSwitcher" />
</div>
</template>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import { isAccountSwitcherOpen, isSigninDialogOpen } from '~/composables/dialog'
const accounts = useAccounts()
</script>
<template>
<ModalDrawer
v-model="isAccountSwitcherOpen"
>
<div max-w-60rem mxa p4>
<h1 text-2xl>
Switch Account
</h1>
<template v-for="acc of accounts" :key="acc.id">
<AccountInfo
:account="acc.account"
:link="false"
:full-server="true"
py4 border="b base"
@click="loginTo(acc)"
/>
</template>
<div py2 mx--2>
<button btn-text flex="~ gap-1" items-center @click="openSigninDialog">
<div i-ri:user-add-line />
Add another account
</button>
</div>
</div>
</ModalDrawer>
<ModalDialog v-model="isSigninDialogOpen">
<AccountSignIn m6 />
</ModalDialog>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang='ts'>
const { modelValue } = defineModel<{
modelValue: boolean
}>()
</script>
<template>
<div
class="fixed top-0 bottom-0 left-0 right-0 z-60"
:class="modelValue ? '' : 'pointer-events-none'"
>
<div
class="
bg-base bottom-0 left-0 right-0 top-0 absolute transition-opacity duration-500 ease-out
"
:class="modelValue ? 'opacity-85' : 'opacity-0'"
@click="modelValue = false"
/>
<div
class="
bg-base absolute transition-all duration-200 ease-out shadow rounded-md transform
border border-base left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2
"
:class="modelValue ? 'opacity-100' : 'opacity-0'"
>
<slot />
</div>
</div>
</template>

View File

@ -0,0 +1,61 @@
<script setup lang='ts'>
const {
direction = 'bottom',
} = defineProps<{
direction?: string
}>()
const { modelValue } = defineModel<{
modelValue: boolean
}>()
const positionClass = computed(() => {
switch (direction) {
case 'bottom':
return 'bottom-0 left-0 right-0 border-t'
case 'top':
return 'top-0 left-0 right-0 border-b'
case 'left':
return 'bottom-0 left-0 top-0 border-r'
case 'right':
return 'bottom-0 top-0 right-0 border-l'
default:
return ''
}
})
const transform = computed(() => {
switch (direction) {
case 'bottom':
return 'translateY(100%)'
case 'top':
return 'translateY(-100%)'
case 'left':
return 'translateX(-100%)'
case 'right':
return 'translateX(100%)'
default:
return ''
}
})
</script>
<template>
<div
fixed top-0 bottom-0 left-0 right-0 z-40
:class="modelValue ? '' : 'pointer-events-none'"
>
<div
bg-base bottom-0 left-0 right-0 top-0 absolute transition-opacity duration-500 ease-out
:class="modelValue ? 'opacity-85' : 'opacity-0'"
@click="modelValue = false"
/>
<div
bg-base border border-base absolute transition-all duration-200 ease-out
:class="positionClass"
:style="modelValue ? {} : { transform }"
>
<slot />
</div>
</div>
</template>

View File

@ -18,7 +18,9 @@ export const currentUser = computed<UserLogin | undefined>(() => {
export const currentServer = computed<string>(() => currentUser.value?.server || DEFAULT_SERVER) export const currentServer = computed<string>(() => currentUser.value?.server || DEFAULT_SERVER)
export async function loginCallback(user: UserLogin) { export const useAccounts = () => accounts
export async function loginTo(user: UserLogin) {
const existing = accounts.value.findIndex(u => u.server === user.server && u.token === user.token) const existing = accounts.value.findIndex(u => u.server === user.server && u.token === user.token)
if (existing !== -1) { if (existing !== -1) {
if (currentId.value === accounts.value[existing].account?.id) if (currentId.value === accounts.value[existing].account?.id)

View File

@ -8,9 +8,6 @@ import { RouterLink } from 'vue-router'
type Node = DefaultTreeAdapterMap['childNode'] type Node = DefaultTreeAdapterMap['childNode']
type Element = DefaultTreeAdapterMap['element'] type Element = DefaultTreeAdapterMap['element']
const UserLinkRE = /^https?:\/\/([^/]+)\/@([^/]+)$/
const TagLinkRE = /^https?:\/\/([^/]+)\/tags\/([^/]+)$/
export function defaultHandle(el: Element) { export function defaultHandle(el: Element) {
// Redirect mentions to the user page // Redirect mentions to the user page
if (el.tagName === 'a' && el.attrs.find(i => i.name === 'class' && i.value.includes('mention'))) { if (el.tagName === 'a' && el.attrs.find(i => i.name === 'class' && i.value.includes('mention'))) {

View File

@ -0,0 +1,11 @@
export const isAccountSwitcherOpen = ref(false)
export const isSigninDialogOpen = ref(false)
export function openAccountSwitcher() {
isAccountSwitcherOpen.value = true
}
export function openSigninDialog() {
isSigninDialogOpen.value = true
isAccountSwitcherOpen.value = false
}

View File

@ -1,5 +1,8 @@
import type { Emoji } from 'masto' import type { Emoji } from 'masto'
export const UserLinkRE = /^https?:\/\/([^/]+)\/@([^/]+)$/
export const TagLinkRE = /^https?:\/\/([^/]+)\/tags\/([^/]+)$/
export function getDataUrlFromArr(arr: Uint8ClampedArray, w: number, h: number) { export function getDataUrlFromArr(arr: Uint8ClampedArray, w: number, h: number) {
if (typeof w === 'undefined' || typeof h === 'undefined') if (typeof w === 'undefined' || typeof h === 'undefined')
w = h = Math.sqrt(arr.length / 4) w = h = Math.sqrt(arr.length / 4)

View File

@ -4,7 +4,10 @@
<div class="hidden md:block w-1/4" relative> <div class="hidden md:block w-1/4" relative>
<div sticky top-0 h-screen flex="~ col"> <div sticky top-0 h-screen flex="~ col">
<slot name="left"> <slot name="left">
<AccountMe v-if="currentUser" /> <template v-if="currentUser">
<AccountSwitcher />
<PublishWidget px4 draft-key="home" />
</template>
<AccountSignInEntry v-else /> <AccountSignInEntry v-else />
</slot> </slot>
</div> </div>
@ -23,5 +26,6 @@
</div> </div>
</div> </div>
</main> </main>
<ModalContainer />
</div> </div>
</template> </template>

View File

@ -6,7 +6,7 @@ definePageMeta({
const { query } = useRoute() const { query } = useRoute()
onMounted(async () => { onMounted(async () => {
await loginCallback(query as any) await loginTo(query as any)
await nextTick() await nextTick()
await nextTick() await nextTick()
location.pathname = '/' location.pathname = '/'

2
shim.d.ts vendored
View File

@ -1 +1 @@
/// <references type="unplugin-vue-macros/macros-global" /> /// <reference types="unplugin-vue-macros/macros-global" />

View File

@ -17,7 +17,8 @@ export default defineConfig({
'text-base': 'text-$c-text-base', 'text-base': 'text-$c-text-base',
'interact-disabled': 'disabled:opacity-50 disabled:pointer-events-none disabled:saturate-0', 'interact-disabled': 'disabled:opacity-50 disabled:pointer-events-none disabled:saturate-0',
'btn-solid': 'px-4 py-2 rounded text-white bg-$c-primary hover:bg-$c-primary-active interact-disabled', 'btn-solid': 'px-4 py-2 rounded text-white bg-$c-primary hover:bg-$c-primary-active interact-disabled',
'btn-outline': 'px-4 py-2 rounded text-$c-primary border-$c-primary hover:bg-$c-primary hover:text-white interact-disabled', 'btn-outline': 'px-4 py-2 rounded text-$c-primary border border-$c-primary hover:bg-$c-primary hover:text-white interact-disabled',
'btn-text': 'px-4 py-2 text-$c-primary hover:text-$c-primary-active interact-disabled',
}, },
], ],
presets: [ presets: [