1
0

feat: bump to latest vue 3.4.19 (#2607)

Co-authored-by: patak <matias.capeletto@gmail.com>
This commit is contained in:
Joaquín Sánchez 2024-02-24 13:24:21 +01:00 committed by GitHub
parent 81ef8ff9aa
commit 36004a7eba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 601 additions and 451 deletions

View File

@ -1,5 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
import { fetchAccountByHandle } from '~/composables/cache'
type WatcherType = [acc?: mastodon.v1.Account, h?: string, v?: boolean]
defineOptions({ defineOptions({
inheritAttrs: false, inheritAttrs: false,
@ -11,16 +14,55 @@ const props = defineProps<{
disabled?: boolean disabled?: boolean
}>() }>()
const account = computed(() => props.account || (props.handle ? useAccountByHandle(props.handle!) : undefined)) const hoverCard = ref()
const targetIsVisible = ref(false)
const account = ref<mastodon.v1.Account | null | undefined>(props.account)
useIntersectionObserver(
hoverCard,
([{ intersectionRatio }]) => {
targetIsVisible.value = intersectionRatio <= 0.75
},
)
watch(
() => [props.account, props.handle, targetIsVisible.value] satisfies WatcherType,
([newAccount, newHandle, newVisible], oldProps) => {
if (newAccount) {
account.value = newAccount
return
}
if (!newVisible)
return
if (newHandle) {
const [_oldAccount, oldHandle, _oldVisible] = oldProps ?? [undefined, undefined, false]
if (!oldHandle || newHandle !== oldHandle || !account.value) {
// new handle can be wrong: using server instead of webDomain
fetchAccountByHandle(newHandle).then((acc) => {
if (newHandle === props.handle)
account.value = acc
})
}
return
}
account.value = undefined
}, { immediate: true, flush: 'post' },
)
const userSettings = useUserSettings() const userSettings = useUserSettings()
</script> </script>
<template> <template>
<VMenu v-if="!disabled && account && !getPreferences(userSettings, 'hideAccountHoverCard')" placement="bottom-start" :delay="{ show: 500, hide: 100 }" v-bind="$attrs" :close-on-content-click="false"> <span ref="hoverCard">
<slot /> <VMenu v-if="!disabled && account && !getPreferences(userSettings, 'hideAccountHoverCard')" placement="bottom-start" :delay="{ show: 500, hide: 100 }" v-bind="$attrs" :close-on-content-click="false">
<template #popper> <slot />
<AccountHoverCard v-if="account" :account="account" /> <template #popper>
</template> <AccountHoverCard v-if="account" :account="account" />
</VMenu> </template>
<slot v-else /> </VMenu>
<slot v-else />
</span>
</template> </template>

View File

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CommonRouteTabOption } from '../common/CommonRouteTabs.vue' import type { CommonRouteTabOption } from '~/types'
const { t } = useI18n() const { t } = useI18n()
const route = useRoute() const route = useRoute()
const server = computedEager(() => route.params.server as string) const server = computed(() => route.params.server as string)
const account = computedEager(() => route.params.account as string) const account = computed(() => route.params.account as string)
const tabs = computed<CommonRouteTabOption[]>(() => [ const tabs = computed<CommonRouteTabOption[]>(() => [
{ {

View File

@ -94,8 +94,8 @@ defineExpose({ createEntry, removeEntry, updateEntry })
</template> </template>
<template v-else> <template v-else>
<slot <slot
v-for="item, index of items" v-for="(item, index) of items"
v-bind="{ key: item[keyProp as keyof U] }" v-bind="{ key: (item as U)[keyProp as keyof U] }"
:item="item as U" :item="item as U"
:older="items[index + 1] as U" :older="items[index + 1] as U"
:newer="items[index - 1] as U" :newer="items[index - 1] as U"

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { RouteLocationRaw } from 'vue-router' import type { CommonRouteTabMoreOption, CommonRouteTabOption } from '~/types'
const { options, command, replace, preventScrollTop = false, moreOptions } = defineProps<{ const { options, command, replace, preventScrollTop = false, moreOptions } = defineProps<{
options: CommonRouteTabOption[] options: CommonRouteTabOption[]
@ -10,22 +10,6 @@ const { options, command, replace, preventScrollTop = false, moreOptions } = def
}>() }>()
const { t } = useI18n() const { t } = useI18n()
export interface CommonRouteTabOption {
to: RouteLocationRaw
display: string
disabled?: boolean
name?: string
icon?: string
hide?: boolean
match?: boolean
}
export interface CommonRouteTabMoreOption {
options: CommonRouteTabOption[]
icon?: string
tooltip?: string
match?: boolean
}
const router = useRouter() const router = useRouter()
useCommands(() => command useCommands(() => command
@ -60,7 +44,7 @@ useCommands(() => command
<span ws-nowrap mxa sm:px2 sm:py3 py2 text-center text-secondary-light op50>{{ option.display }}</span> <span ws-nowrap mxa sm:px2 sm:py3 py2 text-center text-secondary-light op50>{{ option.display }}</span>
</div> </div>
</template> </template>
<template v-if="moreOptions?.options?.length"> <template v-if="isHydrated && moreOptions?.options?.length">
<CommonDropdown placement="bottom" flex cursor-pointer mx-1.25rem> <CommonDropdown placement="bottom" flex cursor-pointer mx-1.25rem>
<CommonTooltip placement="top" :content="moreOptions.tooltip || t('action.more')"> <CommonTooltip placement="top" :content="moreOptions.tooltip || t('action.more')">
<button <button

View File

@ -10,6 +10,7 @@ defineProps<Props>()
<template> <template>
<VTooltip <VTooltip
v-if="isHydrated"
v-bind="$attrs" v-bind="$attrs"
auto-hide auto-hide
> >

View File

@ -24,7 +24,7 @@ interface ShortcutItemGroup {
const isMac = useIsMac() const isMac = useIsMac()
const modifierKeyName = computed(() => isMac.value ? '⌘' : 'Ctrl') const modifierKeyName = computed(() => isMac.value ? '⌘' : 'Ctrl')
const shortcutItemGroups: ShortcutItemGroup[] = [ const shortcutItemGroups = computed<ShortcutItemGroup[]>(() => [
{ {
name: t('magic_keys.groups.navigation.title'), name: t('magic_keys.groups.navigation.title'),
items: [ items: [
@ -79,7 +79,7 @@ const shortcutItemGroups: ShortcutItemGroup[] = [
name: t('magic_keys.groups.media.title'), name: t('magic_keys.groups.media.title'),
items: [], items: [],
}, },
] ])
</script> </script>
<template> <template>

View File

@ -91,10 +91,6 @@ export async function fetchAccountByHandle(acct: string): Promise<mastodon.v1.Ac
return account return account
} }
export function useAccountByHandle(acct: string) {
return useAsyncState(() => fetchAccountByHandle(acct), null).state
}
export function useAccountById(id?: string | null) { export function useAccountById(id?: string | null) {
return useAsyncState(() => fetchAccountById(id), null).state return useAsyncState(() => fetchAccountById(id), null).state
} }

View File

@ -362,20 +362,20 @@ export function useUserLocalStorage<T extends object>(key: string, initial: () =
// Backward compatibility, respect webDomain in acct // Backward compatibility, respect webDomain in acct
// In previous versions, acct was username@server instead of username@webDomain // In previous versions, acct was username@server instead of username@webDomain
// for example: elk@m.webtoo.ls instead of elk@webtoo.ls // for example: elk@m.webtoo.ls instead of elk@webtoo.ls
// if (!all.value[id]) { // TODO: add back this condition in the future if (!all.value[id]) {
const [username, webDomain] = id.split('@') const [username, webDomain] = id.split('@')
const server = currentServer.value const server = currentServer.value
if (webDomain && server && server !== webDomain) { if (webDomain && server && server !== webDomain) {
const oldId = `${username}@${server}` const oldId = `${username}@${server}`
const outdatedSettings = all.value[oldId] const outdatedSettings = all.value[oldId]
if (outdatedSettings) { if (outdatedSettings) {
const newAllValue = { ...all.value, [id]: outdatedSettings } const newAllValue = { ...all.value, [id]: outdatedSettings }
delete newAllValue[oldId] delete newAllValue[oldId]
all.value = newAllValue all.value = newAllValue
}
} }
all.value[id] = Object.assign(initial(), all.value[id] || {})
} }
// }
all.value[id] = Object.assign(initial(), all.value[id] || {})
return all.value[id] return all.value[id]
}) })
}) })

View File

@ -12,7 +12,7 @@ export default defineNuxtConfig({
tsConfig: { tsConfig: {
exclude: ['../service-worker'], exclude: ['../service-worker'],
vueCompilerOptions: { vueCompilerOptions: {
target: 3.3, target: 3.4,
}, },
}, },
}, },
@ -75,6 +75,11 @@ export default defineNuxtConfig({
'./composables/settings', './composables/settings',
'./composables/tiptap/index.ts', './composables/tiptap/index.ts',
], ],
imports: [{
name: 'useI18n',
from: '~/utils/i18n',
priority: 100,
}],
injectAtEnd: true, injectAtEnd: true,
}, },
vite: { vite: {
@ -86,6 +91,20 @@ export default defineNuxtConfig({
build: { build: {
target: 'esnext', target: 'esnext',
}, },
optimizeDeps: {
include: [
'@tiptap/vue-3', 'string-length', 'vue-virtual-scroller', 'emoji-mart', 'iso-639-1',
'@tiptap/extension-placeholder', '@tiptap/extension-document', '@tiptap/extension-paragraph',
'@tiptap/extension-text', '@tiptap/extension-mention', '@tiptap/extension-hard-break',
'@tiptap/extension-bold', '@tiptap/extension-italic', '@tiptap/extension-code',
'@tiptap/extension-history', 'prosemirror-state', 'browser-fs-access', 'blurhash',
'@vueuse/integrations/useFocusTrap', '@tiptap/extension-code-block', 'prosemirror-highlight',
'@tiptap/core', 'tippy.js', 'prosemirror-highlight/shiki', '@fnando/sparkline',
'@vueuse/gesture', 'github-reserved-names', 'file-saver', 'slimeform', 'vue-advanced-cropper',
'workbox-window', 'workbox-precaching', 'workbox-routing', 'workbox-cacheable-response',
'workbox-strategies', 'workbox-expiration',
],
},
}, },
postcss: { postcss: {
plugins: { plugins: {

View File

@ -150,6 +150,9 @@
"nuxt-security@0.13.1": "patches/nuxt-security@0.13.1.patch" "nuxt-security@0.13.1": "patches/nuxt-security@0.13.1.patch"
} }
}, },
"resolutions": {
"vue": "^3.4.19"
},
"simple-git-hooks": { "simple-git-hooks": {
"pre-commit": "pnpm lint-staged" "pre-commit": "pnpm lint-staged"
}, },

View File

@ -11,7 +11,7 @@ definePageMeta({
}) })
const route = useRoute() const route = useRoute()
const id = computedEager(() => route.params.status as string) const id = computed(() => route.params.status as string)
const main = ref<ComponentPublicInstance | null>(null) const main = ref<ComponentPublicInstance | null>(null)
const { data: status, pending, refresh: refreshStatus } = useAsyncData( const { data: status, pending, refresh: refreshStatus } = useAsyncData(
@ -71,7 +71,7 @@ onReactivated(() => {
<div xl:mt-4 mb="50vh" border="b base"> <div xl:mt-4 mb="50vh" border="b base">
<template v-if="!pendingContext"> <template v-if="!pendingContext">
<StatusCard <StatusCard
v-for="comment, i of context?.ancestors" :key="comment.id" v-for="(comment, i) of context?.ancestors" :key="comment.id"
:status="comment" :actions="comment.visibility !== 'direct'" context="account" :status="comment" :actions="comment.visibility !== 'direct'" context="account"
:has-older="true" :newer="context?.ancestors[i - 1]" :has-older="true" :newer="context?.ancestors[i - 1]"
/> />

View File

@ -4,7 +4,7 @@ definePageMeta({
}) })
const params = useRoute().params const params = useRoute().params
const accountName = computedEager(() => toShortHandle(params.account as string)) const accountName = computed(() => toShortHandle(params.account as string))
const { t } = useI18n() const { t } = useI18n()

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
const { t } = useI18n() const { t } = useI18n()
const params = useRoute().params const params = useRoute().params
const handle = computedEager(() => params.account as string) const handle = computed(() => params.account as string)
definePageMeta({ name: 'account-followers' }) definePageMeta({ name: 'account-followers' })

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
const { t } = useI18n() const { t } = useI18n()
const params = useRoute().params const params = useRoute().params
const handle = computedEager(() => params.account as string) const handle = computed(() => params.account as string)
definePageMeta({ name: 'account-following' }) definePageMeta({ name: 'account-following' })

View File

@ -2,7 +2,7 @@
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
const params = useRoute().params const params = useRoute().params
const handle = computedEager(() => params.account as string) const handle = computed(() => params.account as string)
definePageMeta({ name: 'account-index' }) definePageMeta({ name: 'account-index' })

View File

@ -3,7 +3,7 @@ definePageMeta({ name: 'account-media' })
const { t } = useI18n() const { t } = useI18n()
const params = useRoute().params const params = useRoute().params
const handle = computedEager(() => params.account as string) const handle = computed(() => params.account as string)
const account = await fetchAccountByHandle(handle.value) const account = await fetchAccountByHandle(handle.value)

View File

@ -3,7 +3,7 @@ definePageMeta({ name: 'account-replies' })
const { t } = useI18n() const { t } = useI18n()
const params = useRoute().params const params = useRoute().params
const handle = computedEager(() => params.account as string) const handle = computed(() => params.account as string)
const account = await fetchAccountByHandle(handle.value) const account = await fetchAccountByHandle(handle.value)

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CommonRouteTabOption } from '~/components/common/CommonRouteTabs.vue' import type { CommonRouteTabOption } from '~/types'
const { t } = useI18n() const { t } = useI18n()

View File

@ -17,5 +17,5 @@ useHydratedHead({
<p>{{ $t('tooltip.explore_posts_intro') }}</p> <p>{{ $t('tooltip.explore_posts_intro') }}</p>
</CommonAlert> </CommonAlert>
<!-- TODO: Tabs for trending statuses, tags, and links --> <!-- TODO: Tabs for trending statuses, tags, and links -->
<TimelinePaginator :paginator="paginator" context="public" /> <TimelinePaginator v-if="isHydrated" :paginator="paginator" context="public" />
</template> </template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CommonRouteTabOption } from '~/components/common/CommonRouteTabs.vue' import type { CommonRouteTabOption } from '~/types'
definePageMeta({ definePageMeta({
middleware: 'auth', middleware: 'auth',

View File

@ -4,7 +4,7 @@ definePageMeta({
}) })
const params = useRoute().params const params = useRoute().params
const listId = computedEager(() => params.list as string) const listId = computed(() => params.list as string)
const paginator = useMastoClient().v1.lists.$select(listId.value).accounts.list() const paginator = useMastoClient().v1.lists.$select(listId.value).accounts.list()
</script> </script>

View File

@ -4,7 +4,7 @@ definePageMeta({
}) })
const params = useRoute().params const params = useRoute().params
const listId = computedEager(() => params.list as string) const listId = computed(() => params.list as string)
const client = useMastoClient() const client = useMastoClient()

View File

@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
const { t } = useI18n() const { t } = useI18n()
useHydratedHead({ useHydratedHead({
@ -13,7 +11,7 @@ useHydratedHead({
<template #title> <template #title>
<NuxtLink to="/public" timeline-title-style flex items-center gap-2 @click="$scrollToTop"> <NuxtLink to="/public" timeline-title-style flex items-center gap-2 @click="$scrollToTop">
<div i-ri:earth-line /> <div i-ri:earth-line />
<span>{{ t('title.federated_timeline') }}</span> <span>{{ $t('title.federated_timeline') }}</span>
</NuxtLink> </NuxtLink>
</template> </template>

View File

@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n() const { t } = useI18n()
useHydratedHead({ useHydratedHead({
title: () => t('nav.search'), title: () => t('nav.search'),

View File

@ -4,7 +4,7 @@ definePageMeta({
}) })
const params = useRoute().params const params = useRoute().params
const tagName = computedEager(() => params.tag as string) const tagName = computed(() => params.tag as string)
const { client } = useMasto() const { client } = useMasto()
const { data: tag, refresh } = await useAsyncData(() => client.value.v1.tags.$select(tagName.value).fetch(), { default: () => shallowRef() }) const { data: tag, refresh } = await useAsyncData(() => client.value.v1.tags.$select(tagName.value).fetch(), { default: () => shallowRef() })

View File

@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'
definePageMeta({ definePageMeta({
middleware: 'auth', middleware: 'auth',
alias: ['/signin/callback'], alias: ['/signin/callback'],

View File

@ -1,10 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
import { NOTIFICATION_FILTER_TYPES } from '~/constants' import { NOTIFICATION_FILTER_TYPES } from '~/constants'
import type { import type { CommonRouteTabMoreOption, CommonRouteTabOption } from '~/types'
CommonRouteTabMoreOption,
CommonRouteTabOption,
} from '~/components/common/CommonRouteTabs.vue'
definePageMeta({ definePageMeta({
middleware: 'auth', middleware: 'auth',
@ -18,12 +15,12 @@ const tabs = computed<CommonRouteTabOption[]>(() => [
{ {
name: 'all', name: 'all',
to: '/notifications', to: '/notifications',
display: isHydrated.value ? t('tab.notifications_all') : '', display: t('tab.notifications_all'),
}, },
{ {
name: 'mention', name: 'mention',
to: '/notifications/mention', to: '/notifications/mention',
display: isHydrated.value ? t('tab.notifications_mention') : '', display: t('tab.notifications_mention'),
}, },
]) ])
@ -50,13 +47,12 @@ const filterIconMap: Record<mastodon.v1.NotificationType, string> = {
'admin.report': 'i-ri:flag-line', 'admin.report': 'i-ri:flag-line',
} }
const filterText = computed(() => (`${t('tab.notifications_more_tooltip')}${filter ? `: ${t(`tab.notifications_${filter}`)}` : ''}`)) const filterText = computed(() => `${t('tab.notifications_more_tooltip')}${filter.value ? `: ${t(`tab.notifications_${filter.value}`)}` : ''}`)
const notificationFilterRoutes = computed<CommonRouteTabOption[]>(() => NOTIFICATION_FILTER_TYPES.map( const notificationFilterRoutes = computed<CommonRouteTabOption[]>(() => NOTIFICATION_FILTER_TYPES.map(
name => ({ name => ({
name, name,
to: `/notifications/${name}`, to: `/notifications/${name}`,
display: isHydrated.value ? t(`tab.notifications_${name}`) : '', display: t(`tab.notifications_${name}`),
icon: filterIconMap[name], icon: filterIconMap[name],
match: name === filter.value, match: name === filter.value,
}), }),
@ -74,7 +70,7 @@ const moreOptions = computed<CommonRouteTabMoreOption>(() => ({
<template #title> <template #title>
<NuxtLink to="/notifications" timeline-title-style flex items-center gap-2 @click="$scrollToTop"> <NuxtLink to="/notifications" timeline-title-style flex items-center gap-2 @click="$scrollToTop">
<div i-ri:notification-4-line /> <div i-ri:notification-4-line />
<span>{{ isHydrated ? t('nav.notifications') : '' }}</span> <span>{{ t('nav.notifications') }}</span>
</NuxtLink> </NuxtLink>
</template> </template>
@ -82,7 +78,7 @@ const moreOptions = computed<CommonRouteTabMoreOption>(() => ({
<NuxtLink <NuxtLink
flex rounded-4 p1 flex rounded-4 p1
hover:bg-active cursor-pointer transition-100 hover:bg-active cursor-pointer transition-100
:title="isHydrated ? t('settings.notifications.show_btn') : ''" :title="t('settings.notifications.show_btn')"
to="/settings/notifications" to="/settings/notifications"
> >
<span aria-hidden="true" i-ri:notification-badge-line /> <span aria-hidden="true" i-ri:notification-badge-line />

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const { t } = useI18n() const { t } = useI18n()
useHydratedHead({ useHydratedHead({
title: () => `${t('tab.notifications_all')} | ${t('nav.notifications')}`, title: () => `${t('tab.notifications_all')} | ${t('nav.notifications')}`,
}) })

View File

@ -11,7 +11,7 @@ useHydratedHead({
const route = useRoute() const route = useRoute()
const isRootPath = computedEager(() => route.name === 'settings') const isRootPath = computed(() => route.name === 'settings')
</script> </script>
<template> <template>
@ -22,12 +22,12 @@ const isRootPath = computedEager(() => route.name === 'settings')
<template #title> <template #title>
<div timeline-title-style flex items-center gap-2 @click="$scrollToTop"> <div timeline-title-style flex items-center gap-2 @click="$scrollToTop">
<div i-ri:settings-3-line /> <div i-ri:settings-3-line />
<span>{{ isHydrated ? $t('nav.settings') : '' }}</span> <span>{{ $t('nav.settings') }}</span>
</div> </div>
</template> </template>
<div xl:w-97 lg:w-78 w-full> <div xl:w-97 lg:w-78 w-full>
<SettingsItem <SettingsItem
v-if="isHydrated && currentUser" v-if="currentUser"
command command
icon="i-ri:user-line" icon="i-ri:user-line"
:text="$t('settings.profile.label')" :text="$t('settings.profile.label')"
@ -37,12 +37,12 @@ const isRootPath = computedEager(() => route.name === 'settings')
<SettingsItem <SettingsItem
command command
icon="i-ri-compasses-2-line" icon="i-ri-compasses-2-line"
:text="isHydrated ? $t('settings.interface.label') : ''" :text="$t('settings.interface.label')"
to="/settings/interface" to="/settings/interface"
:match="$route.path.startsWith('/settings/interface/')" :match="$route.path.startsWith('/settings/interface/')"
/> />
<SettingsItem <SettingsItem
v-if="isHydrated && currentUser" v-if="currentUser"
command command
icon="i-ri:notification-badge-line" icon="i-ri:notification-badge-line"
:text="$t('settings.notifications_settings')" :text="$t('settings.notifications_settings')"
@ -52,28 +52,28 @@ const isRootPath = computedEager(() => route.name === 'settings')
<SettingsItem <SettingsItem
command command
icon="i-ri-globe-line" icon="i-ri-globe-line"
:text="isHydrated ? $t('settings.language.label') : ''" :text="$t('settings.language.label')"
to="/settings/language" to="/settings/language"
:match="$route.path.startsWith('/settings/language/')" :match="$route.path.startsWith('/settings/language/')"
/> />
<SettingsItem <SettingsItem
command command
icon="i-ri-equalizer-line" icon="i-ri-equalizer-line"
:text="isHydrated ? $t('settings.preferences.label') : ''" :text="$t('settings.preferences.label')"
to="/settings/preferences" to="/settings/preferences"
:match="$route.path.startsWith('/settings/preferences/')" :match="$route.path.startsWith('/settings/preferences/')"
/> />
<SettingsItem <SettingsItem
command command
icon="i-ri-group-line" icon="i-ri-group-line"
:text="isHydrated ? $t('settings.users.label') : ''" :text="$t('settings.users.label')"
to="/settings/users" to="/settings/users"
:match="$route.path.startsWith('/settings/users/')" :match="$route.path.startsWith('/settings/users/')"
/> />
<SettingsItem <SettingsItem
command command
icon="i-ri:information-line" icon="i-ri:information-line"
:text="isHydrated ? $t('settings.about.label') : ''" :text="$t('settings.about.label')"
to="/settings/about" to="/settings/about"
:match="$route.path.startsWith('/settings/about/')" :match="$route.path.startsWith('/settings/about/')"
/> />

View File

@ -15,20 +15,20 @@ useHydratedHead({
<MainContent back-on-small-screen> <MainContent back-on-small-screen>
<template #title> <template #title>
<div text-lg font-bold flex items-center gap-2 @click="$scrollToTop"> <div text-lg font-bold flex items-center gap-2 @click="$scrollToTop">
<span>{{ isHydrated ? $t('settings.notifications.label') : '' }}</span> <span>{{ $t('settings.notifications.label') }}</span>
</div> </div>
</template> </template>
<SettingsItem <SettingsItem
command command
:text="isHydrated ? $t('settings.notifications.notifications.label') : ''" :text="$t('settings.notifications.notifications.label')"
to="/settings/notifications/notifications" to="/settings/notifications/notifications"
/> />
<SettingsItem <SettingsItem
command command
:disabled="!pwaEnabled" :disabled="!pwaEnabled"
:text="isHydrated ? $t('settings.notifications.push_notifications.label') : ''" :text="$t('settings.notifications.push_notifications.label')"
:description="isHydrated ? $t('settings.notifications.push_notifications.description') : ''" :description="$t('settings.notifications.push_notifications.description')"
to="/settings/notifications/push-notifications" to="/settings/notifications/push-notifications"
/> />
</MainContent> </MainContent>

View File

@ -17,7 +17,7 @@ useHydratedHead({
<MainContent back> <MainContent back>
<template #title> <template #title>
<div text-lg font-bold flex items-center gap-2 @click="$scrollToTop"> <div text-lg font-bold flex items-center gap-2 @click="$scrollToTop">
<span>{{ isHydrated ? $t('settings.notifications.push_notifications.label') : '' }}</span> <span>{{ $t('settings.notifications.push_notifications.label') }}</span>
</div> </div>
</template> </template>
<NotificationPreferences show /> <NotificationPreferences show />

View File

@ -111,7 +111,7 @@ onReactivated(refreshInfo)
</template> </template>
<form space-y-5 @submit.prevent="submit"> <form space-y-5 @submit.prevent="submit">
<div v-if="isHydrated && account"> <div v-if="account">
<!-- banner --> <!-- banner -->
<div of-hidden bg="gray-500/20" aspect="3"> <div of-hidden bg="gray-500/20" aspect="3">
<CommonInputImage <CommonInputImage
@ -182,7 +182,7 @@ onReactivated(refreshInfo)
<!-- metadata --> <!-- metadata -->
<SettingsProfileMetadata v-if="isHydrated" v-model="form" /> <SettingsProfileMetadata v-model="form" />
<!-- actions --> <!-- actions -->
<div flex="~ gap2" justify-end> <div flex="~ gap2" justify-end>

View File

@ -14,22 +14,22 @@ useHydratedHead({
<MainContent back-on-small-screen> <MainContent back-on-small-screen>
<template #title> <template #title>
<div text-lg font-bold flex items-center gap-2 @click="$scrollToTop"> <div text-lg font-bold flex items-center gap-2 @click="$scrollToTop">
<span>{{ isHydrated ? $t('settings.profile.label') : '' }}</span> <span>{{ $t('settings.profile.label') }}</span>
</div> </div>
</template> </template>
<SettingsItem <SettingsItem
command large command large
icon="i-ri:user-settings-line" icon="i-ri:user-settings-line"
:text="isHydrated ? $t('settings.profile.appearance.label') : ''" :text="$t('settings.profile.appearance.label')"
:description="isHydrated ? $t('settings.profile.appearance.description') : ''" :description="$t('settings.profile.appearance.description')"
to="/settings/profile/appearance" to="/settings/profile/appearance"
/> />
<SettingsItem <SettingsItem
command large command large
icon="i-ri:hashtag" icon="i-ri:hashtag"
:text="isHydrated ? $t('settings.profile.featured_tags.label') : ''" :text="$t('settings.profile.featured_tags.label')"
:description="isHydrated ? $t('settings.profile.featured_tags.description') : ''" :description="$t('settings.profile.featured_tags.description')"
to="/settings/profile/featured-tags" to="/settings/profile/featured-tags"
/> />
<SettingsItem <SettingsItem

View File

@ -81,12 +81,12 @@ async function importTokens() {
</div> </div>
<div my4 border="t base" /> <div my4 border="t base" />
<button btn-text flex="~ gap-2" items-center @click="exportTokens"> <button btn-text flex="~ gap-2" items-center @click="exportTokens">
<div i-ri-download-2-line /> <span block i-ri-download-2-line />
{{ $t('settings.users.export') }} {{ $t('settings.users.export') }}
</button> </button>
</template> </template>
<button btn-text flex="~ gap-2" items-center @click="importTokens"> <button btn-text flex="~ gap-2" items-center @click="importTokens">
<div i-ri-upload-2-line /> <span block i-ri-upload-2-line />
{{ $t('settings.users.import') }} {{ $t('settings.users.import') }}
</button> </button>
</div> </div>

View File

@ -1,5 +1,5 @@
import FloatingVue from 'floating-vue' import FloatingVue from 'floating-vue'
import { defineNuxtPlugin } from '#app' import { defineNuxtPlugin } from '#imports'
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(FloatingVue) nuxtApp.vueApp.use(FloatingVue)

View File

@ -1,21 +0,0 @@
import type { VueI18n } from 'vue-i18n'
import type { LocaleObject } from 'vue-i18n-routing'
export default defineNuxtPlugin(async (nuxt) => {
const i18n = nuxt.vueApp.config.globalProperties.$i18n as VueI18n
const { setLocale, locales } = i18n
const userSettings = useUserSettings()
const lang = computed(() => userSettings.value.language)
const supportLanguages = (locales as LocaleObject[]).map(locale => locale.code)
if (!supportLanguages.includes(lang.value))
userSettings.value.language = getDefaultLanguage(supportLanguages)
if (lang.value !== i18n.locale)
await setLocale(userSettings.value.language)
watch([lang, isHydrated], () => {
if (isHydrated.value && lang.value !== i18n.locale)
setLocale(lang.value)
}, { immediate: true })
})

28
plugins/setup-i18n.ts Normal file
View File

@ -0,0 +1,28 @@
export default defineNuxtPlugin(async (nuxt) => {
const t = nuxt.vueApp.config.globalProperties.$t
const d = nuxt.vueApp.config.globalProperties.$d
const n = nuxt.vueApp.config.globalProperties.$n
nuxt.vueApp.config.globalProperties.$t = wrapI18n(t)
nuxt.vueApp.config.globalProperties.$d = wrapI18n(d)
nuxt.vueApp.config.globalProperties.$n = wrapI18n(n)
if (process.client) {
const i18n = nuxt.vueApp.config.globalProperties.$i18n as import('vue-i18n').VueI18n
const { setLocale, locales } = i18n
const userSettings = useUserSettings()
const lang = computed(() => userSettings.value.language)
const supportLanguages = (locales as import('vue-i18n-routing').LocaleObject[]).map(locale => locale.code)
if (!supportLanguages.includes(lang.value))
userSettings.value.language = getDefaultLanguage(supportLanguages)
if (lang.value !== i18n.locale)
await setLocale(userSettings.value.language)
watch([lang, isHydrated], () => {
if (isHydrated.value && lang.value !== i18n.locale)
setLocale(lang.value)
}, { immediate: true })
}
})

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
import type { MarkNonNullable, Mutable } from './utils' import type { MarkNonNullable, Mutable } from './utils'
import type { RouteLocationRaw } from '#vue-router'
export interface AppInfo { export interface AppInfo {
id: string id: string
@ -63,6 +64,22 @@ export interface ConfirmDialogLabel {
} }
export type ConfirmDialogChoice = 'confirm' | 'cancel' export type ConfirmDialogChoice = 'confirm' | 'cancel'
export interface CommonRouteTabOption {
to: RouteLocationRaw
display: string
disabled?: boolean
name?: string
icon?: string
hide?: boolean
match?: boolean
}
export interface CommonRouteTabMoreOption {
options: CommonRouteTabOption[]
icon?: string
tooltip?: string
match?: boolean
}
export interface ErrorDialogData { export interface ErrorDialogData {
title: string title: string
messages: string[] messages: string[]

23
utils/i18n.ts Normal file
View File

@ -0,0 +1,23 @@
import { useI18n as useOriginalI18n } from 'vue-i18n'
export function useI18n() {
const {
t,
d,
n,
...rest
} = useOriginalI18n()
return {
...rest,
t: wrapI18n(t),
d: wrapI18n(d),
n: wrapI18n(n),
} satisfies ReturnType<typeof useOriginalI18n>
}
export function wrapI18n<T extends (...args: any[]) => any>(t: T): T {
return <T>((...args: any[]) => {
return isHydrated.value ? t(...args) : ''
})
}