Merge branch 'main' into feature/remove-link-if-matches-preview-URL

This commit is contained in:
Ayo 2023-02-05 19:25:11 +01:00
commit 354914fc8d
136 changed files with 2832 additions and 1923 deletions

View file

@ -1,5 +1,6 @@
NUXT_PUBLIC_TRANSLATE_API= NUXT_PUBLIC_TRANSLATE_API=
NUXT_PUBLIC_DEFAULT_SERVER= NUXT_PUBLIC_DEFAULT_SERVER=
SINGLE_INSTANCE_SERVER=
NUXT_PUBLIC_PRIVACY_POLICY_URL= NUXT_PUBLIC_PRIVACY_POLICY_URL=
# Production only # Production only

View file

@ -44,6 +44,10 @@ These are known deployments using Elk as an alternative Web client for Mastodon
- [elk.h4.io](https://elk.h4.io) - Use Elk for the `h4.io` Server - [elk.h4.io](https://elk.h4.io) - Use Elk for the `h4.io` Server
- [elk.universeodon.com](https://elk.universeodon.com) - Use Elk for the Universeodon Server - [elk.universeodon.com](https://elk.universeodon.com) - Use Elk for the Universeodon Server
- [elk.vmst.io](https://elk.vmst.io) - Use Elk for the `vmst.io` Server - [elk.vmst.io](https://elk.vmst.io) - Use Elk for the `vmst.io` Server
- [elk.hostux.social](https://elk.hostux.social) - Use Elk for the `hostux.social` Server
- [elk.freelancers.online](https://elk.freelancers.online) - Use Elk for the `freelancers.online` Server
- [elk.cupoftea.social](https://elk.cupoftea.social) - Use Elk for the `cupoftea.social` Server
- [elk.aus.social](https://elk.aus.social) - Use Elk for the `aus.social` Server
> **Note**: Community deployments are **NOT** maintained by the Elk team. It may not be synced with Elk's source code. Please do your own research about the host servers before using them. > **Note**: Community deployments are **NOT** maintained by the Elk team. It may not be synced with Elk's source code. Please do your own research about the host servers before using them.

View file

@ -12,7 +12,7 @@ defineProps<{
> >
<slot name="prepend" /> <slot name="prepend" />
<CommonTooltip :content="$t('account.bot')" :disabled="showLabel"> <CommonTooltip :content="$t('account.bot')" :disabled="showLabel">
<div i-ri:robot-line /> <div i-mdi:robot-outline />
</CommonTooltip> </CommonTooltip>
<div v-if="showLabel"> <div v-if="showLabel">
{{ $t('account.bot') }} {{ $t('account.bot') }}

View file

@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
defineProps<{ const { account, hideEmojis = false } = defineProps<{
account: mastodon.v1.Account account: mastodon.v1.Account
hideEmojis?: boolean
}>() }>()
</script> </script>
@ -10,6 +11,7 @@ defineProps<{
<ContentRich <ContentRich
:content="getDisplayName(account, { rich: true })" :content="getDisplayName(account, { rich: true })"
:emojis="account.emojis" :emojis="account.emojis"
:hide-emojis="hideEmojis"
:markdown="false" :markdown="false"
/> />
</template> </template>

View file

@ -117,7 +117,7 @@ const buttonStyle = $computed(() => {
<span hidden group-hover="inline">{{ $t('account.follow_back') }}</span> <span hidden group-hover="inline">{{ $t('account.follow_back') }}</span>
</template> </template>
<template v-else> <template v-else>
<span>{{ $t('account.follow') }}</span> <span>{{ account.locked ? $t('account.request_follow') : $t('account.follow') }}</span>
</template> </template>
</button> </button>
</template> </template>

View file

@ -91,19 +91,22 @@ const isNotifiedOnPost = $computed(() => !!relationship?.notifying)
</component> </component>
<div p4 mt--18 flex flex-col gap-4> <div p4 mt--18 flex flex-col gap-4>
<div relative> <div relative>
<div flex="~ col gap-2 1"> <div flex justify-between>
<button :class="{ 'rounded-full': !isSelf, 'squircle': isSelf }" w-30 h-30 p1 bg-base border-bg-base z-2 @click="previewAvatar"> <button shrink-0 :class="{ 'rounded-full': !isSelf, 'squircle': isSelf }" w-30 h-30 p1 bg-base border-bg-base z-2 @click="previewAvatar">
<AccountAvatar :square="isSelf" :account="account" hover:opacity-90 transition-opacity /> <AccountAvatar :square="isSelf" :account="account" hover:opacity-90 transition-opacity />
</button> </button>
<div flex="~ col gap1"> <div inset-ie-0 flex="~ wrap row-reverse" gap-2 items-center pt18 justify-start>
<div flex justify-between> <!-- Edit profile -->
<AccountDisplayName :account="account" font-bold sm:text-2xl text-xl /> <NuxtLink
<AccountBotIndicator v-if="account.bot" show-label /> v-if="isSelf"
</div> to="/settings/profile/appearance"
<AccountHandle :account="account" /> gap-1 items-center border="1" rounded-full flex="~ gap2 center" font-500 min-w-30 h-fit px3 py1
</div> hover="border-primary text-primary bg-active"
</div> >
<div absolute top-18 inset-ie-0 flex gap-2 items-center> {{ $t('settings.profile.appearance.title') }}
</NuxtLink>
<AccountFollowButton :account="account" :command="command" />
<span inset-ie-0 flex gap-2 items-center>
<AccountMoreButton :account="account" :command="command" /> <AccountMoreButton :account="account" :command="command" />
<CommonTooltip v-if="!isSelf && relationship?.following" :content="getNotificationIconTitle()"> <CommonTooltip v-if="!isSelf && relationship?.following" :content="getNotificationIconTitle()">
<button <button
@ -131,16 +134,15 @@ const isNotifiedOnPost = $computed(() => !!relationship?.notifying)
</template> </template>
</VDropdown> </VDropdown>
</CommonTooltip> </CommonTooltip>
<AccountFollowButton :account="account" :command="command" /> </span>
<!-- Edit profile --> </div>
<NuxtLink </div>
v-if="isSelf" <div flex="~ col gap1" pt2>
to="/settings/profile/appearance" <div flex justify-between>
gap-1 items-center border="1" rounded-full flex="~ gap2 center" font-500 min-w-30 h-fit px3 py1 <AccountDisplayName :account="account" font-bold sm:text-2xl text-xl />
hover="border-primary text-primary bg-active" <AccountBotIndicator v-if="account.bot" show-label />
> </div>
{{ $t('settings.profile.appearance.title') }} <AccountHandle :account="account" />
</NuxtLink>
</div> </div>
</div> </div>
<div v-if="account.note" max-h-100 overflow-y-auto> <div v-if="account.note" max-h-100 overflow-y-auto>
@ -154,12 +156,12 @@ const isNotifiedOnPost = $computed(() => !!relationship?.notifying)
<ContentRich :content="field.value" :emojis="account.emojis" /> <ContentRich :content="field.value" :emojis="account.emojis" />
</div> </div>
</div> </div>
<div v-if="iconFields.length" flex="~ wrap gap-4"> <div v-if="iconFields.length" flex="~ wrap gap-2">
<div v-for="field in iconFields" :key="field.name" flex="~ gap-1" items-center> <div v-for="field in iconFields" :key="field.name" flex="~ gap-1" px1 items-center :class="`${field.verifiedAt ? 'border-1 rounded-full border-dark' : ''}`">
<CommonTooltip :content="getFieldIconTitle(field.name)"> <CommonTooltip :content="getFieldIconTitle(field.name)">
<div text-secondary :class="getAccountFieldIcon(field.name)" :title="getFieldIconTitle(field.name)" /> <div text-secondary :class="getAccountFieldIcon(field.name)" :title="getFieldIconTitle(field.name)" />
</CommonTooltip> </CommonTooltip>
<ContentRich text-sm filter-saturate-0 :content="field.value" :emojis="account.emojis" /> <ContentRich text-sm :content="field.value" :emojis="account.emojis" />
</div> </div>
</div> </div>
<AccountPostsFollowers :account="account" /> <AccountPostsFollowers :account="account" />

View file

@ -6,6 +6,8 @@ const { link = true, avatar = true } = defineProps<{
link?: boolean link?: boolean
avatar?: boolean avatar?: boolean
}>() }>()
const userSettings = useUserSettings()
</script> </script>
<template> <template>
@ -16,7 +18,7 @@ const { link = true, avatar = true } = defineProps<{
min-w-0 flex gap-2 items-center min-w-0 flex gap-2 items-center
> >
<AccountAvatar v-if="avatar" :account="account" w-5 h-5 /> <AccountAvatar v-if="avatar" :account="account" w-5 h-5 />
<AccountDisplayName :account="account" line-clamp-1 ws-pre-wrap break-all /> <AccountDisplayName :account="account" :hide-emojis="getPreferences(userSettings, 'hideUsernameEmojis')" line-clamp-1 ws-pre-wrap break-all />
</NuxtLink> </NuxtLink>
</AccountHoverWrapper> </AccountHoverWrapper>
</template> </template>

View file

@ -64,6 +64,9 @@ const result = $computed<QueryResult>(() => commandMode
: searchResult, : searchResult,
) )
const isMac = useIsMac()
const modifierKeyName = $computed(() => isMac.value ? '⌘' : 'Ctrl')
let active = $ref(0) let active = $ref(0)
watch($$(result), (n, o) => { watch($$(result), (n, o) => {
if (n.length !== o.length || !n.items.every((i, idx) => i === o.items[idx])) if (n.length !== o.length || !n.items.every((i, idx) => i === o.items[idx]))
@ -233,8 +236,8 @@ const onKeyDown = (e: KeyboardEvent) => {
<!-- Footer --> <!-- Footer -->
<div class="flex items-center px-3 py-1 text-xs"> <div class="flex items-center px-3 py-1 text-xs">
<div i-ri:lightbulb-flash-line /> Tip: Use <div i-ri:lightbulb-flash-line /> Tip: Use
<CommandKey name="Ctrl+K" /> to search, <CommandKey :name="`${modifierKeyName}+K`" /> to search,
<CommandKey name="Ctrl+/" /> to activate command mode. <CommandKey :name="`${modifierKeyName}+/`" /> to activate command mode.
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,22 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ defineProps<{ describedBy: string }>()
describedBy: string
}>()
defineOptions({
inheritAttrs: false,
})
</script> </script>
<template> <template>
<div <div
role="alert" role="alert"
aria-live="polite"
:aria-describedby="describedBy" :aria-describedby="describedBy"
flex="~ col" flex="~ col"
gap-1 text-sm gap-1 text-sm
pt-1 ps-2 pe-1 pb-2 pt-1 ps-2 pe-1 pb-2
text-red-600 dark:text-red-400 text-red-600 dark:text-red-400
border="~ base rounded red-600 dark:red-400" border="~ base rounded red-600 dark:red-400"
v-bind="$attrs"
> >
<slot /> <slot />
</div> </div>

View file

@ -43,8 +43,31 @@ defineSlots<{
}>() }>()
const { t } = useI18n() const { t } = useI18n()
const nuxtApp = useNuxtApp()
const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, $$(stream), eventType, preprocess) const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, $$(stream), eventType, preprocess)
nuxtApp.hook('elk-logo:click', () => {
update()
nuxtApp.$scrollToTop()
})
function createEntry(item: any) {
items.value = [...items.value, preprocess?.([item]) ?? item]
}
function updateEntry(item: any) {
const id = item[keyProp]
const index = items.value.findIndex(i => (i as any)[keyProp] === id)
if (index > -1)
items.value = [...items.value.slice(0, index), preprocess?.([item]) ?? item, ...items.value.slice(index + 1)]
}
function removeEntry(entryId: any) {
items.value = items.value.filter(i => (i as any)[keyProp] !== entryId)
}
defineExpose({ createEntry, removeEntry, updateEntry })
</script> </script>
<template> <template>

View file

@ -46,6 +46,7 @@ useCommand({
v-bind="$attrs" v-bind="$attrs"
:is="is" :is="is"
ref="el" ref="el"
w-full
flex gap-3 items-center cursor-pointer px4 py3 flex gap-3 items-center cursor-pointer px4 py3
select-none select-none
hover-bg-active hover-bg-active

View file

@ -7,10 +7,12 @@ defineOptions({
const { const {
content, content,
emojis, emojis,
hideEmojis = false,
markdown = true, markdown = true,
} = defineProps<{ } = defineProps<{
content: string content: string
emojis?: mastodon.v1.CustomEmoji[] emojis?: mastodon.v1.CustomEmoji[]
hideEmojis?: boolean
markdown?: boolean markdown?: boolean
}>() }>()
@ -21,6 +23,7 @@ export default () => h(
{ class: 'content-rich', dir: 'auto' }, { class: 'content-rich', dir: 'auto' },
contentToVNode(content, { contentToVNode(content, {
emojis: emojisObject.value, emojis: emojisObject.value,
hideEmojis,
markdown, markdown,
}), }),
) )

View file

@ -6,7 +6,8 @@ const { paginator } = defineProps<{
}>() }>()
function preprocess(items: mastodon.v1.Conversation[]): mastodon.v1.Conversation[] { function preprocess(items: mastodon.v1.Conversation[]): mastodon.v1.Conversation[] {
return items.filter(items => !items.lastStatus?.filtered?.find( const isAuthored = (conversation: mastodon.v1.Conversation) => conversation.lastStatus ? conversation.lastStatus.account.id === currentUser.value?.account.id : false
return items.filter(item => isAuthored(item) || !item.lastStatus?.filtered?.find(
filter => filter.filter.filterAction === 'hide' && filter.filter.context.includes('thread'), filter => filter.filter.filterAction === 'hide' && filter.filter.context.includes('thread'),
)) ))
} }

View file

@ -30,7 +30,7 @@ const emit = defineEmits<{
</p> </p>
{{ $t('help.desc_para3') }} {{ $t('help.desc_para3') }}
<p flex="~ gap-2 wrap" mxa> <p flex="~ gap-2 wrap" mxa>
<template v-for="team of teams" :key="team.github"> <template v-for="team of elkTeamMembers" :key="team.github">
<NuxtLink :href="`https://github.com/sponsors/${team.github}`" target="_blank" external rounded-full transition duration-300 border="~ transparent" hover="scale-105 border-primary"> <NuxtLink :href="`https://github.com/sponsors/${team.github}`" target="_blank" external rounded-full transition duration-300 border="~ transparent" hover="scale-105 border-primary">
<img :src="`/avatars/${team.github}-100x100.png`" :alt="team.display" rounded-full w-15 h-15 height="60" width="60"> <img :src="`/avatars/${team.github}-100x100.png`" :alt="team.display" rounded-full w-15 h-15 height="60" width="60">
</NuxtLink> </NuxtLink>
@ -38,7 +38,7 @@ const emit = defineEmits<{
</p> </p>
<p italic flex justify-center w-full> <p italic flex justify-center w-full>
<NuxtLink href="https://github.com/sponsors/elk-zone" target="_blank"> <NuxtLink href="https://github.com/sponsors/elk-zone" target="_blank">
<span text-xl font-script hover:text-primary transition duration-300>The Elk Team</span> <span text-xl font-script hover:text-primary transition duration-300>{{ $t('help.footer_team') }}</span>
</NuxtLink> </NuxtLink>
</p> </p>

View file

@ -36,8 +36,19 @@ async function edit() {
:to="getAccountRoute(account)" :to="getAccountRoute(account)"
/> />
<div> <div>
<CommonTooltip :content="isRemoved ? $t('list.add_account') : $t('list.remove_account')" :hover="isRemoved ? 'text-green' : 'text-red'"> <CommonTooltip
<button :class="isRemoved ? 'i-ri:user-add-line' : 'i-ri:user-unfollow-line'" text-xl @click="edit" /> :content="isRemoved ? $t('list.add_account') : $t('list.remove_account')"
:hover="isRemoved ? 'text-green' : 'text-red'"
no-auto-focus
>
<button
text-sm p2 border-1 transition-colors
border-dark
btn-action-icon
@click="edit"
>
<span :class="isRemoved ? 'i-ri:user-add-line' : 'i-ri:user-unfollow-line'" />
</button>
</CommonTooltip> </CommonTooltip>
</div> </div>
</div> </div>

View file

@ -0,0 +1,233 @@
<script setup lang="ts">
import type { mastodon } from 'masto'
const emit = defineEmits<{
(e: 'listUpdated', list: mastodon.v1.List): void
(e: 'listRemoved', id: string): void
}>()
const { list } = $defineProps<{
list: mastodon.v1.List
}>()
const { modelValue } = defineModel<{
modelValue: string
}>()
modelValue.value = list.title
const { t } = useI18n()
const client = useMastoClient()
let isEditing = $ref<boolean>(false)
let busy = $ref<boolean>(false)
let deleteBusy = $ref<boolean>(false)
let actionError = $ref<string | undefined>(undefined)
const enableSaveButton = computed(() => list.title !== modelValue.value)
const edit = ref()
const deleteBtn = ref()
const input = ref()
const prepareEdit = () => {
isEditing = true
actionError = undefined
nextTick(() => {
input.value?.focus()
})
}
const cancelEdit = () => {
isEditing = false
actionError = undefined
modelValue.value = list.title
nextTick(() => {
edit.value?.focus()
})
}
async function finishEditing() {
if (busy || !isEditing || !enableSaveButton.value)
return
busy = true
actionError = undefined
await nextTick()
try {
const updateList = await client.v1.lists.update(list.id, {
title: modelValue.value,
})
cancelEdit()
emit('listUpdated', updateList)
}
catch (err) {
console.error(err)
actionError = (err as Error).message
nextTick(() => {
input.value?.focus()
})
}
finally {
busy = false
}
}
async function removeList() {
if (deleteBusy)
return
deleteBusy = true
actionError = undefined
await nextTick()
const confirmDelete = await openConfirmDialog({
title: t('confirm.delete_list.title', [list.title]),
confirm: t('confirm.delete_list.confirm'),
cancel: t('confirm.delete_list.cancel'),
})
if (confirmDelete === 'confirm') {
await nextTick()
try {
await client.v1.lists.remove(list.id)
emit('listRemoved', list.id)
}
catch (err) {
console.error(err)
actionError = (err as Error).message
nextTick(() => {
deleteBtn.value?.focus()
})
}
finally {
deleteBusy = false
}
}
else {
deleteBusy = false
}
}
function clearError() {
actionError = undefined
nextTick(() => {
if (isEditing)
input.value?.focus()
else
deleteBtn.value?.focus()
})
}
onDeactivated(cancelEdit)
</script>
<template>
<form
hover:bg-active flex justify-between items-center gap-x-2
:aria-describedby="actionError ? `action-list-error-${list.id}` : undefined"
:class="actionError ? 'border border-base border-rounded rounded-be-is-0 rounded-be-ie-0 border-b-unset border-$c-danger-active' : null"
@submit.prevent="finishEditing"
>
<div
v-if="isEditing"
bg-base border="~ base" h10 m2 ps-1 pe-4 rounded-3 w-full flex="~ row"
items-center relative focus-within:box-shadow-outline gap-3
>
<CommonTooltip v-if="isEditing" :content="$t('list.cancel_edit')" no-auto-focus>
<button
type="button"
rounded-full text-sm p2 transition-colors
hover:text-primary
@click="cancelEdit()"
>
<span block text-current i-ri:close-fill />
</button>
</CommonTooltip>
<input
ref="input"
v-model="modelValue"
rounded-3
w-full
bg-transparent
outline="focus:none"
pe-4
pb="1px"
flex-1
placeholder-text-secondary
@keydown.esc="cancelEdit()"
>
</div>
<NuxtLink v-else :to="`list/${list.id}`" block grow p4>
{{ list.title }}
</NuxtLink>
<div mr4 flex gap2>
<CommonTooltip v-if="isEditing" :content="$t('list.save')" no-auto-focus>
<button
type="submit"
text-sm p2 border-1 transition-colors
border-dark hover:text-primary
btn-action-icon
:disabled="deleteBusy || !enableSaveButton || busy"
>
<template v-if="isEditing">
<span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
<span block i-ri:loader-2-fill aria-hidden="true" />
</span>
<span v-else block text-current i-ri:save-2-fill class="rtl-flip" />
</template>
</button>
</CommonTooltip>
<CommonTooltip v-else :content="$t('list.edit')" no-auto-focus>
<button
ref="edit"
type="button"
text-sm p2 border-1 transition-colors
border-dark hover:text-primary
btn-action-icon
@click.prevent="prepareEdit"
>
<span block text-current i-ri:edit-2-line class="rtl-flip" />
</button>
</CommonTooltip>
<CommonTooltip :content="$t('list.delete')" no-auto-focus>
<button
ref="delete"
type="button"
text-sm p2 border-1 transition-colors
border-dark hover:text-primary
btn-action-icon
:disabled="isEditing"
@click.prevent="removeList"
>
<span v-if="deleteBusy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
<span block i-ri:loader-2-fill aria-hidden="true" />
</span>
<span v-else block text-current i-ri:delete-bin-2-line class="rtl-flip" />
</button>
</CommonTooltip>
</div>
</form>
<CommonErrorMessage
v-if="actionError"
:id="`action-list-error-${list.id}`"
:described-by="`action-list-failed-${list.id}`"
class="rounded-bs-is-0 rounded-bs-ie-0 border-t-dashed m-b-2"
>
<header :id="`action-list-failed-${list.id}`" flex justify-between>
<div flex items-center gap-x-2 font-bold>
<div aria-hidden="true" i-ri:error-warning-fill />
<p>{{ $t(`list.${isEditing ? 'edit_error' : 'delete_error'}`) }}</p>
</div>
<CommonTooltip placement="bottom" :content="$t('list.clear_error')" no-auto-focus>
<button
flex rounded-4 p1 hover:bg-active cursor-pointer transition-100 :aria-label="$t('list.clear_error')"
@click="clearError"
>
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
</button>
</CommonTooltip>
</header>
<ol ps-2 sm:ps-1>
<li flex="~ col sm:row" gap-y-1 sm:gap-x-2>
<strong sr-only>{{ $t('list.error_prefix') }}</strong>
<span>{{ actionError }}</span>
</li>
</ol>
</CommonErrorMessage>
</template>

View file

@ -39,9 +39,13 @@ async function edit(listId: string) {
:hover="indexOfUserInList(item.id) === -1 ? 'text-green' : 'text-red'" :hover="indexOfUserInList(item.id) === -1 ? 'text-green' : 'text-red'"
> >
<button <button
:class="indexOfUserInList(item.id) === -1 ? 'i-ri:user-add-line' : 'i-ri:user-unfollow-line'" text-sm p2 border-1 transition-colors
text-xl @click="() => edit(item.id)" border-dark
/> btn-action-icon
@click="() => edit(item.id)"
>
<span :class="indexOfUserInList(item.id) === -1 ? 'i-ri:user-add-line' : 'i-ri:user-unfollow-line'" />
</button>
</CommonTooltip> </CommonTooltip>
</div> </div>
</template> </template>

View file

@ -4,6 +4,8 @@ defineProps<{
backOnSmallScreen?: boolean backOnSmallScreen?: boolean
/** Show the back button on both small and big screens */ /** Show the back button on both small and big screens */
back?: boolean back?: boolean
/** Do not applying overflow hidden to let use floatable components in title */
noOverflowHidden?: boolean
}>() }>()
const route = useRoute() const route = useRoute()
@ -18,8 +20,8 @@ const wideLayout = computed(() => route.meta.wideLayout ?? false)
border="b base" bg="[rgba(var(--rgb-bg-base),0.7)]" border="b base" bg="[rgba(var(--rgb-bg-base),0.7)]"
class="native:lg:w-[calc(100vw-5rem)] native:xl:w-[calc(135%+(100vw-1200px)/2)]" class="native:lg:w-[calc(100vw-5rem)] native:xl:w-[calc(135%+(100vw-1200px)/2)]"
> >
<div flex justify-between px5 py2 :class="{ 'xl:hidden': $route.name !== 'tag' }" data-tauri-drag-region class="native:xl:flex"> <div flex justify-between px5 py2 :class="{ 'xl:hidden': $route.name !== 'tag' }" class="native:xl:flex">
<div flex gap-3 items-center overflow-hidden py2 class="native-mac:pl-14 native-mac:sm:pl-0"> <div flex gap-3 items-center :overflow-hidden="!noOverflowHidden ? '' : false" py2 w-full>
<NuxtLink <NuxtLink
v-if="backOnSmallScreen || back" flex="~ gap1" items-center btn-text p-0 xl:hidden v-if="backOnSmallScreen || back" flex="~ gap1" items-center btn-text p-0 xl:hidden
:aria-label="$t('nav.back')" :aria-label="$t('nav.back')"
@ -27,10 +29,10 @@ const wideLayout = computed(() => route.meta.wideLayout ?? false)
> >
<div i-ri:arrow-left-line class="rtl-flip" /> <div i-ri:arrow-left-line class="rtl-flip" />
</NuxtLink> </NuxtLink>
<div truncate> <div :truncate="!noOverflowHidden ? '' : false" flex w-full data-tauri-drag-region class="native-mac:justify-center native-mac:text-center native-mac:sm:justify-start">
<slot name="title" /> <slot name="title" />
</div> </div>
<div h-7 w-1px /> <div sm:hidden h-7 w-1px />
</div> </div>
<div flex items-center flex-shrink-0 gap-x-2> <div flex items-center flex-shrink-0 gap-x-2>
<slot name="actions" /> <slot name="actions" />

View file

@ -5,6 +5,7 @@ import {
isCommandPanelOpen, isCommandPanelOpen,
isConfirmDialogOpen, isConfirmDialogOpen,
isEditHistoryDialogOpen, isEditHistoryDialogOpen,
isErrorDialogOpen,
isFavouritedBoostedByDialogOpen, isFavouritedBoostedByDialogOpen,
isMediaPreviewOpen, isMediaPreviewOpen,
isPreviewHelpOpen, isPreviewHelpOpen,
@ -87,6 +88,9 @@ const handleFavouritedBoostedByClose = () => {
<ModalDialog v-model="isConfirmDialogOpen" py-4 px-8 max-w-125> <ModalDialog v-model="isConfirmDialogOpen" py-4 px-8 max-w-125>
<ModalConfirm v-if="confirmDialogLabel" v-bind="confirmDialogLabel" @choice="handleConfirmChoice" /> <ModalConfirm v-if="confirmDialogLabel" v-bind="confirmDialogLabel" @choice="handleConfirmChoice" />
</ModalDialog> </ModalDialog>
<ModalDialog v-model="isErrorDialogOpen" py-4 px-8 max-w-125>
<ModalError v-if="errorDialogData" v-bind="errorDialogData" />
</ModalDialog>
<ModalDialog <ModalDialog
v-model="isFavouritedBoostedByDialogOpen" v-model="isFavouritedBoostedByDialogOpen"
max-w-180 max-w-180

View file

@ -0,0 +1,31 @@
<script setup lang="ts">
import type { ErrorDialogData } from '~/types'
defineProps<ErrorDialogData>()
</script>
<template>
<div flex="~ col" gap-6>
<div font-bold text-lg text-center>
{{ title }}
</div>
<div
flex="~ col"
gap-1 text-sm
pt-1 ps-2 pe-1 pb-2
text-red-600 dark:text-red-400
border="~ base rounded red-600 dark:red-400"
>
<ol ps-2 sm:ps-1>
<li v-for="(message, i) in messages" :key="i" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
{{ message }}
</li>
</ol>
</div>
<div flex justify-end gap-2>
<button btn-text @click="closeErrorDialog()">
{{ close }}
</button>
</div>
</div>
</template>

View file

@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { SwipeDirection } from '@vueuse/core' import { SwipeDirection } from '@vueuse/core'
import { useGesture } from '@vueuse/gesture'
import type { PermissiveMotionProperties } from '@vueuse/motion'
import { useReducedMotion } from '@vueuse/motion' import { useReducedMotion } from '@vueuse/motion'
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
@ -23,18 +25,36 @@ const reduceMotion = useReducedMotion()
const canAnimate = computed(() => !reduceMotion.value && animateTimeout.value) const canAnimate = computed(() => !reduceMotion.value && animateTimeout.value)
const { motionProperties } = useMotionProperties(target, {
cursor: 'grab',
scale: 1,
x: 0,
y: 0,
})
const { set } = useSpring(motionProperties as Partial<PermissiveMotionProperties>)
function resetZoom() {
set({ scale: 1 })
}
watch(modelValue, resetZoom)
const { width, height } = useElementSize(target) const { width, height } = useElementSize(target)
const { isSwiping, lengthX, lengthY, direction } = useSwipe(target, { const { isSwiping, lengthX, lengthY, direction } = useSwipe(target, {
threshold: 5, threshold: 5,
passive: false, passive: false,
onSwipeEnd(e, direction) { onSwipeEnd(e, direction) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
if (direction === SwipeDirection.RIGHT && Math.abs(distanceX.value) > threshold) if (direction === SwipeDirection.RIGHT && Math.abs(distanceX.value) > threshold) {
modelValue.value = Math.max(0, modelValue.value - 1) modelValue.value = Math.max(0, modelValue.value - 1)
resetZoom()
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
if (direction === SwipeDirection.LEFT && Math.abs(distanceX.value) > threshold) if (direction === SwipeDirection.LEFT && Math.abs(distanceX.value) > threshold) {
modelValue.value = Math.min(media.length - 1, modelValue.value + 1) modelValue.value = Math.min(media.length - 1, modelValue.value + 1)
resetZoom()
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
if (direction === SwipeDirection.UP && Math.abs(distanceY.value) > threshold) if (direction === SwipeDirection.UP && Math.abs(distanceY.value) > threshold)
@ -42,6 +62,21 @@ const { isSwiping, lengthX, lengthY, direction } = useSwipe(target, {
}, },
}) })
useGesture({
onPinch({ offset: [distance, angle] }) {
set({ scale: 1 + distance / 200 })
},
onMove({ movement: [x, y], dragging, pinching }) {
if (dragging && !pinching)
set({ x, y })
},
}, {
domTarget: target,
eventOptions: {
passive: true,
},
})
const distanceX = computed(() => { const distanceX = computed(() => {
if (width.value === 0) if (width.value === 0)
return 0 return 0

View file

@ -14,7 +14,7 @@ const moreMenuVisible = ref(false)
<NuxtLink to="/home" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop"> <NuxtLink to="/home" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
<div i-ri:home-5-line /> <div i-ri:home-5-line />
</NuxtLink> </NuxtLink>
<NuxtLink to="/search" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop"> <NuxtLink :to="isHydrated ? `/${currentServer}/explore` : '/explore'" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
<div i-ri:search-line /> <div i-ri:search-line />
</NuxtLink> </NuxtLink>
<NuxtLink to="/notifications" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop"> <NuxtLink to="/notifications" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
@ -28,9 +28,6 @@ const moreMenuVisible = ref(false)
<NuxtLink :to="`/${currentServer}/explore`" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop"> <NuxtLink :to="`/${currentServer}/explore`" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
<div i-ri:hashtag /> <div i-ri:hashtag />
</NuxtLink> </NuxtLink>
<NuxtLink to="/search" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
<div i-ri:search-line />
</NuxtLink>
<NuxtLink group :to="`/${currentServer}/public/local`" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop"> <NuxtLink group :to="`/${currentServer}/public/local`" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
<div i-ri:group-2-line /> <div i-ri:group-2-line />
</NuxtLink> </NuxtLink>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const buildInfo = useRuntimeConfig().public.buildInfo const buildInfo = useAppConfig().buildInfo
const timeAgoOptions = useTimeAgoOptions() const timeAgoOptions = useTimeAgoOptions()
const userSettings = useUserSettings() const userSettings = useUserSettings()

View file

@ -6,12 +6,11 @@ const { notifications } = useNotifications()
</script> </script>
<template> <template>
<nav sm:px3 flex="~ col gap2" shrink text-size-base leading-normal md:text-lg> <nav sm:px3 flex="~ col gap2" shrink text-size-base leading-normal md:text-lg h-full>
<div shrink hidden sm:block mt-4 />
<SearchWidget lg:ms-1 lg:me-5 hidden xl:block /> <SearchWidget lg:ms-1 lg:me-5 hidden xl:block />
<NavSideItem :text="$t('nav.search')" to="/search" icon="i-ri:search-line" xl:hidden :command="command" /> <NavSideItem :text="$t('nav.search')" :to="isHydrated ? `/${currentServer}/explore` : '/explore'" icon="i-ri:search-line" hidden sm:block xl:hidden :command="command" />
<div shrink hidden sm:block mt-4 /> <div shrink hidden sm:block mt-2 />
<NavSideItem :text="$t('nav.home')" to="/home" icon="i-ri:home-5-line" user-only :command="command" /> <NavSideItem :text="$t('nav.home')" to="/home" icon="i-ri:home-5-line" user-only :command="command" />
<NavSideItem :text="$t('nav.notifications')" to="/notifications" icon="i-ri:notification-4-line" user-only :command="command"> <NavSideItem :text="$t('nav.notifications')" to="/notifications" icon="i-ri:notification-4-line" user-only :command="command">
<template #icon> <template #icon>
@ -29,10 +28,10 @@ const { notifications } = useNotifications()
<NavSideItem :text="$t('action.compose')" to="/compose" icon="i-ri:quill-pen-line" user-only :command="command" /> <NavSideItem :text="$t('action.compose')" to="/compose" icon="i-ri:quill-pen-line" user-only :command="command" />
<div shrink hidden sm:block mt-4 /> <div shrink hidden sm:block mt-4 />
<NavSideItem :text="$t('nav.explore')" :to="isHydrated ? `/${currentServer}/explore` : '/explore'" icon="i-ri:hashtag" :command="command" /> <NavSideItem :text="$t('nav.explore')" :to="isHydrated ? `/${currentServer}/explore` : '/explore'" icon="i-ri:hashtag" :command="command" xs:hidden sm:hidden xl:block />
<NavSideItem :text="$t('nav.local')" :to="isHydrated ? `/${currentServer}/public/local` : '/public/local'" icon="i-ri:group-2-line " :command="command" /> <NavSideItem :text="$t('nav.local')" :to="isHydrated ? `/${currentServer}/public/local` : '/public/local'" icon="i-ri:group-2-line " :command="command" />
<NavSideItem :text="$t('nav.federated')" :to="isHydrated ? `/${currentServer}/public` : '/public'" icon="i-ri:earth-line" :command="command" /> <NavSideItem :text="$t('nav.federated')" :to="isHydrated ? `/${currentServer}/public` : '/public'" icon="i-ri:earth-line" :command="command" />
<NavSideItem :text="$t('nav.lists')" :to="`/${currentServer}/lists`" icon="i-ri:list-check" :command="command" /> <NavSideItem :text="$t('nav.lists')" :to="isHydrated ? `/${currentServer}/lists` : '/lists'" icon="i-ri:list-check" user-only :command="command" />
<div shrink hidden sm:block mt-4 /> <div shrink hidden sm:block mt-4 />
<NavSideItem :text="$t('nav.settings')" to="/settings" icon="i-ri:settings-3-line" :command="command" /> <NavSideItem :text="$t('nav.settings')" to="/settings" icon="i-ri:settings-3-line" :command="command" />

View file

@ -2,6 +2,13 @@
const { env } = useBuildInfo() const { env } = useBuildInfo()
const router = useRouter() const router = useRouter()
const back = ref<any>('') const back = ref<any>('')
const nuxtApp = useNuxtApp()
const onClickLogo = () => {
nuxtApp.hooks.callHook('elk-logo:click')
}
onMounted(() => { onMounted(() => {
back.value = router.options.history.state.back back.value = router.options.history.state.back
}) })
@ -11,16 +18,15 @@ router.afterEach(() => {
</script> </script>
<template> <template>
<!-- Use external to force refresh page and jump to top of timeline --> <div flex justify-between sticky top-0 bg-base z-1 py-4 native:py-7 data-tauri-drag-region>
<div flex justify-between>
<NuxtLink <NuxtLink
flex items-end gap-4 flex items-end gap-3
py2 px-5 py2 px-5
text-2xl text-2xl
select-none select-none
focus-visible:ring="2 current" focus-visible:ring="2 current"
to="/" to="/home"
external @click.prevent="onClickLogo"
> >
<NavLogo shrink-0 aspect="1/1" sm:h-8 xl:h-10 class="rtl-flip" /> <NavLogo shrink-0 aspect="1/1" sm:h-8 xl:h-10 class="rtl-flip" />
<div hidden xl:block text-secondary> <div hidden xl:block text-secondary>

View file

@ -1,3 +1,7 @@
<script setup>
const { busy, oauth, singleInstanceServer } = useSignIn()
</script>
<template> <template>
<VDropdown v-if="isHydrated && currentUser" sm:hidden> <VDropdown v-if="isHydrated && currentUser" sm:hidden>
<div style="-webkit-touch-callout: none;"> <div style="-webkit-touch-callout: none;">
@ -15,7 +19,24 @@
<UserSwitcher ref="switcher" @click="hide()" /> <UserSwitcher ref="switcher" @click="hide()" />
</template> </template>
</VDropdown> </VDropdown>
<template v-else>
<button
v-if="singleInstanceServer"
flex="~ row"
gap-x-1 items-center justify-center btn-solid text-sm px-2 py-1 xl:hidden
:disabled="busy"
@click="oauth()"
>
<span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
<span block i-ri:loader-2-fill aria-hidden="true" />
</span>
<span v-else aria-hidden="true" block i-ri:login-circle-line class="rtl-flip" />
<i18n-t keypath="action.sign_in_to">
<strong>{{ currentServer }}</strong>
</i18n-t>
</button>
<button v-else btn-solid text-sm px-2 py-1 text-center xl:hidden @click="openSigninDialog()"> <button v-else btn-solid text-sm px-2 py-1 text-center xl:hidden @click="openSigninDialog()">
{{ $t('action.sign_in') }} {{ $t('action.sign_in') }}
</button> </button>
</template>
</template> </template>

View file

@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { PushSubscriptionError } from '~/composables/push-notifications/types'
defineProps<{ show?: boolean }>() defineProps<{ show?: boolean }>()
const { const {
@ -17,7 +15,7 @@ const {
} = usePushManager() } = usePushManager()
const { t } = useI18n() const { t } = useI18n()
const pwaEnabled = useRuntimeConfig().public.pwaEnabled const pwaEnabled = useAppConfig().pwaEnabled
let busy = $ref<boolean>(false) let busy = $ref<boolean>(false)
let animateSave = $ref<boolean>(false) let animateSave = $ref<boolean>(false)

View file

@ -36,5 +36,11 @@ const { modelValue } = defineModel<{
</CommonTooltip> </CommonTooltip>
</head> </head>
<p>{{ message }}</p> <p>{{ message }}</p>
<p py-2>
<NuxtLink font-bold text-primary href="https://github.com/elk-zone/elk" target="_blank" flex="~ row" items-center gap-x-2>
{{ $t('settings.notifications.push_notifications.subscription_error.repo_link') }}
<span inline-block aria-hidden="true" i-ri:external-link-line class="rtl-flip" />
</NuxtLink>
</p>
</div> </div>
</template> </template>

View file

@ -34,7 +34,7 @@ const toggleApply = () => {
text-white px2 py2 rounded-full cursor-pointer text-white px2 py2 rounded-full cursor-pointer
@click="$emit('remove')" @click="$emit('remove')"
> >
<div i-ri:close-line text-3 :class="[isHydrated && isSmallScreen ? 'text-6' : 'text-3']" /> <div i-ri:close-line text-3 text-6 md:text-3 />
</div> </div>
</div> </div>
<div absolute right-2 bottom-2> <div absolute right-2 bottom-2>

View file

@ -0,0 +1,52 @@
<script setup lang="ts">
import type { Editor } from '@tiptap/core'
const { editor } = defineProps<{
editor: Editor
}>()
</script>
<template>
<CommonTooltip placement="top" :content="$t('tooltip.open_editor_tools')">
<VDropdown v-if="editor" placement="top">
<button
btn-action-icon
>
<div i-ri:font-size-2 />
</button>
<template #popper>
<div flex gap-1>
<CommonTooltip placement="top" :content="$t('tooltip.toggle_code_block')">
<button
btn-action-icon
:aria-label="$t('tooltip.toggle_code_block')"
:class="editor.isActive('codeBlock') ? 'text-primary' : ''"
@click="editor?.chain().focus().toggleCodeBlock().run()"
>
<div i-ri:code-s-slash-line />
</button>
</CommonTooltip>
<CommonTooltip placement="top" :content="$t('tooltip.toggle_bold')">
<button
btn-action-icon
:aria-label="$t('tooltip.toggle_bold')"
:class="editor.isActive('bold') ? 'text-primary' : ''"
@click="editor?.chain().focus().toggleBold().run()"
>
<div i-ri:bold />
</button>
</CommonTooltip>
<CommonTooltip placement="top" :content="$t('tooltip.toggle_italic')">
<button
btn-action-icon
:aria-label="$t('tooltip.toggle_italic')"
:class="editor.isActive('italic') ? 'text-primary' : ''"
@click="editor?.chain().focus().toggleItalic().run()"
>
<div i-ri:italic />
</button>
</CommonTooltip>
</div>
</template>
</VDropdown>
</CommonTooltip>
</template>

View file

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import ISO6391 from 'iso-639-1'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
let { modelValue } = $defineModel<{ let { modelValue } = $defineModel<{
@ -7,20 +6,11 @@ let { modelValue } = $defineModel<{
}>() }>()
const { t } = useI18n() const { t } = useI18n()
const userSettings = useUserSettings()
const languageKeyword = $ref('') const languageKeyword = $ref('')
const languageList: { const fuse = new Fuse(languagesNameList, {
code: string
nativeName: string
name: string
}[] = ISO6391.getAllCodes().map(code => ({
code,
nativeName: ISO6391.getNativeName(code),
name: ISO6391.getName(code),
}))
const fuse = new Fuse(languageList, {
keys: ['code', 'nativeName', 'name'], keys: ['code', 'nativeName', 'name'],
shouldSort: true, shouldSort: true,
}) })
@ -28,25 +18,52 @@ const fuse = new Fuse(languageList, {
const languages = $computed(() => const languages = $computed(() =>
languageKeyword.trim() languageKeyword.trim()
? fuse.search(languageKeyword).map(r => r.item) ? fuse.search(languageKeyword).map(r => r.item)
: [...languageList].sort(({ code: a }, { code: b }) => { : [...languagesNameList].filter(entry => !userSettings.value.disabledTranslationLanguages.includes(entry.code))
.sort(({ code: a }, { code: b }) => {
return a === modelValue ? -1 : b === modelValue ? 1 : a.localeCompare(b) return a === modelValue ? -1 : b === modelValue ? 1 : a.localeCompare(b)
}), }),
) )
const preferredLanguages = computed(() => {
const result = []
for (const langCode of userSettings.value.disabledTranslationLanguages) {
const completeLang = languagesNameList.find(listEntry => listEntry.code === langCode)
if (completeLang)
result.push(completeLang)
}
return result
},
)
function chooseLanguage(language: string) { function chooseLanguage(language: string) {
modelValue = language modelValue = language
} }
</script> </script>
<template> <template>
<div> <div relative of-x-hidden>
<div p2>
<input <input
v-model="languageKeyword" v-model="languageKeyword"
:placeholder="t('language.search')" :placeholder="t('language.search')"
p2 mb2 border-rounded w-full bg-transparent p2 border-rounded w-full bg-transparent
outline-none border="~ base" outline-none border="~ base"
> >
</div>
<div max-h-40vh overflow-auto> <div max-h-40vh overflow-auto>
<template v-if="!languageKeyword.trim()">
<CommonDropdownItem
v-for="{ code, nativeName, name } in preferredLanguages"
:key="code"
:text="nativeName"
:description="name"
:checked="code === modelValue"
@click="chooseLanguage(code)"
/>
<hr class="border-base ">
</template>
<CommonDropdownItem <CommonDropdownItem
v-for="{ code, nativeName, name } in languages" v-for="{ code, nativeName, name } in languages"
:key="code" :key="code"

View file

@ -6,7 +6,7 @@ import type { Draft } from '~/types'
const { const {
draftKey, draftKey,
initial = getDefaultDraft() as never /* Bug of vue-core */, initial = getDefaultDraft,
expanded = false, expanded = false,
placeholder, placeholder,
dialogLabelledBy, dialogLabelledBy,
@ -35,7 +35,7 @@ const {
dropZoneRef, dropZoneRef,
} = $(useUploadMediaAttachment($$(draft))) } = $(useUploadMediaAttachment($$(draft)))
let { shouldExpanded, isExpanded, isSending, isPublishDisabled, publishDraft, failedMessages } = $(usePublish( let { shouldExpanded, isExpanded, isSending, isPublishDisabled, publishDraft, failedMessages, preferredLanguage, publishSpoilerText } = $(usePublish(
{ {
draftState, draftState,
...$$({ expanded, isUploading, initialDraft: initial }), ...$$({ expanded, isUploading, initialDraft: initial }),
@ -62,6 +62,7 @@ const { editor } = useTiptap({
}, },
onPaste: handlePaste, onPaste: handlePaste,
}) })
const characterCount = $computed(() => { const characterCount = $computed(() => {
let length = stringLength(htmlToText(editor.value?.getHTML() || '')) let length = stringLength(htmlToText(editor.value?.getHTML() || ''))
@ -73,9 +74,13 @@ const characterCount = $computed(() => {
}).join(' ').length + 1 }).join(' ').length + 1
} }
length += stringLength(publishSpoilerText)
return length return length
}) })
const postLanguageDisplay = $computed(() => languagesNameList.find(i => i.code === (draft.params.language || preferredLanguage))?.nativeName)
async function handlePaste(evt: ClipboardEvent) { async function handlePaste(evt: ClipboardEvent) {
const files = evt.clipboardData?.files const files = evt.clipboardData?.files
if (!files || files.length === 0) if (!files || files.length === 0)
@ -108,10 +113,16 @@ useWebShareTarget(async ({ data: { data, action } }: any) => {
editor.value?.commands.focus('end') editor.value?.commands.focus('end')
if (data.text !== undefined) for (const text of data.textParts) {
editor.value?.commands.insertContent(data.text) for (const line of text.split('\n')) {
editor.value?.commands.insertContent({
type: 'paragraph',
content: [{ type: 'text', text: line }],
})
}
}
if (data.files !== undefined) if (data.files.length !== 0)
await uploadAttachments(data.files) await uploadAttachments(data.files)
}) })
@ -147,13 +158,13 @@ defineExpose({
> >
<ContentMentionGroup v-if="draft.mentions?.length && shouldExpanded" replying> <ContentMentionGroup v-if="draft.mentions?.length && shouldExpanded" replying>
<button v-for="m, i of draft.mentions" :key="m" text-primary hover:color-red @click="draft.mentions?.splice(i, 1)"> <button v-for="m, i of draft.mentions" :key="m" text-primary hover:color-red @click="draft.mentions?.splice(i, 1)">
{{ acctToShortHandle(m) }} {{ accountToShortHandle(m) }}
</button> </button>
</ContentMentionGroup> </ContentMentionGroup>
<div v-if="draft.params.sensitive"> <div v-if="draft.params.sensitive">
<input <input
v-model="draft.params.spoilerText" v-model="publishSpoilerText"
type="text" type="text"
:placeholder="$t('placeholder.content_warning')" :placeholder="$t('placeholder.content_warning')"
p2 border-rounded w-full bg-transparent p2 border-rounded w-full bg-transparent
@ -161,8 +172,8 @@ defineExpose({
> >
</div> </div>
<PublishErrMessage v-if="failedMessages.length > 0" described-by="publish-failed"> <CommonErrorMessage v-if="failedMessages.length > 0" described-by="publish-failed">
<head id="publish-failed" flex justify-between> <header id="publish-failed" flex justify-between>
<div flex items-center gap-x-2 font-bold> <div flex items-center gap-x-2 font-bold>
<div aria-hidden="true" i-ri:error-warning-fill /> <div aria-hidden="true" i-ri:error-warning-fill />
<p>{{ $t('state.publish_failed') }}</p> <p>{{ $t('state.publish_failed') }}</p>
@ -175,14 +186,14 @@ defineExpose({
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line /> <span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
</button> </button>
</CommonTooltip> </CommonTooltip>
</head> </header>
<ol ps-2 sm:ps-1> <ol ps-2 sm:ps-1>
<li v-for="(error, i) in failedMessages" :key="i" flex="~ col sm:row" gap-y-1 sm:gap-x-2> <li v-for="(error, i) in failedMessages" :key="i" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
<strong>{{ i + 1 }}.</strong> <strong>{{ i + 1 }}.</strong>
<span>{{ error }}</span> <span>{{ error }}</span>
</li> </li>
</ol> </ol>
</PublishErrMessage> </CommonErrorMessage>
<div relative flex-1 flex flex-col> <div relative flex-1 flex flex-col>
<EditorContent <EditorContent
@ -198,11 +209,11 @@ defineExpose({
</div> </div>
{{ $t('state.uploading') }} {{ $t('state.uploading') }}
</div> </div>
<PublishErrMessage <CommonErrorMessage
v-else-if="failedAttachments.length > 0" v-else-if="failedAttachments.length > 0"
:described-by="isExceedingAttachmentLimit ? 'upload-failed uploads-per-post' : 'upload-failed'" :described-by="isExceedingAttachmentLimit ? 'upload-failed uploads-per-post' : 'upload-failed'"
> >
<head id="upload-failed" flex justify-between> <header id="upload-failed" flex justify-between>
<div flex items-center gap-x-2 font-bold> <div flex items-center gap-x-2 font-bold>
<div aria-hidden="true" i-ri:error-warning-fill /> <div aria-hidden="true" i-ri:error-warning-fill />
<p>{{ $t('state.upload_failed') }}</p> <p>{{ $t('state.upload_failed') }}</p>
@ -215,7 +226,7 @@ defineExpose({
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line /> <span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
</button> </button>
</CommonTooltip> </CommonTooltip>
</head> </header>
<div v-if="isExceedingAttachmentLimit" id="uploads-per-post" ps-2 sm:ps-1 text-small> <div v-if="isExceedingAttachmentLimit" id="uploads-per-post" ps-2 sm:ps-1 text-small>
{{ $t('state.attachments_exceed_server_limit') }} {{ $t('state.attachments_exceed_server_limit') }}
</div> </div>
@ -225,7 +236,7 @@ defineExpose({
<span>{{ error[0] }}</span> <span>{{ error[0] }}</span>
</li> </li>
</ol> </ol>
</PublishErrMessage> </CommonErrorMessage>
<div v-if="draft.attachments.length" flex="~ col gap-2" overflow-auto> <div v-if="draft.attachments.length" flex="~ col gap-2" overflow-auto>
<PublishAttachment <PublishAttachment
@ -259,18 +270,7 @@ defineExpose({
</button> </button>
</CommonTooltip> </CommonTooltip>
<template v-if="editor"> <PublishEditorTools v-if="editor" :editor="editor" />
<CommonTooltip placement="top" :content="$t('tooltip.toggle_code_block')">
<button
btn-action-icon
:aria-label="$t('tooltip.toggle_code_block')"
:class="editor.isActive('codeBlock') ? 'text-primary' : ''"
@click="editor?.chain().focus().toggleCodeBlock().run()"
>
<div i-ri:code-s-slash-line />
</button>
</CommonTooltip>
</template>
<div flex-auto /> <div flex-auto />
@ -278,6 +278,20 @@ defineExpose({
{{ characterCount ?? 0 }}<span text-secondary-light>/</span><span text-secondary-light>{{ characterLimit }}</span> {{ characterCount ?? 0 }}<span text-secondary-light>/</span><span text-secondary-light>{{ characterLimit }}</span>
</div> </div>
<CommonTooltip placement="top" :content="$t('tooltip.change_language')">
<CommonDropdown placement="bottom" auto-boundary-max-size>
<button btn-action-icon :aria-label="$t('tooltip.change_language')" w-max mr1>
<span v-if="postLanguageDisplay" text-secondary text-sm ml1>{{ postLanguageDisplay }}</span>
<div v-else i-ri:translate-2 />
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
</button>
<template #popper>
<PublishLanguagePicker v-model="draft.params.language" min-w-80 />
</template>
</CommonDropdown>
</CommonTooltip>
<CommonTooltip placement="top" :content="$t('tooltip.add_content_warning')"> <CommonTooltip placement="top" :content="$t('tooltip.add_content_warning')">
<button btn-action-icon :aria-label="$t('tooltip.add_content_warning')" @click="toggleSensitive"> <button btn-action-icon :aria-label="$t('tooltip.add_content_warning')" @click="toggleSensitive">
<div v-if="draft.params.sensitive" i-ri:alarm-warning-fill text-orange /> <div v-if="draft.params.sensitive" i-ri:alarm-warning-fill text-orange />
@ -285,19 +299,6 @@ defineExpose({
</button> </button>
</CommonTooltip> </CommonTooltip>
<CommonTooltip placement="top" :content="$t('tooltip.change_language')">
<CommonDropdown placement="bottom" auto-boundary-max-size>
<button btn-action-icon :aria-label="$t('tooltip.change_language')" w-12 mr--1>
<div i-ri:translate-2 />
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
</button>
<template #popper>
<PublishLanguagePicker v-model="draft.params.language" min-w-80 p3 />
</template>
</CommonDropdown>
</CommonTooltip>
<PublishVisibilityPicker v-model="draft.params.visibility" :editing="!!draft.editingStatus"> <PublishVisibilityPicker v-model="draft.params.visibility" :editing="!!draft.editingStatus">
<template #default="{ visibility }"> <template #default="{ visibility }">
<button :disabled="!!draft.editingStatus" :aria-label="$t('tooltip.change_content_visibility')" btn-action-icon :class="{ 'w-12': !draft.editingStatus }"> <button :disabled="!!draft.editingStatus" :aria-label="$t('tooltip.change_content_visibility')" btn-action-icon :class="{ 'w-12': !draft.editingStatus }">

View file

@ -5,9 +5,14 @@ const index = ref(0)
const { t } = useI18n() const { t } = useI18n()
const el = ref<HTMLElement>() const el = ref<HTMLElement>()
const input = ref<HTMLInputElement>()
const router = useRouter() const router = useRouter()
const { focused } = useFocusWithin(el) const { focused } = useFocusWithin(el)
defineExpose({
input,
})
const results = computed(() => { const results = computed(() => {
if (query.value.length === 0) if (query.value.length === 0)
return [] return []
@ -68,6 +73,7 @@ const activate = () => {
bg-transparent bg-transparent
outline="focus:none" outline="focus:none"
pe-4 pe-4
ml-1
:placeholder="isHydrated ? t('nav.search') : ''" :placeholder="isHydrated ? t('nav.search') : ''"
pb="1px" pb="1px"
placeholder-text-secondary placeholder-text-secondary
@ -77,7 +83,7 @@ const activate = () => {
> >
</div> </div>
<!-- Results --> <!-- Results -->
<div left-0 top-12 absolute w-full z10 group-focus-within="pointer-events-auto visible" invisible pointer-events-none> <div left-0 top-11 absolute w-full z10 group-focus-within="pointer-events-auto visible" invisible pointer-events-none>
<div w-full bg-base border="~ base" rounded-3 max-h-100 overflow-auto py2> <div w-full bg-base border="~ base" rounded-3 max-h-100 overflow-auto py2>
<span v-if="query.trim().length === 0" block text-center text-sm text-secondary> <span v-if="query.trim().length === 0" block text-center text-sm text-secondary>
{{ t('search.search_desc') }} {{ t('search.search_desc') }}

View file

@ -9,7 +9,7 @@ defineProps<{
<template> <template>
<button <button
exact-active-class="text-primary" exact-active-class="text-primary"
block w-full group focus:outline-none block w-full group focus:outline-none text-start
> >
<div <div
w-full flex w-fit px5 py3 md:gap2 gap4 items-center w-full flex w-fit px5 py3 md:gap2 gap4 items-center
@ -18,7 +18,8 @@ defineProps<{
> >
<div flex-1 flex items-center md:gap2 gap4> <div flex-1 flex items-center md:gap2 gap4>
<div <div
flex items-center justify-center flex-shrink-0 v-if="icon" flex items-center justify-center
flex-shrink-0
:class="$slots.description ? 'w-12 h-12' : ''" :class="$slots.description ? 'w-12 h-12' : ''"
> >
<slot name="icon"> <slot name="icon">

View file

@ -0,0 +1,63 @@
<script lang="ts" setup>
import ISO6391 from 'iso-639-1'
const supportedTranslationLanguages = ISO6391.getLanguages([...supportedTranslationCodes])
const userSettings = useUserSettings()
const language = ref<string | null>(null)
const availableOptions = computed(() => {
return Object.values(supportedTranslationLanguages).filter((value) => {
return !userSettings.value.disabledTranslationLanguages.includes(value.code)
})
})
function addDisabledTranslation() {
if (language.value) {
const uniqueValues = new Set(userSettings.value.disabledTranslationLanguages)
uniqueValues.add(language.value)
userSettings.value.disabledTranslationLanguages = [...uniqueValues]
language.value = null
}
}
function removeDisabledTranslation(code: string) {
const uniqueValues = new Set(userSettings.value.disabledTranslationLanguages)
uniqueValues.delete(code)
userSettings.value.disabledTranslationLanguages = [...uniqueValues]
}
</script>
<template>
<div>
<CommonCheckbox v-model="userSettings.preferences.hideTranslation" :label="$t('settings.preferences.hide_translation')" />
<div v-if="!userSettings.preferences.hideTranslation" class="mt-1 ms-2">
<p class=" mb-2">
{{ $t('settings.language.translations.hide_specific') }}
</p>
<div class="ms-4">
<ul>
<li v-for="langCode in userSettings.disabledTranslationLanguages" :key="langCode" class="flex items-center">
<div>{{ ISO6391.getNativeName(langCode) }}</div>
<button class="btn-text" type="button" :title="$t('settings.language.translations.remove')" @click.prevent="removeDisabledTranslation(langCode)">
<span class="block i-ri:close-line" aria-hidden="true" />
</button>
</li>
</ul>
<div class="flex items-center mt-2">
<select v-model="language" class="select-settings">
<option disabled selected :value="null">
{{ $t('settings.language.translations.choose_language') }}
</option>
<option v-for="availableOption in availableOptions" :key="availableOption.code" :value="availableOption.code">
{{ availableOption.nativeName }}
</option>
</select>
<button class="btn-text" @click="addDisabledTranslation">
{{ $t('settings.language.translations.add') }}
</button>
</div>
</div>
</div>
</div>
</template>

View file

@ -5,6 +5,8 @@ const { account, link = true } = defineProps<{
account: mastodon.v1.Account account: mastodon.v1.Account
link?: boolean link?: boolean
}>() }>()
const userSettings = useUserSettings()
</script> </script>
<template> <template>
@ -13,7 +15,7 @@ const { account, link = true } = defineProps<{
flex="~ col" min-w-0 md:flex="~ row gap-2" md:items-center flex="~ col" min-w-0 md:flex="~ row gap-2" md:items-center
text-link-rounded text-link-rounded
> >
<AccountDisplayName :account="account" font-bold line-clamp-1 ws-pre-wrap break-all /> <AccountDisplayName :account="account" :hide-emojis="getPreferences(userSettings, 'hideUsernameEmojis')" font-bold line-clamp-1 ws-pre-wrap break-all />
<AccountHandle :account="account" class="zen-none" /> <AccountHandle :account="account" class="zen-none" />
</NuxtLink> </NuxtLink>
</template> </template>

View file

@ -49,7 +49,7 @@ useCommand({
<component <component
:is="as" :is="as"
v-bind="$attrs" ref="el" v-bind="$attrs" ref="el"
w-fit flex gap-1 items-center transition-all w-fit flex gap-1 items-center transition-all select-none
rounded group rounded group
:hover=" !disabled ? hover : undefined" :hover=" !disabled ? hover : undefined"
focus:outline-none focus:outline-none

View file

@ -87,6 +87,8 @@ useIntersectionObserver(video, (entries) => {
} }
}) })
}, { threshold: 0.75 }) }, { threshold: 0.75 })
const userSettings = useUserSettings()
</script> </script>
<template> <template>
@ -167,7 +169,7 @@ useIntersectionObserver(video, (entries) => {
/> />
</button> </button>
</template> </template>
<div v-if="attachment.description" :class="isAudio ? '' : 'absolute left-2 bottom-2'"> <div v-if="attachment.description && !getPreferences(userSettings, 'hideAltIndicatorOnPosts')" :class="isAudio ? '' : 'absolute left-2 bottom-2'">
<VDropdown :distance="6" placement="bottom-start"> <VDropdown :distance="6" placement="bottom-start">
<button <button
font-bold text-sm font-bold text-sm
@ -176,9 +178,9 @@ useIntersectionObserver(video, (entries) => {
: 'rounded-1 bg-black/65 text-white hover:bg-black px1.2 py0.2'" : 'rounded-1 bg-black/65 text-white hover:bg-black px1.2 py0.2'"
> >
<div hidden> <div hidden>
read {{ attachment.type }} description {{ $t('status.img_alt.read', [attachment.type]) }}
</div> </div>
ALT {{ $t('status.img_alt.ALT') }}
</button> </button>
<template #popper> <template #popper>
<div p4 flex flex-col gap-2 max-w-130> <div p4 flex flex-col gap-2 max-w-130>

View file

@ -26,7 +26,7 @@ const props = withDefaults(
const userSettings = useUserSettings() const userSettings = useUserSettings()
const status = $computed(() => { const status = $computed(() => {
if (props.status.reblog && !props.status.content) if (props.status.reblog && (!props.status.content || props.status.content === props.status.reblog.content))
return props.status.reblog return props.status.reblog
return props.status return props.status
}) })
@ -79,7 +79,8 @@ const showReplyTo = $computed(() => !replyToMain && !directReply)
<div <div
:id="`status-${status.id}`" :id="`status-${status.id}`"
ref="el" ref="el"
relative flex="~ col gap1" p="l-3 r-4 b-2" relative flex="~ col gap1"
p="b-2 is-3 ie-4"
:class="{ 'hover:bg-active': hover }" :class="{ 'hover:bg-active': hover }"
tabindex="0" tabindex="0"
focus:outline-none focus-visible:ring="2 primary" focus:outline-none focus-visible:ring="2 primary"
@ -95,12 +96,12 @@ const showReplyTo = $computed(() => !replyToMain && !directReply)
<template v-if="status.inReplyToAccountId"> <template v-if="status.inReplyToAccountId">
<StatusReplyingTo <StatusReplyingTo
v-if="showReplyTo" v-if="showReplyTo"
ml-20px pt-1 pl-5 m="is-5" p="t-1 is-5"
:status="status" :status="status"
:is-self-reply="isSelfReply" :is-self-reply="isSelfReply"
:class="faded ? 'text-secondary-light' : ''" :class="faded ? 'text-secondary-light' : ''"
/> />
<div flex="~ col gap-1" items-center pos="absolute top-0 left-0" w="77px" z--1> <div flex="~ col gap-1" items-center pos="absolute top-0 inset-is-0" w="77px" z--1>
<template v-if="showReplyTo"> <template v-if="showReplyTo">
<div w="1px" h="0.5" border="x base" mt-3 /> <div w="1px" h="0.5" border="x base" mt-3 />
<div w="1px" h="0.5" border="x base" /> <div w="1px" h="0.5" border="x base" />

View file

@ -15,7 +15,11 @@ const filterResult = $computed(() => status.filtered?.length ? status.filtered[0
const filter = $computed(() => filterResult?.filter) const filter = $computed(() => filterResult?.filter)
const filterPhrase = $computed(() => filter?.title) const filterPhrase = $computed(() => filter?.title)
const isFiltered = $computed(() => filterPhrase && (context && context !== 'details' ? filter?.context.includes(context) : false)) const isFiltered = $computed(() => status.account.id !== currentUser.value?.account.id && filterPhrase && context && context !== 'details' && !!filter?.context.includes(context))
// check spoiler text or media attachment
// needed to handle accounts that mark all their posts as sensitive
const hasSensitiveSpoilerOrMedia = $computed(() => status.sensitive && (!!status.spoilerText || !!status.mediaAttachments.length))
const cleanSharedLink = !status.poll const cleanSharedLink = !status.poll
&& !status.mediaAttachments.length && !status.mediaAttachments.length
@ -31,13 +35,13 @@ const cleanSharedLink = !status.poll
}" }"
> >
<StatusBody v-if="!isFiltered && status.sensitive && !status.spoilerText" :status="status" :newer="newer" :with-action="!isDetails" :class="isDetails ? 'text-xl' : ''" /> <StatusBody v-if="!isFiltered && status.sensitive && !status.spoilerText" :status="status" :newer="newer" :with-action="!isDetails" :class="isDetails ? 'text-xl' : ''" />
<StatusSpoiler :enabled="status.sensitive || isFiltered" :filter="isFiltered" :is-d-m="isDM"> <StatusSpoiler :enabled="hasSensitiveSpoilerOrMedia || isFiltered" :filter="isFiltered" :is-d-m="isDM">
<template v-if="filterPhrase" #spoiler> <template v-if="status.spoilerText" #spoiler>
<p>{{ `${$t('status.filter_hidden_phrase')}: ${filterPhrase}` }}</p>
</template>
<template v-else-if="status.spoilerText" #spoiler>
<p>{{ status.spoilerText }}</p> <p>{{ status.spoilerText }}</p>
</template> </template>
<template v-else-if="filterPhrase" #spoiler>
<p>{{ `${$t('status.filter_hidden_phrase')}: ${filterPhrase}` }}</p>
</template>
<StatusBody v-if="!status.sensitive || status.spoilerText" :clean-shared-link="cleanSharedLink" :status="status" :newer="newer" :with-action="!isDetails" :class="isDetails ? 'text-xl' : ''" /> <StatusBody v-if="!status.sensitive || status.spoilerText" :clean-shared-link="cleanSharedLink" :status="status" :newer="newer" :with-action="!isDetails" :class="isDetails ? 'text-xl' : ''" />
<StatusTranslation :status="status" /> <StatusTranslation :status="status" />
<StatusPoll v-if="status.poll" :status="status" /> <StatusPoll v-if="status.poll" :status="status" />

View file

@ -17,6 +17,6 @@ const gitHubCards = $(usePreferences('experimentalGitHubCards'))
<template> <template>
<LazyStatusPreviewGitHub v-if="gitHubCards && providerName === 'GitHub'" :card="card" /> <LazyStatusPreviewGitHub v-if="gitHubCards && providerName === 'GitHub'" :card="card" />
<LazyStatusPreviewStackBlitz v-else-if="gitHubCards && providerName === 'stackblitz.com'" :card="card" :small-picture-only="smallPictureOnly" :root="root" /> <LazyStatusPreviewStackBlitz v-else-if="gitHubCards && providerName === 'StackBlitz'" :card="card" :small-picture-only="smallPictureOnly" :root="root" />
<StatusPreviewCardNormal v-else :card="card" :clean-shared-link="cleanSharedLink" :small-picture-only="smallPictureOnly" :root="root" /> <StatusPreviewCardNormal v-else :card="card" :clean-shared-link="cleanSharedLink" :small-picture-only="smallPictureOnly" :root="root" />
</template> </template>

View file

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
import reservedNames from 'github-reserved-names'
const props = defineProps<{ const props = defineProps<{
card: mastodon.v1.PreviewCard card: mastodon.v1.PreviewCard
@ -20,24 +21,22 @@ interface Meta {
} }
} }
const specialRoutes = ['orgs', 'sponsors', 'stars'] // Supported paths
// /user
// /user/repo
// /user/repo/issues/number
// /user/repo/pull/number
// /sponsors/user
const supportedReservedRoutes = ['sponsors']
const meta = $computed(() => { const meta = $computed(() => {
const { url } = props.card const { url } = props.card
const path = url.split('https://github.com/')[1] const path = url.split('https://github.com/')[1]
const [firstName, secondName] = path?.split('/') || []
if (!firstName || (reservedNames.check(firstName) && !supportedReservedRoutes.includes(firstName)))
return undefined
// Supported paths const firstIsUser = firstName && !supportedReservedRoutes.includes(firstName)
// /user
// /user/repo
// /user/repo/issues/number
// /user/repo/pull/number
// /orgs/user
// /sponsors/user
// /stars/user
const firstName = path.match(/([\w-]+)(\/|$)/)?.[1]
const secondName = path.match(/[\w-]+\/([\w-]+)/)?.[1]
const firstIsUser = firstName && !specialRoutes.includes(firstName)
const user = firstIsUser ? firstName : secondName const user = firstIsUser ? firstName : secondName
const repo = firstIsUser ? secondName : undefined const repo = firstIsUser ? secondName : undefined
@ -86,7 +85,7 @@ const meta = $computed(() => {
<template> <template>
<div <div
v-if="card.image" v-if="card.image && meta"
flex flex-col flex flex-col
display-block of-hidden display-block of-hidden
bg-card bg-card
@ -132,4 +131,5 @@ const meta = $computed(() => {
</div> </div>
</div> </div>
</div> </div>
<StatusPreviewCardNormal v-else :card="card" />
</template> </template>

View file

@ -21,9 +21,9 @@ const maxLines = 20
const meta = $computed(() => { const meta = $computed(() => {
const { description } = props.card const { description } = props.card
const meta = description.match(/.+\n\nCode Snippet from (.+), lines ([\w-]+)\n\n(.+)/s) const meta = description.match(/.*Code Snippet from (.+), lines (\S+)\n\n(.+)/s)
const file = meta?.[1] const file = meta?.[1]
const lines = meta?.[2].replaceAll('N', '') const lines = meta?.[2]
const code = meta?.[3].split('\n').slice(0, maxLines).join('\n') const code = meta?.[3].split('\n').slice(0, maxLines).join('\n')
const project = props.card.title?.replace(' - StackBlitz', '') const project = props.card.title?.replace(' - StackBlitz', '')
const info = $ref<Meta>({ const info = $ref<Meta>({
@ -38,7 +38,12 @@ const meta = $computed(() => {
const vnodeCode = $computed(() => { const vnodeCode = $computed(() => {
if (!meta.code) if (!meta.code)
return null return null
const vnode = contentToVNode(`<p>\`\`\`${meta.file?.split('.')?.[1] ?? ''}\n${meta.code}\n\`\`\`\</p>`, { const code = meta.code
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/`/g, '&#96;')
const vnode = contentToVNode(`<p>\`\`\`${meta.file?.split('.')?.[1] ?? ''}\n${code}\n\`\`\`\</p>`, {
markdown: true, markdown: true,
}) })
return vnode return vnode

View file

@ -12,7 +12,7 @@ const {
} = useTranslation(status, getLanguageCode()) } = useTranslation(status, getLanguageCode())
const preferenceHideTranslation = usePreferences('hideTranslation') const preferenceHideTranslation = usePreferences('hideTranslation')
const showButton = computed(() => !preferenceHideTranslation.value && isTranslationEnabled && status.language !== getLanguageCode()) const showButton = computed(() => !preferenceHideTranslation.value && isTranslationEnabled)
let translating = $ref(false) let translating = $ref(false)
const toggleTranslation = async () => { const toggleTranslation = async () => {

View file

@ -40,6 +40,9 @@ watch(items, () => {
}) })
function onKeyDown(event: KeyboardEvent) { function onKeyDown(event: KeyboardEvent) {
if (items.length === 0)
return false
if (event.key === 'ArrowUp') { if (event.key === 'ArrowUp') {
selectedIndex = ((selectedIndex + items.length) - 1) % items.length selectedIndex = ((selectedIndex + items.length) - 1) % items.length
return true return true

View file

@ -15,6 +15,9 @@ watch(items, () => {
}) })
function onKeyDown(event: KeyboardEvent) { function onKeyDown(event: KeyboardEvent) {
if (items.length === 0)
return false
if (event.key === 'ArrowUp') { if (event.key === 'ArrowUp') {
selectedIndex = ((selectedIndex + items.length) - 1) % items.length selectedIndex = ((selectedIndex + items.length) - 1) % items.length
return true return true

View file

@ -15,6 +15,9 @@ watch(items, () => {
}) })
function onKeyDown(event: KeyboardEvent) { function onKeyDown(event: KeyboardEvent) {
if (items.length === 0)
return false
if (event.key === 'ArrowUp') { if (event.key === 'ArrowUp') {
selectedIndex = ((selectedIndex + items.length) - 1) % items.length selectedIndex = ((selectedIndex + items.length) - 1) % items.length
return true return true

View file

@ -5,7 +5,7 @@ const all = useUsers()
const router = useRouter() const router = useRouter()
const clickUser = (user: UserLogin) => { const clickUser = (user: UserLogin) => {
if (user.account.id === currentUser.value?.account.id) if (user.account.acct === currentUser.value?.account.acct)
router.push(getAccountRoute(user.account)) router.push(getAccountRoute(user.account))
else else
switchUser(user) switchUser(user)
@ -21,7 +21,7 @@ const clickUser = (user: UserLogin) => {
flex rounded flex rounded
cursor-pointer cursor-pointer
aria-label="Switch user" aria-label="Switch user"
:class="user.account.id === currentUser?.account.id ? '' : 'op25 grayscale'" :class="user.account.acct === currentUser?.account.acct ? '' : 'op25 grayscale'"
hover="filter-none op100" hover="filter-none op100"
@click="clickUser(user)" @click="clickUser(user)"
> >

View file

@ -1,66 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { $fetch } from 'ofetch'
const input = $ref<HTMLInputElement>() const input = ref<HTMLInputElement | undefined>()
let server = $ref<string>('')
let busy = $ref<boolean>(false)
let error = $ref<boolean>(false)
let displayError = $ref<boolean>(false)
let knownServers = $ref<string[]>([]) let knownServers = $ref<string[]>([])
let autocompleteIndex = $ref(0) let autocompleteIndex = $ref(0)
let autocompleteShow = $ref(false) let autocompleteShow = $ref(false)
const users = useUsers() const { busy, error, displayError, server, oauth } = useSignIn(input)
const userSettings = useUserSettings()
async function oauth() {
if (busy)
return
busy = true
error = false
displayError = false
await nextTick()
if (server)
server = server.split('/')[0]
try {
const url = await (globalThis.$fetch as any)(`/api/${server || publicServer.value}/login`, {
method: 'POST',
body: {
force_login: users.value.some(u => u.server === server),
origin: location.origin,
lang: userSettings.value.language,
},
})
location.href = url
}
catch (err) {
console.error(err)
displayError = true
error = true
await nextTick()
input?.focus()
await nextTick()
setTimeout(() => {
busy = false
error = false
}, 512)
}
}
let fuse = $shallowRef(new Fuse([] as string[])) let fuse = $shallowRef(new Fuse([] as string[]))
const filteredServers = $computed(() => { const filteredServers = $computed(() => {
if (!server) if (!server.value)
return [] return []
const results = fuse.search(server, { limit: 6 }).map(result => result.item) const results = fuse.search(server.value, { limit: 6 }).map(result => result.item)
if (results[0] === server) if (results[0] === server.value)
return [] return []
return results return results
@ -78,12 +33,12 @@ function isValidUrl(str: string) {
} }
async function handleInput() { async function handleInput() {
const input = server.trim() const input = server.value.trim()
if (input.startsWith('https://')) if (input.startsWith('https://'))
server = input.replace('https://', '') server.value = input.replace('https://', '')
if (input.length) if (input.length)
displayError = false displayError.value = false
if ( if (
isValidUrl(`https://${input}`) isValidUrl(`https://${input}`)
@ -110,7 +65,7 @@ function move(delta: number) {
function onEnter(e: KeyboardEvent) { function onEnter(e: KeyboardEvent) {
if (autocompleteShow === true && filteredServers[autocompleteIndex]) { if (autocompleteShow === true && filteredServers[autocompleteIndex]) {
server = filteredServers[autocompleteIndex] server.value = filteredServers[autocompleteIndex]
e.preventDefault() e.preventDefault()
autocompleteShow = false autocompleteShow = false
} }
@ -124,16 +79,16 @@ function escapeAutocomplete(evt: KeyboardEvent) {
} }
function select(index: number) { function select(index: number) {
server = filteredServers[index] server.value = filteredServers[index]
} }
onMounted(async () => { onMounted(async () => {
input?.focus() input?.value?.focus()
knownServers = await (globalThis.$fetch as any)('/api/list-servers') knownServers = await (globalThis.$fetch as any)('/api/list-servers')
fuse = new Fuse(knownServers, { shouldSort: true }) fuse = new Fuse(knownServers, { shouldSort: true })
}) })
onClickOutside($$(input), () => { onClickOutside(input, () => {
autocompleteShow = false autocompleteShow = false
}) })
</script> </script>

View file

@ -1,3 +1,7 @@
<script setup lang="ts">
const { busy, oauth, singleInstanceServer } = useSignIn()
</script>
<template> <template>
<div p8 lg:flex="~ col gap2" hidden> <div p8 lg:flex="~ col gap2" hidden>
<p v-if="isHydrated" text-sm> <p v-if="isHydrated" text-sm>
@ -8,7 +12,19 @@
<p text-sm text-secondary> <p text-sm text-secondary>
{{ $t('user.sign_in_desc') }} {{ $t('user.sign_in_desc') }}
</p> </p>
<button btn-solid rounded-3 text-center mt-2 select-none @click="openSigninDialog()"> <button
v-if="singleInstanceServer"
flex="~ row" gap-x-2 items-center justify-center btn-solid text-center rounded-3
:disabled="busy"
@click="oauth()"
>
<span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
<span block i-ri:loader-2-fill aria-hidden="true" />
</span>
<span v-else aria-hidden="true" block i-ri:login-circle-line class="rtl-flip" />
{{ $t('action.sign_in') }}
</button>
<button v-else btn-solid rounded-3 text-center mt-2 select-none @click="openSigninDialog()">
{{ $t('action.sign_in') }} {{ $t('action.sign_in') }}
</button> </button>
</div> </div>

View file

@ -6,6 +6,7 @@ const emit = defineEmits<{
}>() }>()
const all = useUsers() const all = useUsers()
const { busy, singleInstanceServer, oauth } = useSignIn()
const sorted = computed(() => { const sorted = computed(() => {
return [ return [
@ -21,6 +22,12 @@ const clickUser = (user: UserLogin) => {
else else
switchUser(user) switchUser(user)
} }
const processSignIn = () => {
if (singleInstanceServer)
oauth()
else
openSigninDialog()
}
</script> </script>
<template> <template>
@ -43,7 +50,7 @@ const clickUser = (user: UserLogin) => {
:text="$t('user.add_existing')" :text="$t('user.add_existing')"
icon="i-ri:user-add-line" icon="i-ri:user-add-line"
w-full w-full
@click="openSigninDialog" @click="processSignIn"
/> />
<CommonDropdownItem <CommonDropdownItem
is="button" is="button"
@ -51,7 +58,7 @@ const clickUser = (user: UserLogin) => {
:text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])" :text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])"
icon="i-ri:logout-box-line rtl-flip" icon="i-ri:logout-box-line rtl-flip"
w-full w-full
@click="signout" @click="signOut"
/> />
</div> </div>
</div> </div>

View file

@ -7,7 +7,7 @@ export interface Team {
mastodon: string mastodon: string
} }
export const teams: Team[] = [ export const elkTeamMembers: Team[] = [
{ {
github: 'antfu', github: 'antfu',
display: 'Anthony Fu', display: 'Anthony Fu',
@ -35,5 +35,5 @@ export const teams: Team[] = [
].sort(() => Math.random() - 0.5) ].sort(() => Math.random() - 0.5)
export function useBuildInfo() { export function useBuildInfo() {
return useRuntimeConfig().public.buildInfo as BuildInfo return useAppConfig().buildInfo as BuildInfo
} }

View file

@ -59,7 +59,8 @@ export function fetchAccountById(id?: string | null): Promise<mastodon.v1.Accoun
export async function fetchAccountByHandle(acct: string): Promise<mastodon.v1.Account> { export async function fetchAccountByHandle(acct: string): Promise<mastodon.v1.Account> {
const server = currentServer.value const server = currentServer.value
const userId = currentUser.value?.account.id const userId = currentUser.value?.account.id
const key = `${server}:${userId}:account:${acct}` const userAcct = acct.endsWith(`@${server}`) ? acct.slice(0, -server.length - 1) : acct
const key = `${server}:${userId}:account:${userAcct}`
const cached = cache.get(key) const cached = cache.get(key)
if (cached) if (cached)
return cached return cached
@ -69,9 +70,9 @@ export async function fetchAccountByHandle(acct: string): Promise<mastodon.v1.Ac
const client = useMastoClient() const client = useMastoClient()
let account: mastodon.v1.Account let account: mastodon.v1.Account
if (!isGotoSocial.value) if (!isGotoSocial.value)
account = await client.v1.accounts.lookup({ acct }) account = await client.v1.accounts.lookup({ acct: userAcct })
else else
account = (await client.v1.search({ q: `@${acct}`, type: 'accounts' })).accounts[0] account = (await client.v1.search({ q: `@${userAcct}`, type: 'accounts' })).accounts[0]
if (account.acct && !account.acct.includes('@') && domain) if (account.acct && !account.acct.includes('@') && domain)
account.acct = `${account.acct}@${domain}` account.acct = `${account.acct}@${domain}`
@ -107,6 +108,7 @@ export function removeCachedStatus(id: string, server = currentServer.value) {
export function cacheAccount(account: mastodon.v1.Account, server = currentServer.value, override?: boolean) { export function cacheAccount(account: mastodon.v1.Account, server = currentServer.value, override?: boolean) {
const userId = currentUser.value?.account.id const userId = currentUser.value?.account.id
const userAcct = account.acct.endsWith(`@${server}`) ? account.acct.slice(0, -server.length - 1) : account.acct
setCached(`${server}:${userId}:account:${account.id}`, account, override) setCached(`${server}:${userId}:account:${account.id}`, account, override)
setCached(`${server}:${userId}:account:${account.acct}`, account, override) setCached(`${server}:${userId}:account:${userAcct}`, account, override)
} }

View file

@ -245,6 +245,7 @@ export const provideGlobalCommands = () => {
const masto = useMasto() const masto = useMasto()
const colorMode = useColorMode() const colorMode = useColorMode()
const userSettings = useUserSettings() const userSettings = useUserSettings()
const { singleInstanceServer, oauth } = useSignIn()
useCommand({ useCommand({
scope: 'Navigation', scope: 'Navigation',
@ -310,6 +311,9 @@ export const provideGlobalCommands = () => {
icon: 'i-ri:user-add-line', icon: 'i-ri:user-add-line',
onActivate() { onActivate() {
if (singleInstanceServer)
oauth()
else
openSigninDialog() openSigninDialog()
}, },
}) })
@ -349,7 +353,7 @@ export const provideGlobalCommands = () => {
icon: 'i-ri:logout-box-line', icon: 'i-ri:logout-box-line',
onActivate() { onActivate() {
signout() signOut()
}, },
}) })
} }

View file

@ -8,6 +8,7 @@ import { emojiRegEx, getEmojiAttributes } from '../config/emojis'
export interface ContentParseOptions { export interface ContentParseOptions {
emojis?: Record<string, mastodon.v1.CustomEmoji> emojis?: Record<string, mastodon.v1.CustomEmoji>
hideEmojis?: boolean
mentions?: mastodon.v1.StatusMention[] mentions?: mastodon.v1.StatusMention[]
markdown?: boolean markdown?: boolean
replaceUnicodeEmoji?: boolean replaceUnicodeEmoji?: boolean
@ -82,6 +83,7 @@ export function parseMastodonHTML(
replaceUnicodeEmoji = true, replaceUnicodeEmoji = true,
convertMentionLink = false, convertMentionLink = false,
collapseMentionLink = false, collapseMentionLink = false,
hideEmojis = false,
mentions, mentions,
status, status,
inReplyToStatus, inReplyToStatus,
@ -110,9 +112,17 @@ export function parseMastodonHTML(
...options.astTransforms || [], ...options.astTransforms || [],
] ]
if (hideEmojis) {
transforms.push(removeUnicodeEmoji)
transforms.push(removeCustomEmoji(options.emojis ?? {}))
}
else {
if (replaceUnicodeEmoji) if (replaceUnicodeEmoji)
transforms.push(transformUnicodeEmoji) transforms.push(transformUnicodeEmoji)
transforms.push(replaceCustomEmoji(options.emojis ?? {}))
}
if (markdown) if (markdown)
transforms.push(transformMarkdown) transforms.push(transformMarkdown)
@ -122,8 +132,6 @@ export function parseMastodonHTML(
if (convertMentionLink) if (convertMentionLink)
transforms.push(transformMentionLink) transforms.push(transformMentionLink)
transforms.push(replaceCustomEmoji(options.emojis || {}))
transforms.push(transformParagraphs) transforms.push(transformParagraphs)
if (collapseMentionLink) if (collapseMentionLink)
@ -353,6 +361,25 @@ function filterHref() {
} }
} }
function removeUnicodeEmoji(node: Node) {
if (node.type !== TEXT_NODE)
return node
let start = 0
const matches = [] as (string | Node)[]
findAndReplaceEmojisInText(emojiRegEx, node.value, (match, result) => {
matches.push(result.slice(start).trimEnd())
start = result.length + match.match.length
return undefined
})
if (matches.length === 0)
return node
matches.push(node.value.slice(start))
return matches.filter(Boolean)
}
function transformUnicodeEmoji(node: Node) { function transformUnicodeEmoji(node: Node) {
if (node.type !== TEXT_NODE) if (node.type !== TEXT_NODE)
return node return node
@ -374,6 +401,28 @@ function transformUnicodeEmoji(node: Node) {
return matches.filter(Boolean) return matches.filter(Boolean)
} }
function removeCustomEmoji(customEmojis: Record<string, mastodon.v1.CustomEmoji>): Transform {
return (node) => {
if (node.type !== TEXT_NODE)
return node
const split = node.value.split(/\s?:([\w-]+?):/g)
if (split.length === 1)
return node
return split.map((name, i) => {
if (i % 2 === 0)
return name
const emoji = customEmojis[name] as mastodon.v1.CustomEmoji
if (!emoji)
return `:${name}:`
return ''
}).filter(Boolean)
}
}
function replaceCustomEmoji(customEmojis: Record<string, mastodon.v1.CustomEmoji>): Transform { function replaceCustomEmoji(customEmojis: Record<string, mastodon.v1.CustomEmoji>): Transform {
return (node) => { return (node) => {
if (node.type !== TEXT_NODE) if (node.type !== TEXT_NODE)

View file

@ -10,6 +10,14 @@ import ContentCode from '~/components/content/ContentCode.vue'
import ContentMentionGroup from '~/components/content/ContentMentionGroup.vue' import ContentMentionGroup from '~/components/content/ContentMentionGroup.vue'
import AccountHoverWrapper from '~/components/account/AccountHoverWrapper.vue' import AccountHoverWrapper from '~/components/account/AccountHoverWrapper.vue'
function getTexualAstComponents(astChildren: Node[]): string {
return astChildren
.filter(({ type }) => type === TEXT_NODE)
.map(({ value }) => value)
.reduce((accumulator, current) => accumulator + current, '')
.trim()
}
/** /**
* Raw HTML to VNodes * Raw HTML to VNodes
*/ */
@ -17,11 +25,18 @@ export function contentToVNode(
content: string, content: string,
options?: ContentParseOptions, options?: ContentParseOptions,
): VNode { ): VNode {
const tree = parseMastodonHTML(content, options) let tree = parseMastodonHTML(content, options)
const textContents = getTexualAstComponents(tree.children)
// if the username only contains emojis, we should probably show the emojis anyway to avoid a blank name
if (options?.hideEmojis && textContents.length === 0)
tree = parseMastodonHTML(content, { ...options, hideEmojis: false })
return h(Fragment, (tree.children as Node[] || []).map(n => treeToVNode(n))) return h(Fragment, (tree.children as Node[] || []).map(n => treeToVNode(n)))
} }
export function nodeToVNode(node: Node): VNode | string | null { function nodeToVNode(node: Node): VNode | string | null {
if (node.type === TEXT_NODE) if (node.type === TEXT_NODE)
return node.value return node.value

View file

@ -1,9 +1,10 @@
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
import type { ConfirmDialogChoice, ConfirmDialogLabel, Draft } from '~/types' import type { ConfirmDialogChoice, ConfirmDialogLabel, Draft, ErrorDialogData } from '~/types'
import { STORAGE_KEY_FIRST_VISIT } from '~/constants' import { STORAGE_KEY_FIRST_VISIT } from '~/constants'
export const confirmDialogChoice = ref<ConfirmDialogChoice>() export const confirmDialogChoice = ref<ConfirmDialogChoice>()
export const confirmDialogLabel = ref<ConfirmDialogLabel>() export const confirmDialogLabel = ref<ConfirmDialogLabel>()
export const errorDialogData = ref<ErrorDialogData>()
export const mediaPreviewList = ref<mastodon.v1.MediaAttachment[]>([]) export const mediaPreviewList = ref<mastodon.v1.MediaAttachment[]>([])
export const mediaPreviewIndex = ref(0) export const mediaPreviewIndex = ref(0)
@ -22,6 +23,7 @@ export const isEditHistoryDialogOpen = ref(false)
export const isPreviewHelpOpen = ref(isFirstVisit.value) export const isPreviewHelpOpen = ref(isFirstVisit.value)
export const isCommandPanelOpen = ref(false) export const isCommandPanelOpen = ref(false)
export const isConfirmDialogOpen = ref(false) export const isConfirmDialogOpen = ref(false)
export const isErrorDialogOpen = ref(false)
export const isFavouritedBoostedByDialogOpen = ref(false) export const isFavouritedBoostedByDialogOpen = ref(false)
export const lastPublishDialogStatus = ref<mastodon.v1.Status | null>(null) export const lastPublishDialogStatus = ref<mastodon.v1.Status | null>(null)
@ -101,6 +103,17 @@ export function openMediaPreview(attachments: mastodon.v1.MediaAttachment[], ind
}, '') }, '')
} }
export async function openErrorDialog(data: ErrorDialogData) {
errorDialogData.value = data
isErrorDialogOpen.value = true
await until(isErrorDialogOpen).toBe(false)
}
export function closeErrorDialog() {
isErrorDialogOpen.value = false
}
export function closeMediaPreview() { export function closeMediaPreview() {
history.back() history.back()
} }

View file

@ -1,76 +0,0 @@
import type { PermissiveMotionProperties } from '@vueuse/motion'
import type { Handlers } from '@vueuse/gesture'
import { useMotionProperties, useSpring } from '@vueuse/motion'
import { useGesture } from '@vueuse/gesture'
import type { MaybeRef } from '@vueuse/core'
export interface CarouselOptions {
hasNext: MaybeRef<boolean>
hasPrev: MaybeRef<boolean>
onPrev: () => void
onNext: () => void
}
export const useImageGesture = (
domTarget: MaybeRef<HTMLElement>,
carouselOptions?: CarouselOptions,
) => {
const { motionProperties } = useMotionProperties(domTarget, {
cursor: 'grab',
scale: 1,
x: 0,
y: 0,
})
const { set } = useSpring(motionProperties as Partial<PermissiveMotionProperties>)
const handlers: Handlers = {
onPinch({ offset: [d] }) {
set({ scale: 1 + d / 200 })
},
onDragStart() {
set({ cursor: 'grabbing' })
},
onDrag({ movement: [x, y], pinching }) {
if (!pinching)
set({ x, y, cursor: 'grabbing' })
},
onDragEnd({ vxvy: [vx], pinching }) {
if (pinching)
return
set({ cursor: 'grab' })
if (carouselOptions) {
const isSwipe = Math.abs(vx) > 0.25
if (isSwipe) {
if (vx > 0 && unref(carouselOptions.hasPrev))
carouselOptions.onPrev()
else if (vx < 0 && unref(carouselOptions.hasNext))
carouselOptions.onNext()
}
}
set({ x: 0, y: 0 })
},
onMove({ movement: [x, y], dragging, pinching }) {
if (dragging && !pinching)
set({ x, y })
},
onWheel({ event, dragging, pinching }) {
if (!dragging && !pinching && event.altKey) {
event.preventDefault()
// @ts-expect-error why is ts complaining here (motionProperties.scale)?
set({ scale: motionProperties.scale + event.deltaY * 0.001 })
}
},
onDblclick() {
set({ scale: 1 })
},
onTouchstart(event) {
if (event.touches === 2)
set({ scale: 1 })
},
}
useGesture(handlers, { domTarget })
}

11
composables/langugage.ts Normal file
View file

@ -0,0 +1,11 @@
import ISO6391 from 'iso-639-1'
export const languagesNameList: {
code: string
nativeName: string
name: string
}[] = ISO6391.getAllCodes().map(code => ({
code,
nativeName: ISO6391.getNativeName(code),
name: ISO6391.getName(code),
}))

View file

@ -7,14 +7,14 @@ export function getDisplayName(account: mastodon.v1.Account, options?: { rich?:
return displayName.replace(/:([\w-]+?):/g, '') return displayName.replace(/:([\w-]+?):/g, '')
} }
export function acctToShortHandle(acct: string) { export function accountToShortHandle(acct: string) {
return `@${acct.includes('@') ? acct.split('@')[0] : acct}` return `@${acct.includes('@') ? acct.split('@')[0] : acct}`
} }
export function getShortHandle({ acct }: mastodon.v1.Account) { export function getShortHandle({ acct }: mastodon.v1.Account) {
if (!acct) if (!acct)
return '' return ''
return acctToShortHandle(acct) return accountToShortHandle(acct)
} }
export function getServerName(account: mastodon.v1.Account) { export function getServerName(account: mastodon.v1.Account) {

View file

@ -47,7 +47,7 @@ export function mastoLogin(masto: ElkMasto, user: Pick<UserLogin, 'server' | 'to
setParams({ setParams({
streamingApiUrl: newInstance.urls.streamingApi, streamingApiUrl: newInstance.urls.streamingApi,
}) })
instances.value[server] = newInstance instanceStorage.value[server] = newInstance
}) })
return instance return instance

View file

@ -4,20 +4,34 @@ import type { mastodon } from 'masto'
import type { UseDraft } from './statusDrafts' import type { UseDraft } from './statusDrafts'
import type { Draft } from '~~/types' import type { Draft } from '~~/types'
export const usePublish = (options: { export function usePublish(options: {
draftState: UseDraft draftState: UseDraft
expanded: Ref<boolean> expanded: Ref<boolean>
isUploading: Ref<boolean> isUploading: Ref<boolean>
initialDraft: Ref<() => Draft> initialDraft: Ref<() => Draft>
}) => { }) {
const { expanded, isUploading, initialDraft } = $(options) const { expanded, isUploading, initialDraft } = $(options)
let { draft, isEmpty } = $(options.draftState) let { draft, isEmpty } = $(options.draftState)
const { client } = $(useMasto()) const { client } = $(useMasto())
const settings = useUserSettings()
const preferredLanguage = $computed(() => (settings.value?.language || 'en').split('-')[0])
let isSending = $ref(false) let isSending = $ref(false)
const isExpanded = $ref(false) const isExpanded = $ref(false)
const failedMessages = $ref<string[]>([]) const failedMessages = $ref<string[]>([])
const publishSpoilerText = $computed({
get() {
return draft.params.sensitive ? draft.params.spoilerText : ''
},
set(val) {
if (!draft.params.sensitive)
return
draft.params.spoilerText = val
},
})
const shouldExpanded = $computed(() => expanded || isExpanded || !isEmpty) const shouldExpanded = $computed(() => expanded || isExpanded || !isEmpty)
const isPublishDisabled = $computed(() => { const isPublishDisabled = $computed(() => {
return isEmpty || isUploading || isSending || (draft.attachments.length === 0 && !draft.params.status) || failedMessages.length > 0 return isEmpty || isUploading || isSending || (draft.attachments.length === 0 && !draft.params.status) || failedMessages.length > 0
@ -29,14 +43,19 @@ export const usePublish = (options: {
}, { deep: true }) }, { deep: true })
async function publishDraft() { async function publishDraft() {
if (isPublishDisabled)
return
let content = htmlToText(draft.params.status || '') let content = htmlToText(draft.params.status || '')
if (draft.mentions?.length) if (draft.mentions?.length)
content = `${draft.mentions.map(i => `@${i}`).join(' ')} ${content}` content = `${draft.mentions.map(i => `@${i}`).join(' ')} ${content}`
const payload = { const payload = {
...draft.params, ...draft.params,
spoilerText: publishSpoilerText,
status: content, status: content,
mediaIds: draft.attachments.map(a => a.id), mediaIds: draft.attachments.map(a => a.id),
language: draft.params.language || preferredLanguage,
...(isGlitchEdition.value ? { 'content-type': 'text/markdown' } : {}), ...(isGlitchEdition.value ? { 'content-type': 'text/markdown' } : {}),
} as mastodon.v1.CreateStatusParams } as mastodon.v1.CreateStatusParams
@ -58,6 +77,7 @@ export const usePublish = (options: {
let status: mastodon.v1.Status let status: mastodon.v1.Status
if (!draft.editingStatus) if (!draft.editingStatus)
status = await client.v1.statuses.create(payload) status = await client.v1.statuses.create(payload)
else else
status = await client.v1.statuses.update(draft.editingStatus.id, payload) status = await client.v1.statuses.update(draft.editingStatus.id, payload)
if (draft.params.inReplyToId) if (draft.params.inReplyToId)
@ -82,14 +102,15 @@ export const usePublish = (options: {
shouldExpanded, shouldExpanded,
isPublishDisabled, isPublishDisabled,
failedMessages, failedMessages,
preferredLanguage,
publishSpoilerText,
publishDraft, publishDraft,
}) })
} }
export type MediaAttachmentUploadError = [filename: string, message: string] export type MediaAttachmentUploadError = [filename: string, message: string]
export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => { export function useUploadMediaAttachment(draftRef: Ref<Draft>) {
const draft = $(draftRef) const draft = $(draftRef)
const { client } = $(useMasto()) const { client } = $(useMasto())
const { t } = useI18n() const { t } = useI18n()
@ -157,9 +178,10 @@ export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
return $$({ return $$({
isUploading, isUploading,
isExceedingAttachmentLimit, isExceedingAttachmentLimit,
isOverDropZone,
failedAttachments, failedAttachments,
dropZoneRef, dropZoneRef,
isOverDropZone,
uploadAttachments, uploadAttachments,
pickAttachments, pickAttachments,

View file

@ -33,7 +33,7 @@ export function getDefaultDraft(options: Partial<Mutable<mastodon.v1.CreateStatu
visibility: visibility || 'public', visibility: visibility || 'public',
sensitive: sensitive ?? false, sensitive: sensitive ?? false,
spoilerText: spoilerText || '', spoilerText: spoilerText || '',
language: language || getDefaultLanguage(), language: language || '', // auto inferred from current language on posting
}, },
mentions, mentions,
lastUpdated: Date.now(), lastUpdated: Date.now(),
@ -52,16 +52,6 @@ export async function getDraftFromStatus(status: mastodon.v1.Status): Promise<Dr
}) })
} }
function getDefaultLanguage() {
const userSettings = useUserSettings()
const defaultLanguage = userSettings.value.language
if (defaultLanguage)
return defaultLanguage.split('-')[0]
return 'en'
}
function getAccountsToMention(status: mastodon.v1.Status) { function getAccountsToMention(status: mastodon.v1.Status) {
const userId = currentUser.value?.account.id const userId = currentUser.value?.account.id
const accountsToMention = new Set<string>() const accountsToMention = new Set<string>()
@ -84,6 +74,7 @@ export function getReplyDraft(status: mastodon.v1.Status) {
inReplyToId: status!.id, inReplyToId: status!.id,
visibility: status.visibility, visibility: status.visibility,
mentions: accountsToMention, mentions: accountsToMention,
language: status.language,
}) })
}, },
} }
@ -98,7 +89,6 @@ export const isEmptyDraft = (draft: Draft | null | undefined) => {
return (text.length === 0) return (text.length === 0)
&& attachments.length === 0 && attachments.length === 0
&& (params.spoilerText || '').length === 0
} }
export interface UseDraft { export interface UseDraft {

View file

@ -8,6 +8,40 @@ export interface TranslationResponse {
} }
} }
// @see https://github.com/LibreTranslate/LibreTranslate/tree/main/libretranslate/locales
export const supportedTranslationCodes = [
'ar',
'az',
'cs',
'da',
'de',
'el',
'en',
'eo',
'es',
'fa',
'fi',
'fr',
'ga',
'he',
'hi',
'hu',
'id',
'it',
'ja',
'ko',
'nl',
'pl',
'pt',
'ru',
'sk',
'sv',
'tr',
'uk',
'vi',
'zh',
] as const
export const getLanguageCode = () => { export const getLanguageCode = () => {
let code = 'en' let code = 'en'
const getCode = (code: string) => code.replace(/-.*$/, '') const getCode = (code: string) => code.replace(/-.*$/, '')
@ -63,9 +97,16 @@ export function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEd
translations.set(status, reactive({ visible: false, text: '', success: false, error: '' })) translations.set(status, reactive({ visible: false, text: '', success: false, error: '' }))
const translation = translations.get(status)! const translation = translations.get(status)!
const userSettings = useUserSettings()
const shouldTranslate = 'language' in status && status.language && status.language !== to
&& supportedTranslationCodes.includes(to as any)
&& supportedTranslationCodes.includes(status.language as any)
&& !userSettings.value.disabledTranslationLanguages.includes(status.language)
const enabled = /*! !useRuntimeConfig().public.translateApi && */ shouldTranslate
async function toggle() { async function toggle() {
if (!('language' in status)) if (!shouldTranslate)
return return
if (!translation.text) { if (!translation.text) {
@ -79,7 +120,7 @@ export function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEd
} }
return { return {
enabled: !!useRuntimeConfig().public.translateApi, enabled,
toggle, toggle,
translation, translation,
} }

View file

@ -46,9 +46,9 @@ export const createPushSubscription = async (
if (error.code === 11 && error.name === 'InvalidStateError') if (error.code === 11 && error.name === 'InvalidStateError')
useError = new PushSubscriptionError('too_many_registrations', 'Too many registrations') useError = new PushSubscriptionError('too_many_registrations', 'Too many registrations')
else if (error.code === 20 && error.name === 'AbortError') else if (error.code === 20 && error.name === 'AbortError')
console.error('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.') useError = new PushSubscriptionError('vapid_not_supported', 'Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.')
else if (error.code === 5 && error.name === 'InvalidCharacterError') else if (error.code === 5 && error.name === 'InvalidCharacterError')
console.error('The VAPID public key seems to be invalid:', vapidKey) useError = new PushSubscriptionError('invalid_vapid_key', `The VAPID public key seems to be invalid: ${vapidKey}`)
return getRegistration() return getRegistration()
.then(getPushSubscription) .then(getPushSubscription)

View file

@ -25,7 +25,8 @@ export interface CustomEmojisInfo {
emojis: mastodon.v1.CustomEmoji[] emojis: mastodon.v1.CustomEmoji[]
} }
export type PushSubscriptionErrorCode = 'too_many_registrations' export type PushSubscriptionErrorCode = 'too_many_registrations' | 'vapid_not_supported' | 'invalid_vapid_key'
export class PushSubscriptionError extends Error { export class PushSubscriptionError extends Error {
code: PushSubscriptionErrorCode code: PushSubscriptionErrorCode
constructor(code: PushSubscriptionErrorCode, message?: string) { constructor(code: PushSubscriptionErrorCode, message?: string) {

View file

@ -1,4 +1,5 @@
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
import type { import type {
CreatePushNotification, CreatePushNotification,
PushNotificationPolicy, PushNotificationPolicy,
@ -27,19 +28,17 @@ export const usePushManager = () => {
const isSupported = $computed(() => supportsPushNotifications) const isSupported = $computed(() => supportsPushNotifications)
const hiddenNotification = useLocalStorage<PushNotificationRequest>(STORAGE_KEY_NOTIFICATION, {}) const hiddenNotification = useLocalStorage<PushNotificationRequest>(STORAGE_KEY_NOTIFICATION, {})
const configuredPolicy = useLocalStorage<PushNotificationPolicy>(STORAGE_KEY_NOTIFICATION_POLICY, {}) const configuredPolicy = useLocalStorage<PushNotificationPolicy>(STORAGE_KEY_NOTIFICATION_POLICY, {})
const pushNotificationData = ref({ const pushNotificationData = ref(createRawSettings(
follow: currentUser.value?.pushSubscription?.alerts.follow ?? true, currentUser.value?.pushSubscription,
favourite: currentUser.value?.pushSubscription?.alerts.favourite ?? true, configuredPolicy.value[currentUser.value?.account?.acct ?? ''],
reblog: currentUser.value?.pushSubscription?.alerts.reblog ?? true, ))
mention: currentUser.value?.pushSubscription?.alerts.mention ?? true, const oldPushNotificationData = ref(createRawSettings(
poll: currentUser.value?.pushSubscription?.alerts.poll ?? true, currentUser.value?.pushSubscription,
policy: configuredPolicy.value[currentUser.value?.account?.acct ?? ''] ?? 'all', configuredPolicy.value[currentUser.value?.account?.acct ?? ''],
}) ))
// don't clone, we're using indexeddb
const { history, commit, clear } = useManualRefHistory(pushNotificationData)
const saveEnabled = computed(() => { const saveEnabled = computed(() => {
const current = pushNotificationData.value const current = pushNotificationData.value
const previous = history.value?.[0]?.snapshot const previous = oldPushNotificationData.value
return current.favourite !== previous.favourite return current.favourite !== previous.favourite
|| current.reblog !== previous.reblog || current.reblog !== previous.reblog
|| current.mention !== previous.mention || current.mention !== previous.mention
@ -50,14 +49,14 @@ export const usePushManager = () => {
watch(() => currentUser.value?.pushSubscription, (subscription) => { watch(() => currentUser.value?.pushSubscription, (subscription) => {
isSubscribed.value = !!subscription isSubscribed.value = !!subscription
pushNotificationData.value = { pushNotificationData.value = createRawSettings(
follow: subscription?.alerts.follow ?? false, subscription,
favourite: subscription?.alerts.favourite ?? false, configuredPolicy.value[currentUser.value?.account?.acct ?? ''],
reblog: subscription?.alerts.reblog ?? false, )
mention: subscription?.alerts.mention ?? false, oldPushNotificationData.value = createRawSettings(
poll: subscription?.alerts.poll ?? false, subscription,
policy: configuredPolicy.value[currentUser.value?.account?.acct ?? ''] ?? 'all', configuredPolicy.value[currentUser.value?.account?.acct ?? ''],
} )
}, { immediate: true, flush: 'post' }) }, { immediate: true, flush: 'post' })
const subscribe = async ( const subscribe = async (
@ -121,7 +120,15 @@ export const usePushManager = () => {
if (policy) if (policy)
pushNotificationData.value.policy = policy pushNotificationData.value.policy = policy
commit() const current = pushNotificationData.value
oldPushNotificationData.value = {
favourite: current.favourite,
reblog: current.reblog,
mention: current.mention,
follow: current.follow,
poll: current.poll,
policy: current.policy,
}
if (policy) if (policy)
configuredPolicy.value[currentUser.value!.account.acct ?? ''] = policy configuredPolicy.value[currentUser.value!.account.acct ?? ''] = policy
@ -129,27 +136,25 @@ export const usePushManager = () => {
configuredPolicy.value[currentUser.value!.account.acct ?? ''] = pushNotificationData.value.policy configuredPolicy.value[currentUser.value!.account.acct ?? ''] = pushNotificationData.value.policy
await nextTick() await nextTick()
clear()
await nextTick()
} }
const undoChanges = () => { const undoChanges = () => {
const current = pushNotificationData.value const previous = oldPushNotificationData.value
const previous = history.value[0].snapshot pushNotificationData.value = {
current.favourite = previous.favourite favourite: previous.favourite,
current.reblog = previous.reblog reblog: previous.reblog,
current.mention = previous.mention mention: previous.mention,
current.follow = previous.follow follow: previous.follow,
current.poll = previous.poll poll: previous.poll,
current.policy = previous.policy policy: previous.policy,
}
configuredPolicy.value[currentUser.value!.account.acct ?? ''] = previous.policy configuredPolicy.value[currentUser.value!.account.acct ?? ''] = previous.policy
commit()
clear()
} }
const updateSubscription = async () => { const updateSubscription = async () => {
if (currentUser.value) { if (currentUser.value) {
const previous = history.value[0].snapshot const previous = oldPushNotificationData.value
// const previous = history.value[0].snapshot
const data = { const data = {
alerts: { alerts: {
follow: pushNotificationData.value.follow, follow: pushNotificationData.value.follow,
@ -190,3 +195,17 @@ export const usePushManager = () => {
unsubscribe, unsubscribe,
} }
} }
function createRawSettings(
pushSubscription?: mastodon.v1.WebPushSubscription,
subscriptionPolicy?: mastodon.v1.SubscriptionPolicy,
) {
return {
follow: pushSubscription?.alerts.follow ?? true,
favourite: pushSubscription?.alerts.favourite ?? true,
reblog: pushSubscription?.alerts.reblog ?? true,
mention: pushSubscription?.alerts.mention ?? true,
poll: pushSubscription?.alerts.poll ?? true,
policy: subscriptionPolicy ?? 'all',
}
}

View file

@ -2,6 +2,5 @@ import { breakpointsTailwind } from '@vueuse/core'
export const breakpoints = useBreakpoints(breakpointsTailwind) export const breakpoints = useBreakpoints(breakpointsTailwind)
export const isSmallScreen = breakpoints.smallerOrEqual('md')
export const isMediumScreen = breakpoints.smallerOrEqual('lg')
export const isMediumOrLargeScreen = breakpoints.between('sm', 'xl') export const isMediumOrLargeScreen = breakpoints.between('sm', 'xl')
export const isExtraLargeScreen = breakpoints.smallerOrEqual('xl')

View file

@ -8,14 +8,17 @@ export type OldFontSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
export type ColorMode = 'light' | 'dark' | 'system' export type ColorMode = 'light' | 'dark' | 'system'
export interface PreferencesSettings { export interface PreferencesSettings {
hideAltIndicatorOnPosts: boolean
hideBoostCount: boolean hideBoostCount: boolean
hideReplyCount: boolean hideReplyCount: boolean
hideFavoriteCount: boolean hideFavoriteCount: boolean
hideFollowerCount: boolean hideFollowerCount: boolean
hideTranslation: boolean hideTranslation: boolean
hideUsernameEmojis: boolean
hideAccountHoverCard: boolean hideAccountHoverCard: boolean
grayscaleMode: boolean grayscaleMode: boolean
enableAutoplay: boolean enableAutoplay: boolean
enablePinchToZoom: boolean
experimentalVirtualScroller: boolean experimentalVirtualScroller: boolean
experimentalGitHubCards: boolean experimentalGitHubCards: boolean
experimentalUserPicker: boolean experimentalUserPicker: boolean
@ -26,6 +29,7 @@ export interface UserSettings {
colorMode?: ColorMode colorMode?: ColorMode
fontSize: FontSize fontSize: FontSize
language: string language: string
disabledTranslationLanguages: string[]
zenMode: boolean zenMode: boolean
themeColors?: ThemeColors themeColors?: ThemeColors
} }
@ -56,20 +60,24 @@ export function getDefaultUserSettings(locales: string[]): UserSettings {
return { return {
language: getDefaultLanguage(locales), language: getDefaultLanguage(locales),
fontSize: DEFAULT_FONT_SIZE, fontSize: DEFAULT_FONT_SIZE,
disabledTranslationLanguages: [],
zenMode: false, zenMode: false,
preferences: {}, preferences: {},
} }
} }
export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = { export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = {
hideAltIndicatorOnPosts: false,
hideBoostCount: false, hideBoostCount: false,
hideReplyCount: false, hideReplyCount: false,
hideFavoriteCount: false, hideFavoriteCount: false,
hideFollowerCount: false, hideFollowerCount: false,
hideTranslation: false, hideTranslation: false,
hideUsernameEmojis: false,
hideAccountHoverCard: false, hideAccountHoverCard: false,
grayscaleMode: false, grayscaleMode: false,
enableAutoplay: true, enableAutoplay: true,
enablePinchToZoom: false,
experimentalVirtualScroller: true, experimentalVirtualScroller: true,
experimentalGitHubCards: true, experimentalGitHubCards: true,
experimentalUserPicker: true, experimentalUserPicker: true,

View file

@ -5,6 +5,7 @@ export function setupPageHeader() {
const { locale, locales, t } = useI18n() const { locale, locales, t } = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const buildInfo = useBuildInfo() const buildInfo = useBuildInfo()
const enablePinchToZoom = usePreferences('enablePinchToZoom')
const localeMap = (locales.value as LocaleObject[]).reduce((acc, l) => { const localeMap = (locales.value as LocaleObject[]).reduce((acc, l) => {
acc[l.code!] = l.dir ?? 'auto' acc[l.code!] = l.dir ?? 'auto'
@ -15,7 +16,12 @@ export function setupPageHeader() {
htmlAttrs: { htmlAttrs: {
lang: () => locale.value, lang: () => locale.value,
dir: () => localeMap[locale.value] ?? 'auto', dir: () => localeMap[locale.value] ?? 'auto',
class: () => enablePinchToZoom.value ? ['enable-pinch-to-zoom'] : [],
}, },
meta: [{
name: 'viewport',
content: () => `width=device-width,initial-scale=1${enablePinchToZoom.value ? '' : ',maximum-scale=1,user-scalable=0'},viewport-fit=cover`,
}],
titleTemplate: (title) => { titleTemplate: (title) => {
let titleTemplate = title ?? '' let titleTemplate = title ?? ''
@ -43,7 +49,7 @@ export function setupPageHeader() {
return titleTemplate return titleTemplate
}, },
link: process.client && useRuntimeConfig().public.pwaEnabled link: process.client && useAppConfig().pwaEnabled
? () => [{ ? () => [{
key: 'webmanifest', key: 'webmanifest',
rel: 'manifest', rel: 'manifest',

77
composables/sign-in.ts Normal file
View file

@ -0,0 +1,77 @@
import type { Ref } from 'vue'
export const useSignIn = (input?: Ref<HTMLInputElement | undefined>) => {
const singleInstanceServer = useAppConfig().singleInstanceServer
const userSettings = useUserSettings()
const users = useUsers()
const { t } = useI18n()
const busy = ref(false)
const error = ref(false)
const server = ref('')
const displayError = ref(false)
async function oauth() {
if (busy.value)
return
busy.value = true
error.value = false
displayError.value = false
await nextTick()
if (!singleInstanceServer && server.value)
server.value = server.value.split('/')[0]
try {
let href: string
if (singleInstanceServer) {
href = await (globalThis.$fetch as any)(`/api/${publicServer.value}/login`, {
method: 'POST',
body: {
force_login: users.value.length > 0,
origin: location.origin,
lang: userSettings.value.language,
},
})
busy.value = false
}
else {
href = await (globalThis.$fetch as any)(`/api/${server.value || publicServer.value}/login`, {
method: 'POST',
body: {
force_login: users.value.some(u => u.server === server.value),
origin: location.origin,
lang: userSettings.value.language,
},
})
}
location.href = href
}
catch (err) {
if (singleInstanceServer) {
console.error(err)
busy.value = false
await openErrorDialog({
title: t('common.error'),
messages: [t('error.sign_in_error')],
close: t('action.close'),
})
}
else {
displayError.value = true
error.value = true
await nextTick()
input?.value?.focus()
await nextTick()
setTimeout(() => {
busy.value = false
error.value = false
}, 512)
}
}
}
return { busy, displayError, error, server, singleInstanceServer, oauth }
}

View file

@ -11,7 +11,7 @@ function areStatusesConsecutive(a: mastodon.v1.Status, b: mastodon.v1.Status) {
function removeFilteredItems(items: mastodon.v1.Status[], context: mastodon.v1.FilterContext): mastodon.v1.Status[] { function removeFilteredItems(items: mastodon.v1.Status[], context: mastodon.v1.FilterContext): mastodon.v1.Status[] {
const isStrict = (filter: mastodon.v1.FilterResult) => filter.filter.filterAction === 'hide' && filter.filter.context.includes(context) const isStrict = (filter: mastodon.v1.FilterResult) => filter.filter.filterAction === 'hide' && filter.filter.context.includes(context)
const isFiltered = (item: mastodon.v1.Status) => !item.filtered?.find(isStrict) const isFiltered = (item: mastodon.v1.Status) => (item.account.id === currentUser.value?.account.id) || !item.filtered?.find(isStrict)
const isReblogFiltered = (item: mastodon.v1.Status) => !item.reblog?.filtered?.find(isStrict) const isReblogFiltered = (item: mastodon.v1.Status) => !item.reblog?.filtered?.find(isStrict)
return [...items].filter(isFiltered).filter(isReblogFiltered) return [...items].filter(isFiltered).filter(isReblogFiltered)

View file

@ -7,7 +7,6 @@ import type { UserLogin } from '~/types'
import type { Overwrite } from '~/types/utils' import type { Overwrite } from '~/types/utils'
import { import {
DEFAULT_POST_CHARS_LIMIT, DEFAULT_POST_CHARS_LIMIT,
STORAGE_KEY_CURRENT_USER,
STORAGE_KEY_CURRENT_USER_HANDLE, STORAGE_KEY_CURRENT_USER_HANDLE,
STORAGE_KEY_NODES, STORAGE_KEY_NODES,
STORAGE_KEY_NOTIFICATION, STORAGE_KEY_NOTIFICATION,
@ -44,20 +43,20 @@ const initializeUsers = async (): Promise<Ref<UserLogin[]> | RemovableRef<UserLo
} }
const users = await initializeUsers() const users = await initializeUsers()
export const instances = useLocalStorage<Record<string, mastodon.v1.Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true }) const nodes = useLocalStorage<Record<string, any>>(STORAGE_KEY_NODES, {}, { deep: true })
export const nodes = useLocalStorage<Record<string, any>>(STORAGE_KEY_NODES, {}, { deep: true }) const currentUserHandle = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER_HANDLE, mock ? mock.user.account.id : '')
const currentUserId = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER, mock ? mock.user.account.id : '') export const instanceStorage = useLocalStorage<Record<string, mastodon.v1.Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true })
export type ElkInstance = Partial<mastodon.v1.Instance> & { export type ElkInstance = Partial<mastodon.v1.Instance> & {
uri: string uri: string
/** support GoToSocial */ /** support GoToSocial */
accountDomain?: string | null accountDomain?: string | null
} }
export const getInstanceCache = (server: string): mastodon.v1.Instance | undefined => instances.value[server] export const getInstanceCache = (server: string): mastodon.v1.Instance | undefined => instanceStorage.value[server]
export const currentUser = computed<UserLogin | undefined>(() => { export const currentUser = computed<UserLogin | undefined>(() => {
if (currentUserId.value) { if (currentUserHandle.value) {
const user = users.value.find(user => user.account?.id === currentUserId.value) const user = users.value.find(user => user.account?.acct === currentUserHandle.value)
if (user) if (user)
return user return user
} }
@ -66,7 +65,7 @@ export const currentUser = computed<UserLogin | undefined>(() => {
}) })
const publicInstance = ref<ElkInstance | null>(null) const publicInstance = ref<ElkInstance | null>(null)
export const currentInstance = computed<null | ElkInstance>(() => currentUser.value ? instances.value[currentUser.value.server] ?? null : publicInstance.value) export const currentInstance = computed<null | ElkInstance>(() => currentUser.value ? instanceStorage.value[currentUser.value.server] ?? null : publicInstance.value)
export function getInstanceDomain(instance: ElkInstance) { export function getInstanceDomain(instance: ElkInstance) {
return instance.accountDomain || withoutProtocol(instance.uri) return instance.accountDomain || withoutProtocol(instance.uri)
@ -84,12 +83,12 @@ if (process.client) {
const windowReload = () => { const windowReload = () => {
document.visibilityState === 'visible' && window.location.reload() document.visibilityState === 'visible' && window.location.reload()
} }
watch(currentUserId, async (id, oldId) => { watch(currentUserHandle, async (handle, oldHandle) => {
// when sign in or switch account // when sign in or switch account
if (id) { if (handle) {
if (id === currentUser.value?.account?.id) { if (handle === currentUser.value?.account?.acct) {
// when sign in, the other tab will not have the user, idb is not reactive // when sign in, the other tab will not have the user, idb is not reactive
const newUser = users.value.find(user => user.account?.id === id) const newUser = users.value.find(user => user.account?.acct === handle)
// if the user is there, then we are switching account // if the user is there, then we are switching account
if (newUser) { if (newUser) {
// check if the change is on current tab: if so, don't reload // check if the change is on current tab: if so, don't reload
@ -101,19 +100,13 @@ if (process.client) {
window.addEventListener('visibilitychange', windowReload, { capture: true }) window.addEventListener('visibilitychange', windowReload, { capture: true })
} }
// when sign out // when sign out
else if (oldId) { else if (oldHandle) {
const oldUser = users.value.find(user => user.account?.id === oldId) const oldUser = users.value.find(user => user.account?.acct === oldHandle)
// when sign out, the other tab will not have the user, idb is not reactive // when sign out, the other tab will not have the user, idb is not reactive
if (oldUser) if (oldUser)
window.addEventListener('visibilitychange', windowReload, { capture: true }) window.addEventListener('visibilitychange', windowReload, { capture: true })
} }
}, { immediate: true, flush: 'post' }) }, { immediate: true, flush: 'post' })
// for injected script to read
const currentUserHandle = computed(() => currentUser.value?.account.acct || '')
watchEffect(() => {
localStorage.setItem(STORAGE_KEY_CURRENT_USER_HANDLE, currentUserHandle.value)
})
} }
export const useUsers = () => users export const useUsers = () => users
@ -144,12 +137,12 @@ export async function loginTo(masto: ElkMasto, user: Overwrite<UserLogin, { acco
const account = getUser()?.account const account = getUser()?.account
if (account) if (account)
currentUserId.value = account.id currentUserHandle.value = account.acct
const [me, pushSubscription] = await Promise.all([ const [me, pushSubscription] = await Promise.all([
fetchAccountInfo(client, user.server), fetchAccountInfo(client, user.server),
// if PWA is not enabled, don't get push subscription // if PWA is not enabled, don't get push subscription
useRuntimeConfig().public.pwaEnabled useAppConfig().pwaEnabled
// we get 404 response instead empty data // we get 404 response instead empty data
? client.v1.webPushSubscriptions.fetch().catch(() => Promise.resolve(undefined)) ? client.v1.webPushSubscriptions.fetch().catch(() => Promise.resolve(undefined))
: Promise.resolve(undefined), : Promise.resolve(undefined),
@ -168,7 +161,7 @@ export async function loginTo(masto: ElkMasto, user: Overwrite<UserLogin, { acco
}) })
} }
currentUserId.value = me.id currentUserHandle.value = me.acct
} }
export async function fetchAccountInfo(client: mastodon.Client, server: string) { export async function fetchAccountInfo(client: mastodon.Client, server: string) {
@ -194,7 +187,7 @@ export async function removePushNotificationData(user: UserLogin, fromSWPushMana
// clear push notification policy // clear push notification policy
delete useLocalStorage<PushNotificationPolicy>(STORAGE_KEY_NOTIFICATION_POLICY, {}).value[acct] delete useLocalStorage<PushNotificationPolicy>(STORAGE_KEY_NOTIFICATION_POLICY, {}).value[acct]
const pwaEnabled = useRuntimeConfig().public.pwaEnabled const pwaEnabled = useAppConfig().pwaEnabled
const pwa = useNuxtApp().$pwa const pwa = useNuxtApp().$pwa
const registrationError = pwa?.registrationError === true const registrationError = pwa?.registrationError === true
const unregister = pwaEnabled && !registrationError && pwa?.registrationError === true && fromSWPushManager const unregister = pwaEnabled && !registrationError && pwa?.registrationError === true && fromSWPushManager
@ -238,7 +231,7 @@ export async function switchUser(user: UserLogin) {
} }
} }
export async function signout() { export async function signOut() {
// TODO: confirm // TODO: confirm
if (!currentUser.value) if (!currentUser.value)
return return
@ -253,24 +246,24 @@ export async function signout() {
// Clear stale data // Clear stale data
clearUserLocalStorage() clearUserLocalStorage()
if (!users.value.some((u, i) => u.server === currentUser.value!.server && i !== index)) if (!users.value.some((u, i) => u.server === currentUser.value!.server && i !== index))
delete instances.value[currentUser.value.server] delete instanceStorage.value[currentUser.value.server]
await removePushNotifications(currentUser.value) await removePushNotifications(currentUser.value)
await removePushNotificationData(currentUser.value) await removePushNotificationData(currentUser.value)
currentUserId.value = '' currentUserHandle.value = ''
// Remove the current user from the users // Remove the current user from the users
users.value.splice(index, 1) users.value.splice(index, 1)
} }
// Set currentUserId to next user if available // Set currentUserId to next user if available
currentUserId.value = users.value[0]?.account?.id currentUserHandle.value = users.value[0]?.account?.acct
if (!currentUserId.value) if (!currentUserHandle.value)
await useRouter().push('/') await useRouter().push('/')
loginTo(masto, currentUser.value) loginTo(masto, currentUser.value || { server: publicServer.value })
} }
export function checkLogin() { export function checkLogin() {

View file

@ -7,7 +7,6 @@ export const STORAGE_KEY_DRAFTS = 'elk-drafts'
export const STORAGE_KEY_USERS = 'elk-users' export const STORAGE_KEY_USERS = 'elk-users'
export const STORAGE_KEY_SERVERS = 'elk-servers' export const STORAGE_KEY_SERVERS = 'elk-servers'
export const STORAGE_KEY_NODES = 'elk-nodes' export const STORAGE_KEY_NODES = 'elk-nodes'
export const STORAGE_KEY_CURRENT_USER = 'elk-current-user'
export const STORAGE_KEY_CURRENT_USER_HANDLE = 'elk-current-user-handle' export const STORAGE_KEY_CURRENT_USER_HANDLE = 'elk-current-user-handle'
export const STORAGE_KEY_NOTIFY_TAB = 'elk-notify-tab' export const STORAGE_KEY_NOTIFY_TAB = 'elk-notify-tab'
export const STORAGE_KEY_FIRST_VISIT = 'elk-first-visit' export const STORAGE_KEY_FIRST_VISIT = 'elk-first-visit'

View file

@ -45,6 +45,7 @@ There are 5 environment variables to add.
| NUXT_CLOUDFLARE_NAMESPACE_ID | This is your CloudFlare KV Namespace ID. You can find it in "Workers > KV". | | NUXT_CLOUDFLARE_NAMESPACE_ID | This is your CloudFlare KV Namespace ID. You can find it in "Workers > KV". |
| NUXT_STORAGE_DRIVER | Because we're using CloudFlare, we'll need to set this to `cloudflare`. | | NUXT_STORAGE_DRIVER | Because we're using CloudFlare, we'll need to set this to `cloudflare`. |
| NUXT_PUBLIC_DEFAULT_SERVER | This is the address of the Mastodon instance that will show up when a user visits your Elk deployment and is not logged in. If you don't make that variable, it will point to `m.webtoo.ls` by default. | | NUXT_PUBLIC_DEFAULT_SERVER | This is the address of the Mastodon instance that will show up when a user visits your Elk deployment and is not logged in. If you don't make that variable, it will point to `m.webtoo.ls` by default. |
| SINGLE_INSTANCE_SERVER | This can't be set at runtime, but if enabled at build-time it will disable signing in to servers other than the server specified in `NUXT_PUBLIC_DEFAULT_SERVER` |
| NUXT_PUBLIC_PRIVACY_POLICY_URL | This is the URL to a web page with information on your privacy policy. | | NUXT_PUBLIC_PRIVACY_POLICY_URL | This is the URL to a web page with information on your privacy policy. |
That's it! All that's left to do is... That's it! All that's left to do is...

View file

@ -9,7 +9,7 @@
"preview": "nuxi preview" "preview": "nuxi preview"
}, },
"devDependencies": { "devDependencies": {
"@nuxt-themes/docus": "^1.4.7", "@nuxt-themes/docus": "^1.6.1",
"nuxt": "^3.1.0" "nuxt": "^3.1.1"
} }
} }

View file

@ -15,16 +15,16 @@ const isGrayscale = usePreferences('grayscaleMode')
</script> </script>
<template> <template>
<div h-full :data-mode="isHydrated && isGrayscale ? 'grayscale' : ''"> <div h-full :data-mode="isHydrated && isGrayscale ? 'grayscale' : ''" data-tauri-drag-region>
<main flex w-full mxa lg:max-w-80rem class="native:grid native:sm:grid-cols-[auto_1fr] native:lg:grid-cols-[auto_minmax(600px,2fr)_1fr]"> <main flex w-full mxa lg:max-w-80rem class="native:grid native:sm:grid-cols-[auto_1fr] native:lg:grid-cols-[auto_minmax(600px,2fr)_1fr]">
<aside class="hidden native:w-auto sm:flex w-1/8 md:w-1/6 lg:w-1/5 xl:w-1/4 justify-end xl:me-4 zen-hide" relative> <aside class="native:w-auto w-1/8 md:w-1/6 lg:w-1/5 xl:w-1/4 zen-hide" hidden sm:flex justify-end xl:me-4 native:me-0 relative>
<div sticky top-0 w-20 xl:w-100 h-screen flex="~ col" lt-xl-items-center> <div sticky top-0 w-20 xl:w-100 h-screen flex="~ col" lt-xl-items-center>
<slot name="left"> <slot name="left">
<div flex="~ col" overflow-y-auto justify-between h-full max-w-full pt-5 native:pt-7 overflow-x-hidden> <div flex="~ col" overflow-y-auto justify-between h-full max-w-full overflow-x-hidden>
<NavTitle /> <NavTitle />
<NavSide command /> <NavSide command />
<div flex-auto /> <div flex-auto />
<div v-if="isHydrated" flex flex-col> <div v-if="isHydrated" flex flex-col sticky bottom-0 bg-base>
<div hidden xl:block> <div hidden xl:block>
<UserSignInEntry v-if="!currentUser" /> <UserSignInEntry v-if="!currentUser" />
</div> </div>
@ -59,7 +59,7 @@ const isGrayscale = usePreferences('grayscaleMode')
<NavBottom v-if="isHydrated" sm:hidden /> <NavBottom v-if="isHydrated" sm:hidden />
</div> </div>
</div> </div>
<aside v-if="isHydrated && !wideLayout" class="hidden sm:none lg:block w-1/4 zen-hide"> <aside v-if="isHydrated && !wideLayout" class="hidden lg:w-1/5 xl:w-1/4 sm:none xl:block native:w-full zen-hide">
<div sticky top-0 h-screen flex="~ col" gap-2 py3 ms-2> <div sticky top-0 h-screen flex="~ col" gap-2 py3 ms-2>
<slot name="right"> <slot name="right">
<div flex-auto /> <div flex-auto />

View file

@ -28,6 +28,8 @@
"muted_users": "المستخدمون المكتومون", "muted_users": "المستخدمون المكتومون",
"muting": "قُمت بكتم", "muting": "قُمت بكتم",
"mutuals": "المتبادلون", "mutuals": "المتبادلون",
"notifications_on_post_disable": "التوقف عن إشعاري عندما ينشر {username}",
"notifications_on_post_enable": "إشعاري عندما ينشر {username}",
"pinned": "المثبتة", "pinned": "المثبتة",
"posts": "المنشورات", "posts": "المنشورات",
"posts_count": "{0} منشورات|{0} منشور|{0} منشورين|{0} منشورات|{0} منشور|{0} منشور", "posts_count": "{0} منشورات|{0} منشور|{0} منشورين|{0} منشورات|{0} منشور|{0} منشور",
@ -35,7 +37,9 @@
"profile_unavailable": "حساب غير متوفر", "profile_unavailable": "حساب غير متوفر",
"unblock": "إلغاء حظر", "unblock": "إلغاء حظر",
"unfollow": "إلغاء متابعة", "unfollow": "إلغاء متابعة",
"unmute": "إلغاء كتم" "unmute": "إلغاء كتم",
"view_other_followers": "قد لا يتم عرض المتابعين من خوادم مختلفين عن الخاص بك.",
"view_other_following": "قد لا يتم عرض من تتابع من خوادم مختلفين عن الخاص بك."
}, },
"action": { "action": {
"apply": "تطبيق", "apply": "تطبيق",
@ -44,6 +48,7 @@
"boost": "إعادة نشر", "boost": "إعادة نشر",
"boost_count": "{0}", "boost_count": "{0}",
"boosted": "أعيد نشرها", "boosted": "أعيد نشرها",
"clear_publish_failed": "مسح أخطاء النشر",
"clear_upload_failed": "مسح أخطاء تحميل الملف", "clear_upload_failed": "مسح أخطاء تحميل الملف",
"close": "أغلق", "close": "أغلق",
"compose": "منشور جديد", "compose": "منشور جديد",
@ -66,7 +71,7 @@
"switch_account": "تغيير الحساب", "switch_account": "تغيير الحساب",
"vote": "تصويت" "vote": "تصويت"
}, },
"app_desc_short": "منصة تواصل Mastodon رشيقة", "app_desc_short": "منصة تواصل ماستودون رشيقة",
"app_logo": "Elk شعار", "app_logo": "Elk شعار",
"app_name": "Elk", "app_name": "Elk",
"attachment": { "attachment": {
@ -101,9 +106,52 @@
"draft_title": "مسودة {0}", "draft_title": "مسودة {0}",
"drafts": "المسودات ({v})" "drafts": "المسودات ({v})"
}, },
"confirm": {
"block_account": {
"cancel": "إلغاء",
"confirm": "حظر",
"title": "هل أنت متأكد أنك تريد حظر {0}؟"
},
"block_domain": {
"cancel": "إلغاء",
"confirm": "حظر",
"title": "هل أنت متأكد أنك تريد حظر {0}؟"
},
"common": {
"cancel": "لا",
"confirm": "نعم"
},
"delete_posts": {
"cancel": "إلغاء",
"confirm": "حذف",
"title": "هل أنت متأكد أنك تريد حذف هذا المنشور؟"
},
"mute_account": {
"cancel": "إلغاء",
"confirm": "كتم",
"title": "هل أنت متأكد أنك تريد كتم {0}؟"
},
"show_reblogs": {
"cancel": "إلغاء",
"confirm": "اعرض",
"title": "هل أنت متأكد أنك تريد إظهار تعزيزات من {0}؟"
},
"unfollow": {
"cancel": "إلغاء",
"confirm": "إلغاء المتابعة",
"title": "هل أنت متأكد أنك تريد إلغاء المتابعة؟"
}
},
"conversation": { "conversation": {
"with": "مع" "with": "مع"
}, },
"custom_cards": {
"stackblitz": {
"lines": "الخطوط {0}",
"open": "افتح",
"snippet_from": "مقتطف من {0}"
}
},
"error": { "error": {
"account_not_found": "حساب {0} غير موجود", "account_not_found": "حساب {0} غير موجود",
"explore-list-empty": "لا توجد مشاركات شائعة الآن. تحقق مرة أخرى لاحقًا!", "explore-list-empty": "لا توجد مشاركات شائعة الآن. تحقق مرة أخرى لاحقًا!",
@ -113,6 +161,12 @@
"unsupported_file_format": "لا يمكن تحميل هذا النوع من الملفات" "unsupported_file_format": "لا يمكن تحميل هذا النوع من الملفات"
}, },
"help": { "help": {
"build_preview": {
"desc1": "أنت تشاهد حاليًا إصدار معاينة من Elk عامة - {0}.",
"desc2": "قد يحتوي على تغييرات لم تتم مراجعتها أو حتى ضارة.",
"desc3": "لا تسجل الدخول بحسابك الحقيقي.",
"title": "معاينة النشر"
},
"desc_highlight": "توقع بعض الأخطاء والميزات المفقودة هنا وهناك.", "desc_highlight": "توقع بعض الأخطاء والميزات المفقودة هنا وهناك.",
"desc_para1": "نشكرك على اهتمامك بتجربة Elk ، عميل ماستدون العام!", "desc_para1": "نشكرك على اهتمامك بتجربة Elk ، عميل ماستدون العام!",
"desc_para2": "نحن نعمل بجد على التطوير وتحسينه بمرور الوقت. وسندعوك قريبًا للانضمام إلى القوة بمجرد أن نجعلها مفتوحة المصدر قريبًا!", "desc_para2": "نحن نعمل بجد على التطوير وتحسينه بمرور الوقت. وسندعوك قريبًا للانضمام إلى القوة بمجرد أن نجعلها مفتوحة المصدر قريبًا!",
@ -125,10 +179,16 @@
"language": { "language": {
"search": "بحث" "search": "بحث"
}, },
"list": {
"add_account": "إضافة حساب إلى القائمة",
"modify_account": "تعديل القوائم مع الحساب",
"remove_account": "إزالة الحساب من القائمة"
},
"menu": { "menu": {
"block_account": "حظر {0}", "block_account": "حظر {0}",
"block_domain": "حظر المجال {0}", "block_domain": "حظر المجال {0}",
"copy_link_to_post": "انسخ الرابط إلى هذا المنشور", "copy_link_to_post": "انسخ الرابط إلى هذا المنشور",
"copy_original_link_to_post": "انسخ الرابط الأصلي لهذا المنشور",
"delete": "حذف", "delete": "حذف",
"delete_and_redraft": "حذف وإعادة صياغة", "delete_and_redraft": "حذف وإعادة صياغة",
"delete_confirm": { "delete_confirm": {
@ -165,14 +225,18 @@
"blocked_users": "المستخدمين المحظورين", "blocked_users": "المستخدمين المحظورين",
"bookmarks": "العلامات المرجعية", "bookmarks": "العلامات المرجعية",
"built_at": "Built {0}", "built_at": "Built {0}",
"compose": "تأليف",
"conversations": "المحادثات", "conversations": "المحادثات",
"explore": "استكشف", "explore": "استكشف",
"favourites": "المفضلة", "favourites": "المفضلة",
"federated": "الفديرالية", "federated": "الفديرالية",
"home": "الرئيسيّة", "home": "الرئيسيّة",
"list": "قائمة",
"lists": "القوائم",
"local": "المحلي", "local": "المحلي",
"muted_users": "المستخدمون المكتموصين", "muted_users": "المستخدمون المكتموصين",
"notifications": "التنبيهات", "notifications": "التنبيهات",
"privacy": "خصوصية",
"profile": "الصفحة التعريفية", "profile": "الصفحة التعريفية",
"search": "البحث", "search": "البحث",
"select_feature_flags": "تبديل علامات الميزات", "select_feature_flags": "تبديل علامات الميزات",
@ -202,27 +266,29 @@
}, },
"pwa": { "pwa": {
"dismiss": "تجاهل", "dismiss": "تجاهل",
"title": "يتوفر تحديث Elk الجديد", "install": "تحميل",
"install_title": "تحميل Elk",
"title": "يتوفر تحديث Elk جديد",
"update": "تحديث", "update": "تحديث",
"update_available_short": "تحديث Elk", "update_available_short": "تحديث Elk",
"webmanifest": { "webmanifest": {
"canary": { "canary": {
"description": "نسخة ويب رشيقة ل Mastodon (النسخة الإنشائية)", "description": "نسخة ويب رشيقة لماستودون (النسخة الإنشائية)",
"name": "Elk (النسخة الإنشائية)", "name": "Elk (النسخة الإنشائية)",
"short_name": "Elk (النسخة الإنشائية)" "short_name": "Elk (النسخة الإنشائية)"
}, },
"dev": { "dev": {
"description": "نسخة ويب رشيقة ل Mastodon (النسخة التطويرية)", "description": "نسخة ويب رشيقة لماستودون (النسخة التطويرية)",
"name": "Elk (النسخة التطويرية)", "name": "Elk (النسخة التطويرية)",
"short_name": "Elk (النسخة التطويرية)" "short_name": "Elk (النسخة التطويرية)"
}, },
"preview": { "preview": {
"description": "نسخة ويب رشيقة ل Mastodon (معاينة)", "description": "نسخة ويب رشيقة لماستودون (معاينة)",
"name": "Elk (معاينة)", "name": "Elk (معاينة)",
"short_name": "Elk (معاينة)" "short_name": "Elk (معاينة)"
}, },
"release": { "release": {
"description": "نسخة ويب رشيقة ل Mastodon", "description": "نسخة ويب رشيقة لماستودون",
"name": "Elk", "name": "Elk",
"short_name": "Elk" "short_name": "Elk"
} }
@ -241,10 +307,11 @@
"sponsors": "الرعاة", "sponsors": "الرعاة",
"sponsors_body_1": "تم تمويل Elk من قبل الشركات والأفراد التاليين:", "sponsors_body_1": "تم تمويل Elk من قبل الشركات والأفراد التاليين:",
"sponsors_body_2": "وكذا من قبل الشركات التالية:", "sponsors_body_2": "وكذا من قبل الشركات التالية:",
"sponsors_body_3": "إذا كنت تستمتع بإستخدام Elk، فنحن نشجعك على التبرع لدعم المشروع." "sponsors_body_3": "إذا كنت تستمتع بإستخدام Elk، فنحن نشجعك على التبرع لدعم المشروع.",
"version": "الإصدار"
}, },
"account_settings": { "account_settings": {
"description": "قم بتحرير إعدادات حسابك في موقع Mastodon الأصلي", "description": "قم بتحرير إعدادات حسابك في موقع ماستودون الأصل",
"label": "إعدادت الحساب" "label": "إعدادت الحساب"
}, },
"feature_flags": { "feature_flags": {
@ -260,7 +327,8 @@
"font_size": "حجم الخط", "font_size": "حجم الخط",
"label": "واجهه المستخدم", "label": "واجهه المستخدم",
"light_mode": "وضع الضوء", "light_mode": "وضع الضوء",
"system_mode": "النظام" "system_mode": "النظام",
"theme_color": "وضع المظهر"
}, },
"language": { "language": {
"display_language": "اللغة المعروضة", "display_language": "اللغة المعروضة",
@ -317,7 +385,19 @@
}, },
"notifications_settings": "التنبيهات", "notifications_settings": "التنبيهات",
"preferences": { "preferences": {
"label": "التفضيلات" "enable_autoplay": "تفعيل التشغيل التلقائي",
"github_cards": "بطاقات GitHub",
"grayscale_mode": "مظهر رمادي",
"hide_account_hover_card": "إخفاء بطاقة الحساب عند المرور فوقها",
"hide_boost_count": "إخفاء عدد التعزيز",
"hide_favorite_count": "إخفاء التعداد المفضل",
"hide_follower_count": "إخفاء عدد المتابعين",
"hide_reply_count": "إخفاء عدد الردود",
"hide_translation": "إخفاء الترجمة",
"label": "التفضيلات",
"title": "الميزات التجريبية",
"user_picker": "منتقي الحسابات",
"virtual_scroll": "التمرير الافتراضي"
}, },
"profile": { "profile": {
"appearance": { "appearance": {
@ -340,22 +420,20 @@
"export": "تصدير معلومات المستخدم", "export": "تصدير معلومات المستخدم",
"import": "استيراد معلومات المستخدم", "import": "استيراد معلومات المستخدم",
"label": "المستخدمون المسجلون" "label": "المستخدمون المسجلون"
},
"wellness": {
"feature": {
"hide_boost_count": "إخفاء عدد المشاركات",
"hide_favorite_count": "إخفاء عدد المفضلة",
"hide_follower_count": "إخفاء عدد المتابعين"
},
"label": "الصحة العامة"
} }
}, },
"share-target": {
"description": "يمكن تحديث Elk بحيث يمكنك مشاركة المحتوى من التطبيقات الأخرى ، ما عليك سوى تثبيت Elk على جهازك أو الكمبيوتر وتسجيل الدخول.",
"hint": "لمشاركة المحتوى مع Elk ، يجب تثبيت Elk ويجب تسجيل الدخول.",
"title": "شارك مع Elk"
},
"state": { "state": {
"attachments_exceed_server_limit": "تجاوز عدد المرفقات الحد الأقصى لكل منشور.", "attachments_exceed_server_limit": "تجاوز عدد المرفقات الحد الأقصى لكل منشور.",
"attachments_limit_error": "تجاوز الحد لل منشور", "attachments_limit_error": "تجاوز الحد لل منشور",
"edited": "(معدل)", "edited": "(معدل)",
"editing": "تعديل", "editing": "تعديل",
"loading": "جاري التحميل ...", "loading": "جاري التحميل ...",
"publish_failed": "فشل النشر",
"publishing": "قيد النشر", "publishing": "قيد النشر",
"upload_failed": "التحميل فشل", "upload_failed": "التحميل فشل",
"uploading": "جاري التحميل ..." "uploading": "جاري التحميل ..."
@ -390,6 +468,7 @@
"edited": "تم تعديله في {0}" "edited": "تم تعديله في {0}"
}, },
"tab": { "tab": {
"accounts": "الحسابات",
"for_you": "مصممة لك", "for_you": "مصممة لك",
"hashtags": "هاشتاغ", "hashtags": "هاشتاغ",
"media": "الصور/الفيديو", "media": "الصور/الفيديو",
@ -455,6 +534,7 @@
"explore_links_intro": "يتم التحدث عن هذه القصص الإخبارية من قبل الأشخاص الموجودين على هذه الشبكة وغيرها من الشبكات اللامركزية في الوقت الحالي", "explore_links_intro": "يتم التحدث عن هذه القصص الإخبارية من قبل الأشخاص الموجودين على هذه الشبكة وغيرها من الشبكات اللامركزية في الوقت الحالي",
"explore_posts_intro": "تكتسب هذه المنشورات الكثير من النشاط على الشبكة وغيرها من الشبكات اللامركزية في الوقت الحالي", "explore_posts_intro": "تكتسب هذه المنشورات الكثير من النشاط على الشبكة وغيرها من الشبكات اللامركزية في الوقت الحالي",
"explore_tags_intro": "تكتسب هذه الهاشتاغ الكثير من النشاط بين الأشخاص على هذه الشبكة وغيرها من الشبكات اللامركزية في الوقت الحالي", "explore_tags_intro": "تكتسب هذه الهاشتاغ الكثير من النشاط بين الأشخاص على هذه الشبكة وغيرها من الشبكات اللامركزية في الوقت الحالي",
"publish_failed": "أغلق الرسائل الفاشلة أعلى المحرر لإعادة نشر المنشورات",
"toggle_code_block": "تبديل كتلة التعليمات البرمجية" "toggle_code_block": "تبديل كتلة التعليمات البرمجية"
}, },
"user": { "user": {

View file

@ -270,7 +270,14 @@
}, },
"language": { "language": {
"display_language": "Anzeigesprache", "display_language": "Anzeigesprache",
"label": "Sprache" "label": "Sprache",
"translations": {
"add": "Hinzufügen",
"choose_language": "Sprache wählen",
"heading": "Übersetzungen",
"hide_specific": "Bestimmte Übersetzungen ausblenden",
"remove": "Entfernen"
}
}, },
"notifications": { "notifications": {
"label": "Benachrichtigungen", "label": "Benachrichtigungen",
@ -328,7 +335,7 @@
"hide_boost_count": "Boost-Zähler ausblenden", "hide_boost_count": "Boost-Zähler ausblenden",
"hide_favorite_count": "Favoritenzahl ausblenden", "hide_favorite_count": "Favoritenzahl ausblenden",
"hide_follower_count": "Anzahl der Follower ausblenden", "hide_follower_count": "Anzahl der Follower ausblenden",
"hide_translation": "Übersetzungen ausblenden", "hide_translation": "Übersetzungen komplett ausblenden",
"label": "Einstellungen", "label": "Einstellungen",
"title": "Experimentelle Funktionen", "title": "Experimentelle Funktionen",
"user_picker": "Benutzerauswahl", "user_picker": "Benutzerauswahl",

View file

@ -35,6 +35,7 @@
"posts_count": "{0} Posts|{0} Post|{0} Posts", "posts_count": "{0} Posts|{0} Post|{0} Posts",
"profile_description": "{0}'s profile header", "profile_description": "{0}'s profile header",
"profile_unavailable": "Profile unavailable", "profile_unavailable": "Profile unavailable",
"request_follow": "Request to follow",
"unblock": "Unblock", "unblock": "Unblock",
"unfollow": "Unfollow", "unfollow": "Unfollow",
"unmute": "Unmute", "unmute": "Unmute",
@ -68,6 +69,7 @@
"save": "Save", "save": "Save",
"save_changes": "Save changes", "save_changes": "Save changes",
"sign_in": "Sign in", "sign_in": "Sign in",
"sign_in_to": "Sign in to {0}",
"switch_account": "Switch account", "switch_account": "Switch account",
"vote": "Vote" "vote": "Vote"
}, },
@ -116,6 +118,11 @@
"cancel": "No", "cancel": "No",
"confirm": "Yes" "confirm": "Yes"
}, },
"delete_list": {
"cancel": "Cancel",
"confirm": "Delete",
"title": "Are you sure you want to delete the \"{0}\" list?"
},
"delete_posts": { "delete_posts": {
"cancel": "Cancel", "cancel": "Cancel",
"confirm": "Delete", "confirm": "Delete",
@ -169,6 +176,7 @@
"desc_para4": "Elk is Open Source. If you'd like to help with testing, giving feedback, or contributing,", "desc_para4": "Elk is Open Source. If you'd like to help with testing, giving feedback, or contributing,",
"desc_para5": "reach out to us on GitHub", "desc_para5": "reach out to us on GitHub",
"desc_para6": "and get involved.", "desc_para6": "and get involved.",
"footer_team": "The Elk Team",
"title": "Elk is in Preview!" "title": "Elk is in Preview!"
}, },
"language": { "language": {
@ -176,8 +184,19 @@
}, },
"list": { "list": {
"add_account": "Add account to list", "add_account": "Add account to list",
"cancel_edit": "Cancel editing",
"clear_error": "Clear error",
"create": "Create",
"delete": "Delete this list",
"delete_error": "There was an error while deleting the list",
"edit": "Edit this list",
"edit_error": "There was an error while updating the list",
"error": "There was an error while creating the list",
"error_prefix": "Error: ",
"list_title_placeholder": "List title",
"modify_account": "Modify lists with account", "modify_account": "Modify lists with account",
"remove_account": "Remove account from list" "remove_account": "Remove account from list",
"save": "Save changes"
}, },
"menu": { "menu": {
"block_account": "Block {0}", "block_account": "Block {0}",
@ -290,6 +309,7 @@
}, },
"settings": { "settings": {
"about": { "about": {
"built_at": "Built",
"label": "About", "label": "About",
"meet_the_team": "Meet the team", "meet_the_team": "Meet the team",
"sponsor_action": "Sponsor us", "sponsor_action": "Sponsor us",
@ -316,7 +336,14 @@
}, },
"language": { "language": {
"display_language": "Display Language", "display_language": "Display Language",
"label": "Language" "label": "Language",
"translations": {
"add": "Add",
"choose_language": "Choose language",
"heading": "Translations",
"hide_specific": "Hide specific translations",
"remove": "Remove"
}
}, },
"notifications": { "notifications": {
"label": "Notifications", "label": "Notifications",
@ -345,10 +372,13 @@
"save_settings": "Save settings", "save_settings": "Save settings",
"subscription_error": { "subscription_error": {
"clear_error": "Clear error", "clear_error": "Clear error",
"invalid_vapid_key": "The VAPID public key seems to be invalid.",
"permission_denied": "Permission denied: enable notifications in your browser.", "permission_denied": "Permission denied: enable notifications in your browser.",
"repo_link": "Elk's repository in Github",
"request_error": "An error occurred while requesting the subscription, try again and if the error persists, please report the issue to the Elk repository.", "request_error": "An error occurred while requesting the subscription, try again and if the error persists, please report the issue to the Elk repository.",
"title": "Could not subscribe to push notifications", "title": "Could not subscribe to push notifications",
"too_many_registrations": "Due to browser limitations, Elk cannot use the push notifications service for multiple accounts on different servers. You should unsubscribe from push notifications on another account and try again." "too_many_registrations": "Due to browser limitations, Elk cannot use the push notifications service for multiple accounts on different servers. You should unsubscribe from push notifications on another account and try again.",
"vapid_not_supported": "Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol."
}, },
"title": "Push notifications settings", "title": "Push notifications settings",
"undo_settings": "Undo changes", "undo_settings": "Undo changes",
@ -365,23 +395,29 @@
"re_auth": "It seems that your server does not support push notifications. Try sign out and sign in again, if this message still appears contact your server administrator." "re_auth": "It seems that your server does not support push notifications. Try sign out and sign in again, if this message still appears contact your server administrator."
} }
}, },
"show_btn": "Go to notifications settings" "show_btn": "Go to notifications settings",
"under_construction": "Under construction"
}, },
"notifications_settings": "Notifications", "notifications_settings": "Notifications",
"preferences": { "preferences": {
"enable_autoplay": "Enable Autoplay", "enable_autoplay": "Enable Autoplay",
"enable_pinch_to_zoom": "Enable pinch to zoom",
"github_cards": "GitHub Cards", "github_cards": "GitHub Cards",
"grayscale_mode": "Grayscale mode", "grayscale_mode": "Grayscale mode",
"hide_account_hover_card": "Hide account hover card", "hide_account_hover_card": "Hide account hover card",
"hide_alt_indi_on_posts": "Hide alt indicator on posts",
"hide_boost_count": "Hide boost count", "hide_boost_count": "Hide boost count",
"hide_favorite_count": "Hide favorite count", "hide_favorite_count": "Hide favorite count",
"hide_follower_count": "Hide follower count", "hide_follower_count": "Hide follower count",
"hide_reply_count": "Hide reply count", "hide_reply_count": "Hide reply count",
"hide_translation": "Hide translation", "hide_translation": "Hide translation",
"hide_username_emojis": "Hide username emojis",
"hide_username_emojis_description": "Hides emojis from usernames in timelines. Emojis will still be visible in their profiles.",
"label": "Preferences", "label": "Preferences",
"title": "Experimental Features", "title": "Experimental Features",
"user_picker": "User Picker", "user_picker": "User Picker",
"virtual_scroll": "Virtual Scrolling" "virtual_scroll": "Virtual Scrolling",
"wellbeing": "Wellbeing"
}, },
"profile": { "profile": {
"appearance": { "appearance": {
@ -430,8 +466,10 @@
"filter_removed_phrase": "Removed by filter", "filter_removed_phrase": "Removed by filter",
"filter_show_anyway": "Show anyway", "filter_show_anyway": "Show anyway",
"img_alt": { "img_alt": {
"ALT": "ALT",
"desc": "Description", "desc": "Description",
"dismiss": "Dismiss" "dismiss": "Dismiss",
"read": "Read {0} description"
}, },
"poll": { "poll": {
"count": "{0} votes|{0} vote|{0} votes", "count": "{0} votes|{0} vote|{0} votes",
@ -519,8 +557,11 @@
"explore_links_intro": "These news stories are being talked about by people on this and other servers of the decentralized network right now.", "explore_links_intro": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
"explore_posts_intro": "These posts from this and other servers in the decentralized network are gaining traction on this server right now.", "explore_posts_intro": "These posts from this and other servers in the decentralized network are gaining traction on this server right now.",
"explore_tags_intro": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.", "explore_tags_intro": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
"open_editor_tools": "Editor tools",
"publish_failed": "Close failed messages at the top of editor to republish posts", "publish_failed": "Close failed messages at the top of editor to republish posts",
"toggle_code_block": "Toggle code block" "toggle_bold": "Toggle bold",
"toggle_code_block": "Toggle code block",
"toggle_italic": "Toggle italic"
}, },
"user": { "user": {
"add_existing": "Add an existing account", "add_existing": "Add an existing account",

View file

@ -1 +1,191 @@
{} {
"a11y": {
"locale_changed": "Idioma configurado en {0}",
"locale_changing": "Actualizando idioma, espera..."
},
"account": {
"avatar_description": "Foto de perfil de",
"blocked_by": "Estás bloqueado por este usuario.",
"blocked_domains": "Dominios ocultos",
"favourites": "Publicaciones Favoritas",
"go_to_profile": "Ver perfil",
"moved_title": "indicó que su nueva cuenta es ",
"mutuals": "Mutuales",
"notifications_on_post_disable": "No notificar cuando {username} publique",
"notifications_on_post_enable": "Notificarme cuando {username} publique",
"pinned": "Publicaciones ancladas",
"profile_description": "Imagen de portada de {0}",
"unmute": "Quitar silencio"
},
"action": {
"apply": "Guardar cambios",
"bookmark": "Marcar",
"confirm": "Cortar",
"edit": "Actualizar",
"enter_app": "Ingresar",
"favourite": "Marcar como favorita",
"favourited": "Marcada como favorita",
"reset": "Resetear",
"switch_account": "Cambiar de cuenta"
},
"app_logo": "Logo de Elk",
"attachment": {
"remove_label": "Eliminar archivo adjunto"
},
"command": {
"n-people-in-the-past-n-days": "{0} usuarios en los últimos {1} días"
},
"common": {
"end_of_list": "Fin de la lista",
"offline_desc": "No tienes acceso a internet. Por favor, comprueba que tienes una conexión a la red."
},
"confirm": {
"block_account": {
"cancel": "No",
"confirm": "Sí, bloquear",
"title": "¿De verdad quieres bloquear a {0}?"
},
"block_domain": {
"cancel": "No",
"confirm": "Sí ocultar",
"title": "¿De verdad quieres ocultar a {0}?"
},
"delete_posts": {
"title": "¿De verdad quieres eliminar esta publicación?"
},
"mute_account": {
"title": "¿De verdad quieres silenciar a {0}?"
},
"show_reblogs": {
"cancel": "No",
"confirm": "Sí, ver",
"title": "¿De verdad quieres ver los retoots de {0}"
},
"unfollow": {
"title": "¿De verdad quieres dejar de seguir?"
}
},
"error": {
"file_size_cannot_exceed_n_mb": "El tamaño del archivo no puede ser de más de {0}MB",
"unsupported_file_format": "Formato de archivo no soportado"
},
"help": {
"desc_highlight": "Es normal que aparezcan algunos errores y funcionalidades que aún estén en desarrollo.",
"desc_para1": "¡Gracias por tu interés en probar Elk, nuestro cliente genérico en desarrollo para Mastodon!",
"desc_para2": "Estamos haciendo lo posible para ir mejorando constantemente.",
"desc_para4": "Elk es de código abierto. Si quieres probar para ayudar, opinar o contribuir,",
"desc_para5": "contáctanos a través de GitHub"
},
"list": {
"add_account": "Añadir cuenta a la lista",
"remove_account": "Quitar cuenta de la lista"
},
"menu": {
"block_domain": "Ocultar dominio {0}",
"delete_and_redraft": "Eliminar y volver a borrador",
"edit": "Actualizar",
"pin_on_profile": "Anclar en tu perfil",
"show_favourited_and_boosted_by": "Ver quien marcó como favorita y quien retooteó",
"show_reblogs": "Ver retoots de {0}",
"unblock_domain": "Ver dominio {0}",
"unmute_account": "Quitar silencio a {0}",
"unmute_conversation": "Quitar silencio de la publicación",
"unpin_on_profile": "Desanclar del perfil"
},
"nav": {
"back": "Atrás",
"blocked_domains": "Dominios ocultos",
"built_at": "Generado {0}",
"conversations": "Mensajes directos",
"favourites": "Favoritas",
"federated": "Historia federada",
"local": "Historia local",
"settings": "Preferencias",
"toggle_theme": "Cambiar tema de color",
"zen_mode": "Modo sin distracciones"
},
"notification": {
"followed_you": "te siguió",
"update_status": "actualizó su publicación"
},
"placeholder": {
"default_1": "¿En qué piensas?"
},
"search": {
"search_empty": "No se encontraron resultados para la búsqueda"
},
"settings": {
"about": {
"built_at": "Compilado el",
"sponsor_action": "Patrocina"
},
"account_settings": {
"description": "Actualiza los ajustes de tu cuenta en la interfaz de Mastodon.",
"label": "Configuración de cuenta"
},
"interface": {
"color_mode": "Temas de color",
"dark_mode": "Tema oscuro",
"default": " (predeterminado)",
"font_size": "Tamaño de fuente",
"light_mode": "Tema claro",
"system_mode": "Color del sistema"
},
"language": {
"display_language": "Idioma en pantalla",
"translations": {
"add": "Añadir",
"hide_specific": "Ocultar una traducción específica",
"remove": "Quitar"
}
},
"notifications": {
"notifications": {
"label": "Preferencias de notificaciones"
},
"push_notifications": {
"label": "Preferencias de notificaciones push"
},
"show_btn": "Ir a preferencias de notificaciones",
"under_construction": "En desarrollo"
},
"preferences": {
"grayscale_mode": "Tema en escala de grises"
},
"profile": {
"appearance": {
"description": "Actualizar foto, nombre de usuario, perfil, etc.",
"display_name": "Nombre visible",
"profile_metadata_desc": "Puedes ver en tu perfil hasta 4 elementos en forma de tabla",
"title": "Actualizar perfil"
},
"featured_tags": {
"description": "Los usuarios navegan por tus publicaciones públicas con estas etiquetas.",
"label": "Etiquetas destacadas"
}
},
"users": {
"label": "Usuarios en línea"
}
},
"status": {
"spoiler_show_less": "Menos"
},
"tab": {
"hashtags": "Etiquetas"
},
"timeline": {
"show_new_items": "Ver {v} nuevas publicaciones|Ver {v} nueva publicación|Ver {v} nuevas publicaciones"
},
"title": {
"federated_timeline": "Historia federada",
"local_timeline": "Historia local"
},
"tooltip": {
"add_emojis": "Insertar emoji",
"change_content_visibility": "Cambiar visibilidad"
},
"user": {
"add_existing": "Añadir una cuenta existente"
}
}

View file

@ -8,7 +8,7 @@
}, },
"account": { "account": {
"avatar_description": "avatar de {0}", "avatar_description": "avatar de {0}",
"blocked_by": "Estás bloqueado por este usuario.", "blocked_by": "Has sido bloqueado por este usuario.",
"blocked_domains": "Dominios bloqueados", "blocked_domains": "Dominios bloqueados",
"blocked_users": "Usuarios bloqueados", "blocked_users": "Usuarios bloqueados",
"blocking": "Bloqueado", "blocking": "Bloqueado",
@ -35,6 +35,7 @@
"posts_count": "{0} Publicaciones|{0} Publicación|{0} Publicaciones", "posts_count": "{0} Publicaciones|{0} Publicación|{0} Publicaciones",
"profile_description": "Encabezado del perfil de {0}", "profile_description": "Encabezado del perfil de {0}",
"profile_unavailable": "Perfil no disponible", "profile_unavailable": "Perfil no disponible",
"request_follow": "Solicitud para seguirte",
"unblock": "Desbloquear", "unblock": "Desbloquear",
"unfollow": "Dejar de seguir", "unfollow": "Dejar de seguir",
"unmute": "Dejar de silenciar", "unmute": "Dejar de silenciar",
@ -68,6 +69,7 @@
"save": "Guardar", "save": "Guardar",
"save_changes": "Guardar cambios", "save_changes": "Guardar cambios",
"sign_in": "Iniciar sesión", "sign_in": "Iniciar sesión",
"sign_in_to": "Iniciar sesión en {0}",
"switch_account": "Cambiar cuenta", "switch_account": "Cambiar cuenta",
"vote": "Votar" "vote": "Votar"
}, },
@ -76,7 +78,7 @@
"app_name": "Elk", "app_name": "Elk",
"attachment": { "attachment": {
"edit_title": "Descripción", "edit_title": "Descripción",
"remove_label": "Eliminar archivo adjunto" "remove_label": "Eliminar fichero adjunto"
}, },
"command": { "command": {
"activate": "Activar", "activate": "Activar",
@ -116,6 +118,11 @@
"cancel": "No", "cancel": "No",
"confirm": "Si" "confirm": "Si"
}, },
"delete_list": {
"cancel": "Cancelar",
"confirm": "Eliminar",
"title": "¿Está seguro de querer eliminar la lista \"{0}\"?"
},
"delete_posts": { "delete_posts": {
"cancel": "Cancelar", "cancel": "Cancelar",
"confirm": "Eliminar", "confirm": "Eliminar",
@ -150,10 +157,10 @@
"error": { "error": {
"account_not_found": "No se encontró la cuenta {0}", "account_not_found": "No se encontró la cuenta {0}",
"explore-list-empty": "No hay tendencias en este momento. ¡Vuelve más tarde!", "explore-list-empty": "No hay tendencias en este momento. ¡Vuelve más tarde!",
"file_size_cannot_exceed_n_mb": "El tamaño del archivo no puede exceder los {0}MB", "file_size_cannot_exceed_n_mb": "El tamaño del fichero no puede exceder los {0}MB",
"sign_in_error": "No se pudo conectar con el servidor.", "sign_in_error": "No se pudo conectar con el servidor.",
"status_not_found": "Estado no encontrado", "status_not_found": "Publicación no encontrada",
"unsupported_file_format": "Tipo de archivo no soportado" "unsupported_file_format": "Tipo de fichero no soportado"
}, },
"help": { "help": {
"build_preview": { "build_preview": {
@ -164,11 +171,12 @@
}, },
"desc_highlight": "Es normal encontrar algunos errores y características faltantes aquí y allá.", "desc_highlight": "Es normal encontrar algunos errores y características faltantes aquí y allá.",
"desc_para1": "¡Gracias por el interés en probar Elk, nuestro cliente genérico en desarrollo para Mastodon!", "desc_para1": "¡Gracias por el interés en probar Elk, nuestro cliente genérico en desarrollo para Mastodon!",
"desc_para2": "Estamos trabajando duro en el desarrollo y mejorándolo constantemente. ¡Y pronto te invitaremos a que te unas una vez que lo hagamos de código abierto!", "desc_para2": "Estamos trabajando duro en el desarrollo y mejorándolo constantemente.",
"desc_para3": "Para ayudar a impulsar el desarrollo, puedes patrocinar a los miembros de nuestro equipo con los enlaces a continuación.", "desc_para3": "Para ayudar a impulsar el desarrollo, puedes patrocinar a los miembros de nuestro equipo con los enlaces a continuación. ¡Esperamos que estés disfrutando Elk!",
"desc_para4": "Antes de eso, si te gustaría ayudar probando, dando opinión o contribuyendo,", "desc_para4": "Elk es de código abierto, si te gustaría ayudar probando, dando opinión o contribuyendo,",
"desc_para5": "ponte en contacto con nosotros a través de GitHub", "desc_para5": "ponte en contacto con nosotros a través de GitHub",
"desc_para6": "para participar.", "desc_para6": "para participar.",
"footer_team": "El equipo de desarrollo de Elk",
"title": "¡Elk está en Vista Previa!" "title": "¡Elk está en Vista Previa!"
}, },
"language": { "language": {
@ -176,8 +184,19 @@
}, },
"list": { "list": {
"add_account": "Agregar cuenta a la lista", "add_account": "Agregar cuenta a la lista",
"cancel_edit": "Cancelar edición",
"clear_error": "Limpiar error",
"create": "Crear",
"delete": "Eliminar esta lista",
"delete_error": "Se produjo un error eliminando la lista",
"edit": "Ediar esta lista",
"edit_error": "Se produjo un error modificando la lista",
"error": "Se produjo un error creando la lista",
"error_prefix": "Error: ",
"list_title_placeholder": "Título de la lista",
"modify_account": "Modificar listas con cuenta", "modify_account": "Modificar listas con cuenta",
"remove_account": "Eliminar cuenta de la lista" "remove_account": "Eliminar cuenta de la lista",
"save": "Guardar"
}, },
"menu": { "menu": {
"block_account": "Bloquear a {0}", "block_account": "Bloquear a {0}",
@ -245,7 +264,7 @@
"reblogged_post": "retooteó tu publicación", "reblogged_post": "retooteó tu publicación",
"request_to_follow": "ha solicitado seguirte", "request_to_follow": "ha solicitado seguirte",
"signed_up": "registrado", "signed_up": "registrado",
"update_status": "ha actualizado su estado" "update_status": "ha actualizado su publicación"
}, },
"placeholder": { "placeholder": {
"content_warning": "Escribe tu advertencia aquí", "content_warning": "Escribe tu advertencia aquí",
@ -290,6 +309,7 @@
}, },
"settings": { "settings": {
"about": { "about": {
"built_at": "Fecha de compilación",
"label": "Acerca de", "label": "Acerca de",
"meet_the_team": "Conoce al equipo", "meet_the_team": "Conoce al equipo",
"sponsor_action": "Patrocinar", "sponsor_action": "Patrocinar",
@ -301,14 +321,14 @@
"version": "Versión" "version": "Versión"
}, },
"account_settings": { "account_settings": {
"description": "Edita los ajustes de tu cuenta en la interfaz de Mastodon", "description": "Edita los ajustes de tu cuenta en la interfaz de Mastodon.",
"label": "Ajustes de cuenta" "label": "Ajustes de cuenta"
}, },
"interface": { "interface": {
"color_mode": "Modos de color", "color_mode": "Modos de color",
"dark_mode": "Modo oscuro", "dark_mode": "Modo oscuro",
"default": " (por defecto)", "default": " (por defecto)",
"font_size": "Tamaño de Letra", "font_size": "Tamaño de letra",
"label": "Interfaz", "label": "Interfaz",
"light_mode": "Modo claro", "light_mode": "Modo claro",
"system_mode": "Sistema", "system_mode": "Sistema",
@ -316,7 +336,14 @@
}, },
"language": { "language": {
"display_language": "Idioma de pantalla", "display_language": "Idioma de pantalla",
"label": "Idioma" "label": "Idioma",
"translations": {
"add": "Agregar",
"choose_language": "Seleccionar idioma",
"heading": "Traducciones",
"hide_specific": "Ocultar una traducción en específico",
"remove": "Eliminar"
}
}, },
"notifications": { "notifications": {
"label": "Notificaciones", "label": "Notificaciones",
@ -332,7 +359,7 @@
"reblog": "Retooteo de tus publicaciones", "reblog": "Retooteo de tus publicaciones",
"title": "¿Qué notificaciones recibir?" "title": "¿Qué notificaciones recibir?"
}, },
"description": "Reciba notificaciones incluso cuando no estés utilizando Elk.", "description": "Recibe notificaciones incluso cuando no estés utilizando Elk.",
"instructions": "¡No olvides guardar los cambios utilizando el botón @:settings.notifications.push_notifications.save_settings{'!'}", "instructions": "¡No olvides guardar los cambios utilizando el botón @:settings.notifications.push_notifications.save_settings{'!'}",
"label": "Ajustes de notificaciones push", "label": "Ajustes de notificaciones push",
"policy": { "policy": {
@ -345,10 +372,13 @@
"save_settings": "Guardar cambios", "save_settings": "Guardar cambios",
"subscription_error": { "subscription_error": {
"clear_error": "Limpiar error", "clear_error": "Limpiar error",
"invalid_vapid_key": "La clave pública VAPID parece no ser válida.",
"permission_denied": "Permiso denegado: habilita las notificaciones en tu navegador.", "permission_denied": "Permiso denegado: habilita las notificaciones en tu navegador.",
"repo_link": "Repositorio de Elk en Github",
"request_error": "Se produjo un error al solicitar la suscripción, inténtalo de nuevo y si el error persiste, notifica la incidencia en el repositorio de Elk.", "request_error": "Se produjo un error al solicitar la suscripción, inténtalo de nuevo y si el error persiste, notifica la incidencia en el repositorio de Elk.",
"title": "No se pudo suscribir a las notificaciones push", "title": "No se pudo suscribir a las notificaciones push",
"too_many_registrations": "Debido a las limitaciones del navegador, Elk no puede habilitar las notificaciones push para múltiples cuentas en diferentes servidores. Deberá cancelar las subscripciones a notificaciones push en las otras cuentas e intentarlo de nuevo." "too_many_registrations": "Debido a las limitaciones del navegador, Elk no puede habilitar las notificaciones push para múltiples cuentas en diferentes servidores. Deberá cancelar las subscripciones a notificaciones push en las otras cuentas e intentarlo de nuevo.",
"vapid_not_supported": "Su navegador es compatible con las notificaciones web push, pero no parece implementar el protocolo VAPID."
}, },
"title": "Ajustes de notificaciones push", "title": "Ajustes de notificaciones push",
"undo_settings": "Deshacer cambios", "undo_settings": "Deshacer cambios",
@ -365,19 +395,22 @@
"re_auth": "Parece que tu servidor no soporta notificaciones push. Prueba a cerrar la sesión y volver a iniciarla, si este mensaje sigue apareciendo contacta con el administrador de tu servidor." "re_auth": "Parece que tu servidor no soporta notificaciones push. Prueba a cerrar la sesión y volver a iniciarla, si este mensaje sigue apareciendo contacta con el administrador de tu servidor."
} }
}, },
"show_btn": "Ir a ajustes de notificaciones" "show_btn": "Ir a ajustes de notificaciones",
"under_construction": "En construcción"
}, },
"notifications_settings": "Notificaciones", "notifications_settings": "Notificaciones",
"preferences": { "preferences": {
"enable_autoplay": "Habilitar auto-reproducción", "enable_autoplay": "Habilitar reproducción automática",
"enable_pinch_to_zoom": "Habilitar pellizcar para hacer zoom",
"github_cards": "Tarjetas GitHub", "github_cards": "Tarjetas GitHub",
"grayscale_mode": "Modo escala de grises", "grayscale_mode": "Modo escala de grises",
"hide_account_hover_card": "Ocultar tarjeta flotante de cuenta", "hide_account_hover_card": "Ocultar tarjeta flotante de cuenta",
"hide_boost_count": "Ocultar contador de retoots", "hide_boost_count": "Ocultar contador de retoots",
"hide_favorite_count": "Ocultar contador de favoritas", "hide_favorite_count": "Ocultar número de publicaciones favoritas",
"hide_follower_count": "Ocultar contador de seguidores", "hide_follower_count": "Ocultar número de seguidores",
"hide_reply_count": "Ocultar contador de respuestas", "hide_reply_count": "Ocultar número de respuestas",
"hide_translation": "Ocultar traducción", "hide_translation": "Ocultar traducción",
"hide_username_emojis": "Ocultar emojis en el nombre de usuario",
"label": "Preferencias", "label": "Preferencias",
"title": "Funcionalidades experimentales", "title": "Funcionalidades experimentales",
"user_picker": "Selector de usuarios", "user_picker": "Selector de usuarios",
@ -395,7 +428,7 @@
}, },
"featured_tags": { "featured_tags": {
"description": "Las personas pueden navegar por tus publicaciones públicas con estas etiquetas.", "description": "Las personas pueden navegar por tus publicaciones públicas con estas etiquetas.",
"label": "Etiquetas destacados" "label": "Etiquetas destacadas"
}, },
"label": "Perfil" "label": "Perfil"
}, },
@ -510,7 +543,7 @@
}, },
"tooltip": { "tooltip": {
"add_content_warning": "Añadir advertencia de contenido", "add_content_warning": "Añadir advertencia de contenido",
"add_emojis": "Añadir emojis", "add_emojis": "Agregar emojis",
"add_media": "Añadir imágenes, video o audio", "add_media": "Añadir imágenes, video o audio",
"add_publishable_content": "Publicar contenido", "add_publishable_content": "Publicar contenido",
"change_content_visibility": "Cambiar visibilidad de contenido", "change_content_visibility": "Cambiar visibilidad de contenido",
@ -519,12 +552,15 @@
"explore_links_intro": "Estas noticias están siendo comentadas ahora mismo por los usuarios de este y otros servidores de la red descentralizada.", "explore_links_intro": "Estas noticias están siendo comentadas ahora mismo por los usuarios de este y otros servidores de la red descentralizada.",
"explore_posts_intro": "Estos mensajes de este y otros servidores de la red descentralizada están siendo tendencia ahora mismo en este servidor.", "explore_posts_intro": "Estos mensajes de este y otros servidores de la red descentralizada están siendo tendencia ahora mismo en este servidor.",
"explore_tags_intro": "Estas etiquetas están siendo tendencia ahora mismo entre los usuarios de este y otros servidores de la red descentralizada.", "explore_tags_intro": "Estas etiquetas están siendo tendencia ahora mismo entre los usuarios de este y otros servidores de la red descentralizada.",
"open_editor_tools": "Herramientas de edición",
"publish_failed": "Cierra los mensajes fallidos en la parte superior del editor para volver a publicar", "publish_failed": "Cierra los mensajes fallidos en la parte superior del editor para volver a publicar",
"toggle_code_block": "Cambiar a bloque de código" "toggle_bold": "Cambiar a negrita",
"toggle_code_block": "Cambiar a bloque de código",
"toggle_italic": "Cambiar a cursiva"
}, },
"user": { "user": {
"add_existing": "Agregar una cuenta existente", "add_existing": "Agregar una cuenta existente",
"server_address_label": "Dirección de Servidor de Mastodon", "server_address_label": "Dirección de servidor de Mastodon",
"sign_in_desc": "Inicia sesión para seguir perfiles o etiquetas, marcar cómo favorita, compartir y responder a publicaciones, o interactuar con un servidor diferente con tu usuario.", "sign_in_desc": "Inicia sesión para seguir perfiles o etiquetas, marcar cómo favorita, compartir y responder a publicaciones, o interactuar con un servidor diferente con tu usuario.",
"sign_in_notice_title": "Viendo información pública de {0}", "sign_in_notice_title": "Viendo información pública de {0}",
"sign_out_account": "Cerrar sesión {0}", "sign_out_account": "Cerrar sesión {0}",

View file

@ -8,9 +8,9 @@
}, },
"account": { "account": {
"avatar_description": "Avatar de {0}", "avatar_description": "Avatar de {0}",
"blocked_by": "Vous êtes bloqué·e par cet·te utilisateur·ice.", "blocked_by": "Ce compte vous a bloqué",
"blocked_domains": "Domaines bloqués", "blocked_domains": "Domaines bloqués",
"blocked_users": "Utilisateur·ice·s bloqué·e·s", "blocked_users": "Comptes bloqués",
"blocking": "Bloqué·e", "blocking": "Bloqué·e",
"bot": "Automatisé", "bot": "Automatisé",
"favourites": "Aimés", "favourites": "Aimés",
@ -21,11 +21,11 @@
"followers_count": "{0} abonné·e|{0} abonné·e|{0} abonné·e·s", "followers_count": "{0} abonné·e|{0} abonné·e|{0} abonné·e·s",
"following": "Suivi·e", "following": "Suivi·e",
"following_count": "{0} abonnements", "following_count": "{0} abonnements",
"follows_you": "Vous suit", "follows_you": "@:account.follow_back",
"go_to_profile": "Aller à son profil", "go_to_profile": "Aller à son profil",
"joined": "a rejoint", "joined": "a rejoint",
"moved_title": "a indiqué que son nouveau compte est désormais :", "moved_title": "a indiqué que son nouveau compte est désormais :",
"muted_users": "Utilisateur·ice·s masqué·e·s", "muted_users": "Comptes masqués",
"muting": "Masqué·e", "muting": "Masqué·e",
"mutuals": "Abonné·e·s", "mutuals": "Abonné·e·s",
"notifications_on_post_disable": "Arrêtez de me notifier lorsque {username} publie", "notifications_on_post_disable": "Arrêtez de me notifier lorsque {username} publie",
@ -39,8 +39,8 @@
"unblock": "Débloquer", "unblock": "Débloquer",
"unfollow": "Ne plus suivre", "unfollow": "Ne plus suivre",
"unmute": "Réafficher", "unmute": "Réafficher",
"view_other_followers": "Les abonné·e·s d'autres instances peuvent ne pas être affiché·e·s.", "view_other_followers": "Les comptes abonnés d'autres instances peuvent ne pas être affichés.",
"view_other_following": "Les suivis d'autres instances peuvent ne pas être affichés." "view_other_following": "Les comptes suivis d'autres instances peuvent ne pas être affichés."
}, },
"action": { "action": {
"apply": "Appliquer", "apply": "Appliquer",
@ -118,10 +118,15 @@
"confirm": "Oui", "confirm": "Oui",
"title": "Êtes-vous sûr·e ?" "title": "Êtes-vous sûr·e ?"
}, },
"delete_list": {
"cancel": "Annuler",
"confirm": "Supprimer",
"title": "Voulez-vous vraiment supprimer la liste \"{0}\" ?"
},
"delete_posts": { "delete_posts": {
"cancel": "Annuler", "cancel": "Annuler",
"confirm": "Supprimer", "confirm": "Supprimer",
"title": "Certain·e de vouloir supprimer ce message ?" "title": "Voulez-vous vraiment supprimer ce message ?"
}, },
"mute_account": { "mute_account": {
"cancel": "Annuler", "cancel": "Annuler",
@ -171,11 +176,28 @@
"desc_para4": "Avant cela, si vous voulez aider à tester, donner des retours ou contribuer", "desc_para4": "Avant cela, si vous voulez aider à tester, donner des retours ou contribuer",
"desc_para5": "contactez nous sur GitHub", "desc_para5": "contactez nous sur GitHub",
"desc_para6": "et rejoignez l'aventure.", "desc_para6": "et rejoignez l'aventure.",
"footer_team": "L'équipe Elk",
"title": "Elk est en mode Aperçu !" "title": "Elk est en mode Aperçu !"
}, },
"language": { "language": {
"search": "Recherche" "search": "Recherche"
}, },
"list": {
"add_account": "Ajouter le compte à la liste",
"cancel_edit": "Annuler l'édition",
"clear_error": "Effacer l'erreur",
"create": "Créer",
"delete": "Supprimer cette liste",
"delete_error": "Il y a eu une erreur lors de la suppression de la liste",
"edit": "Editer cette liste",
"edit_error": "Il y a eu une erreur lors de la mise à jour de la liste",
"error": "Il y a eu une erreur lors de la création de la liste",
"error_prefix": "Erreur :",
"list_title_placeholder": "Nom de la liste",
"modify_account": "Modifier les listes de ce compte",
"remove_account": "Supprimer ce compte de listes",
"save": "Enregistrer les changements"
},
"menu": { "menu": {
"block_account": "Bloquer {0}", "block_account": "Bloquer {0}",
"block_domain": "Bloquer le domaine {0}", "block_domain": "Bloquer le domaine {0}",
@ -212,7 +234,7 @@
"nav": { "nav": {
"back": "Retourner à la page précédente", "back": "Retourner à la page précédente",
"blocked_domains": "Domaines bloqués", "blocked_domains": "Domaines bloqués",
"blocked_users": "Utilisateur·ice·s bloqué·e·s", "blocked_users": "Comptes bloqués",
"bookmarks": "Marque-pages", "bookmarks": "Marque-pages",
"built_at": "Dernière compilation {0}", "built_at": "Dernière compilation {0}",
"compose": "Composer", "compose": "Composer",
@ -221,9 +243,12 @@
"favourites": "Favoris", "favourites": "Favoris",
"federated": "Fédérés", "federated": "Fédérés",
"home": "Accueil", "home": "Accueil",
"list": "Liste",
"lists": "Listes",
"local": "Local", "local": "Local",
"muted_users": "Utilisateur·ice·s masqué·e·s", "muted_users": "Comptes masqués",
"notifications": "Notifications", "notifications": "Notifications",
"privacy": "Données privées",
"profile": "Profil", "profile": "Profil",
"search": "Rechercher", "search": "Rechercher",
"select_feature_flags": "Activer/Désactiver Feature Flags", "select_feature_flags": "Activer/Désactiver Feature Flags",
@ -287,11 +312,12 @@
}, },
"settings": { "settings": {
"about": { "about": {
"built_at": "Dernière compilation",
"label": "À propos", "label": "À propos",
"meet_the_team": "Rencontrez l'équipe", "meet_the_team": "Rencontrez l'équipe",
"sponsor_action": "Soutenez-nous", "sponsor_action": "Soutenez-nous",
"sponsor_action_desc": "Pour financer l'équipe développant Elk", "sponsor_action_desc": "Pour financer l'équipe développant Elk",
"sponsors": "Donateur·ice·s", "sponsors": "Soutiens financiers",
"sponsors_body_1": "Elk existe grâce aux généreux soutien de :", "sponsors_body_1": "Elk existe grâce aux généreux soutien de :",
"sponsors_body_2": "Et toutes les personnes et sociétés soutenant l'équipe Elk et ses membres.", "sponsors_body_2": "Et toutes les personnes et sociétés soutenant l'équipe Elk et ses membres.",
"sponsors_body_3": "Si vous appréciez l'application, envisagez de nous soutenir :", "sponsors_body_3": "Si vous appréciez l'application, envisagez de nous soutenir :",
@ -313,7 +339,14 @@
}, },
"language": { "language": {
"display_language": "Langue d'affichage", "display_language": "Langue d'affichage",
"label": "Langue" "label": "Langue",
"translations": {
"add": "Ajouter",
"choose_language": "Choisir une langue",
"heading": "Traductions",
"hide_specific": "Cacher certaines traductions",
"remove": "Retirer"
}
}, },
"notifications": { "notifications": {
"label": "Notifications", "label": "Notifications",
@ -323,7 +356,7 @@
"push_notifications": { "push_notifications": {
"alerts": { "alerts": {
"favourite": "Messages aimés", "favourite": "Messages aimés",
"follow": "Nouveaux abonné·e·s", "follow": "Nouveaux abonnés",
"mention": "Mentions", "mention": "Mentions",
"poll": "Sondages", "poll": "Sondages",
"reblog": "Messages partagés", "reblog": "Messages partagés",
@ -362,16 +395,21 @@
"re_auth": "Il semble que votre serveur ne supporte pas les notifications push. \nEssayez de vous déconnecter et de vous reconnecter, si ce message persiste, contactez l'administrateur de votre serveur." "re_auth": "Il semble que votre serveur ne supporte pas les notifications push. \nEssayez de vous déconnecter et de vous reconnecter, si ce message persiste, contactez l'administrateur de votre serveur."
} }
}, },
"show_btn": "Se rendre aux paramètres des notifications" "show_btn": "Se rendre aux paramètres des notifications",
"under_construction": "En construction"
}, },
"notifications_settings": "Notifications", "notifications_settings": "Notifications",
"preferences": { "preferences": {
"enable_autoplay": "Activer la lecture automatique", "enable_autoplay": "Activer la lecture automatique",
"github_cards": "GitHub Cards", "enable_pinch_to_zoom": "Activer le zoom par pincement",
"github_cards": "Cartes GitHub",
"grayscale_mode": "Mode niveaux de gris", "grayscale_mode": "Mode niveaux de gris",
"hide_account_hover_card": "Masquer la carte de survol du compte",
"hide_boost_count": "Cacher les compteurs de partages", "hide_boost_count": "Cacher les compteurs de partages",
"hide_favorite_count": "Cacher les compteurs de favoris", "hide_favorite_count": "Cacher les compteurs de favoris",
"hide_follower_count": "Cacher les compteurs d'abonné·e·s", "hide_follower_count": "Cacher les compteurs d'abonné·e·s",
"hide_reply_count": "Cacher les compteurs de réponses",
"hide_translation": "Cacher traduction",
"label": "Préférences", "label": "Préférences",
"title": "Fonctionnalités expérimentales", "title": "Fonctionnalités expérimentales",
"user_picker": "User Picker", "user_picker": "User Picker",
@ -380,7 +418,7 @@
"profile": { "profile": {
"appearance": { "appearance": {
"bio": "Bio", "bio": "Bio",
"description": "Éditer l'avatar, nom d'utilisateur·ice, profil, etc.", "description": "Éditer l'avatar, nom du compte, profil, etc.",
"display_name": "Nom d'affichage", "display_name": "Nom d'affichage",
"label": "Apparence", "label": "Apparence",
"profile_metadata": "Métadonnées de profil", "profile_metadata": "Métadonnées de profil",
@ -395,9 +433,9 @@
}, },
"select_a_settings": "Sélectionner un paramètre", "select_a_settings": "Sélectionner un paramètre",
"users": { "users": {
"export": "Exporter les tokens d'utilisateur·ice", "export": "Exporter les tokens de compte",
"import": "Importer des tokens d'utilisateur·ice", "import": "Importer des tokens de compte",
"label": "Utilisateur·ice·s connecté·e·s" "label": "Comptes connectés"
} }
}, },
"share-target": { "share-target": {
@ -421,10 +459,13 @@
"edited": "Edité {0}", "edited": "Edité {0}",
"favourited_by": "Aimé par", "favourited_by": "Aimé par",
"filter_hidden_phrase": "Filtré par", "filter_hidden_phrase": "Filtré par",
"filter_removed_phrase": "Supprimé par le filtre",
"filter_show_anyway": "Montrer coûte que coûte", "filter_show_anyway": "Montrer coûte que coûte",
"img_alt": { "img_alt": {
"ALT": "ALT",
"desc": "Description", "desc": "Description",
"dismiss": "Fermer" "dismiss": "Fermer",
"read": "Lire la description de {0}"
}, },
"poll": { "poll": {
"count": "{0} votes", "count": "{0} votes",
@ -445,8 +486,10 @@
"edited": "a édité {0}" "edited": "a édité {0}"
}, },
"tab": { "tab": {
"accounts": "Comptes",
"for_you": "Pour vous", "for_you": "Pour vous",
"hashtags": "Hashtags", "hashtags": "Hashtags",
"list": "Liste",
"media": "Média", "media": "Média",
"news": "Actualités", "news": "Actualités",
"notifications_all": "Tout", "notifications_all": "Tout",

View file

@ -116,6 +116,11 @@
"cancel": "いいえ", "cancel": "いいえ",
"confirm": "はい" "confirm": "はい"
}, },
"delete_list": {
"cancel": "キャンセル",
"confirm": "削除",
"title": "リスト \"{0}\" を本当に削除したいですか?"
},
"delete_posts": { "delete_posts": {
"cancel": "キャンセル", "cancel": "キャンセル",
"confirm": "削除", "confirm": "削除",
@ -169,6 +174,7 @@
"desc_para4": "Elkはオープンソースです。テスト、フィードバックの提供、コントリビューションで開発を助けたい場合は", "desc_para4": "Elkはオープンソースです。テスト、フィードバックの提供、コントリビューションで開発を助けたい場合は",
"desc_para5": "GitHubで連絡をとり", "desc_para5": "GitHubで連絡をとり",
"desc_para6": "参加してください。", "desc_para6": "参加してください。",
"footer_team": "Elk チーム",
"title": "Elkはプレビュー版です" "title": "Elkはプレビュー版です"
}, },
"language": { "language": {
@ -176,8 +182,14 @@
}, },
"list": { "list": {
"add_account": "アカウントをリストに追加", "add_account": "アカウントをリストに追加",
"cancel_edit": "編集をキャンセル",
"create": "作成",
"delete": "リストを削除",
"edit": "リストを編集",
"list_title_placeholder": "リストのタイトル",
"modify_account": "このアカウントでリストを編集", "modify_account": "このアカウントでリストを編集",
"remove_account": "アカウントをリストから削除" "remove_account": "アカウントをリストから削除",
"save": "変更を保存"
}, },
"menu": { "menu": {
"block_account": "{0}さんをブロック", "block_account": "{0}さんをブロック",
@ -290,6 +302,7 @@
}, },
"settings": { "settings": {
"about": { "about": {
"built_at": "ビルド",
"label": "Elkについて", "label": "Elkについて",
"meet_the_team": "チーム紹介", "meet_the_team": "チーム紹介",
"sponsor_action": "サポートしてください", "sponsor_action": "サポートしてください",
@ -316,7 +329,14 @@
}, },
"language": { "language": {
"display_language": "表示言語", "display_language": "表示言語",
"label": "言語" "label": "言語",
"translations": {
"add": "追加",
"choose_language": "言語を選択",
"heading": "翻訳",
"hide_specific": "特定の翻訳を隠す",
"remove": "削除"
}
}, },
"notifications": { "notifications": {
"label": "通知", "label": "通知",
@ -365,11 +385,13 @@
"re_auth": "あなたのサーバーはプッシュ通知をサポートしていないようです。もしサインアウトとサインインをやり直してもこのメッセージがまだ表示される場合は、サーバー管理者に連絡してください。" "re_auth": "あなたのサーバーはプッシュ通知をサポートしていないようです。もしサインアウトとサインインをやり直してもこのメッセージがまだ表示される場合は、サーバー管理者に連絡してください。"
} }
}, },
"show_btn": "通知設定に移動" "show_btn": "通知設定に移動",
"under_construction": "工事中"
}, },
"notifications_settings": "通知", "notifications_settings": "通知",
"preferences": { "preferences": {
"enable_autoplay": "自動再生を有効化", "enable_autoplay": "自動再生を有効化",
"enable_pinch_to_zoom": "ピンチによるズームを有効にする",
"github_cards": "GitHubカード", "github_cards": "GitHubカード",
"grayscale_mode": "グレースケールモード", "grayscale_mode": "グレースケールモード",
"hide_account_hover_card": "アカウントのホバーカードを隠す", "hide_account_hover_card": "アカウントのホバーカードを隠す",
@ -430,8 +452,10 @@
"filter_removed_phrase": "フィルターにより削除", "filter_removed_phrase": "フィルターにより削除",
"filter_show_anyway": "とにかく表示", "filter_show_anyway": "とにかく表示",
"img_alt": { "img_alt": {
"ALT": "ALT",
"desc": "説明文", "desc": "説明文",
"dismiss": "閉じる" "dismiss": "閉じる",
"read": "{0} の説明文を読む"
}, },
"poll": { "poll": {
"count": "{0} 票", "count": "{0} 票",

View file

@ -252,7 +252,7 @@
"install": "Zainstaluj", "install": "Zainstaluj",
"install_title": "Instalacja Elk", "install_title": "Instalacja Elk",
"title": "Dostępna nowa aktualizacja Elk!", "title": "Dostępna nowa aktualizacja Elk!",
"update": "Aktualizacja", "update": "Aktualizuj",
"update_available_short": "Zaktualizuj Elk", "update_available_short": "Zaktualizuj Elk",
"webmanifest": { "webmanifest": {
"canary": { "canary": {
@ -309,7 +309,14 @@
}, },
"language": { "language": {
"display_language": "Język aplikacji", "display_language": "Język aplikacji",
"label": "Język" "label": "Język",
"translations": {
"add": "Dodaj",
"choose_language": "Wybierz język",
"heading": "Tłumaczenia",
"hide_specific": "Ukryj określone tłumaczenia",
"remove": "Usuń"
}
}, },
"notifications": { "notifications": {
"label": "Powiadomienia", "label": "Powiadomienia",
@ -380,7 +387,7 @@
"appearance": { "appearance": {
"bio": "Biogram", "bio": "Biogram",
"description": "Edytuj awatar, nazwę użytkownika, profil itp.", "description": "Edytuj awatar, nazwę użytkownika, profil itp.",
"display_name": "Wyświetlana nazwa", "display_name": "Widoczna nazwa",
"label": "Wygląd", "label": "Wygląd",
"profile_metadata": "Metadane profilu", "profile_metadata": "Metadane profilu",
"profile_metadata_desc": "Możesz mieć maksymalnie {0} elementy wyświetlane jako tabela w swoim profilu", "profile_metadata_desc": "Możesz mieć maksymalnie {0} elementy wyświetlane jako tabela w swoim profilu",
@ -424,7 +431,7 @@
"filter_show_anyway": "Pokaż mimo wszystko", "filter_show_anyway": "Pokaż mimo wszystko",
"img_alt": { "img_alt": {
"desc": "Opis", "desc": "Opis",
"dismiss": "Odrzuć" "dismiss": "Zamknij"
}, },
"poll": { "poll": {
"count": "{0} głosów|{0} głos|{0} głosy|{0} głosów", "count": "{0} głosów|{0} głos|{0} głosy|{0} głosów",

View file

@ -116,6 +116,11 @@
"cancel": "Não", "cancel": "Não",
"confirm": "Sim" "confirm": "Sim"
}, },
"delete_list": {
"cancel": "Cancelar",
"confirm": "Eliminar",
"title": "Tem a certeza que pretende elimnar a lista \"{0}\"?"
},
"delete_posts": { "delete_posts": {
"cancel": "Cancelar", "cancel": "Cancelar",
"confirm": "Eliminar", "confirm": "Eliminar",
@ -169,6 +174,7 @@
"desc_para4": "Elk é um software de código aberto. Se quiser ajudar a testar a aplicação, dando o seu feedback ou contributo,", "desc_para4": "Elk é um software de código aberto. Se quiser ajudar a testar a aplicação, dando o seu feedback ou contributo,",
"desc_para5": "pode encontrar-nos no GitHub", "desc_para5": "pode encontrar-nos no GitHub",
"desc_para6": "e participar.", "desc_para6": "e participar.",
"footer_team": "A Equipa do Elk",
"title": "Elk está em Antevisão!" "title": "Elk está em Antevisão!"
}, },
"language": { "language": {
@ -176,8 +182,19 @@
}, },
"list": { "list": {
"add_account": "Adicionar conta à lista", "add_account": "Adicionar conta à lista",
"cancel_edit": "Cancelar edição",
"clear_error": "Limpar erro",
"create": "Criar",
"delete": "Eliminar esta lista",
"delete_error": "Ocorreu um erro ao eliminar a lista",
"edit": "Editar esta lista",
"edit_error": "Ocorreu um erro ao atualizar a lista",
"error": "Ocorreu um erro ao criar a lista",
"error_prefix": "Erro: ",
"list_title_placeholder": "Título da lista",
"modify_account": "Modificar listas com a conta", "modify_account": "Modificar listas com a conta",
"remove_account": "Remover conta da lista" "remove_account": "Remover conta da lista",
"save": "Salvar alterações"
}, },
"menu": { "menu": {
"block_account": "Bloquear {0}", "block_account": "Bloquear {0}",
@ -290,6 +307,7 @@
}, },
"settings": { "settings": {
"about": { "about": {
"built_at": "Produzido",
"label": "Sobre", "label": "Sobre",
"meet_the_team": "Conheça a equipa", "meet_the_team": "Conheça a equipa",
"sponsor_action": "Patrocine-nos", "sponsor_action": "Patrocine-nos",
@ -316,7 +334,14 @@
}, },
"language": { "language": {
"display_language": "Idioma de Apresentação", "display_language": "Idioma de Apresentação",
"label": "Idioma" "label": "Idioma",
"translations": {
"add": "Adicionar",
"choose_language": "Selecionar idioma",
"heading": "Traduções",
"hide_specific": "Esconder para idiomas específicos",
"remove": "Remover"
}
}, },
"notifications": { "notifications": {
"label": "Notificações", "label": "Notificações",
@ -345,10 +370,13 @@
"save_settings": "Salvar configurações", "save_settings": "Salvar configurações",
"subscription_error": { "subscription_error": {
"clear_error": "Limpar erro", "clear_error": "Limpar erro",
"invalid_vapid_key": "A chave pública VAPID parece ser inválida.",
"permission_denied": "Permissão negada: habilite as notificações no seu browser.", "permission_denied": "Permissão negada: habilite as notificações no seu browser.",
"repo_link": "Repositório do Elk no GitHub",
"request_error": "Um erro ocorreu durante o pedido de subscrição, tente novamente e se o erro persistir, por favor reporte o problema no repositório do Elk.", "request_error": "Um erro ocorreu durante o pedido de subscrição, tente novamente e se o erro persistir, por favor reporte o problema no repositório do Elk.",
"title": "Não é possível subscrever as notificações push", "title": "Não é possível subscrever as notificações push",
"too_many_registrations": "Devido a limitações do browser, o Elk não consegue utilizar o serviço de notificações push para múltiplas contas em diferentes servidores. Deve cancelar a subscrição de notificações push nas outras contas e tentar novamente." "too_many_registrations": "Devido a limitações do browser, o Elk não consegue utilizar o serviço de notificações push para múltiplas contas em diferentes servidores. Deve cancelar a subscrição de notificações push nas outras contas e tentar novamente.",
"vapid_not_supported": "O seu browser suporta Notificações Web Push, mas não parece ter implementado o protocolo VAPID."
}, },
"title": "Configuração de notificações push", "title": "Configuração de notificações push",
"undo_settings": "Reverter alterações", "undo_settings": "Reverter alterações",
@ -365,23 +393,28 @@
"re_auth": "Parece que o seu servidor não suporta notificações push. Tenta desconectar e voltar a entrar, se esta mensagem permanecer contacte o administrador do seu servidor." "re_auth": "Parece que o seu servidor não suporta notificações push. Tenta desconectar e voltar a entrar, se esta mensagem permanecer contacte o administrador do seu servidor."
} }
}, },
"show_btn": "Ir para a configuração de notificações" "show_btn": "Ir para a configuração de notificações",
"under_construction": "Em construção"
}, },
"notifications_settings": "Notificações", "notifications_settings": "Notificações",
"preferences": { "preferences": {
"enable_autoplay": "Habilitar Reprodução Automática", "enable_autoplay": "Habilitar Reprodução Automática",
"enable_pinch_to_zoom": "Habilitar afastar/aproximar dedos para fazer zoom",
"github_cards": "Cartões do GitHub", "github_cards": "Cartões do GitHub",
"grayscale_mode": "Modo tons de cinza", "grayscale_mode": "Modo tons de cinza",
"hide_account_hover_card": "Esconder cartão flutuante de conta", "hide_account_hover_card": "Esconder cartão flutuante de conta",
"hide_alt_indi_on_posts": "Esconder indicador alt nas publicações",
"hide_boost_count": "Esconder contagem de partilhas", "hide_boost_count": "Esconder contagem de partilhas",
"hide_favorite_count": "Esconder contagem de favoritos", "hide_favorite_count": "Esconder contagem de favoritos",
"hide_follower_count": "Esconder contagem de seguidores", "hide_follower_count": "Esconder contagem de seguidores",
"hide_reply_count": "Esconder contagem de respostas", "hide_reply_count": "Esconder contagem de respostas",
"hide_translation": "Esconder botão de tradução", "hide_translation": "Esconder botão de tradução",
"hide_username_emojis": "Esconder emojis no nome de utilizador",
"label": "Preferências", "label": "Preferências",
"title": "Funcionalidades Experimentais", "title": "Funcionalidades Experimentais",
"user_picker": "Selecionador de Utilizador", "user_picker": "Selecionador de Utilizador",
"virtual_scroll": "Deslocamento Virtual" "virtual_scroll": "Deslocamento Virtual",
"wellbeing": "Bem-estar"
}, },
"profile": { "profile": {
"appearance": { "appearance": {
@ -430,8 +463,10 @@
"filter_removed_phrase": "Removida pelo filtro", "filter_removed_phrase": "Removida pelo filtro",
"filter_show_anyway": "Mostrar mesmo assim", "filter_show_anyway": "Mostrar mesmo assim",
"img_alt": { "img_alt": {
"ALT": "ALT",
"desc": "Descrição", "desc": "Descrição",
"dismiss": "Dispensar" "dismiss": "Dispensar",
"read": "Ler descrição de {0}"
}, },
"poll": { "poll": {
"count": "{0} votos|{0} voto|{0} votos", "count": "{0} votos|{0} voto|{0} votos",

View file

@ -222,7 +222,7 @@
"settings": "Настройки", "settings": "Настройки",
"show_intro": "Показать интро", "show_intro": "Показать интро",
"toggle_theme": "Переключить тему", "toggle_theme": "Переключить тему",
"zen_mode": "Рижим дзен" "zen_mode": "Режим дзен"
}, },
"notification": { "notification": {
"favourited_post": "добавили ваш пост в избранное", "favourited_post": "добавили ваш пост в избранное",
@ -401,7 +401,7 @@
"state": { "state": {
"attachments_exceed_server_limit": "Количество вложенных файлов превысило лимит на одно сообщение.", "attachments_exceed_server_limit": "Количество вложенных файлов превысило лимит на одно сообщение.",
"attachments_limit_error": "Превышен лимит вложенных файлов на одно сообщение", "attachments_limit_error": "Превышен лимит вложенных файлов на одно сообщение",
"edited": "(Отредактированно)", "edited": "(Отредактировано)",
"editing": "Редактирование", "editing": "Редактирование",
"loading": "Погрузка...", "loading": "Погрузка...",
"publish_failed": "Ошибка публикации", "publish_failed": "Ошибка публикации",
@ -435,7 +435,7 @@
"try_original_site": "Посмотреть на оригинальном сайте" "try_original_site": "Посмотреть на оригинальном сайте"
}, },
"status_history": { "status_history": {
"created": "созданно {0}", "created": "создано {0}",
"edited": "отредактировано {0}" "edited": "отредактировано {0}"
}, },
"tab": { "tab": {

View file

@ -169,11 +169,17 @@
"desc_para4": "鹿鸣是开源的,如果你愿意帮助测试、提供反馈或作出贡献,", "desc_para4": "鹿鸣是开源的,如果你愿意帮助测试、提供反馈或作出贡献,",
"desc_para5": "在 GitHub 上联系我们", "desc_para5": "在 GitHub 上联系我们",
"desc_para6": "来参与其中。", "desc_para6": "来参与其中。",
"footer_team": "鹿鸣开发团队",
"title": "预览鹿鸣!" "title": "预览鹿鸣!"
}, },
"language": { "language": {
"search": "搜索" "search": "搜索"
}, },
"list": {
"add_account": "向列表中添加用户",
"modify_account": "修改列表中的用户",
"remove_account": "移除列表中的用户"
},
"menu": { "menu": {
"block_account": "拉黑 {0}", "block_account": "拉黑 {0}",
"block_domain": "拉黑域名 {0}", "block_domain": "拉黑域名 {0}",
@ -216,9 +222,12 @@
"favourites": "喜欢", "favourites": "喜欢",
"federated": "跨站", "federated": "跨站",
"home": "主页", "home": "主页",
"list": "列表",
"lists": "列表",
"local": "本地", "local": "本地",
"muted_users": "已屏蔽的用户", "muted_users": "已屏蔽的用户",
"notifications": "通知", "notifications": "通知",
"privacy": "隐私协议",
"profile": "个人资料", "profile": "个人资料",
"search": "搜索", "search": "搜索",
"select_feature_flags": "功能开关", "select_feature_flags": "功能开关",
@ -248,6 +257,8 @@
}, },
"pwa": { "pwa": {
"dismiss": "忽略", "dismiss": "忽略",
"install": "安装",
"install_title": "安装鹿鸣",
"title": "鹿鸣存在新的更新", "title": "鹿鸣存在新的更新",
"update": "更新", "update": "更新",
"update_available_short": "更新鹿鸣", "update_available_short": "更新鹿鸣",
@ -280,7 +291,16 @@
}, },
"settings": { "settings": {
"about": { "about": {
"label": "关于" "built_at": "构建于",
"label": "关于",
"meet_the_team": "认识开发团队",
"sponsor_action": "赞助我们",
"sponsor_action_desc": "支持团队开发鹿鸣",
"sponsors": "赞助者",
"sponsors_body_1": "鹿鸣能够出现要感谢以下赞助者的慷慨赞助和帮助:",
"sponsors_body_2": "以及赞助鹿鸣开发团队和成员的所有公司和个人。",
"sponsors_body_3": "如果你喜欢这个应用程序,请考虑赞助我们:",
"version": "版本"
}, },
"account_settings": { "account_settings": {
"description": "在 Mastodon UI 中编辑你的账号设置", "description": "在 Mastodon UI 中编辑你的账号设置",
@ -293,19 +313,19 @@
"font_size": "字号", "font_size": "字号",
"label": "外观", "label": "外观",
"light_mode": "浅色", "light_mode": "浅色",
"size_label": {
"lg": "大",
"md": "中",
"sm": "小",
"xl": "特大",
"xs": "特小"
},
"system_mode": "跟随系统", "system_mode": "跟随系统",
"theme_color": "主题颜色" "theme_color": "主题颜色"
}, },
"language": { "language": {
"display_language": "首选语言", "display_language": "首选语言",
"label": "语言" "label": "语言",
"translations": {
"add": "添加",
"choose_language": "选择语言",
"heading": "翻译",
"hide_specific": "隐藏指定的翻译",
"remove": "删除"
}
}, },
"notifications": { "notifications": {
"label": "通知", "label": "通知",
@ -354,16 +374,20 @@
"re_auth": "您的服务器似乎不支持推送通知。尝试退出用户并重新登录。如果此消息仍然出现,请联系您服务器的管理员。" "re_auth": "您的服务器似乎不支持推送通知。尝试退出用户并重新登录。如果此消息仍然出现,请联系您服务器的管理员。"
} }
}, },
"show_btn": "前往通知设置" "show_btn": "前往通知设置",
"under_construction": "建设中"
}, },
"notifications_settings": "通知", "notifications_settings": "通知",
"preferences": { "preferences": {
"enable_autoplay": "开启自动播放", "enable_autoplay": "开启自动播放",
"enable_pinch_to_zoom": "启用双指缩放功能",
"github_cards": "GitHub 卡片", "github_cards": "GitHub 卡片",
"grayscale_mode": "灰色模式", "grayscale_mode": "灰色模式",
"hide_account_hover_card": "隐藏用户悬浮卡",
"hide_boost_count": "隐藏转发数", "hide_boost_count": "隐藏转发数",
"hide_favorite_count": "隐藏收藏数", "hide_favorite_count": "隐藏收藏数",
"hide_follower_count": "隐藏关注者数", "hide_follower_count": "隐藏关注者数",
"hide_reply_count": "隐藏回复数",
"hide_translation": "隐藏翻译", "hide_translation": "隐藏翻译",
"label": "首选项", "label": "首选项",
"title": "实验功能", "title": "实验功能",
@ -393,6 +417,11 @@
"label": "当前用户" "label": "当前用户"
} }
}, },
"share-target": {
"description": "只需要在你的设备或电脑上安装并登录鹿鸣,通过简单的配置,你就可以从其他应用中分享内容至鹿鸣。",
"hint": "为了分享内容至鹿鸣,你必须安装并登录鹿鸣。",
"title": "分享至鹿鸣"
},
"state": { "state": {
"attachments_exceed_server_limit": "附件的数量超出了最大限制", "attachments_exceed_server_limit": "附件的数量超出了最大限制",
"attachments_limit_error": "超出每篇帖文的最大限制", "attachments_limit_error": "超出每篇帖文的最大限制",
@ -409,6 +438,7 @@
"edited": "在 {0} 编辑了", "edited": "在 {0} 编辑了",
"favourited_by": "被喜欢", "favourited_by": "被喜欢",
"filter_hidden_phrase": "筛选依据", "filter_hidden_phrase": "筛选依据",
"filter_removed_phrase": "从筛选中移除",
"filter_show_anyway": "仍然展示", "filter_show_anyway": "仍然展示",
"img_alt": { "img_alt": {
"desc": "描述", "desc": "描述",
@ -433,8 +463,10 @@
"edited": "在 {0} 编辑了" "edited": "在 {0} 编辑了"
}, },
"tab": { "tab": {
"accounts": "用户",
"for_you": "推荐关注", "for_you": "推荐关注",
"hashtags": "话题标签", "hashtags": "话题标签",
"list": "列表",
"media": "媒体", "media": "媒体",
"news": "最新消息", "news": "最新消息",
"notifications_all": "全部", "notifications_all": "全部",

View file

@ -5,16 +5,18 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
if (!('server' in to.params)) if (!('server' in to.params))
return return
const server = to.params.server as string || currentServer.value
const user = currentUser.value const user = currentUser.value
const masto = useMasto() const masto = useMasto()
if (!user) { if (!user) {
if (from.params.server !== to.params.server) const fromServer = from.params.server || currentServer.value
loginTo(masto, { server: to.params.server as string }) if (fromServer !== server)
loginTo(masto, { server })
return return
} }
// No need to additionally resolve an id if we're already logged in // No need to additionally resolve an id if we're already logged in
if (user.server === to.params.server) if (user.server === server)
return return
// Tags don't need to be redirected to a local id // Tags don't need to be redirected to a local id
@ -22,7 +24,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
return return
// Handle redirecting to new permalink structure for users with old links // Handle redirecting to new permalink structure for users with old links
if (!to.params.server) { if (!useAppConfig().singleInstanceServer && !to.params.server) {
return { return {
...to, ...to,
params: { params: {

View file

@ -0,0 +1,10 @@
export default defineNuxtRouteMiddleware(async (to) => {
if (process.server || !useAppConfig().singleInstanceServer)
return
if (to.params.server) {
const newTo = { ...to }
delete newTo.params.server
return newTo
}
})

View file

@ -1,6 +1,7 @@
export default defineNuxtRouteMiddleware((to) => { export default defineNuxtRouteMiddleware((to) => {
if (process.server) if (process.server)
return return
if (to.path === '/signin/callback') if (to.path === '/signin/callback')
return return

View file

@ -19,8 +19,12 @@ export default defineNuxtModule({
env, env,
} }
nuxt.options.runtimeConfig.public.env = env nuxt.options.appConfig = nuxt.options.appConfig || {}
nuxt.options.runtimeConfig.public.buildInfo = buildInfo nuxt.options.appConfig.env = env
nuxt.options.appConfig.buildInfo = buildInfo
nuxt.options.nitro.virtual = nuxt.options.nitro.virtual || {}
nuxt.options.nitro.virtual['#build-info'] = `export const env = ${JSON.stringify(env)}`
nuxt.options.nitro.publicAssets = nuxt.options.nitro.publicAssets || [] nuxt.options.nitro.publicAssets = nuxt.options.nitro.publicAssets || []
if (env === 'dev') if (env === 'dev')

View file

@ -13,6 +13,7 @@ interface ExtendedManifestOptions extends ManifestOptions {
method: string method: string
enctype: string enctype: string
params: { params: {
title: string
text: string text: string
url: string url: string
files: [{ files: [{
@ -104,8 +105,9 @@ export const createI18n = async (): Promise<LocalizedWebManifest> => {
method: 'POST', method: 'POST',
enctype: 'multipart/form-data', enctype: 'multipart/form-data',
params: { params: {
title: 'title',
text: 'text', text: 'text',
url: 'text', url: 'url',
files: [ files: [
{ {
name: 'files', name: 'files',
@ -150,8 +152,9 @@ export const createI18n = async (): Promise<LocalizedWebManifest> => {
method: 'POST', method: 'POST',
enctype: 'multipart/form-data', enctype: 'multipart/form-data',
params: { params: {
title: 'title',
text: 'text', text: 'text',
url: 'text', url: 'url',
files: [ files: [
{ {
name: 'files', name: 'files',

View file

@ -23,6 +23,9 @@ export default defineNuxtModule<VitePWANuxtOptions>({
} }
let webmanifests: LocalizedWebManifest | undefined let webmanifests: LocalizedWebManifest | undefined
nuxt.options.appConfig = nuxt.options.appConfig || {}
nuxt.options.appConfig.pwaEnabled = !options.disable
// TODO: combine with configurePWAOptions? // TODO: combine with configurePWAOptions?
nuxt.hook('nitro:init', (nitro) => { nuxt.hook('nitro:init', (nitro) => {
options.outDir = nitro.options.output.publicDir options.outDir = nitro.options.output.publicDir

View file

@ -22,7 +22,9 @@ export default defineNuxtModule({
...nuxt.options.alias, ...nuxt.options.alias,
'unstorage/drivers/fs': 'unenv/runtime/mock/proxy', 'unstorage/drivers/fs': 'unenv/runtime/mock/proxy',
'unstorage/drivers/cloudflare-kv-http': 'unenv/runtime/mock/proxy', 'unstorage/drivers/cloudflare-kv-http': 'unenv/runtime/mock/proxy',
'#storage-config': resolve('./runtime/storage-config'),
'node:events': 'unenv/runtime/node/events/index', 'node:events': 'unenv/runtime/node/events/index',
'#build-info': resolve('./runtime/build-info'),
} }
nuxt.hook('vite:extend', ({ config }) => { nuxt.hook('vite:extend', ({ config }) => {

View file

@ -0,0 +1 @@
export const env = useAppConfig().env

View file

@ -55,7 +55,6 @@ export default defineNuxtPlugin(async () => {
const localCall = createCall(toNodeListener(h3App) as any) const localCall = createCall(toNodeListener(h3App) as any)
const localFetch = createLocalFetch(localCall, globalThis.fetch) const localFetch = createLocalFetch(localCall, globalThis.fetch)
// @ts-expect-error slight differences in api
globalThis.$fetch = createFetch({ globalThis.$fetch = createFetch({
// @ts-expect-error slight differences in api // @ts-expect-error slight differences in api
fetch: localFetch, fetch: localFetch,

View file

@ -0,0 +1,2 @@
export const driver = undefined
export const fsBase = ''

View file

@ -1,10 +1,9 @@
import { createResolver } from '@nuxt/kit' import { createResolver, useNuxt } from '@nuxt/kit'
import Inspect from 'vite-plugin-inspect' import Inspect from 'vite-plugin-inspect'
import { isCI, isDevelopment, isWindows } from 'std-env' import { isCI, isDevelopment, isWindows } from 'std-env'
import { isPreview } from './config/env' import { isPreview } from './config/env'
import { i18n } from './config/i18n' import { i18n } from './config/i18n'
import { pwa } from './config/pwa' import { pwa } from './config/pwa'
import type { BuildInfo } from './types'
const { resolve } = createResolver(import.meta.url) const { resolve } = createResolver(import.meta.url)
@ -25,6 +24,7 @@ export default defineNuxtConfig({
'@vue-macros/nuxt', '@vue-macros/nuxt',
'@nuxtjs/i18n', '@nuxtjs/i18n',
'@nuxtjs/color-mode', '@nuxtjs/color-mode',
'nuxt-vitest',
...(isDevelopment || isWindows) ? [] : ['nuxt-security'], ...(isDevelopment || isWindows) ? [] : ['nuxt-security'],
'~/modules/purge-comments', '~/modules/purge-comments',
'~/modules/setup-components', '~/modules/setup-components',
@ -66,6 +66,7 @@ export default defineNuxtConfig({
'./composables/settings', './composables/settings',
'./composables/tiptap/index.ts', './composables/tiptap/index.ts',
], ],
injectAtEnd: true,
}, },
vite: { vite: {
define: { define: {
@ -75,6 +76,15 @@ export default defineNuxtConfig({
}, },
build: { build: {
target: 'esnext', target: 'esnext',
rollupOptions: {
output: {
manualChunks: (id) => {
// TODO: find and resolve issue in nuxt/vite/pwa
if (id.includes('.svg') || id.includes('entry'))
return 'entry'
},
},
},
}, },
plugins: [ plugins: [
Inspect(), Inspect(),
@ -85,6 +95,12 @@ export default defineNuxtConfig({
'postcss-nested': {}, 'postcss-nested': {},
}, },
}, },
appConfig: {
singleInstanceServer: process.env.SINGLE_INSTANCE_SERVER === 'true',
storage: {
driver: process.env.NUXT_STORAGE_DRIVER ?? (isCI ? 'cloudflare' : 'fs'),
},
},
runtimeConfig: { runtimeConfig: {
adminKey: '', adminKey: '',
cloudflare: { cloudflare: {
@ -94,17 +110,14 @@ export default defineNuxtConfig({
}, },
public: { public: {
privacyPolicyUrl: '', privacyPolicyUrl: '',
env: '', // set in build-env module // We use LibreTranslate (https://github.com/LibreTranslate/LibreTranslate) as
buildInfo: {} as BuildInfo, // set in build-env module // our default translation server #76
pwaEnabled: !isDevelopment || process.env.VITE_DEV_PWA === 'true',
// We use LibreTranslate(https://github.com/LibreTranslate/LibreTranslate) as our default translation server #76
translateApi: '', translateApi: '',
// Use the instance where Elk has its Mastodon account as the default // Use the instance where Elk has its Mastodon account as the default
defaultServer: 'm.webtoo.ls', defaultServer: 'm.webtoo.ls',
}, },
storage: { storage: {
driver: isCI ? 'cloudflare' : 'fs', fsBase: 'node_modules/.cache/app',
fsBase: 'node_modules/.cache/servers',
}, },
}, },
routeRules: { routeRules: {
@ -115,6 +128,9 @@ export default defineNuxtConfig({
}, },
}, },
}, },
build: {
transpile: ['masto'],
},
nitro: { nitro: {
esbuild: { esbuild: {
options: { options: {
@ -127,11 +143,17 @@ export default defineNuxtConfig({
ignore: ['/settings'], ignore: ['/settings'],
}, },
}, },
hooks: {
'nitro:config': function (config) {
const nuxt = useNuxt()
config.virtual = config.virtual || {}
config.virtual['#storage-config'] = `export const driver = ${JSON.stringify(nuxt.options.appConfig.storage.driver)}`
},
},
app: { app: {
keepalive: true, keepalive: true,
head: { head: {
// Prevent arbitrary zooming on mobile devices viewport: 'width=device-width,initial-scale=1,viewport-fit=cover',
viewport: 'width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,viewport-fit=cover',
bodyAttrs: { bodyAttrs: {
class: 'overflow-x-hidden', class: 'overflow-x-hidden',
}, },
@ -193,3 +215,9 @@ declare global {
} }
} }
} }
declare module 'nuxt/dist/app' {
interface RuntimeNuxtHooks {
'elk-logo:click': () => void
}
}

View file

@ -1,6 +1,6 @@
{ {
"type": "module", "type": "module",
"version": "0.6.2", "version": "0.7.2",
"private": true, "private": true,
"packageManager": "pnpm@7.9.0", "packageManager": "pnpm@7.9.0",
"license": "MIT", "license": "MIT",
@ -49,6 +49,7 @@
"focus-trap": "^7.2.0", "focus-trap": "^7.2.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"github-reserved-names": "^2.0.4",
"idb-keyval": "^6.2.0", "idb-keyval": "^6.2.0",
"iso-639-1": "^2.1.15", "iso-639-1": "^2.1.15",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
@ -56,7 +57,7 @@
"masto": "^5.6.1", "masto": "^5.6.1",
"pinia": "^2.0.29", "pinia": "^2.0.29",
"shiki": "^0.12.1", "shiki": "^0.12.1",
"shiki-es": "^0.1.2", "shiki-es": "^0.2.0",
"slimeform": "^0.9.0", "slimeform": "^0.9.0",
"string-length": "^5.0.1", "string-length": "^5.0.1",
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log", "tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log",
@ -75,6 +76,7 @@
"@iconify-json/carbon": "^1.1.14", "@iconify-json/carbon": "^1.1.14",
"@iconify-json/logos": "^1.1.22", "@iconify-json/logos": "^1.1.22",
"@iconify-json/material-symbols": "^1.1.26", "@iconify-json/material-symbols": "^1.1.26",
"@iconify-json/mdi": "^1.1.44",
"@iconify-json/ph": "^1.1.3", "@iconify-json/ph": "^1.1.3",
"@iconify-json/ri": "^1.1.4", "@iconify-json/ri": "^1.1.4",
"@iconify-json/twemoji": "^1.1.10", "@iconify-json/twemoji": "^1.1.10",
@ -88,8 +90,8 @@
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/prettier": "^2.7.2", "@types/prettier": "^2.7.2",
"@types/wicg-file-system-access": "^2020.9.5", "@types/wicg-file-system-access": "^2020.9.5",
"@unocss/nuxt": "^0.48.5", "@unocss/nuxt": "^0.49.0",
"@vue-macros/nuxt": "^0.3.3", "@vue-macros/nuxt": "^0.3.7",
"@vueuse/math": "^9.11.1", "@vueuse/math": "^9.11.1",
"@vueuse/nuxt": "^9.11.1", "@vueuse/nuxt": "^9.11.1",
"bumpp": "^8.2.1", "bumpp": "^8.2.1",
@ -99,10 +101,10 @@
"esno": "^0.16.3", "esno": "^0.16.3",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"fs-extra": "^11.1.0", "fs-extra": "^11.1.0",
"jsdom": "^21.1.0",
"lint-staged": "^13.1.0", "lint-staged": "^13.1.0",
"nuxt": "3.0.0", "nuxt": "3.1.1",
"nuxt-security": "^0.10.1", "nuxt-security": "^0.10.1",
"nuxt-vitest": "^0.6.4",
"postcss-nested": "^6.0.0", "postcss-nested": "^6.0.0",
"prettier": "^2.8.3", "prettier": "^2.8.3",
"rollup-plugin-node-polyfills": "^0.2.1", "rollup-plugin-node-polyfills": "^0.2.1",
@ -112,19 +114,18 @@
"std-env": "^3.3.1", "std-env": "^3.3.1",
"theme-vitesse": "^0.6.0", "theme-vitesse": "^0.6.0",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"unplugin-auto-import": "^0.12.1", "unimport": "^2.1.0",
"unplugin-auto-import": "^0.13.0",
"unplugin-vue-inspector": "^0.0.2", "unplugin-vue-inspector": "^0.0.2",
"vite-plugin-inspect": "^0.7.14", "vite-plugin-inspect": "^0.7.14",
"vite-plugin-pwa": "^0.14.1", "vite-plugin-pwa": "^0.14.1",
"vitest": "^0.28.1", "vitest": "^0.28.3",
"vitest-environment-nuxt": "0.4.0",
"vue-tsc": "^1.0.24", "vue-tsc": "^1.0.24",
"workbox-build": "^6.5.4", "workbox-build": "^6.5.4",
"workbox-window": "^6.5.4" "workbox-window": "^6.5.4"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"mlly": "1.1.0",
"@tiptap/extension-bubble-menu": "2.0.0-beta.204", "@tiptap/extension-bubble-menu": "2.0.0-beta.204",
"@tiptap/extension-floating-menu": "2.0.0-beta.204", "@tiptap/extension-floating-menu": "2.0.0-beta.204",
"@tiptap/core": "2.0.0-beta.204", "@tiptap/core": "2.0.0-beta.204",
@ -146,7 +147,9 @@
"@tiptap/extension-paragraph": "2.0.0-beta.204", "@tiptap/extension-paragraph": "2.0.0-beta.204",
"@tiptap/extension-strike": "2.0.0-beta.204", "@tiptap/extension-strike": "2.0.0-beta.204",
"@tiptap/extension-text": "2.0.0-beta.204", "@tiptap/extension-text": "2.0.0-beta.204",
"vitest>vite": "^3.2.5" "vitest>vite": "^3.2.5",
"@nuxt/kit": "^3.1.1",
"@nuxt/schema": "^3.1.1"
} }
}, },
"simple-git-hooks": { "simple-git-hooks": {

Some files were not shown because too many files have changed in this diff Show more