forked from Mirrors/elk
Merge branch 'main' into feature/emoji-autocomplete
This commit is contained in:
commit
17e8130638
170 changed files with 3169 additions and 1141 deletions
|
@ -10,6 +10,8 @@ NUXT_CLOUDFLARE_API_TOKEN=
|
|||
NUXT_STORAGE_DRIVER=
|
||||
NUXT_STORAGE_FS_BASE=
|
||||
|
||||
NUXT_ADMIN_KEY=
|
||||
|
||||
NUXT_PUBLIC_DISABLE_VERSION_CHECK=
|
||||
|
||||
NUXT_GITHUB_CLIENT_ID=
|
||||
|
|
26
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
26
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
<!-- Thank you for contributing! -->
|
||||
|
||||
### Description
|
||||
|
||||
<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->
|
||||
|
||||
### Additional context
|
||||
|
||||
<!-- e.g. is there anything you'd like reviewers to focus on? -->
|
||||
|
||||
---
|
||||
|
||||
### What is the purpose of this pull request? <!-- (put an "X" next to an item) -->
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New Feature
|
||||
- [ ] Documentation update
|
||||
- [ ] Translations update
|
||||
- [ ] Other
|
||||
|
||||
### Before submitting the PR, please make sure you do the following
|
||||
|
||||
- [ ] Read the [Contributing Guidelines](https://github.com/elk-zone/elk/blob/main/CONTRIBUTING.md).
|
||||
- [ ] Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
|
||||
- [ ] Provide related snapshots or videos.
|
||||
- [ ] Provide a description in this PR that addresses **what** the PR is solving, or reference the issue that it solves (e.g. `fixes #123`).
|
|
@ -93,9 +93,9 @@ We are using [vue-i18n](https://vue-i18n.intlify.dev/) via [nuxt-i18n](https://i
|
|||
|
||||
1. Add a new file in [locales](./locales) folder with the language code as the filename.
|
||||
2. Copy [en-US](./locales/en-US.json) and translate the strings.
|
||||
3. Add the language to the `locales` array in [config/i18n.ts](./config/i18n.ts#L13)
|
||||
4. If the language is `right-to-left`, add `dir` option with `rtl` value, for example, for [ar-EG](./config/i18n.ts#L79)
|
||||
5. If the language requires special pluralization rules, add `pluralRule` callback option, for example, for [ar-EG](./config/i18n.ts#L80)
|
||||
3. Add the language to the `locales` array in [config/i18n.ts](./config/i18n.ts#L12), below `en` variants and `ar-EG`.
|
||||
4. If the language is `right-to-left`, add `dir` option with `rtl` value, for example, for [ar-EG](./config/i18n.ts#L27)
|
||||
5. If the language requires special pluralization rules, add `pluralRule` callback option, for example, for [ar-EG](./config/i18n.ts#L27)
|
||||
|
||||
Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization) for more info.
|
||||
|
||||
|
|
11
README.md
11
README.md
|
@ -28,11 +28,20 @@ A nimble Mastodon web client
|
|||
|
||||
It is already quite usable, but it isn't ready for wide adoption yet. We recommend you use it if you would like to help us build it. We appreciate your feedback and contributions. Check out the [Open Issues](https://github.com/elk-zone/elk/issues) and jump in the action. Join the [Elk discord server](https://chat.elk.zone) to chat with us and learn more about the project.
|
||||
|
||||
The client is deployed on:
|
||||
## Official Deployment
|
||||
|
||||
The Elk team maintains a deployment at:
|
||||
|
||||
- 🦌 Production: [elk.zone](https://elk.zone)
|
||||
- 🐙 Canary: [main.elk.zone](https://main.elk.zone) (deploys on every commit to `main` branch)
|
||||
|
||||
## Ecosystem
|
||||
|
||||
These are known deployments using Elk as an alternative Web client for Mastodon servers or as a base for other projects in the fediverse:
|
||||
|
||||
- [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
|
||||
|
||||
## 💖 Sponsors
|
||||
|
||||
We are grateful for the generous sponsorship and help of:
|
||||
|
|
1
app.vue
1
app.vue
|
@ -3,6 +3,7 @@ setupPageHeader()
|
|||
provideGlobalCommands()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
if (process.server && !route.path.startsWith('/settings')) {
|
||||
useHead({
|
||||
meta: [
|
||||
|
|
|
@ -8,15 +8,15 @@ const { account, command, context, ...props } = defineProps<{
|
|||
command?: boolean
|
||||
}>()
|
||||
|
||||
const isSelf = $computed(() => currentUser.value?.account.id === account.id)
|
||||
const isSelf = $(useSelfAccount(() => account))
|
||||
const enable = $computed(() => !isSelf && currentUser.value)
|
||||
const relationship = $computed(() => props.relationship || useRelationship(account).value)
|
||||
|
||||
const masto = useMasto()
|
||||
const { client } = $(useMasto())
|
||||
async function toggleFollow() {
|
||||
relationship!.following = !relationship!.following
|
||||
try {
|
||||
const newRel = await masto.v1.accounts[relationship!.following ? 'follow' : 'unfollow'](account.id)
|
||||
const newRel = await client.v1.accounts[relationship!.following ? 'follow' : 'unfollow'](account.id)
|
||||
Object.assign(relationship!, newRel)
|
||||
}
|
||||
catch (err) {
|
||||
|
@ -29,7 +29,7 @@ async function toggleFollow() {
|
|||
async function unblock() {
|
||||
relationship!.blocking = false
|
||||
try {
|
||||
const newRel = await masto.v1.accounts.unblock(account.id)
|
||||
const newRel = await client.v1.accounts.unblock(account.id)
|
||||
Object.assign(relationship!, newRel)
|
||||
}
|
||||
catch (err) {
|
||||
|
@ -42,7 +42,7 @@ async function unblock() {
|
|||
async function unmute() {
|
||||
relationship!.muting = false
|
||||
try {
|
||||
const newRel = await masto.v1.accounts.unmute(account.id)
|
||||
const newRel = await client.v1.accounts.unmute(account.id)
|
||||
Object.assign(relationship!, newRel)
|
||||
}
|
||||
catch (err) {
|
||||
|
|
|
@ -6,6 +6,8 @@ const { account } = defineProps<{
|
|||
command?: boolean
|
||||
}>()
|
||||
|
||||
const { client } = $(useMasto())
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const createdAt = $(useFormattedDateTime(() => account.createdAt, {
|
||||
|
@ -14,6 +16,8 @@ const createdAt = $(useFormattedDateTime(() => account.createdAt, {
|
|||
year: 'numeric',
|
||||
}))
|
||||
|
||||
const relationship = $(useRelationship(account))
|
||||
|
||||
const namedFields = ref<mastodon.v1.AccountField[]>([])
|
||||
const iconFields = ref<mastodon.v1.AccountField[]>([])
|
||||
|
||||
|
@ -39,6 +43,18 @@ function previewAvatar() {
|
|||
}])
|
||||
}
|
||||
|
||||
async function toggleNotifications() {
|
||||
relationship!.notifying = !relationship?.notifying
|
||||
try {
|
||||
const newRel = await client.v1.accounts.follow(account.id, { notify: relationship?.notifying })
|
||||
Object.assign(relationship!, newRel)
|
||||
}
|
||||
catch {
|
||||
// TODO error handling
|
||||
relationship!.notifying = !relationship?.notifying
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
const named: mastodon.v1.AccountField[] = []
|
||||
const icons: mastodon.v1.AccountField[] = []
|
||||
|
@ -59,7 +75,8 @@ watchEffect(() => {
|
|||
iconFields.value = icons
|
||||
})
|
||||
|
||||
const isSelf = $computed(() => currentUser.value?.account.id === account.id)
|
||||
const isSelf = $(useSelfAccount(() => account))
|
||||
const isNotifiedOnPost = $computed(() => !!relationship?.notifying)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -83,6 +100,17 @@ const isSelf = $computed(() => currentUser.value?.account.id === account.id)
|
|||
</div>
|
||||
<div absolute top-18 inset-ie-0 flex gap-2 items-center>
|
||||
<AccountMoreButton :account="account" :command="command" />
|
||||
<button
|
||||
v-if="!isSelf && relationship?.following"
|
||||
:aria-pressed="isNotifiedOnPost"
|
||||
:aria-label="t('account.notify_on_post', { username: `@${account.username}` })"
|
||||
rounded-full 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:bell-fill block text-current />
|
||||
<span v-else i-ri-bell-line block text-current />
|
||||
</button>
|
||||
<AccountFollowButton :account="account" :command="command" />
|
||||
<!-- Edit profile -->
|
||||
<NuxtLink
|
||||
|
@ -93,11 +121,6 @@ const isSelf = $computed(() => currentUser.value?.account.id === account.id)
|
|||
>
|
||||
{{ $t('settings.profile.appearance.title') }}
|
||||
</NuxtLink>
|
||||
<!-- <button flex gap-1 items-center w-full rounded op75 hover="op100 text-purple" group>
|
||||
<div rounded p2 group-hover="bg-rose/10">
|
||||
<div i-ri:bell-line />
|
||||
</div>
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="account.note" max-h-100 overflow-y-auto>
|
||||
|
|
|
@ -12,7 +12,7 @@ const { link = true, avatar = true } = defineProps<{
|
|||
<AccountHoverWrapper :account="account">
|
||||
<NuxtLink
|
||||
:to="link ? getAccountRoute(account) : undefined"
|
||||
:class="link ? 'text-link-rounded ms-0 ps-0' : ''"
|
||||
:class="link ? 'text-link-rounded -ml-1.8rem pl-1.8rem rtl-(ml0 pl-0.5rem -mr-1.8rem pr-1.8rem)' : ''"
|
||||
min-w-0 flex gap-2 items-center
|
||||
>
|
||||
<AccountAvatar v-if="avatar" :account="account" w-5 h-5 />
|
||||
|
|
|
@ -7,39 +7,49 @@ const { account } = defineProps<{
|
|||
}>()
|
||||
let relationship = $(useRelationship(account))
|
||||
|
||||
const isSelf = $computed(() => currentUser.value?.account.id === account.id)
|
||||
const isSelf = $(useSelfAccount(() => account))
|
||||
|
||||
const masto = useMasto()
|
||||
const toggleMute = async () => {
|
||||
// TODO: Add confirmation
|
||||
const { t } = useI18n()
|
||||
const { client } = $(useMasto())
|
||||
|
||||
const isConfirmed = async (title: string) => {
|
||||
return await openConfirmDialog(t('common.confirm_dialog.title', [title])) === 'confirm'
|
||||
}
|
||||
|
||||
const toggleMute = async (title: string) => {
|
||||
if (!await isConfirmed(title))
|
||||
return
|
||||
|
||||
relationship!.muting = !relationship!.muting
|
||||
relationship = relationship!.muting
|
||||
? await masto.v1.accounts.mute(account.id, {
|
||||
? await client.v1.accounts.mute(account.id, {
|
||||
// TODO support more options
|
||||
})
|
||||
: await masto.v1.accounts.unmute(account.id)
|
||||
: await client.v1.accounts.unmute(account.id)
|
||||
}
|
||||
|
||||
const toggleBlockUser = async () => {
|
||||
// TODO: Add confirmation
|
||||
const toggleBlockUser = async (title: string) => {
|
||||
if (!await isConfirmed(title))
|
||||
return
|
||||
|
||||
relationship!.blocking = !relationship!.blocking
|
||||
relationship = await masto.v1.accounts[relationship!.blocking ? 'block' : 'unblock'](account.id)
|
||||
relationship = await client.v1.accounts[relationship!.blocking ? 'block' : 'unblock'](account.id)
|
||||
}
|
||||
|
||||
const toggleBlockDomain = async () => {
|
||||
// TODO: Add confirmation
|
||||
const toggleBlockDomain = async (title: string) => {
|
||||
if (!await isConfirmed(title))
|
||||
return
|
||||
|
||||
relationship!.domainBlocking = !relationship!.domainBlocking
|
||||
await masto.v1.domainBlocks[relationship!.domainBlocking ? 'block' : 'unblock'](getServerName(account))
|
||||
await client.v1.domainBlocks[relationship!.domainBlocking ? 'block' : 'unblock'](getServerName(account))
|
||||
}
|
||||
|
||||
const toggleReblogs = async () => {
|
||||
// TODO: Add confirmation
|
||||
const toggleReblogs = async (title: string) => {
|
||||
if (!await isConfirmed(title))
|
||||
return
|
||||
|
||||
const showingReblogs = !relationship?.showingReblogs
|
||||
relationship = await masto.v1.accounts.follow(account.id, { reblogs: showingReblogs })
|
||||
relationship = await client.v1.accounts.follow(account.id, { reblogs: showingReblogs })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -80,14 +90,14 @@ const toggleReblogs = async () => {
|
|||
icon="i-ri:repeat-line"
|
||||
:text="$t('menu.show_reblogs', [`@${account.acct}`])"
|
||||
:command="command"
|
||||
@click="toggleReblogs"
|
||||
@click="toggleReblogs($t('menu.show_reblogs', [`@${account.acct}`]))"
|
||||
/>
|
||||
<CommonDropdownItem
|
||||
v-else
|
||||
:text="$t('menu.hide_reblogs', [`@${account.acct}`])"
|
||||
icon="i-ri:repeat-line"
|
||||
:command="command"
|
||||
@click="toggleReblogs"
|
||||
@click="toggleReblogs($t('menu.hide_reblogs', [`@${account.acct}`]))"
|
||||
/>
|
||||
|
||||
<CommonDropdownItem
|
||||
|
@ -95,14 +105,14 @@ const toggleReblogs = async () => {
|
|||
:text="$t('menu.mute_account', [`@${account.acct}`])"
|
||||
icon="i-ri:volume-up-fill"
|
||||
:command="command"
|
||||
@click="toggleMute"
|
||||
@click="toggleMute($t('menu.mute_account', [`@${account.acct}`]))"
|
||||
/>
|
||||
<CommonDropdownItem
|
||||
v-else
|
||||
:text="$t('menu.unmute_account', [`@${account.acct}`])"
|
||||
icon="i-ri:volume-mute-line"
|
||||
:command="command"
|
||||
@click="toggleMute"
|
||||
@click="toggleMute($t('menu.unmute_account', [`@${account.acct}`]))"
|
||||
/>
|
||||
|
||||
<CommonDropdownItem
|
||||
|
@ -110,14 +120,14 @@ const toggleReblogs = async () => {
|
|||
:text="$t('menu.block_account', [`@${account.acct}`])"
|
||||
icon="i-ri:forbid-2-line"
|
||||
:command="command"
|
||||
@click="toggleBlockUser"
|
||||
@click="toggleBlockUser($t('menu.block_account', [`@${account.acct}`]))"
|
||||
/>
|
||||
<CommonDropdownItem
|
||||
v-else
|
||||
:text="$t('menu.unblock_account', [`@${account.acct}`])"
|
||||
icon="i-ri:checkbox-circle-line"
|
||||
:command="command"
|
||||
@click="toggleBlockUser"
|
||||
@click="toggleBlockUser($t('menu.unblock_account', [`@${account.acct}`]))"
|
||||
/>
|
||||
|
||||
<template v-if="getServerName(account) !== currentServer">
|
||||
|
@ -126,14 +136,14 @@ const toggleReblogs = async () => {
|
|||
:text="$t('menu.block_domain', [getServerName(account)])"
|
||||
icon="i-ri:shut-down-line"
|
||||
:command="command"
|
||||
@click="toggleBlockDomain"
|
||||
@click="toggleBlockDomain($t('menu.block_domain', [getServerName(account)]))"
|
||||
/>
|
||||
<CommonDropdownItem
|
||||
v-else
|
||||
:text="$t('menu.unblock_domain', [getServerName(account)])"
|
||||
icon="i-ri:restart-line"
|
||||
:command="command"
|
||||
@click="toggleBlockDomain"
|
||||
@click="toggleBlockDomain($t('menu.unblock_domain', [getServerName(account)]))"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import type { Paginator, mastodon } from 'masto'
|
||||
|
||||
const { paginator } = defineProps<{
|
||||
const { paginator, account, context } = defineProps<{
|
||||
paginator: Paginator<mastodon.v1.Account[], mastodon.DefaultPaginationParams>
|
||||
context?: 'following' | 'followers'
|
||||
account?: mastodon.v1.Account
|
||||
relationshipContext?: 'followedBy' | 'following'
|
||||
}>()
|
||||
|
||||
const fallbackContext = $computed(() => {
|
||||
return ['following', 'followers'].includes(context!)
|
||||
})
|
||||
const showOriginSite = $computed(() =>
|
||||
account && account.id !== currentUser.value?.account.id && getServerName(account) !== currentServer.value,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -17,5 +26,18 @@ const { paginator } = defineProps<{
|
|||
border="b base" py2 px4
|
||||
/>
|
||||
</template>
|
||||
<template v-if="fallbackContext && showOriginSite" #done>
|
||||
<div p5 text-secondary text-center flex flex-col items-center gap1>
|
||||
<span italic>{{ $t(`account.view_other_${context}`) }}</span>
|
||||
<NuxtLink
|
||||
:href="account!.url" target="_blank" external
|
||||
flex="~ gap-1" items-center text-primary
|
||||
hover="underline text-primary-active"
|
||||
>
|
||||
<div i-ri:external-link-fill />
|
||||
{{ $t('menu.open_in_original_site') }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
</CommonPaginator>
|
||||
</template>
|
||||
|
|
|
@ -40,7 +40,7 @@ const userSettings = useUserSettings()
|
|||
</template>
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
v-if="!getWellnessSetting(userSettings, 'hideFollowerCount')"
|
||||
v-if="!getPreferences(userSettings, 'hideFollowerCount')"
|
||||
:to="getAccountFollowersRoute(account)"
|
||||
replace text-secondary
|
||||
exact-active-class="text-primary"
|
||||
|
|
|
@ -88,17 +88,19 @@ watch(file, (image, _, onCleanup) => {
|
|||
w-full
|
||||
h-full
|
||||
>
|
||||
<div absolute bg="black/50" text-white rounded-full text-xl w12 h12 flex justify-center items-center hover="bg-black/40 text-primary">
|
||||
<div i-ri:upload-line />
|
||||
</div>
|
||||
<span absolute bg="black/50" text-white rounded-full text-xl w12 h12 flex justify-center items-center hover="bg-black/40 text-primary">
|
||||
<span block i-ri:upload-line />
|
||||
</span>
|
||||
|
||||
<div
|
||||
<span
|
||||
v-if="loading"
|
||||
absolute inset-0
|
||||
bg="black/30" text-white
|
||||
flex justify-center items-center
|
||||
>
|
||||
<div class="i-ri:loader-4-line animate-spin animate-duration-[2.5s]" text-4xl />
|
||||
</div>
|
||||
<span class="animate-spin animate-duration-[2.5s] preserve-3d">
|
||||
<span block i-ri:loader-4-line text-4xl />
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
|
|
|
@ -42,7 +42,7 @@ defineSlots<{
|
|||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, stream, eventType, preprocess)
|
||||
const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, $$(stream), eventType, preprocess)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -9,6 +9,7 @@ defineProps<{
|
|||
<template>
|
||||
<VTooltip
|
||||
v-bind="$attrs"
|
||||
auto-hide
|
||||
>
|
||||
<slot />
|
||||
<template #popper>
|
||||
|
|
|
@ -18,5 +18,6 @@ const highlighted = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<pre class="code-block" v-html="highlighted" />
|
||||
<pre v-if="lang" class="code-block" v-html="highlighted" />
|
||||
<pre v-else class="code-block">{{ raw }}</pre>
|
||||
</template>
|
||||
|
|
|
@ -17,7 +17,7 @@ defineProps<{
|
|||
<div flex justify-between px5 py2 :class="{ 'xl:hidden': $route.name !== 'tag' }">
|
||||
<div flex gap-3 items-center overflow-hidden py2>
|
||||
<NuxtLink
|
||||
v-if="backOnSmallScreen || back" flex="~ gap1" items-center btn-text p-0 lg:hidden
|
||||
v-if="backOnSmallScreen || back" flex="~ gap1" items-center btn-text p-0 xl:hidden
|
||||
:aria-label="$t('nav.back')"
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
|
@ -31,7 +31,7 @@ defineProps<{
|
|||
<div flex items-center flex-shrink-0 gap-x-2>
|
||||
<slot name="actions" />
|
||||
<PwaBadge lg:hidden />
|
||||
<NavUser v-if="isMastoInitialised" />
|
||||
<NavUser v-if="isHydrated" />
|
||||
<NavUserSkeleton v-else />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -51,7 +51,7 @@ const handleFavouritedBoostedByClose = () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="isMastoInitialised">
|
||||
<template v-if="isHydrated">
|
||||
<ModalDialog v-model="isSigninDialogOpen" py-4 px-8 max-w-125>
|
||||
<UserSignIn />
|
||||
</ModalDialog>
|
||||
|
|
|
@ -53,6 +53,10 @@ const { modelValue: visible } = defineModel<{
|
|||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const deactivated = useDeactivated()
|
||||
const route = useRoute()
|
||||
|
||||
|
@ -132,12 +136,6 @@ useEventListener('keydown', (e: KeyboardEvent) => {
|
|||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<!-- Dialog component -->
|
||||
|
|
|
@ -10,7 +10,7 @@ const moreMenuVisible = ref(false)
|
|||
class="after-content-empty after:(h-[calc(100%+0.5px)] w-0.1px pointer-events-none)"
|
||||
>
|
||||
<!-- These weird styles above are used for scroll locking, don't change it unless you know exactly what you're doing. -->
|
||||
<template v-if="isMastoInitialised && currentUser">
|
||||
<template v-if="currentUser">
|
||||
<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>
|
||||
|
@ -24,7 +24,7 @@ const moreMenuVisible = ref(false)
|
|||
<div i-ri:at-line />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-if="isMastoInitialised && !currentUser">
|
||||
<template v-else>
|
||||
<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>
|
||||
|
|
|
@ -16,7 +16,7 @@ const { notifications } = useNotifications()
|
|||
<NavSideItem :text="$t('nav.notifications')" to="/notifications" icon="i-ri:notification-4-line" user-only :command="command">
|
||||
<template #icon>
|
||||
<div flex relative>
|
||||
<div class="i-ri:notification-4-line" md:text-size-inherit text-xl />
|
||||
<div class="i-ri:notification-4-line" text-xl />
|
||||
<div v-if="notifications" class="top-[-0.3rem] right-[-0.3rem]" absolute font-bold rounded-full h-4 w-4 text-xs bg-primary text-inverted flex items-center justify-center>
|
||||
{{ notifications < 10 ? notifications : '•' }}
|
||||
</div>
|
||||
|
@ -29,9 +29,9 @@ 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="isMastoInitialised ? `/${currentServer}/explore` : '/explore'" icon="i-ri:hashtag" :command="command" />
|
||||
<NavSideItem :text="$t('nav.local')" :to="isMastoInitialised ? `/${currentServer}/public/local` : '/public/local'" icon="i-ri:group-2-line " :command="command" />
|
||||
<NavSideItem :text="$t('nav.federated')" :to="isMastoInitialised ? `/${currentServer}/public` : '/public'" icon="i-ri:earth-line" :command="command" />
|
||||
<NavSideItem :text="$t('nav.explore')" :to="isHydrated ? `/${currentServer}/explore` : '/explore'" icon="i-ri:hashtag" :command="command" />
|
||||
<NavSideItem :text="$t('nav.local')" :to="isHydrated ? `/${currentServer}/public/local` : '/public/local'" icon="i-ri:group-2-line " :command="command" />
|
||||
<NavSideItem :text="$t('nav.federated')" :to="isHydrated ? `/${currentServer}/public` : '/public'" icon="i-ri:earth-line" :command="command" />
|
||||
|
||||
<div shrink hidden sm:block mt-4 />
|
||||
<NavSideItem :text="$t('nav.settings')" to="/settings" icon="i-ri:settings-3-line" :command="command" />
|
||||
|
|
|
@ -29,7 +29,7 @@ useCommand({
|
|||
})
|
||||
|
||||
let activeClass = $ref('text-primary')
|
||||
onMastoInit(async () => {
|
||||
onHydrated(async () => {
|
||||
// TODO: force NuxtLink to reevaluate, we now we are in this route though, so we should force it to active
|
||||
// we don't have currentServer defined until later
|
||||
activeClass = ''
|
||||
|
@ -39,8 +39,8 @@ onMastoInit(async () => {
|
|||
|
||||
// Optimize rendering for the common case of being logged in, only show visual feedback for disabled user-only items
|
||||
// when we know there is no user.
|
||||
const noUserDisable = computed(() => !isMastoInitialised.value || (props.userOnly && !currentUser.value))
|
||||
const noUserVisual = computed(() => isMastoInitialised.value && props.userOnly && !currentUser.value)
|
||||
const noUserDisable = computed(() => !isHydrated.value || (props.userOnly && !currentUser.value))
|
||||
const noUserVisual = computed(() => isHydrated.value && props.userOnly && !currentUser.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -66,7 +66,7 @@ const noUserVisual = computed(() => isMastoInitialised.value && props.userOnly &
|
|||
<div :class="icon" text-xl />
|
||||
</slot>
|
||||
<slot>
|
||||
<span block sm:hidden xl:block>{{ isHydrated ? text : ' ' }}</span>
|
||||
<span block sm:hidden xl:block select-none>{{ isHydrated ? text : ' ' }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
</CommonTooltip>
|
||||
|
|
|
@ -17,6 +17,7 @@ router.afterEach(() => {
|
|||
flex items-end gap-4
|
||||
py2 px-5
|
||||
text-2xl
|
||||
select-none
|
||||
focus-visible:ring="2 current"
|
||||
to="/"
|
||||
external
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<VDropdown v-if="isMastoInitialised && currentUser" sm:hidden>
|
||||
<VDropdown v-if="isHydrated && currentUser" sm:hidden>
|
||||
<div style="-webkit-touch-callout: none;">
|
||||
<AccountAvatar
|
||||
ref="avatar"
|
||||
|
|
|
@ -66,7 +66,10 @@ const isLegacyAccount = computed(() => !currentUser.value?.vapidKey)
|
|||
:disabled="busy || isLegacyAccount"
|
||||
@click="$emit('subscribe')"
|
||||
>
|
||||
<span aria-hidden="true" :class="busy && animate ? 'i-ri:loader-2-fill animate-spin' : 'i-ri:check-line'" />
|
||||
<span v-if="busy && animate" aria-hidden="true" block animate-spin preserve-3d>
|
||||
<span block i-ri:loader-2-fill aria-hidden="true" />
|
||||
</span>
|
||||
<span v-else aria-hidden="true" block i-ri:check-line />
|
||||
<span>{{ $t('settings.notifications.push_notifications.warning.enable_desktop') }}</span>
|
||||
</button>
|
||||
<slot name="error" />
|
||||
|
|
|
@ -147,7 +147,10 @@ onActivated(() => (busy = false))
|
|||
:class="busy || !saveEnabled ? 'border-transparent' : null"
|
||||
:disabled="busy || !saveEnabled"
|
||||
>
|
||||
<span :class="busy && animateSave ? 'i-ri:loader-2-fill animate-spin' : 'i-ri:save-2-fill'" />
|
||||
<span v-if="busy && animateSave" aria-hidden="true" block animate-spin preserve-3d>
|
||||
<span block i-ri:loader-2-fill aria-hidden="true" />
|
||||
</span>
|
||||
<span v-else block aria-hidden="true" i-ri:save-2-fill />
|
||||
{{ $t('settings.notifications.push_notifications.save_settings') }}
|
||||
</button>
|
||||
<button
|
||||
|
@ -157,7 +160,7 @@ onActivated(() => (busy = false))
|
|||
:disabled="busy || !saveEnabled"
|
||||
@click="undoChanges"
|
||||
>
|
||||
<span aria-hidden="true" class="i-material-symbols:undo-rounded" />
|
||||
<span aria-hidden="true" class="block i-material-symbols:undo-rounded" />
|
||||
{{ $t('settings.notifications.push_notifications.undo_settings') }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -169,7 +172,10 @@ onActivated(() => (busy = false))
|
|||
:class="busy ? 'border-transparent' : null"
|
||||
:disabled="busy"
|
||||
>
|
||||
<span aria-hidden="true" :class="busy && animateRemoveSubscription ? 'i-ri:loader-2-fill animate-spin' : 'i-material-symbols:cancel-rounded'" />
|
||||
<span v-if="busy && animateRemoveSubscription" aria-hidden="true" block animate-spin preserve-3d>
|
||||
<span block i-ri:loader-2-fill aria-hidden="true" />
|
||||
</span>
|
||||
<span v-else block aria-hidden="true" i-material-symbols:cancel-rounded />
|
||||
{{ $t('settings.notifications.push_notifications.unsubscribe') }}
|
||||
</button>
|
||||
</form>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup>
|
||||
const disabled = computed(() => !isMastoInitialised.value || !currentUser.value)
|
||||
const disabledVisual = computed(() => isMastoInitialised.value && !currentUser.value)
|
||||
const disabled = computed(() => !isHydrated.value || !currentUser.value)
|
||||
const disabledVisual = computed(() => isHydrated.value && !currentUser.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -44,7 +44,7 @@ const hideEmojiPicker = () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<CommonTooltip content="Add emojis">
|
||||
<CommonTooltip :content="$t('tooltip.add_emojis')">
|
||||
<VDropdown
|
||||
auto-boundary-max-size
|
||||
@apply-show="openEmojiPicker()"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { EditorContent } from '@tiptap/vue-3'
|
||||
import type { mastodon } from 'masto'
|
||||
import type { Ref } from 'vue'
|
||||
import type { Draft } from '~/types'
|
||||
|
||||
const {
|
||||
|
@ -90,6 +89,19 @@ async function publish() {
|
|||
emit('published', status)
|
||||
}
|
||||
|
||||
useWebShareTarget(async ({ data: { data, action } }: any) => {
|
||||
if (action !== 'compose-with-shared-data')
|
||||
return
|
||||
|
||||
editor.value?.commands.focus('end')
|
||||
|
||||
if (data.text !== undefined)
|
||||
editor.value?.commands.insertContent(data.text)
|
||||
|
||||
if (data.files !== undefined)
|
||||
await uploadAttachments(data.files)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
focusEditor: () => {
|
||||
editor.value?.commands?.focus?.()
|
||||
|
@ -98,7 +110,7 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isMastoInitialised && currentUser" flex="~ col gap-4" py3 px2 sm:px4>
|
||||
<div v-if="isHydrated && currentUser" flex="~ col gap-4" py3 px2 sm:px4>
|
||||
<template v-if="draft.editingStatus">
|
||||
<div flex="~ col gap-1">
|
||||
<div id="state-editing" text-secondary self-center>
|
||||
|
@ -145,7 +157,9 @@ defineExpose({
|
|||
</div>
|
||||
|
||||
<div v-if="isUploading" flex gap-1 items-center text-sm p1 text-primary>
|
||||
<div i-ri:loader-2-fill animate-spin />
|
||||
<div animate-spin preserve-3d>
|
||||
<div i-ri:loader-2-fill />
|
||||
</div>
|
||||
{{ $t('state.uploading') }}
|
||||
</div>
|
||||
<div
|
||||
|
@ -199,7 +213,7 @@ defineExpose({
|
|||
<div flex gap-4>
|
||||
<div w-12 h-full sm:block hidden />
|
||||
<div
|
||||
v-if="shouldExpanded" flex="~ gap-1 1 wrap" m="s--1" pt-2 justify="between" max-w-full
|
||||
v-if="shouldExpanded" flex="~ gap-1 1 wrap" m="s--1" pt-2 justify="end" max-w-full
|
||||
border="t base"
|
||||
>
|
||||
<PublishEmojiPicker
|
||||
|
@ -274,7 +288,9 @@ defineExpose({
|
|||
aria-describedby="publish-tooltip"
|
||||
@click="publish"
|
||||
>
|
||||
<div v-if="isSending" i-ri:loader-2-fill animate-spin />
|
||||
<span v-if="isSending" block animate-spin preserve-3d>
|
||||
<div block i-ri:loader-2-fill />
|
||||
</span>
|
||||
<span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span>
|
||||
<span v-else-if="draft.params.inReplyToId">{{ $t('action.reply') }}</span>
|
||||
<span v-else>{{ !isSending ? $t('action.publish') : $t('state.publishing') }}</span>
|
||||
|
|
|
@ -66,6 +66,7 @@ const activate = () => {
|
|||
bg-transparent
|
||||
outline="focus:none"
|
||||
pe-4
|
||||
select-none
|
||||
:placeholder="isHydrated ? t('nav.search') : ''"
|
||||
pb="1px"
|
||||
placeholder-text-secondary
|
||||
|
|
|
@ -9,9 +9,9 @@ function setColorMode(mode: ColorMode) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ gap4" w-full>
|
||||
<div flex="~ gap4 wrap" w-full>
|
||||
<button
|
||||
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base
|
||||
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base ws-nowrap
|
||||
:tabindex="colorMode.preference === 'dark' ? 0 : -1"
|
||||
:class="colorMode.preference === 'dark' ? 'pointer-events-none' : 'filter-saturate-0'"
|
||||
@click="setColorMode('dark')"
|
||||
|
@ -20,7 +20,7 @@ function setColorMode(mode: ColorMode) {
|
|||
{{ $t('settings.interface.dark_mode') }}
|
||||
</button>
|
||||
<button
|
||||
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base
|
||||
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base ws-nowrap
|
||||
:tabindex="colorMode.preference === 'light' ? 0 : -1"
|
||||
:class="colorMode.preference === 'light' ? 'pointer-events-none' : 'filter-saturate-0'"
|
||||
@click="setColorMode('light')"
|
||||
|
@ -29,7 +29,7 @@ function setColorMode(mode: ColorMode) {
|
|||
{{ $t('settings.interface.light_mode') }}
|
||||
</button>
|
||||
<button
|
||||
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base
|
||||
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base ws-nowrap
|
||||
:tabindex="colorMode.preference === 'system' ? 0 : -1"
|
||||
:class="colorMode.preference === 'system' ? 'pointer-events-none' : 'filter-saturate-0'"
|
||||
@click="setColorMode('system')"
|
||||
|
|
|
@ -49,7 +49,7 @@ useCommand({
|
|||
<component
|
||||
:is="as"
|
||||
v-bind="$attrs" ref="el"
|
||||
w-fit flex gap-1 items-center
|
||||
w-fit flex gap-1 items-center transition-all
|
||||
rounded group
|
||||
:hover=" !disabled ? hover : undefined"
|
||||
focus:outline-none
|
||||
|
|
|
@ -55,7 +55,7 @@ const reply = () => {
|
|||
<div flex-1>
|
||||
<StatusActionButton
|
||||
:content="$t('action.boost')"
|
||||
:text="!getWellnessSetting(userSettings, 'hideBoostCount') && status.reblogsCount ? status.reblogsCount : ''"
|
||||
:text="!getPreferences(userSettings, 'hideBoostCount') && status.reblogsCount ? status.reblogsCount : ''"
|
||||
color="text-green" hover="text-green" group-hover="bg-green/10"
|
||||
icon="i-ri:repeat-line"
|
||||
active-icon="i-ri:repeat-fill"
|
||||
|
@ -64,7 +64,7 @@ const reply = () => {
|
|||
:command="command"
|
||||
@click="toggleReblog()"
|
||||
>
|
||||
<template v-if="status.reblogsCount && !getWellnessSetting(userSettings, 'hideBoostCount')" #text>
|
||||
<template v-if="status.reblogsCount && !getPreferences(userSettings, 'hideBoostCount')" #text>
|
||||
<CommonLocalizedNumber
|
||||
keypath="action.boost_count"
|
||||
:count="status.reblogsCount"
|
||||
|
@ -76,7 +76,7 @@ const reply = () => {
|
|||
<div flex-1>
|
||||
<StatusActionButton
|
||||
:content="$t('action.favourite')"
|
||||
:text="!getWellnessSetting(userSettings, 'hideFavoriteCount') && status.favouritesCount ? status.favouritesCount : ''"
|
||||
:text="!getPreferences(userSettings, 'hideFavoriteCount') && status.favouritesCount ? status.favouritesCount : ''"
|
||||
color="text-rose" hover="text-rose" group-hover="bg-rose/10"
|
||||
icon="i-ri:heart-3-line"
|
||||
active-icon="i-ri:heart-3-fill"
|
||||
|
@ -85,7 +85,7 @@ const reply = () => {
|
|||
:command="command"
|
||||
@click="toggleFavourite()"
|
||||
>
|
||||
<template v-if="status.favouritesCount && !getWellnessSetting(userSettings, 'hideFavoriteCount')" #text>
|
||||
<template v-if="status.favouritesCount && !getPreferences(userSettings, 'hideFavoriteCount')" #text>
|
||||
<CommonLocalizedNumber
|
||||
keypath="action.favourite_count"
|
||||
:count="status.favouritesCount"
|
||||
|
|
|
@ -39,7 +39,7 @@ const toggleTranslation = async () => {
|
|||
isLoading.translation = false
|
||||
}
|
||||
|
||||
const masto = useMasto()
|
||||
const { client } = $(useMasto())
|
||||
|
||||
const getPermalinkUrl = (status: mastodon.v1.Status) => {
|
||||
const url = getStatusPermalinkRoute(status)
|
||||
|
@ -70,7 +70,7 @@ const deleteStatus = async () => {
|
|||
return
|
||||
|
||||
removeCachedStatus(status.id)
|
||||
await masto.v1.statuses.remove(status.id)
|
||||
await client.v1.statuses.remove(status.id)
|
||||
|
||||
if (route.name === 'status')
|
||||
router.back()
|
||||
|
@ -88,7 +88,7 @@ const deleteAndRedraft = async () => {
|
|||
}
|
||||
|
||||
removeCachedStatus(status.id)
|
||||
await masto.v1.statuses.remove(status.id)
|
||||
await client.v1.statuses.remove(status.id)
|
||||
await openPublishDialog('dialog', await getDraftFromStatus(status), true)
|
||||
|
||||
// Go to the new status, if the page is the old status
|
||||
|
@ -214,7 +214,7 @@ const showFavoritedAndBoostedBy = () => {
|
|||
@click="toggleTranslation"
|
||||
/>
|
||||
|
||||
<template v-if="isMastoInitialised && currentUser">
|
||||
<template v-if="isHydrated && currentUser">
|
||||
<template v-if="isAuthor">
|
||||
<CommonDropdownItem
|
||||
:text="status.pinned ? $t('menu.unpin_on_profile') : $t('menu.pin_on_profile')"
|
||||
|
|
|
@ -188,7 +188,7 @@ useIntersectionObserver(video, (entries) => {
|
|||
{{ $t('status.img_alt.dismiss') }}
|
||||
</button>
|
||||
</div>
|
||||
<p>
|
||||
<p whitespace-pre-wrap>
|
||||
{{ attachment.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,8 @@ const props = withDefaults(defineProps<{
|
|||
actions: true,
|
||||
})
|
||||
|
||||
const userSettings = useUserSettings()
|
||||
|
||||
const status = $computed(() => {
|
||||
if (props.status.reblog && props.status.reblog)
|
||||
return props.status.reblog
|
||||
|
@ -59,7 +61,7 @@ const isDM = $computed(() => status.visibility === 'direct')
|
|||
{{ status.application?.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div border="t base" pt-2>
|
||||
<div border="t base" py-2>
|
||||
<StatusActions v-if="actions" :status="status" details :command="command" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,8 +3,10 @@ import { favouritedBoostedByStatusId } from '~/composables/dialog'
|
|||
|
||||
const type = ref<'favourited-by' | 'boosted-by'>('favourited-by')
|
||||
|
||||
const { client } = $(useMasto())
|
||||
|
||||
function load() {
|
||||
return useMasto().v1.statuses[type.value === 'favourited-by' ? 'listFavouritedBy' : 'listRebloggedBy'](favouritedBoostedByStatusId.value!)
|
||||
return client.v1.statuses[type.value === 'favourited-by' ? 'listFavouritedBy' : 'listRebloggedBy'](favouritedBoostedByStatusId.value!)
|
||||
}
|
||||
|
||||
const paginator = $computed(() => load())
|
||||
|
|
|
@ -15,7 +15,8 @@ const expiredTimeAgo = useTimeAgo(poll.expiresAt!, timeAgoOptions)
|
|||
const expiredTimeFormatted = useFormattedDateTime(poll.expiresAt!)
|
||||
const { formatPercentage } = useHumanReadableNumber()
|
||||
|
||||
const masto = useMasto()
|
||||
const { client } = $(useMasto())
|
||||
|
||||
async function vote(e: Event) {
|
||||
const formData = new FormData(e.target as HTMLFormElement)
|
||||
const choices = formData.getAll('choices') as string[]
|
||||
|
@ -30,25 +31,29 @@ async function vote(e: Event) {
|
|||
poll.votersCount = (poll.votersCount || 0) + 1
|
||||
cacheStatus({ ...status, poll }, undefined, true)
|
||||
|
||||
await masto.v1.polls.vote(poll.id, { choices })
|
||||
await client.v1.polls.vote(poll.id, { choices })
|
||||
}
|
||||
|
||||
const votersCount = $computed(() => poll.votersCount ?? 0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col w-full items-stretch gap-3 dir="auto">
|
||||
<form v-if="!poll.voted && !poll.expired" flex flex-col gap-4 accent-primary @click.stop="noop" @submit.prevent="vote">
|
||||
<label v-for="(option, index) of poll.options" :key="index" flex items-center gap-2 px-2>
|
||||
<div flex flex-col w-full items-stretch gap-2 py3 dir="auto">
|
||||
<form v-if="!poll.voted && !poll.expired" flex="~ col gap3" accent-primary @click.stop="noop" @submit.prevent="vote">
|
||||
<label v-for="(option, index) of poll.options" :key="index" flex="~ gap2" items-center>
|
||||
<input name="choices" :value="index" :type="poll.multiple ? 'checkbox' : 'radio'">
|
||||
{{ option.title }}
|
||||
</label>
|
||||
<button btn-solid>
|
||||
<button btn-solid mt-1>
|
||||
{{ $t('action.vote') }}
|
||||
</button>
|
||||
</form>
|
||||
<template v-else>
|
||||
<div v-for="(option, index) of poll.options" :key="index" py-1 relative :style="{ '--bar-width': toPercentage((option.votesCount || 0) / poll.votesCount) }">
|
||||
<div
|
||||
v-for="(option, index) of poll.options"
|
||||
:key="index" py-1 relative
|
||||
:style="{ '--bar-width': toPercentage((option.votesCount || 0) / poll.votesCount) }"
|
||||
>
|
||||
<div flex justify-between pb-2 w-full>
|
||||
<span inline-flex align-items>
|
||||
{{ option.title }}
|
||||
|
@ -61,7 +66,7 @@ const votersCount = $computed(() => poll.votersCount ?? 0)
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div text-sm flex="~ inline" gap-x-1>
|
||||
<div text-sm flex="~ inline" gap-x-1 text-secondary>
|
||||
<CommonLocalizedNumber
|
||||
keypath="status.poll.count"
|
||||
:count="poll.votesCount"
|
||||
|
|
|
@ -21,7 +21,7 @@ const isSquare = $computed(() => (
|
|||
))
|
||||
const providerName = $computed(() => props.card.providerName ? props.card.providerName : new URL(props.card.url).hostname)
|
||||
|
||||
const gitHubCards = $(useFeatureFlag('experimentalGitHubCards'))
|
||||
const gitHubCards = $(usePreferences('experimentalGitHubCards'))
|
||||
|
||||
// TODO: handle card.type: 'photo' | 'video' | 'rich';
|
||||
const cardTypeIconMap: Record<mastodon.v1.PreviewCardType, string> = {
|
||||
|
|
|
@ -6,7 +6,7 @@ const { status } = defineProps<{
|
|||
status: mastodon.v1.Status
|
||||
}>()
|
||||
|
||||
const paginator = useMasto().v1.statuses.listHistory(status.id)
|
||||
const paginator = useMastoClient().v1.statuses.listHistory(status.id)
|
||||
|
||||
const showHistory = (edit: mastodon.v1.StatusEdit) => {
|
||||
openEditHistoryDialog(edit)
|
||||
|
|
|
@ -9,13 +9,13 @@ const emit = defineEmits<{
|
|||
(event: 'change'): void
|
||||
}>()
|
||||
|
||||
const masto = useMasto()
|
||||
const { client } = $(useMasto())
|
||||
|
||||
const toggleFollowTag = async () => {
|
||||
if (tag.following)
|
||||
await masto.v1.tags.unfollow(tag.name)
|
||||
await client.v1.tags.unfollow(tag.name)
|
||||
else
|
||||
await masto.v1.tags.follow(tag.name)
|
||||
await client.v1.tags.follow(tag.name)
|
||||
|
||||
emit('change')
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
const paginator = useMasto().v1.blocks.list()
|
||||
const paginator = useMastoClient().v1.blocks.list()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
const paginator = useMasto().v1.bookmarks.list()
|
||||
const paginator = useMastoClient().v1.bookmarks.list()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
const paginator = useMasto().v1.conversations.list()
|
||||
const paginator = useMastoClient().v1.conversations.list()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
const masto = useMasto()
|
||||
const paginator = masto.v1.domainBlocks.list()
|
||||
const { client } = $(useMasto())
|
||||
const paginator = client.v1.domainBlocks.list()
|
||||
|
||||
const unblock = async (domain: string) => {
|
||||
await masto.v1.domainBlocks.unblock(domain)
|
||||
await client.v1.domainBlocks.unblock(domain)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
const paginator = useMasto().v1.favourites.list()
|
||||
const paginator = useMastoClient().v1.favourites.list()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
const paginator = useMasto().v1.timelines.listHome({ limit: 30 })
|
||||
const stream = useMasto().v1.stream.streamUser()
|
||||
onBeforeUnmount(() => stream?.then(s => s.disconnect()))
|
||||
const paginator = useMastoClient().v1.timelines.listHome({ limit: 30 })
|
||||
const stream = $(useStreaming(client => client.v1.stream.streamUser()))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
// Default limit is 20 notifications, and servers are normally caped to 30
|
||||
const paginator = useMasto().v1.notifications.list({ limit: 30, types: ['mention'] })
|
||||
const paginator = useMastoClient().v1.notifications.list({ limit: 30, types: ['mention'] })
|
||||
const stream = $(useStreaming(client => client.v1.stream.streamUser()))
|
||||
|
||||
const { clearNotifications } = useNotifications()
|
||||
onActivated(clearNotifications)
|
||||
|
||||
const stream = useMasto().v1.stream.streamUser()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
const paginator = useMasto().v1.mutes.list()
|
||||
const paginator = useMastoClient().v1.mutes.list()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
// Default limit is 20 notifications, and servers are normally caped to 30
|
||||
const paginator = useMasto().v1.notifications.list({ limit: 30 })
|
||||
const paginator = useMastoClient().v1.notifications.list({ limit: 30 })
|
||||
const stream = useStreaming(client => client.v1.stream.streamUser())
|
||||
|
||||
const { clearNotifications } = useNotifications()
|
||||
onActivated(clearNotifications)
|
||||
|
||||
const stream = useMasto().v1.stream.streamUser()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -14,7 +14,7 @@ const { paginator, stream, account, buffer = 10 } = defineProps<{
|
|||
}>()
|
||||
|
||||
const { formatNumber } = useHumanReadableNumber()
|
||||
const virtualScroller = $(useFeatureFlag('experimentalVirtualScroller'))
|
||||
const virtualScroller = $(usePreferences('experimentalVirtualScroller'))
|
||||
|
||||
const showOriginSite = $computed(() =>
|
||||
account && account.id !== currentUser.value?.account.id && getServerName(account) !== currentServer.value,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
const paginator = useMasto().v1.accounts.listStatuses(currentUser.value!.account.id, { pinned: true })
|
||||
const paginator = useMastoClient().v1.accounts.listStatuses(currentUser.value!.account.id, { pinned: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
const paginator = useMasto().v1.timelines.listPublic({ limit: 30 })
|
||||
const stream = useMasto().v1.stream.streamPublicTimeline()
|
||||
onBeforeUnmount(() => stream.then(s => s.disconnect()))
|
||||
const paginator = useMastoClient().v1.timelines.listPublic({ limit: 30 })
|
||||
const stream = useStreaming(client => client.v1.stream.streamPublicTimeline())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
const paginator = useMasto().v1.timelines.listPublic({ limit: 30, local: true })
|
||||
const stream = useMasto().v1.stream.streamCommunityTimeline()
|
||||
onBeforeUnmount(() => stream.then(s => s.disconnect()))
|
||||
const paginator = useMastoClient().v1.timelines.listPublic({ limit: 30, local: true })
|
||||
const stream = useStreaming(client => client.v1.stream.streamCommunityTimeline())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -46,7 +46,9 @@ defineExpose({
|
|||
<div v-if="isPending || items.length" relative bg-base text-base shadow border="~ base rounded" text-sm py-2 overflow-x-hidden overflow-y-auto max-h-100>
|
||||
<template v-if="isPending">
|
||||
<div flex gap-1 items-center p2 animate-pulse>
|
||||
<div i-ri:loader-2-line animate-spin />
|
||||
<div animate-spin preserve-3d>
|
||||
<div i-ri:loader-2-line />
|
||||
</div>
|
||||
<span>Fetching...</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -46,7 +46,9 @@ defineExpose({
|
|||
<div v-if="isPending || items.length" relative bg-base text-base shadow border="~ base rounded" text-sm py-2 overflow-x-hidden overflow-y-auto max-h-100>
|
||||
<template v-if="isPending">
|
||||
<div flex gap-1 items-center p2 animate-pulse>
|
||||
<div i-ri:loader-2-line animate-spin />
|
||||
<div animate-spin preserve-3d>
|
||||
<div i-ri:loader-2-line />
|
||||
</div>
|
||||
<span>Fetching...</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<VDropdown :distance="0" placement="top-start">
|
||||
<VDropdown :distance="0" placement="top-start" strategy="fixed">
|
||||
<button btn-action-icon :aria-label="$t('action.switch_account')">
|
||||
<div :class="{ 'hidden xl:block': currentUser }" i-ri:more-2-line />
|
||||
<AccountAvatar v-if="currentUser" xl:hidden :account="currentUser.account" w-9 h-9 square />
|
||||
|
|
|
@ -2,14 +2,13 @@
|
|||
import type { UserLogin } from '~/types'
|
||||
|
||||
const all = useUsers()
|
||||
|
||||
const router = useRouter()
|
||||
const masto = useMasto()
|
||||
const switchUser = (user: UserLogin) => {
|
||||
|
||||
const clickUser = (user: UserLogin) => {
|
||||
if (user.account.id === currentUser.value?.account.id)
|
||||
router.push(getAccountRoute(user.account))
|
||||
else
|
||||
masto.loginTo(user)
|
||||
switchUser(user)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -24,7 +23,7 @@ const switchUser = (user: UserLogin) => {
|
|||
aria-label="Switch user"
|
||||
:class="user.account.id === currentUser?.account.id ? '' : 'op25 grayscale'"
|
||||
hover="filter-none op100"
|
||||
@click="switchUser(user)"
|
||||
@click="clickUser(user)"
|
||||
>
|
||||
<AccountAvatar w-13 h-13 :account="user.account" square />
|
||||
</button>
|
||||
|
|
|
@ -11,6 +11,9 @@ let knownServers = $ref<string[]>([])
|
|||
let autocompleteIndex = $ref(0)
|
||||
let autocompleteShow = $ref(false)
|
||||
|
||||
const users = useUsers()
|
||||
const userSettings = useUserSettings()
|
||||
|
||||
async function oauth() {
|
||||
if (busy)
|
||||
return
|
||||
|
@ -25,12 +28,15 @@ async function oauth() {
|
|||
server = server.split('/')[0]
|
||||
|
||||
try {
|
||||
location.href = await (globalThis.$fetch as any)(`/api/${server || publicServer.value}/login`, {
|
||||
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)
|
||||
|
@ -208,7 +214,10 @@ onClickOutside($$(input), () => {
|
|||
</span>
|
||||
</div>
|
||||
<button flex="~ row" gap-x-2 items-center btn-solid mt2 :disabled="!server || busy">
|
||||
<span aria-hidden="true" inline-block :class="busy ? 'i-ri:loader-2-fill animate animate-spin' : 'i-ri:login-circle-line'" class="rtl-flip" />
|
||||
<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>
|
||||
</form>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div p8 lg:flex="~ col gap2" hidden>
|
||||
<p v-if="isMastoInitialised" text-sm>
|
||||
<p v-if="isHydrated" text-sm>
|
||||
<i18n-t keypath="user.sign_in_notice_title">
|
||||
<strong>{{ currentServer }}</strong>
|
||||
</i18n-t>
|
||||
|
@ -8,7 +8,7 @@
|
|||
<p text-sm text-secondary>
|
||||
{{ $t('user.sign_in_desc') }}
|
||||
</p>
|
||||
<button btn-solid rounded-3 text-center mt-2 @click="openSigninDialog()">
|
||||
<button btn-solid rounded-3 text-center mt-2 select-none @click="openSigninDialog()">
|
||||
{{ $t('action.sign_in') }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -15,12 +15,11 @@ const sorted = computed(() => {
|
|||
})
|
||||
|
||||
const router = useRouter()
|
||||
const masto = useMasto()
|
||||
const switchUser = (user: UserLogin) => {
|
||||
const clickUser = (user: UserLogin) => {
|
||||
if (user.account.id === currentUser.value?.account.id)
|
||||
router.push(getAccountRoute(user.account))
|
||||
else
|
||||
masto.loginTo(user)
|
||||
switchUser(user)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -31,7 +30,7 @@ const switchUser = (user: UserLogin) => {
|
|||
flex rounded px4 py3 text-left
|
||||
hover:bg-active cursor-pointer transition-100
|
||||
aria-label="Switch user"
|
||||
@click="switchUser(user)"
|
||||
@click="clickUser(user)"
|
||||
>
|
||||
<AccountInfo :account="user.account" :hover-card="false" square />
|
||||
<div flex-auto />
|
||||
|
@ -45,7 +44,7 @@ const switchUser = (user: UserLogin) => {
|
|||
@click="openSigninDialog"
|
||||
/>
|
||||
<CommonDropdownItem
|
||||
v-if="isMastoInitialised && currentUser"
|
||||
v-if="isHydrated && currentUser"
|
||||
:text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])"
|
||||
icon="i-ri:logout-box-line rtl-flip"
|
||||
@click="signout"
|
||||
|
|
|
@ -19,11 +19,12 @@ function removeCached(key: string) {
|
|||
|
||||
export function fetchStatus(id: string, force = false): Promise<mastodon.v1.Status> {
|
||||
const server = currentServer.value
|
||||
const key = `${server}:status:${id}`
|
||||
const userId = currentUser.value?.account.id
|
||||
const key = `${server}:${userId}:status:${id}`
|
||||
const cached = cache.get(key)
|
||||
if (cached && !force)
|
||||
return cached
|
||||
const promise = useMasto().v1.statuses.fetch(id)
|
||||
const promise = useMastoClient().v1.statuses.fetch(id)
|
||||
.then((status) => {
|
||||
cacheStatus(status)
|
||||
return status
|
||||
|
@ -37,12 +38,13 @@ export function fetchAccountById(id?: string | null): Promise<mastodon.v1.Accoun
|
|||
return Promise.resolve(null)
|
||||
|
||||
const server = currentServer.value
|
||||
const key = `${server}:account:${id}`
|
||||
const userId = currentUser.value?.account.id
|
||||
const key = `${server}:${userId}:account:${id}`
|
||||
const cached = cache.get(key)
|
||||
if (cached)
|
||||
return cached
|
||||
const domain = currentInstance.value?.uri
|
||||
const promise = useMasto().v1.accounts.fetch(id)
|
||||
const domain = currentInstance.value ? getInstanceDomain(currentInstance.value) : null
|
||||
const promise = useMastoClient().v1.accounts.fetch(id)
|
||||
.then((r) => {
|
||||
if (r.acct && !r.acct.includes('@') && domain)
|
||||
r.acct = `${r.acct}@${domain}`
|
||||
|
@ -56,16 +58,28 @@ 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 key = `${server}:account:${acct}`
|
||||
const userId = currentUser.value?.account.id
|
||||
const key = `${server}:${userId}:account:${acct}`
|
||||
const cached = cache.get(key)
|
||||
if (cached)
|
||||
return cached
|
||||
const domain = currentInstance.value?.uri
|
||||
const account = useMasto().v1.accounts.lookup({ acct })
|
||||
.then((r) => {
|
||||
if (r.acct && !r.acct.includes('@') && domain)
|
||||
r.acct = `${r.acct}@${domain}`
|
||||
const domain = currentInstance.value ? getInstanceDomain(currentInstance.value) : undefined
|
||||
|
||||
async function lookupAccount() {
|
||||
const client = useMastoClient()
|
||||
let account: mastodon.v1.Account
|
||||
if (!isGotoSocial.value)
|
||||
account = await client.v1.accounts.lookup({ acct })
|
||||
else
|
||||
account = (await client.v1.search({ q: `@${acct}`, type: 'accounts' })).accounts[0]
|
||||
|
||||
if (account.acct && !account.acct.includes('@') && domain)
|
||||
account.acct = `${account.acct}@${domain}`
|
||||
return account
|
||||
}
|
||||
|
||||
const account = lookupAccount()
|
||||
.then((r) => {
|
||||
cacheAccount(r, server, true)
|
||||
return r
|
||||
})
|
||||
|
@ -82,14 +96,17 @@ export function useAccountById(id?: string | null) {
|
|||
}
|
||||
|
||||
export function cacheStatus(status: mastodon.v1.Status, server = currentServer.value, override?: boolean) {
|
||||
setCached(`${server}:status:${status.id}`, status, override)
|
||||
const userId = currentUser.value?.account.id
|
||||
setCached(`${server}:${userId}:status:${status.id}`, status, override)
|
||||
}
|
||||
|
||||
export function removeCachedStatus(id: string, server = currentServer.value) {
|
||||
removeCached(`${server}:status:${id}`)
|
||||
const userId = currentUser.value?.account.id
|
||||
removeCached(`${server}:${userId}:status:${id}`)
|
||||
}
|
||||
|
||||
export function cacheAccount(account: mastodon.v1.Account, server = currentServer.value, override?: boolean) {
|
||||
setCached(`${server}:account:${account.id}`, account, override)
|
||||
setCached(`${server}:account:${account.acct}`, account, override)
|
||||
const userId = currentUser.value?.account.id
|
||||
setCached(`${server}:${userId}:account:${account.id}`, account, override)
|
||||
setCached(`${server}:${userId}:account:${account.acct}`, account, override)
|
||||
}
|
||||
|
|
|
@ -337,7 +337,7 @@ export const provideGlobalCommands = () => {
|
|||
icon: 'i-ri:user-shared-line',
|
||||
|
||||
onActivate() {
|
||||
masto.loginTo(user)
|
||||
loginTo(masto, user)
|
||||
},
|
||||
})))
|
||||
useCommand({
|
||||
|
|
|
@ -35,6 +35,12 @@ const sanitizer = sanitize({
|
|||
code: {
|
||||
class: filterClasses(/^language-\w+$/),
|
||||
},
|
||||
// other elements supported in glitch
|
||||
h1: {},
|
||||
ol: {},
|
||||
ul: {},
|
||||
li: {},
|
||||
em: {},
|
||||
})
|
||||
|
||||
/**
|
||||
|
@ -163,7 +169,7 @@ export function treeToText(input: Node): string {
|
|||
if ('children' in input)
|
||||
body = (input.children as Node[]).map(n => treeToText(n)).join('')
|
||||
|
||||
if (input.name === 'img') {
|
||||
if (input.name === 'img' || input.name === 'picture') {
|
||||
if (input.attributes.class?.includes('custom-emoji'))
|
||||
return `:${input.attributes['data-emoji-id']}:`
|
||||
if (input.attributes.class?.includes('iconify-emoji'))
|
||||
|
@ -320,11 +326,34 @@ function replaceCustomEmoji(customEmojis: Record<string, mastodon.v1.CustomEmoji
|
|||
if (i % 2 === 0)
|
||||
return name
|
||||
|
||||
const emoji = customEmojis[name]
|
||||
const emoji = customEmojis[name] as mastodon.v1.CustomEmoji
|
||||
if (!emoji)
|
||||
return `:${name}:`
|
||||
|
||||
return h('img', { 'src': emoji.url, 'alt': `:${name}:`, 'class': 'custom-emoji', 'data-emoji-id': name })
|
||||
return h(
|
||||
'picture',
|
||||
{
|
||||
'alt': `:${name}:`,
|
||||
'class': 'custom-emoji',
|
||||
'data-emoji-id': name,
|
||||
},
|
||||
[
|
||||
h(
|
||||
'source',
|
||||
{
|
||||
srcset: emoji.staticUrl,
|
||||
media: '(prefers-reduced-motion: reduce)',
|
||||
},
|
||||
),
|
||||
h(
|
||||
'img',
|
||||
{
|
||||
src: emoji.url,
|
||||
alt: `:${name}:`,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
}).filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,8 @@ export async function updateCustomEmojis() {
|
|||
if (Date.now() - currentCustomEmojis.value.lastUpdate < TTL)
|
||||
return
|
||||
|
||||
const masto = useMasto()
|
||||
const emojis = await masto.v1.customEmojis.list()
|
||||
const { client } = $(useMasto())
|
||||
const emojis = await client.v1.customEmojis.list()
|
||||
Object.assign(currentCustomEmojis.value, {
|
||||
lastUpdate: Date.now(),
|
||||
emojis,
|
||||
|
|
|
@ -3,6 +3,8 @@ import type { Ref } from 'vue'
|
|||
import { del, get, set, update } from 'idb-keyval'
|
||||
import type { UseIDBOptions } from '@vueuse/integrations/useIDBKeyval'
|
||||
|
||||
const isIDBSupported = !process.test && typeof indexedDB !== 'undefined'
|
||||
|
||||
export async function useAsyncIDBKeyval<T>(
|
||||
key: IDBValidKey,
|
||||
initialValue: MaybeComputedRef<T>,
|
||||
|
@ -22,6 +24,8 @@ export async function useAsyncIDBKeyval<T>(
|
|||
const rawInit: T = resolveUnref(initialValue)
|
||||
|
||||
async function read() {
|
||||
if (!isIDBSupported)
|
||||
return
|
||||
try {
|
||||
const rawValue = await get<T>(key)
|
||||
if (rawValue === undefined) {
|
||||
|
@ -40,6 +44,8 @@ export async function useAsyncIDBKeyval<T>(
|
|||
await read()
|
||||
|
||||
async function write() {
|
||||
if (!isIDBSupported)
|
||||
return
|
||||
try {
|
||||
if (data.value == null) {
|
||||
await del(key)
|
||||
|
|
|
@ -17,7 +17,7 @@ export function getServerName(account: mastodon.v1.Account) {
|
|||
if (account.acct?.includes('@'))
|
||||
return account.acct.split('@')[1]
|
||||
// We should only lack the server name if we're on the same server as the account
|
||||
return currentInstance.value?.uri || ''
|
||||
return currentInstance.value ? getInstanceDomain(currentInstance.value) : ''
|
||||
}
|
||||
|
||||
export function getFullHandle(account: mastodon.v1.Account) {
|
||||
|
@ -38,7 +38,7 @@ export function toShortHandle(fullHandle: string) {
|
|||
|
||||
export function extractAccountHandle(account: mastodon.v1.Account) {
|
||||
let handle = getFullHandle(account).slice(1)
|
||||
const uri = currentInstance.value?.uri ?? currentServer.value
|
||||
const uri = currentInstance.value ? getInstanceDomain(currentInstance.value) : currentServer.value
|
||||
if (currentInstance.value && handle.endsWith(`@${uri}`))
|
||||
handle = handle.slice(0, -uri.length - 1)
|
||||
|
||||
|
|
|
@ -1,11 +1,115 @@
|
|||
import type { ElkMasto } from '~/types'
|
||||
import type { Pausable } from '@vueuse/core'
|
||||
import type { CreateClientParams, WsEvents, mastodon } from 'masto'
|
||||
import { createClient, fetchV1Instance } from 'masto'
|
||||
import type { Ref } from 'vue'
|
||||
import type { ElkInstance } from '../users'
|
||||
import type { Mutable } from '~/types/utils'
|
||||
import type { UserLogin } from '~/types'
|
||||
|
||||
export const createMasto = () => {
|
||||
let client = $shallowRef<mastodon.Client>(undefined as never)
|
||||
let params = $ref<Mutable<CreateClientParams>>()
|
||||
const canStreaming = $computed(() => !!params?.streamingApiUrl)
|
||||
|
||||
const setParams = (newParams: Partial<CreateClientParams>) => {
|
||||
const p = { ...params, ...newParams } as CreateClientParams
|
||||
client = createClient(p)
|
||||
params = p
|
||||
}
|
||||
|
||||
return {
|
||||
client: $$(client),
|
||||
params: readonly($$(params)),
|
||||
canStreaming: $$(canStreaming),
|
||||
setParams,
|
||||
}
|
||||
}
|
||||
export type ElkMasto = ReturnType<typeof createMasto>
|
||||
|
||||
export const useMasto = () => useNuxtApp().$masto as ElkMasto
|
||||
export const useMastoClient = () => useMasto().client.value
|
||||
|
||||
export const isMastoInitialised = computed(() => process.client && useMasto().loggedIn.value)
|
||||
export function mastoLogin(masto: ElkMasto, user: Pick<UserLogin, 'server' | 'token'>) {
|
||||
const { setParams } = $(masto)
|
||||
|
||||
export const onMastoInit = (cb: () => unknown) => {
|
||||
watchOnce(isMastoInitialised, () => {
|
||||
cb()
|
||||
}, { immediate: isMastoInitialised.value })
|
||||
const server = user.server
|
||||
const url = `https://${server}`
|
||||
const instance: ElkInstance = reactive(getInstanceCache(server) || { uri: server, accountDomain: server })
|
||||
setParams({
|
||||
url,
|
||||
accessToken: user?.token,
|
||||
disableVersionCheck: true,
|
||||
streamingApiUrl: instance?.urls?.streamingApi,
|
||||
})
|
||||
|
||||
fetchV1Instance({ url }).then((newInstance) => {
|
||||
Object.assign(instance, newInstance)
|
||||
setParams({
|
||||
streamingApiUrl: newInstance.urls.streamingApi,
|
||||
})
|
||||
instances.value[server] = newInstance
|
||||
})
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
interface UseStreamingOptions<Controls extends boolean> {
|
||||
/**
|
||||
* Expose more controls
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
controls?: Controls
|
||||
/**
|
||||
* Connect on calling
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
immediate?: boolean
|
||||
}
|
||||
|
||||
export function useStreaming(
|
||||
cb: (client: mastodon.Client) => Promise<WsEvents>,
|
||||
options: UseStreamingOptions<true>,
|
||||
): { stream: Ref<Promise<WsEvents> | undefined> } & Pausable
|
||||
export function useStreaming(
|
||||
cb: (client: mastodon.Client) => Promise<WsEvents>,
|
||||
options?: UseStreamingOptions<false>,
|
||||
): Ref<Promise<WsEvents> | undefined>
|
||||
export function useStreaming(
|
||||
cb: (client: mastodon.Client) => Promise<WsEvents>,
|
||||
{ immediate = true, controls }: UseStreamingOptions<boolean> = {},
|
||||
): ({ stream: Ref<Promise<WsEvents> | undefined> } & Pausable) | Ref<Promise<WsEvents> | undefined> {
|
||||
const { canStreaming, client } = useMasto()
|
||||
|
||||
const isActive = ref(immediate)
|
||||
const stream = ref<Promise<WsEvents>>()
|
||||
|
||||
function pause() {
|
||||
isActive.value = false
|
||||
}
|
||||
|
||||
function resume() {
|
||||
isActive.value = true
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (stream.value) {
|
||||
stream.value.then(s => s.disconnect()).catch(() => Promise.resolve())
|
||||
stream.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
cleanup()
|
||||
if (canStreaming.value && isActive.value)
|
||||
stream.value = cb(client.value)
|
||||
})
|
||||
|
||||
tryOnBeforeUnmount(() => isActive.value = false)
|
||||
|
||||
if (controls)
|
||||
return { stream, isActive, pause, resume }
|
||||
else
|
||||
return stream
|
||||
}
|
||||
|
|
|
@ -4,32 +4,39 @@ const notifications = reactive<Record<string, undefined | [Promise<WsEvents>, st
|
|||
|
||||
export const useNotifications = () => {
|
||||
const id = currentUser.value?.account.id
|
||||
const masto = useMasto()
|
||||
|
||||
const { client, canStreaming } = $(useMasto())
|
||||
|
||||
async function clearNotifications() {
|
||||
if (!id || !notifications[id])
|
||||
return
|
||||
const lastReadId = notifications[id]![1][0]
|
||||
notifications[id]![1] = []
|
||||
|
||||
await masto.v1.markers.create({
|
||||
notifications: { lastReadId },
|
||||
})
|
||||
if (lastReadId) {
|
||||
await client.v1.markers.create({
|
||||
notifications: { lastReadId },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function connect(): Promise<void> {
|
||||
if (!isMastoInitialised.value || !id || notifications[id] || !currentUser.value?.token)
|
||||
if (!isHydrated.value || !id || notifications[id] || !currentUser.value?.token)
|
||||
return
|
||||
|
||||
const stream = masto.v1.stream.streamUser()
|
||||
let resolveStream
|
||||
const stream = new Promise<WsEvents>(resolve => resolveStream = resolve)
|
||||
notifications[id] = [stream, []]
|
||||
|
||||
await until($$(canStreaming)).toBe(true)
|
||||
|
||||
client.v1.stream.streamUser().then(resolveStream)
|
||||
stream.then(s => s.on('notification', (n) => {
|
||||
if (notifications[id])
|
||||
notifications[id]![1].unshift(n.id)
|
||||
}))
|
||||
|
||||
const position = await masto.v1.markers.fetch({ timeline: ['notifications'] })
|
||||
const paginator = masto.v1.notifications.list({ limit: 30 })
|
||||
const position = await client.v1.markers.fetch({ timeline: ['notifications'] })
|
||||
const paginator = client.v1.notifications.list({ limit: 30 })
|
||||
do {
|
||||
const result = await paginator.next()
|
||||
if (!result.done && result.value.length) {
|
||||
|
@ -53,10 +60,10 @@ export const useNotifications = () => {
|
|||
}
|
||||
|
||||
watch(currentUser, disconnect)
|
||||
if (isMastoInitialised.value)
|
||||
|
||||
onHydrated(() => {
|
||||
connect()
|
||||
else
|
||||
watchOnce(isMastoInitialised, connect)
|
||||
})
|
||||
|
||||
return {
|
||||
notifications: computed(() => id ? notifications[id]?.[1].length ?? 0 : 0),
|
||||
|
|
|
@ -12,7 +12,7 @@ export const usePublish = (options: {
|
|||
}) => {
|
||||
const { expanded, isUploading, initialDraft } = $(options)
|
||||
let { draft, isEmpty } = $(options.draftState)
|
||||
const masto = useMasto()
|
||||
const { client } = $(useMasto())
|
||||
|
||||
let isSending = $ref(false)
|
||||
const isExpanded = $ref(false)
|
||||
|
@ -51,9 +51,9 @@ export const usePublish = (options: {
|
|||
|
||||
let status: mastodon.v1.Status
|
||||
if (!draft.editingStatus)
|
||||
status = await masto.v1.statuses.create(payload)
|
||||
status = await client.v1.statuses.create(payload)
|
||||
else
|
||||
status = await masto.v1.statuses.update(draft.editingStatus.id, payload)
|
||||
status = await client.v1.statuses.update(draft.editingStatus.id, payload)
|
||||
if (draft.params.inReplyToId)
|
||||
navigateToStatus({ status })
|
||||
|
||||
|
@ -83,7 +83,7 @@ export type MediaAttachmentUploadError = [filename: string, message: string]
|
|||
|
||||
export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
|
||||
const draft = $(draftRef)
|
||||
const masto = useMasto()
|
||||
const { client } = $(useMasto())
|
||||
const { t } = useI18n()
|
||||
|
||||
let isUploading = $ref<boolean>(false)
|
||||
|
@ -96,12 +96,12 @@ export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
|
|||
failedAttachments = []
|
||||
// TODO: display some kind of message if too many media are selected
|
||||
// DONE
|
||||
const limit = currentInstance.value!.configuration.statuses.maxMediaAttachments || 4
|
||||
const limit = currentInstance.value!.configuration?.statuses.maxMediaAttachments || 4
|
||||
for (const file of files.slice(0, limit)) {
|
||||
if (draft.attachments.length < limit) {
|
||||
isExceedingAttachmentLimit = false
|
||||
try {
|
||||
const attachment = await masto.v1.mediaAttachments.create({
|
||||
const attachment = await client.v1.mediaAttachments.create({
|
||||
file,
|
||||
})
|
||||
draft.attachments.push(attachment)
|
||||
|
@ -121,7 +121,7 @@ export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
|
|||
}
|
||||
|
||||
async function pickAttachments() {
|
||||
const mimeTypes = currentInstance.value!.configuration.mediaAttachments.supportedMimeTypes
|
||||
const mimeTypes = currentInstance.value!.configuration?.mediaAttachments.supportedMimeTypes
|
||||
const files = await fileOpen({
|
||||
description: 'Attachments',
|
||||
multiple: true,
|
||||
|
@ -132,7 +132,7 @@ export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
|
|||
|
||||
async function setDescription(att: mastodon.v1.MediaAttachment, description: string) {
|
||||
att.description = description
|
||||
await masto.v1.mediaAttachments.update(att.id, { description: att.description })
|
||||
await client.v1.mediaAttachments.update(att.id, { description: att.description })
|
||||
}
|
||||
|
||||
function removeAttachment(index: number) {
|
||||
|
|
|
@ -27,7 +27,7 @@ export function useRelationship(account: mastodon.v1.Account): Ref<mastodon.v1.R
|
|||
|
||||
async function fetchRelationships() {
|
||||
const requested = Array.from(requestedRelationships.entries()).filter(([, r]) => !r.value)
|
||||
const relationships = await useMasto().v1.accounts.fetchRelationships(requested.map(([id]) => id))
|
||||
const relationships = await useMastoClient().v1.accounts.fetchRelationships(requested.map(([id]) => id))
|
||||
for (let i = 0; i < requested.length; i++)
|
||||
requested[i][1].value = relationships[i]
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ export type SearchResult = HashTagSearchResult | AccountSearchResult | StatusSea
|
|||
|
||||
export function useSearch(query: MaybeComputedRef<string>, options: UseSearchOptions = {}) {
|
||||
const done = ref(false)
|
||||
const masto = useMasto()
|
||||
const { client } = $(useMasto())
|
||||
const loading = ref(false)
|
||||
const accounts = ref<AccountSearchResult[]>([])
|
||||
const hashtags = ref<HashTagSearchResult[]>([])
|
||||
|
@ -59,11 +59,11 @@ export function useSearch(query: MaybeComputedRef<string>, options: UseSearchOpt
|
|||
}
|
||||
|
||||
watch(() => resolveUnref(query), () => {
|
||||
loading.value = !!(q && isMastoInitialised.value)
|
||||
loading.value = !!(q && isHydrated.value)
|
||||
})
|
||||
|
||||
debouncedWatch(() => resolveUnref(query), async () => {
|
||||
if (!q || !isMastoInitialised.value)
|
||||
if (!q || !isHydrated.value)
|
||||
return
|
||||
|
||||
loading.value = true
|
||||
|
@ -72,7 +72,7 @@ export function useSearch(query: MaybeComputedRef<string>, options: UseSearchOpt
|
|||
* Based on the source it seems like modifying the params when calling next would result in a new search,
|
||||
* but that doesn't seem to be the case. So instead we just create a new paginator with the new params.
|
||||
*/
|
||||
paginator = masto.v2.search({
|
||||
paginator = client.v2.search({
|
||||
q,
|
||||
...resolveUnref(options),
|
||||
resolve: !!currentUser.value,
|
||||
|
@ -87,7 +87,7 @@ export function useSearch(query: MaybeComputedRef<string>, options: UseSearchOpt
|
|||
}, { debounce: 300 })
|
||||
|
||||
const next = async () => {
|
||||
if (!q || !isMastoInitialised.value || !paginator)
|
||||
if (!q || !isHydrated.value || !paginator)
|
||||
return
|
||||
|
||||
loading.value = true
|
||||
|
|
|
@ -9,7 +9,7 @@ export interface StatusActionsProps {
|
|||
|
||||
export function useStatusActions(props: StatusActionsProps) {
|
||||
let status = $ref<mastodon.v1.Status>({ ...props.status })
|
||||
const masto = useMasto()
|
||||
const { client } = $(useMasto())
|
||||
|
||||
watch(
|
||||
() => props.status,
|
||||
|
@ -61,7 +61,7 @@ export function useStatusActions(props: StatusActionsProps) {
|
|||
|
||||
const toggleReblog = () => toggleStatusAction(
|
||||
'reblogged',
|
||||
() => masto.v1.statuses[status.reblogged ? 'unreblog' : 'reblog'](status.id).then((res) => {
|
||||
() => client.v1.statuses[status.reblogged ? 'unreblog' : 'reblog'](status.id).then((res) => {
|
||||
if (status.reblogged)
|
||||
// returns the original status
|
||||
return res.reblog!
|
||||
|
@ -72,23 +72,23 @@ export function useStatusActions(props: StatusActionsProps) {
|
|||
|
||||
const toggleFavourite = () => toggleStatusAction(
|
||||
'favourited',
|
||||
() => masto.v1.statuses[status.favourited ? 'unfavourite' : 'favourite'](status.id),
|
||||
() => client.v1.statuses[status.favourited ? 'unfavourite' : 'favourite'](status.id),
|
||||
'favouritesCount',
|
||||
)
|
||||
|
||||
const toggleBookmark = () => toggleStatusAction(
|
||||
'bookmarked',
|
||||
() => masto.v1.statuses[status.bookmarked ? 'unbookmark' : 'bookmark'](status.id),
|
||||
() => client.v1.statuses[status.bookmarked ? 'unbookmark' : 'bookmark'](status.id),
|
||||
)
|
||||
|
||||
const togglePin = async () => toggleStatusAction(
|
||||
'pinned',
|
||||
() => masto.v1.statuses[status.pinned ? 'unpin' : 'pin'](status.id),
|
||||
() => client.v1.statuses[status.pinned ? 'unpin' : 'pin'](status.id),
|
||||
)
|
||||
|
||||
const toggleMute = async () => toggleStatusAction(
|
||||
'muted',
|
||||
() => masto.v1.statuses[status.muted ? 'unmute' : 'mute'](status.id),
|
||||
() => client.v1.statuses[status.muted ? 'unmute' : 'mute'](status.id),
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { STORAGE_KEY_DRAFTS } from '~/constants'
|
|||
import type { Draft, DraftMap } from '~/types'
|
||||
import type { Mutable } from '~/types/utils'
|
||||
|
||||
export const currentUserDrafts = process.server ? computed<DraftMap>(() => ({})) : useUserLocalStorage<DraftMap>(STORAGE_KEY_DRAFTS, () => ({}))
|
||||
export const currentUserDrafts = process.server || process.test ? computed<DraftMap>(() => ({})) : useUserLocalStorage<DraftMap>(STORAGE_KEY_DRAFTS, () => ({}))
|
||||
|
||||
export const builtinDraftKeys = [
|
||||
'dialog',
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import type { Paginator, WsEvents, mastodon } from 'masto'
|
||||
import type { Ref } from 'vue'
|
||||
import type { PaginatorState } from '~/types'
|
||||
|
||||
export function usePaginator<T, P, U = T>(
|
||||
_paginator: Paginator<T[], P>,
|
||||
stream?: Promise<WsEvents>,
|
||||
stream: Ref<Promise<WsEvents> | undefined>,
|
||||
eventType: 'notification' | 'update' = 'update',
|
||||
preprocess: (items: (T | U)[]) => U[] = items => items as unknown as U[],
|
||||
buffer = 10,
|
||||
|
@ -13,7 +14,7 @@ export function usePaginator<T, P, U = T>(
|
|||
// so clone it
|
||||
const paginator = _paginator.clone()
|
||||
|
||||
const state = ref<PaginatorState>(isMastoInitialised.value ? 'idle' : 'loading')
|
||||
const state = ref<PaginatorState>(isHydrated.value ? 'idle' : 'loading')
|
||||
const items = ref<U[]>([])
|
||||
const nextItems = ref<U[]>([])
|
||||
const prevItems = ref<T[]>([])
|
||||
|
@ -29,37 +30,39 @@ export function usePaginator<T, P, U = T>(
|
|||
prevItems.value = []
|
||||
}
|
||||
|
||||
stream?.then((s) => {
|
||||
s.on(eventType, (status) => {
|
||||
if ('uri' in status)
|
||||
watch(stream, (stream) => {
|
||||
stream?.then((s) => {
|
||||
s.on(eventType, (status) => {
|
||||
if ('uri' in status)
|
||||
cacheStatus(status, undefined, true)
|
||||
|
||||
const index = prevItems.value.findIndex((i: any) => i.id === status.id)
|
||||
if (index >= 0)
|
||||
prevItems.value.splice(index, 1)
|
||||
|
||||
prevItems.value.unshift(status as any)
|
||||
})
|
||||
|
||||
// TODO: update statuses
|
||||
s.on('status.update', (status) => {
|
||||
cacheStatus(status, undefined, true)
|
||||
|
||||
const index = prevItems.value.findIndex((i: any) => i.id === status.id)
|
||||
if (index >= 0)
|
||||
prevItems.value.splice(index, 1)
|
||||
const data = items.value as mastodon.v1.Status[]
|
||||
const index = data.findIndex(s => s.id === status.id)
|
||||
if (index >= 0)
|
||||
data[index] = status
|
||||
})
|
||||
|
||||
prevItems.value.unshift(status as any)
|
||||
s.on('delete', (id) => {
|
||||
removeCachedStatus(id)
|
||||
|
||||
const data = items.value as mastodon.v1.Status[]
|
||||
const index = data.findIndex(s => s.id === id)
|
||||
if (index >= 0)
|
||||
data.splice(index, 1)
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: update statuses
|
||||
s.on('status.update', (status) => {
|
||||
cacheStatus(status, undefined, true)
|
||||
|
||||
const data = items.value as mastodon.v1.Status[]
|
||||
const index = data.findIndex(s => s.id === status.id)
|
||||
if (index >= 0)
|
||||
data[index] = status
|
||||
})
|
||||
|
||||
s.on('delete', (id) => {
|
||||
removeCachedStatus(id)
|
||||
|
||||
const data = items.value as mastodon.v1.Status[]
|
||||
const index = data.findIndex(s => s.id === id)
|
||||
if (index >= 0)
|
||||
data.splice(index, 1)
|
||||
})
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
async function loadNext() {
|
||||
if (state.value !== 'idle')
|
||||
|
@ -101,8 +104,8 @@ export function usePaginator<T, P, U = T>(
|
|||
bound.update()
|
||||
}, 1000)
|
||||
|
||||
if (!isMastoInitialised.value) {
|
||||
onMastoInit(() => {
|
||||
if (!isHydrated.value) {
|
||||
onHydrated(() => {
|
||||
state.value = 'idle'
|
||||
loadNext()
|
||||
})
|
||||
|
|
|
@ -132,5 +132,5 @@ async function sendSubscriptionToBackend(
|
|||
data,
|
||||
}
|
||||
|
||||
return await useMasto().v1.webPushSubscriptions.create(params)
|
||||
return await useMastoClient().v1.webPushSubscriptions.create(params)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ const supportsPushNotifications = typeof window !== 'undefined'
|
|||
&& 'getKey' in PushSubscription.prototype
|
||||
|
||||
export const usePushManager = () => {
|
||||
const masto = useMasto()
|
||||
const { client } = $(useMasto())
|
||||
const isSubscribed = ref(false)
|
||||
const notificationPermission = ref<PermissionState | undefined>(
|
||||
Notification.permission === 'denied'
|
||||
|
@ -168,7 +168,7 @@ export const usePushManager = () => {
|
|||
if (policyChanged)
|
||||
await subscribe(data, policy, true)
|
||||
else
|
||||
currentUser.value.pushSubscription = await masto.v1.webPushSubscriptions.update({ data })
|
||||
currentUser.value.pushSubscription = await client.v1.webPushSubscriptions.update({ data })
|
||||
|
||||
policyChanged && await nextTick()
|
||||
|
||||
|
|
|
@ -1,45 +1,44 @@
|
|||
import { DEFAULT_FONT_SIZE, DEFAULT_LANGUAGE } from '~/constants'
|
||||
import { DEFAULT_FONT_SIZE } from '~/constants'
|
||||
|
||||
export type FontSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
export type ColorMode = 'light' | 'dark' | 'system'
|
||||
|
||||
export interface FeatureFlags {
|
||||
export interface PreferencesSettings {
|
||||
hideBoostCount: boolean
|
||||
hideFavoriteCount: boolean
|
||||
hideFollowerCount: boolean
|
||||
experimentalVirtualScroller: boolean
|
||||
experimentalGitHubCards: boolean
|
||||
experimentalUserPicker: boolean
|
||||
}
|
||||
|
||||
export interface WellnessSettings {
|
||||
hideBoostCount: boolean
|
||||
hideFavoriteCount: boolean
|
||||
hideFollowerCount: boolean
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
featureFlags: Partial<FeatureFlags>
|
||||
wellnessSettings: Partial<WellnessSettings>
|
||||
preferences: Partial<PreferencesSettings>
|
||||
colorMode?: ColorMode
|
||||
fontSize: FontSize
|
||||
language: string
|
||||
zenMode?: boolean
|
||||
zenMode: boolean
|
||||
}
|
||||
|
||||
export function getDefaultUserSettings(): UserSettings {
|
||||
export function getDefaultLanguage(languages: string[]) {
|
||||
if (process.server)
|
||||
return 'en-US'
|
||||
return matchLanguages(languages, navigator.languages) || 'en-US'
|
||||
}
|
||||
|
||||
export function getDefaultUserSettings(locales: string[]): UserSettings {
|
||||
return {
|
||||
language: DEFAULT_LANGUAGE,
|
||||
language: getDefaultLanguage(locales),
|
||||
fontSize: DEFAULT_FONT_SIZE,
|
||||
featureFlags: {},
|
||||
wellnessSettings: {},
|
||||
zenMode: false,
|
||||
preferences: {},
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_WELLNESS_SETTINGS: WellnessSettings = {
|
||||
export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = {
|
||||
hideBoostCount: false,
|
||||
hideFavoriteCount: false,
|
||||
hideFollowerCount: false,
|
||||
}
|
||||
|
||||
export const DEFAULT_FEATURE_FLAGS: FeatureFlags = {
|
||||
experimentalVirtualScroller: true,
|
||||
experimentalGitHubCards: true,
|
||||
experimentalUserPicker: true,
|
||||
|
|
|
@ -1,53 +1,35 @@
|
|||
import type { Ref } from 'vue'
|
||||
import type { FeatureFlags, UserSettings, WellnessSettings } from './definition'
|
||||
import type { VueI18n } from 'vue-i18n'
|
||||
import type { LocaleObject } from 'vue-i18n-routing'
|
||||
import type { PreferencesSettings, UserSettings } from './definition'
|
||||
import { STORAGE_KEY_SETTINGS } from '~/constants'
|
||||
|
||||
export const useUserSettings = () => {
|
||||
if (process.server)
|
||||
return useState('user-settings', getDefaultUserSettings)
|
||||
return useUserLocalStorage(STORAGE_KEY_SETTINGS, getDefaultUserSettings)
|
||||
export function useUserSettings() {
|
||||
const i18n = useNuxtApp().vueApp.config.globalProperties.$i18n as VueI18n
|
||||
const { locales } = i18n
|
||||
const supportLanguages = (locales as LocaleObject[]).map(locale => locale.code)
|
||||
return useUserLocalStorage<UserSettings>(STORAGE_KEY_SETTINGS, () => getDefaultUserSettings(supportLanguages))
|
||||
}
|
||||
|
||||
// TODO: refactor & simplify this
|
||||
|
||||
export function useWellnessSetting<T extends keyof WellnessSettings>(name: T): Ref<WellnessSettings[T]> {
|
||||
export function usePreferences<T extends keyof PreferencesSettings>(name: T): Ref<PreferencesSettings[T]> {
|
||||
const userSettings = useUserSettings()
|
||||
return computed({
|
||||
get() {
|
||||
return getWellnessSetting(userSettings.value, name)
|
||||
return getPreferences(userSettings.value, name)
|
||||
},
|
||||
set(value) {
|
||||
userSettings.value.wellnessSettings[name] = value
|
||||
userSettings.value.preferences[name] = value
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function getWellnessSetting<T extends keyof WellnessSettings>(userSettings: UserSettings, name: T): WellnessSettings[T] {
|
||||
return userSettings?.wellnessSettings?.[name] ?? DEFAULT_WELLNESS_SETTINGS[name]
|
||||
export function getPreferences<T extends keyof PreferencesSettings>(userSettings: UserSettings, name: T): PreferencesSettings[T] {
|
||||
return userSettings?.preferences?.[name] ?? DEFAULT__PREFERENCES_SETTINGS[name]
|
||||
}
|
||||
|
||||
export function toggleWellnessSetting(key: keyof WellnessSettings) {
|
||||
const flag = useWellnessSetting(key)
|
||||
flag.value = !flag.value
|
||||
}
|
||||
|
||||
export function useFeatureFlag<T extends keyof FeatureFlags>(name: T): Ref<FeatureFlags[T]> {
|
||||
const userSettings = useUserSettings()
|
||||
return computed({
|
||||
get() {
|
||||
return getFeatureFlag(userSettings.value, name)
|
||||
},
|
||||
set(value) {
|
||||
userSettings.value.featureFlags[name] = value
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function getFeatureFlag<T extends keyof FeatureFlags>(userSettings: UserSettings, name: T): FeatureFlags[T] {
|
||||
return userSettings?.featureFlags?.[name] ?? DEFAULT_FEATURE_FLAGS[name]
|
||||
}
|
||||
|
||||
export function toggleFeatureFlag(key: keyof FeatureFlags) {
|
||||
const flag = useFeatureFlag(key)
|
||||
export function togglePreferences(key: keyof PreferencesSettings) {
|
||||
const flag = usePreferences(key)
|
||||
flag.value = !flag.value
|
||||
}
|
||||
|
|
|
@ -32,10 +32,11 @@ export function useHightlighter(lang: Lang) {
|
|||
.then(() => {
|
||||
registeredLang.value.set(lang, true)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(`[shiki] Failed to load language ${lang}`)
|
||||
console.error(e)
|
||||
registeredLang.value.set(lang, false)
|
||||
.catch(() => {
|
||||
const fallbackLang = 'md'
|
||||
shiki.value?.loadLanguage(fallbackLang).then(() => {
|
||||
registeredLang.value.set(fallbackLang, true)
|
||||
})
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
@ -47,10 +48,22 @@ export function useShikiTheme() {
|
|||
return useColorMode().value === 'dark' ? 'vitesse-dark' : 'vitesse-light'
|
||||
}
|
||||
|
||||
const HTML_ENTITIES = {
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&',
|
||||
'\'': ''',
|
||||
'"': '"',
|
||||
} as Record<string, string>
|
||||
|
||||
function escapeHtml(text: string) {
|
||||
return text.replace(/[<>&'"]/g, ch => HTML_ENTITIES[ch])
|
||||
}
|
||||
|
||||
export function highlightCode(code: string, lang: Lang) {
|
||||
const shiki = useHightlighter(lang)
|
||||
if (!shiki)
|
||||
return code
|
||||
return escapeHtml(code)
|
||||
|
||||
return shiki.codeToHtml(code, {
|
||||
lang,
|
||||
|
|
|
@ -43,7 +43,7 @@ function getDecorations({
|
|||
findChildren(doc, node => node.type.name === name)
|
||||
.forEach((block) => {
|
||||
let from = block.pos + 1
|
||||
const language = block.node.attrs.language || 'text'
|
||||
const language = block.node.attrs.language
|
||||
|
||||
const shiki = useHightlighter(language)
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ export const MentionSuggestion: Partial<SuggestionOptions> = {
|
|||
if (query.length === 0)
|
||||
return []
|
||||
|
||||
const results = await useMasto().v2.search({ q: query, type: 'accounts', limit: 25, resolve: true })
|
||||
const results = await useMastoClient().v2.search({ q: query, type: 'accounts', limit: 25, resolve: true })
|
||||
return results.accounts
|
||||
},
|
||||
render: createSuggestionRenderer(TiptapMentionList),
|
||||
|
@ -36,7 +36,7 @@ export const HashtagSuggestion: Partial<SuggestionOptions> = {
|
|||
if (query.length === 0)
|
||||
return []
|
||||
|
||||
const results = await useMasto().v2.search({
|
||||
const results = await useMastoClient().v2.search({
|
||||
q: query,
|
||||
type: 'hashtags',
|
||||
limit: 25,
|
||||
|
@ -113,10 +113,13 @@ function createSuggestionRenderer(component: Component): SuggestionOptions['rend
|
|||
|
||||
// Use arrow function here because Nuxt will transform it incorrectly as Vue hook causing the build to fail
|
||||
onBeforeUpdate: (props) => {
|
||||
renderer.updateProps({ ...props, isPending: true })
|
||||
props.editor.isFocused && renderer.updateProps({ ...props, isPending: true })
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
if (!props.editor.isFocused)
|
||||
return
|
||||
|
||||
renderer.updateProps({ ...props, isPending: false })
|
||||
|
||||
if (!props.clientRect)
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { createClient, fetchV1Instance } from 'masto'
|
||||
import type { mastodon } from 'masto'
|
||||
import type { Ref } from 'vue'
|
||||
import type { EffectScope, Ref } from 'vue'
|
||||
import type { MaybeComputedRef, RemovableRef } from '@vueuse/core'
|
||||
import type { ElkMasto, UserLogin } from '~/types'
|
||||
import type { ElkMasto } from './masto/masto'
|
||||
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,
|
||||
STORAGE_KEY_NOTIFICATION_POLICY,
|
||||
STORAGE_KEY_SERVERS,
|
||||
|
@ -40,9 +43,17 @@ const initializeUsers = async (): Promise<Ref<UserLogin[]> | RemovableRef<UserLo
|
|||
}
|
||||
|
||||
const users = await initializeUsers()
|
||||
const instances = useLocalStorage<Record<string, mastodon.v1.Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true })
|
||||
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 : '')
|
||||
|
||||
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 currentUser = computed<UserLogin | undefined>(() => {
|
||||
if (currentUserId.value) {
|
||||
const user = users.value.find(user => user.account?.id === currentUserId.value)
|
||||
|
@ -53,13 +64,20 @@ export const currentUser = computed<UserLogin | undefined>(() => {
|
|||
return users.value[0]
|
||||
})
|
||||
|
||||
const publicInstance = ref<mastodon.v1.Instance | null>(null)
|
||||
export const currentInstance = computed<null | mastodon.v1.Instance>(() => currentUser.value ? instances.value[currentUser.value.server] ?? null : publicInstance.value)
|
||||
export const isGlitchEdition = computed(() => currentInstance.value?.version.includes('+glitch'))
|
||||
const publicInstance = ref<ElkInstance | null>(null)
|
||||
export const currentInstance = computed<null | ElkInstance>(() => currentUser.value ? instances.value[currentUser.value.server] ?? null : publicInstance.value)
|
||||
|
||||
export function getInstanceDomain(instance: ElkInstance) {
|
||||
return instance.accountDomain || instance.uri
|
||||
}
|
||||
|
||||
export const publicServer = ref('')
|
||||
export const currentServer = computed<string>(() => currentUser.value?.server || publicServer.value)
|
||||
|
||||
export const currentNodeInfo = computed<null | Record<string, any>>(() => nodes.value[currentServer.value] || null)
|
||||
export const isGotoSocial = computed(() => currentNodeInfo.value?.software?.name === 'gotosocial')
|
||||
export const isGlitchEdition = computed(() => currentInstance.value?.version?.includes('+glitch'))
|
||||
|
||||
// when multiple tabs: we need to reload window when sign in, switch account or sign out
|
||||
if (process.client) {
|
||||
const windowReload = () => {
|
||||
|
@ -89,102 +107,81 @@ if (process.client) {
|
|||
window.addEventListener('visibilitychange', windowReload, { capture: true })
|
||||
}
|
||||
}, { immediate: true, flush: 'post' })
|
||||
}
|
||||
|
||||
export const currentUserHandle = computed(() => currentUser.value?.account.id
|
||||
? `${currentUser.value.account.acct}@${currentInstance.value?.uri || currentServer.value}`
|
||||
: '[anonymous]',
|
||||
)
|
||||
// for injected script to read
|
||||
const currentUserHandle = computed(() => currentUser.value?.account.acct || '')
|
||||
watchEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEY_CURRENT_USER_HANDLE, currentUserHandle.value)
|
||||
})
|
||||
}
|
||||
|
||||
export const useUsers = () => users
|
||||
export const useSelfAccount = (user: MaybeComputedRef<mastodon.v1.Account | undefined>) =>
|
||||
computed(() => currentUser.value && resolveUnref(user)?.id === currentUser.value.account.id)
|
||||
|
||||
export const characterLimit = computed(() => currentInstance.value?.configuration.statuses.maxCharacters ?? DEFAULT_POST_CHARS_LIMIT)
|
||||
export const characterLimit = computed(() => currentInstance.value?.configuration?.statuses.maxCharacters ?? DEFAULT_POST_CHARS_LIMIT)
|
||||
|
||||
async function loginTo(user?: Omit<UserLogin, 'account'> & { account?: mastodon.v1.AccountCredentials }) {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const server = user?.server || route.params.server as string || publicServer.value
|
||||
const url = `https://${server}`
|
||||
const instance = await fetchV1Instance({
|
||||
url,
|
||||
})
|
||||
const masto = createClient({
|
||||
url,
|
||||
streamingApiUrl: instance.urls.streamingApi,
|
||||
accessToken: user?.token,
|
||||
disableVersionCheck: true,
|
||||
})
|
||||
export async function loginTo(masto: ElkMasto, user: Overwrite<UserLogin, { account?: mastodon.v1.AccountCredentials }>) {
|
||||
const { client } = $(masto)
|
||||
const instance = mastoLogin(masto, user)
|
||||
|
||||
// GoToSocial only API
|
||||
const url = `https://${user.server}`
|
||||
fetch(`${url}/nodeinfo/2.0`).then(r => r.json()).then((info) => {
|
||||
nodes.value[user.server] = info
|
||||
}).catch(() => undefined)
|
||||
|
||||
if (!user?.token) {
|
||||
publicServer.value = server
|
||||
publicServer.value = user.server
|
||||
publicInstance.value = instance
|
||||
return
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
return users.value.find(u => u.server === user.server && u.token === user.token)
|
||||
}
|
||||
|
||||
const account = getUser()?.account
|
||||
if (account)
|
||||
currentUserId.value = account.id
|
||||
|
||||
const [me, pushSubscription] = await Promise.all([
|
||||
fetchAccountInfo(client, user.server),
|
||||
// if PWA is not enabled, don't get push subscription
|
||||
useRuntimeConfig().public.pwaEnabled
|
||||
// we get 404 response instead empty data
|
||||
? client.v1.webPushSubscriptions.fetch().catch(() => Promise.resolve(undefined))
|
||||
: Promise.resolve(undefined),
|
||||
])
|
||||
|
||||
const existingUser = getUser()
|
||||
if (existingUser) {
|
||||
existingUser.account = me
|
||||
existingUser.pushSubscription = pushSubscription
|
||||
}
|
||||
else {
|
||||
try {
|
||||
const [me, pushSubscription] = await Promise.all([
|
||||
masto.v1.accounts.verifyCredentials(),
|
||||
// if PWA is not enabled, don't get push subscription
|
||||
useRuntimeConfig().public.pwaEnabled
|
||||
// we get 404 response instead empty data
|
||||
? masto.v1.webPushSubscriptions.fetch().catch(() => Promise.resolve(undefined))
|
||||
: Promise.resolve(undefined),
|
||||
])
|
||||
|
||||
if (!me.acct.includes('@'))
|
||||
me.acct = `${me.acct}@${instance.uri}`
|
||||
|
||||
user.account = me
|
||||
user.pushSubscription = pushSubscription
|
||||
currentUserId.value = me.id
|
||||
instances.value[server] = instance
|
||||
|
||||
if (!users.value.some(u => u.server === user.server && u.token === user.token))
|
||||
users.value.push(user as UserLogin)
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
await signout()
|
||||
}
|
||||
}
|
||||
|
||||
// This only cleans up the URL; page content should stay the same
|
||||
if (route.path === '/signin/callback') {
|
||||
await router.push('/home')
|
||||
}
|
||||
|
||||
else if ('server' in route.params && user?.token && !useNuxtApp()._processingMiddleware) {
|
||||
await router.push({
|
||||
...route,
|
||||
force: true,
|
||||
users.value.push({
|
||||
...user,
|
||||
account: me,
|
||||
pushSubscription,
|
||||
})
|
||||
}
|
||||
|
||||
return masto
|
||||
currentUserId.value = me.id
|
||||
}
|
||||
|
||||
export function setAccountInfo(userId: string, account: mastodon.v1.AccountCredentials) {
|
||||
const index = getUsersIndexByUserId(userId)
|
||||
if (index === -1)
|
||||
return false
|
||||
|
||||
users.value[index].account = account
|
||||
return true
|
||||
}
|
||||
|
||||
export async function pullMyAccountInfo() {
|
||||
const account = await useMasto().v1.accounts.verifyCredentials()
|
||||
export async function fetchAccountInfo(client: mastodon.Client, server: string) {
|
||||
const account = await client.v1.accounts.verifyCredentials()
|
||||
if (!account.acct.includes('@'))
|
||||
account.acct = `${account.acct}@${currentInstance.value!.uri}`
|
||||
|
||||
setAccountInfo(currentUserId.value, account)
|
||||
cacheAccount(account, currentServer.value, true)
|
||||
account.acct = `${account.acct}@${server}`
|
||||
cacheAccount(account, server, true)
|
||||
return account
|
||||
}
|
||||
|
||||
export function getUsersIndexByUserId(userId: string) {
|
||||
return users.value.findIndex(u => u.account?.id === userId)
|
||||
export async function refreshAccountInfo() {
|
||||
const account = await fetchAccountInfo(useMastoClient(), currentServer.value)
|
||||
currentUser.value!.account = account
|
||||
return account
|
||||
}
|
||||
|
||||
export async function removePushNotificationData(user: UserLogin, fromSWPushManager = true) {
|
||||
|
@ -221,7 +218,23 @@ export async function removePushNotifications(user: UserLogin) {
|
|||
return
|
||||
|
||||
// unsubscribe push notifications
|
||||
await useMasto().v1.webPushSubscriptions.remove().catch(() => Promise.resolve())
|
||||
await useMastoClient().v1.webPushSubscriptions.remove().catch(() => Promise.resolve())
|
||||
}
|
||||
|
||||
export async function switchUser(user: UserLogin) {
|
||||
const masto = useMasto()
|
||||
|
||||
await loginTo(masto, user)
|
||||
|
||||
// This only cleans up the URL; page content should stay the same
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
if ('server' in route.params && user?.token && !useNuxtApp()._processingMiddleware) {
|
||||
await router.push({
|
||||
...route,
|
||||
force: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function signout() {
|
||||
|
@ -256,7 +269,7 @@ export async function signout() {
|
|||
if (!currentUserId.value)
|
||||
await useRouter().push('/')
|
||||
|
||||
await masto.loginTo(currentUser.value)
|
||||
loginTo(masto, currentUser.value)
|
||||
}
|
||||
|
||||
export function checkLogin() {
|
||||
|
@ -267,18 +280,37 @@ export function checkLogin() {
|
|||
return true
|
||||
}
|
||||
|
||||
interface UseUserLocalStorageCache {
|
||||
scope: EffectScope
|
||||
value: Ref<Record<string, any>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Create reactive storage for the current user
|
||||
*/
|
||||
export function useUserLocalStorage<T extends object>(key: string, initial: () => T) {
|
||||
const all = useLocalStorage<Record<string, T>>(key, {}, { deep: true })
|
||||
return computed(() => {
|
||||
const id = currentUser.value?.account.id
|
||||
? currentUser.value.account.acct
|
||||
: '[anonymous]'
|
||||
all.value[id] = Object.assign(initial(), all.value[id] || {})
|
||||
return all.value[id]
|
||||
})
|
||||
export function useUserLocalStorage<T extends object>(key: string, initial: () => T): Ref<T> {
|
||||
if (process.server || process.test)
|
||||
return shallowRef(initial())
|
||||
|
||||
// @ts-expect-error bind value to the function
|
||||
const map: Map<string, UseUserLocalStorageCache> = useUserLocalStorage._ = useUserLocalStorage._ || new Map()
|
||||
|
||||
if (!map.has(key)) {
|
||||
const scope = effectScope(true)
|
||||
const value = scope.run(() => {
|
||||
const all = useLocalStorage<Record<string, T>>(key, {}, { deep: true })
|
||||
return computed(() => {
|
||||
const id = currentUser.value?.account.id
|
||||
? currentUser.value.account.acct
|
||||
: '[anonymous]'
|
||||
all.value[id] = Object.assign(initial(), all.value[id] || {})
|
||||
return all.value[id]
|
||||
})
|
||||
})
|
||||
map.set(key, { scope, value: value! })
|
||||
}
|
||||
|
||||
return map.get(key)!.value as Ref<T>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -291,63 +323,11 @@ export function clearUserLocalStorage(account?: mastodon.v1.Account) {
|
|||
return
|
||||
|
||||
const id = `${account.acct}@${currentInstance.value?.uri || currentServer.value}`
|
||||
|
||||
// @ts-expect-error bind value to the function
|
||||
;(useUserLocalStorage._ as Map<string, Ref<Record<string, any>>> | undefined)?.forEach((storage) => {
|
||||
if (storage.value[id])
|
||||
delete storage.value[id]
|
||||
const cacheMap = useUserLocalStorage._ as Map<string, UseUserLocalStorageCache> | undefined
|
||||
cacheMap?.forEach(({ value }) => {
|
||||
if (value.value[id])
|
||||
delete value.value[id]
|
||||
})
|
||||
}
|
||||
|
||||
export const createMasto = () => {
|
||||
const api = shallowRef<mastodon.Client | null>(null)
|
||||
const apiPromise = ref<Promise<mastodon.Client> | null>(null)
|
||||
const initialised = computed(() => !!api.value)
|
||||
|
||||
const masto = new Proxy({} as ElkMasto, {
|
||||
get(_, key: keyof ElkMasto) {
|
||||
if (key === 'loggedIn')
|
||||
return initialised
|
||||
|
||||
if (key === 'loginTo') {
|
||||
return (...args: any[]): Promise<mastodon.Client> => {
|
||||
return apiPromise.value = loginTo(...args).then((r) => {
|
||||
api.value = r
|
||||
return masto
|
||||
}).catch(() => {
|
||||
// Show error page when Mastodon server is down
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusMessage: 'Could not log into account.',
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (api.value && key in api.value)
|
||||
return api.value[key as keyof mastodon.Client]
|
||||
|
||||
if (!api.value) {
|
||||
return new Proxy({}, {
|
||||
get(_, subkey) {
|
||||
if (typeof subkey === 'string' && subkey.startsWith('iterate')) {
|
||||
return (...args: any[]) => {
|
||||
let paginator: any
|
||||
function next() {
|
||||
paginator = paginator || (api.value as any)?.[key][subkey](...args)
|
||||
return paginator.next()
|
||||
}
|
||||
return { next }
|
||||
}
|
||||
}
|
||||
|
||||
return (...args: any[]) => apiPromise.value?.then((r: any) => r[key][subkey](...args))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
})
|
||||
|
||||
return masto
|
||||
}
|
||||
|
|
|
@ -6,6 +6,10 @@ import { useHead } from '#head'
|
|||
|
||||
export const isHydrated = ref(false)
|
||||
|
||||
export const onHydrated = (cb: () => unknown) => {
|
||||
watchOnce(isHydrated, () => cb(), { immediate: isHydrated.value })
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Whether the current component is running in the background
|
||||
*
|
||||
|
|
24
composables/web-share-target.ts
Normal file
24
composables/web-share-target.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
export function useWebShareTarget(listener?: (message: MessageEvent) => void) {
|
||||
if (process.server)
|
||||
return
|
||||
|
||||
onBeforeMount(() => {
|
||||
// PWA must be installed to use share target
|
||||
if (useNuxtApp().$pwa.isInstalled && 'serviceWorker' in navigator) {
|
||||
if (listener)
|
||||
navigator.serviceWorker.addEventListener('message', listener)
|
||||
|
||||
navigator.serviceWorker.getRegistration()
|
||||
.then((registration) => {
|
||||
if (registration && registration.active) {
|
||||
// we need to signal the service worker that we are ready to receive data
|
||||
registration.active.postMessage({ action: 'ready-to-receive' })
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Could not get registration', err))
|
||||
|
||||
if (listener)
|
||||
onBeforeUnmount(() => navigator.serviceWorker.removeEventListener('message', listener))
|
||||
}
|
||||
})
|
||||
}
|
|
@ -20,6 +20,16 @@ const locales: LocaleObjectData[] = [
|
|||
file: 'en-GB.json',
|
||||
name: 'English (UK)',
|
||||
},
|
||||
({
|
||||
code: 'ar-EG',
|
||||
file: 'ar-EG.json',
|
||||
name: 'العربية',
|
||||
dir: 'rtl',
|
||||
pluralRule: (choice: number) => {
|
||||
const name = new Intl.PluralRules('ar-EG').select(choice)
|
||||
return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name]
|
||||
},
|
||||
} satisfies LocaleObjectData),
|
||||
{
|
||||
code: 'de-DE',
|
||||
file: 'de-DE.json',
|
||||
|
@ -72,16 +82,16 @@ const locales: LocaleObjectData[] = [
|
|||
file: 'cs-CZ.json',
|
||||
name: 'Česky',
|
||||
},
|
||||
({
|
||||
code: 'ar-EG',
|
||||
file: 'ar-EG.json',
|
||||
name: 'العربية',
|
||||
dir: 'rtl',
|
||||
pluralRule: (choice: number) => {
|
||||
const name = new Intl.PluralRules('ar-EG').select(choice)
|
||||
return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name]
|
||||
},
|
||||
} satisfies LocaleObjectData),
|
||||
{
|
||||
code: 'pt-PT',
|
||||
file: 'pt-PT.json',
|
||||
name: 'Português',
|
||||
},
|
||||
{
|
||||
code: 'tr-TR',
|
||||
file: 'tr-TR.json',
|
||||
name: 'Türkçe',
|
||||
},
|
||||
].sort((a, b) => a.code.localeCompare(b.code))
|
||||
|
||||
const datetimeFormats = Object.values(locales).reduce((acc, data) => {
|
||||
|
|
|
@ -2,12 +2,13 @@ export const APP_NAME = 'Elk'
|
|||
|
||||
export const DEFAULT_POST_CHARS_LIMIT = 500
|
||||
export const DEFAULT_FONT_SIZE = 'md'
|
||||
export const DEFAULT_LANGUAGE = 'en-US'
|
||||
|
||||
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'
|
||||
export const STORAGE_KEY_SETTINGS = 'elk-settings'
|
||||
|
@ -20,7 +21,4 @@ export const STORAGE_KEY_NOTIFICATION_POLICY = 'elk-notification-policy'
|
|||
|
||||
export const COOKIE_MAX_AGE = 10 * 365 * 24 * 60 * 60 * 1000
|
||||
|
||||
export const COOKIE_KEY_FONT_SIZE = 'elk-font-size'
|
||||
export const COOKIE_KEY_LOCALE = 'elk-lang'
|
||||
|
||||
export const HANDLED_MASTO_URLS = /^(https?:\/\/)?([\w\d-]+\.)+\w+\/(@[@\w\d-\.]+)(\/objects)?(\/\d+)?$/
|
||||
|
|
10
docs/app.vue
10
docs/app.vue
|
@ -3,13 +3,3 @@
|
|||
<NuxtPage />
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'DM Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(/fonts/DM-sans-v11.ttf) format('truetype');
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { defineTheme, palette } from 'pinceau'
|
||||
|
||||
export default defineTheme({
|
||||
font: {
|
||||
sans: 'DM Sans',
|
||||
},
|
||||
color: {
|
||||
primary: palette('#d98018'),
|
||||
},
|
||||
|
|
10
error.vue
10
error.vue
|
@ -11,17 +11,17 @@ const errorCodes: Record<number, string> = {
|
|||
404: 'Page not found',
|
||||
}
|
||||
|
||||
if (process.dev)
|
||||
console.error(error)
|
||||
|
||||
const defaultMessage = 'Something went wrong'
|
||||
|
||||
const message = error.message ?? errorCodes[error.statusCode!] ?? defaultMessage
|
||||
|
||||
const state = ref<'error' | 'reloading'>('error')
|
||||
const masto = useMasto()
|
||||
const reload = async () => {
|
||||
state.value = 'reloading'
|
||||
try {
|
||||
if (!masto.loggedIn.value)
|
||||
await masto.loginTo(currentUser.value)
|
||||
clearError({ redirect: currentUser.value ? '/home' : `/${currentServer.value}/public/local` })
|
||||
}
|
||||
catch (err) {
|
||||
|
@ -47,7 +47,9 @@ const reload = async () => {
|
|||
{{ message }}
|
||||
</div>
|
||||
<button flex items-center gap-2 justify-center btn-solid text-center :disabled="state === 'reloading'">
|
||||
<span v-if="state === 'reloading'" i-ri:loader-2-fill animate-spin inline-block />
|
||||
<span v-if="state === 'reloading'" block animate-spin preserve-3d>
|
||||
<span block i-ri:loader-2-fill />
|
||||
</span>
|
||||
{{ state === 'reloading' ? 'Reloading' : 'Reload' }}
|
||||
</button>
|
||||
</form>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import { useFeatureFlag } from '~/composables/settings'
|
||||
import { usePreferences } from '~/composables/settings'
|
||||
|
||||
const route = useRoute()
|
||||
const userSettings = useUserSettings()
|
||||
|
@ -7,13 +7,13 @@ const userSettings = useUserSettings()
|
|||
const wideLayout = computed(() => route.meta.wideLayout ?? false)
|
||||
|
||||
const showUserPicker = logicAnd(
|
||||
useFeatureFlag('experimentalUserPicker'),
|
||||
usePreferences('experimentalUserPicker'),
|
||||
() => useUsers().value.length > 1,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h-full :class="{ zen: userSettings.zenMode }">
|
||||
<div h-full>
|
||||
<main flex w-full mxa lg:max-w-80rem>
|
||||
<aside class="hidden 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>
|
||||
<div sticky top-0 w-20 xl:w-100 h-screen flex="~ col" lt-xl-items-center>
|
||||
|
@ -22,7 +22,7 @@ const showUserPicker = logicAnd(
|
|||
<NavTitle />
|
||||
<NavSide command />
|
||||
<div flex-auto />
|
||||
<div v-if="isMastoInitialised" flex flex-col>
|
||||
<div v-if="isHydrated" flex flex-col>
|
||||
<div hidden xl:block>
|
||||
<UserSignInEntry v-if="!currentUser" />
|
||||
</div>
|
||||
|
|
|
@ -240,12 +240,6 @@
|
|||
"description": "قم بتحرير إعدادات حسابك في موقع Mastodon الأصلي",
|
||||
"label": "إعدادت الحساب"
|
||||
},
|
||||
"feature_flags": {
|
||||
"github_cards": "GitHub بطاقات",
|
||||
"title": "الميزات التجريبية",
|
||||
"user_picker": "الشريط الجانبي لمبدل المستخدم",
|
||||
"virtual_scroll": "التمرير الافتراضي"
|
||||
},
|
||||
"interface": {
|
||||
"color_mode": "وضع اللون",
|
||||
"dark_mode": "الوضع الداكن",
|
||||
|
@ -315,7 +309,14 @@
|
|||
},
|
||||
"notifications_settings": "التنبيهات",
|
||||
"preferences": {
|
||||
"label": "التفضيلات"
|
||||
"github_cards": "GitHub بطاقات",
|
||||
"hide_boost_count": "إخفاء عدد المشاركات",
|
||||
"hide_favorite_count": "إخفاء عدد المفضلة",
|
||||
"hide_follower_count": "إخفاء عدد المتابعين",
|
||||
"label": "التفضيلات",
|
||||
"title": "الميزات التجريبية",
|
||||
"user_picker": "الشريط الجانبي لمبدل المستخدم",
|
||||
"virtual_scroll": "التمرير الافتراضي"
|
||||
},
|
||||
"profile": {
|
||||
"appearance": {
|
||||
|
@ -338,14 +339,6 @@
|
|||
"export": "Export User Tokens",
|
||||
"import": "Import User Tokens",
|
||||
"label": "المستخدمون المسجلون"
|
||||
},
|
||||
"wellness": {
|
||||
"feature": {
|
||||
"hide_boost_count": "إخفاء عدد المشاركات",
|
||||
"hide_favorite_count": "إخفاء عدد المفضلة",
|
||||
"hide_follower_count": "إخفاء عدد المتابعين"
|
||||
},
|
||||
"label": "صحة النفس"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
{
|
||||
"a11y": {
|
||||
"loading_page": "Seite wird geladen, bitte warten",
|
||||
"loading_titled_page": "Seite {0} wird geladen, bitte warten",
|
||||
"locale_changed": "Sprache wurde zu {0} geändert",
|
||||
"locale_changing": "Sprache wird geändert, bitte warten",
|
||||
"route_loaded": "Seite {0} geladen"
|
||||
},
|
||||
"account": {
|
||||
"avatar_description": "{0}'s Avatar",
|
||||
"blocked_by": "Du wurdest von diesem Account geblockt",
|
||||
"blocked_domains": "Gesperrte Domänen",
|
||||
"blocked_users": "Gesperrte Accounts",
|
||||
"blocking": "Blockiert",
|
||||
"bot": "BOT",
|
||||
"favourites": "Favoriten",
|
||||
"follow": "Folgen",
|
||||
|
@ -18,29 +26,44 @@
|
|||
"joined": "Beigetreten",
|
||||
"moved_title": "hat angegeben, dass dies der neue Account ist:",
|
||||
"muted_users": "Stummgeschaltete Accounts",
|
||||
"muting": "Stummgeschaltet",
|
||||
"mutuals": "Freunde",
|
||||
"notify_on_post": "Benachrichtige mich, wenn {username} etwas postet",
|
||||
"pinned": "Angepinnt",
|
||||
"posts": "Beiträge",
|
||||
"posts_count": "{0} Beiträge",
|
||||
"profile_description": "{0}'s Profil",
|
||||
"profile_unavailable": "Profil nicht verfügbar",
|
||||
"unfollow": "Entfolgen"
|
||||
"unblock": "Entblocken",
|
||||
"unfollow": "Entfolgen",
|
||||
"unmute": "Stummschaltung aufheben",
|
||||
"view_other_followers": "Follower aus anderen Instanzen werden möglicherweise nicht angezeigt.",
|
||||
"view_other_following": "Das Folgen von anderen Instanzen wird möglicherweise nicht angezeigt."
|
||||
},
|
||||
"action": {
|
||||
"apply": "Anwenden",
|
||||
"bookmark": "Lesezeichen",
|
||||
"bookmarked": "Gemerkt",
|
||||
"boost": "Teilen",
|
||||
"boost_count": "{0}",
|
||||
"boosted": "Geteilt",
|
||||
"clear_upload_failed": "Fehler beim Hochladen von Dateien entfernen",
|
||||
"close": "Schliessen",
|
||||
"compose": "Verfassen",
|
||||
"confirm": "Bestätigen",
|
||||
"edit": "Bearbeiten",
|
||||
"enter_app": "App öffnen",
|
||||
"favourite": "Favorisieren",
|
||||
"favourite_count": "{0}",
|
||||
"favourited": "Favorisiert",
|
||||
"more": "Mehr",
|
||||
"next": "Nächster",
|
||||
"prev": "Vorheriger",
|
||||
"publish": "Veröffentlichen",
|
||||
"reply": "Antworten",
|
||||
"reply_count": "{0}",
|
||||
"reset": "Zurücksetzen",
|
||||
"save": "Speichern",
|
||||
"save_changes": "Änderungen speichern",
|
||||
"sign_in": "Anmelden",
|
||||
"switch_account": "Account wechseln",
|
||||
|
@ -49,6 +72,10 @@
|
|||
"app_desc_short": "Ein flinker Mastodon Web-Client",
|
||||
"app_logo": "Elk Logo",
|
||||
"app_name": "Elk",
|
||||
"attachment": {
|
||||
"edit_title": "Beschreibung",
|
||||
"remove_label": "Anhang entfernen"
|
||||
},
|
||||
"command": {
|
||||
"activate": "Aktivieren",
|
||||
"complete": "Vollständig",
|
||||
|
@ -62,25 +89,40 @@
|
|||
"toggle_zen_mode": "Zen-Modus ändern"
|
||||
},
|
||||
"common": {
|
||||
"confirm_dialog": {
|
||||
"cancel": "Abbrechen",
|
||||
"confirm": "OK",
|
||||
"title": "Bist du sicher, {0}?"
|
||||
},
|
||||
"end_of_list": "Ende der Liste",
|
||||
"error": "FEHLER",
|
||||
"in": "in",
|
||||
"not_found": "404 Nicht Gefunden",
|
||||
"offline_desc": "Anscheinend bist du offline. Bitte überprüfe deine Netzwerkverbindung."
|
||||
},
|
||||
"compose": {
|
||||
"draft_title": "Entwurf {0}",
|
||||
"drafts": "Entwürfe ({v})"
|
||||
},
|
||||
"conversation": {
|
||||
"with": "mit"
|
||||
},
|
||||
"error": {
|
||||
"account_not_found": "Account {0} nicht gefunden",
|
||||
"explore-list-empty": "Momentan ist nichts im Trend. Schau später nochmal vorbei!",
|
||||
"file_size_cannot_exceed_n_mb": "Die Dateigröße darf {0} MB nicht überschreiten",
|
||||
"sign_in_error": "Kann nicht zu Server verbinden",
|
||||
"status_not_found": "Beitrag nicht gefunden"
|
||||
"status_not_found": "Beitrag nicht gefunden",
|
||||
"unsupported_file_format": "Nicht unterstütztes Dateiformat"
|
||||
},
|
||||
"help": {
|
||||
"desc_highlight": "Erwarte hier und da einige Bugs und fehlende Funktionen.",
|
||||
"desc_para1": "Danke für dein Interesse in Elk, unser noch in der Bearbeitung befindliche, generische Mastodon Client!",
|
||||
"desc_para2": "Wir arbeiten hart an der Entwicklung und verbessern ihn mit der Zeit. Und wir laden dich schon sehr bald ein uns zu helfen, sobald wir ihn Quelloffen machen!",
|
||||
"desc_para3": "Doch in der Zwischenzeit kannst du der Entwicklung aushelfen, indem du unsere Teammitglieder durch die unten stehenden Links unterstützt.",
|
||||
"desc_para4": "Elk ist Open Source. \nWenn du beim Testen helfen, Feedback geben oder einen Beitrag leisten möchtest,",
|
||||
"desc_para5": "Kontaktiere uns auf GitHub",
|
||||
"desc_para6": "und mache mit.",
|
||||
"title": "Elk ist in der Alpha!"
|
||||
},
|
||||
"language": {
|
||||
|
@ -92,13 +134,22 @@
|
|||
"copy_link_to_post": "Link zu diesem Beitrag kopieren",
|
||||
"delete": "Löschen",
|
||||
"delete_and_redraft": "Löschen und neu erstellen",
|
||||
"delete_confirm": {
|
||||
"cancel": "Abbrechen",
|
||||
"confirm": "Löschen",
|
||||
"title": "Möchtest du diesen Beitrag wirklich löschen?"
|
||||
},
|
||||
"direct_message_account": "Direktnachricht an {0}",
|
||||
"edit": "Bearbeiten",
|
||||
"hide_reblogs": "Boosts von {0} ausblenden",
|
||||
"mention_account": "Erwähne {0}",
|
||||
"mute_account": "{0} stummschalten",
|
||||
"mute_conversation": "Diesem Beitrag stummschalten",
|
||||
"open_in_original_site": "Auf Originalseite öffnen",
|
||||
"pin_on_profile": "An Profil anpinnen",
|
||||
"share_post": "Teile diesen Beitrag",
|
||||
"show_favourited_and_boosted_by": "Zeige mir, wer favorisiert und geboostet hat",
|
||||
"show_reblogs": "Boosts von {0} anzeigen",
|
||||
"show_untranslated": "Übersetzung schliessen",
|
||||
"toggle_theme": {
|
||||
"dark": "Dunkles Farbschema aktivieren",
|
||||
|
@ -112,18 +163,24 @@
|
|||
"unpin_on_profile": "Von Profil lösen"
|
||||
},
|
||||
"nav": {
|
||||
"back": "Zurück",
|
||||
"blocked_domains": "Gesperrte Domänen",
|
||||
"blocked_users": "Blockierte Benutzer",
|
||||
"bookmarks": "Lesezeichen",
|
||||
"built_at": "Letzter Build: {0}",
|
||||
"compose": "Verfassen",
|
||||
"conversations": "Direktnachrichten",
|
||||
"explore": "Entdecken",
|
||||
"favourites": "Favoriten",
|
||||
"federated": "Föderiert",
|
||||
"home": "Startseite",
|
||||
"local": "Lokal",
|
||||
"muted_users": "Stummgeschaltete Benutzer",
|
||||
"notifications": "Mitteilungen",
|
||||
"profile": "Profil",
|
||||
"search": "Suche",
|
||||
"select_feature_flags": "Feature-Flags aktivieren",
|
||||
"select_font_size": "Schriftgröße",
|
||||
"select_language": "Sprache auswählen",
|
||||
"settings": "Einstellungen",
|
||||
"show_intro": "Intro anzeigen",
|
||||
|
@ -144,7 +201,36 @@
|
|||
"content_warning": "Schreib hier deine Warnung",
|
||||
"default_1": "Was geht dir gerade durch den Kopf?",
|
||||
"reply_to_account": "Antwort an {0}",
|
||||
"replying": "Antworten"
|
||||
"replying": "Antworten",
|
||||
"the_thread": "der Thread"
|
||||
},
|
||||
"pwa": {
|
||||
"dismiss": "Ignorieren",
|
||||
"title": "Neues Elk-Update verfügbar!",
|
||||
"update": "Aktualisieren",
|
||||
"update_available_short": "Elk aktualisieren",
|
||||
"webmanifest": {
|
||||
"canary": {
|
||||
"description": "Ein flinker Mastodon-Webclient (Canary)",
|
||||
"name": "Elk (canary)",
|
||||
"short_name": "Elk (canary)"
|
||||
},
|
||||
"dev": {
|
||||
"description": "Ein flinker Mastodon-Webclient (dev)",
|
||||
"name": "Elk (dev)",
|
||||
"short_name": "Elk (dev)"
|
||||
},
|
||||
"preview": {
|
||||
"description": "Ein flinker Mastodon-Webclient (Vorschau)",
|
||||
"name": "Elk (Vorschau)",
|
||||
"short_name": "Elk (Vorschau)"
|
||||
},
|
||||
"release": {
|
||||
"description": "Ein flinker Mastodon-Webclient",
|
||||
"name": "Elk",
|
||||
"short_name": "Elk"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"search_desc": "Suche nach Accounts & Hashtags",
|
||||
|
@ -152,10 +238,21 @@
|
|||
},
|
||||
"settings": {
|
||||
"about": {
|
||||
"label": "Info"
|
||||
"label": "Info",
|
||||
"meet_the_team": "Triff das Team",
|
||||
"sponsor_action": "Sponser uns",
|
||||
"sponsor_action_desc": "Um das Team bei der Entwicklung von Elk zu unterstützen",
|
||||
"sponsors": "Sponsoren",
|
||||
"sponsors_body_1": "Elk wird ermöglicht durch das großzügige Sponsoring und die Hilfe von:",
|
||||
"sponsors_body_2": "Und alle Unternehmen und Einzelpersonen, die das Elk Team und die Mitglieder sponsern.",
|
||||
"sponsors_body_3": "Wenn dir die App gefällt, erwäge uns zu sponsern:"
|
||||
},
|
||||
"account_settings": {
|
||||
"description": "Bearbeite Kontoeinstellungen in der Mastodon-Benutzeroberfläche",
|
||||
"label": "Account Einstellungen"
|
||||
},
|
||||
"feature_flags": {
|
||||
"github_cards": "GitHub-Karten",
|
||||
"github_cards": "GitHub Cards",
|
||||
"title": "Experimentelle Funktionen",
|
||||
"user_picker": "Benutzerauswahl",
|
||||
"virtual_scroll": "Virtuelles Scrollen"
|
||||
|
@ -166,14 +263,79 @@
|
|||
"default": " (Standard)",
|
||||
"font_size": "Schriftgröße",
|
||||
"label": "Oberfläche",
|
||||
"light_mode": "Helles Farbschema"
|
||||
"light_mode": "Helles Farbschema",
|
||||
"size_label": {
|
||||
"lg": "Groß",
|
||||
"md": "Mittel",
|
||||
"sm": "Klein",
|
||||
"xl": "Extra groß",
|
||||
"xs": "Extra klein"
|
||||
},
|
||||
"system_mode": "System"
|
||||
},
|
||||
"language": {
|
||||
"display_language": "Anzeigesprache",
|
||||
"label": "Sprache"
|
||||
},
|
||||
"notifications": {
|
||||
"label": "Benachrichtigungen",
|
||||
"notifications": {
|
||||
"label": "Benachrichtigungseinstellungen"
|
||||
},
|
||||
"push_notifications": {
|
||||
"alerts": {
|
||||
"favourite": "Favoriten",
|
||||
"follow": "Neue Follower",
|
||||
"mention": "Erwähnungen",
|
||||
"poll": "Umfragen",
|
||||
"reblog": "Reblogge deinen Beitrag",
|
||||
"title": "Welche Benachrichtigungen erhalten?"
|
||||
},
|
||||
"description": "Erhalte Benachrichtigungen, auch wenn du Elk nicht verwendest.",
|
||||
"instructions": "Vergesse nicht, Änderungen mit der Schaltfläche @:settings.notifications.push_notifications.save_settings zu speichern!",
|
||||
"label": "Einstellungen für Push-Benachrichtigungen",
|
||||
"policy": {
|
||||
"all": "Von irgendjemandem",
|
||||
"followed": "Von Accounts, denen ich folge",
|
||||
"follower": "Von Menschen, die mir folgen",
|
||||
"none": "Von niemandem",
|
||||
"title": "Von wem kann ich Benachrichtigungen erhalten?"
|
||||
},
|
||||
"save_settings": "Einstellungen speichern",
|
||||
"subscription_error": {
|
||||
"clear_error": "Fehler aufräumen",
|
||||
"permission_denied": "Berechtigung verweigert: Aktiviere Benachrichtigungen im Browser.",
|
||||
"request_error": "Beim Anfordern des Abonnements ist ein Fehler aufgetreten. Versuche es erneut. Wenn der Fehler weiterhin besteht, melde das Problem bitte dem Elk-Repository.",
|
||||
"title": "Push-Benachrichtigungen konnten nicht abonniert werden",
|
||||
"too_many_registrations": "Aufgrund von Browserbeschränkungen kann Elk den Push-Benachrichtigungsdienst nicht für mehrere Konten auf verschiedenen Servern verwenden. \nDu solltest Push-Benachrichtigungen für andere Konten abbestellen und es erneut versuchen."
|
||||
},
|
||||
"title": "Einstellungen für Push-Benachrichtigungen",
|
||||
"undo_settings": "Änderungen rückgängig machen",
|
||||
"unsubscribe": "Push-Benachrichtigungen deaktivieren",
|
||||
"unsupported": "Ihr Browser unterstützt keine Push-Benachrichtigungen.",
|
||||
"warning": {
|
||||
"enable_close": "Schließen",
|
||||
"enable_description": "Um Benachrichtigungen zu erhalten, wenn Elk nicht geöffnet ist, aktiviere Push-Benachrichtigungen. \nDu kannst genau steuern, welche Arten von Interaktionen Push-Benachrichtigungen generieren, indem du die Schaltfläche \"@:settings.notifications.show_btn{'\"'} oben aktivierest, sobald sie aktiviert ist.",
|
||||
"enable_description_desktop": "Um Benachrichtigungen zu erhalten, wenn Elk nicht geöffnet ist, aktiviere Push-Benachrichtigungen. \nDu kannst unter „Einstellungen > Benachrichtigungen > Einstellungen für Push-Benachrichtigungen“ genau steuern, welche Arten von Interaktionen Push-Benachrichtigungen generieren, sobald diese aktiviert sind.",
|
||||
"enable_description_mobile": "Du erreichst die Einstellungen auch über das Navigationsmenü „Einstellungen > Benachrichtigungen > Push-Benachrichtigungseinstellungen“.",
|
||||
"enable_description_settings": "Um Benachrichtigungen zu erhalten, wenn Elk nicht geöffnet ist, aktiviere Push-Benachrichtigungen. \nDu kannst genau steuern, welche Arten von Interaktionen Push-Benachrichtigungen auf demselben Bildschirm generieren, sobald du sie aktivierst.",
|
||||
"enable_desktop": "Aktiviere Push-Benachrichtigungen",
|
||||
"enable_title": "Verpasse nie wieder Benachrichtigungen",
|
||||
"re_auth": "Offenbar unterstützt dein Server keine Push-Benachrichtigungen. \nVersuche dich abzumelden und erneut anzumelden. Wenn diese Meldung weiterhin angezeigt wird, wende dich an dein Serveradministrator."
|
||||
}
|
||||
},
|
||||
"show_btn": "Gehe zu den Benachrichtigungseinstellungen"
|
||||
},
|
||||
"notifications_settings": "Benachrichtigungen",
|
||||
"preferences": {
|
||||
"label": "Einstellungen"
|
||||
"github_cards": "GitHub Cards",
|
||||
"hide_boost_count": "Boost-Zähler ausblenden",
|
||||
"hide_favorite_count": "Favoritenzahl ausblenden",
|
||||
"hide_follower_count": "Anzahl der Follower ausblenden",
|
||||
"label": "Einstellungen",
|
||||
"title": "Experimentelle Funktionen",
|
||||
"user_picker": "Benutzerauswahl",
|
||||
"virtual_scroll": "Virtuelles Scrollen"
|
||||
},
|
||||
"profile": {
|
||||
"appearance": {
|
||||
|
@ -198,14 +360,25 @@
|
|||
"label": "Eingeloggte Benutzer"
|
||||
}
|
||||
},
|
||||
"share-target": {
|
||||
"description": "Elk kann so konfiguriert werden, dass du Inhalte aus anderen Anwendungen teilen kannst, installiere einfach Elk auf deinem Gerät oder Computer und melden dich an.",
|
||||
"hint": "Um Inhalte mit Elk zu teilen, muss Elk installiert sein und du musst angemeldet sein.",
|
||||
"title": "Teile via Elk"
|
||||
},
|
||||
"state": {
|
||||
"attachments_exceed_server_limit": "Die Anzahl der Anhänge hat das Limit pro Beitrag überschritten.",
|
||||
"attachments_limit_error": "Limit pro Beitrag überschritten",
|
||||
"edited": "(bearbeitet)",
|
||||
"editing": "Bearbeiten",
|
||||
"loading": "Laden...",
|
||||
"publishing": "Veröffentlichung",
|
||||
"upload_failed": "Upload fehlgeschlagen",
|
||||
"uploading": "Hochladen..."
|
||||
},
|
||||
"status": {
|
||||
"boosted_by": "Boosted von",
|
||||
"edited": "Zuletzt bearbeitet: {0}",
|
||||
"favourited_by": "Favorisiert von",
|
||||
"filter_hidden_phrase": "Versteckt durch",
|
||||
"filter_removed_phrase": "Entfernt durch Filter",
|
||||
"filter_show_anyway": "Trotzdem zeigen",
|
||||
|
@ -219,9 +392,12 @@
|
|||
"finished": "Beendet: {0}"
|
||||
},
|
||||
"reblogged": "{0} teilte",
|
||||
"replying_to": "Antworten auf {0}",
|
||||
"show_full_thread": "Vollständigen Thread anzeigen",
|
||||
"someone": "Jemand",
|
||||
"spoiler_show_less": "Zeige weniger",
|
||||
"spoiler_show_more": "Zeige mehr",
|
||||
"thread": "Thread",
|
||||
"try_original_site": "Versuche die original Seite"
|
||||
},
|
||||
"status_history": {
|
||||
|
@ -276,7 +452,8 @@
|
|||
"year_past": "vor 0 Jahren|letztes Jahren|vor {n} Jahren"
|
||||
},
|
||||
"timeline": {
|
||||
"show_new_items": "Zeige {v} neue Beiträge|Zeige {v} neuen Beitrag|Zeige {v} neue Beiträge"
|
||||
"show_new_items": "Zeige {v} neue Beiträge|Zeige {v} neuen Beitrag|Zeige {v} neue Beiträge",
|
||||
"view_older_posts": "Ältere Beiträge aus anderen Instanzen werden möglicherweise nicht angezeigt."
|
||||
},
|
||||
"title": {
|
||||
"federated_timeline": "Föderierte Timeline",
|
||||
|
@ -284,9 +461,12 @@
|
|||
},
|
||||
"tooltip": {
|
||||
"add_content_warning": "Inhaltswarnung hinzufügen",
|
||||
"add_emojis": "Emojis hinzufügen",
|
||||
"add_media": "Bilder, ein Video oder eine Audiodatei hinzufügen",
|
||||
"add_publishable_content": "Füge Inhalte zum Veröffentlichen hinzu",
|
||||
"change_content_visibility": "Sichtbarkeit von Inhalten ändern",
|
||||
"change_language": "Sprache ändern",
|
||||
"emoji": "Emoji",
|
||||
"explore_links_intro": "Diese Nachrichten werden gerade von Leuten auf diesem und anderen Servern des dezentralen Netzwerks besprochen.",
|
||||
"explore_posts_intro": "Diese Beiträge von diesem Server gewinnen gerade unter den Leuten von diesem und anderen Servern des dezentralen Netzweks an Reichweite.",
|
||||
"explore_tags_intro": "Diese Hashtags gewinnen gerade unter den Leuten von diesem und anderen Servern des dezentralen Netzweks an Reichweite.",
|
||||
|
@ -296,6 +476,7 @@
|
|||
"add_existing": "Bestehendes Konto hinzufügen",
|
||||
"server_address_label": "Mastodon Server Adresse",
|
||||
"sign_in_desc": "Melde dich an, um Profilen oder Hashtags zu folgen, Beiträge zu favorisieren, zu teilen und zu beantworten oder von deinem Konto auf einem anderen Server aus zu interagieren.",
|
||||
"sign_in_notice_title": "Anzeigen von {0} öffentlichen Daten",
|
||||
"sign_out_account": "{0} abmelden",
|
||||
"tip_no_account": "Wenn du noch kein Mastodon-Konto hast, {0}.",
|
||||
"tip_register_account": "wähle einen Server aus und registriere eines"
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
"desc_highlight": "Expect some bugs and missing features here and there.",
|
||||
"desc_para1": "Thanks for your interest in trying out Elk, our work-in-progress Mastodon web client!",
|
||||
"desc_para2": "we are working hard on the development and improving it over time.",
|
||||
"desc_para3": "To help boosting out development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!",
|
||||
"desc_para3": "To boost development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!",
|
||||
"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.",
|
||||
|
@ -198,11 +198,11 @@
|
|||
},
|
||||
"interface": {
|
||||
"color_mode": "Color Mode",
|
||||
"dark_mode": "Dark Mode",
|
||||
"dark_mode": "Dark",
|
||||
"default": " (default)",
|
||||
"font_size": "Font Size",
|
||||
"label": "Interface",
|
||||
"light_mode": "Light Mode",
|
||||
"light_mode": "Light",
|
||||
"system_mode": "System"
|
||||
},
|
||||
"language": {
|
||||
|
@ -282,6 +282,11 @@
|
|||
"label": "Logged in users"
|
||||
}
|
||||
},
|
||||
"share-target": {
|
||||
"description": "Elk can be configured so that you can share content from other applications, simply install Elk on your device or computer and sign in.",
|
||||
"hint": "In order to share content with Elk, Elk must be installed and you must be signed in.",
|
||||
"title": "Share with Elk"
|
||||
},
|
||||
"state": {
|
||||
"attachments_exceed_server_limit": "The number of attachments exceeded the limit per post.",
|
||||
"attachments_limit_error": "Limit per post exceeded",
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"muted_users": "Muted users",
|
||||
"muting": "Muted",
|
||||
"mutuals": "Mutuals",
|
||||
"notify_on_post": "Notify me when {username} posts",
|
||||
"pinned": "Pinned",
|
||||
"posts": "Posts",
|
||||
"posts_count": "{0} Posts|{0} Post|{0} Posts",
|
||||
|
@ -35,7 +36,9 @@
|
|||
"profile_unavailable": "Profile unavailable",
|
||||
"unblock": "Unblock",
|
||||
"unfollow": "Unfollow",
|
||||
"unmute": "Unmute"
|
||||
"unmute": "Unmute",
|
||||
"view_other_followers": "Followers from other instances may not be displayed.",
|
||||
"view_other_following": "Following from other instances may not be displayed."
|
||||
},
|
||||
"action": {
|
||||
"apply": "Apply",
|
||||
|
@ -89,7 +92,7 @@
|
|||
"confirm_dialog": {
|
||||
"cancel": "No",
|
||||
"confirm": "Yes",
|
||||
"title": "Are you sure?"
|
||||
"title": "Are you sure {0}?"
|
||||
},
|
||||
"end_of_list": "End of the list",
|
||||
"error": "ERROR",
|
||||
|
@ -116,7 +119,7 @@
|
|||
"desc_highlight": "Expect some bugs and missing features here and there.",
|
||||
"desc_para1": "Thanks for your interest in trying out Elk, our work-in-progress Mastodon web client!",
|
||||
"desc_para2": "we are working hard on the development and improving it over time.",
|
||||
"desc_para3": "To help boosting out development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!",
|
||||
"desc_para3": "To boost development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!",
|
||||
"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.",
|
||||
|
@ -165,6 +168,7 @@
|
|||
"blocked_users": "Blocked users",
|
||||
"bookmarks": "Bookmarks",
|
||||
"built_at": "Built {0}",
|
||||
"compose": "Compose",
|
||||
"conversations": "Conversations",
|
||||
"explore": "Explore",
|
||||
"favourites": "Favorites",
|
||||
|
@ -247,19 +251,13 @@
|
|||
"description": "Edit your account settings in Mastodon UI",
|
||||
"label": "Account settings"
|
||||
},
|
||||
"feature_flags": {
|
||||
"github_cards": "GitHub Cards",
|
||||
"title": "Experimental Features",
|
||||
"user_picker": "User Picker",
|
||||
"virtual_scroll": "Virtual Scrolling"
|
||||
},
|
||||
"interface": {
|
||||
"color_mode": "Color Mode",
|
||||
"dark_mode": "Dark Mode",
|
||||
"dark_mode": "Dark",
|
||||
"default": " (default)",
|
||||
"font_size": "Font Size",
|
||||
"label": "Interface",
|
||||
"light_mode": "Light Mode",
|
||||
"light_mode": "Light",
|
||||
"size_label": {
|
||||
"lg": "Large",
|
||||
"md": "Medium",
|
||||
|
@ -324,7 +322,14 @@
|
|||
},
|
||||
"notifications_settings": "Notifications",
|
||||
"preferences": {
|
||||
"label": "Preferences"
|
||||
"github_cards": "GitHub Cards",
|
||||
"hide_boost_count": "Hide boost count",
|
||||
"hide_favorite_count": "Hide favorite count",
|
||||
"hide_follower_count": "Hide follower count",
|
||||
"label": "Preferences",
|
||||
"title": "Experimental Features",
|
||||
"user_picker": "User Picker",
|
||||
"virtual_scroll": "Virtual Scrolling"
|
||||
},
|
||||
"profile": {
|
||||
"appearance": {
|
||||
|
@ -347,16 +352,13 @@
|
|||
"export": "Export User Tokens",
|
||||
"import": "Import User Tokens",
|
||||
"label": "Logged in users"
|
||||
},
|
||||
"wellness": {
|
||||
"feature": {
|
||||
"hide_boost_count": "Hide boost count",
|
||||
"hide_favorite_count": "Hide favorite count",
|
||||
"hide_follower_count": "Hide follower count"
|
||||
},
|
||||
"label": "Wellness"
|
||||
}
|
||||
},
|
||||
"share-target": {
|
||||
"description": "Elk can be configured so that you can share content from other applications, simply install Elk on your device or computer and sign in.",
|
||||
"hint": "In order to share content with Elk, Elk must be installed and you must be signed in.",
|
||||
"title": "Share with Elk"
|
||||
},
|
||||
"state": {
|
||||
"attachments_exceed_server_limit": "The number of attachments exceeded the limit per post.",
|
||||
"attachments_limit_error": "Limit per post exceeded",
|
||||
|
@ -453,6 +455,7 @@
|
|||
},
|
||||
"tooltip": {
|
||||
"add_content_warning": "Add content warning",
|
||||
"add_emojis": "Add emojis",
|
||||
"add_media": "Add images, a video or an audio file",
|
||||
"add_publishable_content": "Add content to publish",
|
||||
"change_content_visibility": "Change content visibility",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"a11y": {
|
||||
"loading_page": "Cargando página, espere por favor",
|
||||
"loading_titled_page": "Cargando página {0}, espere por favor",
|
||||
"loading_page": "Cargando página, espera por favor",
|
||||
"loading_titled_page": "Cargando página {0}, espera por favor",
|
||||
"locale_changed": "Idioma cambiado a {0}",
|
||||
"locale_changing": "Cambiando idioma, espere por favor",
|
||||
"locale_changing": "Cambiando idioma, espera por favor",
|
||||
"route_loaded": "Página {0} cargada"
|
||||
},
|
||||
"account": {
|
||||
|
@ -13,7 +13,7 @@
|
|||
"blocked_users": "Usuarios bloqueados",
|
||||
"blocking": "Bloqueado",
|
||||
"bot": "BOT",
|
||||
"favourites": "Favoritos",
|
||||
"favourites": "Favoritas",
|
||||
"follow": "Seguir",
|
||||
"follow_back": "Seguir de vuelta",
|
||||
"follow_requested": "Solicitado",
|
||||
|
@ -50,9 +50,9 @@
|
|||
"confirm": "Confirmar",
|
||||
"edit": "Editar",
|
||||
"enter_app": "Entrar",
|
||||
"favourite": "Favorito",
|
||||
"favourite": "Favorita",
|
||||
"favourite_count": "{0}",
|
||||
"favourited": "Marcado como favorito",
|
||||
"favourited": "Marcado como favorita",
|
||||
"more": "Más",
|
||||
"next": "Siguiente",
|
||||
"prev": "Anterior",
|
||||
|
@ -95,7 +95,7 @@
|
|||
"error": "ERROR",
|
||||
"in": "en",
|
||||
"not_found": "404 No Encontrado",
|
||||
"offline_desc": "Al parecer estás fuera de línea. Por favor, comprueba tu conexión a la red."
|
||||
"offline_desc": "Al parecer no tienes conexión a internet. Por favor, comprueba tu conexión a la red."
|
||||
},
|
||||
"compose": {
|
||||
"draft_title": "Borrador {0}",
|
||||
|
@ -133,7 +133,7 @@
|
|||
"delete_and_redraft": "Borrar y volver a borrador",
|
||||
"delete_confirm": {
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Borrar",
|
||||
"confirm": "Eliminar",
|
||||
"title": "¿Estás seguro que deseas eliminar esta publicación?"
|
||||
},
|
||||
"direct_message_account": "Mensaje directo a {0}",
|
||||
|
@ -145,7 +145,7 @@
|
|||
"open_in_original_site": "Abrir página original",
|
||||
"pin_on_profile": "Fijar en tu perfil",
|
||||
"share_post": "Compartir esta publicación",
|
||||
"show_favourited_and_boosted_by": "Mostrar quien marcó como favorito y quien retooteó",
|
||||
"show_favourited_and_boosted_by": "Mostrar quien marcó como favorita y quien retooteó",
|
||||
"show_reblogs": "Mostrar retoots de {0}",
|
||||
"show_untranslated": "Mostrar original",
|
||||
"toggle_theme": {
|
||||
|
@ -167,7 +167,7 @@
|
|||
"built_at": "Compilado {0}",
|
||||
"conversations": "Conversaciones",
|
||||
"explore": "Explorar",
|
||||
"favourites": "Favoritos",
|
||||
"favourites": "Favoritas",
|
||||
"federated": "Federados",
|
||||
"home": "Inicio",
|
||||
"local": "Local",
|
||||
|
@ -179,12 +179,12 @@
|
|||
"select_font_size": "Cambiar tamaño de letra",
|
||||
"select_language": "Cambiar idioma",
|
||||
"settings": "Ajustes",
|
||||
"show_intro": "Mostrar intro",
|
||||
"toggle_theme": "Cambiar tema",
|
||||
"show_intro": "Mostrar introducción",
|
||||
"toggle_theme": "Cambiar modo de color",
|
||||
"zen_mode": "Modo Zen"
|
||||
},
|
||||
"notification": {
|
||||
"favourited_post": "marcó tu publicación como favorito",
|
||||
"favourited_post": "marcó como favorita tu publicación",
|
||||
"followed_you": "te ha seguido",
|
||||
"followed_you_count": "{0} personas te siguieron|{0} persona te siguió|{0} personas te siguieron",
|
||||
"missing_type": "MISSING notification.type:",
|
||||
|
@ -230,29 +230,30 @@
|
|||
},
|
||||
"search": {
|
||||
"search_desc": "Buscar personas y etiquetas",
|
||||
"search_empty": "No se pudo encontrar nada para estos términos de búsqueda"
|
||||
"search_empty": "No hubo resultados para estos términos de búsqueda"
|
||||
},
|
||||
"settings": {
|
||||
"about": {
|
||||
"label": "Acerca de"
|
||||
"label": "Acerca de",
|
||||
"meet_the_team": "Conoce al equipo",
|
||||
"sponsor_action": "Patrocinar",
|
||||
"sponsor_action_desc": "Apoya al equipo detrás de Elk",
|
||||
"sponsors": "Patrocinadores",
|
||||
"sponsors_body_1": "Elk es posible gracias al generoso patrocinio y apoyo de:",
|
||||
"sponsors_body_2": "Y todas las empresas y personas que patrocinan al equipo de Elk y sus miembros.",
|
||||
"sponsors_body_3": "Si estás disfrutando de la aplicación, considera patrocinarnos:"
|
||||
},
|
||||
"account_settings": {
|
||||
"description": "Edita los ajustes de tu cuenta en la interfaz de Mastodon",
|
||||
"label": "Ajustes de cuenta"
|
||||
},
|
||||
"feature_flags": {
|
||||
"github_cards": "Tarjetas GitHub",
|
||||
"title": "Funcionalidades experimentales",
|
||||
"user_picker": "Selector de usuarios",
|
||||
"virtual_scroll": "Desplazamiento virtual"
|
||||
},
|
||||
"interface": {
|
||||
"color_mode": "Modo Color",
|
||||
"dark_mode": "Modo Oscuro",
|
||||
"color_mode": "Modos de color",
|
||||
"dark_mode": "Modo oscuro",
|
||||
"default": " (por defecto)",
|
||||
"font_size": "Tamaño de Letra",
|
||||
"label": "Interfaz",
|
||||
"light_mode": "Modo Claro",
|
||||
"light_mode": "Modo claro",
|
||||
"size_label": {
|
||||
"lg": "Grande",
|
||||
"md": "Mediana",
|
||||
|
@ -272,7 +273,7 @@
|
|||
},
|
||||
"push_notifications": {
|
||||
"alerts": {
|
||||
"favourite": "Favoritos",
|
||||
"favourite": "Favoritas",
|
||||
"follow": "Nuevos seguidores",
|
||||
"mention": "Menciones",
|
||||
"poll": "Encuestas",
|
||||
|
@ -316,12 +317,19 @@
|
|||
},
|
||||
"notifications_settings": "Notificaciones",
|
||||
"preferences": {
|
||||
"label": "Preferencias"
|
||||
"github_cards": "Tarjetas GitHub",
|
||||
"hide_boost_count": "Ocultar contador de retoots",
|
||||
"hide_favorite_count": "Ocultar contador de favoritas",
|
||||
"hide_follower_count": "Ocultar contador de seguidores",
|
||||
"label": "Preferencias",
|
||||
"title": "Funcionalidades experimentales",
|
||||
"user_picker": "Selector de usuarios",
|
||||
"virtual_scroll": "Desplazamiento virtual"
|
||||
},
|
||||
"profile": {
|
||||
"appearance": {
|
||||
"bio": "Biografía",
|
||||
"description": "Editar avatar, nombre de usuario, perfil, etc.",
|
||||
"description": "Modificar avatar, nombre de usuario, perfil, etc.",
|
||||
"display_name": "Nombre a mostrar",
|
||||
"label": "Apariencia",
|
||||
"profile_metadata": "Metadatos de perfil",
|
||||
|
@ -329,7 +337,7 @@
|
|||
"title": "Editar perfil"
|
||||
},
|
||||
"featured_tags": {
|
||||
"description": "Las personas pueden navegar por tus publicaciones públicas con estas hashtags.",
|
||||
"description": "Las personas pueden navegar por tus publicaciones públicas con estos hashtags.",
|
||||
"label": "Hashtags destacados"
|
||||
},
|
||||
"label": "Perfil"
|
||||
|
@ -341,19 +349,25 @@
|
|||
"label": "Usuarios conectados"
|
||||
}
|
||||
},
|
||||
"share-target": {
|
||||
"description": "Elk puede ser configurado para que pueda compartir contenido desde otras aplicaciones, simplemente tiene que instalar Elk en su dispositivo u ordenador e iniciar sesión.",
|
||||
"hint": "Para poder compartir contenido con Elk, debes instalar Elk e iniciar sesión.",
|
||||
"title": "Compartir con Elk"
|
||||
},
|
||||
"state": {
|
||||
"attachments_exceed_server_limit": "Número máximo de archivos adjuntos por publicación excedido.",
|
||||
"attachments_limit_error": "Límite por publicación excedido",
|
||||
"edited": "(Editado)",
|
||||
"editing": "Editando",
|
||||
"loading": "Cargando...",
|
||||
"publishing": "Publicando",
|
||||
"upload_failed": "Subida fallida",
|
||||
"uploading": "Subiendo..."
|
||||
},
|
||||
"status": {
|
||||
"boosted_by": "Retooteado por",
|
||||
"edited": "Editado {0}",
|
||||
"favourited_by": "Marcado como favorito por",
|
||||
"favourited_by": "Marcado como favorita por",
|
||||
"filter_hidden_phrase": "Filtrado por",
|
||||
"filter_removed_phrase": "Eliminado por filtrado",
|
||||
"filter_show_anyway": "Mostrar de todas formas",
|
||||
|
@ -436,20 +450,21 @@
|
|||
},
|
||||
"tooltip": {
|
||||
"add_content_warning": "Añadir advertencia de contenido",
|
||||
"add_emojis": "Añadir emojis",
|
||||
"add_media": "Añadir imágenes, video o audio",
|
||||
"add_publishable_content": "Agregar contenido a publicar",
|
||||
"add_publishable_content": "Publicar contenido",
|
||||
"change_content_visibility": "Cambiar visibilidad de contenido",
|
||||
"change_language": "Cambiar idioma",
|
||||
"emoji": "Emoji",
|
||||
"explore_links_intro": "Estas noticias están siendo comentadas por la gente en este y otros servidores de la red descentralizada en este momento.",
|
||||
"explore_posts_intro": "Estos mensajes de este y otros servidores de la red descentralizada están ganando tracción en este servidor en este momento.",
|
||||
"explore_tags_intro": "Estas etiquetas están ganando tracción entre la gente de este y otros servidores de la red descentralizada en este momento.",
|
||||
"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.",
|
||||
"toggle_code_block": "Cambiar a bloque de código"
|
||||
},
|
||||
"user": {
|
||||
"add_existing": "Agregar una cuenta existente",
|
||||
"server_address_label": "Dirección de Servidor de Mastodon",
|
||||
"sign_in_desc": "Inicia sesión para seguir perfiles o hashtags, marcar como favorito, compartir and responder a publicaciones, o interactuar desde tu usuario con un servidor diferente.",
|
||||
"sign_in_desc": "Inicia sesión para seguir perfiles o hashtags, 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}",
|
||||
"tip_no_account": "Si aún no tienes una cuenta Mastodon, {0}.",
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"blocked_by": "Vous êtes bloqué·e par cet·te utilisateur·ice.",
|
||||
"blocked_domains": "Domaines bloqués",
|
||||
"blocked_users": "Utilisateur·ice·s bloqué·e·s",
|
||||
"blocking": "Blocked",
|
||||
"blocking": "Bloqué·e",
|
||||
"bot": "Automatisé",
|
||||
"favourites": "Appréciés",
|
||||
"favourites": "Aimés",
|
||||
"follow": "Suivre",
|
||||
"follow_back": "Suivre en retour",
|
||||
"follow_requested": "Abonnement demandé",
|
||||
|
@ -27,15 +27,15 @@
|
|||
"moved_title": "a indiqué que son nouveau compte est désormais :",
|
||||
"muted_users": "Utilisateur·ice·s masqué·e·s",
|
||||
"muting": "Masqué·e",
|
||||
"mutuals": "@:account.following",
|
||||
"mutuals": "Abonné·e·s",
|
||||
"pinned": "Épinglés",
|
||||
"posts": "Messages",
|
||||
"posts_count": "{0} Messages",
|
||||
"profile_description": "En-tête du profil de {0}",
|
||||
"profile_unavailable": "Profil non accessible",
|
||||
"unblock": "Unblock",
|
||||
"unblock": "Débloquer",
|
||||
"unfollow": "Ne plus suivre",
|
||||
"unmute": "Unmute"
|
||||
"unmute": "Réafficher"
|
||||
},
|
||||
"action": {
|
||||
"apply": "Appliquer",
|
||||
|
@ -49,8 +49,8 @@
|
|||
"confirm": "Confirmer",
|
||||
"edit": "Éditer",
|
||||
"enter_app": "Entrer dans l'application",
|
||||
"favourite": "Ajouter aux messages appréciés",
|
||||
"favourited": "Ajouté aux messages appréciés",
|
||||
"favourite": "J'aime",
|
||||
"favourited": "Aimé",
|
||||
"more": "Plus",
|
||||
"next": "Suivant",
|
||||
"prev": "Précédent",
|
||||
|
@ -142,7 +142,7 @@
|
|||
"open_in_original_site": "Ouvrir sur le site d'origine",
|
||||
"pin_on_profile": "Épingler sur le profil",
|
||||
"share_post": "Partager ce message",
|
||||
"show_favourited_and_boosted_by": "Montrer qui a apprécié et partagé",
|
||||
"show_favourited_and_boosted_by": "Montrer qui a aimé et partagé",
|
||||
"show_reblogs": "Voir les partages de {0}",
|
||||
"show_untranslated": "Montrer le message non-traduit",
|
||||
"toggle_theme": {
|
||||
|
@ -164,7 +164,7 @@
|
|||
"built_at": "Dernière compilation {0}",
|
||||
"conversations": "Conversations",
|
||||
"explore": "Explorer",
|
||||
"favourites": "Appréciés",
|
||||
"favourites": "Favoris",
|
||||
"federated": "Fédérés",
|
||||
"home": "Accueil",
|
||||
"local": "Local",
|
||||
|
@ -181,7 +181,7 @@
|
|||
"zen_mode": "Mode Zen"
|
||||
},
|
||||
"notification": {
|
||||
"favourited_post": "apprécie votre message",
|
||||
"favourited_post": "a aimé votre message",
|
||||
"followed_you": "vous suit",
|
||||
"followed_you_count": "{0} personnes vous suivent|{0} personne vous suit|{0} personnes vous suivent",
|
||||
"missing_type": "MISSING notification.type:",
|
||||
|
@ -244,12 +244,6 @@
|
|||
"description": "Modifiez les paramètres de votre compte dans l'interface de Mastodon",
|
||||
"label": "Paramètres de compte"
|
||||
},
|
||||
"feature_flags": {
|
||||
"github_cards": "GitHub Cards",
|
||||
"title": "Fonctionnalités expérimentales",
|
||||
"user_picker": "User Picker",
|
||||
"virtual_scroll": "Défilement virtuel"
|
||||
},
|
||||
"interface": {
|
||||
"color_mode": "Couleur de thème",
|
||||
"dark_mode": "Mode sombre",
|
||||
|
@ -277,11 +271,11 @@
|
|||
},
|
||||
"push_notifications": {
|
||||
"alerts": {
|
||||
"favourite": "Apprécier",
|
||||
"favourite": "Messages aimés",
|
||||
"follow": "Nouveaux abonné·e·s",
|
||||
"mention": "Mentions",
|
||||
"poll": "Sondages",
|
||||
"reblog": "A republié votre message",
|
||||
"reblog": "Messages partagés",
|
||||
"title": "Quelles notifications recevoir ?"
|
||||
},
|
||||
"description": "Recevez des notifications même lorsque vous n'utilisez pas Elk.",
|
||||
|
@ -320,7 +314,14 @@
|
|||
},
|
||||
"notifications_settings": "Notifications",
|
||||
"preferences": {
|
||||
"label": "Préférences"
|
||||
"github_cards": "GitHub Cards",
|
||||
"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",
|
||||
"label": "Préférences",
|
||||
"title": "Fonctionnalités expérimentales",
|
||||
"user_picker": "User Picker",
|
||||
"virtual_scroll": "Défilement virtuel"
|
||||
},
|
||||
"profile": {
|
||||
"appearance": {
|
||||
|
@ -343,14 +344,6 @@
|
|||
"export": "Exporter les tokens d'utilisateur·ice",
|
||||
"import": "Importer des tokens d'utilisateur·ice",
|
||||
"label": "Utilisateur·ice·s connecté·e·s"
|
||||
},
|
||||
"wellness": {
|
||||
"feature": {
|
||||
"hide_boost_count": "Cacher les compteurs de partages",
|
||||
"hide_favorite_count": "Cacher les compteurs d'appréciations",
|
||||
"hide_follower_count": "Cacher les compteurs d'abonné·e·s"
|
||||
},
|
||||
"label": "Bien-être"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
|
@ -366,7 +359,7 @@
|
|||
"status": {
|
||||
"boosted_by": "Partagé par",
|
||||
"edited": "Edité {0}",
|
||||
"favourited_by": "Apprécié par",
|
||||
"favourited_by": "Aimé par",
|
||||
"filter_hidden_phrase": "Filtré par",
|
||||
"filter_removed_phrase": "Caché par le filtre",
|
||||
"filter_show_anyway": "Montrer coûte que coûte",
|
||||
|
@ -462,7 +455,7 @@
|
|||
"user": {
|
||||
"add_existing": "Ajouter un compte existant",
|
||||
"server_address_label": "Adresse du serveur mastodon",
|
||||
"sign_in_desc": "Connectez-vous pour suivre des profils ou des hashtags, dire que vous appréciez, partager et répondre à des messages, ou interagir à partir de votre compte sur un autre serveur...",
|
||||
"sign_in_desc": "Connectez-vous pour suivre des profils ou des hashtags, aimer, partagez et répondre à des messages, ou interagir à partir de votre compte d'autre serveur.",
|
||||
"sign_in_notice_title": "Affichage de {0} données publiques",
|
||||
"sign_out_account": "Se déconnecter de {0}",
|
||||
"tip_no_account": "Si vous n'avez pas encore de compte Mastodon, {0}.",
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
"desc_highlight": "Expect some bugs and missing features here and there.",
|
||||
"desc_para1": "Thanks for your interest in trying out Elk, our work-in-progress Mastodon web client!",
|
||||
"desc_para2": "we are working hard on the development and improving it over time.",
|
||||
"desc_para3": "To help boosting out development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!",
|
||||
"desc_para3": "To boost development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!",
|
||||
"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.",
|
||||
|
|
495
locales/pt-PT.json
Normal file
495
locales/pt-PT.json
Normal file
|
@ -0,0 +1,495 @@
|
|||
{
|
||||
"a11y": {
|
||||
"loading_page": "A carregar página, por favor aguarde",
|
||||
"loading_titled_page": "A carregar página {0}, por favor aguarde",
|
||||
"locale_changed": "Idioma alterado para {0}",
|
||||
"locale_changing": "A alterar idioma, por favor aguarde",
|
||||
"route_loaded": "Página {0} carregada"
|
||||
},
|
||||
"account": {
|
||||
"avatar_description": "Imagem de perfil de {0}",
|
||||
"blocked_by": "Está bloqueado por este utilizador.",
|
||||
"blocked_domains": "Domínios bloqueados",
|
||||
"blocked_users": "Utilizadores bloqueados",
|
||||
"blocking": "Bloqueado",
|
||||
"bot": "BOT",
|
||||
"favourites": "Favoritos",
|
||||
"follow": "Seguir",
|
||||
"follow_back": "Seguir de volta",
|
||||
"follow_requested": "Pedido",
|
||||
"followers": "Seguidores",
|
||||
"followers_count": "{0} Seguidores|{0} Seguidor|{0} Seguidores",
|
||||
"following": "A seguir",
|
||||
"following_count": "A seguir {0}",
|
||||
"follows_you": "Segue-o",
|
||||
"go_to_profile": "Ir para o perfil",
|
||||
"joined": "Juntou-se a",
|
||||
"moved_title": "indicou que a sua novo conta é agora:",
|
||||
"muted_users": "Utilizadores silenciados",
|
||||
"muting": "Silenciados",
|
||||
"mutuals": "Mútuos",
|
||||
"notify_on_post": "Notifique-me quando {username} publicar",
|
||||
"pinned": "Fixado",
|
||||
"posts": "Publicações",
|
||||
"posts_count": "{0} Publicações|{0} Publicação|{0} Publicações",
|
||||
"profile_description": "Descrição de perfil de {0}",
|
||||
"profile_unavailable": "Perfil indisponível",
|
||||
"unblock": "Desbloquear",
|
||||
"unfollow": "Deixar de seguir",
|
||||
"unmute": "Deixar de silenciar",
|
||||
"view_other_followers": "Os seguidores de outras instâncias podem não ser exibidos.",
|
||||
"view_other_following": "As pessoas que segue de outras instâncias podem não ser exibidas."
|
||||
},
|
||||
"action": {
|
||||
"apply": "Aplicar",
|
||||
"bookmark": "Salvar",
|
||||
"bookmarked": "Adicionado aos itens salvos",
|
||||
"boost": "Partilhar",
|
||||
"boost_count": "{0}",
|
||||
"boosted": "Partilhado",
|
||||
"clear_upload_failed": "Limpar erro de carregamento de ficheiro",
|
||||
"close": "Fechar",
|
||||
"compose": "Compor",
|
||||
"confirm": "Confirmar",
|
||||
"edit": "Editar",
|
||||
"enter_app": "Entrar na App",
|
||||
"favourite": "Adicionar aos favoritos",
|
||||
"favourite_count": "{0}",
|
||||
"favourited": "Adicionado aos favoritos",
|
||||
"more": "Mais",
|
||||
"next": "Próximo",
|
||||
"prev": "Anterior",
|
||||
"publish": "Publicar",
|
||||
"reply": "Responder",
|
||||
"reply_count": "{0}",
|
||||
"reset": "Repor",
|
||||
"save": "Guardar",
|
||||
"save_changes": "Guardar alterações",
|
||||
"sign_in": "Entrar",
|
||||
"switch_account": "Mudar contar",
|
||||
"vote": "Votar"
|
||||
},
|
||||
"app_desc_short": "Uma ágil aplicação web para o Mastodon",
|
||||
"app_logo": "Logo do Elk",
|
||||
"app_name": "Elk",
|
||||
"attachment": {
|
||||
"edit_title": "Descrição",
|
||||
"remove_label": "Remover anexo"
|
||||
},
|
||||
"command": {
|
||||
"activate": "Ativar",
|
||||
"complete": "Completar",
|
||||
"compose_desc": "Escrever uma nova publicação",
|
||||
"n-people-in-the-past-n-days": "{0} pessoas nos últimos {1} dias",
|
||||
"select_lang": "Selecionar idioma",
|
||||
"sign_in_desc": "Adicionar uma conta existente",
|
||||
"switch_account": "Mudar para {0}",
|
||||
"switch_account_desc": "Mudar para outra conta",
|
||||
"toggle_dark_mode": "Alternar modo escuro",
|
||||
"toggle_zen_mode": "Alternar modo zen"
|
||||
},
|
||||
"common": {
|
||||
"confirm_dialog": {
|
||||
"cancel": "Não",
|
||||
"confirm": "Sim",
|
||||
"title": "Tem a certeza?"
|
||||
},
|
||||
"end_of_list": "Fim da lista",
|
||||
"error": "ERRO",
|
||||
"in": "em",
|
||||
"not_found": "404 Não Encontrado",
|
||||
"offline_desc": "Parece que está offline. Por favor, confirme a sua conexão à internet."
|
||||
},
|
||||
"compose": {
|
||||
"draft_title": "Rascunho {0}",
|
||||
"drafts": "Rascunhos ({v})"
|
||||
},
|
||||
"conversation": {
|
||||
"with": "com"
|
||||
},
|
||||
"error": {
|
||||
"account_not_found": "Conta {0} não encontrada",
|
||||
"explore-list-empty": "Nada está em tendência agora. Confirme mais tarde!",
|
||||
"file_size_cannot_exceed_n_mb": "O tamanho do ficheiro não pode exceder {0}MB",
|
||||
"sign_in_error": "Não é possível conectar ao servidor.",
|
||||
"status_not_found": "Publicação não encontrada",
|
||||
"unsupported_file_format": "Formato de ficheiro não suportado"
|
||||
},
|
||||
"help": {
|
||||
"desc_highlight": "Espere alguns problemas e funcionalidades em falta.",
|
||||
"desc_para1": "Obrigado pelo seu interesse em experimentar o Elk, o nosso aplicativo web para o Mastodon, ainda em construção!",
|
||||
"desc_para2": "Estamos a trabalhar arduamente no seu desenvolvimento e melhoria ao longo do tempo.",
|
||||
"desc_para3": "Para ajudar a impulsionar o desenvolvimento, pode patrocionar a Equipa através do GitHub Sponsors. Esperamos que aprecie o Elk!",
|
||||
"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.",
|
||||
"title": "Elk está em Antevisão!"
|
||||
},
|
||||
"language": {
|
||||
"search": "Procurar"
|
||||
},
|
||||
"menu": {
|
||||
"block_account": "Bloquear {0}",
|
||||
"block_domain": "Bloquear domínio {0}",
|
||||
"copy_link_to_post": "Copiar ligação para esta publicação",
|
||||
"delete": "Eliminar",
|
||||
"delete_and_redraft": "Eliminar & re-editar",
|
||||
"delete_confirm": {
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Eliminar",
|
||||
"title": "Tem a certeza que pretende eliminar esta publicação?"
|
||||
},
|
||||
"direct_message_account": "Mensagem direta a {0}",
|
||||
"edit": "Editar",
|
||||
"hide_reblogs": "Esconder partilhas de {0}",
|
||||
"mention_account": "Mencionar {0}",
|
||||
"mute_account": "Silenciar {0}",
|
||||
"mute_conversation": "Silenciar esta publicação",
|
||||
"open_in_original_site": "Abrir no sítio original",
|
||||
"pin_on_profile": "Fixar no perfil",
|
||||
"share_post": "Partilhar esta publicação",
|
||||
"show_favourited_and_boosted_by": "Mostrar quem adicionou aos favoritos e partilhou",
|
||||
"show_reblogs": "Mostrar partilhas de {0}",
|
||||
"show_untranslated": "Mostrar não traduzidas",
|
||||
"toggle_theme": {
|
||||
"dark": "Alternar modo escuro",
|
||||
"light": "Alternar modo claro"
|
||||
},
|
||||
"translate_post": "Traduzir publicação",
|
||||
"unblock_account": "Desbloquear {0}",
|
||||
"unblock_domain": "Desbloquear domínio {0}",
|
||||
"unmute_account": "Deixar de silenciar {0}",
|
||||
"unmute_conversation": "Deixar de silenciar esta publicação",
|
||||
"unpin_on_profile": "Desafixar do perfil"
|
||||
},
|
||||
"nav": {
|
||||
"back": "Voltar",
|
||||
"blocked_domains": "Domínios bloqueados",
|
||||
"blocked_users": "Utilizadores bloqueados",
|
||||
"bookmarks": "Itens Salvos",
|
||||
"built_at": "Produzido {0}",
|
||||
"compose": "Compor",
|
||||
"conversations": "Conversações",
|
||||
"explore": "Explorar",
|
||||
"favourites": "Favoritos",
|
||||
"federated": "Federada",
|
||||
"home": "Início",
|
||||
"local": "Local",
|
||||
"muted_users": "Utilizadores silenciados",
|
||||
"notifications": "Notificações",
|
||||
"profile": "Perfil",
|
||||
"search": "Procurar",
|
||||
"select_feature_flags": "Alternar Funcionalidades",
|
||||
"select_font_size": "Tamanho da Fonte",
|
||||
"select_language": "Idioma de Apresentação",
|
||||
"settings": "Definições",
|
||||
"show_intro": "Mostrar introdução",
|
||||
"toggle_theme": "Alternar Tema",
|
||||
"zen_mode": "Modo Zen"
|
||||
},
|
||||
"notification": {
|
||||
"favourited_post": "adicionou a sua publicação aos favoritos",
|
||||
"followed_you": "começou a segui-lo",
|
||||
"followed_you_count": "{0} pessoas seguem-no|{0} pessoa segue-o|{0} pessoas seguem-no",
|
||||
"missing_type": "notification.type em FALTA:",
|
||||
"reblogged_post": "partilhou a sua publicação",
|
||||
"request_to_follow": "pediu para segui-lo",
|
||||
"signed_up": "inscreveu-se",
|
||||
"update_status": "atualizou a sua publicação"
|
||||
},
|
||||
"placeholder": {
|
||||
"content_warning": "Escreva aqui o seu aviso",
|
||||
"default_1": "Em que está a pensar?",
|
||||
"reply_to_account": "Responder a {0}",
|
||||
"replying": "Respondedo",
|
||||
"the_thread": "a conversa"
|
||||
},
|
||||
"pwa": {
|
||||
"dismiss": "Dispensar",
|
||||
"title": "Nova atualização do Elk disponível!",
|
||||
"update": "Atualizar",
|
||||
"update_available_short": "Atualizar Elk",
|
||||
"webmanifest": {
|
||||
"canary": {
|
||||
"description": "Uma ágil aplicação web para o Mastodon (canary)",
|
||||
"name": "Elk (canary)",
|
||||
"short_name": "Elk (canary)"
|
||||
},
|
||||
"dev": {
|
||||
"description": "Uma ágil aplicação web para o Mastodon (dev)",
|
||||
"name": "Elk (dev)",
|
||||
"short_name": "Elk (dev)"
|
||||
},
|
||||
"preview": {
|
||||
"description": "Uma ágil aplicação web para o Mastodon (preview)",
|
||||
"name": "Elk (preview)",
|
||||
"short_name": "Elk (preview)"
|
||||
},
|
||||
"release": {
|
||||
"description": "Uma ágil aplicação web para o Mastodon",
|
||||
"name": "Elk",
|
||||
"short_name": "Elk"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"search_desc": "Procure por pessoas e hashtags",
|
||||
"search_empty": "Não foi possível encontrar nada para os termos que pesquisou"
|
||||
},
|
||||
"settings": {
|
||||
"about": {
|
||||
"label": "Sobre",
|
||||
"meet_the_team": "Conheça a equipa",
|
||||
"sponsor_action": "Patrocine-nos",
|
||||
"sponsor_action_desc": "Para ajudar a equipa que desenvolve o Elk",
|
||||
"sponsors": "Patrocinadores",
|
||||
"sponsors_body_1": "O Elk é possível graças ao genoroso patrocinio e ajuda de:",
|
||||
"sponsors_body_2": "E todas as empresas e pessoas que apoiam a Equipa do Elk e os seus membros.",
|
||||
"sponsors_body_3": "Se está a gostar de utilizar esta aplicação, considere apoiar-nos:"
|
||||
},
|
||||
"account_settings": {
|
||||
"description": "Editar as configurações da sua conta na aplicação web do Mastodon",
|
||||
"label": "Configurações da conta"
|
||||
},
|
||||
"feature_flags": {
|
||||
"github_cards": "Cartões do GitHub",
|
||||
"title": "Funcionalidades Experimentais",
|
||||
"user_picker": "Selecionador de Utilizador",
|
||||
"virtual_scroll": "Deslocamento Virtual"
|
||||
},
|
||||
"interface": {
|
||||
"color_mode": "Modo de cores",
|
||||
"dark_mode": "Modo Escuro",
|
||||
"default": " (padrão)",
|
||||
"font_size": "Tamanho da fonte",
|
||||
"label": "Apresentação",
|
||||
"light_mode": "Modo Claro",
|
||||
"size_label": {
|
||||
"lg": "Grande",
|
||||
"md": "Médio",
|
||||
"sm": "Pequeno",
|
||||
"xl": "Extra grande",
|
||||
"xs": "Extra pequeno"
|
||||
},
|
||||
"system_mode": "Sistema"
|
||||
},
|
||||
"language": {
|
||||
"display_language": "Idioma de Apresentação",
|
||||
"label": "Idioma"
|
||||
},
|
||||
"notifications": {
|
||||
"label": "Notificações",
|
||||
"notifications": {
|
||||
"label": "Configurar notificações"
|
||||
},
|
||||
"push_notifications": {
|
||||
"alerts": {
|
||||
"favourite": "Favoritos",
|
||||
"follow": "Novos seguidores",
|
||||
"mention": "Menções",
|
||||
"poll": "Votações",
|
||||
"reblog": "Partilha das sua publicação",
|
||||
"title": "Que notificações quer receber?"
|
||||
},
|
||||
"description": "Receba notificações mesmo quando não está a utilizar o Elk.",
|
||||
"instructions": "Não esqueça de salvar as suas alterações utilizando o botão @:settings.notifications.push_notifications.save_settings!",
|
||||
"label": "Configurar notificações push",
|
||||
"policy": {
|
||||
"all": "De todos",
|
||||
"followed": "De pessoas que sigo",
|
||||
"follower": "De pessoas que me seguem",
|
||||
"none": "De ninguém",
|
||||
"title": "De quem quer receber notificações?"
|
||||
},
|
||||
"save_settings": "Salvar configurações",
|
||||
"subscription_error": {
|
||||
"clear_error": "Limpar erro",
|
||||
"permission_denied": "Permissão negada: habilite as notificações no seu browser.",
|
||||
"request_error": "Um erro ocorreu durante o pedido de subcriçã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 multiplas contas em diferentes servidores. Deve cancelar a subcrição de notificações push nas outras contas e tentar novamente."
|
||||
},
|
||||
"title": "Configuração de notificações push",
|
||||
"undo_settings": "Reverter alterações",
|
||||
"unsubscribe": "Desabilitar notificações push",
|
||||
"unsupported": "O seu browser não suporta notificações push.",
|
||||
"warning": {
|
||||
"enable_close": "Fechar",
|
||||
"enable_description": "Para receber notificações quanto o Elk não está aberto, habilite as notificações push. Poderá controlar com precisão que tipos de interações geram notificações push através do \"@:settings.notifications.show_btn{'\"'} botão acima, uma vez habilitadas.",
|
||||
"enable_description_desktop": "Para receber notificações quanto o Elk não está aberto, habilite as notificações push. Poderá controlar com precisão que tipos de interações geram notificações push em \"Preferências > Notificaçõess > Configuração de notificações push\", uma vez habilitadas.",
|
||||
"enable_description_mobile": "Pode também aceder às configurações através do menu de navegação \"Preferências > Notificações > Configuração de notificações push\".",
|
||||
"enable_description_settings": "Para receber notificações quanto o Elk não está aberto, habilite as notificações push. Poderá controlar com precisão que tipos de interações geram notificações neste mesmo ecrã, uma vez habilitadas.",
|
||||
"enable_desktop": "Habilitar notificações push",
|
||||
"enable_title": "Nunca perca nada",
|
||||
"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 nofiticações"
|
||||
},
|
||||
"notifications_settings": "Notificações",
|
||||
"preferences": {
|
||||
"label": "Preferências"
|
||||
},
|
||||
"profile": {
|
||||
"appearance": {
|
||||
"bio": "Bio",
|
||||
"description": "Editar imagem de perfil, nome, perfil, etc.",
|
||||
"display_name": "Nome de apresentação",
|
||||
"label": "Aspecto",
|
||||
"profile_metadata": "Metadados de perfil",
|
||||
"profile_metadata_desc": "Pode ter até {0} itens expostos, em forma de tabela, no seu perfil",
|
||||
"title": "Editar perfil"
|
||||
},
|
||||
"featured_tags": {
|
||||
"description": "As pessoas podem encontrar as suas publicações públicas que incluem essas hashtags.",
|
||||
"label": "Hashtags destacadas"
|
||||
},
|
||||
"label": "Perfil"
|
||||
},
|
||||
"select_a_settings": "Selecionar uma configuração",
|
||||
"users": {
|
||||
"export": "Exportar Tokens de Acesso",
|
||||
"import": "Importar Tokens de Acesso",
|
||||
"label": "Utilizadores conectados"
|
||||
},
|
||||
"wellness": {
|
||||
"feature": {
|
||||
"hide_boost_count": "Esconder contagem de partilhas",
|
||||
"hide_favorite_count": "Esconder contagem de favoritos",
|
||||
"hide_follower_count": "Esconder contagem de seguidores"
|
||||
},
|
||||
"label": "Bem-estar"
|
||||
}
|
||||
},
|
||||
"share-target": {
|
||||
"description": "Elk pode ser configurado para que possa partilhar conteúdos de outras aplicações, basta instalar Elk no seu dispositivo ou computador e iniciar sessão.",
|
||||
"hint": "Para poder partilhar conteúdo com o Elk, este tem de estar instalado e você ter iniciado sessão.",
|
||||
"title": "Partilhar com o Elk"
|
||||
},
|
||||
"state": {
|
||||
"attachments_exceed_server_limit": "O número de anexos excedeu o limite permitido por publicação.",
|
||||
"attachments_limit_error": "Limite permitido por publicação excedido",
|
||||
"edited": "(Editado)",
|
||||
"editing": "Editando",
|
||||
"loading": "Carregando...",
|
||||
"publishing": "Publicando",
|
||||
"upload_failed": "Falhou carregamento",
|
||||
"uploading": "A carregar..."
|
||||
},
|
||||
"status": {
|
||||
"boosted_by": "Partilhada Por",
|
||||
"edited": "Editada {0}",
|
||||
"favourited_by": "Adicionada Aos Favoritos Por",
|
||||
"filter_hidden_phrase": "Filtrada por",
|
||||
"filter_removed_phrase": "Removida pelo filtro",
|
||||
"filter_show_anyway": "Mostrar mesmo assim",
|
||||
"img_alt": {
|
||||
"desc": "Descrição",
|
||||
"dismiss": "Dispensar"
|
||||
},
|
||||
"poll": {
|
||||
"count": "{0} votos|{0} voto|{0} votos",
|
||||
"ends": "termina {0}",
|
||||
"finished": "terminou {0}"
|
||||
},
|
||||
"reblogged": "{0} partilhou",
|
||||
"replying_to": "Respondendo a {0}",
|
||||
"show_full_thread": "Mostrar toda a conversa",
|
||||
"someone": "alguém",
|
||||
"spoiler_show_less": "Mostrar menos",
|
||||
"spoiler_show_more": "Mostrar mais",
|
||||
"thread": "Conversa",
|
||||
"try_original_site": "Tentar o sítio original"
|
||||
},
|
||||
"status_history": {
|
||||
"created": "criada {0}",
|
||||
"edited": "editada {0}"
|
||||
},
|
||||
"tab": {
|
||||
"for_you": "Para sí",
|
||||
"hashtags": "Hashtags",
|
||||
"media": "Media",
|
||||
"news": "Notícias",
|
||||
"notifications_all": "Todas",
|
||||
"notifications_mention": "Menções",
|
||||
"posts": "Publicações",
|
||||
"posts_with_replies": "Publicações e Respostas"
|
||||
},
|
||||
"tag": {
|
||||
"follow": "Seguir",
|
||||
"follow_label": "Seguir hashtag {0}",
|
||||
"unfollow": "Deixar de seguir",
|
||||
"unfollow_label": "Deixar de seguir hashtag {0}"
|
||||
},
|
||||
"time_ago_options": {
|
||||
"day_future": "em 0 dias|amanhã|em {n} diass",
|
||||
"day_past": "0 dias atrás|ontem|{n} dias atrás",
|
||||
"hour_future": "em 0 horas|em 1 hora|em {n} horas",
|
||||
"hour_past": "0 horas atrás|1 hora atrás|{n} horas aatás",
|
||||
"just_now": "agora mesmo",
|
||||
"minute_future": "em 0 minutos|em 1 minuto|em {n} minutos",
|
||||
"minute_past": "0 minutos atrás|1 minuto atrás|{n} minutos atrás",
|
||||
"month_future": "em 0 mês|próximo mês|em {n} meses",
|
||||
"month_past": "0 meses atrás|mês passado|{n} meses atrás",
|
||||
"second_future": "agora mesmo|em {n} segundos|em {n} segundos",
|
||||
"second_past": "agora mesmo|{n} segundo atrás|{n} segundos atrás",
|
||||
"short_day_future": "em {n}d",
|
||||
"short_day_past": "{n}d",
|
||||
"short_hour_future": "em {n}h",
|
||||
"short_hour_past": "{n}h",
|
||||
"short_minute_future": "em {n}min",
|
||||
"short_minute_past": "{n}min",
|
||||
"short_month_future": "em {n}M",
|
||||
"short_month_past": "{n}M",
|
||||
"short_second_future": "em {n}s",
|
||||
"short_second_past": "{n}s",
|
||||
"short_week_future": "in {n}S",
|
||||
"short_week_past": "{n}S",
|
||||
"short_year_future": "in {n}A",
|
||||
"short_year_past": "{n}A",
|
||||
"week_future": "em 0 semanas|próxima semana|em {n} semanas",
|
||||
"week_past": "0 semanas atrás|semana passada|{n} semanas atrás",
|
||||
"year_future": "em 0 anos|próximo ano|em {n} anos",
|
||||
"year_past": "0 anos atrás|ano passado|{n} anos atrás"
|
||||
},
|
||||
"timeline": {
|
||||
"show_new_items": "Mostrar {v} novos itens|Mostrar {v} novo item|Mostrar {v} novos itens",
|
||||
"view_older_posts": "Publicações antigas de outras instâncias podem não ser apresentadas."
|
||||
},
|
||||
"title": {
|
||||
"federated_timeline": "Cronologia Federada",
|
||||
"local_timeline": "Cronologia Local"
|
||||
},
|
||||
"tooltip": {
|
||||
"add_content_warning": "Adicionar aviso de conteúdo",
|
||||
"add_emojis": "Adicionar emojis",
|
||||
"add_media": "Adicionar imagens, um video ou um ficheiro audio",
|
||||
"add_publishable_content": "Adicionar conteúdo a publicar",
|
||||
"change_content_visibility": "Alterar visibilidade do conteúdo",
|
||||
"change_language": "Alterar idioma",
|
||||
"emoji": "Emoji",
|
||||
"explore_links_intro": "Estas notícias estão, neste momento, a ser faladas por pessoas neste e noutros servidores da rede descentralizada.",
|
||||
"explore_posts_intro": "Estas publicações deste e de outros servidores na rede descentralizada estão, neste momento, a ganhar popularidade neste servidor.",
|
||||
"explore_tags_intro": "Estes hashtags estão, neste momento, a ganhar popularidade entre as pessoas neste e noutros servidores da rede descentralizada.",
|
||||
"toggle_code_block": "Alternar bloco de código"
|
||||
},
|
||||
"user": {
|
||||
"add_existing": "Adicionar uma conta existente",
|
||||
"server_address_label": "Endereço do Servidor Mastodon",
|
||||
"sign_in_desc": "Entre, para seguir pessoas ou hashtags, adicionar aos favoritos, partilhar e responder a publicações, ou interagir a partir da sua conta de outro servidor.",
|
||||
"sign_in_notice_title": "A visualizar os dados públicos de {0}",
|
||||
"sign_out_account": "Desconectar {0}",
|
||||
"tip_no_account": "Se ainda não tem uma conta Mastodon, {0}.",
|
||||
"tip_register_account": "escolha um servidor e inscreva-se"
|
||||
},
|
||||
"visibility": {
|
||||
"direct": "Direta",
|
||||
"direct_desc": "Visível apenas pelos utilizadores mencionados",
|
||||
"private": "Apenas seguidores",
|
||||
"private_desc": "Visível apenas pelos seus seguidores",
|
||||
"public": "Publico",
|
||||
"public_desc": "Visível por todos",
|
||||
"unlisted": "Não listada",
|
||||
"unlisted_desc": "Visível por todos, mas não incluida nas funcionalidades de divulgação"
|
||||
}
|
||||
}
|
479
locales/tr-TR.json
Normal file
479
locales/tr-TR.json
Normal file
|
@ -0,0 +1,479 @@
|
|||
{
|
||||
"a11y": {
|
||||
"loading_page": "Sayfa yükleniyor, lütfen bekleyin",
|
||||
"loading_titled_page": "Sayfa {0} yükleniyor, lütfen bekleyin",
|
||||
"locale_changed": "Dil değiştirildi, yeni dil: {0}",
|
||||
"locale_changing": "Dil değiştiriliyor, lütfen bekleyin",
|
||||
"route_loaded": "Sayfa {0} yüklendi"
|
||||
},
|
||||
"account": {
|
||||
"avatar_description": "{0} avatarı",
|
||||
"blocked_by": "Bu kullanıcı sizi engellemiş",
|
||||
"blocked_domains": "Engellenen alan adları",
|
||||
"blocked_users": "Engellenen kullanıcılar",
|
||||
"blocking": "Engelli",
|
||||
"bot": "BOT",
|
||||
"favourites": "Favoriles",
|
||||
"follow": "Takip et",
|
||||
"follow_back": "Geri takip et",
|
||||
"follow_requested": "İstek gönderildi",
|
||||
"followers": "Takipçiler",
|
||||
"followers_count": "{0} Takipçi",
|
||||
"following": "Takip edilenler",
|
||||
"following_count": "{0} takip edilen",
|
||||
"follows_you": "Seni takip ediyor",
|
||||
"go_to_profile": "Profile git",
|
||||
"joined": "Katıldı",
|
||||
"moved_title": "belirttiği yeni hesabı:",
|
||||
"muted_users": "Susturulmuş kullanıcılar",
|
||||
"muting": "Susturulmuş",
|
||||
"mutuals": "Karşılıklı takip",
|
||||
"pinned": "Sabitlendi",
|
||||
"posts": "Gönderiler",
|
||||
"posts_count": "{0} Gönderi",
|
||||
"profile_description": "{0} profil başlığı",
|
||||
"profile_unavailable": "Profil mevcut değil",
|
||||
"unblock": "Engeli kaldır",
|
||||
"unfollow": "Takibi bırak",
|
||||
"unmute": "Susturulmayı kaldır"
|
||||
},
|
||||
"action": {
|
||||
"apply": "Uygula",
|
||||
"bookmark": "Yer imlerine ekle",
|
||||
"bookmarked": "Yer imlerine eklendi",
|
||||
"boost": "Boost",
|
||||
"boost_count": "{0}",
|
||||
"boosted": "Boost edildi",
|
||||
"clear_upload_failed": "Dosya yükleme hatalarını temizle",
|
||||
"close": "Kapat",
|
||||
"compose": "Oluştur",
|
||||
"confirm": "Onayla",
|
||||
"edit": "Düzenle",
|
||||
"enter_app": "Uygulamaya gir",
|
||||
"favourite": "Favorilere ekle",
|
||||
"favourite_count": "{0}",
|
||||
"favourited": "Favorilere eklendi",
|
||||
"more": "Daha fazla",
|
||||
"next": "Sonraki",
|
||||
"prev": "Önceki",
|
||||
"publish": "Yayımla",
|
||||
"reply": "Cevap ver",
|
||||
"reply_count": "{0}",
|
||||
"reset": "Sıfırla",
|
||||
"save": "Kaydet",
|
||||
"save_changes": "Değişikleri kaydet",
|
||||
"sign_in": "Giriş yap",
|
||||
"switch_account": "Hesap değiştir",
|
||||
"vote": "Oy ver"
|
||||
},
|
||||
"app_desc_short": "Hızlı bir Mastodon web istemcisi",
|
||||
"app_logo": "Elk Logosu",
|
||||
"app_name": "Elk",
|
||||
"attachment": {
|
||||
"edit_title": "Açıklama",
|
||||
"remove_label": "Eki kaldır"
|
||||
},
|
||||
"command": {
|
||||
"activate": "Etkinleştir",
|
||||
"complete": "Tamamla",
|
||||
"compose_desc": "Yeni bir gönderi yaz",
|
||||
"n-people-in-the-past-n-days": "geçen {1} gündeki {0} kişi",
|
||||
"select_lang": "Dil seç",
|
||||
"sign_in_desc": "Var olan bir hesap ekle",
|
||||
"switch_account": "{0} hesabına geç",
|
||||
"switch_account_desc": "Başka bir hesaba geç",
|
||||
"toggle_dark_mode": "Karanlık mod durumunu değiştir",
|
||||
"toggle_zen_mode": "Zen mod durumunu değiştir"
|
||||
},
|
||||
"common": {
|
||||
"confirm_dialog": {
|
||||
"cancel": "Hayır",
|
||||
"confirm": "Evet",
|
||||
"title": "Emin misiniz?"
|
||||
},
|
||||
"end_of_list": "Listenin sonu",
|
||||
"error": "HATA",
|
||||
"in": "içinde",
|
||||
"not_found": "404 Bulunamadı",
|
||||
"offline_desc": "Çevrimdışısınız gibi görünüyor. Lütfen internet bağlantınızı kontrol edin."
|
||||
},
|
||||
"compose": {
|
||||
"draft_title": "Taslak {0}",
|
||||
"drafts": "Taslaklar ({v})"
|
||||
},
|
||||
"conversation": {
|
||||
"with": "ile"
|
||||
},
|
||||
"error": {
|
||||
"account_not_found": "Hesap {0} bulunamadı",
|
||||
"explore-list-empty": "Şu anda hiçbir şey trend değil. Daha sonra tekrar kontrol edin!",
|
||||
"file_size_cannot_exceed_n_mb": "Dosya boyutu {0}MB'ı geçemez",
|
||||
"sign_in_error": "Sunucuya bağlanılamadı.",
|
||||
"status_not_found": "Gönderi bulunamadı",
|
||||
"unsupported_file_format": "Desteklenmeyen dosya biçimi"
|
||||
},
|
||||
"help": {
|
||||
"desc_highlight": "Orada burada bir kaç hata ve eksik özellik bekleyin.",
|
||||
"desc_para1": "Elk'i, bizim çalışması devam eden Mastodon web istemcimizi, denemedeki ilginiz için teşekkürler!",
|
||||
"desc_para2": "Zaman içinde geliştirmek ve iyileştirmek için çok çalışıyoruz.",
|
||||
"desc_para3": "Geliştirmemizi hızlandırmak için takıma Github Sponsors üzerinden sponsor olabilirsinizi. Umarız Elk'i beğenirsiniz!",
|
||||
"desc_para4": "Elk açık kaynaklıdır. Test etmek, geri dönüş vermek veya katkıda bulunmak isterseniz,",
|
||||
"desc_para5": "GitHub'da bize ulaşın",
|
||||
"desc_para6": "ve dahil olun.",
|
||||
"title": "Elk ön izlemede!"
|
||||
},
|
||||
"language": {
|
||||
"search": "Ara"
|
||||
},
|
||||
"menu": {
|
||||
"block_account": "Engele {0}",
|
||||
"block_domain": "Alan adı {0} engelle",
|
||||
"copy_link_to_post": "Bu gönderinin linkini kopyala",
|
||||
"delete": "Sil",
|
||||
"delete_and_redraft": "Sil & yeniden taslak yap",
|
||||
"delete_confirm": {
|
||||
"cancel": "İptal et",
|
||||
"confirm": "Sil",
|
||||
"title": "Bu gönderiyi silmek istediğinizden emin misiniz?"
|
||||
},
|
||||
"direct_message_account": "{0} özel mesaj gönder",
|
||||
"edit": "Düzenle",
|
||||
"hide_reblogs": "{0} boostlarını gizle",
|
||||
"mention_account": "{0} etiketle",
|
||||
"mute_account": "{0} sustur",
|
||||
"mute_conversation": "Bu gönderiyi sustur",
|
||||
"open_in_original_site": "Orijinal sitede aç",
|
||||
"pin_on_profile": "Profilde sabitle",
|
||||
"share_post": "Bu gönderiyi paylaş",
|
||||
"show_favourited_and_boosted_by": "Favoriye ekleyenleri ve boost edenleri göster",
|
||||
"show_reblogs": "{0} boostlarını göster",
|
||||
"show_untranslated": "Çevrilmemiş halini göster",
|
||||
"toggle_theme": {
|
||||
"dark": "Karanlık mod durumunu değiştir",
|
||||
"light": "Aydınlık mod durumunu değiştir"
|
||||
},
|
||||
"translate_post": "Gönderiyi çevir",
|
||||
"unblock_account": "{0} engelini kaldır",
|
||||
"unblock_domain": "Alan adı {0} engelini kaldır",
|
||||
"unmute_account": "{0} sesini aç",
|
||||
"unmute_conversation": "Gönderinin sesini aç",
|
||||
"unpin_on_profile": "Profildeki sabiti kaldır"
|
||||
},
|
||||
"nav": {
|
||||
"back": "Geri git",
|
||||
"blocked_domains": "Engellenen alan adları",
|
||||
"blocked_users": "Engellenen kullanıcılar",
|
||||
"bookmarks": "Yer imleri",
|
||||
"built_at": "{0} derlendi",
|
||||
"conversations": "Konuşmalar",
|
||||
"explore": "Keşfet",
|
||||
"favourites": "Favoriler",
|
||||
"federated": "Federe",
|
||||
"home": "Ev",
|
||||
"local": "Yerel",
|
||||
"muted_users": "Susturulmuş kullanıcılar",
|
||||
"notifications": "Bildirimler",
|
||||
"profile": "Profil",
|
||||
"search": "Ara",
|
||||
"select_feature_flags": "Özelliklerin Durumunu Değiştir",
|
||||
"select_font_size": "Font Boyutu",
|
||||
"select_language": "Görünüm Dili",
|
||||
"settings": "Ayarlar",
|
||||
"show_intro": "Girişi göster",
|
||||
"toggle_theme": "Temayı Değiştir",
|
||||
"zen_mode": "Zen Modu"
|
||||
},
|
||||
"notification": {
|
||||
"favourited_post": "gönderini favoriledi",
|
||||
"followed_you": "Seni takip etti",
|
||||
"followed_you_count": "{0} kişi seni takip etti",
|
||||
"missing_type": "EKSİK notification.type:",
|
||||
"reblogged_post": "gönderini yeniden blogladı",
|
||||
"request_to_follow": "Takip isteği attı",
|
||||
"signed_up": "Kaydoldu",
|
||||
"update_status": "Gönderisini güncelledi"
|
||||
},
|
||||
"placeholder": {
|
||||
"content_warning": "Uyarını buraya yaz",
|
||||
"default_1": "Aklında ne var?",
|
||||
"reply_to_account": "{0} cevap ver",
|
||||
"replying": "Cevap veriliyor",
|
||||
"the_thread": "konu"
|
||||
},
|
||||
"pwa": {
|
||||
"dismiss": "Görmezden gel",
|
||||
"title": "Yeni Elk güncellemesi mevcut!",
|
||||
"update": "Güncelle",
|
||||
"update_available_short": "Elk'i güncelle",
|
||||
"webmanifest": {
|
||||
"canary": {
|
||||
"description": "Hızlı bir Mastodon web istemcisi (canary)",
|
||||
"name": "Elk (canary)",
|
||||
"short_name": "Elk (canary)"
|
||||
},
|
||||
"dev": {
|
||||
"description": "Hızlı bir Mastodon web istemcisi (dev)",
|
||||
"name": "Elk (dev)",
|
||||
"short_name": "Elk (dev)"
|
||||
},
|
||||
"preview": {
|
||||
"description": "Hızlı bir Mastodon web istemcisi (preview)",
|
||||
"name": "Elk (preview)",
|
||||
"short_name": "Elk (preview)"
|
||||
},
|
||||
"release": {
|
||||
"description": "Hızlı bir Mastodon web istemcisi",
|
||||
"name": "Elk",
|
||||
"short_name": "Elk"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"search_desc": "İnsanları & etiketleri ara",
|
||||
"search_empty": "Bu arama için sonuç bulunamadı"
|
||||
},
|
||||
"settings": {
|
||||
"about": {
|
||||
"label": "Hakkında",
|
||||
"meet_the_team": "Takım ile buluş",
|
||||
"sponsor_action": "Bize spponsor ol",
|
||||
"sponsor_action_desc": "Elk'i geliştiren takıma destek olmak için",
|
||||
"sponsors": "Sponsorlar",
|
||||
"sponsors_body_1": "Elk cömert sponsorluk ve şunların yardımı sayesinde mümkün oldu:",
|
||||
"sponsors_body_2": "Ve Elk takımına ve üyelerine sponsor olan tüm şirketler ve şahıslar.",
|
||||
"sponsors_body_3": "Eğer uygulamadan hoşlandıysanız bize sponsor olmayı düşünün:"
|
||||
},
|
||||
"account_settings": {
|
||||
"description": "Mastodon UI'da hesap ayarlarını değiştir",
|
||||
"label": "Hesap ayarları"
|
||||
},
|
||||
"interface": {
|
||||
"color_mode": "Renk Modu",
|
||||
"dark_mode": "Karanlık Mod",
|
||||
"default": " (varsayılan)",
|
||||
"font_size": "Font Boyutu",
|
||||
"label": "Arayüz",
|
||||
"light_mode": "Aydınlık Mod",
|
||||
"size_label": {
|
||||
"lg": "Büyük",
|
||||
"md": "Orta",
|
||||
"sm": "Küçük",
|
||||
"xl": "Çok büyük",
|
||||
"xs": "Çok küçük"
|
||||
},
|
||||
"system_mode": "Sistem"
|
||||
},
|
||||
"language": {
|
||||
"display_language": "Görünüm Dili",
|
||||
"label": "Dil"
|
||||
},
|
||||
"notifications": {
|
||||
"label": "Bildirimler",
|
||||
"notifications": {
|
||||
"label": "Bildirim ayarları"
|
||||
},
|
||||
"push_notifications": {
|
||||
"alerts": {
|
||||
"favourite": "Favoriler",
|
||||
"follow": "Yeni takipçiler",
|
||||
"mention": "Bahsetmeler",
|
||||
"poll": "Anketler",
|
||||
"reblog": "Gönderinizi yeniden bloglamalar",
|
||||
"title": "Hangi bildirimleri alacaksınız??"
|
||||
},
|
||||
"description": "Elk'i kullanmıyorken bile bildirimleri alın.",
|
||||
"instructions": "@:settings.notifications.push_notifications.save_settings butonunu kullanarak değişikleri kaydetmeyi unutmayın!",
|
||||
"label": "Anlık bildirim ayarları",
|
||||
"policy": {
|
||||
"all": "Herkesden",
|
||||
"followed": "Takip ettiğim kişilerden",
|
||||
"follower": "Takipçilerden",
|
||||
"none": "Kimseden",
|
||||
"title": "Kimden bildirim alabilirim??"
|
||||
},
|
||||
"save_settings": "Ayarları kaydet",
|
||||
"subscription_error": {
|
||||
"clear_error": "Hatayı temizle",
|
||||
"permission_denied": "Erişim engellendi: tarayıcınızda bildirimleri etkinleştirin.",
|
||||
"request_error": "Abonelik talep edilirken bir hata oluştu, tekrar deneyin ve hata devam ederse lütfen sorunu Elk deposuna bildirin.",
|
||||
"title": "Anlık bildirimlere abone olunamadı",
|
||||
"too_many_registrations": "Tarayıcı kısıtlamaları nedeniyle Elk, farklı sunuculardaki birden çok hesap için anlık bildirimler hizmetini kullanamaz. Başka bir hesaptaki anlık bildirim aboneliğinden çıkmalı ve tekrar denemelisiniz."
|
||||
},
|
||||
"title": "Anlık bildirim ayarları",
|
||||
"undo_settings": "Değişiklikleri geri al",
|
||||
"unsubscribe": "Anlık bildirimleri devre dışı bırak",
|
||||
"unsupported": "Tarayıcınız anlık bildirimleri desteklemiyor.",
|
||||
"warning": {
|
||||
"enable_close": "Kapat",
|
||||
"enable_description": "Elk açık değilken bildirim almak için anlık bildirimleri etkinleştirin. Etkinleştirildikten sonra yukarıdaki \"@:settings.notifications.show_btn{'\"'} düğmesini kullanarak tam olarak ne tür etkileşimlerin anlık bildirimler oluşturduğunu kontrol edebilirsiniz.",
|
||||
"enable_description_desktop": "Elk açık değilken bildirim almak için anlık bildirimleri etkinleştirin. Etkinleştirildikten sonra \"Ayarlar > Bildirimler > Anlık bildirim ayarları\"nda hangi tür etkileşimlerin anlık bildirimler oluşturduğunu tam olarak kontrol edebilirsiniz.",
|
||||
"enable_description_mobile": "Ayarlara \"Ayarlar > Bildirimler > Anlık bildirim ayarları\" gezinme menüsünü kullanarak da erişebilirsiniz.",
|
||||
"enable_description_settings": "Elk açık değilken bildirim almak için anlık bildirimleri etkinleştirin. Etkinleştirdikten sonra, aynı ekranda hangi tür etkileşimlerin anlık bildirimleri oluşturduğunu tam olarak kontrol edebileceksiniz.",
|
||||
"enable_desktop": "Anlık bildirimleri etkinleştir",
|
||||
"enable_title": "Hiçbirşeyi kaçırma",
|
||||
"re_auth": "Görünüşe göre sunucunuz anlık bildirimleri desteklemiyor. Çıkış yapmayı deneyin ve tekrar giriş yapın, bu mesaj hala görünüyorsa sunucu yöneticinizle iletişime geçin."
|
||||
}
|
||||
},
|
||||
"show_btn": "Bildirim ayarlarına git"
|
||||
},
|
||||
"notifications_settings": "Bildirimler",
|
||||
"preferences": {
|
||||
"github_cards": "GitHub Cards",
|
||||
"hide_boost_count": "Boost sayısını gizle",
|
||||
"hide_favorite_count": "Favori sayısını gizle",
|
||||
"hide_follower_count": "Takipçi sayısını gizle",
|
||||
"label": "Ayarlar",
|
||||
"title": "Deneysel Özellikler",
|
||||
"user_picker": "Kullanıcı Seçici",
|
||||
"virtual_scroll": "Görsel Kaydırma"
|
||||
},
|
||||
"profile": {
|
||||
"appearance": {
|
||||
"bio": "Açıklama",
|
||||
"description": "avatar, kullanıcı adı, profil vb. düzenle",
|
||||
"display_name": "Görünen ad",
|
||||
"label": "Görünüm",
|
||||
"profile_metadata": "Profil üstverisi",
|
||||
"profile_metadata_desc": "Profilinizde bir tablo olarak görüntülenen en fazla {0} öğeye sahip olabilirsiniz.",
|
||||
"title": "Profili düzenle"
|
||||
},
|
||||
"featured_tags": {
|
||||
"description": "İnsanlar bu etiketlerler altında herkese açık gönderilerinize göz atabilir.",
|
||||
"label": "Öne çıkan etiketler"
|
||||
},
|
||||
"label": "Profil"
|
||||
},
|
||||
"select_a_settings": "Bir ayar seç",
|
||||
"users": {
|
||||
"export": "Kullanıcı Tokenlerini Dışa Aktar",
|
||||
"import": "Kullanıcı Tokenlerini İçe Aktar",
|
||||
"label": "Giriş yapılan kullanıcılar"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"attachments_exceed_server_limit": "Ek sayısı gönderi başına sınırı aştı.",
|
||||
"attachments_limit_error": "Gönderi başına sınır aşıldı",
|
||||
"edited": "(Düzenlendi)",
|
||||
"editing": "Düzenleniyor",
|
||||
"loading": "Yükleniyor...",
|
||||
"publishing": "Yayımlanıyor",
|
||||
"upload_failed": "Yükleme başarısız",
|
||||
"uploading": "Yükleniyor..."
|
||||
},
|
||||
"status": {
|
||||
"boosted_by": "Tarafından boostlandı:",
|
||||
"edited": "Düzenlendi {0}",
|
||||
"favourited_by": "Tarafından favorilendi:",
|
||||
"filter_hidden_phrase": "Tarafından filtrelendi:",
|
||||
"filter_removed_phrase": "Filtre tarafından silindi",
|
||||
"filter_show_anyway": "Yine de göster",
|
||||
"img_alt": {
|
||||
"desc": "Açıklama",
|
||||
"dismiss": "Görmezden gel"
|
||||
},
|
||||
"poll": {
|
||||
"count": "{0} oy",
|
||||
"ends": "{0} biter",
|
||||
"finished": "{0} bitti"
|
||||
},
|
||||
"reblogged": "{0} yeniden blogladı",
|
||||
"replying_to": "{0} cevap veriliyor",
|
||||
"show_full_thread": "Tüm konuyu göster",
|
||||
"someone": "biri",
|
||||
"spoiler_show_less": "Daha az göster",
|
||||
"spoiler_show_more": "Daha çok göster",
|
||||
"thread": "Konu",
|
||||
"try_original_site": "Orijinal siteyi dene"
|
||||
},
|
||||
"status_history": {
|
||||
"created": "{0} oluşturuldu",
|
||||
"edited": "{0} düzenlendi"
|
||||
},
|
||||
"tab": {
|
||||
"for_you": "Senin için",
|
||||
"hashtags": "Etiketler",
|
||||
"media": "Medya",
|
||||
"news": "Haberler",
|
||||
"notifications_all": "Hepsi",
|
||||
"notifications_mention": "Bahsetmeler",
|
||||
"posts": "Gönderiler",
|
||||
"posts_with_replies": "Gönderiler & Yanıtlar"
|
||||
},
|
||||
"tag": {
|
||||
"follow": "Takip et",
|
||||
"follow_label": "{0} etiketini takip et",
|
||||
"unfollow": "Takibi bırak",
|
||||
"unfollow_label": "{0} etiketini takibi bırak"
|
||||
},
|
||||
"time_ago_options": {
|
||||
"day_future": "{n} gün içinde",
|
||||
"day_past": "{n} gün önce",
|
||||
"hour_future": "{n} saat içinde",
|
||||
"hour_past": "{n} saat önce",
|
||||
"just_now": "şimdi",
|
||||
"minute_future": "{n} dakika içinde",
|
||||
"minute_past": "{n} dakika önce",
|
||||
"month_future": "{n} ay içinde",
|
||||
"month_past": "{n} ay önce",
|
||||
"second_future": "şimdi|{n} saniye içinde",
|
||||
"second_past": "şimdi|{n} saniye önce",
|
||||
"short_day_future": "{n} günde",
|
||||
"short_day_past": "{n}d",
|
||||
"short_hour_future": "{n} saatte",
|
||||
"short_hour_past": "{n}h",
|
||||
"short_minute_future": "{n} dakikada",
|
||||
"short_minute_past": "{n}min",
|
||||
"short_month_future": "{n} ayda",
|
||||
"short_month_past": "{n}mo",
|
||||
"short_second_future": "{n} saniyede",
|
||||
"short_second_past": "{n}s",
|
||||
"short_week_future": "{n} haftada",
|
||||
"short_week_past": "{n}w",
|
||||
"short_year_future": "{n} yılda",
|
||||
"short_year_past": "{n}y",
|
||||
"week_future": "{n} hafta içinde",
|
||||
"week_past": "{n} hafta önce",
|
||||
"year_future": "{n} yıl içinde",
|
||||
"year_past": "{n} yıl önce"
|
||||
},
|
||||
"timeline": {
|
||||
"show_new_items": "{v} yeni öğe göster",
|
||||
"view_older_posts": "Diğer sunuculardan eski gönderiler görüntülenmeyebilir."
|
||||
},
|
||||
"title": {
|
||||
"federated_timeline": "Federe Edilmiş Zaman Akışı",
|
||||
"local_timeline": "Yerel Zaman Akışı"
|
||||
},
|
||||
"tooltip": {
|
||||
"add_content_warning": "İçerik uyarısı ekle",
|
||||
"add_emojis": "Emoji ekle",
|
||||
"add_media": "resim, video yada ses dosyası ekle",
|
||||
"add_publishable_content": "Yayımlanacak içerik ekle",
|
||||
"change_content_visibility": "İçerik görünürlüğünü değiştir",
|
||||
"change_language": "Dil değiştir",
|
||||
"emoji": "Emoji",
|
||||
"explore_links_intro": "Bu haberler, şu anda merkezi olmayan ağın bu ve diğer sunucularındaki insanlar tarafından konuşuluyor.",
|
||||
"explore_posts_intro": "Bu gönderiler, şu anda merkezi olmayan ağın bu ve diğer sunucularındaki insanlar tarafından ilgi görüyor.",
|
||||
"explore_tags_intro": "Bu etiketler, şu anda merkezi olmayan ağın bu ve diğer sunucularındaki insanlar tarafından ilgi görüyor.",
|
||||
"toggle_code_block": "Kod bloğu durumunu değiştir"
|
||||
},
|
||||
"user": {
|
||||
"add_existing": "Var olan bir hesap ekle",
|
||||
"server_address_label": "Mastodon Sunucu Adresi",
|
||||
"sign_in_desc": "Profilleri veya hashtag'leri takip etmek, favorilere eklemek, gönderileri paylaşmak ve yanıtlamak veya farklı bir sunucudaki hesabınızdan etkileşim kurmak için oturum açın.",
|
||||
"sign_in_notice_title": "{0} herkese açık veri görüntüleniyor",
|
||||
"sign_out_account": "{0} çıkış yap",
|
||||
"tip_no_account": "Eğer bir Mastodon hesabınız yoksa, {0}.",
|
||||
"tip_register_account": "sunucunuzu seçin ve kaydolun"
|
||||
},
|
||||
"visibility": {
|
||||
"direct": "Direkt",
|
||||
"direct_desc": "Sadece bahsedilen kullanıcılara görünür",
|
||||
"private": "Sadece takipçiler",
|
||||
"private_desc": "Sadece takipçilere görünür",
|
||||
"public": "Herkese açık",
|
||||
"public_desc": "Herkese görünür",
|
||||
"unlisted": "Liste dışı",
|
||||
"unlisted_desc": "Herkes tarafından görülebilir, ancak keşif özellikleri devre dışı bırakılmıştır"
|
||||
}
|
||||
}
|
|
@ -35,7 +35,9 @@
|
|||
"profile_unavailable": "个人资料不可见",
|
||||
"unblock": "取消拉黑",
|
||||
"unfollow": "取消关注",
|
||||
"unmute": "取消屏蔽"
|
||||
"unmute": "取消屏蔽",
|
||||
"view_other_followers": "其他站点上的关注者可能不会在这里显示。",
|
||||
"view_other_following": "其他站点上正在关注的人可能不会在这里显示。"
|
||||
},
|
||||
"action": {
|
||||
"apply": "应用",
|
||||
|
@ -63,7 +65,7 @@
|
|||
"switch_account": "切换帐号",
|
||||
"vote": "投票"
|
||||
},
|
||||
"app_desc_short": "用 🧡 制作的 Mastodon 客户端",
|
||||
"app_desc_short": "一个灵巧的 Mastodon 客户端",
|
||||
"app_logo": "应用图标",
|
||||
"app_name": "鹿鸣",
|
||||
"attachment": {
|
||||
|
@ -86,7 +88,7 @@
|
|||
"confirm_dialog": {
|
||||
"cancel": "否",
|
||||
"confirm": "是",
|
||||
"title": "你确定吗?"
|
||||
"title": "你确定 {0} 吗?"
|
||||
},
|
||||
"end_of_list": "列表到底啦",
|
||||
"error": "错误",
|
||||
|
@ -162,6 +164,7 @@
|
|||
"blocked_users": "已拉黑的用户",
|
||||
"bookmarks": "书签",
|
||||
"built_at": "构建于 {0}",
|
||||
"compose": "撰写",
|
||||
"conversations": "私信",
|
||||
"explore": "探索",
|
||||
"favourites": "喜欢",
|
||||
|
@ -204,22 +207,22 @@
|
|||
"update_available_short": "更新鹿鸣",
|
||||
"webmanifest": {
|
||||
"canary": {
|
||||
"description": "用 🧡 制作的 Mastodon 客户端(Canary)",
|
||||
"description": "一个灵巧的 Mastodon 客户端(Canary)",
|
||||
"name": "鹿鸣 Canary",
|
||||
"short_name": "鹿鸣 Canary"
|
||||
},
|
||||
"dev": {
|
||||
"description": "用 🧡 制作的 Mastodon 客户端(开发版)",
|
||||
"description": "一个灵巧的 Mastodon 客户端(开发版)",
|
||||
"name": "鹿鸣 开发版",
|
||||
"short_name": "鹿鸣 开发版"
|
||||
},
|
||||
"preview": {
|
||||
"description": "用 🧡 制作的 Mastodon 客户端(预览版)",
|
||||
"description": "一个灵巧的 Mastodon 客户端(预览版)",
|
||||
"name": "鹿鸣 预览版",
|
||||
"short_name": "鹿鸣 预览版"
|
||||
},
|
||||
"release": {
|
||||
"description": "用 🧡 制作的 Mastodon 客户端",
|
||||
"description": "一个灵巧的 Mastodon 客户端",
|
||||
"name": "鹿鸣",
|
||||
"short_name": "鹿鸣"
|
||||
}
|
||||
|
@ -237,26 +240,21 @@
|
|||
"description": "在 Mastodon UI 中编辑你的账号设置",
|
||||
"label": "账号设置"
|
||||
},
|
||||
"feature_flags": {
|
||||
"github_cards": "GitHub 卡片",
|
||||
"title": "实验功能",
|
||||
"user_picker": "用户选择器",
|
||||
"virtual_scroll": "虚拟滚动"
|
||||
},
|
||||
"interface": {
|
||||
"color_mode": "颜色",
|
||||
"dark_mode": "深色模式",
|
||||
"dark_mode": "深色",
|
||||
"default": "(默认)",
|
||||
"font_size": "字号",
|
||||
"label": "外观",
|
||||
"light_mode": "浅色模式",
|
||||
"light_mode": "浅色",
|
||||
"size_label": {
|
||||
"lg": "大",
|
||||
"md": "中",
|
||||
"sm": "小",
|
||||
"xl": "特大",
|
||||
"xs": "特小"
|
||||
}
|
||||
},
|
||||
"system_mode": "跟随系统"
|
||||
},
|
||||
"language": {
|
||||
"display_language": "首选语言",
|
||||
|
@ -312,7 +310,14 @@
|
|||
},
|
||||
"notifications_settings": "通知",
|
||||
"preferences": {
|
||||
"label": "首选项"
|
||||
"github_cards": "GitHub 卡片",
|
||||
"hide_boost_count": "隐藏转发数",
|
||||
"hide_favorite_count": "隐藏收藏数",
|
||||
"hide_follower_count": "隐藏关注者数",
|
||||
"label": "首选项",
|
||||
"title": "实验功能",
|
||||
"user_picker": "用户选择器",
|
||||
"virtual_scroll": "虚拟滚动"
|
||||
},
|
||||
"profile": {
|
||||
"appearance": {
|
||||
|
@ -432,6 +437,7 @@
|
|||
},
|
||||
"tooltip": {
|
||||
"add_content_warning": "添加内容警告标识",
|
||||
"add_emojis": "添加表情符号",
|
||||
"add_media": "添加图片、视频或者音频文件",
|
||||
"add_publishable_content": "添加要发布的内容",
|
||||
"change_content_visibility": "修改内容是否可见",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue