forked from Mirrors/elk
Merge branch 'main' into feature/remove-link-if-matches-preview-URL
This commit is contained in:
commit
354914fc8d
136 changed files with 2832 additions and 1923 deletions
|
@ -1,5 +1,6 @@
|
|||
NUXT_PUBLIC_TRANSLATE_API=
|
||||
NUXT_PUBLIC_DEFAULT_SERVER=
|
||||
SINGLE_INSTANCE_SERVER=
|
||||
NUXT_PUBLIC_PRIVACY_POLICY_URL=
|
||||
|
||||
# Production only
|
||||
|
|
|
@ -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.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.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.
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ defineProps<{
|
|||
>
|
||||
<slot name="prepend" />
|
||||
<CommonTooltip :content="$t('account.bot')" :disabled="showLabel">
|
||||
<div i-ri:robot-line />
|
||||
<div i-mdi:robot-outline />
|
||||
</CommonTooltip>
|
||||
<div v-if="showLabel">
|
||||
{{ $t('account.bot') }}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import type { mastodon } from 'masto'
|
||||
|
||||
defineProps<{
|
||||
const { account, hideEmojis = false } = defineProps<{
|
||||
account: mastodon.v1.Account
|
||||
hideEmojis?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
@ -10,6 +11,7 @@ defineProps<{
|
|||
<ContentRich
|
||||
:content="getDisplayName(account, { rich: true })"
|
||||
:emojis="account.emojis"
|
||||
:hide-emojis="hideEmojis"
|
||||
:markdown="false"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -117,7 +117,7 @@ const buttonStyle = $computed(() => {
|
|||
<span hidden group-hover="inline">{{ $t('account.follow_back') }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>{{ $t('account.follow') }}</span>
|
||||
<span>{{ account.locked ? $t('account.request_follow') : $t('account.follow') }}</span>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
@ -91,56 +91,58 @@ const isNotifiedOnPost = $computed(() => !!relationship?.notifying)
|
|||
</component>
|
||||
<div p4 mt--18 flex flex-col gap-4>
|
||||
<div relative>
|
||||
<div flex="~ col gap-2 1">
|
||||
<button :class="{ 'rounded-full': !isSelf, 'squircle': isSelf }" w-30 h-30 p1 bg-base border-bg-base z-2 @click="previewAvatar">
|
||||
<div flex justify-between>
|
||||
<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 />
|
||||
</button>
|
||||
<div flex="~ col gap1">
|
||||
<div flex justify-between>
|
||||
<AccountDisplayName :account="account" font-bold sm:text-2xl text-xl />
|
||||
<AccountBotIndicator v-if="account.bot" show-label />
|
||||
</div>
|
||||
<AccountHandle :account="account" />
|
||||
<div inset-ie-0 flex="~ wrap row-reverse" gap-2 items-center pt18 justify-start>
|
||||
<!-- Edit profile -->
|
||||
<NuxtLink
|
||||
v-if="isSelf"
|
||||
to="/settings/profile/appearance"
|
||||
gap-1 items-center border="1" rounded-full flex="~ gap2 center" font-500 min-w-30 h-fit px3 py1
|
||||
hover="border-primary text-primary bg-active"
|
||||
>
|
||||
{{ $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" />
|
||||
<CommonTooltip v-if="!isSelf && relationship?.following" :content="getNotificationIconTitle()">
|
||||
<button
|
||||
:aria-pressed="isNotifiedOnPost"
|
||||
:aria-label="t('account.notifications_on_post_enable', { username: `@${account.username}` })"
|
||||
rounded-full text-sm p2 border-1 transition-colors
|
||||
:class="isNotifiedOnPost ? 'text-primary border-primary hover:bg-red/20 hover:text-red hover:border-red' : 'border-base hover:text-primary'"
|
||||
@click="toggleNotifications"
|
||||
>
|
||||
<span v-if="isNotifiedOnPost" i-ri:notification-4-fill block text-current />
|
||||
<span v-else i-ri-notification-4-line block text-current />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<CommonTooltip :content="$t('list.modify_account')">
|
||||
<VDropdown v-if="!isSelf && relationship?.following">
|
||||
<button
|
||||
:aria-label="$t('list.modify_account')"
|
||||
rounded-full text-sm p2 border-1 transition-colors
|
||||
border-base hover:text-primary
|
||||
>
|
||||
<span i-ri:play-list-add-fill block text-current />
|
||||
</button>
|
||||
<template #popper>
|
||||
<ListLists :user-id="account.id" />
|
||||
</template>
|
||||
</VDropdown>
|
||||
</CommonTooltip>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div absolute top-18 inset-ie-0 flex gap-2 items-center>
|
||||
<AccountMoreButton :account="account" :command="command" />
|
||||
<CommonTooltip v-if="!isSelf && relationship?.following" :content="getNotificationIconTitle()">
|
||||
<button
|
||||
:aria-pressed="isNotifiedOnPost"
|
||||
:aria-label="t('account.notifications_on_post_enable', { username: `@${account.username}` })"
|
||||
rounded-full text-sm p2 border-1 transition-colors
|
||||
:class="isNotifiedOnPost ? 'text-primary border-primary hover:bg-red/20 hover:text-red hover:border-red' : 'border-base hover:text-primary'"
|
||||
@click="toggleNotifications"
|
||||
>
|
||||
<span v-if="isNotifiedOnPost" i-ri:notification-4-fill block text-current />
|
||||
<span v-else i-ri-notification-4-line block text-current />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<CommonTooltip :content="$t('list.modify_account')">
|
||||
<VDropdown v-if="!isSelf && relationship?.following">
|
||||
<button
|
||||
:aria-label="$t('list.modify_account')"
|
||||
rounded-full text-sm p2 border-1 transition-colors
|
||||
border-base hover:text-primary
|
||||
>
|
||||
<span i-ri:play-list-add-fill block text-current />
|
||||
</button>
|
||||
<template #popper>
|
||||
<ListLists :user-id="account.id" />
|
||||
</template>
|
||||
</VDropdown>
|
||||
</CommonTooltip>
|
||||
<AccountFollowButton :account="account" :command="command" />
|
||||
<!-- Edit profile -->
|
||||
<NuxtLink
|
||||
v-if="isSelf"
|
||||
to="/settings/profile/appearance"
|
||||
gap-1 items-center border="1" rounded-full flex="~ gap2 center" font-500 min-w-30 h-fit px3 py1
|
||||
hover="border-primary text-primary bg-active"
|
||||
>
|
||||
{{ $t('settings.profile.appearance.title') }}
|
||||
</NuxtLink>
|
||||
<div flex="~ col gap1" pt2>
|
||||
<div flex justify-between>
|
||||
<AccountDisplayName :account="account" font-bold sm:text-2xl text-xl />
|
||||
<AccountBotIndicator v-if="account.bot" show-label />
|
||||
</div>
|
||||
<AccountHandle :account="account" />
|
||||
</div>
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iconFields.length" flex="~ wrap gap-4">
|
||||
<div v-for="field in iconFields" :key="field.name" flex="~ gap-1" items-center>
|
||||
<div v-if="iconFields.length" flex="~ wrap gap-2">
|
||||
<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)">
|
||||
<div text-secondary :class="getAccountFieldIcon(field.name)" :title="getFieldIconTitle(field.name)" />
|
||||
</CommonTooltip>
|
||||
<ContentRich text-sm filter-saturate-0 :content="field.value" :emojis="account.emojis" />
|
||||
<ContentRich text-sm :content="field.value" :emojis="account.emojis" />
|
||||
</div>
|
||||
</div>
|
||||
<AccountPostsFollowers :account="account" />
|
||||
|
|
|
@ -6,6 +6,8 @@ const { link = true, avatar = true } = defineProps<{
|
|||
link?: boolean
|
||||
avatar?: boolean
|
||||
}>()
|
||||
|
||||
const userSettings = useUserSettings()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -16,7 +18,7 @@ const { link = true, avatar = true } = defineProps<{
|
|||
min-w-0 flex gap-2 items-center
|
||||
>
|
||||
<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>
|
||||
</AccountHoverWrapper>
|
||||
</template>
|
||||
|
|
|
@ -64,6 +64,9 @@ const result = $computed<QueryResult>(() => commandMode
|
|||
: searchResult,
|
||||
)
|
||||
|
||||
const isMac = useIsMac()
|
||||
const modifierKeyName = $computed(() => isMac.value ? '⌘' : 'Ctrl')
|
||||
|
||||
let active = $ref(0)
|
||||
watch($$(result), (n, o) => {
|
||||
if (n.length !== o.length || !n.items.every((i, idx) => i === o.items[idx]))
|
||||
|
@ -233,8 +236,8 @@ const onKeyDown = (e: KeyboardEvent) => {
|
|||
<!-- Footer -->
|
||||
<div class="flex items-center px-3 py-1 text-xs">
|
||||
<div i-ri:lightbulb-flash-line /> Tip: Use
|
||||
<CommandKey name="Ctrl+K" /> to search,
|
||||
<CommandKey name="Ctrl+/" /> to activate command mode.
|
||||
<CommandKey :name="`${modifierKeyName}+K`" /> to search,
|
||||
<CommandKey :name="`${modifierKeyName}+/`" /> to activate command mode.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
describedBy: string
|
||||
}>()
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
defineProps<{ describedBy: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
:aria-describedby="describedBy"
|
||||
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"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
|
@ -43,8 +43,31 @@ defineSlots<{
|
|||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -46,6 +46,7 @@ useCommand({
|
|||
v-bind="$attrs"
|
||||
:is="is"
|
||||
ref="el"
|
||||
w-full
|
||||
flex gap-3 items-center cursor-pointer px4 py3
|
||||
select-none
|
||||
hover-bg-active
|
||||
|
|
|
@ -7,10 +7,12 @@ defineOptions({
|
|||
const {
|
||||
content,
|
||||
emojis,
|
||||
hideEmojis = false,
|
||||
markdown = true,
|
||||
} = defineProps<{
|
||||
content: string
|
||||
emojis?: mastodon.v1.CustomEmoji[]
|
||||
hideEmojis?: boolean
|
||||
markdown?: boolean
|
||||
}>()
|
||||
|
||||
|
@ -21,6 +23,7 @@ export default () => h(
|
|||
{ class: 'content-rich', dir: 'auto' },
|
||||
contentToVNode(content, {
|
||||
emojis: emojisObject.value,
|
||||
hideEmojis,
|
||||
markdown,
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -6,7 +6,8 @@ const { paginator } = defineProps<{
|
|||
}>()
|
||||
|
||||
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'),
|
||||
))
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ const emit = defineEmits<{
|
|||
</p>
|
||||
{{ $t('help.desc_para3') }}
|
||||
<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">
|
||||
<img :src="`/avatars/${team.github}-100x100.png`" :alt="team.display" rounded-full w-15 h-15 height="60" width="60">
|
||||
</NuxtLink>
|
||||
|
@ -38,7 +38,7 @@ const emit = defineEmits<{
|
|||
</p>
|
||||
<p italic flex justify-center w-full>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
|
|
|
@ -36,8 +36,19 @@ async function edit() {
|
|||
:to="getAccountRoute(account)"
|
||||
/>
|
||||
<div>
|
||||
<CommonTooltip :content="isRemoved ? $t('list.add_account') : $t('list.remove_account')" :hover="isRemoved ? 'text-green' : 'text-red'">
|
||||
<button :class="isRemoved ? 'i-ri:user-add-line' : 'i-ri:user-unfollow-line'" text-xl @click="edit" />
|
||||
<CommonTooltip
|
||||
: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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
233
components/list/ListEntry.vue
Normal file
233
components/list/ListEntry.vue
Normal 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>
|
|
@ -39,9 +39,13 @@ async function edit(listId: string) {
|
|||
:hover="indexOfUserInList(item.id) === -1 ? 'text-green' : 'text-red'"
|
||||
>
|
||||
<button
|
||||
:class="indexOfUserInList(item.id) === -1 ? 'i-ri:user-add-line' : 'i-ri:user-unfollow-line'"
|
||||
text-xl @click="() => edit(item.id)"
|
||||
/>
|
||||
text-sm p2 border-1 transition-colors
|
||||
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>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -4,6 +4,8 @@ defineProps<{
|
|||
backOnSmallScreen?: boolean
|
||||
/** Show the back button on both small and big screens */
|
||||
back?: boolean
|
||||
/** Do not applying overflow hidden to let use floatable components in title */
|
||||
noOverflowHidden?: boolean
|
||||
}>()
|
||||
|
||||
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)]"
|
||||
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 gap-3 items-center overflow-hidden py2 class="native-mac:pl-14 native-mac:sm:pl-0">
|
||||
<div flex justify-between px5 py2 :class="{ 'xl:hidden': $route.name !== 'tag' }" class="native:xl:flex">
|
||||
<div flex gap-3 items-center :overflow-hidden="!noOverflowHidden ? '' : false" py2 w-full>
|
||||
<NuxtLink
|
||||
v-if="backOnSmallScreen || back" flex="~ gap1" items-center btn-text p-0 xl:hidden
|
||||
: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" />
|
||||
</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" />
|
||||
</div>
|
||||
<div h-7 w-1px />
|
||||
<div sm:hidden h-7 w-1px />
|
||||
</div>
|
||||
<div flex items-center flex-shrink-0 gap-x-2>
|
||||
<slot name="actions" />
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
isCommandPanelOpen,
|
||||
isConfirmDialogOpen,
|
||||
isEditHistoryDialogOpen,
|
||||
isErrorDialogOpen,
|
||||
isFavouritedBoostedByDialogOpen,
|
||||
isMediaPreviewOpen,
|
||||
isPreviewHelpOpen,
|
||||
|
@ -87,6 +88,9 @@ const handleFavouritedBoostedByClose = () => {
|
|||
<ModalDialog v-model="isConfirmDialogOpen" py-4 px-8 max-w-125>
|
||||
<ModalConfirm v-if="confirmDialogLabel" v-bind="confirmDialogLabel" @choice="handleConfirmChoice" />
|
||||
</ModalDialog>
|
||||
<ModalDialog v-model="isErrorDialogOpen" py-4 px-8 max-w-125>
|
||||
<ModalError v-if="errorDialogData" v-bind="errorDialogData" />
|
||||
</ModalDialog>
|
||||
<ModalDialog
|
||||
v-model="isFavouritedBoostedByDialogOpen"
|
||||
max-w-180
|
||||
|
|
31
components/modal/ModalError.vue
Normal file
31
components/modal/ModalError.vue
Normal 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>
|
|
@ -1,5 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { SwipeDirection } from '@vueuse/core'
|
||||
import { useGesture } from '@vueuse/gesture'
|
||||
import type { PermissiveMotionProperties } from '@vueuse/motion'
|
||||
import { useReducedMotion } from '@vueuse/motion'
|
||||
import type { mastodon } from 'masto'
|
||||
|
||||
|
@ -23,18 +25,36 @@ const reduceMotion = useReducedMotion()
|
|||
|
||||
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 { isSwiping, lengthX, lengthY, direction } = useSwipe(target, {
|
||||
threshold: 5,
|
||||
passive: false,
|
||||
onSwipeEnd(e, direction) {
|
||||
// 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)
|
||||
resetZoom()
|
||||
}
|
||||
|
||||
// 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)
|
||||
resetZoom()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
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(() => {
|
||||
if (width.value === 0)
|
||||
return 0
|
||||
|
|
|
@ -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">
|
||||
<div i-ri:home-5-line />
|
||||
</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 />
|
||||
</NuxtLink>
|
||||
<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">
|
||||
<div i-ri:hashtag />
|
||||
</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">
|
||||
<div i-ri:group-2-line />
|
||||
</NuxtLink>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
const buildInfo = useRuntimeConfig().public.buildInfo
|
||||
const buildInfo = useAppConfig().buildInfo
|
||||
const timeAgoOptions = useTimeAgoOptions()
|
||||
|
||||
const userSettings = useUserSettings()
|
||||
|
|
|
@ -6,12 +6,11 @@ const { notifications } = useNotifications()
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<nav sm:px3 flex="~ col gap2" shrink text-size-base leading-normal md:text-lg>
|
||||
<div shrink hidden sm:block mt-4 />
|
||||
<nav sm:px3 flex="~ col gap2" shrink text-size-base leading-normal md:text-lg h-full>
|
||||
<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.notifications')" to="/notifications" icon="i-ri:notification-4-line" user-only :command="command">
|
||||
<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" />
|
||||
|
||||
<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.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 />
|
||||
<NavSideItem :text="$t('nav.settings')" to="/settings" icon="i-ri:settings-3-line" :command="command" />
|
||||
|
|
|
@ -2,6 +2,13 @@
|
|||
const { env } = useBuildInfo()
|
||||
const router = useRouter()
|
||||
const back = ref<any>('')
|
||||
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
const onClickLogo = () => {
|
||||
nuxtApp.hooks.callHook('elk-logo:click')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
back.value = router.options.history.state.back
|
||||
})
|
||||
|
@ -11,16 +18,15 @@ router.afterEach(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Use external to force refresh page and jump to top of timeline -->
|
||||
<div flex justify-between>
|
||||
<div flex justify-between sticky top-0 bg-base z-1 py-4 native:py-7 data-tauri-drag-region>
|
||||
<NuxtLink
|
||||
flex items-end gap-4
|
||||
flex items-end gap-3
|
||||
py2 px-5
|
||||
text-2xl
|
||||
select-none
|
||||
focus-visible:ring="2 current"
|
||||
to="/"
|
||||
external
|
||||
to="/home"
|
||||
@click.prevent="onClickLogo"
|
||||
>
|
||||
<NavLogo shrink-0 aspect="1/1" sm:h-8 xl:h-10 class="rtl-flip" />
|
||||
<div hidden xl:block text-secondary>
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
<script setup>
|
||||
const { busy, oauth, singleInstanceServer } = useSignIn()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDropdown v-if="isHydrated && currentUser" sm:hidden>
|
||||
<div style="-webkit-touch-callout: none;">
|
||||
|
@ -15,7 +19,24 @@
|
|||
<UserSwitcher ref="switcher" @click="hide()" />
|
||||
</template>
|
||||
</VDropdown>
|
||||
<button v-else btn-solid text-sm px-2 py-1 text-center xl:hidden @click="openSigninDialog()">
|
||||
{{ $t('action.sign_in') }}
|
||||
</button>
|
||||
<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()">
|
||||
{{ $t('action.sign_in') }}
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import { PushSubscriptionError } from '~/composables/push-notifications/types'
|
||||
|
||||
defineProps<{ show?: boolean }>()
|
||||
|
||||
const {
|
||||
|
@ -17,7 +15,7 @@ const {
|
|||
} = usePushManager()
|
||||
const { t } = useI18n()
|
||||
|
||||
const pwaEnabled = useRuntimeConfig().public.pwaEnabled
|
||||
const pwaEnabled = useAppConfig().pwaEnabled
|
||||
|
||||
let busy = $ref<boolean>(false)
|
||||
let animateSave = $ref<boolean>(false)
|
||||
|
|
|
@ -36,5 +36,11 @@ const { modelValue } = defineModel<{
|
|||
</CommonTooltip>
|
||||
</head>
|
||||
<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>
|
||||
</template>
|
||||
|
|
|
@ -34,7 +34,7 @@ const toggleApply = () => {
|
|||
text-white px2 py2 rounded-full cursor-pointer
|
||||
@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 absolute right-2 bottom-2>
|
||||
|
|
52
components/publish/PublishEditorTools.vue
Normal file
52
components/publish/PublishEditorTools.vue
Normal 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>
|
|
@ -1,5 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import ISO6391 from 'iso-639-1'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
let { modelValue } = $defineModel<{
|
||||
|
@ -7,20 +6,11 @@ let { modelValue } = $defineModel<{
|
|||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const userSettings = useUserSettings()
|
||||
|
||||
const languageKeyword = $ref('')
|
||||
|
||||
const languageList: {
|
||||
code: string
|
||||
nativeName: string
|
||||
name: string
|
||||
}[] = ISO6391.getAllCodes().map(code => ({
|
||||
code,
|
||||
nativeName: ISO6391.getNativeName(code),
|
||||
name: ISO6391.getName(code),
|
||||
}))
|
||||
|
||||
const fuse = new Fuse(languageList, {
|
||||
const fuse = new Fuse(languagesNameList, {
|
||||
keys: ['code', 'nativeName', 'name'],
|
||||
shouldSort: true,
|
||||
})
|
||||
|
@ -28,9 +18,22 @@ const fuse = new Fuse(languageList, {
|
|||
const languages = $computed(() =>
|
||||
languageKeyword.trim()
|
||||
? fuse.search(languageKeyword).map(r => r.item)
|
||||
: [...languageList].sort(({ code: a }, { code: b }) => {
|
||||
return a === modelValue ? -1 : b === modelValue ? 1 : a.localeCompare(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)
|
||||
}),
|
||||
)
|
||||
|
||||
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) {
|
||||
|
@ -39,14 +42,28 @@ function chooseLanguage(language: string) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<input
|
||||
v-model="languageKeyword"
|
||||
:placeholder="t('language.search')"
|
||||
p2 mb2 border-rounded w-full bg-transparent
|
||||
outline-none border="~ base"
|
||||
>
|
||||
<div relative of-x-hidden>
|
||||
<div p2>
|
||||
<input
|
||||
v-model="languageKeyword"
|
||||
:placeholder="t('language.search')"
|
||||
p2 border-rounded w-full bg-transparent
|
||||
outline-none border="~ base"
|
||||
>
|
||||
</div>
|
||||
<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
|
||||
v-for="{ code, nativeName, name } in languages"
|
||||
:key="code"
|
||||
|
|
|
@ -6,7 +6,7 @@ import type { Draft } from '~/types'
|
|||
|
||||
const {
|
||||
draftKey,
|
||||
initial = getDefaultDraft() as never /* Bug of vue-core */,
|
||||
initial = getDefaultDraft,
|
||||
expanded = false,
|
||||
placeholder,
|
||||
dialogLabelledBy,
|
||||
|
@ -35,7 +35,7 @@ const {
|
|||
dropZoneRef,
|
||||
} = $(useUploadMediaAttachment($$(draft)))
|
||||
|
||||
let { shouldExpanded, isExpanded, isSending, isPublishDisabled, publishDraft, failedMessages } = $(usePublish(
|
||||
let { shouldExpanded, isExpanded, isSending, isPublishDisabled, publishDraft, failedMessages, preferredLanguage, publishSpoilerText } = $(usePublish(
|
||||
{
|
||||
draftState,
|
||||
...$$({ expanded, isUploading, initialDraft: initial }),
|
||||
|
@ -62,6 +62,7 @@ const { editor } = useTiptap({
|
|||
},
|
||||
onPaste: handlePaste,
|
||||
})
|
||||
|
||||
const characterCount = $computed(() => {
|
||||
let length = stringLength(htmlToText(editor.value?.getHTML() || ''))
|
||||
|
||||
|
@ -73,9 +74,13 @@ const characterCount = $computed(() => {
|
|||
}).join(' ').length + 1
|
||||
}
|
||||
|
||||
length += stringLength(publishSpoilerText)
|
||||
|
||||
return length
|
||||
})
|
||||
|
||||
const postLanguageDisplay = $computed(() => languagesNameList.find(i => i.code === (draft.params.language || preferredLanguage))?.nativeName)
|
||||
|
||||
async function handlePaste(evt: ClipboardEvent) {
|
||||
const files = evt.clipboardData?.files
|
||||
if (!files || files.length === 0)
|
||||
|
@ -108,10 +113,16 @@ useWebShareTarget(async ({ data: { data, action } }: any) => {
|
|||
|
||||
editor.value?.commands.focus('end')
|
||||
|
||||
if (data.text !== undefined)
|
||||
editor.value?.commands.insertContent(data.text)
|
||||
for (const text of data.textParts) {
|
||||
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)
|
||||
})
|
||||
|
||||
|
@ -147,13 +158,13 @@ defineExpose({
|
|||
>
|
||||
<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)">
|
||||
{{ acctToShortHandle(m) }}
|
||||
{{ accountToShortHandle(m) }}
|
||||
</button>
|
||||
</ContentMentionGroup>
|
||||
|
||||
<div v-if="draft.params.sensitive">
|
||||
<input
|
||||
v-model="draft.params.spoilerText"
|
||||
v-model="publishSpoilerText"
|
||||
type="text"
|
||||
:placeholder="$t('placeholder.content_warning')"
|
||||
p2 border-rounded w-full bg-transparent
|
||||
|
@ -161,8 +172,8 @@ defineExpose({
|
|||
>
|
||||
</div>
|
||||
|
||||
<PublishErrMessage v-if="failedMessages.length > 0" described-by="publish-failed">
|
||||
<head id="publish-failed" flex justify-between>
|
||||
<CommonErrorMessage v-if="failedMessages.length > 0" described-by="publish-failed">
|
||||
<header id="publish-failed" flex justify-between>
|
||||
<div flex items-center gap-x-2 font-bold>
|
||||
<div aria-hidden="true" i-ri:error-warning-fill />
|
||||
<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 />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</head>
|
||||
</header>
|
||||
<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>
|
||||
<strong>{{ i + 1 }}.</strong>
|
||||
<span>{{ error }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</PublishErrMessage>
|
||||
</CommonErrorMessage>
|
||||
|
||||
<div relative flex-1 flex flex-col>
|
||||
<EditorContent
|
||||
|
@ -198,11 +209,11 @@ defineExpose({
|
|||
</div>
|
||||
{{ $t('state.uploading') }}
|
||||
</div>
|
||||
<PublishErrMessage
|
||||
<CommonErrorMessage
|
||||
v-else-if="failedAttachments.length > 0"
|
||||
: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 aria-hidden="true" i-ri:error-warning-fill />
|
||||
<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 />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</head>
|
||||
</header>
|
||||
<div v-if="isExceedingAttachmentLimit" id="uploads-per-post" ps-2 sm:ps-1 text-small>
|
||||
{{ $t('state.attachments_exceed_server_limit') }}
|
||||
</div>
|
||||
|
@ -225,7 +236,7 @@ defineExpose({
|
|||
<span>{{ error[0] }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</PublishErrMessage>
|
||||
</CommonErrorMessage>
|
||||
|
||||
<div v-if="draft.attachments.length" flex="~ col gap-2" overflow-auto>
|
||||
<PublishAttachment
|
||||
|
@ -259,18 +270,7 @@ defineExpose({
|
|||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<template v-if="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>
|
||||
<PublishEditorTools v-if="editor" :editor="editor" />
|
||||
|
||||
<div flex-auto />
|
||||
|
||||
|
@ -278,6 +278,20 @@ defineExpose({
|
|||
{{ characterCount ?? 0 }}<span text-secondary-light>/</span><span text-secondary-light>{{ characterLimit }}</span>
|
||||
</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')">
|
||||
<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 />
|
||||
|
@ -285,19 +299,6 @@ defineExpose({
|
|||
</button>
|
||||
</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">
|
||||
<template #default="{ visibility }">
|
||||
<button :disabled="!!draft.editingStatus" :aria-label="$t('tooltip.change_content_visibility')" btn-action-icon :class="{ 'w-12': !draft.editingStatus }">
|
||||
|
|
|
@ -5,9 +5,14 @@ const index = ref(0)
|
|||
|
||||
const { t } = useI18n()
|
||||
const el = ref<HTMLElement>()
|
||||
const input = ref<HTMLInputElement>()
|
||||
const router = useRouter()
|
||||
const { focused } = useFocusWithin(el)
|
||||
|
||||
defineExpose({
|
||||
input,
|
||||
})
|
||||
|
||||
const results = computed(() => {
|
||||
if (query.value.length === 0)
|
||||
return []
|
||||
|
@ -68,6 +73,7 @@ const activate = () => {
|
|||
bg-transparent
|
||||
outline="focus:none"
|
||||
pe-4
|
||||
ml-1
|
||||
:placeholder="isHydrated ? t('nav.search') : ''"
|
||||
pb="1px"
|
||||
placeholder-text-secondary
|
||||
|
@ -77,7 +83,7 @@ const activate = () => {
|
|||
>
|
||||
</div>
|
||||
<!-- 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>
|
||||
<span v-if="query.trim().length === 0" block text-center text-sm text-secondary>
|
||||
{{ t('search.search_desc') }}
|
||||
|
|
|
@ -9,7 +9,7 @@ defineProps<{
|
|||
<template>
|
||||
<button
|
||||
exact-active-class="text-primary"
|
||||
block w-full group focus:outline-none
|
||||
block w-full group focus:outline-none text-start
|
||||
>
|
||||
<div
|
||||
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 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' : ''"
|
||||
>
|
||||
<slot name="icon">
|
||||
|
|
63
components/settings/SettingsTranslations.vue
Normal file
63
components/settings/SettingsTranslations.vue
Normal 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>
|
|
@ -5,6 +5,8 @@ const { account, link = true } = defineProps<{
|
|||
account: mastodon.v1.Account
|
||||
link?: boolean
|
||||
}>()
|
||||
|
||||
const userSettings = useUserSettings()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -13,7 +15,7 @@ const { account, link = true } = defineProps<{
|
|||
flex="~ col" min-w-0 md:flex="~ row gap-2" md:items-center
|
||||
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" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
|
|
@ -49,7 +49,7 @@ useCommand({
|
|||
<component
|
||||
:is="as"
|
||||
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
|
||||
:hover=" !disabled ? hover : undefined"
|
||||
focus:outline-none
|
||||
|
|
|
@ -87,6 +87,8 @@ useIntersectionObserver(video, (entries) => {
|
|||
}
|
||||
})
|
||||
}, { threshold: 0.75 })
|
||||
|
||||
const userSettings = useUserSettings()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -167,7 +169,7 @@ useIntersectionObserver(video, (entries) => {
|
|||
/>
|
||||
</button>
|
||||
</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">
|
||||
<button
|
||||
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'"
|
||||
>
|
||||
<div hidden>
|
||||
read {{ attachment.type }} description
|
||||
{{ $t('status.img_alt.read', [attachment.type]) }}
|
||||
</div>
|
||||
ALT
|
||||
{{ $t('status.img_alt.ALT') }}
|
||||
</button>
|
||||
<template #popper>
|
||||
<div p4 flex flex-col gap-2 max-w-130>
|
||||
|
|
|
@ -26,7 +26,7 @@ const props = withDefaults(
|
|||
const userSettings = useUserSettings()
|
||||
|
||||
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
|
||||
})
|
||||
|
@ -79,7 +79,8 @@ const showReplyTo = $computed(() => !replyToMain && !directReply)
|
|||
<div
|
||||
:id="`status-${status.id}`"
|
||||
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 }"
|
||||
tabindex="0"
|
||||
focus:outline-none focus-visible:ring="2 primary"
|
||||
|
@ -95,12 +96,12 @@ const showReplyTo = $computed(() => !replyToMain && !directReply)
|
|||
<template v-if="status.inReplyToAccountId">
|
||||
<StatusReplyingTo
|
||||
v-if="showReplyTo"
|
||||
ml-20px pt-1 pl-5
|
||||
m="is-5" p="t-1 is-5"
|
||||
:status="status"
|
||||
:is-self-reply="isSelfReply"
|
||||
: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">
|
||||
<div w="1px" h="0.5" border="x base" mt-3 />
|
||||
<div w="1px" h="0.5" border="x base" />
|
||||
|
|
|
@ -15,7 +15,11 @@ const filterResult = $computed(() => status.filtered?.length ? status.filtered[0
|
|||
const filter = $computed(() => filterResult?.filter)
|
||||
|
||||
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
|
||||
&& !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' : ''" />
|
||||
<StatusSpoiler :enabled="status.sensitive || isFiltered" :filter="isFiltered" :is-d-m="isDM">
|
||||
<template v-if="filterPhrase" #spoiler>
|
||||
<p>{{ `${$t('status.filter_hidden_phrase')}: ${filterPhrase}` }}</p>
|
||||
</template>
|
||||
<template v-else-if="status.spoilerText" #spoiler>
|
||||
<StatusSpoiler :enabled="hasSensitiveSpoilerOrMedia || isFiltered" :filter="isFiltered" :is-d-m="isDM">
|
||||
<template v-if="status.spoilerText" #spoiler>
|
||||
<p>{{ status.spoilerText }}</p>
|
||||
</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' : ''" />
|
||||
<StatusTranslation :status="status" />
|
||||
<StatusPoll v-if="status.poll" :status="status" />
|
||||
|
|
|
@ -17,6 +17,6 @@ const gitHubCards = $(usePreferences('experimentalGitHubCards'))
|
|||
|
||||
<template>
|
||||
<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" />
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import type { mastodon } from 'masto'
|
||||
import reservedNames from 'github-reserved-names'
|
||||
|
||||
const props = defineProps<{
|
||||
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 { url } = props.card
|
||||
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
|
||||
// /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 firstIsUser = firstName && !supportedReservedRoutes.includes(firstName)
|
||||
const user = firstIsUser ? firstName : secondName
|
||||
const repo = firstIsUser ? secondName : undefined
|
||||
|
||||
|
@ -86,7 +85,7 @@ const meta = $computed(() => {
|
|||
|
||||
<template>
|
||||
<div
|
||||
v-if="card.image"
|
||||
v-if="card.image && meta"
|
||||
flex flex-col
|
||||
display-block of-hidden
|
||||
bg-card
|
||||
|
@ -132,4 +131,5 @@ const meta = $computed(() => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<StatusPreviewCardNormal v-else :card="card" />
|
||||
</template>
|
||||
|
|
|
@ -21,9 +21,9 @@ const maxLines = 20
|
|||
|
||||
const meta = $computed(() => {
|
||||
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 lines = meta?.[2].replaceAll('N', '')
|
||||
const lines = meta?.[2]
|
||||
const code = meta?.[3].split('\n').slice(0, maxLines).join('\n')
|
||||
const project = props.card.title?.replace(' - StackBlitz', '')
|
||||
const info = $ref<Meta>({
|
||||
|
@ -38,7 +38,12 @@ const meta = $computed(() => {
|
|||
const vnodeCode = $computed(() => {
|
||||
if (!meta.code)
|
||||
return null
|
||||
const vnode = contentToVNode(`<p>\`\`\`${meta.file?.split('.')?.[1] ?? ''}\n${meta.code}\n\`\`\`\</p>`, {
|
||||
const code = meta.code
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/`/g, '`')
|
||||
|
||||
const vnode = contentToVNode(`<p>\`\`\`${meta.file?.split('.')?.[1] ?? ''}\n${code}\n\`\`\`\</p>`, {
|
||||
markdown: true,
|
||||
})
|
||||
return vnode
|
||||
|
|
|
@ -12,7 +12,7 @@ const {
|
|||
} = useTranslation(status, getLanguageCode())
|
||||
const preferenceHideTranslation = usePreferences('hideTranslation')
|
||||
|
||||
const showButton = computed(() => !preferenceHideTranslation.value && isTranslationEnabled && status.language !== getLanguageCode())
|
||||
const showButton = computed(() => !preferenceHideTranslation.value && isTranslationEnabled)
|
||||
|
||||
let translating = $ref(false)
|
||||
const toggleTranslation = async () => {
|
||||
|
|
|
@ -40,6 +40,9 @@ watch(items, () => {
|
|||
})
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (items.length === 0)
|
||||
return false
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
selectedIndex = ((selectedIndex + items.length) - 1) % items.length
|
||||
return true
|
||||
|
|
|
@ -15,6 +15,9 @@ watch(items, () => {
|
|||
})
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (items.length === 0)
|
||||
return false
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
selectedIndex = ((selectedIndex + items.length) - 1) % items.length
|
||||
return true
|
||||
|
|
|
@ -15,6 +15,9 @@ watch(items, () => {
|
|||
})
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (items.length === 0)
|
||||
return false
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
selectedIndex = ((selectedIndex + items.length) - 1) % items.length
|
||||
return true
|
||||
|
|
|
@ -5,7 +5,7 @@ const all = useUsers()
|
|||
const router = useRouter()
|
||||
|
||||
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))
|
||||
else
|
||||
switchUser(user)
|
||||
|
@ -21,7 +21,7 @@ const clickUser = (user: UserLogin) => {
|
|||
flex rounded
|
||||
cursor-pointer
|
||||
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"
|
||||
@click="clickUser(user)"
|
||||
>
|
||||
|
|
|
@ -1,66 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import Fuse from 'fuse.js'
|
||||
import { $fetch } from 'ofetch'
|
||||
|
||||
const input = $ref<HTMLInputElement>()
|
||||
let server = $ref<string>('')
|
||||
let busy = $ref<boolean>(false)
|
||||
let error = $ref<boolean>(false)
|
||||
let displayError = $ref<boolean>(false)
|
||||
const input = ref<HTMLInputElement | undefined>()
|
||||
let knownServers = $ref<string[]>([])
|
||||
let autocompleteIndex = $ref(0)
|
||||
let autocompleteShow = $ref(false)
|
||||
|
||||
const users = useUsers()
|
||||
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)
|
||||
}
|
||||
}
|
||||
const { busy, error, displayError, server, oauth } = useSignIn(input)
|
||||
|
||||
let fuse = $shallowRef(new Fuse([] as string[]))
|
||||
|
||||
const filteredServers = $computed(() => {
|
||||
if (!server)
|
||||
if (!server.value)
|
||||
return []
|
||||
|
||||
const results = fuse.search(server, { limit: 6 }).map(result => result.item)
|
||||
if (results[0] === server)
|
||||
const results = fuse.search(server.value, { limit: 6 }).map(result => result.item)
|
||||
if (results[0] === server.value)
|
||||
return []
|
||||
|
||||
return results
|
||||
|
@ -78,12 +33,12 @@ function isValidUrl(str: string) {
|
|||
}
|
||||
|
||||
async function handleInput() {
|
||||
const input = server.trim()
|
||||
const input = server.value.trim()
|
||||
if (input.startsWith('https://'))
|
||||
server = input.replace('https://', '')
|
||||
server.value = input.replace('https://', '')
|
||||
|
||||
if (input.length)
|
||||
displayError = false
|
||||
displayError.value = false
|
||||
|
||||
if (
|
||||
isValidUrl(`https://${input}`)
|
||||
|
@ -110,7 +65,7 @@ function move(delta: number) {
|
|||
|
||||
function onEnter(e: KeyboardEvent) {
|
||||
if (autocompleteShow === true && filteredServers[autocompleteIndex]) {
|
||||
server = filteredServers[autocompleteIndex]
|
||||
server.value = filteredServers[autocompleteIndex]
|
||||
e.preventDefault()
|
||||
autocompleteShow = false
|
||||
}
|
||||
|
@ -124,16 +79,16 @@ function escapeAutocomplete(evt: KeyboardEvent) {
|
|||
}
|
||||
|
||||
function select(index: number) {
|
||||
server = filteredServers[index]
|
||||
server.value = filteredServers[index]
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
input?.focus()
|
||||
input?.value?.focus()
|
||||
knownServers = await (globalThis.$fetch as any)('/api/list-servers')
|
||||
fuse = new Fuse(knownServers, { shouldSort: true })
|
||||
})
|
||||
|
||||
onClickOutside($$(input), () => {
|
||||
onClickOutside(input, () => {
|
||||
autocompleteShow = false
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
const { busy, oauth, singleInstanceServer } = useSignIn()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div p8 lg:flex="~ col gap2" hidden>
|
||||
<p v-if="isHydrated" text-sm>
|
||||
|
@ -8,7 +12,19 @@
|
|||
<p text-sm text-secondary>
|
||||
{{ $t('user.sign_in_desc') }}
|
||||
</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') }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,7 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
const all = useUsers()
|
||||
const { busy, singleInstanceServer, oauth } = useSignIn()
|
||||
|
||||
const sorted = computed(() => {
|
||||
return [
|
||||
|
@ -21,6 +22,12 @@ const clickUser = (user: UserLogin) => {
|
|||
else
|
||||
switchUser(user)
|
||||
}
|
||||
const processSignIn = () => {
|
||||
if (singleInstanceServer)
|
||||
oauth()
|
||||
else
|
||||
openSigninDialog()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -43,7 +50,7 @@ const clickUser = (user: UserLogin) => {
|
|||
:text="$t('user.add_existing')"
|
||||
icon="i-ri:user-add-line"
|
||||
w-full
|
||||
@click="openSigninDialog"
|
||||
@click="processSignIn"
|
||||
/>
|
||||
<CommonDropdownItem
|
||||
is="button"
|
||||
|
@ -51,7 +58,7 @@ const clickUser = (user: UserLogin) => {
|
|||
:text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])"
|
||||
icon="i-ri:logout-box-line rtl-flip"
|
||||
w-full
|
||||
@click="signout"
|
||||
@click="signOut"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@ export interface Team {
|
|||
mastodon: string
|
||||
}
|
||||
|
||||
export const teams: Team[] = [
|
||||
export const elkTeamMembers: Team[] = [
|
||||
{
|
||||
github: 'antfu',
|
||||
display: 'Anthony Fu',
|
||||
|
@ -35,5 +35,5 @@ export const teams: Team[] = [
|
|||
].sort(() => Math.random() - 0.5)
|
||||
|
||||
export function useBuildInfo() {
|
||||
return useRuntimeConfig().public.buildInfo as BuildInfo
|
||||
return useAppConfig().buildInfo as BuildInfo
|
||||
}
|
||||
|
|
|
@ -59,7 +59,8 @@ export function fetchAccountById(id?: string | null): Promise<mastodon.v1.Accoun
|
|||
export async function fetchAccountByHandle(acct: string): Promise<mastodon.v1.Account> {
|
||||
const server = currentServer.value
|
||||
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)
|
||||
if (cached)
|
||||
return cached
|
||||
|
@ -69,9 +70,9 @@ export async function fetchAccountByHandle(acct: string): Promise<mastodon.v1.Ac
|
|||
const client = useMastoClient()
|
||||
let account: mastodon.v1.Account
|
||||
if (!isGotoSocial.value)
|
||||
account = await client.v1.accounts.lookup({ acct })
|
||||
account = await client.v1.accounts.lookup({ acct: userAcct })
|
||||
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)
|
||||
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) {
|
||||
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.acct}`, account, override)
|
||||
setCached(`${server}:${userId}:account:${userAcct}`, account, override)
|
||||
}
|
||||
|
|
|
@ -245,6 +245,7 @@ export const provideGlobalCommands = () => {
|
|||
const masto = useMasto()
|
||||
const colorMode = useColorMode()
|
||||
const userSettings = useUserSettings()
|
||||
const { singleInstanceServer, oauth } = useSignIn()
|
||||
|
||||
useCommand({
|
||||
scope: 'Navigation',
|
||||
|
@ -310,7 +311,10 @@ export const provideGlobalCommands = () => {
|
|||
icon: 'i-ri:user-add-line',
|
||||
|
||||
onActivate() {
|
||||
openSigninDialog()
|
||||
if (singleInstanceServer)
|
||||
oauth()
|
||||
else
|
||||
openSigninDialog()
|
||||
},
|
||||
})
|
||||
useCommand({
|
||||
|
@ -349,7 +353,7 @@ export const provideGlobalCommands = () => {
|
|||
icon: 'i-ri:logout-box-line',
|
||||
|
||||
onActivate() {
|
||||
signout()
|
||||
signOut()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { emojiRegEx, getEmojiAttributes } from '../config/emojis'
|
|||
|
||||
export interface ContentParseOptions {
|
||||
emojis?: Record<string, mastodon.v1.CustomEmoji>
|
||||
hideEmojis?: boolean
|
||||
mentions?: mastodon.v1.StatusMention[]
|
||||
markdown?: boolean
|
||||
replaceUnicodeEmoji?: boolean
|
||||
|
@ -82,6 +83,7 @@ export function parseMastodonHTML(
|
|||
replaceUnicodeEmoji = true,
|
||||
convertMentionLink = false,
|
||||
collapseMentionLink = false,
|
||||
hideEmojis = false,
|
||||
mentions,
|
||||
status,
|
||||
inReplyToStatus,
|
||||
|
@ -110,8 +112,16 @@ export function parseMastodonHTML(
|
|||
...options.astTransforms || [],
|
||||
]
|
||||
|
||||
if (replaceUnicodeEmoji)
|
||||
transforms.push(transformUnicodeEmoji)
|
||||
if (hideEmojis) {
|
||||
transforms.push(removeUnicodeEmoji)
|
||||
transforms.push(removeCustomEmoji(options.emojis ?? {}))
|
||||
}
|
||||
else {
|
||||
if (replaceUnicodeEmoji)
|
||||
transforms.push(transformUnicodeEmoji)
|
||||
|
||||
transforms.push(replaceCustomEmoji(options.emojis ?? {}))
|
||||
}
|
||||
|
||||
if (markdown)
|
||||
transforms.push(transformMarkdown)
|
||||
|
@ -122,8 +132,6 @@ export function parseMastodonHTML(
|
|||
if (convertMentionLink)
|
||||
transforms.push(transformMentionLink)
|
||||
|
||||
transforms.push(replaceCustomEmoji(options.emojis || {}))
|
||||
|
||||
transforms.push(transformParagraphs)
|
||||
|
||||
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) {
|
||||
if (node.type !== TEXT_NODE)
|
||||
return node
|
||||
|
@ -374,6 +401,28 @@ function transformUnicodeEmoji(node: Node) {
|
|||
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 {
|
||||
return (node) => {
|
||||
if (node.type !== TEXT_NODE)
|
||||
|
|
|
@ -10,6 +10,14 @@ import ContentCode from '~/components/content/ContentCode.vue'
|
|||
import ContentMentionGroup from '~/components/content/ContentMentionGroup.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
|
||||
*/
|
||||
|
@ -17,11 +25,18 @@ export function contentToVNode(
|
|||
content: string,
|
||||
options?: ContentParseOptions,
|
||||
): 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)))
|
||||
}
|
||||
|
||||
export function nodeToVNode(node: Node): VNode | string | null {
|
||||
function nodeToVNode(node: Node): VNode | string | null {
|
||||
if (node.type === TEXT_NODE)
|
||||
return node.value
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
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'
|
||||
|
||||
export const confirmDialogChoice = ref<ConfirmDialogChoice>()
|
||||
export const confirmDialogLabel = ref<ConfirmDialogLabel>()
|
||||
export const errorDialogData = ref<ErrorDialogData>()
|
||||
|
||||
export const mediaPreviewList = ref<mastodon.v1.MediaAttachment[]>([])
|
||||
export const mediaPreviewIndex = ref(0)
|
||||
|
@ -22,6 +23,7 @@ export const isEditHistoryDialogOpen = ref(false)
|
|||
export const isPreviewHelpOpen = ref(isFirstVisit.value)
|
||||
export const isCommandPanelOpen = ref(false)
|
||||
export const isConfirmDialogOpen = ref(false)
|
||||
export const isErrorDialogOpen = ref(false)
|
||||
export const isFavouritedBoostedByDialogOpen = ref(false)
|
||||
|
||||
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() {
|
||||
history.back()
|
||||
}
|
||||
|
|
|
@ -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
11
composables/langugage.ts
Normal 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),
|
||||
}))
|
|
@ -7,14 +7,14 @@ export function getDisplayName(account: mastodon.v1.Account, options?: { rich?:
|
|||
return displayName.replace(/:([\w-]+?):/g, '')
|
||||
}
|
||||
|
||||
export function acctToShortHandle(acct: string) {
|
||||
export function accountToShortHandle(acct: string) {
|
||||
return `@${acct.includes('@') ? acct.split('@')[0] : acct}`
|
||||
}
|
||||
|
||||
export function getShortHandle({ acct }: mastodon.v1.Account) {
|
||||
if (!acct)
|
||||
return ''
|
||||
return acctToShortHandle(acct)
|
||||
return accountToShortHandle(acct)
|
||||
}
|
||||
|
||||
export function getServerName(account: mastodon.v1.Account) {
|
||||
|
|
|
@ -47,7 +47,7 @@ export function mastoLogin(masto: ElkMasto, user: Pick<UserLogin, 'server' | 'to
|
|||
setParams({
|
||||
streamingApiUrl: newInstance.urls.streamingApi,
|
||||
})
|
||||
instances.value[server] = newInstance
|
||||
instanceStorage.value[server] = newInstance
|
||||
})
|
||||
|
||||
return instance
|
||||
|
|
|
@ -4,20 +4,34 @@ import type { mastodon } from 'masto'
|
|||
import type { UseDraft } from './statusDrafts'
|
||||
import type { Draft } from '~~/types'
|
||||
|
||||
export const usePublish = (options: {
|
||||
export function usePublish(options: {
|
||||
draftState: UseDraft
|
||||
expanded: Ref<boolean>
|
||||
isUploading: Ref<boolean>
|
||||
initialDraft: Ref<() => Draft>
|
||||
}) => {
|
||||
}) {
|
||||
const { expanded, isUploading, initialDraft } = $(options)
|
||||
let { draft, isEmpty } = $(options.draftState)
|
||||
const { client } = $(useMasto())
|
||||
const settings = useUserSettings()
|
||||
|
||||
const preferredLanguage = $computed(() => (settings.value?.language || 'en').split('-')[0])
|
||||
|
||||
let isSending = $ref(false)
|
||||
const isExpanded = $ref(false)
|
||||
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 isPublishDisabled = $computed(() => {
|
||||
return isEmpty || isUploading || isSending || (draft.attachments.length === 0 && !draft.params.status) || failedMessages.length > 0
|
||||
|
@ -29,19 +43,24 @@ export const usePublish = (options: {
|
|||
}, { deep: true })
|
||||
|
||||
async function publishDraft() {
|
||||
if (isPublishDisabled)
|
||||
return
|
||||
|
||||
let content = htmlToText(draft.params.status || '')
|
||||
if (draft.mentions?.length)
|
||||
content = `${draft.mentions.map(i => `@${i}`).join(' ')} ${content}`
|
||||
|
||||
const payload = {
|
||||
...draft.params,
|
||||
spoilerText: publishSpoilerText,
|
||||
status: content,
|
||||
mediaIds: draft.attachments.map(a => a.id),
|
||||
language: draft.params.language || preferredLanguage,
|
||||
...(isGlitchEdition.value ? { 'content-type': 'text/markdown' } : {}),
|
||||
} as mastodon.v1.CreateStatusParams
|
||||
|
||||
if (process.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.info({
|
||||
raw: draft.params.status,
|
||||
...payload,
|
||||
|
@ -58,6 +77,7 @@ export const usePublish = (options: {
|
|||
let status: mastodon.v1.Status
|
||||
if (!draft.editingStatus)
|
||||
status = await client.v1.statuses.create(payload)
|
||||
|
||||
else
|
||||
status = await client.v1.statuses.update(draft.editingStatus.id, payload)
|
||||
if (draft.params.inReplyToId)
|
||||
|
@ -82,14 +102,15 @@ export const usePublish = (options: {
|
|||
shouldExpanded,
|
||||
isPublishDisabled,
|
||||
failedMessages,
|
||||
|
||||
preferredLanguage,
|
||||
publishSpoilerText,
|
||||
publishDraft,
|
||||
})
|
||||
}
|
||||
|
||||
export type MediaAttachmentUploadError = [filename: string, message: string]
|
||||
|
||||
export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
|
||||
export function useUploadMediaAttachment(draftRef: Ref<Draft>) {
|
||||
const draft = $(draftRef)
|
||||
const { client } = $(useMasto())
|
||||
const { t } = useI18n()
|
||||
|
@ -115,7 +136,7 @@ export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
|
|||
draft.attachments.push(attachment)
|
||||
}
|
||||
catch (e) {
|
||||
// TODO: add some human-readable error message, problem is that masto api will not return response code
|
||||
// TODO: add some human-readable error message, problem is that masto api will not return response code
|
||||
console.error(e)
|
||||
failedAttachments = [...failedAttachments, [file.name, (e as Error).message]]
|
||||
}
|
||||
|
@ -157,9 +178,10 @@ export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
|
|||
return $$({
|
||||
isUploading,
|
||||
isExceedingAttachmentLimit,
|
||||
isOverDropZone,
|
||||
|
||||
failedAttachments,
|
||||
dropZoneRef,
|
||||
isOverDropZone,
|
||||
|
||||
uploadAttachments,
|
||||
pickAttachments,
|
||||
|
|
|
@ -33,7 +33,7 @@ export function getDefaultDraft(options: Partial<Mutable<mastodon.v1.CreateStatu
|
|||
visibility: visibility || 'public',
|
||||
sensitive: sensitive ?? false,
|
||||
spoilerText: spoilerText || '',
|
||||
language: language || getDefaultLanguage(),
|
||||
language: language || '', // auto inferred from current language on posting
|
||||
},
|
||||
mentions,
|
||||
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) {
|
||||
const userId = currentUser.value?.account.id
|
||||
const accountsToMention = new Set<string>()
|
||||
|
@ -84,6 +74,7 @@ export function getReplyDraft(status: mastodon.v1.Status) {
|
|||
inReplyToId: status!.id,
|
||||
visibility: status.visibility,
|
||||
mentions: accountsToMention,
|
||||
language: status.language,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
@ -98,7 +89,6 @@ export const isEmptyDraft = (draft: Draft | null | undefined) => {
|
|||
|
||||
return (text.length === 0)
|
||||
&& attachments.length === 0
|
||||
&& (params.spoilerText || '').length === 0
|
||||
}
|
||||
|
||||
export interface UseDraft {
|
||||
|
|
|
@ -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 = () => {
|
||||
let code = 'en'
|
||||
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: '' }))
|
||||
|
||||
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() {
|
||||
if (!('language' in status))
|
||||
if (!shouldTranslate)
|
||||
return
|
||||
|
||||
if (!translation.text) {
|
||||
|
@ -79,7 +120,7 @@ export function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEd
|
|||
}
|
||||
|
||||
return {
|
||||
enabled: !!useRuntimeConfig().public.translateApi,
|
||||
enabled,
|
||||
toggle,
|
||||
translation,
|
||||
}
|
||||
|
|
|
@ -46,9 +46,9 @@ export const createPushSubscription = async (
|
|||
if (error.code === 11 && error.name === 'InvalidStateError')
|
||||
useError = new PushSubscriptionError('too_many_registrations', 'Too many registrations')
|
||||
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')
|
||||
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()
|
||||
.then(getPushSubscription)
|
||||
|
|
|
@ -25,7 +25,8 @@ export interface CustomEmojisInfo {
|
|||
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 {
|
||||
code: PushSubscriptionErrorCode
|
||||
constructor(code: PushSubscriptionErrorCode, message?: string) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { mastodon } from 'masto'
|
||||
|
||||
import type {
|
||||
CreatePushNotification,
|
||||
PushNotificationPolicy,
|
||||
|
@ -27,19 +28,17 @@ export const usePushManager = () => {
|
|||
const isSupported = $computed(() => supportsPushNotifications)
|
||||
const hiddenNotification = useLocalStorage<PushNotificationRequest>(STORAGE_KEY_NOTIFICATION, {})
|
||||
const configuredPolicy = useLocalStorage<PushNotificationPolicy>(STORAGE_KEY_NOTIFICATION_POLICY, {})
|
||||
const pushNotificationData = ref({
|
||||
follow: currentUser.value?.pushSubscription?.alerts.follow ?? true,
|
||||
favourite: currentUser.value?.pushSubscription?.alerts.favourite ?? true,
|
||||
reblog: currentUser.value?.pushSubscription?.alerts.reblog ?? true,
|
||||
mention: currentUser.value?.pushSubscription?.alerts.mention ?? true,
|
||||
poll: currentUser.value?.pushSubscription?.alerts.poll ?? true,
|
||||
policy: configuredPolicy.value[currentUser.value?.account?.acct ?? ''] ?? 'all',
|
||||
})
|
||||
// don't clone, we're using indexeddb
|
||||
const { history, commit, clear } = useManualRefHistory(pushNotificationData)
|
||||
const pushNotificationData = ref(createRawSettings(
|
||||
currentUser.value?.pushSubscription,
|
||||
configuredPolicy.value[currentUser.value?.account?.acct ?? ''],
|
||||
))
|
||||
const oldPushNotificationData = ref(createRawSettings(
|
||||
currentUser.value?.pushSubscription,
|
||||
configuredPolicy.value[currentUser.value?.account?.acct ?? ''],
|
||||
))
|
||||
const saveEnabled = computed(() => {
|
||||
const current = pushNotificationData.value
|
||||
const previous = history.value?.[0]?.snapshot
|
||||
const previous = oldPushNotificationData.value
|
||||
return current.favourite !== previous.favourite
|
||||
|| current.reblog !== previous.reblog
|
||||
|| current.mention !== previous.mention
|
||||
|
@ -50,14 +49,14 @@ export const usePushManager = () => {
|
|||
|
||||
watch(() => currentUser.value?.pushSubscription, (subscription) => {
|
||||
isSubscribed.value = !!subscription
|
||||
pushNotificationData.value = {
|
||||
follow: subscription?.alerts.follow ?? false,
|
||||
favourite: subscription?.alerts.favourite ?? false,
|
||||
reblog: subscription?.alerts.reblog ?? false,
|
||||
mention: subscription?.alerts.mention ?? false,
|
||||
poll: subscription?.alerts.poll ?? false,
|
||||
policy: configuredPolicy.value[currentUser.value?.account?.acct ?? ''] ?? 'all',
|
||||
}
|
||||
pushNotificationData.value = createRawSettings(
|
||||
subscription,
|
||||
configuredPolicy.value[currentUser.value?.account?.acct ?? ''],
|
||||
)
|
||||
oldPushNotificationData.value = createRawSettings(
|
||||
subscription,
|
||||
configuredPolicy.value[currentUser.value?.account?.acct ?? ''],
|
||||
)
|
||||
}, { immediate: true, flush: 'post' })
|
||||
|
||||
const subscribe = async (
|
||||
|
@ -121,7 +120,15 @@ export const usePushManager = () => {
|
|||
if (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)
|
||||
configuredPolicy.value[currentUser.value!.account.acct ?? ''] = policy
|
||||
|
@ -129,27 +136,25 @@ export const usePushManager = () => {
|
|||
configuredPolicy.value[currentUser.value!.account.acct ?? ''] = pushNotificationData.value.policy
|
||||
|
||||
await nextTick()
|
||||
clear()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const undoChanges = () => {
|
||||
const current = pushNotificationData.value
|
||||
const previous = history.value[0].snapshot
|
||||
current.favourite = previous.favourite
|
||||
current.reblog = previous.reblog
|
||||
current.mention = previous.mention
|
||||
current.follow = previous.follow
|
||||
current.poll = previous.poll
|
||||
current.policy = previous.policy
|
||||
const previous = oldPushNotificationData.value
|
||||
pushNotificationData.value = {
|
||||
favourite: previous.favourite,
|
||||
reblog: previous.reblog,
|
||||
mention: previous.mention,
|
||||
follow: previous.follow,
|
||||
poll: previous.poll,
|
||||
policy: previous.policy,
|
||||
}
|
||||
configuredPolicy.value[currentUser.value!.account.acct ?? ''] = previous.policy
|
||||
commit()
|
||||
clear()
|
||||
}
|
||||
|
||||
const updateSubscription = async () => {
|
||||
if (currentUser.value) {
|
||||
const previous = history.value[0].snapshot
|
||||
const previous = oldPushNotificationData.value
|
||||
// const previous = history.value[0].snapshot
|
||||
const data = {
|
||||
alerts: {
|
||||
follow: pushNotificationData.value.follow,
|
||||
|
@ -190,3 +195,17 @@ export const usePushManager = () => {
|
|||
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',
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,5 @@ import { breakpointsTailwind } from '@vueuse/core'
|
|||
|
||||
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 isExtraLargeScreen = breakpoints.smallerOrEqual('xl')
|
||||
|
|
|
@ -8,14 +8,17 @@ export type OldFontSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|||
export type ColorMode = 'light' | 'dark' | 'system'
|
||||
|
||||
export interface PreferencesSettings {
|
||||
hideAltIndicatorOnPosts: boolean
|
||||
hideBoostCount: boolean
|
||||
hideReplyCount: boolean
|
||||
hideFavoriteCount: boolean
|
||||
hideFollowerCount: boolean
|
||||
hideTranslation: boolean
|
||||
hideUsernameEmojis: boolean
|
||||
hideAccountHoverCard: boolean
|
||||
grayscaleMode: boolean
|
||||
enableAutoplay: boolean
|
||||
enablePinchToZoom: boolean
|
||||
experimentalVirtualScroller: boolean
|
||||
experimentalGitHubCards: boolean
|
||||
experimentalUserPicker: boolean
|
||||
|
@ -26,6 +29,7 @@ export interface UserSettings {
|
|||
colorMode?: ColorMode
|
||||
fontSize: FontSize
|
||||
language: string
|
||||
disabledTranslationLanguages: string[]
|
||||
zenMode: boolean
|
||||
themeColors?: ThemeColors
|
||||
}
|
||||
|
@ -56,20 +60,24 @@ export function getDefaultUserSettings(locales: string[]): UserSettings {
|
|||
return {
|
||||
language: getDefaultLanguage(locales),
|
||||
fontSize: DEFAULT_FONT_SIZE,
|
||||
disabledTranslationLanguages: [],
|
||||
zenMode: false,
|
||||
preferences: {},
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = {
|
||||
hideAltIndicatorOnPosts: false,
|
||||
hideBoostCount: false,
|
||||
hideReplyCount: false,
|
||||
hideFavoriteCount: false,
|
||||
hideFollowerCount: false,
|
||||
hideTranslation: false,
|
||||
hideUsernameEmojis: false,
|
||||
hideAccountHoverCard: false,
|
||||
grayscaleMode: false,
|
||||
enableAutoplay: true,
|
||||
enablePinchToZoom: false,
|
||||
experimentalVirtualScroller: true,
|
||||
experimentalGitHubCards: true,
|
||||
experimentalUserPicker: true,
|
||||
|
|
|
@ -5,6 +5,7 @@ export function setupPageHeader() {
|
|||
const { locale, locales, t } = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
const buildInfo = useBuildInfo()
|
||||
const enablePinchToZoom = usePreferences('enablePinchToZoom')
|
||||
|
||||
const localeMap = (locales.value as LocaleObject[]).reduce((acc, l) => {
|
||||
acc[l.code!] = l.dir ?? 'auto'
|
||||
|
@ -15,7 +16,12 @@ export function setupPageHeader() {
|
|||
htmlAttrs: {
|
||||
lang: () => locale.value,
|
||||
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) => {
|
||||
let titleTemplate = title ?? ''
|
||||
|
||||
|
@ -43,7 +49,7 @@ export function setupPageHeader() {
|
|||
|
||||
return titleTemplate
|
||||
},
|
||||
link: process.client && useRuntimeConfig().public.pwaEnabled
|
||||
link: process.client && useAppConfig().pwaEnabled
|
||||
? () => [{
|
||||
key: 'webmanifest',
|
||||
rel: 'manifest',
|
||||
|
|
77
composables/sign-in.ts
Normal file
77
composables/sign-in.ts
Normal 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 }
|
||||
}
|
|
@ -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[] {
|
||||
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)
|
||||
|
||||
return [...items].filter(isFiltered).filter(isReblogFiltered)
|
||||
|
|
|
@ -7,7 +7,6 @@ import type { UserLogin } from '~/types'
|
|||
import type { Overwrite } from '~/types/utils'
|
||||
import {
|
||||
DEFAULT_POST_CHARS_LIMIT,
|
||||
STORAGE_KEY_CURRENT_USER,
|
||||
STORAGE_KEY_CURRENT_USER_HANDLE,
|
||||
STORAGE_KEY_NODES,
|
||||
STORAGE_KEY_NOTIFICATION,
|
||||
|
@ -44,20 +43,20 @@ const initializeUsers = async (): Promise<Ref<UserLogin[]> | RemovableRef<UserLo
|
|||
}
|
||||
|
||||
const users = await initializeUsers()
|
||||
export const instances = useLocalStorage<Record<string, mastodon.v1.Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true })
|
||||
export const nodes = useLocalStorage<Record<string, any>>(STORAGE_KEY_NODES, {}, { deep: true })
|
||||
const currentUserId = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER, mock ? mock.user.account.id : '')
|
||||
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 : '')
|
||||
export const instanceStorage = useLocalStorage<Record<string, mastodon.v1.Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true })
|
||||
|
||||
export type ElkInstance = Partial<mastodon.v1.Instance> & {
|
||||
uri: string
|
||||
/** support GoToSocial */
|
||||
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>(() => {
|
||||
if (currentUserId.value) {
|
||||
const user = users.value.find(user => user.account?.id === currentUserId.value)
|
||||
if (currentUserHandle.value) {
|
||||
const user = users.value.find(user => user.account?.acct === currentUserHandle.value)
|
||||
if (user)
|
||||
return user
|
||||
}
|
||||
|
@ -66,7 +65,7 @@ export const currentUser = computed<UserLogin | undefined>(() => {
|
|||
})
|
||||
|
||||
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) {
|
||||
return instance.accountDomain || withoutProtocol(instance.uri)
|
||||
|
@ -84,12 +83,12 @@ if (process.client) {
|
|||
const windowReload = () => {
|
||||
document.visibilityState === 'visible' && window.location.reload()
|
||||
}
|
||||
watch(currentUserId, async (id, oldId) => {
|
||||
watch(currentUserHandle, async (handle, oldHandle) => {
|
||||
// when sign in or switch account
|
||||
if (id) {
|
||||
if (id === currentUser.value?.account?.id) {
|
||||
if (handle) {
|
||||
if (handle === currentUser.value?.account?.acct) {
|
||||
// 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 (newUser) {
|
||||
// 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 })
|
||||
}
|
||||
// when sign out
|
||||
else if (oldId) {
|
||||
const oldUser = users.value.find(user => user.account?.id === oldId)
|
||||
else if (oldHandle) {
|
||||
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
|
||||
if (oldUser)
|
||||
window.addEventListener('visibilitychange', windowReload, { capture: true })
|
||||
}
|
||||
}, { 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
|
||||
|
@ -144,12 +137,12 @@ export async function loginTo(masto: ElkMasto, user: Overwrite<UserLogin, { acco
|
|||
|
||||
const account = getUser()?.account
|
||||
if (account)
|
||||
currentUserId.value = account.id
|
||||
currentUserHandle.value = account.acct
|
||||
|
||||
const [me, pushSubscription] = await Promise.all([
|
||||
fetchAccountInfo(client, user.server),
|
||||
// if PWA is not enabled, don't get push subscription
|
||||
useRuntimeConfig().public.pwaEnabled
|
||||
useAppConfig().pwaEnabled
|
||||
// we get 404 response instead empty data
|
||||
? client.v1.webPushSubscriptions.fetch().catch(() => 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) {
|
||||
|
@ -194,7 +187,7 @@ export async function removePushNotificationData(user: UserLogin, fromSWPushMana
|
|||
// clear push notification policy
|
||||
delete useLocalStorage<PushNotificationPolicy>(STORAGE_KEY_NOTIFICATION_POLICY, {}).value[acct]
|
||||
|
||||
const pwaEnabled = useRuntimeConfig().public.pwaEnabled
|
||||
const pwaEnabled = useAppConfig().pwaEnabled
|
||||
const pwa = useNuxtApp().$pwa
|
||||
const registrationError = pwa?.registrationError === true
|
||||
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
|
||||
if (!currentUser.value)
|
||||
return
|
||||
|
@ -253,24 +246,24 @@ export async function signout() {
|
|||
// Clear stale data
|
||||
clearUserLocalStorage()
|
||||
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 removePushNotificationData(currentUser.value)
|
||||
|
||||
currentUserId.value = ''
|
||||
currentUserHandle.value = ''
|
||||
// Remove the current user from the users
|
||||
users.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 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('/')
|
||||
|
||||
loginTo(masto, currentUser.value)
|
||||
loginTo(masto, currentUser.value || { server: publicServer.value })
|
||||
}
|
||||
|
||||
export function checkLogin() {
|
||||
|
|
|
@ -7,7 +7,6 @@ export const STORAGE_KEY_DRAFTS = 'elk-drafts'
|
|||
export const STORAGE_KEY_USERS = 'elk-users'
|
||||
export const STORAGE_KEY_SERVERS = 'elk-servers'
|
||||
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_NOTIFY_TAB = 'elk-notify-tab'
|
||||
export const STORAGE_KEY_FIRST_VISIT = 'elk-first-visit'
|
||||
|
|
|
@ -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_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. |
|
||||
| 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. |
|
||||
|
||||
That's it! All that's left to do is...
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"preview": "nuxi preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt-themes/docus": "^1.4.7",
|
||||
"nuxt": "^3.1.0"
|
||||
"@nuxt-themes/docus": "^1.6.1",
|
||||
"nuxt": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,16 +15,16 @@ const isGrayscale = usePreferences('grayscaleMode')
|
|||
</script>
|
||||
|
||||
<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]">
|
||||
<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>
|
||||
<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 />
|
||||
<NavSide command />
|
||||
<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>
|
||||
<UserSignInEntry v-if="!currentUser" />
|
||||
</div>
|
||||
|
@ -59,7 +59,7 @@ const isGrayscale = usePreferences('grayscaleMode')
|
|||
<NavBottom v-if="isHydrated" sm:hidden />
|
||||
</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>
|
||||
<slot name="right">
|
||||
<div flex-auto />
|
||||
|
|
118
locales/ar.json
118
locales/ar.json
|
@ -28,6 +28,8 @@
|
|||
"muted_users": "المستخدمون المكتومون",
|
||||
"muting": "قُمت بكتم",
|
||||
"mutuals": "المتبادلون",
|
||||
"notifications_on_post_disable": "التوقف عن إشعاري عندما ينشر {username}",
|
||||
"notifications_on_post_enable": "إشعاري عندما ينشر {username}",
|
||||
"pinned": "المثبتة",
|
||||
"posts": "المنشورات",
|
||||
"posts_count": "{0} منشورات|{0} منشور|{0} منشورين|{0} منشورات|{0} منشور|{0} منشور",
|
||||
|
@ -35,7 +37,9 @@
|
|||
"profile_unavailable": "حساب غير متوفر",
|
||||
"unblock": "إلغاء حظر",
|
||||
"unfollow": "إلغاء متابعة",
|
||||
"unmute": "إلغاء كتم"
|
||||
"unmute": "إلغاء كتم",
|
||||
"view_other_followers": "قد لا يتم عرض المتابعين من خوادم مختلفين عن الخاص بك.",
|
||||
"view_other_following": "قد لا يتم عرض من تتابع من خوادم مختلفين عن الخاص بك."
|
||||
},
|
||||
"action": {
|
||||
"apply": "تطبيق",
|
||||
|
@ -44,6 +48,7 @@
|
|||
"boost": "إعادة نشر",
|
||||
"boost_count": "{0}",
|
||||
"boosted": "أعيد نشرها",
|
||||
"clear_publish_failed": "مسح أخطاء النشر",
|
||||
"clear_upload_failed": "مسح أخطاء تحميل الملف",
|
||||
"close": "أغلق",
|
||||
"compose": "منشور جديد",
|
||||
|
@ -66,7 +71,7 @@
|
|||
"switch_account": "تغيير الحساب",
|
||||
"vote": "تصويت"
|
||||
},
|
||||
"app_desc_short": "منصة تواصل Mastodon رشيقة",
|
||||
"app_desc_short": "منصة تواصل ماستودون رشيقة",
|
||||
"app_logo": "Elk شعار",
|
||||
"app_name": "Elk",
|
||||
"attachment": {
|
||||
|
@ -101,9 +106,52 @@
|
|||
"draft_title": "مسودة {0}",
|
||||
"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": {
|
||||
"with": "مع"
|
||||
},
|
||||
"custom_cards": {
|
||||
"stackblitz": {
|
||||
"lines": "الخطوط {0}",
|
||||
"open": "افتح",
|
||||
"snippet_from": "مقتطف من {0}"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"account_not_found": "حساب {0} غير موجود",
|
||||
"explore-list-empty": "لا توجد مشاركات شائعة الآن. تحقق مرة أخرى لاحقًا!",
|
||||
|
@ -113,6 +161,12 @@
|
|||
"unsupported_file_format": "لا يمكن تحميل هذا النوع من الملفات"
|
||||
},
|
||||
"help": {
|
||||
"build_preview": {
|
||||
"desc1": "أنت تشاهد حاليًا إصدار معاينة من Elk عامة - {0}.",
|
||||
"desc2": "قد يحتوي على تغييرات لم تتم مراجعتها أو حتى ضارة.",
|
||||
"desc3": "لا تسجل الدخول بحسابك الحقيقي.",
|
||||
"title": "معاينة النشر"
|
||||
},
|
||||
"desc_highlight": "توقع بعض الأخطاء والميزات المفقودة هنا وهناك.",
|
||||
"desc_para1": "نشكرك على اهتمامك بتجربة Elk ، عميل ماستدون العام!",
|
||||
"desc_para2": "نحن نعمل بجد على التطوير وتحسينه بمرور الوقت. وسندعوك قريبًا للانضمام إلى القوة بمجرد أن نجعلها مفتوحة المصدر قريبًا!",
|
||||
|
@ -125,10 +179,16 @@
|
|||
"language": {
|
||||
"search": "بحث"
|
||||
},
|
||||
"list": {
|
||||
"add_account": "إضافة حساب إلى القائمة",
|
||||
"modify_account": "تعديل القوائم مع الحساب",
|
||||
"remove_account": "إزالة الحساب من القائمة"
|
||||
},
|
||||
"menu": {
|
||||
"block_account": "حظر {0}",
|
||||
"block_domain": "حظر المجال {0}",
|
||||
"copy_link_to_post": "انسخ الرابط إلى هذا المنشور",
|
||||
"copy_original_link_to_post": "انسخ الرابط الأصلي لهذا المنشور",
|
||||
"delete": "حذف",
|
||||
"delete_and_redraft": "حذف وإعادة صياغة",
|
||||
"delete_confirm": {
|
||||
|
@ -165,14 +225,18 @@
|
|||
"blocked_users": "المستخدمين المحظورين",
|
||||
"bookmarks": "العلامات المرجعية",
|
||||
"built_at": "Built {0}",
|
||||
"compose": "تأليف",
|
||||
"conversations": "المحادثات",
|
||||
"explore": "استكشف",
|
||||
"favourites": "المفضلة",
|
||||
"federated": "الفديرالية",
|
||||
"home": "الرئيسيّة",
|
||||
"list": "قائمة",
|
||||
"lists": "القوائم",
|
||||
"local": "المحلي",
|
||||
"muted_users": "المستخدمون المكتموصين",
|
||||
"notifications": "التنبيهات",
|
||||
"privacy": "خصوصية",
|
||||
"profile": "الصفحة التعريفية",
|
||||
"search": "البحث",
|
||||
"select_feature_flags": "تبديل علامات الميزات",
|
||||
|
@ -202,27 +266,29 @@
|
|||
},
|
||||
"pwa": {
|
||||
"dismiss": "تجاهل",
|
||||
"title": "يتوفر تحديث Elk الجديد",
|
||||
"install": "تحميل",
|
||||
"install_title": "تحميل Elk",
|
||||
"title": "يتوفر تحديث Elk جديد",
|
||||
"update": "تحديث",
|
||||
"update_available_short": "تحديث Elk",
|
||||
"webmanifest": {
|
||||
"canary": {
|
||||
"description": "نسخة ويب رشيقة ل Mastodon (النسخة الإنشائية)",
|
||||
"description": "نسخة ويب رشيقة لماستودون (النسخة الإنشائية)",
|
||||
"name": "Elk (النسخة الإنشائية)",
|
||||
"short_name": "Elk (النسخة الإنشائية)"
|
||||
},
|
||||
"dev": {
|
||||
"description": "نسخة ويب رشيقة ل Mastodon (النسخة التطويرية)",
|
||||
"description": "نسخة ويب رشيقة لماستودون (النسخة التطويرية)",
|
||||
"name": "Elk (النسخة التطويرية)",
|
||||
"short_name": "Elk (النسخة التطويرية)"
|
||||
},
|
||||
"preview": {
|
||||
"description": "نسخة ويب رشيقة ل Mastodon (معاينة)",
|
||||
"description": "نسخة ويب رشيقة لماستودون (معاينة)",
|
||||
"name": "Elk (معاينة)",
|
||||
"short_name": "Elk (معاينة)"
|
||||
},
|
||||
"release": {
|
||||
"description": "نسخة ويب رشيقة ل Mastodon",
|
||||
"description": "نسخة ويب رشيقة لماستودون",
|
||||
"name": "Elk",
|
||||
"short_name": "Elk"
|
||||
}
|
||||
|
@ -241,10 +307,11 @@
|
|||
"sponsors": "الرعاة",
|
||||
"sponsors_body_1": "تم تمويل Elk من قبل الشركات والأفراد التاليين:",
|
||||
"sponsors_body_2": "وكذا من قبل الشركات التالية:",
|
||||
"sponsors_body_3": "إذا كنت تستمتع بإستخدام Elk، فنحن نشجعك على التبرع لدعم المشروع."
|
||||
"sponsors_body_3": "إذا كنت تستمتع بإستخدام Elk، فنحن نشجعك على التبرع لدعم المشروع.",
|
||||
"version": "الإصدار"
|
||||
},
|
||||
"account_settings": {
|
||||
"description": "قم بتحرير إعدادات حسابك في موقع Mastodon الأصلي",
|
||||
"description": "قم بتحرير إعدادات حسابك في موقع ماستودون الأصل",
|
||||
"label": "إعدادت الحساب"
|
||||
},
|
||||
"feature_flags": {
|
||||
|
@ -260,7 +327,8 @@
|
|||
"font_size": "حجم الخط",
|
||||
"label": "واجهه المستخدم",
|
||||
"light_mode": "وضع الضوء",
|
||||
"system_mode": "النظام"
|
||||
"system_mode": "النظام",
|
||||
"theme_color": "وضع المظهر"
|
||||
},
|
||||
"language": {
|
||||
"display_language": "اللغة المعروضة",
|
||||
|
@ -317,7 +385,19 @@
|
|||
},
|
||||
"notifications_settings": "التنبيهات",
|
||||
"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": {
|
||||
"appearance": {
|
||||
|
@ -340,22 +420,20 @@
|
|||
"export": "تصدير معلومات المستخدم",
|
||||
"import": "استيراد معلومات المستخدم",
|
||||
"label": "المستخدمون المسجلون"
|
||||
},
|
||||
"wellness": {
|
||||
"feature": {
|
||||
"hide_boost_count": "إخفاء عدد المشاركات",
|
||||
"hide_favorite_count": "إخفاء عدد المفضلة",
|
||||
"hide_follower_count": "إخفاء عدد المتابعين"
|
||||
},
|
||||
"label": "الصحة العامة"
|
||||
}
|
||||
},
|
||||
"share-target": {
|
||||
"description": "يمكن تحديث Elk بحيث يمكنك مشاركة المحتوى من التطبيقات الأخرى ، ما عليك سوى تثبيت Elk على جهازك أو الكمبيوتر وتسجيل الدخول.",
|
||||
"hint": "لمشاركة المحتوى مع Elk ، يجب تثبيت Elk ويجب تسجيل الدخول.",
|
||||
"title": "شارك مع Elk"
|
||||
},
|
||||
"state": {
|
||||
"attachments_exceed_server_limit": "تجاوز عدد المرفقات الحد الأقصى لكل منشور.",
|
||||
"attachments_limit_error": "تجاوز الحد لل منشور",
|
||||
"edited": "(معدل)",
|
||||
"editing": "تعديل",
|
||||
"loading": "جاري التحميل ...",
|
||||
"publish_failed": "فشل النشر",
|
||||
"publishing": "قيد النشر",
|
||||
"upload_failed": "التحميل فشل",
|
||||
"uploading": "جاري التحميل ..."
|
||||
|
@ -390,6 +468,7 @@
|
|||
"edited": "تم تعديله في {0}"
|
||||
},
|
||||
"tab": {
|
||||
"accounts": "الحسابات",
|
||||
"for_you": "مصممة لك",
|
||||
"hashtags": "هاشتاغ",
|
||||
"media": "الصور/الفيديو",
|
||||
|
@ -455,6 +534,7 @@
|
|||
"explore_links_intro": "يتم التحدث عن هذه القصص الإخبارية من قبل الأشخاص الموجودين على هذه الشبكة وغيرها من الشبكات اللامركزية في الوقت الحالي",
|
||||
"explore_posts_intro": "تكتسب هذه المنشورات الكثير من النشاط على الشبكة وغيرها من الشبكات اللامركزية في الوقت الحالي",
|
||||
"explore_tags_intro": "تكتسب هذه الهاشتاغ الكثير من النشاط بين الأشخاص على هذه الشبكة وغيرها من الشبكات اللامركزية في الوقت الحالي",
|
||||
"publish_failed": "أغلق الرسائل الفاشلة أعلى المحرر لإعادة نشر المنشورات",
|
||||
"toggle_code_block": "تبديل كتلة التعليمات البرمجية"
|
||||
},
|
||||
"user": {
|
||||
|
|
|
@ -270,7 +270,14 @@
|
|||
},
|
||||
"language": {
|
||||
"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": {
|
||||
"label": "Benachrichtigungen",
|
||||
|
@ -328,7 +335,7 @@
|
|||
"hide_boost_count": "Boost-Zähler ausblenden",
|
||||
"hide_favorite_count": "Favoritenzahl ausblenden",
|
||||
"hide_follower_count": "Anzahl der Follower ausblenden",
|
||||
"hide_translation": "Übersetzungen ausblenden",
|
||||
"hide_translation": "Übersetzungen komplett ausblenden",
|
||||
"label": "Einstellungen",
|
||||
"title": "Experimentelle Funktionen",
|
||||
"user_picker": "Benutzerauswahl",
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
"posts_count": "{0} Posts|{0} Post|{0} Posts",
|
||||
"profile_description": "{0}'s profile header",
|
||||
"profile_unavailable": "Profile unavailable",
|
||||
"request_follow": "Request to follow",
|
||||
"unblock": "Unblock",
|
||||
"unfollow": "Unfollow",
|
||||
"unmute": "Unmute",
|
||||
|
@ -68,6 +69,7 @@
|
|||
"save": "Save",
|
||||
"save_changes": "Save changes",
|
||||
"sign_in": "Sign in",
|
||||
"sign_in_to": "Sign in to {0}",
|
||||
"switch_account": "Switch account",
|
||||
"vote": "Vote"
|
||||
},
|
||||
|
@ -116,6 +118,11 @@
|
|||
"cancel": "No",
|
||||
"confirm": "Yes"
|
||||
},
|
||||
"delete_list": {
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Delete",
|
||||
"title": "Are you sure you want to delete the \"{0}\" list?"
|
||||
},
|
||||
"delete_posts": {
|
||||
"cancel": "Cancel",
|
||||
"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_para5": "reach out to us on GitHub",
|
||||
"desc_para6": "and get involved.",
|
||||
"footer_team": "The Elk Team",
|
||||
"title": "Elk is in Preview!"
|
||||
},
|
||||
"language": {
|
||||
|
@ -176,8 +184,19 @@
|
|||
},
|
||||
"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",
|
||||
"remove_account": "Remove account from list"
|
||||
"remove_account": "Remove account from list",
|
||||
"save": "Save changes"
|
||||
},
|
||||
"menu": {
|
||||
"block_account": "Block {0}",
|
||||
|
@ -290,6 +309,7 @@
|
|||
},
|
||||
"settings": {
|
||||
"about": {
|
||||
"built_at": "Built",
|
||||
"label": "About",
|
||||
"meet_the_team": "Meet the team",
|
||||
"sponsor_action": "Sponsor us",
|
||||
|
@ -316,7 +336,14 @@
|
|||
},
|
||||
"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": {
|
||||
"label": "Notifications",
|
||||
|
@ -345,10 +372,13 @@
|
|||
"save_settings": "Save settings",
|
||||
"subscription_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.",
|
||||
"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.",
|
||||
"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",
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"show_btn": "Go to notifications settings"
|
||||
"show_btn": "Go to notifications settings",
|
||||
"under_construction": "Under construction"
|
||||
},
|
||||
"notifications_settings": "Notifications",
|
||||
"preferences": {
|
||||
"enable_autoplay": "Enable Autoplay",
|
||||
"enable_pinch_to_zoom": "Enable pinch to zoom",
|
||||
"github_cards": "GitHub Cards",
|
||||
"grayscale_mode": "Grayscale mode",
|
||||
"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_favorite_count": "Hide favorite count",
|
||||
"hide_follower_count": "Hide follower count",
|
||||
"hide_reply_count": "Hide reply count",
|
||||
"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",
|
||||
"title": "Experimental Features",
|
||||
"user_picker": "User Picker",
|
||||
"virtual_scroll": "Virtual Scrolling"
|
||||
"virtual_scroll": "Virtual Scrolling",
|
||||
"wellbeing": "Wellbeing"
|
||||
},
|
||||
"profile": {
|
||||
"appearance": {
|
||||
|
@ -430,8 +466,10 @@
|
|||
"filter_removed_phrase": "Removed by filter",
|
||||
"filter_show_anyway": "Show anyway",
|
||||
"img_alt": {
|
||||
"ALT": "ALT",
|
||||
"desc": "Description",
|
||||
"dismiss": "Dismiss"
|
||||
"dismiss": "Dismiss",
|
||||
"read": "Read {0} description"
|
||||
},
|
||||
"poll": {
|
||||
"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_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.",
|
||||
"open_editor_tools": "Editor tools",
|
||||
"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": {
|
||||
"add_existing": "Add an existing account",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
},
|
||||
"account": {
|
||||
"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_users": "Usuarios bloqueados",
|
||||
"blocking": "Bloqueado",
|
||||
|
@ -35,6 +35,7 @@
|
|||
"posts_count": "{0} Publicaciones|{0} Publicación|{0} Publicaciones",
|
||||
"profile_description": "Encabezado del perfil de {0}",
|
||||
"profile_unavailable": "Perfil no disponible",
|
||||
"request_follow": "Solicitud para seguirte",
|
||||
"unblock": "Desbloquear",
|
||||
"unfollow": "Dejar de seguir",
|
||||
"unmute": "Dejar de silenciar",
|
||||
|
@ -68,6 +69,7 @@
|
|||
"save": "Guardar",
|
||||
"save_changes": "Guardar cambios",
|
||||
"sign_in": "Iniciar sesión",
|
||||
"sign_in_to": "Iniciar sesión en {0}",
|
||||
"switch_account": "Cambiar cuenta",
|
||||
"vote": "Votar"
|
||||
},
|
||||
|
@ -76,7 +78,7 @@
|
|||
"app_name": "Elk",
|
||||
"attachment": {
|
||||
"edit_title": "Descripción",
|
||||
"remove_label": "Eliminar archivo adjunto"
|
||||
"remove_label": "Eliminar fichero adjunto"
|
||||
},
|
||||
"command": {
|
||||
"activate": "Activar",
|
||||
|
@ -116,6 +118,11 @@
|
|||
"cancel": "No",
|
||||
"confirm": "Si"
|
||||
},
|
||||
"delete_list": {
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Eliminar",
|
||||
"title": "¿Está seguro de querer eliminar la lista \"{0}\"?"
|
||||
},
|
||||
"delete_posts": {
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Eliminar",
|
||||
|
@ -150,10 +157,10 @@
|
|||
"error": {
|
||||
"account_not_found": "No se encontró la cuenta {0}",
|
||||
"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.",
|
||||
"status_not_found": "Estado no encontrado",
|
||||
"unsupported_file_format": "Tipo de archivo no soportado"
|
||||
"status_not_found": "Publicación no encontrada",
|
||||
"unsupported_file_format": "Tipo de fichero no soportado"
|
||||
},
|
||||
"help": {
|
||||
"build_preview": {
|
||||
|
@ -164,11 +171,12 @@
|
|||
},
|
||||
"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_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_para3": "Para ayudar a impulsar el desarrollo, puedes patrocinar a los miembros de nuestro equipo con los enlaces a continuación.",
|
||||
"desc_para4": "Antes de eso, si te gustaría ayudar probando, dando opinión o contribuyendo,",
|
||||
"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. ¡Esperamos que estés disfrutando Elk!",
|
||||
"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_para6": "para participar.",
|
||||
"footer_team": "El equipo de desarrollo de Elk",
|
||||
"title": "¡Elk está en Vista Previa!"
|
||||
},
|
||||
"language": {
|
||||
|
@ -176,8 +184,19 @@
|
|||
},
|
||||
"list": {
|
||||
"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",
|
||||
"remove_account": "Eliminar cuenta de la lista"
|
||||
"remove_account": "Eliminar cuenta de la lista",
|
||||
"save": "Guardar"
|
||||
},
|
||||
"menu": {
|
||||
"block_account": "Bloquear a {0}",
|
||||
|
@ -245,7 +264,7 @@
|
|||
"reblogged_post": "retooteó tu publicación",
|
||||
"request_to_follow": "ha solicitado seguirte",
|
||||
"signed_up": "registrado",
|
||||
"update_status": "ha actualizado su estado"
|
||||
"update_status": "ha actualizado su publicación"
|
||||
},
|
||||
"placeholder": {
|
||||
"content_warning": "Escribe tu advertencia aquí",
|
||||
|
@ -290,6 +309,7 @@
|
|||
},
|
||||
"settings": {
|
||||
"about": {
|
||||
"built_at": "Fecha de compilación",
|
||||
"label": "Acerca de",
|
||||
"meet_the_team": "Conoce al equipo",
|
||||
"sponsor_action": "Patrocinar",
|
||||
|
@ -301,14 +321,14 @@
|
|||
"version": "Versión"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"interface": {
|
||||
"color_mode": "Modos de color",
|
||||
"dark_mode": "Modo oscuro",
|
||||
"default": " (por defecto)",
|
||||
"font_size": "Tamaño de Letra",
|
||||
"font_size": "Tamaño de letra",
|
||||
"label": "Interfaz",
|
||||
"light_mode": "Modo claro",
|
||||
"system_mode": "Sistema",
|
||||
|
@ -316,7 +336,14 @@
|
|||
},
|
||||
"language": {
|
||||
"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": {
|
||||
"label": "Notificaciones",
|
||||
|
@ -332,7 +359,7 @@
|
|||
"reblog": "Retooteo de tus publicaciones",
|
||||
"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{'!'}",
|
||||
"label": "Ajustes de notificaciones push",
|
||||
"policy": {
|
||||
|
@ -345,10 +372,13 @@
|
|||
"save_settings": "Guardar cambios",
|
||||
"subscription_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.",
|
||||
"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.",
|
||||
"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",
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"show_btn": "Ir a ajustes de notificaciones"
|
||||
"show_btn": "Ir a ajustes de notificaciones",
|
||||
"under_construction": "En construcción"
|
||||
},
|
||||
"notifications_settings": "Notificaciones",
|
||||
"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",
|
||||
"grayscale_mode": "Modo escala de grises",
|
||||
"hide_account_hover_card": "Ocultar tarjeta flotante de cuenta",
|
||||
"hide_boost_count": "Ocultar contador de retoots",
|
||||
"hide_favorite_count": "Ocultar contador de favoritas",
|
||||
"hide_follower_count": "Ocultar contador de seguidores",
|
||||
"hide_reply_count": "Ocultar contador de respuestas",
|
||||
"hide_favorite_count": "Ocultar número de publicaciones favoritas",
|
||||
"hide_follower_count": "Ocultar número de seguidores",
|
||||
"hide_reply_count": "Ocultar número de respuestas",
|
||||
"hide_translation": "Ocultar traducción",
|
||||
"hide_username_emojis": "Ocultar emojis en el nombre de usuario",
|
||||
"label": "Preferencias",
|
||||
"title": "Funcionalidades experimentales",
|
||||
"user_picker": "Selector de usuarios",
|
||||
|
@ -395,7 +428,7 @@
|
|||
},
|
||||
"featured_tags": {
|
||||
"description": "Las personas pueden navegar por tus publicaciones públicas con estas etiquetas.",
|
||||
"label": "Etiquetas destacados"
|
||||
"label": "Etiquetas destacadas"
|
||||
},
|
||||
"label": "Perfil"
|
||||
},
|
||||
|
@ -510,7 +543,7 @@
|
|||
},
|
||||
"tooltip": {
|
||||
"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_publishable_content": "Publicar 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_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.",
|
||||
"open_editor_tools": "Herramientas de edición",
|
||||
"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": {
|
||||
"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_notice_title": "Viendo información pública de {0}",
|
||||
"sign_out_account": "Cerrar sesión {0}",
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
},
|
||||
"account": {
|
||||
"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_users": "Utilisateur·ice·s bloqué·e·s",
|
||||
"blocked_users": "Comptes bloqués",
|
||||
"blocking": "Bloqué·e",
|
||||
"bot": "Automatisé",
|
||||
"favourites": "Aimés",
|
||||
|
@ -21,11 +21,11 @@
|
|||
"followers_count": "{0} abonné·e|{0} abonné·e|{0} abonné·e·s",
|
||||
"following": "Suivi·e",
|
||||
"following_count": "{0} abonnements",
|
||||
"follows_you": "Vous suit",
|
||||
"follows_you": "@:account.follow_back",
|
||||
"go_to_profile": "Aller à son profil",
|
||||
"joined": "a rejoint",
|
||||
"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",
|
||||
"mutuals": "Abonné·e·s",
|
||||
"notifications_on_post_disable": "Arrêtez de me notifier lorsque {username} publie",
|
||||
|
@ -39,8 +39,8 @@
|
|||
"unblock": "Débloquer",
|
||||
"unfollow": "Ne plus suivre",
|
||||
"unmute": "Réafficher",
|
||||
"view_other_followers": "Les abonné·e·s d'autres instances peuvent ne pas être affiché·e·s.",
|
||||
"view_other_following": "Les suivis d'autres instances peuvent ne pas être affichés."
|
||||
"view_other_followers": "Les comptes abonnés 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": {
|
||||
"apply": "Appliquer",
|
||||
|
@ -118,10 +118,15 @@
|
|||
"confirm": "Oui",
|
||||
"title": "Êtes-vous sûr·e ?"
|
||||
},
|
||||
"delete_list": {
|
||||
"cancel": "Annuler",
|
||||
"confirm": "Supprimer",
|
||||
"title": "Voulez-vous vraiment supprimer la liste \"{0}\" ?"
|
||||
},
|
||||
"delete_posts": {
|
||||
"cancel": "Annuler",
|
||||
"confirm": "Supprimer",
|
||||
"title": "Certain·e de vouloir supprimer ce message ?"
|
||||
"title": "Voulez-vous vraiment supprimer ce message ?"
|
||||
},
|
||||
"mute_account": {
|
||||
"cancel": "Annuler",
|
||||
|
@ -171,11 +176,28 @@
|
|||
"desc_para4": "Avant cela, si vous voulez aider à tester, donner des retours ou contribuer",
|
||||
"desc_para5": "contactez nous sur GitHub",
|
||||
"desc_para6": "et rejoignez l'aventure.",
|
||||
"footer_team": "L'équipe Elk",
|
||||
"title": "Elk est en mode Aperçu !"
|
||||
},
|
||||
"language": {
|
||||
"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": {
|
||||
"block_account": "Bloquer {0}",
|
||||
"block_domain": "Bloquer le domaine {0}",
|
||||
|
@ -212,7 +234,7 @@
|
|||
"nav": {
|
||||
"back": "Retourner à la page précédente",
|
||||
"blocked_domains": "Domaines bloqués",
|
||||
"blocked_users": "Utilisateur·ice·s bloqué·e·s",
|
||||
"blocked_users": "Comptes bloqués",
|
||||
"bookmarks": "Marque-pages",
|
||||
"built_at": "Dernière compilation {0}",
|
||||
"compose": "Composer",
|
||||
|
@ -221,9 +243,12 @@
|
|||
"favourites": "Favoris",
|
||||
"federated": "Fédérés",
|
||||
"home": "Accueil",
|
||||
"list": "Liste",
|
||||
"lists": "Listes",
|
||||
"local": "Local",
|
||||
"muted_users": "Utilisateur·ice·s masqué·e·s",
|
||||
"muted_users": "Comptes masqués",
|
||||
"notifications": "Notifications",
|
||||
"privacy": "Données privées",
|
||||
"profile": "Profil",
|
||||
"search": "Rechercher",
|
||||
"select_feature_flags": "Activer/Désactiver Feature Flags",
|
||||
|
@ -287,11 +312,12 @@
|
|||
},
|
||||
"settings": {
|
||||
"about": {
|
||||
"built_at": "Dernière compilation",
|
||||
"label": "À propos",
|
||||
"meet_the_team": "Rencontrez l'équipe",
|
||||
"sponsor_action": "Soutenez-nous",
|
||||
"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_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 :",
|
||||
|
@ -313,7 +339,14 @@
|
|||
},
|
||||
"language": {
|
||||
"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": {
|
||||
"label": "Notifications",
|
||||
|
@ -323,7 +356,7 @@
|
|||
"push_notifications": {
|
||||
"alerts": {
|
||||
"favourite": "Messages aimés",
|
||||
"follow": "Nouveaux abonné·e·s",
|
||||
"follow": "Nouveaux abonnés",
|
||||
"mention": "Mentions",
|
||||
"poll": "Sondages",
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"preferences": {
|
||||
"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",
|
||||
"hide_account_hover_card": "Masquer la carte de survol du compte",
|
||||
"hide_boost_count": "Cacher les compteurs de partages",
|
||||
"hide_favorite_count": "Cacher les compteurs de favoris",
|
||||
"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",
|
||||
"title": "Fonctionnalités expérimentales",
|
||||
"user_picker": "User Picker",
|
||||
|
@ -380,7 +418,7 @@
|
|||
"profile": {
|
||||
"appearance": {
|
||||
"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",
|
||||
"label": "Apparence",
|
||||
"profile_metadata": "Métadonnées de profil",
|
||||
|
@ -395,9 +433,9 @@
|
|||
},
|
||||
"select_a_settings": "Sélectionner un paramètre",
|
||||
"users": {
|
||||
"export": "Exporter les tokens d'utilisateur·ice",
|
||||
"import": "Importer des tokens d'utilisateur·ice",
|
||||
"label": "Utilisateur·ice·s connecté·e·s"
|
||||
"export": "Exporter les tokens de compte",
|
||||
"import": "Importer des tokens de compte",
|
||||
"label": "Comptes connectés"
|
||||
}
|
||||
},
|
||||
"share-target": {
|
||||
|
@ -421,10 +459,13 @@
|
|||
"edited": "Edité {0}",
|
||||
"favourited_by": "Aimé par",
|
||||
"filter_hidden_phrase": "Filtré par",
|
||||
"filter_removed_phrase": "Supprimé par le filtre",
|
||||
"filter_show_anyway": "Montrer coûte que coûte",
|
||||
"img_alt": {
|
||||
"ALT": "ALT",
|
||||
"desc": "Description",
|
||||
"dismiss": "Fermer"
|
||||
"dismiss": "Fermer",
|
||||
"read": "Lire la description de {0}"
|
||||
},
|
||||
"poll": {
|
||||
"count": "{0} votes",
|
||||
|
@ -445,8 +486,10 @@
|
|||
"edited": "a édité {0}"
|
||||
},
|
||||
"tab": {
|
||||
"accounts": "Comptes",
|
||||
"for_you": "Pour vous",
|
||||
"hashtags": "Hashtags",
|
||||
"list": "Liste",
|
||||
"media": "Média",
|
||||
"news": "Actualités",
|
||||
"notifications_all": "Tout",
|
||||
|
|
|
@ -116,6 +116,11 @@
|
|||
"cancel": "いいえ",
|
||||
"confirm": "はい"
|
||||
},
|
||||
"delete_list": {
|
||||
"cancel": "キャンセル",
|
||||
"confirm": "削除",
|
||||
"title": "リスト \"{0}\" を本当に削除したいですか?"
|
||||
},
|
||||
"delete_posts": {
|
||||
"cancel": "キャンセル",
|
||||
"confirm": "削除",
|
||||
|
@ -169,6 +174,7 @@
|
|||
"desc_para4": "Elkはオープンソースです。テスト、フィードバックの提供、コントリビューションで開発を助けたい場合は",
|
||||
"desc_para5": "GitHubで連絡をとり",
|
||||
"desc_para6": "参加してください。",
|
||||
"footer_team": "Elk チーム",
|
||||
"title": "Elkはプレビュー版です!"
|
||||
},
|
||||
"language": {
|
||||
|
@ -176,8 +182,14 @@
|
|||
},
|
||||
"list": {
|
||||
"add_account": "アカウントをリストに追加",
|
||||
"cancel_edit": "編集をキャンセル",
|
||||
"create": "作成",
|
||||
"delete": "リストを削除",
|
||||
"edit": "リストを編集",
|
||||
"list_title_placeholder": "リストのタイトル",
|
||||
"modify_account": "このアカウントでリストを編集",
|
||||
"remove_account": "アカウントをリストから削除"
|
||||
"remove_account": "アカウントをリストから削除",
|
||||
"save": "変更を保存"
|
||||
},
|
||||
"menu": {
|
||||
"block_account": "{0}さんをブロック",
|
||||
|
@ -290,6 +302,7 @@
|
|||
},
|
||||
"settings": {
|
||||
"about": {
|
||||
"built_at": "ビルド",
|
||||
"label": "Elkについて",
|
||||
"meet_the_team": "チーム紹介",
|
||||
"sponsor_action": "サポートしてください",
|
||||
|
@ -316,7 +329,14 @@
|
|||
},
|
||||
"language": {
|
||||
"display_language": "表示言語",
|
||||
"label": "言語"
|
||||
"label": "言語",
|
||||
"translations": {
|
||||
"add": "追加",
|
||||
"choose_language": "言語を選択",
|
||||
"heading": "翻訳",
|
||||
"hide_specific": "特定の翻訳を隠す",
|
||||
"remove": "削除"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"label": "通知",
|
||||
|
@ -365,11 +385,13 @@
|
|||
"re_auth": "あなたのサーバーはプッシュ通知をサポートしていないようです。もしサインアウトとサインインをやり直してもこのメッセージがまだ表示される場合は、サーバー管理者に連絡してください。"
|
||||
}
|
||||
},
|
||||
"show_btn": "通知設定に移動"
|
||||
"show_btn": "通知設定に移動",
|
||||
"under_construction": "工事中"
|
||||
},
|
||||
"notifications_settings": "通知",
|
||||
"preferences": {
|
||||
"enable_autoplay": "自動再生を有効化",
|
||||
"enable_pinch_to_zoom": "ピンチによるズームを有効にする",
|
||||
"github_cards": "GitHubカード",
|
||||
"grayscale_mode": "グレースケールモード",
|
||||
"hide_account_hover_card": "アカウントのホバーカードを隠す",
|
||||
|
@ -430,8 +452,10 @@
|
|||
"filter_removed_phrase": "フィルターにより削除",
|
||||
"filter_show_anyway": "とにかく表示",
|
||||
"img_alt": {
|
||||
"ALT": "ALT",
|
||||
"desc": "説明文",
|
||||
"dismiss": "閉じる"
|
||||
"dismiss": "閉じる",
|
||||
"read": "{0} の説明文を読む"
|
||||
},
|
||||
"poll": {
|
||||
"count": "{0} 票",
|
||||
|
|
|
@ -252,7 +252,7 @@
|
|||
"install": "Zainstaluj",
|
||||
"install_title": "Instalacja Elk",
|
||||
"title": "Dostępna nowa aktualizacja Elk!",
|
||||
"update": "Aktualizacja",
|
||||
"update": "Aktualizuj",
|
||||
"update_available_short": "Zaktualizuj Elk",
|
||||
"webmanifest": {
|
||||
"canary": {
|
||||
|
@ -309,7 +309,14 @@
|
|||
},
|
||||
"language": {
|
||||
"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": {
|
||||
"label": "Powiadomienia",
|
||||
|
@ -380,7 +387,7 @@
|
|||
"appearance": {
|
||||
"bio": "Biogram",
|
||||
"description": "Edytuj awatar, nazwę użytkownika, profil itp.",
|
||||
"display_name": "Wyświetlana nazwa",
|
||||
"display_name": "Widoczna nazwa",
|
||||
"label": "Wygląd",
|
||||
"profile_metadata": "Metadane 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",
|
||||
"img_alt": {
|
||||
"desc": "Opis",
|
||||
"dismiss": "Odrzuć"
|
||||
"dismiss": "Zamknij"
|
||||
},
|
||||
"poll": {
|
||||
"count": "{0} głosów|{0} głos|{0} głosy|{0} głosów",
|
||||
|
|
|
@ -116,6 +116,11 @@
|
|||
"cancel": "Não",
|
||||
"confirm": "Sim"
|
||||
},
|
||||
"delete_list": {
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Eliminar",
|
||||
"title": "Tem a certeza que pretende elimnar a lista \"{0}\"?"
|
||||
},
|
||||
"delete_posts": {
|
||||
"cancel": "Cancelar",
|
||||
"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_para5": "pode encontrar-nos no GitHub",
|
||||
"desc_para6": "e participar.",
|
||||
"footer_team": "A Equipa do Elk",
|
||||
"title": "Elk está em Antevisão!"
|
||||
},
|
||||
"language": {
|
||||
|
@ -176,8 +182,19 @@
|
|||
},
|
||||
"list": {
|
||||
"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",
|
||||
"remove_account": "Remover conta da lista"
|
||||
"remove_account": "Remover conta da lista",
|
||||
"save": "Salvar alterações"
|
||||
},
|
||||
"menu": {
|
||||
"block_account": "Bloquear {0}",
|
||||
|
@ -290,6 +307,7 @@
|
|||
},
|
||||
"settings": {
|
||||
"about": {
|
||||
"built_at": "Produzido",
|
||||
"label": "Sobre",
|
||||
"meet_the_team": "Conheça a equipa",
|
||||
"sponsor_action": "Patrocine-nos",
|
||||
|
@ -316,7 +334,14 @@
|
|||
},
|
||||
"language": {
|
||||
"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": {
|
||||
"label": "Notificações",
|
||||
|
@ -345,10 +370,13 @@
|
|||
"save_settings": "Salvar configurações",
|
||||
"subscription_error": {
|
||||
"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.",
|
||||
"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.",
|
||||
"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",
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"preferences": {
|
||||
"enable_autoplay": "Habilitar Reprodução Automática",
|
||||
"enable_pinch_to_zoom": "Habilitar afastar/aproximar dedos para fazer zoom",
|
||||
"github_cards": "Cartões do GitHub",
|
||||
"grayscale_mode": "Modo tons de cinza",
|
||||
"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_favorite_count": "Esconder contagem de favoritos",
|
||||
"hide_follower_count": "Esconder contagem de seguidores",
|
||||
"hide_reply_count": "Esconder contagem de respostas",
|
||||
"hide_translation": "Esconder botão de tradução",
|
||||
"hide_username_emojis": "Esconder emojis no nome de utilizador",
|
||||
"label": "Preferências",
|
||||
"title": "Funcionalidades Experimentais",
|
||||
"user_picker": "Selecionador de Utilizador",
|
||||
"virtual_scroll": "Deslocamento Virtual"
|
||||
"virtual_scroll": "Deslocamento Virtual",
|
||||
"wellbeing": "Bem-estar"
|
||||
},
|
||||
"profile": {
|
||||
"appearance": {
|
||||
|
@ -430,8 +463,10 @@
|
|||
"filter_removed_phrase": "Removida pelo filtro",
|
||||
"filter_show_anyway": "Mostrar mesmo assim",
|
||||
"img_alt": {
|
||||
"ALT": "ALT",
|
||||
"desc": "Descrição",
|
||||
"dismiss": "Dispensar"
|
||||
"dismiss": "Dispensar",
|
||||
"read": "Ler descrição de {0}"
|
||||
},
|
||||
"poll": {
|
||||
"count": "{0} votos|{0} voto|{0} votos",
|
||||
|
|
|
@ -222,7 +222,7 @@
|
|||
"settings": "Настройки",
|
||||
"show_intro": "Показать интро",
|
||||
"toggle_theme": "Переключить тему",
|
||||
"zen_mode": "Рижим дзен"
|
||||
"zen_mode": "Режим дзен"
|
||||
},
|
||||
"notification": {
|
||||
"favourited_post": "добавили ваш пост в избранное",
|
||||
|
@ -401,7 +401,7 @@
|
|||
"state": {
|
||||
"attachments_exceed_server_limit": "Количество вложенных файлов превысило лимит на одно сообщение.",
|
||||
"attachments_limit_error": "Превышен лимит вложенных файлов на одно сообщение",
|
||||
"edited": "(Отредактированно)",
|
||||
"edited": "(Отредактировано)",
|
||||
"editing": "Редактирование",
|
||||
"loading": "Погрузка...",
|
||||
"publish_failed": "Ошибка публикации",
|
||||
|
@ -435,7 +435,7 @@
|
|||
"try_original_site": "Посмотреть на оригинальном сайте"
|
||||
},
|
||||
"status_history": {
|
||||
"created": "созданно {0}",
|
||||
"created": "создано {0}",
|
||||
"edited": "отредактировано {0}"
|
||||
},
|
||||
"tab": {
|
||||
|
|
|
@ -169,11 +169,17 @@
|
|||
"desc_para4": "鹿鸣是开源的,如果你愿意帮助测试、提供反馈或作出贡献,",
|
||||
"desc_para5": "在 GitHub 上联系我们",
|
||||
"desc_para6": "来参与其中。",
|
||||
"footer_team": "鹿鸣开发团队",
|
||||
"title": "预览鹿鸣!"
|
||||
},
|
||||
"language": {
|
||||
"search": "搜索"
|
||||
},
|
||||
"list": {
|
||||
"add_account": "向列表中添加用户",
|
||||
"modify_account": "修改列表中的用户",
|
||||
"remove_account": "移除列表中的用户"
|
||||
},
|
||||
"menu": {
|
||||
"block_account": "拉黑 {0}",
|
||||
"block_domain": "拉黑域名 {0}",
|
||||
|
@ -216,9 +222,12 @@
|
|||
"favourites": "喜欢",
|
||||
"federated": "跨站",
|
||||
"home": "主页",
|
||||
"list": "列表",
|
||||
"lists": "列表",
|
||||
"local": "本地",
|
||||
"muted_users": "已屏蔽的用户",
|
||||
"notifications": "通知",
|
||||
"privacy": "隐私协议",
|
||||
"profile": "个人资料",
|
||||
"search": "搜索",
|
||||
"select_feature_flags": "功能开关",
|
||||
|
@ -248,6 +257,8 @@
|
|||
},
|
||||
"pwa": {
|
||||
"dismiss": "忽略",
|
||||
"install": "安装",
|
||||
"install_title": "安装鹿鸣",
|
||||
"title": "鹿鸣存在新的更新",
|
||||
"update": "更新",
|
||||
"update_available_short": "更新鹿鸣",
|
||||
|
@ -280,7 +291,16 @@
|
|||
},
|
||||
"settings": {
|
||||
"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": {
|
||||
"description": "在 Mastodon UI 中编辑你的账号设置",
|
||||
|
@ -293,19 +313,19 @@
|
|||
"font_size": "字号",
|
||||
"label": "外观",
|
||||
"light_mode": "浅色",
|
||||
"size_label": {
|
||||
"lg": "大",
|
||||
"md": "中",
|
||||
"sm": "小",
|
||||
"xl": "特大",
|
||||
"xs": "特小"
|
||||
},
|
||||
"system_mode": "跟随系统",
|
||||
"theme_color": "主题颜色"
|
||||
},
|
||||
"language": {
|
||||
"display_language": "首选语言",
|
||||
"label": "语言"
|
||||
"label": "语言",
|
||||
"translations": {
|
||||
"add": "添加",
|
||||
"choose_language": "选择语言",
|
||||
"heading": "翻译",
|
||||
"hide_specific": "隐藏指定的翻译",
|
||||
"remove": "删除"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"label": "通知",
|
||||
|
@ -354,16 +374,20 @@
|
|||
"re_auth": "您的服务器似乎不支持推送通知。尝试退出用户并重新登录。如果此消息仍然出现,请联系您服务器的管理员。"
|
||||
}
|
||||
},
|
||||
"show_btn": "前往通知设置"
|
||||
"show_btn": "前往通知设置",
|
||||
"under_construction": "建设中"
|
||||
},
|
||||
"notifications_settings": "通知",
|
||||
"preferences": {
|
||||
"enable_autoplay": "开启自动播放",
|
||||
"enable_pinch_to_zoom": "启用双指缩放功能",
|
||||
"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": "实验功能",
|
||||
|
@ -393,6 +417,11 @@
|
|||
"label": "当前用户"
|
||||
}
|
||||
},
|
||||
"share-target": {
|
||||
"description": "只需要在你的设备或电脑上安装并登录鹿鸣,通过简单的配置,你就可以从其他应用中分享内容至鹿鸣。",
|
||||
"hint": "为了分享内容至鹿鸣,你必须安装并登录鹿鸣。",
|
||||
"title": "分享至鹿鸣"
|
||||
},
|
||||
"state": {
|
||||
"attachments_exceed_server_limit": "附件的数量超出了最大限制",
|
||||
"attachments_limit_error": "超出每篇帖文的最大限制",
|
||||
|
@ -409,6 +438,7 @@
|
|||
"edited": "在 {0} 编辑了",
|
||||
"favourited_by": "被喜欢",
|
||||
"filter_hidden_phrase": "筛选依据",
|
||||
"filter_removed_phrase": "从筛选中移除",
|
||||
"filter_show_anyway": "仍然展示",
|
||||
"img_alt": {
|
||||
"desc": "描述",
|
||||
|
@ -433,8 +463,10 @@
|
|||
"edited": "在 {0} 编辑了"
|
||||
},
|
||||
"tab": {
|
||||
"accounts": "用户",
|
||||
"for_you": "推荐关注",
|
||||
"hashtags": "话题标签",
|
||||
"list": "列表",
|
||||
"media": "媒体",
|
||||
"news": "最新消息",
|
||||
"notifications_all": "全部",
|
||||
|
|
|
@ -5,16 +5,18 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
|||
if (!('server' in to.params))
|
||||
return
|
||||
|
||||
const server = to.params.server as string || currentServer.value
|
||||
const user = currentUser.value
|
||||
const masto = useMasto()
|
||||
if (!user) {
|
||||
if (from.params.server !== to.params.server)
|
||||
loginTo(masto, { server: to.params.server as string })
|
||||
const fromServer = from.params.server || currentServer.value
|
||||
if (fromServer !== server)
|
||||
loginTo(masto, { server })
|
||||
return
|
||||
}
|
||||
|
||||
// No need to additionally resolve an id if we're already logged in
|
||||
if (user.server === to.params.server)
|
||||
if (user.server === server)
|
||||
return
|
||||
|
||||
// Tags don't need to be redirected to a local id
|
||||
|
@ -22,7 +24,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
|||
return
|
||||
|
||||
// Handle redirecting to new permalink structure for users with old links
|
||||
if (!to.params.server) {
|
||||
if (!useAppConfig().singleInstanceServer && !to.params.server) {
|
||||
return {
|
||||
...to,
|
||||
params: {
|
10
middleware/2.single-instance.global.ts
Normal file
10
middleware/2.single-instance.global.ts
Normal 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
|
||||
}
|
||||
})
|
|
@ -1,6 +1,7 @@
|
|||
export default defineNuxtRouteMiddleware((to) => {
|
||||
if (process.server)
|
||||
return
|
||||
|
||||
if (to.path === '/signin/callback')
|
||||
return
|
||||
|
||||
|
|
|
@ -19,8 +19,12 @@ export default defineNuxtModule({
|
|||
env,
|
||||
}
|
||||
|
||||
nuxt.options.runtimeConfig.public.env = env
|
||||
nuxt.options.runtimeConfig.public.buildInfo = buildInfo
|
||||
nuxt.options.appConfig = nuxt.options.appConfig || {}
|
||||
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 || []
|
||||
if (env === 'dev')
|
||||
|
|
|
@ -13,6 +13,7 @@ interface ExtendedManifestOptions extends ManifestOptions {
|
|||
method: string
|
||||
enctype: string
|
||||
params: {
|
||||
title: string
|
||||
text: string
|
||||
url: string
|
||||
files: [{
|
||||
|
@ -104,8 +105,9 @@ export const createI18n = async (): Promise<LocalizedWebManifest> => {
|
|||
method: 'POST',
|
||||
enctype: 'multipart/form-data',
|
||||
params: {
|
||||
title: 'title',
|
||||
text: 'text',
|
||||
url: 'text',
|
||||
url: 'url',
|
||||
files: [
|
||||
{
|
||||
name: 'files',
|
||||
|
@ -150,8 +152,9 @@ export const createI18n = async (): Promise<LocalizedWebManifest> => {
|
|||
method: 'POST',
|
||||
enctype: 'multipart/form-data',
|
||||
params: {
|
||||
title: 'title',
|
||||
text: 'text',
|
||||
url: 'text',
|
||||
url: 'url',
|
||||
files: [
|
||||
{
|
||||
name: 'files',
|
||||
|
|
|
@ -23,6 +23,9 @@ export default defineNuxtModule<VitePWANuxtOptions>({
|
|||
}
|
||||
let webmanifests: LocalizedWebManifest | undefined
|
||||
|
||||
nuxt.options.appConfig = nuxt.options.appConfig || {}
|
||||
nuxt.options.appConfig.pwaEnabled = !options.disable
|
||||
|
||||
// TODO: combine with configurePWAOptions?
|
||||
nuxt.hook('nitro:init', (nitro) => {
|
||||
options.outDir = nitro.options.output.publicDir
|
||||
|
|
|
@ -22,7 +22,9 @@ export default defineNuxtModule({
|
|||
...nuxt.options.alias,
|
||||
'unstorage/drivers/fs': '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',
|
||||
'#build-info': resolve('./runtime/build-info'),
|
||||
}
|
||||
|
||||
nuxt.hook('vite:extend', ({ config }) => {
|
||||
|
|
1
modules/tauri/runtime/build-info.ts
Normal file
1
modules/tauri/runtime/build-info.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const env = useAppConfig().env
|
|
@ -55,7 +55,6 @@ export default defineNuxtPlugin(async () => {
|
|||
const localCall = createCall(toNodeListener(h3App) as any)
|
||||
const localFetch = createLocalFetch(localCall, globalThis.fetch)
|
||||
|
||||
// @ts-expect-error slight differences in api
|
||||
globalThis.$fetch = createFetch({
|
||||
// @ts-expect-error slight differences in api
|
||||
fetch: localFetch,
|
||||
|
|
2
modules/tauri/runtime/storage-config.ts
Normal file
2
modules/tauri/runtime/storage-config.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const driver = undefined
|
||||
export const fsBase = ''
|
|
@ -1,10 +1,9 @@
|
|||
import { createResolver } from '@nuxt/kit'
|
||||
import { createResolver, useNuxt } from '@nuxt/kit'
|
||||
import Inspect from 'vite-plugin-inspect'
|
||||
import { isCI, isDevelopment, isWindows } from 'std-env'
|
||||
import { isPreview } from './config/env'
|
||||
import { i18n } from './config/i18n'
|
||||
import { pwa } from './config/pwa'
|
||||
import type { BuildInfo } from './types'
|
||||
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
|
||||
|
@ -25,6 +24,7 @@ export default defineNuxtConfig({
|
|||
'@vue-macros/nuxt',
|
||||
'@nuxtjs/i18n',
|
||||
'@nuxtjs/color-mode',
|
||||
'nuxt-vitest',
|
||||
...(isDevelopment || isWindows) ? [] : ['nuxt-security'],
|
||||
'~/modules/purge-comments',
|
||||
'~/modules/setup-components',
|
||||
|
@ -66,6 +66,7 @@ export default defineNuxtConfig({
|
|||
'./composables/settings',
|
||||
'./composables/tiptap/index.ts',
|
||||
],
|
||||
injectAtEnd: true,
|
||||
},
|
||||
vite: {
|
||||
define: {
|
||||
|
@ -75,6 +76,15 @@ export default defineNuxtConfig({
|
|||
},
|
||||
build: {
|
||||
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: [
|
||||
Inspect(),
|
||||
|
@ -85,6 +95,12 @@ export default defineNuxtConfig({
|
|||
'postcss-nested': {},
|
||||
},
|
||||
},
|
||||
appConfig: {
|
||||
singleInstanceServer: process.env.SINGLE_INSTANCE_SERVER === 'true',
|
||||
storage: {
|
||||
driver: process.env.NUXT_STORAGE_DRIVER ?? (isCI ? 'cloudflare' : 'fs'),
|
||||
},
|
||||
},
|
||||
runtimeConfig: {
|
||||
adminKey: '',
|
||||
cloudflare: {
|
||||
|
@ -94,17 +110,14 @@ export default defineNuxtConfig({
|
|||
},
|
||||
public: {
|
||||
privacyPolicyUrl: '',
|
||||
env: '', // set in build-env module
|
||||
buildInfo: {} as BuildInfo, // set in build-env module
|
||||
pwaEnabled: !isDevelopment || process.env.VITE_DEV_PWA === 'true',
|
||||
// We use LibreTranslate(https://github.com/LibreTranslate/LibreTranslate) as our default translation server #76
|
||||
// We use LibreTranslate (https://github.com/LibreTranslate/LibreTranslate) as
|
||||
// our default translation server #76
|
||||
translateApi: '',
|
||||
// Use the instance where Elk has its Mastodon account as the default
|
||||
defaultServer: 'm.webtoo.ls',
|
||||
},
|
||||
storage: {
|
||||
driver: isCI ? 'cloudflare' : 'fs',
|
||||
fsBase: 'node_modules/.cache/servers',
|
||||
fsBase: 'node_modules/.cache/app',
|
||||
},
|
||||
},
|
||||
routeRules: {
|
||||
|
@ -115,6 +128,9 @@ export default defineNuxtConfig({
|
|||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
transpile: ['masto'],
|
||||
},
|
||||
nitro: {
|
||||
esbuild: {
|
||||
options: {
|
||||
|
@ -127,11 +143,17 @@ export default defineNuxtConfig({
|
|||
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: {
|
||||
keepalive: true,
|
||||
head: {
|
||||
// Prevent arbitrary zooming on mobile devices
|
||||
viewport: 'width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,viewport-fit=cover',
|
||||
viewport: 'width=device-width,initial-scale=1,viewport-fit=cover',
|
||||
bodyAttrs: {
|
||||
class: 'overflow-x-hidden',
|
||||
},
|
||||
|
@ -193,3 +215,9 @@ declare global {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'nuxt/dist/app' {
|
||||
interface RuntimeNuxtHooks {
|
||||
'elk-logo:click': () => void
|
||||
}
|
||||
}
|
||||
|
|
25
package.json
25
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"type": "module",
|
||||
"version": "0.6.2",
|
||||
"version": "0.7.2",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@7.9.0",
|
||||
"license": "MIT",
|
||||
|
@ -49,6 +49,7 @@
|
|||
"focus-trap": "^7.2.0",
|
||||
"form-data": "^4.0.0",
|
||||
"fuse.js": "^6.6.2",
|
||||
"github-reserved-names": "^2.0.4",
|
||||
"idb-keyval": "^6.2.0",
|
||||
"iso-639-1": "^2.1.15",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
@ -56,7 +57,7 @@
|
|||
"masto": "^5.6.1",
|
||||
"pinia": "^2.0.29",
|
||||
"shiki": "^0.12.1",
|
||||
"shiki-es": "^0.1.2",
|
||||
"shiki-es": "^0.2.0",
|
||||
"slimeform": "^0.9.0",
|
||||
"string-length": "^5.0.1",
|
||||
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log",
|
||||
|
@ -75,6 +76,7 @@
|
|||
"@iconify-json/carbon": "^1.1.14",
|
||||
"@iconify-json/logos": "^1.1.22",
|
||||
"@iconify-json/material-symbols": "^1.1.26",
|
||||
"@iconify-json/mdi": "^1.1.44",
|
||||
"@iconify-json/ph": "^1.1.3",
|
||||
"@iconify-json/ri": "^1.1.4",
|
||||
"@iconify-json/twemoji": "^1.1.10",
|
||||
|
@ -88,8 +90,8 @@
|
|||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/prettier": "^2.7.2",
|
||||
"@types/wicg-file-system-access": "^2020.9.5",
|
||||
"@unocss/nuxt": "^0.48.5",
|
||||
"@vue-macros/nuxt": "^0.3.3",
|
||||
"@unocss/nuxt": "^0.49.0",
|
||||
"@vue-macros/nuxt": "^0.3.7",
|
||||
"@vueuse/math": "^9.11.1",
|
||||
"@vueuse/nuxt": "^9.11.1",
|
||||
"bumpp": "^8.2.1",
|
||||
|
@ -99,10 +101,10 @@
|
|||
"esno": "^0.16.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"fs-extra": "^11.1.0",
|
||||
"jsdom": "^21.1.0",
|
||||
"lint-staged": "^13.1.0",
|
||||
"nuxt": "3.0.0",
|
||||
"nuxt": "3.1.1",
|
||||
"nuxt-security": "^0.10.1",
|
||||
"nuxt-vitest": "^0.6.4",
|
||||
"postcss-nested": "^6.0.0",
|
||||
"prettier": "^2.8.3",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
|
@ -112,19 +114,18 @@
|
|||
"std-env": "^3.3.1",
|
||||
"theme-vitesse": "^0.6.0",
|
||||
"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",
|
||||
"vite-plugin-inspect": "^0.7.14",
|
||||
"vite-plugin-pwa": "^0.14.1",
|
||||
"vitest": "^0.28.1",
|
||||
"vitest-environment-nuxt": "0.4.0",
|
||||
"vitest": "^0.28.3",
|
||||
"vue-tsc": "^1.0.24",
|
||||
"workbox-build": "^6.5.4",
|
||||
"workbox-window": "^6.5.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"mlly": "1.1.0",
|
||||
"@tiptap/extension-bubble-menu": "2.0.0-beta.204",
|
||||
"@tiptap/extension-floating-menu": "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-strike": "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": {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue