consistent use of i18n for translations, replace , and global variables with local definitions

This commit is contained in:
ArneBo 2024-12-07 16:23:33 +01:00 committed by upsiflu
parent 4d78c2143c
commit edad84b0b6
188 changed files with 2252 additions and 1916 deletions

View File

@ -47,8 +47,8 @@ const labels = computed(() => ({
:::{tab-item} Template :::{tab-item} Template
```html ```html
<h2>{{ $t('components.About.header.funkwhale') }}</h2> <h2>{{ t('components.About.header.funkwhale') }}</h2>
<button>{{ $t('components.About.button.cancel') }}</button> <button>{{ t('components.About.button.cancel') }}</button>
``` ```
::: :::
@ -84,11 +84,11 @@ Some strings change depending on whether they are plural or not. You can create
v-if="object.artist?.content_category === 'podcast'" v-if="object.artist?.content_category === 'podcast'"
class="meta ellipsis" class="meta ellipsis"
> >
{{ $t('components.audio.ChannelCard.meta.episodes', {episode_count: {{ t('components.audio.ChannelCard.meta.episodes', {episode_count:
object.artist.tracks_count}) }} object.artist.tracks_count}) }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.audio.ChannelCard.meta.tracks', {tracks_count: {{ t('components.audio.ChannelCard.meta.tracks', {tracks_count:
object.artist?.tracks_count}) }} object.artist?.tracks_count}) }}
</span> </span>
<tags-list <tags-list

View File

@ -4,9 +4,13 @@ import { computed, ref, defineAsyncComponent, nextTick, onMounted } from 'vue'
import { useQueue } from '~/composables/audio/queue' import { useQueue } from '~/composables/audio/queue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import onKeyboardShortcut from '~/composables/onKeyboardShortcut' import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
const { t } = useI18n()
const ChannelUploadModal = defineAsyncComponent(() => import('~/components/channels/UploadModal.vue')) const ChannelUploadModal = defineAsyncComponent(() => import('~/components/channels/UploadModal.vue'))
const PlaylistModal = defineAsyncComponent(() => import('~/components/playlists/PlaylistModal.vue')) const PlaylistModal = defineAsyncComponent(() => import('~/components/playlists/PlaylistModal.vue'))
const FilterModal = defineAsyncComponent(() => import('~/components/moderation/FilterModal.vue')) const FilterModal = defineAsyncComponent(() => import('~/components/moderation/FilterModal.vue'))
@ -85,7 +89,7 @@ const showSetInstanceModal = ref(false)
<component :is="Component" /> <component :is="Component" />
<template #fallback> <template #fallback>
<!-- TODO (wvffle): Add loader --> <!-- TODO (wvffle): Add loader -->
{{ $t('App.loading') }} {{ t('App.loading') }}
</template> </template>
</Suspense> </Suspense>
</keep-alive> </keep-alive>

View File

@ -67,10 +67,10 @@ const headerStyle = computed(() => {
<div class="column" /> <div class="column" />
</div> </div>
<h2 class="header"> <h2 class="header">
{{ $t('components.About.header.funkwhale') }} {{ t('components.About.header.funkwhale') }}
</h2> </h2>
<p> <p>
{{ $t('components.About.description.funkwhale') }} {{ t('components.About.description.funkwhale') }}
</p> </p>
</div> </div>
</div> </div>
@ -84,14 +84,14 @@ const headerStyle = computed(() => {
class="signup-form content" class="signup-form content"
> >
<h3 class="header"> <h3 class="header">
{{ $t('components.About.header.signup') }} {{ t('components.About.header.signup') }}
</h3> </h3>
<template v-if="openRegistrations"> <template v-if="openRegistrations">
<p> <p>
{{ $t('components.About.description.signup') }} {{ t('components.About.description.signup') }}
</p> </p>
<p v-if="defaultUploadQuota"> <p v-if="defaultUploadQuota">
{{ $t('components.About.description.quota', {quota: defaultUploadQuota}) }} {{ t('components.About.description.quota', {quota: defaultUploadQuota}) }}
</p> </p>
<signup-form <signup-form
button-classes="success" button-classes="success"
@ -100,7 +100,7 @@ const headerStyle = computed(() => {
</template> </template>
<div v-else> <div v-else>
<p> <p>
{{ $t('components.About.help.closedRegistrations') }} {{ t('components.About.help.closedRegistrations') }}
</p> </p>
<a <a
@ -108,7 +108,7 @@ const headerStyle = computed(() => {
rel="noopener" rel="noopener"
href="https://funkwhale.audio/#get-started" href="https://funkwhale.audio/#get-started"
> >
{{ $t('components.About.link.findOtherPod') }} {{ t('components.About.link.findOtherPod') }}
&nbsp;<i class="external alternate icon" /> &nbsp;<i class="external alternate icon" />
</a> </a>
</div> </div>
@ -118,13 +118,13 @@ const headerStyle = computed(() => {
class="signup-form content" class="signup-form content"
> >
<h3 class="header"> <h3 class="header">
{{ $t('components.About.header.signup') }} {{ t('components.About.header.signup') }}
<div class="ui positive message"> <div class="ui positive message">
<div class="header"> <div class="header">
{{ $t('components.About.message.loggedIn') }} {{ t('components.About.message.loggedIn') }}
</div> </div>
<p> <p>
{{ $t('components.About.message.greeting', {username: $store.state.auth.username}) }} {{ t('components.About.message.greeting', {username: $store.state.auth.username}) }}
</p> </p>
</div> </div>
</h3> </h3>
@ -145,7 +145,7 @@ const headerStyle = computed(() => {
id="description" id="description"
class="ui header" class="ui header"
> >
{{ $t('components.About.header.aboutPod') }} {{ t('components.About.header.aboutPod') }}
</h3> </h3>
<div <div
v-if="shortDescription" v-if="shortDescription"
@ -154,7 +154,7 @@ const headerStyle = computed(() => {
{{ shortDescription }} {{ shortDescription }}
</div> </div>
<p v-else> <p v-else>
{{ $t('components.About.placeholder.noDescription') }} {{ t('components.About.placeholder.noDescription') }}
</p> </p>
<template v-if="stats"> <template v-if="stats">
@ -164,14 +164,14 @@ const headerStyle = computed(() => {
<span class="statistics-figure ui text"> <span class="statistics-figure ui text">
<span class="ui big text"><strong>{{ stats.users.toLocaleString($store.state.ui.momentLocale) }}</strong></span> <span class="ui big text"><strong>{{ stats.users.toLocaleString($store.state.ui.momentLocale) }}</strong></span>
<br> <br>
{{ $t('components.About.stat.activeUsers', stats.users) }} {{ t('components.About.stat.activeUsers', stats.users) }}
</span> </span>
</div> </div>
<div class="column"> <div class="column">
<span class="statistics-figure ui text"> <span class="statistics-figure ui text">
<span class="ui big text"><strong>{{ stats.hours.toLocaleString($store.state.ui.momentLocale) }}</strong></span> <span class="ui big text"><strong>{{ stats.hours.toLocaleString($store.state.ui.momentLocale) }}</strong></span>
<br> <br>
{{ $t('components.About.stat.hoursOfMusic', stats.hours) }} {{ t('components.About.stat.hoursOfMusic', stats.hours) }}
</span> </span>
</div> </div>
</div> </div>
@ -182,7 +182,7 @@ const headerStyle = computed(() => {
to="/about/pod" to="/about/pod"
class="ui fluid basic secondary button" class="ui fluid basic secondary button"
> >
{{ $t('components.About.link.learnMore') }} {{ t('components.About.link.learnMore') }}
</router-link> </router-link>
</div> </div>
</div> </div>
@ -201,10 +201,10 @@ const headerStyle = computed(() => {
id="description" id="description"
class="ui header" class="ui header"
> >
{{ $t('components.About.header.publicContent') }} {{ t('components.About.header.publicContent') }}
</h3> </h3>
<p> <p>
{{ $t('components.About.description.publicContent') }} {{ t('components.About.description.publicContent') }}
</p> </p>
</div> </div>
</router-link> </router-link>
@ -218,11 +218,11 @@ const headerStyle = computed(() => {
id="description" id="description"
class="ui header" class="ui header"
> >
{{ $t('components.About.link.findOtherPod') }} {{ t('components.About.link.findOtherPod') }}
&nbsp;<i class="external alternate icon" /> &nbsp;<i class="external alternate icon" />
</h3> </h3>
<p> <p>
{{ $t('components.About.description.publicContent') }} {{ t('components.About.description.publicContent') }}
</p> </p>
</div> </div>
</a> </a>
@ -236,11 +236,11 @@ const headerStyle = computed(() => {
id="description" id="description"
class="ui header" class="ui header"
> >
{{ $t('components.About.header.findApp') }} {{ t('components.About.header.findApp') }}
&nbsp;<i class="external alternate icon" /> &nbsp;<i class="external alternate icon" />
</h3> </h3>
<p> <p>
{{ $t('components.About.description.findApp') }} {{ t('components.About.description.findApp') }}
</p> </p>
</div> </div>
</a> </a>
@ -250,7 +250,7 @@ const headerStyle = computed(() => {
to="/about/pod" to="/about/pod"
class="ui right floated basic secondary button" class="ui right floated basic secondary button"
> >
{{ $t('components.About.header.aboutPod') }} {{ t('components.About.header.aboutPod') }}
<i class="icon arrow right" /> <i class="icon arrow right" />
</router-link> </router-link>
</div> </div>

View File

@ -99,32 +99,32 @@ const headerStyle = computed(() => {
to="/about/pod" to="/about/pod"
class="item" class="item"
> >
{{ $t('components.AboutPod.link.about') }} {{ t('components.AboutPod.link.about') }}
</router-link> </router-link>
<router-link <router-link
to="/about/pod#rules" to="/about/pod#rules"
class="item" class="item"
> >
{{ $t('components.AboutPod.link.rules') }} {{ t('components.AboutPod.link.rules') }}
</router-link> </router-link>
<router-link <router-link
to="/about/pod#terms" to="/about/pod#terms"
class="item" class="item"
> >
{{ $t('components.AboutPod.link.terms') }} {{ t('components.AboutPod.link.terms') }}
</router-link> </router-link>
<router-link <router-link
to="/about/pod#features" to="/about/pod#features"
class="item" class="item"
> >
{{ $t('components.AboutPod.link.features') }} {{ t('components.AboutPod.link.features') }}
</router-link> </router-link>
<router-link <router-link
v-if="stats" v-if="stats"
to="/about/pod#statistics" to="/about/pod#statistics"
class="item" class="item"
> >
{{ $t('components.AboutPod.link.statistics') }} {{ t('components.AboutPod.link.statistics') }}
</router-link> </router-link>
</div> </div>
</div> </div>
@ -134,49 +134,49 @@ const headerStyle = computed(() => {
id="description about-this-pod" id="description about-this-pod"
class="ui header" class="ui header"
> >
{{ $t('components.AboutPod.header.about') }} {{ t('components.AboutPod.header.about') }}
</h2> </h2>
<sanitized-html <sanitized-html
v-if="longDescription" v-if="longDescription"
:html="longDescription" :html="longDescription"
/> />
<p v-else> <p v-else>
{{ $t('components.AboutPod.placeholder.noDescription') }} {{ t('components.AboutPod.placeholder.noDescription') }}
</p> </p>
<h3 <h3
id="rules" id="rules"
class="ui header" class="ui header"
> >
{{ $t('components.AboutPod.header.rules') }} {{ t('components.AboutPod.header.rules') }}
</h3> </h3>
<sanitized-html <sanitized-html
v-if="rules" v-if="rules"
:html="rules" :html="rules"
/> />
<p v-else> <p v-else>
{{ $t('components.AboutPod.placeholder.noRules') }} {{ t('components.AboutPod.placeholder.noRules') }}
</p> </p>
<h3 <h3
id="terms" id="terms"
class="ui header" class="ui header"
> >
{{ $t('components.AboutPod.header.terms') }} {{ t('components.AboutPod.header.terms') }}
</h3> </h3>
<sanitized-html <sanitized-html
v-if="terms" v-if="terms"
:html="terms" :html="terms"
/> />
<p v-else> <p v-else>
{{ $t('components.AboutPod.placeholder.noTerms') }} {{ t('components.AboutPod.placeholder.noTerms') }}
</p> </p>
<h3 <h3
id="features" id="features"
class="header" class="header"
> >
{{ $t('components.AboutPod.header.features') }} {{ t('components.AboutPod.header.features') }}
</h3> </h3>
<div class="features-container ui two column stackable grid"> <div class="features-container ui two column stackable grid">
<div class="column"> <div class="column">
@ -184,7 +184,7 @@ const headerStyle = computed(() => {
<tbody> <tbody>
<tr> <tr>
<td> <td>
{{ $t('components.AboutPod.feature.version') }} {{ t('components.AboutPod.feature.version') }}
</td> </td>
<td <td
v-if="version" v-if="version"
@ -199,13 +199,13 @@ const headerStyle = computed(() => {
class="right aligned" class="right aligned"
> >
<span class="features-status ui text"> <span class="features-status ui text">
{{ $t('components.AboutPod.notApplicable') }} {{ t('components.AboutPod.notApplicable') }}
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.AboutPod.feature.federation') }} {{ t('components.AboutPod.feature.federation') }}
</td> </td>
<td <td
v-if="federationEnabled" v-if="federationEnabled"
@ -213,7 +213,7 @@ const headerStyle = computed(() => {
> >
<span class="features-status ui text"> <span class="features-status ui text">
<i class="check icon" /> <i class="check icon" />
{{ $t('components.AboutPod.feature.status.enabled') }} {{ t('components.AboutPod.feature.status.enabled') }}
</span> </span>
</td> </td>
<td <td
@ -222,13 +222,13 @@ const headerStyle = computed(() => {
> >
<span class="features-status ui text"> <span class="features-status ui text">
<i class="x icon" /> <i class="x icon" />
{{ $t('components.AboutPod.feature.status.disabled') }} {{ t('components.AboutPod.feature.status.disabled') }}
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.AboutPod.feature.allowList') }} {{ t('components.AboutPod.feature.allowList') }}
</td> </td>
<td <td
v-if="allowListEnabled" v-if="allowListEnabled"
@ -236,7 +236,7 @@ const headerStyle = computed(() => {
> >
<span class="features-status ui text"> <span class="features-status ui text">
<i class="check icon" /> <i class="check icon" />
{{ $t('components.AboutPod.feature.status.enabled') }} {{ t('components.AboutPod.feature.status.enabled') }}
</span> </span>
</td> </td>
<td <td
@ -245,7 +245,7 @@ const headerStyle = computed(() => {
> >
<span class="features-status ui text"> <span class="features-status ui text">
<i class="x icon" /> <i class="x icon" />
{{ $t('components.AboutPod.feature.status.disabled') }} {{ t('components.AboutPod.feature.status.disabled') }}
</span> </span>
</td> </td>
</tr> </tr>
@ -257,7 +257,7 @@ const headerStyle = computed(() => {
<tbody> <tbody>
<tr> <tr>
<td> <td>
{{ $t('components.AboutPod.feature.anonymousAccess') }} {{ t('components.AboutPod.feature.anonymousAccess') }}
</td> </td>
<td <td
v-if="anonymousCanListen" v-if="anonymousCanListen"
@ -265,7 +265,7 @@ const headerStyle = computed(() => {
> >
<span class="features-status ui text"> <span class="features-status ui text">
<i class="check icon" /> <i class="check icon" />
{{ $t('components.AboutPod.feature.status.enabled') }} {{ t('components.AboutPod.feature.status.enabled') }}
</span> </span>
</td> </td>
<td <td
@ -274,13 +274,13 @@ const headerStyle = computed(() => {
> >
<span class="features-status ui text"> <span class="features-status ui text">
<i class="x icon" /> <i class="x icon" />
{{ $t('components.AboutPod.feature.status.disabled') }} {{ t('components.AboutPod.feature.status.disabled') }}
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.AboutPod.feature.registrations') }} {{ t('components.AboutPod.feature.registrations') }}
</td> </td>
<td <td
v-if="openRegistrations" v-if="openRegistrations"
@ -288,7 +288,7 @@ const headerStyle = computed(() => {
> >
<span class="features-status ui text"> <span class="features-status ui text">
<i class="check icon" /> <i class="check icon" />
{{ $t('components.AboutPod.feature.status.open') }} {{ t('components.AboutPod.feature.status.open') }}
</span> </span>
</td> </td>
<td <td
@ -297,13 +297,13 @@ const headerStyle = computed(() => {
> >
<span class="features-status ui text"> <span class="features-status ui text">
<i class="x icon" /> <i class="x icon" />
{{ $t('components.AboutPod.feature.status.closed') }} {{ t('components.AboutPod.feature.status.closed') }}
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.AboutPod.feature.quota') }} {{ t('components.AboutPod.feature.quota') }}
</td> </td>
<td <td
v-if="defaultUploadQuota" v-if="defaultUploadQuota"
@ -318,7 +318,7 @@ const headerStyle = computed(() => {
class="right aligned" class="right aligned"
> >
<span class="features-status ui text"> <span class="features-status ui text">
{{ $t('components.AboutPod.notApplicable') }} {{ t('components.AboutPod.notApplicable') }}
</span> </span>
</td> </td>
</tr> </tr>
@ -332,7 +332,7 @@ const headerStyle = computed(() => {
id="statistics" id="statistics"
class="header" class="header"
> >
{{ $t('components.AboutPod.header.statistics') }} {{ t('components.AboutPod.header.statistics') }}
</h3> </h3>
<div class="statistics-container"> <div class="statistics-container">
<div <div
@ -342,7 +342,7 @@ const headerStyle = computed(() => {
<span class="statistics-figure ui text"> <span class="statistics-figure ui text">
<span class="ui big text"><strong>{{ stats.hours.toLocaleString($store.state.ui.momentLocale) }}</strong></span> <span class="ui big text"><strong>{{ stats.hours.toLocaleString($store.state.ui.momentLocale) }}</strong></span>
<br> <br>
{{ $t('components.AboutPod.stat.hoursOfMusic', stats.hours) }} {{ t('components.AboutPod.stat.hoursOfMusic', stats.hours) }}
</span> </span>
</div> </div>
<div <div
@ -352,7 +352,7 @@ const headerStyle = computed(() => {
<span class="statistics-figure ui text"> <span class="statistics-figure ui text">
<span class="ui big text"><strong>{{ stats.artists.toLocaleString($store.state.ui.momentLocale) }}</strong></span> <span class="ui big text"><strong>{{ stats.artists.toLocaleString($store.state.ui.momentLocale) }}</strong></span>
<br> <br>
{{ $t('components.AboutPod.stat.artistsCount', stats.artists) }} {{ t('components.AboutPod.stat.artistsCount', stats.artists) }}
</span> </span>
</div> </div>
<div <div
@ -362,7 +362,7 @@ const headerStyle = computed(() => {
<span class="statistics-figure ui text"> <span class="statistics-figure ui text">
<span class="ui big text"><strong>{{ stats.albums.toLocaleString($store.state.ui.momentLocale) }}</strong></span> <span class="ui big text"><strong>{{ stats.albums.toLocaleString($store.state.ui.momentLocale) }}</strong></span>
<br> <br>
{{ $t('components.AboutPod.stat.albumsCount', stats.albums) }} {{ t('components.AboutPod.stat.albumsCount', stats.albums) }}
</span> </span>
</div> </div>
<div <div
@ -372,7 +372,7 @@ const headerStyle = computed(() => {
<span class="statistics-figure ui text"> <span class="statistics-figure ui text">
<span class="ui big text"><strong>{{ stats.tracks.toLocaleString($store.state.ui.momentLocale) }}</strong></span> <span class="ui big text"><strong>{{ stats.tracks.toLocaleString($store.state.ui.momentLocale) }}</strong></span>
<br> <br>
{{ $t('components.AboutPod.stat.tracksCount', stats.tracks) }} {{ t('components.AboutPod.stat.tracksCount', stats.tracks) }}
</span> </span>
</div> </div>
<div <div
@ -382,7 +382,7 @@ const headerStyle = computed(() => {
<span class="statistics-figure ui text"> <span class="statistics-figure ui text">
<span class="ui big text"><strong>{{ stats.users.toLocaleString($store.state.ui.momentLocale) }}</strong></span> <span class="ui big text"><strong>{{ stats.users.toLocaleString($store.state.ui.momentLocale) }}</strong></span>
<br> <br>
{{ $t('components.AboutPod.stat.activeUsers', stats.users) }} {{ t('components.AboutPod.stat.activeUsers', stats.users) }}
</span> </span>
</div> </div>
<div <div
@ -392,7 +392,7 @@ const headerStyle = computed(() => {
<span class="statistics-figure ui text"> <span class="statistics-figure ui text">
<span class="ui big text"><strong>{{ stats.listenings.toLocaleString($store.state.ui.momentLocale) }}</strong></span> <span class="ui big text"><strong>{{ stats.listenings.toLocaleString($store.state.ui.momentLocale) }}</strong></span>
<br> <br>
{{ $t('components.AboutPod.stat.listeningsCount', stats.listenings) }} {{ t('components.AboutPod.stat.listeningsCount', stats.listenings) }}
</span> </span>
</div> </div>
</div> </div>
@ -403,13 +403,13 @@ const headerStyle = computed(() => {
id="contact" id="contact"
class="ui header" class="ui header"
> >
{{ $t('components.AboutPod.header.contact') }} {{ t('components.AboutPod.header.contact') }}
</h3> </h3>
<a <a
v-if="contactEmail" v-if="contactEmail"
:href="`mailto:${contactEmail}`" :href="`mailto:${contactEmail}`"
> >
{{ $t('components.AboutPod.message.contact', { contactEmail }) }} {{ t('components.AboutPod.message.contact', { contactEmail }) }}
</a> </a>
</template> </template>
@ -420,7 +420,7 @@ const headerStyle = computed(() => {
class="ui left floated basic secondary button" class="ui left floated basic secondary button"
> >
<i class="icon arrow left" /> <i class="icon arrow left" />
{{ $t('components.AboutPod.link.introduction') }} {{ t('components.AboutPod.link.introduction') }}
</router-link> </router-link>
</div> </div>
</div> </div>

View File

@ -73,7 +73,7 @@ whenever(() => store.state.auth.authenticated, () => {
<div class="segment-content"> <div class="segment-content">
<h1 class="ui center aligned large header"> <h1 class="ui center aligned large header">
<span> <span>
{{ $t('components.Home.header.welcome', {podName: podName}) }} {{ t('components.Home.header.welcome', {podName: podName}) }}
</span> </span>
<div <div
v-if="shortDescription" v-if="shortDescription"
@ -88,7 +88,7 @@ whenever(() => store.state.auth.authenticated, () => {
<div class="ui stackable grid"> <div class="ui stackable grid">
<div class="ten wide column"> <div class="ten wide column">
<h2 class="header"> <h2 class="header">
{{ $t('components.Home.header.about') }} {{ t('components.Home.header.about') }}
</h2> </h2>
<div <div
id="pod" id="pod"
@ -97,7 +97,7 @@ whenever(() => store.state.auth.authenticated, () => {
<div class="ui stackable grid"> <div class="ui stackable grid">
<div class="eight wide column"> <div class="eight wide column">
<p v-if="!longDescription"> <p v-if="!longDescription">
{{ $t('components.Home.placeholder.noDescription') }} {{ t('components.Home.placeholder.noDescription') }}
</p> </p>
<template v-if="longDescription || rules"> <template v-if="longDescription || rules">
<sanitized-html <sanitized-html
@ -120,7 +120,7 @@ whenever(() => store.state.auth.authenticated, () => {
class="ui link" class="ui link"
:to="{name: 'about'}" :to="{name: 'about'}"
> >
{{ $t('components.Home.link.learnMore') }} {{ t('components.Home.link.learnMore') }}
</router-link> </router-link>
</div> </div>
</div> </div>
@ -135,7 +135,7 @@ whenever(() => store.state.auth.authenticated, () => {
class="ui link" class="ui link"
:to="{name: 'about', hash: '#rules'}" :to="{name: 'about', hash: '#rules'}"
> >
{{ $t('components.Home.link.rules') }} {{ t('components.Home.link.rules') }}
</router-link> </router-link>
</div> </div>
</div> </div>
@ -145,20 +145,20 @@ whenever(() => store.state.auth.authenticated, () => {
<div class="eight wide column"> <div class="eight wide column">
<template v-if="stats"> <template v-if="stats">
<h3 class="sub header"> <h3 class="sub header">
{{ $t('components.Home.header.statistics') }} {{ t('components.Home.header.statistics') }}
</h3> </h3>
<p> <p>
<i class="user icon" /> <i class="user icon" />
{{ $t('components.Home.stat.activeUsers', stats.users) }} {{ t('components.Home.stat.activeUsers', stats.users) }}
</p> </p>
<p> <p>
<i class="music icon" /> <i class="music icon" />
{{ $t('components.Home.stat.hoursOfMusic', stats.hours) }} {{ t('components.Home.stat.hoursOfMusic', stats.hours) }}
</p> </p>
</template> </template>
<template v-if="contactEmail"> <template v-if="contactEmail">
<h3 class="sub header"> <h3 class="sub header">
{{ $t('components.Home.header.contact') }} {{ t('components.Home.header.contact') }}
</h3> </h3>
<i class="at icon" /> <i class="at icon" />
<a :href="`mailto:${contactEmail}`">{{ contactEmail }}</a> <a :href="`mailto:${contactEmail}`">{{ contactEmail }}</a>
@ -181,13 +181,13 @@ whenever(() => store.state.auth.authenticated, () => {
<div class="ui stackable grid"> <div class="ui stackable grid">
<div class="four wide column"> <div class="four wide column">
<h3 class="header"> <h3 class="header">
{{ $t('components.Home.header.aboutFunkwhale') }} {{ t('components.Home.header.aboutFunkwhale') }}
</h3> </h3>
<p> <p>
{{ $t('components.Home.description.funkwhale.paragraph1') }} {{ t('components.Home.description.funkwhale.paragraph1') }}
</p> </p>
<p> <p>
{{ $t('components.Home.description.funkwhale.paragraph2') }} {{ t('components.Home.description.funkwhale.paragraph2') }}
</p> </p>
<a <a
target="_blank" target="_blank"
@ -195,12 +195,12 @@ whenever(() => store.state.auth.authenticated, () => {
href="https://funkwhale.audio" href="https://funkwhale.audio"
> >
<i class="external alternate icon" /> <i class="external alternate icon" />
{{ $t('components.Home.link.funkwhale') }} {{ t('components.Home.link.funkwhale') }}
</a> </a>
</div> </div>
<div class="four wide column"> <div class="four wide column">
<h3 class="header"> <h3 class="header">
{{ $t('components.Home.header.login') }} {{ t('components.Home.header.login') }}
</h3> </h3>
<login-form <login-form
button-classes="success" button-classes="success"
@ -210,14 +210,14 @@ whenever(() => store.state.auth.authenticated, () => {
</div> </div>
<div class="four wide column"> <div class="four wide column">
<h3 class="header"> <h3 class="header">
{{ $t('components.Home.header.signup') }} {{ t('components.Home.header.signup') }}
</h3> </h3>
<template v-if="openRegistrations"> <template v-if="openRegistrations">
<p> <p>
{{ $t('components.Home.description.signup') }} {{ t('components.Home.description.signup') }}
</p> </p>
<p v-if="defaultUploadQuota"> <p v-if="defaultUploadQuota">
{{ $t('components.Home.description.quota', { quota: humanSize(defaultUploadQuota * 1000 * 1000) }) }} {{ t('components.Home.description.quota', { quota: humanSize(defaultUploadQuota * 1000 * 1000) }) }}
</p> </p>
<signup-form <signup-form
button-classes="success" button-classes="success"
@ -226,7 +226,7 @@ whenever(() => store.state.auth.authenticated, () => {
</template> </template>
<div v-else> <div v-else>
<p> <p>
{{ $t('components.Home.help.registrationsClosed') }} {{ t('components.Home.help.registrationsClosed') }}
</p> </p>
<a <a
target="_blank" target="_blank"
@ -234,14 +234,14 @@ whenever(() => store.state.auth.authenticated, () => {
href="https://funkwhale.audio/#get-started" href="https://funkwhale.audio/#get-started"
> >
<i class="external alternate icon" /> <i class="external alternate icon" />
{{ $t('components.Home.link.findOtherPod') }} {{ t('components.Home.link.findOtherPod') }}
</a> </a>
</div> </div>
</div> </div>
<div class="four wide column"> <div class="four wide column">
<h3 class="header"> <h3 class="header">
{{ $t('components.Home.header.links') }} {{ t('components.Home.header.links') }}
</h3> </h3>
<div class="ui relaxed list"> <div class="ui relaxed list">
<div class="item"> <div class="item">
@ -252,10 +252,10 @@ whenever(() => store.state.auth.authenticated, () => {
class="header" class="header"
to="/library" to="/library"
> >
{{ $t('components.Home.link.publicContent.label') }} {{ t('components.Home.link.publicContent.label') }}
</router-link> </router-link>
<div class="description"> <div class="description">
{{ $t('components.Home.link.publicContent.description') }} {{ t('components.Home.link.publicContent.description') }}
</div> </div>
</div> </div>
</div> </div>
@ -268,10 +268,10 @@ whenever(() => store.state.auth.authenticated, () => {
target="_blank" target="_blank"
rel="noopener" rel="noopener"
> >
{{ $t('components.Home.link.mobileApps.label') }} {{ t('components.Home.link.mobileApps.label') }}
</a> </a>
<div class="description"> <div class="description">
{{ $t('components.Home.link.mobileApps.description') }} {{ t('components.Home.link.mobileApps.description') }}
</div> </div>
</div> </div>
</div> </div>
@ -284,10 +284,10 @@ whenever(() => store.state.auth.authenticated, () => {
target="_blank" target="_blank"
rel="noopener" rel="noopener"
> >
{{ $t('components.Home.link.userGuides.label') }} {{ t('components.Home.link.userGuides.label') }}
</a> </a>
<div class="description"> <div class="description">
{{ $t('components.Home.link.userGuides.description') }} {{ t('components.Home.link.userGuides.description') }}
</div> </div>
</div> </div>
</div> </div>
@ -304,16 +304,16 @@ whenever(() => store.state.auth.authenticated, () => {
:limit="10" :limit="10"
> >
<template #title> <template #title>
{{ $t('components.Home.header.newAlbums') }} {{ t('components.Home.header.newAlbums') }}
</template> </template>
<router-link to="/library"> <router-link to="/library">
{{ $t('components.Home.link.viewMore') }} {{ t('components.Home.link.viewMore') }}
<div class="ui hidden divider" /> <div class="ui hidden divider" />
</router-link> </router-link>
</album-widget> </album-widget>
<div class="ui hidden section divider" /> <div class="ui hidden section divider" />
<h3 class="ui header"> <h3 class="ui header">
{{ $t('components.Home.header.newChannels') }} {{ t('components.Home.header.newChannels') }}
</h3> </h3>
<channels-widget <channels-widget
:show-modification-date="true" :show-modification-date="true"

View File

@ -20,11 +20,11 @@ const labels = computed(() => ({
<h1 class="ui huge header"> <h1 class="ui huge header">
<i class="warning icon" /> <i class="warning icon" />
<div class="content"> <div class="content">
{{ $t('components.PageNotFound.header.pageNotFound') }} {{ t('components.PageNotFound.header.pageNotFound') }}
</div> </div>
</h1> </h1>
<p> <p>
{{ $t('components.PageNotFound.message.pageNotFound') }} {{ t('components.PageNotFound.message.pageNotFound') }}
</p> </p>
<a :href="path">{{ path }}</a> <a :href="path">{{ path }}</a>
<div class="ui hidden divider" /> <div class="ui hidden divider" />
@ -32,7 +32,7 @@ const labels = computed(() => ({
class="ui icon labeled right button" class="ui icon labeled right button"
to="/" to="/"
> >
{{ $t('components.PageNotFound.link.home') }} {{ t('components.PageNotFound.link.home') }}
<i class="right arrow icon" /> <i class="right arrow icon" />
</router-link> </router-link>
</div> </div>

View File

@ -5,7 +5,6 @@ import { whenever, watchDebounced, useCurrentElement, useScrollLock, useFullscre
import { nextTick, ref, computed, watchEffect, defineAsyncComponent } from 'vue' import { nextTick, ref, computed, watchEffect, defineAsyncComponent } from 'vue'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap' import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'
import { usePlayer } from '~/composables/audio/player' import { usePlayer } from '~/composables/audio/player'
@ -14,6 +13,8 @@ import { useQueue } from '~/composables/audio/queue'
import time from '~/utils/time' import time from '~/utils/time'
import { useI18n } from 'vue-i18n'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue' import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue' import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
import PlayerControls from '~/components/audio/PlayerControls.vue' import PlayerControls from '~/components/audio/PlayerControls.vue'
@ -21,6 +22,8 @@ import PlayerControls from '~/components/audio/PlayerControls.vue'
import VirtualList from '~/components/vui/list/VirtualList.vue' import VirtualList from '~/components/vui/list/VirtualList.vue'
import QueueItem from '~/components/QueueItem.vue' import QueueItem from '~/components/QueueItem.vue'
const { t } = useI18n()
const MilkDrop = defineAsyncComponent(() => import('~/components/audio/visualizer/MilkDrop.vue')) const MilkDrop = defineAsyncComponent(() => import('~/components/audio/visualizer/MilkDrop.vue'))
const { const {
@ -194,12 +197,12 @@ if (!isWebGLSupported) {
<img <img
v-if="fullscreen" v-if="fullscreen"
class="cover-shadow" class="cover-shadow"
:src="$store.getters['instance/absoluteUrl'](currentTrack.coverUrl)" :src="store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
> >
<img <img
ref="cover" ref="cover"
alt="" alt=""
:src="$store.getters['instance/absoluteUrl'](currentTrack.coverUrl)" :src="store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
> >
</template> </template>
<milk-drop <milk-drop
@ -212,7 +215,7 @@ if (!isWebGLSupported) {
v-if="!fullscreen || !idle" v-if="!fullscreen || !idle"
class="cover-buttons" class="cover-buttons"
> >
<tooltip :content="!isWebGLSupported && $t('components.Queue.message.webglUnsupported')"> <tooltip :content="!isWebGLSupported && t('components.Queue.message.webglUnsupported')">
<button <button
v-if="coverType === CoverType.COVER_ART" v-if="coverType === CoverType.COVER_ART"
class="ui secondary button" class="ui secondary button"
@ -267,11 +270,11 @@ if (!isWebGLSupported) {
v-for="ac in currentTrack.artistCredit" v-for="ac in currentTrack.artistCredit"
:key="ac.artist.id" :key="ac.artist.id"
> >
{{ ac.credit ?? $t('components.Queue.meta.unknownArtist') }} {{ ac.credit ?? t('components.Queue.meta.unknownArtist') }}
<span>{{ ac.joinphrase }}</span> <span>{{ ac.joinphrase }}</span>
</div> </div>
<span class="symbol hyphen middle" /> <span class="symbol hyphen middle" />
{{ currentTrack.albumTitle ?? $t('components.Queue.meta.unknownAlbum') }} {{ currentTrack.albumTitle ?? t('components.Queue.meta.unknownAlbum') }}
</h2> </h2>
</div> </div>
</Transition> </Transition>
@ -296,7 +299,7 @@ if (!isWebGLSupported) {
:to="{name: 'library.artists.detail', params: {id: ac.artist.id }}" :to="{name: 'library.artists.detail', params: {id: ac.artist.id }}"
@click.stop.prevent="" @click.stop.prevent=""
> >
{{ ac.credit ?? $t('components.Queue.meta.unknownArtist') }} {{ ac.credit ?? t('components.Queue.meta.unknownArtist') }}
</router-link> </router-link>
<span>{{ ac.joinphrase }}</span> <span>{{ ac.joinphrase }}</span>
</template> </template>
@ -307,7 +310,7 @@ if (!isWebGLSupported) {
class="discrete link album" class="discrete link album"
:to="{name: 'library.albums.detail', params: {id: currentTrack.albumId }}" :to="{name: 'library.albums.detail', params: {id: currentTrack.albumId }}"
> >
{{ currentTrack.albumTitle ?? $t('components.Queue.meta.unknownAlbum') }} {{ currentTrack.albumTitle ?? t('components.Queue.meta.unknownAlbum') }}
</router-link> </router-link>
</template> </template>
</div> </div>
@ -318,14 +321,14 @@ if (!isWebGLSupported) {
class="ui small warning message" class="ui small warning message"
> >
<h3 class="header"> <h3 class="header">
{{ $t('components.Queue.header.failure') }} {{ t('components.Queue.header.failure') }}
</h3> </h3>
<p v-if="hasNext && isPlaying"> <p v-if="hasNext && isPlaying">
{{ $t('components.Queue.message.automaticPlay') }} {{ t('components.Queue.message.automaticPlay') }}
<i class="loading spinner icon" /> <i class="loading spinner icon" />
</p> </p>
<p> <p>
{{ $t('components.Queue.warning.connectivity') }} {{ t('components.Queue.warning.connectivity') }}
</p> </p>
</div> </div>
<div <div
@ -333,24 +336,24 @@ if (!isWebGLSupported) {
class="ui small warning message" class="ui small warning message"
> >
<h3 class="header"> <h3 class="header">
{{ $t('components.Queue.header.noSources') }} {{ t('components.Queue.header.noSources') }}
</h3> </h3>
<p v-if="hasNext && isPlaying"> <p v-if="hasNext && isPlaying">
{{ $t('components.Queue.message.automaticPlay') }} {{ t('components.Queue.message.automaticPlay') }}
<i class="loading spinner icon" /> <i class="loading spinner icon" />
</p> </p>
</div> </div>
<div class="additional-controls desktop-and-below"> <div class="additional-controls desktop-and-below">
<track-favorite-icon <track-favorite-icon
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
:track="currentTrack" :track="currentTrack"
/> />
<track-playlist-icon <track-playlist-icon
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
:track="currentTrack" :track="currentTrack"
/> />
<button <button
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
:class="['ui', 'really', 'basic', 'circular', 'icon', 'button']" :class="['ui', 'really', 'basic', 'circular', 'icon', 'button']"
:aria-label="labels.addArtistContentFilter" :aria-label="labels.addArtistContentFilter"
:title="labels.addArtistContentFilter" :title="labels.addArtistContentFilter"
@ -386,8 +389,8 @@ if (!isWebGLSupported) {
<span class="right floated timer total">{{ time.parse(Math.round(duration)) }}</span> <span class="right floated timer total">{{ time.parse(Math.round(duration)) }}</span>
</template> </template>
<template v-else> <template v-else>
<span class="left floated timer">{{ $t('components.Queue.meta.startTime') }}</span> <span class="left floated timer">{{ t('components.Queue.meta.startTime') }}</span>
<span class="right floated timer">{{ $t('components.Queue.meta.startTime') }}</span> <span class="right floated timer">{{ t('components.Queue.meta.startTime') }}</span>
</template> </template>
</div> </div>
</div> </div>
@ -401,7 +404,7 @@ if (!isWebGLSupported) {
<button <button
v-t="'components.Queue.button.close'" v-t="'components.Queue.button.close'"
class="ui right floated basic button" class="ui right floated basic button"
@click="$store.commit('ui/queueFocused', null)" @click="store.commit('ui/queueFocused', null)"
/> />
<button <button
v-t="'components.Queue.button.clear'" v-t="'components.Queue.button.clear'"
@ -451,29 +454,29 @@ if (!isWebGLSupported) {
</template> </template>
<template #footer> <template #footer>
<div <div
v-if="$store.state.radios.populating" v-if="store.state.radios.populating"
class="radio-populating" class="radio-populating"
> >
<i class="loading spinner icon" /> <i class="loading spinner icon" />
{{ labels.populating }} {{ labels.populating }}
</div> </div>
<div <div
v-if="$store.state.radios.running" v-if="store.state.radios.running"
class="ui info message radio-message" class="ui info message radio-message"
> >
<div class="content"> <div class="content">
<h3 class="header"> <h3 class="header">
<i class="feed icon" /> <i class="feed icon" />
{{ $t('components.Queue.header.radio') }} {{ t('components.Queue.header.radio') }}
</h3> </h3>
<p> <p>
{{ $t('components.Queue.message.radio') }} {{ t('components.Queue.message.radio') }}
</p> </p>
<button <button
class="ui basic primary button" class="ui basic primary button"
@click="$store.dispatch('radios/stop')" @click="store.dispatch('radios/stop')"
> >
{{ $t('components.Queue.button.stopRadio') }} {{ t('components.Queue.button.stopRadio') }}
</button> </button>
</div> </div>
</div> </div>

View File

@ -3,6 +3,9 @@ import type { QueueItemSource } from '~/types'
import time from '~/utils/time' import time from '~/utils/time'
import { generateTrackCreditStringFromQueue } from '~/utils/utils' import { generateTrackCreditStringFromQueue } from '~/utils/utils'
import { useStore } from '~/store'
const store = useStore()
interface Events { interface Events {
(e: 'play', index: number): void (e: 'play', index: number): void
@ -55,14 +58,14 @@ defineProps<Props>()
</div> </div>
<div class="controls"> <div class="controls">
<button <button
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
:aria-label="source.labels.favorite" :aria-label="source.labels.favorite"
:title="source.labels.favorite" :title="source.labels.favorite"
class="ui really basic circular icon button" class="ui really basic circular icon button"
@click.stop="$store.dispatch('favorites/toggle', source.id)" @click.stop="store.dispatch('favorites/toggle', source.id)"
> >
<i <i
:class="$store.getters['favorites/isFavorite'](source.id) ? 'pink' : ''" :class="store.getters['favorites/isFavorite'](source.id) ? 'pink' : ''"
class="heart icon" class="heart icon"
/> />
</button> </button>

View File

@ -174,7 +174,7 @@ watch(() => props.initialId, () => {
@click.prevent="type = 'rss'" @click.prevent="type = 'rss'"
> >
<i class="feed icon" /> <i class="feed icon" />
{{ $t('components.RemoteSearchForm.button.rss') }} {{ t('components.RemoteSearchForm.button.rss') }}
</button> </button>
<div class="or" /> <div class="or" />
<button <button
@ -182,7 +182,7 @@ watch(() => props.initialId, () => {
@click.prevent="type = 'artists'" @click.prevent="type = 'artists'"
> >
<i class="globe icon" /> <i class="globe icon" />
{{ $t('components.RemoteSearchForm.button.fediverse') }} {{ t('components.RemoteSearchForm.button.fediverse') }}
</button> </button>
</div> </div>
<div v-else> <div v-else>
@ -197,7 +197,7 @@ watch(() => props.initialId, () => {
class="ui negative message" class="ui negative message"
> >
<h3 class="header"> <h3 class="header">
{{ $t('components.RemoteSearchForm.header.fetchFailed') }} {{ t('components.RemoteSearchForm.header.fetchFailed') }}
</h3> </h3>
<ul class="list"> <ul class="list">
<li <li
@ -213,10 +213,10 @@ watch(() => props.initialId, () => {
{{ labels.fieldLabel }} {{ labels.fieldLabel }}
</label> </label>
<p v-if="type === 'rss'"> <p v-if="type === 'rss'">
{{ $t('components.RemoteSearchForm.description.rss') }} {{ t('components.RemoteSearchForm.description.rss') }}
</p> </p>
<p v-else-if="type === 'artists'"> <p v-else-if="type === 'artists'">
{{ $t('components.RemoteSearchForm.description.fediverse') }} {{ t('components.RemoteSearchForm.description.fediverse') }}
</p> </p>
<input <input
id="object-id" id="object-id"
@ -233,7 +233,7 @@ watch(() => props.initialId, () => {
:class="['ui', 'primary', {loading: isLoading}, 'button']" :class="['ui', 'primary', {loading: isLoading}, 'button']"
:disabled="isLoading || !id || id.length === 0" :disabled="isLoading || !id || id.length === 0"
> >
{{ $t('components.RemoteSearchForm.button.search') }} {{ t('components.RemoteSearchForm.button.search') }}
</button> </button>
</form> </form>
<div <div
@ -242,7 +242,7 @@ watch(() => props.initialId, () => {
class="ui warning message" class="ui warning message"
> >
<p> <p>
{{ $t('components.RemoteSearchForm.warning.unsupported') }} {{ t('components.RemoteSearchForm.warning.unsupported') }}
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,7 +1,13 @@
<script setup lang="ts">
import { useStore } from '~/store'
const store = useStore()
</script>
<template> <template>
<div class="ui toast-container"> <div class="ui toast-container">
<message <message
v-for="message in $store.state.ui.messages" v-for="message in store.state.ui.messages"
:key="message.key" :key="message.key"
:message="message" :message="message"
/> />

View File

@ -110,7 +110,7 @@ const player = computed(() => [
<template> <template>
<semantic-modal v-model:show="showRef"> <semantic-modal v-model:show="showRef">
<header class="header"> <header class="header">
{{ $t('components.ShortcutsModal.header.modal') }} {{ t('components.ShortcutsModal.header.modal') }}
</header> </header>
<section class="scrolling content"> <section class="scrolling content">
<div class="ui stackable two column grid"> <div class="ui stackable two column grid">
@ -154,7 +154,7 @@ const player = computed(() => [
</section> </section>
<footer class="actions"> <footer class="actions">
<button class="ui basic cancel button"> <button class="ui basic cancel button">
{{ $t('components.ShortcutsModal.button.close') }} {{ t('components.ShortcutsModal.button.close') }}
</button> </button>
</footer> </footer>
</semantic-modal> </semantic-modal>

View File

@ -132,7 +132,7 @@ onMounted(() => {
> >
<i class="logo bordered inverted vibrant big icon"> <i class="logo bordered inverted vibrant big icon">
<logo class="logo" /> <logo class="logo" />
<span class="visually-hidden">{{ $t('components.Sidebar.link.home') }}</span> <span class="visually-hidden">{{ t('components.Sidebar.link.home') }}</span>
</i> </i>
</router-link> </router-link>
<nav class="top ui compact right aligned inverted text menu"> <nav class="top ui compact right aligned inverted text menu">
@ -152,7 +152,7 @@ onMounted(() => {
</div> </div>
<div class="menu"> <div class="menu">
<h3 class="header"> <h3 class="header">
{{ $t('components.Sidebar.header.administration') }} {{ t('components.Sidebar.header.administration') }}
</h3> </h3>
<div class="divider" /> <div class="divider" />
<router-link <router-link
@ -167,7 +167,7 @@ onMounted(() => {
> >
{{ $store.state.ui.notifications.pendingReviewEdits }} {{ $store.state.ui.notifications.pendingReviewEdits }}
</div> </div>
{{ $t('components.Sidebar.link.library') }} {{ t('components.Sidebar.link.library') }}
</router-link> </router-link>
<router-link <router-link
v-if="$store.state.auth.availablePermissions['moderation']" v-if="$store.state.auth.availablePermissions['moderation']"
@ -181,21 +181,21 @@ onMounted(() => {
> >
{{ $store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests }} {{ $store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests }}
</div> </div>
{{ $t('components.Sidebar.link.moderation') }} {{ t('components.Sidebar.link.moderation') }}
</router-link> </router-link>
<router-link <router-link
v-if="$store.state.auth.availablePermissions['settings']" v-if="$store.state.auth.availablePermissions['settings']"
class="item" class="item"
:to="{name: 'manage.users.users.list'}" :to="{name: 'manage.users.users.list'}"
> >
{{ $t('components.Sidebar.link.users') }} {{ t('components.Sidebar.link.users') }}
</router-link> </router-link>
<router-link <router-link
v-if="$store.state.auth.availablePermissions['settings']" v-if="$store.state.auth.availablePermissions['settings']"
class="item" class="item"
:to="{path: '/manage/settings'}" :to="{path: '/manage/settings'}"
> >
{{ $t('components.Sidebar.link.settings') }} {{ t('components.Sidebar.link.settings') }}
</router-link> </router-link>
</div> </div>
</div> </div>
@ -355,14 +355,14 @@ onMounted(() => {
class="ui fluid tiny primary button" class="ui fluid tiny primary button"
:to="{name: 'login'}" :to="{name: 'login'}"
> >
{{ $t('components.Sidebar.link.login') }} {{ t('components.Sidebar.link.login') }}
</router-link> </router-link>
<div class="ui small hidden divider" /> <div class="ui small hidden divider" />
<router-link <router-link
class="ui fluid tiny button" class="ui fluid tiny button"
:to="{path: '/signup'}" :to="{path: '/signup'}"
> >
{{ $t('components.Sidebar.link.createAccount') }} {{ t('components.Sidebar.link.createAccount') }}
</router-link> </router-link>
</div> </div>
<nav <nav
@ -374,7 +374,7 @@ onMounted(() => {
id="navigation-label" id="navigation-label"
class="visually-hidden" class="visually-hidden"
> >
{{ $t('components.Sidebar.header.main') }} {{ t('components.Sidebar.header.main') }}
</h1> </h1>
<div class="ui small hidden divider" /> <div class="ui small hidden divider" />
<section <section
@ -394,7 +394,7 @@ onMounted(() => {
@click="expanded = 'explore'" @click="expanded = 'explore'"
@focus="expanded = 'explore'" @focus="expanded = 'explore'"
> >
{{ $t('components.Sidebar.header.explore') }} {{ t('components.Sidebar.header.explore') }}
<i <i
v-if="expanded !== 'explore'" v-if="expanded !== 'explore'"
class="angle right icon" class="angle right icon"
@ -406,7 +406,7 @@ onMounted(() => {
:to="{name: 'search'}" :to="{name: 'search'}"
> >
<i class="search icon" /> <i class="search icon" />
{{ $t('components.Sidebar.link.search') }} {{ t('components.Sidebar.link.search') }}
</router-link> </router-link>
<router-link <router-link
class="item" class="item"
@ -414,42 +414,42 @@ onMounted(() => {
active-class="_active" active-class="_active"
> >
<i class="music icon" /> <i class="music icon" />
{{ $t('components.Sidebar.link.browse') }} {{ t('components.Sidebar.link.browse') }}
</router-link> </router-link>
<router-link <router-link
class="item" class="item"
:to="{name: 'library.podcasts.browse'}" :to="{name: 'library.podcasts.browse'}"
> >
<i class="podcast icon" /> <i class="podcast icon" />
{{ $t('components.Sidebar.link.podcasts') }} {{ t('components.Sidebar.link.podcasts') }}
</router-link> </router-link>
<router-link <router-link
class="item" class="item"
:to="{name: 'library.albums.browse'}" :to="{name: 'library.albums.browse'}"
> >
<i class="compact disc icon" /> <i class="compact disc icon" />
{{ $t('components.Sidebar.link.albums') }} {{ t('components.Sidebar.link.albums') }}
</router-link> </router-link>
<router-link <router-link
class="item" class="item"
:to="{name: 'library.artists.browse'}" :to="{name: 'library.artists.browse'}"
> >
<i class="user icon" /> <i class="user icon" />
{{ $t('components.Sidebar.link.artists') }} {{ t('components.Sidebar.link.artists') }}
</router-link> </router-link>
<router-link <router-link
class="item" class="item"
:to="{name: 'library.playlists.browse'}" :to="{name: 'library.playlists.browse'}"
> >
<i class="list icon" /> <i class="list icon" />
{{ $t('components.Sidebar.link.playlists') }} {{ t('components.Sidebar.link.playlists') }}
</router-link> </router-link>
<router-link <router-link
class="item" class="item"
:to="{name: 'library.radios.browse'}" :to="{name: 'library.radios.browse'}"
> >
<i class="feed icon" /> <i class="feed icon" />
{{ $t('components.Sidebar.link.radios') }} {{ t('components.Sidebar.link.radios') }}
</router-link> </router-link>
</div> </div>
</div> </div>
@ -464,7 +464,7 @@ onMounted(() => {
@click="expanded = 'myLibrary'" @click="expanded = 'myLibrary'"
@focus="expanded = 'myLibrary'" @focus="expanded = 'myLibrary'"
> >
{{ $t('components.Sidebar.header.library') }} {{ t('components.Sidebar.header.library') }}
<i <i
v-if="expanded !== 'myLibrary'" v-if="expanded !== 'myLibrary'"
class="angle right icon" class="angle right icon"
@ -476,42 +476,42 @@ onMounted(() => {
:to="{name: 'library.me'}" :to="{name: 'library.me'}"
> >
<i class="music icon" /> <i class="music icon" />
{{ $t('components.Sidebar.link.browse') }} {{ t('components.Sidebar.link.browse') }}
</router-link> </router-link>
<router-link <router-link
class="item" class="item"
:to="{name: 'library.albums.me'}" :to="{name: 'library.albums.me'}"
> >
<i class="compact disc icon" /> <i class="compact disc icon" />
{{ $t('components.Sidebar.link.albums') }} {{ t('components.Sidebar.link.albums') }}
</router-link> </router-link>
<router-link <router-link
class="item" class="item"
:to="{name: 'library.artists.me'}" :to="{name: 'library.artists.me'}"
> >
<i class="user icon" /> <i class="user icon" />
{{ $t('components.Sidebar.link.artists') }} {{ t('components.Sidebar.link.artists') }}
</router-link> </router-link>
<router-link <router-link
class="item" class="item"
:to="{name: 'library.playlists.me'}" :to="{name: 'library.playlists.me'}"
> >
<i class="list icon" /> <i class="list icon" />
{{ $t('components.Sidebar.link.playlists') }} {{ t('components.Sidebar.link.playlists') }}
</router-link> </router-link>
<router-link <router-link
class="item" class="item"
:to="{name: 'library.radios.me'}" :to="{name: 'library.radios.me'}"
> >
<i class="feed icon" /> <i class="feed icon" />
{{ $t('components.Sidebar.link.radios') }} {{ t('components.Sidebar.link.radios') }}
</router-link> </router-link>
<router-link <router-link
class="item" class="item"
:to="{name: 'favorites'}" :to="{name: 'favorites'}"
> >
<i class="heart icon" /> <i class="heart icon" />
{{ $t('components.Sidebar.link.favorites') }} {{ t('components.Sidebar.link.favorites') }}
</router-link> </router-link>
</div> </div>
</div> </div>
@ -520,11 +520,11 @@ onMounted(() => {
class="header item" class="header item"
:to="{name: 'subscriptions'}" :to="{name: 'subscriptions'}"
> >
{{ $t('components.Sidebar.link.channels') }} {{ t('components.Sidebar.link.channels') }}
</router-link> </router-link>
<div class="item"> <div class="item">
<h3 class="header"> <h3 class="header">
{{ $t('components.Sidebar.header.more') }} {{ t('components.Sidebar.header.more') }}
</h3> </h3>
<div class="menu"> <div class="menu">
<router-link <router-link
@ -533,7 +533,7 @@ onMounted(() => {
active-class="router-link-exact-active active" active-class="router-link-exact-active active"
> >
<i class="info icon" /> <i class="info icon" />
{{ $t('components.Sidebar.link.about') }} {{ t('components.Sidebar.link.about') }}
</router-link> </router-link>
</div> </div>
</div> </div>
@ -545,7 +545,7 @@ onMounted(() => {
to="/instance-chooser" to="/instance-chooser"
class="link item" class="link item"
> >
{{ $t('components.Sidebar.link.switchInstance') }} {{ t('components.Sidebar.link.switchInstance') }}
</router-link> </router-link>
</div> </div>
</nav> </nav>

View File

@ -6,6 +6,9 @@ import useFormData from '~/composables/useFormData'
import { ref, computed, reactive } from 'vue' import { ref, computed, reactive } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
interface Props { interface Props {
group: SettingsGroup group: SettingsGroup
@ -111,7 +114,7 @@ const save = async () => {
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.admin.SettingsGroup.header.error') }} {{ t('components.admin.SettingsGroup.header.error') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -126,7 +129,7 @@ const save = async () => {
v-if="result" v-if="result"
class="ui positive message" class="ui positive message"
> >
{{ $t('components.admin.SettingsGroup.message.success') }} {{ t('components.admin.SettingsGroup.message.success') }}
</div> </div>
<div <div
v-for="(setting, key) in settings" v-for="(setting, key) in settings"
@ -240,13 +243,13 @@ const save = async () => {
<div v-if="values[setting.identifier]"> <div v-if="values[setting.identifier]">
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<h3 class="ui header"> <h3 class="ui header">
{{ $t('components.admin.SettingsGroup.header.image') }} {{ t('components.admin.SettingsGroup.header.image') }}
</h3> </h3>
<img <img
v-if="values[setting.identifier]" v-if="values[setting.identifier]"
class="ui image" class="ui image"
alt="" alt=""
:src="$store.getters['instance/absoluteUrl'](values[setting.identifier])" :src="store.getters['instance/absoluteUrl'](values[setting.identifier])"
> >
</div> </div>
</div> </div>
@ -255,7 +258,7 @@ const save = async () => {
type="submit" type="submit"
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']" :class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']"
> >
{{ $t('components.admin.SettingsGroup.button.save') }} {{ t('components.admin.SettingsGroup.button.save') }}
</button> </button>
</form> </form>
</template> </template>

View File

@ -69,13 +69,13 @@ const move = (idx: number, increment: number) => {
:class="[{active: !isPreviewing}, 'item']" :class="[{active: !isPreviewing}, 'item']"
@click.stop.prevent="isPreviewing = false" @click.stop.prevent="isPreviewing = false"
> >
{{ $t('components.admin.SignupFormBuilder.button.edit') }} {{ t('components.admin.SignupFormBuilder.button.edit') }}
</button> </button>
<button <button
:class="[{active: isPreviewing}, 'item']" :class="[{active: isPreviewing}, 'item']"
@click.stop.prevent="isPreviewing = true" @click.stop.prevent="isPreviewing = true"
> >
{{ $t('components.admin.SignupFormBuilder.button.preview') }} {{ t('components.admin.SignupFormBuilder.button.preview') }}
</button> </button>
</div> </div>
<div <div
@ -95,10 +95,10 @@ const move = (idx: number, increment: number) => {
> >
<div class="field"> <div class="field">
<label for="help-text"> <label for="help-text">
{{ $t('components.admin.SignupFormBuilder.label.helpText') }} {{ t('components.admin.SignupFormBuilder.label.helpText') }}
</label> </label>
<p> <p>
{{ $t('components.admin.SignupFormBuilder.help.helpText') }} {{ t('components.admin.SignupFormBuilder.help.helpText') }}
</p> </p>
<content-form <content-form
v-if="value.help_text" v-if="value.help_text"
@ -109,24 +109,24 @@ const move = (idx: number, increment: number) => {
</div> </div>
<div class="field"> <div class="field">
<label> <label>
{{ $t('components.admin.SignupFormBuilder.label.additionalFields') }} {{ t('components.admin.SignupFormBuilder.label.additionalFields') }}
</label> </label>
<p> <p>
{{ $t('components.admin.SignupFormBuilder.help.additionalFields') }} {{ t('components.admin.SignupFormBuilder.help.additionalFields') }}
</p> </p>
<table v-if="value.fields?.length > 0"> <table v-if="value.fields?.length > 0">
<thead> <thead>
<tr> <tr>
<th> <th>
{{ $t('components.admin.SignupFormBuilder.table.additionalFields.header.label') }} {{ t('components.admin.SignupFormBuilder.table.additionalFields.header.label') }}
</th> </th>
<th> <th>
{{ $t('components.admin.SignupFormBuilder.table.additionalFields.header.type') }} {{ t('components.admin.SignupFormBuilder.table.additionalFields.header.type') }}
</th> </th>
<th> <th>
{{ $t('components.admin.SignupFormBuilder.table.additionalFields.header.required') }} {{ t('components.admin.SignupFormBuilder.table.additionalFields.header.required') }}
</th> </th>
<th><span class="visually-hidden">{{ $t('components.admin.SignupFormBuilder.table.additionalFields.header.actions') }}</span></th> <th><span class="visually-hidden">{{ t('components.admin.SignupFormBuilder.table.additionalFields.header.actions') }}</span></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -144,20 +144,20 @@ const move = (idx: number, increment: number) => {
<td> <td>
<select v-model="field.input_type"> <select v-model="field.input_type">
<option value="short_text"> <option value="short_text">
{{ $t('components.admin.SignupFormBuilder.table.additionalFields.type.short') }} {{ t('components.admin.SignupFormBuilder.table.additionalFields.type.short') }}
</option> </option>
<option value="long_text"> <option value="long_text">
{{ $t('components.admin.SignupFormBuilder.table.additionalFields.type.long') }} {{ t('components.admin.SignupFormBuilder.table.additionalFields.type.long') }}
</option> </option>
</select> </select>
</td> </td>
<td> <td>
<select v-model="field.required"> <select v-model="field.required">
<option :value="true"> <option :value="true">
{{ $t('components.admin.SignupFormBuilder.table.additionalFields.required.true') }} {{ t('components.admin.SignupFormBuilder.table.additionalFields.required.true') }}
</option> </option>
<option :value="false"> <option :value="false">
{{ $t('components.admin.SignupFormBuilder.table.additionalFields.required.false') }} {{ t('components.admin.SignupFormBuilder.table.additionalFields.required.false') }}
</option> </option>
</select> </select>
</td> </td>
@ -192,7 +192,7 @@ const move = (idx: number, increment: number) => {
class="ui basic button" class="ui basic button"
@click.stop.prevent="addField" @click.stop.prevent="addField"
> >
{{ $t('components.admin.SignupFormBuilder.button.add') }} {{ t('components.admin.SignupFormBuilder.button.add') }}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import type { Album } from '~/types' import type { Album } from '~/types'
@ -9,6 +11,8 @@ interface Props {
album: Album; album: Album;
} }
const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const { album } = props const { album } = props
@ -37,7 +41,7 @@ if (import.meta.env.PROD) {
class="funkwhale link" class="funkwhale link"
@click.stop="navigate('artist')" @click.stop="navigate('artist')"
> >
{{ ac.credit ?? $t('components.Queue.meta.unknownArtist') }} {{ ac.credit ?? t('components.Queue.meta.unknownArtist') }}
{{ ac.joinphrase }} {{ ac.joinphrase }}
</a> </a>
@ -57,7 +61,7 @@ if (import.meta.env.PROD) {
</router-link> </router-link>
<template #footer> <template #footer>
{{ $t('components.audio.album.Card.meta.tracks', album.tracks?.length || 0) }} {{ t('components.audio.album.Card.meta.tracks', album.tracks?.length || 0) }}
<fw-options-button /> <fw-options-button />
</template> </template>
</fw-card> </fw-card>

View File

@ -3,6 +3,8 @@ import type { Album } from '~/types'
import { reactive, ref, watch } from 'vue' import { reactive, ref, watch } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
@ -10,6 +12,8 @@ import AlbumCard from '~/components/album/Card.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
const { t } = useI18n()
interface Props { interface Props {
filters: Record<string, string | boolean> filters: Record<string, string | boolean>
showCount?: boolean showCount?: boolean
@ -112,7 +116,7 @@ watch(
:class="['ui', 'basic', 'button']" :class="['ui', 'basic', 'button']"
@click="fetchData(nextPage)" @click="fetchData(nextPage)"
> >
{{ $t('components.audio.album.Widget.button.more') }} {{ t('components.audio.album.Widget.button.more') }}
</button> </button>
</template> </template>
</div> </div>

View File

@ -2,6 +2,8 @@
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { computed } from 'vue' import { computed } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import type { Artist } from '~/types' import type { Artist } from '~/types'
@ -11,6 +13,8 @@ interface Props {
artist: Artist; artist: Artist;
} }
const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const { artist } = props const { artist } = props
@ -56,10 +60,10 @@ const imageUrl = computed(() => cover.value?.urls.original
<template #footer> <template #footer>
<span v-if="artist.content_category === 'music'"> <span v-if="artist.content_category === 'music'">
{{ $t('components.audio.artist.Card.meta.tracks', artist.tracks_count) }} {{ t('components.audio.artist.Card.meta.tracks', artist.tracks_count) }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.audio.artist.Card.meta.episodes', artist.tracks_count) }} {{ t('components.audio.artist.Card.meta.episodes', artist.tracks_count) }}
</span> </span>
</template> </template>

View File

@ -3,6 +3,8 @@ import type { Artist } from '~/types'
import { reactive, ref, watch } from 'vue' import { reactive, ref, watch } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
@ -10,6 +12,8 @@ import ArtistCard from '~/components/artist/Card.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
const { t } = useI18n()
interface Props { interface Props {
filters: Record<string, string | boolean> filters: Record<string, string | boolean>
search?: boolean search?: boolean
@ -108,7 +112,7 @@ watch(
:class="['ui', 'basic', 'button']" :class="['ui', 'basic', 'button']"
@click="fetchData(nextPage)" @click="fetchData(nextPage)"
> >
{{ $t('components.audio.artist.Widget.button.more') }} {{ t('components.audio.artist.Widget.button.more') }}
</button> </button>
</template> </template>
</div> </div>

View File

@ -1,5 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ArtistCredit } from '~/types' import type { ArtistCredit } from '~/types'
import { useStore } from '~/store'
const store = useStore()
interface Props { interface Props {
artistCredit: ArtistCredit[] artistCredit: ArtistCredit[]
@ -28,7 +31,7 @@ const getRoute = (ac: ArtistCredit) => {
> >
<img <img
v-if="ac.index === 0 && ac.artist.cover && ac.artist.cover.urls.original" v-if="ac.index === 0 && ac.artist.cover && ac.artist.cover.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](ac.artist.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](ac.artist.cover.urls.medium_square_crop)"
alt="" alt=""
:class="[{circular: ac.artist.content_category != 'podcast'}]" :class="[{circular: ac.artist.content_category != 'podcast'}]"
> >

View File

@ -2,6 +2,9 @@
import type { Artist } from '~/types' import type { Artist } from '~/types'
import { computed } from 'vue' import { computed } from 'vue'
import { useStore } from '~/store'
const store = useStore()
interface Props { interface Props {
artist: Artist artist: Artist
@ -22,7 +25,7 @@ const route = computed(() => props.artist.channel
> >
<img <img
v-if="artist.cover && artist.cover.urls.original" v-if="artist.cover && artist.cover.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](artist.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](artist.cover.urls.medium_square_crop)"
alt="" alt=""
:class="[{circular: artist.content_category != 'podcast'}]" :class="[{circular: artist.content_category != 'podcast'}]"
> >

View File

@ -5,6 +5,7 @@ import { momentFormat } from '~/utils/filters'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'
import { computed } from 'vue' import { computed } from 'vue'
import { useRouter } from 'vue-router'
import moment from 'moment' import moment from 'moment'
@ -17,6 +18,7 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const store = useStore() const store = useStore()
const router = useRouter()
const imageUrl = computed(() => props.object.artist?.cover const imageUrl = computed(() => props.object.artist?.cover
? store.getters['instance/absoluteUrl'](props.object.artist.cover.urls.medium_square_crop) ? store.getters['instance/absoluteUrl'](props.object.artist.cover.urls.medium_square_crop)
@ -45,7 +47,7 @@ const updatedAgo = computed(() => moment(props.object.artist?.modification_date)
<div <div
v-lazy:background-image="imageUrl" v-lazy:background-image="imageUrl"
:class="['ui', 'head-image', {'circular': object.artist?.content_category != 'podcast'}, {'padded': object.artist?.content_category === 'podcast'}, 'image', {'default-cover': !object.artist?.cover}]" :class="['ui', 'head-image', {'circular': object.artist?.content_category != 'podcast'}, {'padded': object.artist?.content_category === 'podcast'}, 'image', {'default-cover': !object.artist?.cover}]"
@click="$router.push({name: 'channels.detail', params: {id: urlId}})" @click="router.push({name: 'channels.detail', params: {id: urlId}})"
> >
<play-button <play-button
:icon-only="true" :icon-only="true"
@ -68,10 +70,10 @@ const updatedAgo = computed(() => moment(props.object.artist?.modification_date)
v-if="object.artist?.content_category === 'podcast'" v-if="object.artist?.content_category === 'podcast'"
class="meta ellipsis" class="meta ellipsis"
> >
{{ $t('components.audio.ChannelCard.meta.episodes', object.artist.tracks_count) }} {{ t('components.audio.ChannelCard.meta.episodes', object.artist.tracks_count) }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.audio.ChannelCard.meta.tracks', object.artist?.tracks_count ?? 0) }} {{ t('components.audio.ChannelCard.meta.tracks', object.artist?.tracks_count ?? 0) }}
</span> </span>
<tags-list <tags-list
label-classes="tiny" label-classes="tiny"

View File

@ -3,6 +3,7 @@ import type { Cover, Track, BackendResponse, BackendError } from '~/types'
import { clone } from 'lodash-es' import { clone } from 'lodash-es'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
import PodcastTable from '~/components/audio/podcast/Table.vue' import PodcastTable from '~/components/audio/podcast/Table.vue'
@ -18,6 +19,7 @@ interface Props {
defaultCover: Cover | null defaultCover: Cover | null
isPodcast: boolean isPodcast: boolean
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -103,7 +105,7 @@ watch(page, fetchData, { immediate: true })
@refresh="fetchData()" @refresh="fetchData()"
> >
<p> <p>
{{ $t('components.audio.ChannelEntries.help.subscribe') }} {{ t('components.audio.ChannelEntries.help.subscribe') }}
</p> </p>
</empty-state> </empty-state>
</template> </template>

View File

@ -5,10 +5,16 @@ import { computed } from 'vue'
import { usePlayer } from '~/composables/audio/player' import { usePlayer } from '~/composables/audio/player'
import { useQueue } from '~/composables/audio/queue' import { useQueue } from '~/composables/audio/queue'
import { useStore } from '~/store'
import { useRouter } from 'vue-router'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue' import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import PlayButton from '~/components/audio/PlayButton.vue' import PlayButton from '~/components/audio/PlayButton.vue'
const store = useStore()
const router = useRouter()
interface Props { interface Props {
entry: Track entry: Track
defaultCover: Cover defaultCover: Cover
@ -37,30 +43,30 @@ const duration = computed(() => props.entry.uploads.find(upload => upload.durati
</div> </div>
<img <img
v-if="cover && cover.urls.original" v-if="cover && cover.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"
alt="" alt=""
class="channel-image image" class="channel-image image"
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})" @click="router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
> >
<img <img
v-else-if="entry.artist_credit?.[0].artist.content_category === 'podcast' && defaultCover != undefined" v-else-if="entry.artist_credit?.[0].artist.content_category === 'podcast' && defaultCover != undefined"
v-lazy="$store.getters['instance/absoluteUrl'](defaultCover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](defaultCover.urls.medium_square_crop)"
class="channel-image image" class="channel-image image"
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})" @click="router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
> >
<img <img
v-else-if="entry.album && entry.album.cover && entry.album.cover.urls.original" v-else-if="entry.album && entry.album.cover && entry.album.cover.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](entry.album.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](entry.album.cover.urls.medium_square_crop)"
alt="" alt=""
class="channel-image image" class="channel-image image"
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})" @click="router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
> >
<img <img
v-else v-else
alt="" alt=""
class="channel-image image" class="channel-image image"
src="../../assets/audio/default-cover.png" src="../../assets/audio/default-cover.png"
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})" @click="router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
> >
<div class="ellipsis content"> <div class="ellipsis content">
<strong> <strong>
@ -78,7 +84,7 @@ const duration = computed(() => props.entry.uploads.find(upload => upload.durati
/> />
</div> </div>
<div class="meta"> <div class="meta">
<template v-if="$store.state.auth.authenticated && $store.getters['favorites/isFavorite'](entry.id)"> <template v-if="store.state.auth.authenticated && store.getters['favorites/isFavorite'](entry.id)">
<track-favorite-icon <track-favorite-icon
class="tiny" class="tiny"
:track="entry" :track="entry"

View File

@ -165,7 +165,7 @@ defineExpose({
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.audio.ChannelForm.header.error') }} {{ t('components.audio.ChannelForm.header.error') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -182,7 +182,7 @@ defineExpose({
class="ui grouped channel-type required field" class="ui grouped channel-type required field"
> >
<legend> <legend>
{{ $t('components.audio.ChannelForm.legend.purpose') }} {{ t('components.audio.ChannelForm.legend.purpose') }}
</legend> </legend>
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<div class="field"> <div class="field">
@ -210,7 +210,7 @@ defineExpose({
<template v-if="!creating || step === 2"> <template v-if="!creating || step === 2">
<div class="ui required field"> <div class="ui required field">
<label for="channel-name"> <label for="channel-name">
{{ $t('components.audio.ChannelForm.label.name') }} {{ t('components.audio.ChannelForm.label.name') }}
</label> </label>
<input <input
v-model="newValues.name" v-model="newValues.name"
@ -221,7 +221,7 @@ defineExpose({
</div> </div>
<div class="ui required field"> <div class="ui required field">
<label for="channel-username"> <label for="channel-username">
{{ $t('components.audio.ChannelForm.label.username') }} {{ t('components.audio.ChannelForm.label.username') }}
</label> </label>
<div class="ui left labeled input"> <div class="ui left labeled input">
<div class="ui basic label"> <div class="ui basic label">
@ -238,7 +238,7 @@ defineExpose({
<template v-if="creating"> <template v-if="creating">
<div class="ui small hidden divider" /> <div class="ui small hidden divider" />
<p> <p>
{{ $t('components.audio.ChannelForm.help.username') }} {{ t('components.audio.ChannelForm.help.username') }}
</p> </p>
</template> </template>
</div> </div>
@ -248,7 +248,7 @@ defineExpose({
:image-class="newValues.content_category === 'podcast' ? '' : 'circular'" :image-class="newValues.content_category === 'podcast' ? '' : 'circular'"
@delete="newValues.cover = null" @delete="newValues.cover = null"
> >
{{ $t('components.audio.ChannelForm.label.image') }} {{ t('components.audio.ChannelForm.label.image') }}
</attachment-input> </attachment-input>
</div> </div>
<div class="ui small hidden divider" /> <div class="ui small hidden divider" />
@ -256,7 +256,7 @@ defineExpose({
<div class="ten wide column"> <div class="ten wide column">
<div class="ui field"> <div class="ui field">
<label for="channel-tags"> <label for="channel-tags">
{{ $t('components.audio.ChannelForm.label.tags') }} {{ t('components.audio.ChannelForm.label.tags') }}
</label> </label>
<tags-selector <tags-selector
id="channel-tags" id="channel-tags"
@ -271,7 +271,7 @@ defineExpose({
> >
<div class="ui required field"> <div class="ui required field">
<label for="channel-language"> <label for="channel-language">
{{ $t('components.audio.ChannelForm.label.language') }} {{ t('components.audio.ChannelForm.label.language') }}
</label> </label>
<select <select
id="channel-language" id="channel-language"
@ -294,7 +294,7 @@ defineExpose({
<div class="ui small hidden divider" /> <div class="ui small hidden divider" />
<div class="ui field"> <div class="ui field">
<label for="channel-name"> <label for="channel-name">
{{ $t('components.audio.ChannelForm.label.description') }} {{ t('components.audio.ChannelForm.label.description') }}
</label> </label>
<content-form v-model="newValues.description" /> <content-form v-model="newValues.description" />
</div> </div>
@ -304,7 +304,7 @@ defineExpose({
> >
<div class="ui required field"> <div class="ui required field">
<label for="channel-itunes-category"> <label for="channel-itunes-category">
{{ $t('components.audio.ChannelForm.label.category') }} {{ t('components.audio.ChannelForm.label.category') }}
</label> </label>
<select <select
id="itunes-category" id="itunes-category"
@ -324,7 +324,7 @@ defineExpose({
</div> </div>
<div class="ui field"> <div class="ui field">
<label for="channel-itunes-category"> <label for="channel-itunes-category">
{{ $t('components.audio.ChannelForm.label.subcategory') }} {{ t('components.audio.ChannelForm.label.subcategory') }}
</label> </label>
<select <select
id="itunes-category" id="itunes-category"
@ -349,7 +349,7 @@ defineExpose({
> >
<div class="ui field"> <div class="ui field">
<label for="channel-itunes-email"> <label for="channel-itunes-email">
{{ $t('components.audio.ChannelForm.label.email') }} {{ t('components.audio.ChannelForm.label.email') }}
</label> </label>
<input <input
id="channel-itunes-email" id="channel-itunes-email"
@ -360,7 +360,7 @@ defineExpose({
</div> </div>
<div class="ui field"> <div class="ui field">
<label for="channel-itunes-name"> <label for="channel-itunes-name">
{{ $t('components.audio.ChannelForm.label.owner') }} {{ t('components.audio.ChannelForm.label.owner') }}
</label> </label>
<input <input
id="channel-itunes-name" id="channel-itunes-name"
@ -371,7 +371,7 @@ defineExpose({
</div> </div>
</div> </div>
<p> <p>
{{ $t('components.audio.ChannelForm.help.podcastFields') }} {{ t('components.audio.ChannelForm.help.podcastFields') }}
</p> </p>
</template> </template>
</template> </template>
@ -380,7 +380,7 @@ defineExpose({
class="ui active inverted dimmer" class="ui active inverted dimmer"
> >
<div class="ui text loader"> <div class="ui text loader">
{{ $t('components.audio.ChannelForm.loader.loading') }} {{ t('components.audio.ChannelForm.loader.loading') }}
</div> </div>
</div> </div>
</form> </form>

View File

@ -3,6 +3,13 @@ import type { Album } from '~/types'
import PlayButton from '~/components/audio/PlayButton.vue' import PlayButton from '~/components/audio/PlayButton.vue'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import { useRouter } from 'vue-router'
const { t } = useI18n()
const store = useStore()
const router = useRouter()
interface Props { interface Props {
serie: Album serie: Album
@ -18,31 +25,31 @@ const cover = computed(() => props.serie?.cover ?? null)
<div class="two-images"> <div class="two-images">
<img <img
v-if="cover && cover.urls.original" v-if="cover && cover.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"
alt="" alt=""
class="channel-image" class="channel-image"
@click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" @click="router.push({name: 'library.albums.detail', params: {id: serie.id}})"
> >
<img <img
v-else v-else
alt="" alt=""
class="channel-image" class="channel-image"
src="../../assets/audio/default-cover.png" src="../../assets/audio/default-cover.png"
@click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" @click="router.push({name: 'library.albums.detail', params: {id: serie.id}})"
> >
<img <img
v-if="cover && cover.urls.original" v-if="cover && cover.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"
alt="" alt=""
class="channel-image" class="channel-image"
@click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" @click="router.push({name: 'library.albums.detail', params: {id: serie.id}})"
> >
<img <img
v-else v-else
alt="" alt=""
class="channel-image" class="channel-image"
src="../../assets/audio/default-cover.png" src="../../assets/audio/default-cover.png"
@click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" @click="router.push({name: 'library.albums.detail', params: {id: serie.id}})"
> >
</div> </div>
<div class="content ellipsis"> <div class="content ellipsis">
@ -56,7 +63,7 @@ const cover = computed(() => props.serie?.cover ?? null)
</strong> </strong>
<div class="description"> <div class="description">
<span> <span>
{{ $t('components.audio.ChannelSerieCard.meta.episodes', serie.tracks_count) }} {{ t('components.audio.ChannelSerieCard.meta.episodes', serie.tracks_count) }}
</span> </span>
</div> </div>
</div> </div>

View File

@ -3,6 +3,7 @@ import type { BackendError, Album } from '~/types'
import { clone } from 'lodash-es' import { clone } from 'lodash-es'
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
import ChannelSerieCard from '~/components/audio/ChannelSerieCard.vue' import ChannelSerieCard from '~/components/audio/ChannelSerieCard.vue'
@ -14,6 +15,8 @@ interface Props {
limit?: number limit?: number
} }
const { t } = useI18n()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
isPodcast: true, isPodcast: true,
limit: 5 limit: 5
@ -84,7 +87,7 @@ fetchData()
:class="['ui', 'basic', 'button']" :class="['ui', 'basic', 'button']"
@click="fetchData(nextPage)" @click="fetchData(nextPage)"
> >
{{ $t('components.audio.ChannelSeries.button.showMore') }} {{ t('components.audio.ChannelSeries.button.showMore') }}
</button> </button>
</template> </template>
<template v-if="!isLoading && albums.length === 0"> <template v-if="!isLoading && albums.length === 0">
@ -93,7 +96,7 @@ fetchData()
@refresh="fetchData()" @refresh="fetchData()"
> >
<p> <p>
{{ $t('components.audio.ChannelSeries.help.subscribe') }} {{ t('components.audio.ChannelSeries.help.subscribe') }}
</p> </p>
</empty-state> </empty-state>
</template> </template>

View File

@ -3,6 +3,7 @@ import type { BackendError, BackendResponse, Channel } from '~/types'
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import { clone } from 'lodash-es' import { clone } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
@ -17,6 +18,8 @@ interface Props {
limit?: number limit?: number
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
limit: 5 limit: 5
@ -77,7 +80,7 @@ fetchData()
:class="['ui', 'basic', 'button']" :class="['ui', 'basic', 'button']"
@click="fetchData(nextPage)" @click="fetchData(nextPage)"
> >
{{ $t('components.audio.ChannelsWidget.button.showMore') }} {{ t('components.audio.ChannelsWidget.button.showMore') }}
</button> </button>
</template> </template>
<template v-if="!isLoading && channels.length === 0"> <template v-if="!isLoading && channels.length === 0">

View File

@ -3,12 +3,15 @@ import { get } from 'lodash-es'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
interface Props { interface Props {
type: string type: string
id: number id: number
} }
const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const width = ref(null) const width = ref(null)
const height = ref(150) const height = ref(150)
@ -51,20 +54,20 @@ const { copy, copied } = useClipboard({ source: embedCode })
> >
<p> <p>
<strong> <strong>
{{ $t('components.audio.EmbedWizard.warning.anonymous') }} {{ t('components.audio.EmbedWizard.warning.anonymous') }}
</strong> </strong>
</p> </p>
<p> <p>
{{ $t('components.audio.EmbedWizard.help.anonymous') }} {{ t('components.audio.EmbedWizard.help.anonymous') }}
</p> </p>
</div> </div>
<div class="ui form"> <div class="ui form">
<div class="two fields"> <div class="two fields">
<div class="field"> <div class="field">
<div class="field"> <div class="field">
<label for="embed-width">{{ $t('components.audio.EmbedWizard.label.width') }}</label> <label for="embed-width">{{ t('components.audio.EmbedWizard.label.width') }}</label>
<p> <p>
{{ $t('components.audio.EmbedWizard.help.width') }} {{ t('components.audio.EmbedWizard.help.width') }}
</p> </p>
<input <input
id="embed-width" id="embed-width"
@ -77,7 +80,7 @@ const { copy, copied } = useClipboard({ source: embedCode })
<template v-if="type != 'track'"> <template v-if="type != 'track'">
<br> <br>
<div class="field"> <div class="field">
<label for="embed-height">{{ $t('components.audio.EmbedWizard.label.height') }}</label> <label for="embed-height">{{ t('components.audio.EmbedWizard.label.height') }}</label>
<input <input
id="embed-height" id="embed-height"
v-model="height" v-model="height"
@ -95,11 +98,11 @@ const { copy, copied } = useClipboard({ source: embedCode })
@click="copy()" @click="copy()"
> >
<i class="copy icon" /> <i class="copy icon" />
{{ $t('components.audio.EmbedWizard.button.copy') }} {{ t('components.audio.EmbedWizard.button.copy') }}
</button> </button>
<label for="embed-width">{{ $t('components.audio.EmbedWizard.label.embed') }}</label> <label for="embed-width">{{ t('components.audio.EmbedWizard.label.embed') }}</label>
<p> <p>
{{ $t('components.audio.EmbedWizard.help.embed') }} {{ t('components.audio.EmbedWizard.help.embed') }}
</p> </p>
<textarea <textarea
v-model="embedCode" v-model="embedCode"
@ -111,7 +114,7 @@ const { copy, copied } = useClipboard({ source: embedCode })
v-if="copied" v-if="copied"
class="message" class="message"
> >
{{ $t('components.audio.EmbedWizard.message.copy') }} {{ t('components.audio.EmbedWizard.message.copy') }}
</p> </p>
</div> </div>
</div> </div>
@ -123,7 +126,7 @@ const { copy, copied } = useClipboard({ source: embedCode })
:href="iframeSrc" :href="iframeSrc"
target="_blank" target="_blank"
> >
{{ $t('components.audio.EmbedWizard.header.preview') }} {{ t('components.audio.EmbedWizard.header.preview') }}
</a> </a>
</h3> </h3>
<iframe <iframe

View File

@ -3,6 +3,7 @@ import type { Library } from '~/types'
import { computed } from 'vue' import { computed } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
interface Events { interface Events {
(e: 'unfollowed'): void (e: 'unfollowed'): void
@ -13,6 +14,7 @@ interface Props {
library: Library library: Library
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = defineProps<Props>() const props = defineProps<Props>()
@ -39,13 +41,13 @@ const toggle = () => {
> >
<i class="heart icon" /> <i class="heart icon" />
<span v-if="isApproved"> <span v-if="isApproved">
{{ $t('components.audio.LibraryFollowButton.button.unfollow') }} {{ t('components.audio.LibraryFollowButton.button.unfollow') }}
</span> </span>
<span v-else-if="isPending"> <span v-else-if="isPending">
{{ $t('components.audio.LibraryFollowButton.button.cancel') }} {{ t('components.audio.LibraryFollowButton.button.cancel') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.audio.LibraryFollowButton.button.follow') }} {{ t('components.audio.LibraryFollowButton.button.follow') }}
</span> </span>
</button> </button>
</template> </template>

View File

@ -8,6 +8,9 @@ import usePlayOptions from '~/composables/audio/usePlayOptions'
import useReport from '~/composables/moderation/useReport' import useReport from '~/composables/moderation/useReport'
import { useCurrentElement } from '@vueuse/core' import { useCurrentElement } from '@vueuse/core'
import { setupDropdown } from '~/utils/fomantic' import { setupDropdown } from '~/utils/fomantic'
import { useStore } from '~/store'
import { useRouter, useRoute } from 'vue-router'
interface Props extends PlayOptionsProps { interface Props extends PlayOptionsProps {
dropdownIconClasses?: string[] dropdownIconClasses?: string[]
@ -64,6 +67,10 @@ const {
const { report, getReportableObjects } = useReport() const { report, getReportableObjects } = useReport()
const { t } = useI18n() const { t } = useI18n()
const store = useStore()
const router = useRouter()
const route = useRoute()
const labels = computed(() => ({ const labels = computed(() => ({
playNow: t('components.audio.PlayButton.button.playNow'), playNow: t('components.audio.PlayButton.button.playNow'),
addToQueue: t('components.audio.PlayButton.button.addToQueue'), addToQueue: t('components.audio.PlayButton.button.addToQueue'),
@ -139,7 +146,7 @@ const openMenu = () => {
v-else v-else
:class="[playIconClass, 'icon']" :class="[playIconClass, 'icon']"
/> />
<template v-if="!discrete && !iconOnly">&nbsp;<slot>{{ $t('components.audio.PlayButton.button.discretePlay') }}</slot></template> <template v-if="!discrete && !iconOnly">&nbsp;<slot>{{ t('components.audio.PlayButton.button.discretePlay') }}</slot></template>
</button> </button>
<button <button
v-if="!discrete && !iconOnly" v-if="!discrete && !iconOnly"
@ -180,7 +187,7 @@ const openMenu = () => {
class="item basic" class="item basic"
:disabled="!playable" :disabled="!playable"
:title="labels.startRadio" :title="labels.startRadio"
@click.stop.prevent="$store.dispatch('radios/start', {type: 'similar', objectId: track?.id})" @click.stop.prevent="store.dispatch('radios/start', {type: 'similar', objectId: track?.id})"
> >
<i class="feed icon" />{{ labels.startRadio }} <i class="feed icon" />{{ labels.startRadio }}
</button> </button>
@ -188,22 +195,22 @@ const openMenu = () => {
v-if="track" v-if="track"
class="item basic" class="item basic"
:disabled="!playable" :disabled="!playable"
@click.stop="$store.commit('playlists/chooseTrack', track)" @click.stop="store.commit('playlists/chooseTrack', track)"
> >
<i class="list icon" /> <i class="list icon" />
{{ labels.addToPlaylist }} {{ labels.addToPlaylist }}
</button> </button>
<button <button
v-if="track && $route.name !== 'library.tracks.detail'" v-if="track && route.name !== 'library.tracks.detail'"
class="item basic" class="item basic"
@click.stop.prevent="$router.push(`/library/tracks/${track?.id}/`)" @click.stop.prevent="router.push(`/library/tracks/${track?.id}/`)"
> >
<i class="info icon" /> <i class="info icon" />
<span v-if="track.artist_credit?.some(ac => ac.artist.content_category === 'podcast')"> <span v-if="track.artist_credit?.some(ac => ac.artist.content_category === 'podcast')">
{{ $t('components.audio.PlayButton.button.episodeDetails') }} {{ t('components.audio.PlayButton.button.episodeDetails') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.audio.PlayButton.button.trackDetails') }} {{ t('components.audio.PlayButton.button.trackDetails') }}
</span> </span>
</button> </button>
<div class="divider" /> <div class="divider" />

View File

@ -6,6 +6,7 @@ import { useMouse, useWindowSize } from '@vueuse/core'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import onKeyboardShortcut from '~/composables/onKeyboardShortcut' import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
import time from '~/utils/time' import time from '~/utils/time'
@ -43,6 +44,7 @@ const {
} = useQueue() } = useQueue()
const store = useStore() const store = useStore()
const router = useRouter()
const { t } = useI18n() const { t } = useI18n()
const toggleMobilePlayer = () => { const toggleMobilePlayer = () => {
@ -156,12 +158,12 @@ const hideArtist = () => {
<div class="controls track-controls queue-not-focused desktop-and-up"> <div class="controls track-controls queue-not-focused desktop-and-up">
<div <div
class="ui tiny image" class="ui tiny image"
@click.stop.prevent="$router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})" @click.stop.prevent="router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})"
> >
<img <img
ref="cover" ref="cover"
alt="" alt=""
:src="$store.getters['instance/absoluteUrl'](currentTrack.coverUrl)" :src="store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
> >
</div> </div>
<div <div
@ -188,7 +190,7 @@ const hideArtist = () => {
:to="{name: 'library.artists.detail', params: {id: ac.artist.id }}" :to="{name: 'library.artists.detail', params: {id: ac.artist.id }}"
@click.stop.prevent="" @click.stop.prevent=""
> >
{{ ac.credit ?? $t('components.audio.Player.meta.unknownArtist') }} {{ ac.credit ?? t('components.audio.Player.meta.unknownArtist') }}
</router-link> </router-link>
<span>{{ ac.joinphrase }}</span> <span>{{ ac.joinphrase }}</span>
</template> </template>
@ -200,7 +202,7 @@ const hideArtist = () => {
:to="{name: 'library.albums.detail', params: {id: currentTrack.albumId }}" :to="{name: 'library.albums.detail', params: {id: currentTrack.albumId }}"
@click.stop.prevent="" @click.stop.prevent=""
> >
{{ currentTrack.albumTitle ?? $t('components.audio.Player.meta.unknownAlbum') }} {{ currentTrack.albumTitle ?? t('components.audio.Player.meta.unknownAlbum') }}
</router-link> </router-link>
</template> </template>
</div> </div>
@ -211,7 +213,7 @@ const hideArtist = () => {
<img <img
ref="cover" ref="cover"
alt="" alt=""
:src="$store.getters['instance/absoluteUrl'](currentTrack.coverUrl)" :src="store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
> >
</div> </div>
<div class="middle aligned content ellipsis"> <div class="middle aligned content ellipsis">
@ -223,18 +225,18 @@ const hideArtist = () => {
v-for="ac in currentTrack.artistCredit" v-for="ac in currentTrack.artistCredit"
:key="ac.artist.id" :key="ac.artist.id"
> >
{{ ac.credit ?? $t('components.audio.Player.meta.unknownArtist') }} {{ ac.credit ?? t('components.audio.Player.meta.unknownArtist') }}
<span>{{ ac.joinphrase }}</span> <span>{{ ac.joinphrase }}</span>
</div> </div>
<template v-if="currentTrack.albumId !== -1"> <template v-if="currentTrack.albumId !== -1">
<span class="middle slash symbol" /> <span class="middle slash symbol" />
{{ currentTrack.albumTitle ?? $t('components.audio.Player.meta.unknownAlbum') }} {{ currentTrack.albumTitle ?? t('components.audio.Player.meta.unknownAlbum') }}
</template> </template>
</div> </div>
</div> </div>
</div> </div>
<div <div
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
class="controls desktop-and-up fluid align-right" class="controls desktop-and-up fluid align-right"
> >
<track-favorite-icon <track-favorite-icon
@ -340,7 +342,7 @@ const hideArtist = () => {
</button> </button>
<button <button
v-if="$store.state.ui.queueFocused" v-if="store.state.ui.queueFocused"
class="circular control button close-control desktop-and-up" class="circular control button close-control desktop-and-up"
@click.stop="toggleMobilePlayer" @click.stop="toggleMobilePlayer"
> >
@ -354,14 +356,14 @@ const hideArtist = () => {
<i class="large up angle icon" /> <i class="large up angle icon" />
</button> </button>
<button <button
v-if="$store.state.ui.queueFocused === 'player'" v-if="store.state.ui.queueFocused === 'player'"
class="circular control button close-control desktop-and-below" class="circular control button close-control desktop-and-below"
@click.stop="switchTab" @click.stop="switchTab"
> >
<i class="large up angle icon" /> <i class="large up angle icon" />
</button> </button>
<button <button
v-if="$store.state.ui.queueFocused === 'queue'" v-if="store.state.ui.queueFocused === 'queue'"
class="circular control button desktop-and-below" class="circular control button desktop-and-below"
@click.stop="switchTab" @click.stop="switchTab"
> >
@ -370,7 +372,7 @@ const hideArtist = () => {
</div> </div>
<button <button
class="circular control button close-control desktop-and-below" class="circular control button close-control desktop-and-below"
@click.stop="$store.commit('ui/queueFocused', null)" @click.stop="store.commit('ui/queueFocused', null)"
> >
<i class="x icon" /> <i class="x icon" />
</button> </button>

View File

@ -74,7 +74,7 @@ const labels = computed(() => ({
<div> <div>
/front/src/components/audio/Search.vue /front/src/components/audio/Search.vue
<h2> <h2>
{{ $t('components.audio.Search.header.search') }} {{ t('components.audio.Search.header.search') }}
</h2> </h2>
<div :class="['ui', {'loading': isLoading }, 'search']"> <div :class="['ui', {'loading': isLoading }, 'search']">
<div class="ui icon big input"> <div class="ui icon big input">
@ -90,7 +90,7 @@ const labels = computed(() => ({
</div> </div>
<template v-if="query.length > 0"> <template v-if="query.length > 0">
<h3 class="ui title"> <h3 class="ui title">
{{ $t('components.audio.Search.header.artists') }} {{ t('components.audio.Search.header.artists') }}
</h3> </h3>
<div v-if="results.artists.length > 0"> <div v-if="results.artists.length > 0">
<div class="ui cards"> <div class="ui cards">
@ -102,12 +102,12 @@ const labels = computed(() => ({
</div> </div>
</div> </div>
<p v-else> <p v-else>
{{ $t('components.audio.Search.empty.noArtists') }} {{ t('components.audio.Search.empty.noArtists') }}
</p> </p>
</template> </template>
<template v-if="query.length > 0"> <template v-if="query.length > 0">
<h3 class="ui title"> <h3 class="ui title">
{{ $t('components.audio.Search.header.albums') }} {{ t('components.audio.Search.header.albums') }}
</h3> </h3>
<div <div
v-if="results.albums.length > 0" v-if="results.albums.length > 0"
@ -125,7 +125,7 @@ const labels = computed(() => ({
</div> </div>
</div> </div>
<p v-else> <p v-else>
{{ $t('components.audio.Search.empty.noAlbums') }} {{ t('components.audio.Search.empty.noAlbums') }}
</p> </p>
</template> </template>
</div> </div>

View File

@ -6,11 +6,14 @@ import TagsList from '~/components/tags/List.vue'
import { computed } from 'vue' import { computed } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { truncate } from '~/utils/filters' import { truncate } from '~/utils/filters'
import { useI18n } from 'vue-i18n'
interface Props { interface Props {
artist: Artist artist: Artist
} }
const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const cover = computed(() => !props.artist.cover?.urls.original const cover = computed(() => !props.artist.cover?.urls.original
@ -63,10 +66,10 @@ const imageUrl = computed(() => cover.value?.urls.original
</div> </div>
<div class="extra content"> <div class="extra content">
<span v-if="artist.content_category === 'music'"> <span v-if="artist.content_category === 'music'">
{{ $t('components.audio.artist.Card.meta.tracks', artist.tracks_count) }} {{ t('components.audio.artist.Card.meta.tracks', artist.tracks_count) }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.audio.artist.Card.meta.episodes', artist.tracks_count) }} {{ t('components.audio.artist.Card.meta.episodes', artist.tracks_count) }}
</span> </span>
<play-button <play-button
class="right floated basic icon" class="right floated basic icon"

View File

@ -7,6 +7,8 @@ import { useI18n } from 'vue-i18n'
import { usePlayer } from '~/composables/audio/player' import { usePlayer } from '~/composables/audio/player'
import { useQueue } from '~/composables/audio/queue' import { useQueue } from '~/composables/audio/queue'
import { useStore } from '~/store'
import usePlayOptions from '~/composables/audio/usePlayOptions' import usePlayOptions from '~/composables/audio/usePlayOptions'
@ -54,6 +56,8 @@ const { isPlaying } = usePlayer()
const { activateTrack } = usePlayOptions(props) const { activateTrack } = usePlayOptions(props)
const { t } = useI18n() const { t } = useI18n()
const store = useStore()
const actionsButtonLabel = computed(() => t('components.audio.podcast.MobileRow.button.actions')) const actionsButtonLabel = computed(() => t('components.audio.podcast.MobileRow.button.actions'))
</script> </script>
@ -71,13 +75,13 @@ const actionsButtonLabel = computed(() => t('components.audio.podcast.MobileRow.
> >
<img <img
v-if="track.album?.cover?.urls.original" v-if="track.album?.cover?.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)"
alt="" alt=""
class="ui artist-track mini image" class="ui artist-track mini image"
> >
<img <img
v-else-if="track.cover" v-else-if="track.cover"
v-lazy="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)"
alt="" alt=""
class="ui artist-track mini image" class="ui artist-track mini image"
> >
@ -136,7 +140,7 @@ const actionsButtonLabel = computed(() => t('components.audio.podcast.MobileRow.
</p> </p>
</div> </div>
<div <div
v-if="$store.state.auth.authenticated && track.artist_credit?.[0].artist.content_category !== 'podcast'" v-if="store.state.auth.authenticated && track.artist_credit?.[0].artist.content_category !== 'podcast'"
:class="[ :class="[
'meta', 'meta',
'right', 'right',

View File

@ -4,6 +4,7 @@ import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
// import type { Track } from '~/types' // import type { Track } from '~/types'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import SemanticModal from '~/components/semantic/Modal.vue' import SemanticModal from '~/components/semantic/Modal.vue'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
@ -55,11 +56,13 @@ const show = useVModel(props, 'show', emit)
const { report, getReportableObjects } = useReport() const { report, getReportableObjects } = useReport()
const { enqueue, enqueueNext } = usePlayOptions(props) const { enqueue, enqueueNext } = usePlayOptions(props)
const store = useStore() const store = useStore()
const router = useRouter()
const { t } = useI18n()
const isFavorite = computed(() => store.getters['favorites/isFavorite'](props.track.id)) const isFavorite = computed(() => store.getters['favorites/isFavorite'](props.track.id))
const { t } = useI18n()
const favoriteButton = computed(() => isFavorite.value const favoriteButton = computed(() => isFavorite.value
? t('components.audio.podcast.Modal.button.removeFromFavorites') ? t('components.audio.podcast.Modal.button.removeFromFavorites')
: t('components.audio.podcast.Modal.button.addToFavorites') : t('components.audio.podcast.Modal.button.addToFavorites')
@ -101,7 +104,7 @@ const labels = computed(() => ({
<img <img
v-if="track.album && track.album.cover && track.album.cover.urls.original" v-if="track.album && track.album.cover && track.album.cover.urls.original"
v-lazy=" v-lazy="
$store.getters['instance/absoluteUrl']( store.getters['instance/absoluteUrl'](
track.album.cover.urls.medium_square_crop track.album.cover.urls.medium_square_crop
) )
" "
@ -111,7 +114,7 @@ const labels = computed(() => ({
<img <img
v-else-if="track.cover" v-else-if="track.cover"
v-lazy=" v-lazy="
$store.getters['instance/absoluteUrl']( store.getters['instance/absoluteUrl'](
track.cover.urls.medium_square_crop track.cover.urls.medium_square_crop
) )
" "
@ -144,7 +147,7 @@ const labels = computed(() => ({
<div class="content"> <div class="content">
<div class="ui one column unstackable grid"> <div class="ui one column unstackable grid">
<div <div
v-if="$store.state.auth.authenticated && track.artist_credit?.[0].artist?.content_category !== 'podcast'" v-if="store.state.auth.authenticated && track.artist_credit?.[0].artist?.content_category !== 'podcast'"
class="row" class="row"
> >
<div <div
@ -152,7 +155,7 @@ const labels = computed(() => ({
class="column" class="column"
role="button" role="button"
:aria-label="favoriteButton" :aria-label="favoriteButton"
@click.stop="$store.dispatch('favorites/toggle', track.id)" @click.stop="store.dispatch('favorites/toggle', track.id)"
> >
<i <i
:class="[ :class="[
@ -202,7 +205,7 @@ const labels = computed(() => ({
role="button" role="button"
:aria-label="labels.startRadio" :aria-label="labels.startRadio"
@click.stop.prevent=" @click.stop.prevent="
$store.dispatch('radios/start', { store.dispatch('radios/start', {
type: 'similar', type: 'similar',
objectId: track.id, objectId: track.id,
}); });
@ -218,7 +221,7 @@ const labels = computed(() => ({
class="column" class="column"
role="button" role="button"
:aria-label="labels.addToPlaylist" :aria-label="labels.addToPlaylist"
@click.stop="$store.commit('playlists/chooseTrack', track)" @click.stop="store.commit('playlists/chooseTrack', track)"
> >
<i class="list icon track-modal list-icon" /> <i class="list icon track-modal list-icon" />
<span class="track-modal list-item">{{ <span class="track-modal list-item">{{
@ -236,7 +239,7 @@ const labels = computed(() => ({
role="button" role="button"
:aria-label="albumDetailsButton" :aria-label="albumDetailsButton"
@click.prevent.exact=" @click.prevent.exact="
$router.push({ router.push({
name: 'library.albums.detail', name: 'library.albums.detail',
params: { id: track.album?.id }, params: { id: track.album?.id },
}) })
@ -258,7 +261,7 @@ const labels = computed(() => ({
class="column" class="column"
role="button" role="button"
:aria-label="artistDetailsButton" :aria-label="artistDetailsButton"
@click.prevent.exact="$router.push({ name: 'library.artists.detail', params: { id: ac.artist.id } })" @click.prevent.exact="router.push({ name: 'library.artists.detail', params: { id: ac.artist.id } })"
> >
<i class="user icon track-modal list-icon" /> <i class="user icon track-modal list-icon" />
<span class="track-modal list-item">{{ ac.artist.name }}</span> <span class="track-modal list-item">{{ ac.artist.name }}</span>
@ -271,7 +274,7 @@ const labels = computed(() => ({
role="button" role="button"
:aria-label="trackDetailsButton" :aria-label="trackDetailsButton"
@click.prevent.exact=" @click.prevent.exact="
$router.push({ router.push({
name: 'library.tracks.detail', name: 'library.tracks.detail',
params: { id: track.id }, params: { id: track.id },
}) })

View File

@ -5,6 +5,7 @@ import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
import { ref } from 'vue' import { ref } from 'vue'
import { useQueue } from '~/composables/audio/queue' import { useQueue } from '~/composables/audio/queue'
import { useStore } from '~/store'
import axios from 'axios' import axios from 'axios'
@ -48,6 +49,8 @@ const props = withDefaults(defineProps<Props>(), {
account: null account: null
}) })
const store = useStore()
const description = ref('') const description = ref('')
const renderedDescription = useMarkdown(description) const renderedDescription = useMarkdown(description)
@ -83,13 +86,13 @@ await fetchData()
> >
<img <img
v-if="track.cover?.urls.original" v-if="track.cover?.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)"
alt="" alt=""
class="ui artist-track mini image" class="ui artist-track mini image"
> >
<img <img
v-else-if="defaultCover" v-else-if="defaultCover"
v-lazy="$store.getters['instance/absoluteUrl'](defaultCover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](defaultCover.urls.medium_square_crop)"
alt="" alt=""
class="ui artist-track mini image" class="ui artist-track mini image"
> >

View File

@ -7,6 +7,8 @@ import { useI18n } from 'vue-i18n'
import { usePlayer } from '~/composables/audio/player' import { usePlayer } from '~/composables/audio/player'
import { useQueue } from '~/composables/audio/queue' import { useQueue } from '~/composables/audio/queue'
import { useStore } from '~/store'
import usePlayOptions from '~/composables/audio/usePlayOptions' import usePlayOptions from '~/composables/audio/usePlayOptions'
@ -54,6 +56,8 @@ const { isPlaying } = usePlayer()
const { activateTrack } = usePlayOptions(props) const { activateTrack } = usePlayOptions(props)
const { t } = useI18n() const { t } = useI18n()
const store = useStore()
const actionsButtonLabel = computed(() => t('components.audio.track.MobileRow.button.actions')) const actionsButtonLabel = computed(() => t('components.audio.track.MobileRow.button.actions'))
</script> </script>
@ -71,13 +75,13 @@ const actionsButtonLabel = computed(() => t('components.audio.track.MobileRow.bu
> >
<img <img
v-if="track.album?.cover?.urls.original" v-if="track.album?.cover?.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)"
alt="" alt=""
class="ui artist-track mini image" class="ui artist-track mini image"
> >
<img <img
v-else-if="track.cover" v-else-if="track.cover"
v-lazy="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)"
alt="" alt=""
class="ui artist-track mini image" class="ui artist-track mini image"
> >
@ -119,7 +123,7 @@ const actionsButtonLabel = computed(() => t('components.audio.track.MobileRow.bu
</p> </p>
</div> </div>
<div <div
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
:class="[ :class="[
'meta', 'meta',
'right', 'right',

View File

@ -4,6 +4,7 @@ import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
// import type { Track } from '~/types' // import type { Track } from '~/types'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import SemanticModal from '~/components/semantic/Modal.vue' import SemanticModal from '~/components/semantic/Modal.vue'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
@ -59,6 +60,8 @@ const store = useStore()
const isFavorite = computed(() => store.getters['favorites/isFavorite'](props.track.id)) const isFavorite = computed(() => store.getters['favorites/isFavorite'](props.track.id))
const { t } = useI18n() const { t } = useI18n()
const router = useRouter()
const favoriteButton = computed(() => isFavorite.value const favoriteButton = computed(() => isFavorite.value
? t('components.audio.track.Modal.button.removeFromFavorites') ? t('components.audio.track.Modal.button.removeFromFavorites')
: t('components.audio.track.Modal.button.addToFavorites') : t('components.audio.track.Modal.button.addToFavorites')
@ -99,13 +102,13 @@ const labels = computed(() => ({
<div class="ui large centered rounded image"> <div class="ui large centered rounded image">
<img <img
v-if="track.album?.cover?.urls.original" v-if="track.album?.cover?.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)"
alt="" alt=""
class="ui centered image" class="ui centered image"
> >
<img <img
v-else-if="track.cover" v-else-if="track.cover"
v-lazy="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)"
alt="" alt=""
class="ui centered image" class="ui centered image"
> >
@ -133,7 +136,7 @@ const labels = computed(() => ({
<div class="content"> <div class="content">
<div class="ui one column unstackable grid"> <div class="ui one column unstackable grid">
<div <div
v-if="$store.state.auth.authenticated && track.artist_credit?.[0].artist.content_category !== 'podcast'" v-if="store.state.auth.authenticated && track.artist_credit?.[0].artist.content_category !== 'podcast'"
class="row" class="row"
> >
<div <div
@ -141,7 +144,7 @@ const labels = computed(() => ({
class="column" class="column"
role="button" role="button"
:aria-label="favoriteButton" :aria-label="favoriteButton"
@click.stop="$store.dispatch('favorites/toggle', track.id)" @click.stop="store.dispatch('favorites/toggle', track.id)"
> >
<i :class="[ 'heart', 'favorite-icon', { favorited: isFavorite, pink: isFavorite }, 'icon', 'track-modal', 'list-icon' ]" /> <i :class="[ 'heart', 'favorite-icon', { favorited: isFavorite, pink: isFavorite }, 'icon', 'track-modal', 'list-icon' ]" />
<span class="track-modal list-item">{{ favoriteButton }}</span> <span class="track-modal list-item">{{ favoriteButton }}</span>
@ -174,7 +177,7 @@ const labels = computed(() => ({
class="column" class="column"
role="button" role="button"
:aria-label="labels.startRadio" :aria-label="labels.startRadio"
@click.stop.prevent="() => { $store.dispatch('radios/start', { type: 'similar', objectId: track.id }); modal.closeModal() }" @click.stop.prevent="() => { store.dispatch('radios/start', { type: 'similar', objectId: track.id }); modal.closeModal() }"
> >
<i class="rss icon track-modal list-icon" /> <i class="rss icon track-modal list-icon" />
<span class="track-modal list-item">{{ labels.startRadio }}</span> <span class="track-modal list-item">{{ labels.startRadio }}</span>
@ -185,7 +188,7 @@ const labels = computed(() => ({
class="column" class="column"
role="button" role="button"
:aria-label="labels.addToPlaylist" :aria-label="labels.addToPlaylist"
@click.stop="$store.commit('playlists/chooseTrack', track)" @click.stop="store.commit('playlists/chooseTrack', track)"
> >
<i class="list icon track-modal list-icon" /> <i class="list icon track-modal list-icon" />
<span class="track-modal list-item"> <span class="track-modal list-item">
@ -202,7 +205,7 @@ const labels = computed(() => ({
class="column" class="column"
role="button" role="button"
:aria-label="albumDetailsButton" :aria-label="albumDetailsButton"
@click.prevent.exact="$router.push({ name: 'library.albums.detail', params: { id: track.album?.id } })" @click.prevent.exact="router.push({ name: 'library.albums.detail', params: { id: track.album?.id } })"
> >
<i class="compact disc icon track-modal list-icon" /> <i class="compact disc icon track-modal list-icon" />
<span class="track-modal list-item">{{ albumDetailsButton }}</span> <span class="track-modal list-item">{{ albumDetailsButton }}</span>
@ -218,7 +221,7 @@ const labels = computed(() => ({
class="column" class="column"
role="button" role="button"
:aria-label="artistDetailsButton" :aria-label="artistDetailsButton"
@click.prevent.exact="$router.push({ name: 'library.artists.detail', params: { id: ac.artist.id } })" @click.prevent.exact="router.push({ name: 'library.artists.detail', params: { id: ac.artist.id } })"
> >
<i class="user icon track-modal list-icon" /> <i class="user icon track-modal list-icon" />
<span class="track-modal list-item">{{ ac.credit }}</span> <span class="track-modal list-item">{{ ac.credit }}</span>
@ -230,7 +233,7 @@ const labels = computed(() => ({
class="column" class="column"
role="button" role="button"
:aria-label="trackDetailsButton" :aria-label="trackDetailsButton"
@click.prevent.exact="$router.push({ name: 'library.tracks.detail', params: { id: track.id } })" @click.prevent.exact="router.push({ name: 'library.tracks.detail', params: { id: track.id } })"
> >
<i class="info icon track-modal list-icon" /> <i class="info icon track-modal list-icon" />
<span class="track-modal list-item">{{ trackDetailsButton }}</span> <span class="track-modal list-item">{{ trackDetailsButton }}</span>

View File

@ -11,6 +11,10 @@ import PlayIndicator from '~/components/audio/track/PlayIndicator.vue'
import PlayButton from '~/components/audio/PlayButton.vue' import PlayButton from '~/components/audio/PlayButton.vue'
import { usePlayer } from '~/composables/audio/player' import { usePlayer } from '~/composables/audio/player'
import { useQueue } from '~/composables/audio/queue' import { useQueue } from '~/composables/audio/queue'
import { useStore } from '~/store'
const store = useStore()
interface Props extends PlayOptionsProps { interface Props extends PlayOptionsProps {
track: Track track: Track
@ -119,19 +123,19 @@ const hover = ref(false)
> >
<img <img
v-if="track.album?.cover?.urls.original" v-if="track.album?.cover?.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)"
alt="" alt=""
class="ui artist-track mini image" class="ui artist-track mini image"
> >
<img <img
v-else-if="track.cover?.urls.original" v-else-if="track.cover?.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)"
alt="" alt=""
class="ui artist-track mini image" class="ui artist-track mini image"
> >
<img <img
v-else-if="track.artist_credit?.length && track.artist_credit[0].artist.cover?.urls.original" v-else-if="track.artist_credit?.length && track.artist_credit[0].artist.cover?.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](track.artist_credit[0].artist.cover.urls.medium_square_crop) " v-lazy="store.getters['instance/absoluteUrl'](track.artist_credit[0].artist.cover.urls.medium_square_crop) "
alt="" alt=""
class="ui artist-track mini image" class="ui artist-track mini image"
> >
@ -183,7 +187,7 @@ const hover = ref(false)
</template> </template>
</div> </div>
<div <div
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
class="meta right floated column" class="meta right floated column"
> >
<track-favorite-icon <track-favorite-icon

View File

@ -4,6 +4,8 @@ import type { Track } from '~/types'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { clone, uniqBy } from 'lodash-es' import { clone, uniqBy } from 'lodash-es'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useStore } from '~/store'
import axios from 'axios' import axios from 'axios'
@ -84,6 +86,8 @@ const allTracks = computed(() => {
}) })
const { t } = useI18n() const { t } = useI18n()
const store = useStore()
const labels = computed(() => ({ const labels = computed(() => ({
title: t('components.audio.track.Table.table.header.title'), title: t('components.audio.track.Table.table.header.title'),
album: t('components.audio.track.Table.table.header.album'), album: t('components.audio.track.Table.table.header.album'),
@ -202,7 +206,7 @@ const updatePage = (page: number) => {
<b>{{ labels.artist }}</b> <b>{{ labels.artist }}</b>
</div> </div>
<div <div
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
class="meta right floated column" class="meta right floated column"
/> />
<div <div

View File

@ -4,6 +4,7 @@ import type { Track, Listening } from '~/types'
import { ref, reactive, watch } from 'vue' import { ref, reactive, watch } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { clone } from 'lodash-es' import { clone } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
@ -39,6 +40,7 @@ const props = withDefaults(defineProps<Props>(), {
}) })
const store = useStore() const store = useStore()
const { t } = useI18n()
const objects = reactive([] as Listening[]) const objects = reactive([] as Listening[])
const count = ref(0) const count = ref(0)
@ -110,12 +112,12 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
<div class="ui tiny image"> <div class="ui tiny image">
<img <img
v-if="object.track.album && object.track.album.cover" v-if="object.track.album && object.track.album.cover"
v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)"
alt="" alt=""
> >
<img <img
v-else-if="object.track.cover" v-else-if="object.track.cover"
v-lazy="$store.getters['instance/absoluteUrl'](object.track.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](object.track.cover.urls.medium_square_crop)"
alt="" alt=""
> >
<img <img
@ -206,7 +208,7 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
> >
<div class="ui icon header"> <div class="ui icon header">
<i class="music icon" /> <i class="music icon" />
{{ $t('components.audio.track.Widget.empty.noResults') }} {{ t('components.audio.track.Widget.empty.noResults') }}
</div> </div>
<div <div
v-if="isLoading" v-if="isLoading"
@ -221,7 +223,7 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
:class="['ui', 'basic', 'button']" :class="['ui', 'basic', 'button']"
@click="fetchData(nextPage as string)" @click="fetchData(nextPage as string)"
> >
{{ $t('components.audio.track.Widget.button.more') }} {{ t('components.audio.track.Widget.button.more') }}
</button> </button>
</template> </template>
</div> </div>

View File

@ -72,17 +72,17 @@ store.state.auth.applicationSecret = undefined
</div> </div>
<template v-else> <template v-else>
<router-link :to="{name: 'settings'}"> <router-link :to="{name: 'settings'}">
{{ $t('components.auth.ApplicationEdit.link.settings') }} {{ t('components.auth.ApplicationEdit.link.settings') }}
</router-link> </router-link>
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.auth.ApplicationEdit.header.appDetails') }} {{ t('components.auth.ApplicationEdit.header.appDetails') }}
</h2> </h2>
<div class="ui form"> <div class="ui form">
<p> <p>
{{ $t('components.auth.ApplicationEdit.help.appDetails') }} {{ t('components.auth.ApplicationEdit.help.appDetails') }}
</p> </p>
<div class="field"> <div class="field">
<label for="copy-id">{{ $t('components.auth.ApplicationEdit.label.appId') }}</label> <label for="copy-id">{{ t('components.auth.ApplicationEdit.label.appId') }}</label>
<copy-input <copy-input
id="copy-id" id="copy-id"
:value="application.client_id" :value="application.client_id"
@ -94,14 +94,14 @@ store.state.auth.applicationSecret = undefined
> >
<div class="ui small warning message"> <div class="ui small warning message">
<h3 class="header"> <h3 class="header">
{{ $t('components.auth.ApplicationEdit.header.appSecretWarning') }} {{ t('components.auth.ApplicationEdit.header.appSecretWarning') }}
</h3> </h3>
<p> <p>
{{ $t('components.auth.ApplicationEdit.message.appSecretWarning') }} {{ t('components.auth.ApplicationEdit.message.appSecretWarning') }}
</p> </p>
</div> </div>
<label for="copy-secret">{{ $t('components.auth.ApplicationEdit.label.appSecret') }}</label> <label for="copy-secret">{{ t('components.auth.ApplicationEdit.label.appSecret') }}</label>
<copy-input <copy-input
id="copy-secret" id="copy-secret"
:value="secret" :value="secret"
@ -111,7 +111,7 @@ store.state.auth.applicationSecret = undefined
v-if="application.token != undefined" v-if="application.token != undefined"
class="field" class="field"
> >
<label for="copy-secret">{{ $t('components.auth.ApplicationEdit.label.accessToken') }}</label> <label for="copy-secret">{{ t('components.auth.ApplicationEdit.label.accessToken') }}</label>
<copy-input <copy-input
id="copy-secret" id="copy-secret"
:value="application.token" :value="application.token"
@ -121,12 +121,12 @@ store.state.auth.applicationSecret = undefined
@click.prevent="refreshToken" @click.prevent="refreshToken"
> >
<i class="refresh icon" /> <i class="refresh icon" />
{{ $t('components.auth.ApplicationEdit.button.regenerateToken') }} {{ t('components.auth.ApplicationEdit.button.regenerateToken') }}
</a> </a>
</div> </div>
</div> </div>
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.auth.ApplicationEdit.header.editApp') }} {{ t('components.auth.ApplicationEdit.header.editApp') }}
</h2> </h2>
<application-form <application-form
:app="application" :app="application"

View File

@ -120,7 +120,7 @@ const allScopes = computed(() => {
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.auth.ApplicationForm.header.failure') }} {{ t('components.auth.ApplicationForm.header.failure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -132,7 +132,7 @@ const allScopes = computed(() => {
</ul> </ul>
</div> </div>
<div class="ui field"> <div class="ui field">
<label for="application-name">{{ $t('components.auth.ApplicationForm.label.name') }}</label> <label for="application-name">{{ t('components.auth.ApplicationForm.label.name') }}</label>
<input <input
id="application-name" id="application-name"
v-model="fields.name" v-model="fields.name"
@ -142,7 +142,7 @@ const allScopes = computed(() => {
> >
</div> </div>
<div class="ui field"> <div class="ui field">
<label for="redirect-uris">{{ $t('components.auth.ApplicationForm.label.redirectUri') }}</label> <label for="redirect-uris">{{ t('components.auth.ApplicationForm.label.redirectUri') }}</label>
<input <input
id="redirect-uris" id="redirect-uris"
v-model="fields.redirect_uris" v-model="fields.redirect_uris"
@ -150,13 +150,13 @@ const allScopes = computed(() => {
type="text" type="text"
> >
<p class="help"> <p class="help">
{{ $t('components.auth.ApplicationForm.help.redirectUri') }} {{ t('components.auth.ApplicationForm.help.redirectUri') }}
</p> </p>
</div> </div>
<div class="ui field"> <div class="ui field">
<label>{{ $t('components.auth.ApplicationForm.label.scopes.label') }}</label> <label>{{ t('components.auth.ApplicationForm.label.scopes.label') }}</label>
<p> <p>
{{ $t('components.auth.ApplicationForm.label.scopes.description') }} {{ t('components.auth.ApplicationForm.label.scopes.description') }}
</p> </p>
<div class="ui stackable two column grid"> <div class="ui stackable two column grid">
<div <div
@ -203,10 +203,10 @@ const allScopes = computed(() => {
type="submit" type="submit"
> >
<span v-if="app !== null"> <span v-if="app !== null">
{{ $t('components.auth.ApplicationForm.button.update') }} {{ t('components.auth.ApplicationForm.button.update') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.auth.ApplicationForm.button.create') }} {{ t('components.auth.ApplicationForm.button.create') }}
</span> </span>
</button> </button>
</form> </form>

View File

@ -58,7 +58,7 @@ const created = (application: Application) => {
<div class="ui vertical stripe segment"> <div class="ui vertical stripe segment">
<section class="ui text container"> <section class="ui text container">
<router-link :to="{name: 'settings'}"> <router-link :to="{name: 'settings'}">
{{ $t('components.auth.ApplicationNew.link.settings') }} {{ t('components.auth.ApplicationNew.link.settings') }}
</router-link> </router-link>
<h2 class="ui header"> <h2 class="ui header">
{{ labels.title }} {{ labels.title }}

View File

@ -119,7 +119,7 @@ whenever(() => props.clientId, fetchApplication, { immediate: true })
<section class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div class="ui small text container"> <div class="ui small text container">
<h2> <h2>
<i class="lock open icon" />{{ $t('components.auth.Authorize.header.authorize') }} <i class="lock open icon" />{{ t('components.auth.Authorize.header.authorize') }}
</h2> </h2>
<div <div
v-if="errors.length > 0" v-if="errors.length > 0"
@ -130,13 +130,13 @@ whenever(() => props.clientId, fetchApplication, { immediate: true })
v-if="application" v-if="application"
class="header" class="header"
> >
{{ $t('components.auth.Authorize.header.authorizeFailure') }} {{ t('components.auth.Authorize.header.authorizeFailure') }}
</h4> </h4>
<h4 <h4
v-else v-else
class="header" class="header"
> >
{{ $t('components.auth.Authorize.header.fetchFailure') }} {{ t('components.auth.Authorize.header.fetchFailure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -159,7 +159,7 @@ whenever(() => props.clientId, fetchApplication, { immediate: true })
@submit.prevent="submit" @submit.prevent="submit"
> >
<h3> <h3>
{{ $t('components.auth.Authorize.header.access', {app_name: application.name}) }} {{ t('components.auth.Authorize.header.access', {app_name: application.name}) }}
</h3> </h3>
<h4 <h4
@ -172,20 +172,20 @@ whenever(() => props.clientId, fetchApplication, { immediate: true })
:class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']" :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']"
> >
<i class="pencil icon" /> <i class="pencil icon" />
{{ $t('components.auth.Authorize.header.writeOnly') }} {{ t('components.auth.Authorize.header.writeOnly') }}
</span> </span>
<span <span
v-else-if="!topic.write && topic.read" v-else-if="!topic.write && topic.read"
:class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']" :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']"
> >
{{ $t('components.auth.Authorize.header.readOnly') }} {{ t('components.auth.Authorize.header.readOnly') }}
</span> </span>
<span <span
v-else-if="topic.write && topic.read" v-else-if="topic.write && topic.read"
:class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']" :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']"
> >
<i class="pencil icon" /> <i class="pencil icon" />
{{ $t('components.auth.Authorize.header.allScopes') }} {{ t('components.auth.Authorize.header.allScopes') }}
</span> </span>
<i :class="[topic.icon, 'icon']" /> <i :class="[topic.icon, 'icon']" />
<div class="content"> <div class="content">
@ -196,7 +196,7 @@ whenever(() => props.clientId, fetchApplication, { immediate: true })
</div> </div>
</h4> </h4>
<div v-if="unknownRequestedScopes.length > 0"> <div v-if="unknownRequestedScopes.length > 0">
<p><strong>{{ $t('components.auth.Authorize.message.unknownPermissions') }}</strong></p> <p><strong>{{ t('components.auth.Authorize.message.unknownPermissions') }}</strong></p>
<ul <ul
v-for="(unknownscope, key) in unknownRequestedScopes" v-for="(unknownscope, key) in unknownRequestedScopes"
:key="key" :key="key"
@ -209,12 +209,12 @@ whenever(() => props.clientId, fetchApplication, { immediate: true })
type="submit" type="submit"
> >
<i class="lock open icon" /> <i class="lock open icon" />
{{ $t('components.auth.Authorize.button.authorize', { app: application.name }) }} {{ t('components.auth.Authorize.button.authorize', { app: application.name }) }}
</button> </button>
<p <p
v-if="redirectUri === 'urn:ietf:wg:oauth:2.0:oob'" v-if="redirectUri === 'urn:ietf:wg:oauth:2.0:oob'"
> >
{{ $t('components.auth.Authorize.help.copyCode') }} {{ t('components.auth.Authorize.help.copyCode') }}
</p> </p>
<p <p
v-else v-else
@ -225,7 +225,7 @@ whenever(() => props.clientId, fetchApplication, { immediate: true })
</p> </p>
</form> </form>
<div v-else-if="code"> <div v-else-if="code">
<p><strong>{{ $t('components.auth.Authorize.help.pasteCode') }}</strong></p> <p><strong>{{ t('components.auth.Authorize.help.pasteCode') }}</strong></p>
<copy-input :value="code" /> <copy-input :value="code" />
</div> </div>
</div> </div>

View File

@ -87,16 +87,16 @@ const submit = async () => {
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.auth.LoginForm.header.loginFailure') }} {{ t('components.auth.LoginForm.header.loginFailure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
v-if="errors[0] == 'invalid_credentials' && $store.state.instance.settings.moderation.signup_approval_enabled.value" v-if="errors[0] == 'invalid_credentials' && $store.state.instance.settings.moderation.signup_approval_enabled.value"
> >
{{ $t('components.auth.LoginForm.help.approvalRequired') }} {{ t('components.auth.LoginForm.help.approvalRequired') }}
</li> </li>
<li v-else-if="errors[0] == 'invalid_credentials'"> <li v-else-if="errors[0] == 'invalid_credentials'">
{{ $t('components.auth.LoginForm.help.invalidCredentials') }} {{ t('components.auth.LoginForm.help.invalidCredentials') }}
</li> </li>
<li v-else> <li v-else>
{{ errors[0] }} {{ errors[0] }}
@ -106,11 +106,11 @@ const submit = async () => {
<template v-if="domain === $store.getters['instance/domain']"> <template v-if="domain === $store.getters['instance/domain']">
<div class="field"> <div class="field">
<label for="username-field"> <label for="username-field">
{{ $t('components.auth.LoginForm.label.username') }} {{ t('components.auth.LoginForm.label.username') }}
<template v-if="showSignup"> <template v-if="showSignup">
<span class="middle pipe symbol" /> <span class="middle pipe symbol" />
<router-link :to="{ path: '/signup' }"> <router-link :to="{ path: '/signup' }">
{{ $t('components.auth.LoginForm.link.createAccount') }} {{ t('components.auth.LoginForm.link.createAccount') }}
</router-link> </router-link>
</template> </template>
</label> </label>
@ -127,13 +127,13 @@ const submit = async () => {
</div> </div>
<div class="field"> <div class="field">
<label for="password-field"> <label for="password-field">
{{ $t('components.auth.LoginForm.label.password') }} {{ t('components.auth.LoginForm.label.password') }}
<span class="middle pipe symbol" /> <span class="middle pipe symbol" />
<router-link <router-link
tabindex="1" tabindex="1"
:to="{ name: 'auth.password-reset', query: { email: credentials.username } }" :to="{ name: 'auth.password-reset', query: { email: credentials.username } }"
> >
{{ $t('components.auth.LoginForm.link.resetPassword') }} {{ t('components.auth.LoginForm.link.resetPassword') }}
</router-link> </router-link>
</label> </label>
<password-input <password-input
@ -145,14 +145,14 @@ const submit = async () => {
</template> </template>
<template v-else> <template v-else>
<p> <p>
{{ $t('components.auth.LoginForm.message.redirect', { domain: $store.getters['instance/domain'] }) }} {{ t('components.auth.LoginForm.message.redirect', { domain: $store.getters['instance/domain'] }) }}
</p> </p>
</template> </template>
<button <button
:class="['ui', { 'loading': isLoading }, 'right', 'floated', buttonClasses, 'button']" :class="['ui', { 'loading': isLoading }, 'right', 'floated', buttonClasses, 'button']"
type="submit" type="submit"
> >
{{ $t('components.auth.LoginForm.button.login') }} {{ t('components.auth.LoginForm.button.login') }}
</button> </button>
</form> </form>
</template> </template>

View File

@ -1,8 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
const store = useStore()
const { t } = useI18n() const { t } = useI18n()
const labels = computed(() => ({ const labels = computed(() => ({
title: t('components.auth.Logout.title') title: t('components.auth.Logout.title')
})) }))
@ -15,20 +18,20 @@ const labels = computed(() => ({
> >
<section class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div <div
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
class="ui small text container" class="ui small text container"
> >
<h2> <h2>
{{ $t('components.auth.Logout.header.confirm') }} {{ t('components.auth.Logout.header.confirm') }}
</h2> </h2>
<p> <p>
{{ $t('components.auth.Logout.message.loggedIn', { username: $store.state.auth.username }) }} {{ t('components.auth.Logout.message.loggedIn', { username: store.state.auth.username }) }}
</p> </p>
<button <button
class="ui button" class="ui button"
@click="$store.dispatch('auth/logout')" @click="store.dispatch('auth/logout')"
> >
{{ $t('components.auth.Logout.button.logout') }} {{ t('components.auth.Logout.button.logout') }}
</button> </button>
</div> </div>
<div <div
@ -36,13 +39,13 @@ const labels = computed(() => ({
class="ui small text container" class="ui small text container"
> >
<h2> <h2>
{{ $t('components.auth.Logout.header.unauthenticated') }} {{ t('components.auth.Logout.header.unauthenticated') }}
</h2> </h2>
<router-link <router-link
to="/login" to="/login"
class="ui button" class="ui button"
> >
{{ $t('components.auth.Logout.link.login') }} {{ t('components.auth.Logout.link.login') }}
</router-link> </router-link>
</div> </div>
</section> </section>

View File

@ -5,12 +5,15 @@ import axios from 'axios'
import { clone } from 'lodash-es' import { clone } from 'lodash-es'
import useMarkdown, { useMarkdownRaw } from '~/composables/useMarkdown' import useMarkdown, { useMarkdownRaw } from '~/composables/useMarkdown'
import { ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
interface Props { interface Props {
plugin: Plugin plugin: Plugin
libraries: Library[] libraries: Library[]
} }
const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const description = useMarkdown(() => props.plugin.description ?? '') const description = useMarkdown(() => props.plugin.description ?? '')
@ -69,7 +72,7 @@ const submitAndScan = async () => {
target="_blank" target="_blank"
> >
<i class="external icon" /> <i class="external icon" />
{{ $t('components.auth.Plugin.link.documentation') }} {{ t('components.auth.Plugin.link.documentation') }}
</a> </a>
</template> </template>
<div class="ui clearing hidden divider" /> <div class="ui clearing hidden divider" />
@ -79,7 +82,7 @@ const submitAndScan = async () => {
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.auth.Plugin.header.failure') }} {{ t('components.auth.Plugin.header.failure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -97,7 +100,7 @@ const submitAndScan = async () => {
v-model="enabled" v-model="enabled"
type="checkbox" type="checkbox"
> >
<label :for="`${plugin.name}-enabled`">{{ $t('components.auth.Plugin.label.pluginEnabled') }}</label> <label :for="`${plugin.name}-enabled`">{{ t('components.auth.Plugin.label.pluginEnabled') }}</label>
</div> </div>
</div> </div>
<div class="ui clearing hidden divider" /> <div class="ui clearing hidden divider" />
@ -105,7 +108,7 @@ const submitAndScan = async () => {
v-if="plugin.source" v-if="plugin.source"
class="field" class="field"
> >
<label for="plugin-library">{{ $t('components.auth.Plugin.label.library') }}</label> <label for="plugin-library">{{ t('components.auth.Plugin.label.library') }}</label>
<select <select
id="plugin-library" id="plugin-library"
v-model="values['library']" v-model="values['library']"
@ -119,7 +122,7 @@ const submitAndScan = async () => {
</option> </option>
</select> </select>
<div> <div>
{{ $t('components.auth.Plugin.description.library') }} {{ t('components.auth.Plugin.description.library') }}
</div> </div>
</div> </div>
<template v-if="(plugin.conf?.length ?? 0) > 0"> <template v-if="(plugin.conf?.length ?? 0) > 0">
@ -207,14 +210,14 @@ const submitAndScan = async () => {
type="submit" type="submit"
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
> >
{{ $t('components.auth.Plugin.button.save') }} {{ t('components.auth.Plugin.button.save') }}
</button> </button>
<button <button
v-if="plugin.source" v-if="plugin.source"
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
@click.prevent="submitAndScan" @click.prevent="submitAndScan"
> >
{{ $t('components.auth.Plugin.button.scan') }} {{ t('components.auth.Plugin.button.scan') }}
</button> </button>
<div class="ui clearing hidden divider" /> <div class="ui clearing hidden divider" />
</form> </form>

View File

@ -276,7 +276,7 @@ fetchOwnedApps()
<div class="ui vertical stripe segment"> <div class="ui vertical stripe segment">
<section class="ui text container"> <section class="ui text container">
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.auth.Settings.header.accountSettings') }} {{ t('components.auth.Settings.header.accountSettings') }}
</h2> </h2>
<form <form
class="ui form" class="ui form"
@ -287,7 +287,7 @@ fetchOwnedApps()
class="ui positive message" class="ui positive message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.auth.Settings.header.settingsUpdated') }} {{ t('components.auth.Settings.header.settingsUpdated') }}
</h4> </h4>
</div> </div>
<div <div
@ -296,7 +296,7 @@ fetchOwnedApps()
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.auth.Settings.header.updateFailure') }} {{ t('components.auth.Settings.header.updateFailure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -340,14 +340,14 @@ fetchOwnedApps()
:class="['ui', { loading: isLoading }, 'button']" :class="['ui', { loading: isLoading }, 'button']"
type="submit" type="submit"
> >
{{ $t('components.auth.Settings.button.updateSettings') }} {{ t('components.auth.Settings.button.updateSettings') }}
</button> </button>
</form> </form>
</section> </section>
<section class="ui text container"> <section class="ui text container">
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.auth.Settings.header.avatar') }} {{ t('components.auth.Settings.header.avatar') }}
</h2> </h2>
<div class="ui form"> <div class="ui form">
<div <div
@ -356,7 +356,7 @@ fetchOwnedApps()
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.auth.Settings.header.avatarFailure') }} {{ t('components.auth.Settings.header.avatarFailure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -373,7 +373,7 @@ fetchOwnedApps()
@update:model-value="submitAvatar($event)" @update:model-value="submitAvatar($event)"
@delete="avatar = {uuid: null}" @delete="avatar = {uuid: null}"
> >
{{ $t('components.auth.Settings.label.avatar') }} {{ t('components.auth.Settings.label.avatar') }}
</attachment-input> </attachment-input>
</div> </div>
</section> </section>
@ -381,10 +381,10 @@ fetchOwnedApps()
<section class="ui text container"> <section class="ui text container">
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.auth.Settings.header.changePassword') }} {{ t('components.auth.Settings.header.changePassword') }}
</h2> </h2>
<div class="ui message"> <div class="ui message">
{{ $t('components.auth.Settings.description.changePassword.paragraph1') }}&nbsp;{{ $t('components.auth.Settings.description.changePassword.paragraph2') }} {{ t('components.auth.Settings.description.changePassword.paragraph1') }}&nbsp;{{ t('components.auth.Settings.description.changePassword.paragraph2') }}
</div> </div>
<form <form
class="ui form" class="ui form"
@ -396,16 +396,16 @@ fetchOwnedApps()
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.auth.Settings.header.passwordFailure') }} {{ t('components.auth.Settings.header.passwordFailure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li v-if="passwordError == 'invalid_credentials'"> <li v-if="passwordError == 'invalid_credentials'">
{{ $t('components.auth.Settings.help.changePassword') }} {{ t('components.auth.Settings.help.changePassword') }}
</li> </li>
</ul> </ul>
</div> </div>
<div class="field"> <div class="field">
<label for="old-password-field">{{ $t('components.auth.Settings.label.currentPassword') }}</label> <label for="old-password-field">{{ t('components.auth.Settings.label.currentPassword') }}</label>
<password-input <password-input
v-model="credentials.oldPassword" v-model="credentials.oldPassword"
field-id="old-password-field" field-id="old-password-field"
@ -413,7 +413,7 @@ fetchOwnedApps()
/> />
</div> </div>
<div class="field"> <div class="field">
<label for="new-password-field">{{ $t('components.auth.Settings.label.newPassword') }}</label> <label for="new-password-field">{{ t('components.auth.Settings.label.newPassword') }}</label>
<password-input <password-input
v-model="credentials.newPassword" v-model="credentials.newPassword"
field-id="new-password-field" field-id="new-password-field"
@ -424,30 +424,30 @@ fetchOwnedApps()
:class="['ui', {'loading': isLoadingPassword}, {disabled: !credentials.newPassword || !credentials.oldPassword}, 'warning', 'button']" :class="['ui', {'loading': isLoadingPassword}, {disabled: !credentials.newPassword || !credentials.oldPassword}, 'warning', 'button']"
:action="submitPassword" :action="submitPassword"
> >
{{ $t('components.auth.Settings.button.password') }} {{ t('components.auth.Settings.button.password') }}
<template #modal-header> <template #modal-header>
<p> <p>
{{ $t('components.auth.Settings.modal.changePassword.header') }} {{ t('components.auth.Settings.modal.changePassword.header') }}
</p> </p>
</template> </template>
<template #modal-content> <template #modal-content>
<div> <div>
<p> <p>
{{ $t('components.auth.Settings.modal.changePassword.content.warning') }} {{ t('components.auth.Settings.modal.changePassword.content.warning') }}
</p> </p>
<ul> <ul>
<li> <li>
{{ $t('components.auth.Settings.modal.changePassword.content.logout') }} {{ t('components.auth.Settings.modal.changePassword.content.logout') }}
</li> </li>
<li> <li>
{{ $t('components.auth.Settings.modal.changePassword.content.subsonic') }} {{ t('components.auth.Settings.modal.changePassword.content.subsonic') }}
</li> </li>
</ul> </ul>
</div> </div>
</template> </template>
<template #modal-confirm> <template #modal-confirm>
<div> <div>
{{ $t('components.auth.Settings.button.disableSubsonic') }} {{ t('components.auth.Settings.button.disableSubsonic') }}
</div> </div>
</template> </template>
</dangerous-button> </dangerous-button>
@ -464,11 +464,11 @@ fetchOwnedApps()
<h2 class="ui header"> <h2 class="ui header">
<i class="eye slash outline icon" /> <i class="eye slash outline icon" />
<div class="content"> <div class="content">
{{ $t('components.auth.Settings.header.contentFilters') }} {{ t('components.auth.Settings.header.contentFilters') }}
</div> </div>
</h2> </h2>
<p> <p>
{{ $t('components.auth.Settings.description.contentFilters') }} {{ t('components.auth.Settings.description.contentFilters') }}
</p> </p>
<button <button
@ -476,19 +476,19 @@ fetchOwnedApps()
@click="$store.dispatch('moderation/fetchContentFilters')" @click="$store.dispatch('moderation/fetchContentFilters')"
> >
<i class="refresh icon" />&nbsp; <i class="refresh icon" />&nbsp;
{{ $t('components.auth.Settings.button.refresh') }} {{ t('components.auth.Settings.button.refresh') }}
</button> </button>
<h3 class="ui header"> <h3 class="ui header">
{{ $t('components.auth.Settings.header.hiddenArtists') }} {{ t('components.auth.Settings.header.hiddenArtists') }}
</h3> </h3>
<table class="ui compact very basic unstackable table"> <table class="ui compact very basic unstackable table">
<thead> <thead>
<tr> <tr>
<th> <th>
{{ $t('components.auth.Settings.table.artists.header.name') }} {{ t('components.auth.Settings.table.artists.header.name') }}
</th> </th>
<th> <th>
{{ $t('components.auth.Settings.table.artists.header.creationDate') }} {{ t('components.auth.Settings.table.artists.header.creationDate') }}
</th> </th>
<th /> <th />
</tr> </tr>
@ -511,7 +511,7 @@ fetchOwnedApps()
class="ui basic tiny button" class="ui basic tiny button"
@click="$store.dispatch('moderation/deleteContentFilter', filter.uuid)" @click="$store.dispatch('moderation/deleteContentFilter', filter.uuid)"
> >
{{ $t('components.auth.Settings.button.delete') }} {{ t('components.auth.Settings.button.delete') }}
</button> </button>
</td> </td>
</tr> </tr>
@ -526,18 +526,18 @@ fetchOwnedApps()
<h2 class="ui header"> <h2 class="ui header">
<i class="open lock icon" /> <i class="open lock icon" />
<div class="content"> <div class="content">
{{ $t('components.auth.Settings.header.authorizedApps') }} {{ t('components.auth.Settings.header.authorizedApps') }}
</div> </div>
</h2> </h2>
<p> <p>
{{ $t('components.auth.Settings.description.authorizedApps') }} {{ t('components.auth.Settings.description.authorizedApps') }}
</p> </p>
<button <button
:class="['ui', 'icon', { loading: isLoadingApps }, 'button']" :class="['ui', 'icon', { loading: isLoadingApps }, 'button']"
@click="fetchApps()" @click="fetchApps()"
> >
<i class="refresh icon" />&nbsp; <i class="refresh icon" />&nbsp;
{{ $t('components.auth.Settings.button.refresh') }} {{ t('components.auth.Settings.button.refresh') }}
</button> </button>
<table <table
v-if="apps.length > 0" v-if="apps.length > 0"
@ -546,10 +546,10 @@ fetchOwnedApps()
<thead> <thead>
<tr> <tr>
<th> <th>
{{ $t('components.auth.Settings.table.authorizedApps.header.application') }} {{ t('components.auth.Settings.table.authorizedApps.header.application') }}
</th> </th>
<th> <th>
{{ $t('components.auth.Settings.table.authorizedApps.header.permissions') }} {{ t('components.auth.Settings.table.authorizedApps.header.permissions') }}
</th> </th>
<th /> <th />
</tr> </tr>
@ -570,20 +570,20 @@ fetchOwnedApps()
:class="['ui', 'tiny', 'danger', { loading: isRevoking.has(app.client_id) }, 'button']" :class="['ui', 'tiny', 'danger', { loading: isRevoking.has(app.client_id) }, 'button']"
@confirm="revokeApp(app.client_id)" @confirm="revokeApp(app.client_id)"
> >
{{ $t('components.auth.Settings.button.revoke') }} {{ t('components.auth.Settings.button.revoke') }}
<template #modal-header> <template #modal-header>
<p> <p>
{{ $t('components.auth.Settings.modal.revokeApp.header', {app: app.name}) }} {{ t('components.auth.Settings.modal.revokeApp.header', {app: app.name}) }}
</p> </p>
</template> </template>
<template #modal-content> <template #modal-content>
<p> <p>
{{ $t('components.auth.Settings.modal.revokeApp.content.warning') }} {{ t('components.auth.Settings.modal.revokeApp.content.warning') }}
</p> </p>
</template> </template>
<template #modal-confirm> <template #modal-confirm>
<div> <div>
{{ $t('components.auth.Settings.button.revokeAccess') }} {{ t('components.auth.Settings.button.revokeAccess') }}
</div> </div>
</template> </template>
</dangerous-button> </dangerous-button>
@ -593,9 +593,9 @@ fetchOwnedApps()
</table> </table>
<empty-state v-else> <empty-state v-else>
<template #title> <template #title>
{{ $t('components.auth.Settings.header.noApps') }} {{ t('components.auth.Settings.header.noApps') }}
</template> </template>
{{ $t('components.auth.Settings.help.noApps') }} {{ t('components.auth.Settings.help.noApps') }}
</empty-state> </empty-state>
</section> </section>
<section <section
@ -606,17 +606,17 @@ fetchOwnedApps()
<h2 class="ui header"> <h2 class="ui header">
<i class="code icon" /> <i class="code icon" />
<div class="content"> <div class="content">
{{ $t('components.auth.Settings.header.yourApps') }} {{ t('components.auth.Settings.header.yourApps') }}
</div> </div>
</h2> </h2>
<p> <p>
{{ $t('components.auth.Settings.description.yourApps') }} {{ t('components.auth.Settings.description.yourApps') }}
</p> </p>
<router-link <router-link
class="ui success button" class="ui success button"
:to="{name: 'settings.applications.new'}" :to="{name: 'settings.applications.new'}"
> >
{{ $t('components.auth.Settings.link.newApp') }} {{ t('components.auth.Settings.link.newApp') }}
</router-link> </router-link>
<table <table
v-if="ownedApps.length > 0" v-if="ownedApps.length > 0"
@ -625,13 +625,13 @@ fetchOwnedApps()
<thead> <thead>
<tr> <tr>
<th> <th>
{{ $t('components.auth.Settings.table.yourApps.header.application') }} {{ t('components.auth.Settings.table.yourApps.header.application') }}
</th> </th>
<th> <th>
{{ $t('components.auth.Settings.table.yourApps.header.scopes') }} {{ t('components.auth.Settings.table.yourApps.header.scopes') }}
</th> </th>
<th> <th>
{{ $t('components.auth.Settings.table.yourApps.header.creationDate') }} {{ t('components.auth.Settings.table.yourApps.header.creationDate') }}
</th> </th>
<th /> <th />
</tr> </tr>
@ -657,26 +657,26 @@ fetchOwnedApps()
class="ui tiny success button" class="ui tiny success button"
:to="{name: 'settings.applications.edit', params: {id: app.client_id}}" :to="{name: 'settings.applications.edit', params: {id: app.client_id}}"
> >
{{ $t('components.auth.Settings.button.edit') }} {{ t('components.auth.Settings.button.edit') }}
</router-link> </router-link>
<dangerous-button <dangerous-button
:class="['ui', 'tiny', 'danger', { loading: isDeleting.has(app.client_id) }, 'button']" :class="['ui', 'tiny', 'danger', { loading: isDeleting.has(app.client_id) }, 'button']"
@confirm="deleteApp(app.client_id)" @confirm="deleteApp(app.client_id)"
> >
{{ $t('components.auth.Settings.button.remove') }} {{ t('components.auth.Settings.button.remove') }}
<template #modal-header> <template #modal-header>
<p> <p>
{{ $t('components.auth.Settings.modal.deleteApp.header', {app: app.name}) }} {{ t('components.auth.Settings.modal.deleteApp.header', {app: app.name}) }}
</p> </p>
</template> </template>
<template #modal-content> <template #modal-content>
<p> <p>
{{ $t('components.auth.Settings.modal.deleteApp.content.warning') }} {{ t('components.auth.Settings.modal.deleteApp.content.warning') }}
</p> </p>
</template> </template>
<template #modal-confirm> <template #modal-confirm>
<div> <div>
{{ $t('components.auth.Settings.button.removeApp') }} {{ t('components.auth.Settings.button.removeApp') }}
</div> </div>
</template> </template>
</dangerous-button> </dangerous-button>
@ -686,9 +686,9 @@ fetchOwnedApps()
</table> </table>
<empty-state v-else> <empty-state v-else>
<template #title> <template #title>
{{ $t('components.auth.Settings.header.noPersonalApps') }} {{ t('components.auth.Settings.header.noPersonalApps') }}
</template> </template>
{{ $t('components.auth.Settings.help.noPersonalApps') }} {{ t('components.auth.Settings.help.noPersonalApps') }}
</empty-state> </empty-state>
</section> </section>
@ -700,17 +700,17 @@ fetchOwnedApps()
<h2 class="ui header"> <h2 class="ui header">
<i class="code icon" /> <i class="code icon" />
<div class="content"> <div class="content">
{{ $t('components.auth.Settings.header.plugins') }} {{ t('components.auth.Settings.header.plugins') }}
</div> </div>
</h2> </h2>
<p> <p>
{{ $t('components.auth.Settings.description.plugins') }} {{ t('components.auth.Settings.description.plugins') }}
</p> </p>
<router-link <router-link
class="ui success button" class="ui success button"
:to="{name: 'settings.plugins'}" :to="{name: 'settings.plugins'}"
> >
{{ $t('components.auth.Settings.link.managePlugins') }} {{ t('components.auth.Settings.link.managePlugins') }}
</router-link> </router-link>
</section> </section>
<section class="ui text container"> <section class="ui text container">
@ -718,14 +718,14 @@ fetchOwnedApps()
<h2 class="ui header"> <h2 class="ui header">
<i class="comment icon" /> <i class="comment icon" />
<div class="content"> <div class="content">
{{ $t('components.auth.Settings.header.changeEmail') }} {{ t('components.auth.Settings.header.changeEmail') }}
</div> </div>
</h2> </h2>
<p> <p>
{{ $t('components.auth.Settings.description.changeEmail') }} {{ t('components.auth.Settings.description.changeEmail') }}
</p> </p>
<p> <p>
{{ $t('components.auth.Settings.message.currentEmail', { email: $store.state.auth.profile?.email }) }} {{ t('components.auth.Settings.message.currentEmail', { email: $store.state.auth.profile?.email }) }}
</p> </p>
<form <form
class="ui form" class="ui form"
@ -737,7 +737,7 @@ fetchOwnedApps()
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.auth.Settings.header.emailFailure') }} {{ t('components.auth.Settings.header.emailFailure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -749,7 +749,7 @@ fetchOwnedApps()
</ul> </ul>
</div> </div>
<div class="field"> <div class="field">
<label for="new-email">{{ $t('components.auth.Settings.label.newEmail') }}</label> <label for="new-email">{{ t('components.auth.Settings.label.newEmail') }}</label>
<input <input
id="new-email" id="new-email"
v-model="newEmail" v-model="newEmail"
@ -758,7 +758,7 @@ fetchOwnedApps()
> >
</div> </div>
<div class="field"> <div class="field">
<label for="current-password-field-email">{{ $t('components.auth.Settings.label.password') }}</label> <label for="current-password-field-email">{{ t('components.auth.Settings.label.password') }}</label>
<password-input <password-input
v-model="emailPassword" v-model="emailPassword"
field-id="current-password-field-email" field-id="current-password-field-email"
@ -769,7 +769,7 @@ fetchOwnedApps()
type="submit" type="submit"
class="ui button" class="ui button"
> >
{{ $t('components.auth.Settings.button.update') }} {{ t('components.auth.Settings.button.update') }}
</button> </button>
</form> </form>
</section> </section>
@ -778,17 +778,17 @@ fetchOwnedApps()
<h2 class="ui header"> <h2 class="ui header">
<i class="trash icon" /> <i class="trash icon" />
<div class="content"> <div class="content">
{{ $t('components.auth.Settings.header.deleteAccount') }} {{ t('components.auth.Settings.header.deleteAccount') }}
</div> </div>
</h2> </h2>
<p> <p>
{{ $t('components.auth.Settings.description.deleteAccount') }} {{ t('components.auth.Settings.description.deleteAccount') }}
</p> </p>
<div <div
role="alert" role="alert"
class="ui warning message" class="ui warning message"
> >
{{ $t('components.auth.Settings.warning.deleteAccount') }} {{ t('components.auth.Settings.warning.deleteAccount') }}
</div> </div>
<div class="ui form"> <div class="ui form">
<div <div
@ -797,7 +797,7 @@ fetchOwnedApps()
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.auth.Settings.header.accountFailure') }} {{ t('components.auth.Settings.header.accountFailure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -809,7 +809,7 @@ fetchOwnedApps()
</ul> </ul>
</div> </div>
<div class="field"> <div class="field">
<label for="current-password-field">{{ $t('components.auth.Settings.label.currentPassword') }}</label> <label for="current-password-field">{{ t('components.auth.Settings.label.currentPassword') }}</label>
<password-input <password-input
v-model="deleteAccountPassword" v-model="deleteAccountPassword"
field-id="current-password-field" field-id="current-password-field"
@ -820,22 +820,22 @@ fetchOwnedApps()
:class="['ui', {'loading': isDeletingAccount}, {disabled: !deleteAccountPassword}, {danger: deleteAccountPassword}, 'button']" :class="['ui', {'loading': isDeletingAccount}, {disabled: !deleteAccountPassword}, {danger: deleteAccountPassword}, 'button']"
:action="deleteAccount" :action="deleteAccount"
> >
{{ $t('components.auth.Settings.button.deleteAccount') }} {{ t('components.auth.Settings.button.deleteAccount') }}
<template #modal-header> <template #modal-header>
<p> <p>
{{ $t('components.auth.Settings.modal.deleteAccount.header') }} {{ t('components.auth.Settings.modal.deleteAccount.header') }}
</p> </p>
</template> </template>
<template #modal-content> <template #modal-content>
<div> <div>
<p> <p>
{{ $t('components.auth.Settings.modal.deleteAccount.content.warning') }} {{ t('components.auth.Settings.modal.deleteAccount.content.warning') }}
</p> </p>
</div> </div>
</template> </template>
<template #modal-confirm> <template #modal-confirm>
<div> <div>
{{ $t('components.auth.Settings.button.deleteAccountConfirm') }} {{ t('components.auth.Settings.button.deleteAccountConfirm') }}
</div> </div>
</template> </template>
</dangerous-button> </dangerous-button>

View File

@ -88,14 +88,14 @@ fetchInstanceSettings()
<div v-if="submitted"> <div v-if="submitted">
<div class="ui success message"> <div class="ui success message">
<p v-if="signupRequiresApproval"> <p v-if="signupRequiresApproval">
{{ $t('components.auth.SignupForm.message.awaitingReview') }} {{ t('components.auth.SignupForm.message.awaitingReview') }}
</p> </p>
<p v-else> <p v-else>
{{ $t('components.auth.SignupForm.message.accountCreated') }} {{ t('components.auth.SignupForm.message.accountCreated') }}
</p> </p>
</div> </div>
<h2> <h2>
{{ $t('components.auth.SignupForm.header.login') }} {{ t('components.auth.SignupForm.header.login') }}
</h2> </h2>
<login-form <login-form
button-classes="basic success" button-classes="basic success"
@ -111,13 +111,13 @@ fetchInstanceSettings()
v-if="!$store.state.instance.settings.users.registration_enabled.value" v-if="!$store.state.instance.settings.users.registration_enabled.value"
class="ui message" class="ui message"
> >
{{ $t('components.auth.SignupForm.message.registrationClosed') }} {{ t('components.auth.SignupForm.message.registrationClosed') }}
</p> </p>
<p <p
v-else-if="signupRequiresApproval" v-else-if="signupRequiresApproval"
class="ui message" class="ui message"
> >
{{ $t('components.auth.SignupForm.message.requiresReview') }} {{ t('components.auth.SignupForm.message.requiresReview') }}
</p> </p>
<template v-if="formCustomization?.help_text"> <template v-if="formCustomization?.help_text">
<rendered-description <rendered-description
@ -133,7 +133,7 @@ fetchInstanceSettings()
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.auth.SignupForm.header.signupFailure') }} {{ t('components.auth.SignupForm.header.signupFailure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -145,7 +145,7 @@ fetchInstanceSettings()
</ul> </ul>
</div> </div>
<div class="required field"> <div class="required field">
<label for="username-field">{{ $t('components.auth.SignupForm.label.username') }}</label> <label for="username-field">{{ t('components.auth.SignupForm.label.username') }}</label>
<input <input
id="username-field" id="username-field"
ref="username" ref="username"
@ -158,7 +158,7 @@ fetchInstanceSettings()
> >
</div> </div>
<div class="required field"> <div class="required field">
<label for="email-field">{{ $t('components.auth.SignupForm.label.email') }}</label> <label for="email-field">{{ t('components.auth.SignupForm.label.email') }}</label>
<input <input
id="email-field" id="email-field"
ref="email" ref="email"
@ -170,7 +170,7 @@ fetchInstanceSettings()
> >
</div> </div>
<div class="required field"> <div class="required field">
<label for="password-field">{{ $t('components.auth.SignupForm.label.password') }}</label> <label for="password-field">{{ t('components.auth.SignupForm.label.password') }}</label>
<password-input <password-input
v-model="payload.password1" v-model="payload.password1"
field-id="password-field" field-id="password-field"
@ -180,7 +180,7 @@ fetchInstanceSettings()
v-if="!$store.state.instance.settings.users.registration_enabled.value" v-if="!$store.state.instance.settings.users.registration_enabled.value"
class="required field" class="required field"
> >
<label for="invitation-code">{{ $t('components.auth.SignupForm.label.invitation') }}</label> <label for="invitation-code">{{ t('components.auth.SignupForm.label.invitation') }}</label>
<input <input
id="invitation-code" id="invitation-code"
v-model="payload.invitation" v-model="payload.invitation"
@ -217,7 +217,7 @@ fetchInstanceSettings()
:class="['ui', buttonClasses, {'loading': isLoading}, ' right floated button']" :class="['ui', buttonClasses, {'loading': isLoading}, ' right floated button']"
type="submit" type="submit"
> >
{{ $t('components.auth.SignupForm.button.create') }} {{ t('components.auth.SignupForm.button.create') }}
</button> </button>
</form> </form>
</template> </template>

View File

@ -82,26 +82,26 @@ fetchToken()
@submit.prevent="requestNewToken()" @submit.prevent="requestNewToken()"
> >
<h2> <h2>
{{ $t('components.auth.SubsonicTokenForm.header.subsonic') }} {{ t('components.auth.SubsonicTokenForm.header.subsonic') }}
</h2> </h2>
<p <p
v-if="!subsonicEnabled" v-if="!subsonicEnabled"
class="ui message" class="ui message"
> >
{{ $t('components.auth.SubsonicTokenForm.message.unavailable') }} {{ t('components.auth.SubsonicTokenForm.message.unavailable') }}
</p> </p>
<p> <p>
{{ $t('components.auth.SubsonicTokenForm.description.subsonic.paragraph1') }}&nbsp;{{ $t('components.auth.SubsonicTokenForm.description.subsonic.paragraph2') }} {{ t('components.auth.SubsonicTokenForm.description.subsonic.paragraph1') }}&nbsp;{{ t('components.auth.SubsonicTokenForm.description.subsonic.paragraph2') }}
</p> </p>
<p> <p>
{{ $t('components.auth.SubsonicTokenForm.description.subsonic.paragraph3') }} {{ t('components.auth.SubsonicTokenForm.description.subsonic.paragraph3') }}
</p> </p>
<p> <p>
<a <a
href="https://docs.funkwhale.audio/users/apps.html#subsonic-compatible-clients" href="https://docs.funkwhale.audio/users/apps.html#subsonic-compatible-clients"
target="_blank" target="_blank"
> >
{{ $t('components.auth.SubsonicTokenForm.link.apps') }} {{ t('components.auth.SubsonicTokenForm.link.apps') }}
</a> </a>
</p> </p>
<div <div
@ -118,7 +118,7 @@ fetchToken()
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.auth.SubsonicTokenForm.header.error') }} {{ t('components.auth.SubsonicTokenForm.header.error') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -152,20 +152,20 @@ fetchToken()
:class="['ui', {'loading': isLoading}, 'button']" :class="['ui', {'loading': isLoading}, 'button']"
:action="requestNewToken" :action="requestNewToken"
> >
{{ $t('components.auth.SubsonicTokenForm.button.newPassword') }} {{ t('components.auth.SubsonicTokenForm.button.newPassword') }}
<template #modal-header> <template #modal-header>
<p> <p>
{{ $t('components.auth.SubsonicTokenForm.modal.newPassword.header') }} {{ t('components.auth.SubsonicTokenForm.modal.newPassword.header') }}
</p> </p>
</template> </template>
<template #modal-content> <template #modal-content>
<p> <p>
{{ $t('components.auth.SubsonicTokenForm.modal.newPassword.content.warning') }} {{ t('components.auth.SubsonicTokenForm.modal.newPassword.content.warning') }}
</p> </p>
</template> </template>
<template #modal-confirm> <template #modal-confirm>
<div> <div>
{{ $t('components.auth.SubsonicTokenForm.button.confirmNewPassword') }} {{ t('components.auth.SubsonicTokenForm.button.confirmNewPassword') }}
</div> </div>
</template> </template>
</dangerous-button> </dangerous-button>
@ -175,27 +175,27 @@ fetchToken()
:class="['ui', {'loading': isLoading}, 'button']" :class="['ui', {'loading': isLoading}, 'button']"
@click="requestNewToken" @click="requestNewToken"
> >
{{ $t('components.auth.SubsonicTokenForm.button.confirmNewPassword') }} {{ t('components.auth.SubsonicTokenForm.button.confirmNewPassword') }}
</button> </button>
<dangerous-button <dangerous-button
v-if="token" v-if="token"
:class="['ui', {'loading': isLoading}, 'warning', 'button']" :class="['ui', {'loading': isLoading}, 'warning', 'button']"
:action="disable" :action="disable"
> >
{{ $t('components.auth.SubsonicTokenForm.button.disable') }} {{ t('components.auth.SubsonicTokenForm.button.disable') }}
<template #modal-header> <template #modal-header>
<p> <p>
{{ $t('components.auth.SubsonicTokenForm.modal.disableSubsonic.header') }} {{ t('components.auth.SubsonicTokenForm.modal.disableSubsonic.header') }}
</p> </p>
</template> </template>
<template #modal-content> <template #modal-content>
<p> <p>
{{ $t('components.auth.SubsonicTokenForm.modal.disableSubsonic.content.warning') }} {{ t('components.auth.SubsonicTokenForm.modal.disableSubsonic.content.warning') }}
</p> </p>
</template> </template>
<template #modal-confirm> <template #modal-confirm>
<div> <div>
{{ $t('components.auth.SubsonicTokenForm.button.confirmDisable') }} {{ t('components.auth.SubsonicTokenForm.button.confirmDisable') }}
</div> </div>
</template> </template>
</dangerous-button> </dangerous-button>

View File

@ -2,6 +2,7 @@
import type { BackendError, Channel } from '~/types' import type { BackendError, Channel } from '~/types'
import { computed, watch, ref } from 'vue' import { computed, watch, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
interface Events { interface Events {
@ -14,6 +15,8 @@ interface Props {
channel: Channel channel: Channel
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = defineProps<Props>() const props = defineProps<Props>()
@ -59,7 +62,7 @@ defineExpose({
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.channels.AlbumForm.header.error') }} {{ t('components.channels.AlbumForm.header.error') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -72,7 +75,7 @@ defineExpose({
</div> </div>
<div class="ui required field"> <div class="ui required field">
<label for="album-title"> <label for="album-title">
{{ $t('components.channels.AlbumForm.label.albumTitle') }} {{ t('components.channels.AlbumForm.label.albumTitle') }}
</label> </label>
<input <input
v-model="title" v-model="title"

View File

@ -3,6 +3,7 @@ import type { Channel } from '~/types'
import SemanticModal from '~/components/semantic/Modal.vue' import SemanticModal from '~/components/semantic/Modal.vue'
import ChannelAlbumForm from '~/components/channels/AlbumForm.vue' import ChannelAlbumForm from '~/components/channels/AlbumForm.vue'
import { watch, ref } from 'vue' import { watch, ref } from 'vue'
import { useI18n } from 'vue-i18n'
interface Events { interface Events {
(e: 'created'): void (e: 'created'): void
@ -12,6 +13,8 @@ interface Props {
channel: Channel channel: Channel
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
defineProps<Props>() defineProps<Props>()
@ -37,10 +40,10 @@ defineExpose({
> >
<h4 class="header"> <h4 class="header">
<span v-if="channel.content_category === 'podcast'"> <span v-if="channel.content_category === 'podcast'">
{{ $t('components.channels.AlbumModal.header.newSeries') }} {{ t('components.channels.AlbumModal.header.newSeries') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.channels.AlbumModal.header.newAlbum') }} {{ t('components.channels.AlbumModal.header.newAlbum') }}
</span> </span>
</h4> </h4>
<div class="scrolling content"> <div class="scrolling content">
@ -54,14 +57,14 @@ defineExpose({
</div> </div>
<div class="actions"> <div class="actions">
<button class="ui basic cancel button"> <button class="ui basic cancel button">
{{ $t('components.channels.AlbumModal.button.cancel') }} {{ t('components.channels.AlbumModal.button.cancel') }}
</button> </button>
<button <button
:class="['ui', 'primary', {loading: isLoading}, 'button']" :class="['ui', 'primary', {loading: isLoading}, 'button']"
:disabled="!submittable" :disabled="!submittable"
@click.stop.prevent="albumForm.submit()" @click.stop.prevent="albumForm.submit()"
> >
{{ $t('components.channels.AlbumModal.button.create') }} {{ t('components.channels.AlbumModal.button.create') }}
</button> </button>
</div> </div>
</semantic-modal> </semantic-modal>

View File

@ -4,6 +4,7 @@ import type { Album, Channel } from '~/types'
import axios from 'axios' import axios from 'axios'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { reactive, ref, watch } from 'vue' import { reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
interface Events { interface Events {
(e: 'update:modelValue', value: string): void (e: 'update:modelValue', value: string): void
@ -14,6 +15,8 @@ interface Props {
channel: Channel | null channel: Channel | null
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
modelValue: null, modelValue: null,
@ -48,10 +51,10 @@ watch(() => props.channel, fetchData, { immediate: true })
<div> <div>
<label for="album-dropdown"> <label for="album-dropdown">
<span v-if="channel && channel.artist && channel.artist.content_category === 'podcast'"> <span v-if="channel && channel.artist && channel.artist.content_category === 'podcast'">
{{ $t('components.channels.AlbumSelect.label.series') }} {{ t('components.channels.AlbumSelect.label.series') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.channels.AlbumSelect.label.album') }} {{ t('components.channels.AlbumSelect.label.album') }}
</span> </span>
</label> </label>
<select <select
@ -60,7 +63,7 @@ watch(() => props.channel, fetchData, { immediate: true })
class="ui search normal dropdown" class="ui search normal dropdown"
> >
<option value=""> <option value="">
{{ $t('components.channels.AlbumSelect.option.none') }} {{ t('components.channels.AlbumSelect.option.none') }}
</option> </option>
<option <option
v-for="album in albums" v-for="album in albums"
@ -69,7 +72,7 @@ watch(() => props.channel, fetchData, { immediate: true })
> >
{{ album.title }} {{ album.title }}
<span> <span>
{{ $t('components.channels.AlbumSelect.meta.tracks', album.tracks_count) }} {{ t('components.channels.AlbumSelect.meta.tracks', album.tracks_count) }}
</span> </span>
</option> </option>
</select> </select>

View File

@ -4,6 +4,7 @@ import type { License } from '~/types'
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import axios from 'axios' import axios from 'axios'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
interface Events { interface Events {
(e: 'update:modelValue', value: string): void (e: 'update:modelValue', value: string): void
@ -13,6 +14,8 @@ interface Props {
modelValue: string | null modelValue: string | null
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
modelValue: null modelValue: null
@ -55,7 +58,7 @@ fetchLicenses()
<template> <template>
<div> <div>
<label for="license-dropdown"> <label for="license-dropdown">
{{ $t('components.channels.LicenseSelect.label.license') }} {{ t('components.channels.LicenseSelect.label.license') }}
</label> </label>
<select <select
id="license-dropdown" id="license-dropdown"
@ -63,7 +66,7 @@ fetchLicenses()
class="ui search normal dropdown" class="ui search normal dropdown"
> >
<option value=""> <option value="">
{{ $t('components.channels.LicenseSelect.option.none') }} {{ t('components.channels.LicenseSelect.option.none') }}
</option> </option>
<option <option
v-for="l in featuredLicenses" v-for="l in featuredLicenses"
@ -84,7 +87,7 @@ fetchLicenses()
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
{{ $t('components.channels.LicenseSelect.link.license') }} {{ t('components.channels.LicenseSelect.link.license') }}
</a> </a>
</p> </p>
</div> </div>

View File

@ -4,6 +4,9 @@ import type { Channel } from '~/types'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useRoute } from 'vue-router'
const route = useRoute()
import LoginModal from '~/components/common/LoginModal.vue' import LoginModal from '~/components/common/LoginModal.vue'
@ -44,7 +47,7 @@ const loginModal = ref()
<template> <template>
<button <button
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
:class="['ui', 'pink', {'inverted': isSubscribed}, {'favorited': isSubscribed}, 'icon', 'labeled', 'button']" :class="['ui', 'pink', {'inverted': isSubscribed}, {'favorited': isSubscribed}, 'icon', 'labeled', 'button']"
@click.stop="toggle" @click.stop="toggle"
> >
@ -61,7 +64,7 @@ const loginModal = ref()
<login-modal <login-modal
ref="loginModal" ref="loginModal"
class="small" class="small"
:next-route="$route.fullPath" :next-route="route.fullPath"
:message="message.authMessage" :message="message.authMessage"
:cover="channel.artist?.cover!" :cover="channel.artist?.cover!"
@created="loginModal.show = false" @created="loginModal.show = false"

View File

@ -440,7 +440,7 @@ defineExpose({
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.channels.UploadForm.header.error') }} {{ t('components.channels.UploadForm.header.error') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -453,7 +453,7 @@ defineExpose({
</div> </div>
<div :class="['ui', 'required', {hidden: step > 1}, 'field']"> <div :class="['ui', 'required', {hidden: step > 1}, 'field']">
<label for="channel-dropdown"> <label for="channel-dropdown">
{{ $t('components.channels.UploadForm.label.channel') }} {{ t('components.channels.UploadForm.label.channel') }}
</label> </label>
<div <div
id="channel-dropdown" id="channel-dropdown"
@ -476,7 +476,7 @@ defineExpose({
<div class="content"> <div class="content">
<p> <p>
<i class="copyright icon" /> <i class="copyright icon" />
{{ $t('components.channels.UploadForm.help.license') }} {{ t('components.channels.UploadForm.help.license') }}
</p> </p>
</div> </div>
</div> </div>
@ -489,7 +489,7 @@ defineExpose({
<div class="content"> <div class="content">
<p> <p>
<i class="warning icon" /> <i class="warning icon" />
{{ $t('components.channels.UploadForm.warning.quota') }} {{ t('components.channels.UploadForm.warning.quota') }}
</p> </p>
</div> </div>
</div> </div>
@ -500,19 +500,19 @@ defineExpose({
> >
<p> <p>
<i class="redo icon" /> <i class="redo icon" />
{{ $t('components.channels.UploadForm.message.pending') }} {{ t('components.channels.UploadForm.message.pending') }}
</p> </p>
<button <button
class="ui basic button" class="ui basic button"
@click.stop.prevent="includeDraftUploads = false" @click.stop.prevent="includeDraftUploads = false"
> >
{{ $t('components.channels.UploadForm.button.ignore') }} {{ t('components.channels.UploadForm.button.ignore') }}
</button> </button>
<button <button
class="ui basic button" class="ui basic button"
@click.stop.prevent="includeDraftUploads = true" @click.stop.prevent="includeDraftUploads = true"
> >
{{ $t('components.channels.UploadForm.button.resume') }} {{ t('components.channels.UploadForm.button.resume') }}
</button> </button>
</div> </div>
<div <div
@ -564,13 +564,13 @@ defineExpose({
</template> </template>
<template v-else> <template v-else>
<span v-if="file.active"> <span v-if="file.active">
{{ $t('components.channels.UploadForm.status.uploading') }} {{ t('components.channels.UploadForm.status.uploading') }}
</span> </span>
<span v-else-if="file.error"> <span v-else-if="file.error">
{{ $t('components.channels.UploadForm.status.errored') }} {{ t('components.channels.UploadForm.status.errored') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.channels.UploadForm.status.pending') }} {{ t('components.channels.UploadForm.status.pending') }}
</span> </span>
<span class="middle middledot symbol" /> <span class="middle middledot symbol" />
{{ humanSize(file.size ?? 0) }} {{ humanSize(file.size ?? 0) }}
@ -580,12 +580,12 @@ defineExpose({
</template> </template>
<span class="middle middledot symbol" /> <span class="middle middledot symbol" />
<a @click.stop.prevent="remove(file)"> <a @click.stop.prevent="remove(file)">
{{ $t('components.channels.UploadForm.button.remove') }} {{ t('components.channels.UploadForm.button.remove') }}
</a> </a>
<template v-if="file.error"> <template v-if="file.error">
<span class="middle middledot symbol" /> <span class="middle middledot symbol" />
<a @click.stop.prevent="retry(file)"> <a @click.stop.prevent="retry(file)">
{{ $t('components.channels.UploadForm.button.retry') }} {{ t('components.channels.UploadForm.button.retry') }}
</a> </a>
</template> </template>
</div> </div>
@ -604,7 +604,7 @@ defineExpose({
<div class="content"> <div class="content">
<p> <p>
<i class="info icon" /> <i class="info icon" />
{{ $t('components.channels.UploadForm.description.extensions', {extensions: $store.state.ui.supportedExtensions.join(', ')}) }} {{ t('components.channels.UploadForm.description.extensions', {extensions: $store.state.ui.supportedExtensions.join(', ')}) }}
</p> </p>
</div> </div>
</div> </div>
@ -617,11 +617,11 @@ defineExpose({
> >
<div> <div>
<i class="upload icon" />&nbsp; <i class="upload icon" />&nbsp;
{{ $t('components.channels.UploadForm.message.dragAndDrop') }} {{ t('components.channels.UploadForm.message.dragAndDrop') }}
</div> </div>
<div class="ui very small divider" /> <div class="ui very small divider" />
<div> <div>
{{ $t('components.channels.UploadForm.label.openBrowser') }} {{ t('components.channels.UploadForm.label.openBrowser') }}
</div> </div>
</file-upload-widget> </file-upload-widget>
<div class="ui hidden divider" /> <div class="ui hidden divider" />

View File

@ -2,6 +2,7 @@
import type { Upload, Track } from '~/types' import type { Upload, Track } from '~/types'
import { reactive, computed, watch } from 'vue' import { reactive, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import TagsSelector from '~/components/library/TagsSelector.vue' import TagsSelector from '~/components/library/TagsSelector.vue'
import AttachmentInput from '~/components/common/AttachmentInput.vue' import AttachmentInput from '~/components/common/AttachmentInput.vue'
@ -16,6 +17,8 @@ interface Props {
values: Partial<Values> | null values: Partial<Values> | null
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
values: null values: null
@ -37,7 +40,7 @@ watch(newValues, (values) => emit('update:values', values), { immediate: true })
<div :class="['ui', {loading: isLoading}, 'form']"> <div :class="['ui', {loading: isLoading}, 'form']">
<div class="ui required field"> <div class="ui required field">
<label for="upload-title"> <label for="upload-title">
{{ $t('components.channels.UploadMetadataForm.label.title') }} {{ t('components.channels.UploadMetadataForm.label.title') }}
</label> </label>
<input <input
v-model="newValues.title" v-model="newValues.title"
@ -48,13 +51,13 @@ watch(newValues, (values) => emit('update:values', values), { immediate: true })
v-model="newValues.cover" v-model="newValues.cover"
@delete="newValues.cover = ''" @delete="newValues.cover = ''"
> >
{{ $t('components.channels.UploadMetadataForm.label.image') }} {{ t('components.channels.UploadMetadataForm.label.image') }}
</attachment-input> </attachment-input>
<div class="ui small hidden divider" /> <div class="ui small hidden divider" />
<div class="ui two fields"> <div class="ui two fields">
<div class="ui field"> <div class="ui field">
<label for="upload-tags"> <label for="upload-tags">
{{ $t('components.channels.UploadMetadataForm.label.tags') }} {{ t('components.channels.UploadMetadataForm.label.tags') }}
</label> </label>
<tags-selector <tags-selector
id="upload-tags" id="upload-tags"
@ -64,7 +67,7 @@ watch(newValues, (values) => emit('update:values', values), { immediate: true })
</div> </div>
<div class="ui field"> <div class="ui field">
<label for="upload-position"> <label for="upload-position">
{{ $t('components.channels.UploadMetadataForm.label.position') }} {{ t('components.channels.UploadMetadataForm.label.position') }}
</label> </label>
<input <input
v-model="newValues.position" v-model="newValues.position"
@ -76,7 +79,7 @@ watch(newValues, (values) => emit('update:values', values), { immediate: true })
</div> </div>
<div class="ui field"> <div class="ui field">
<label for="upload-description"> <label for="upload-description">
{{ $t('components.channels.UploadMetadataForm.label.description') }} {{ t('components.channels.UploadMetadataForm.label.description') }}
</label> </label>
<content-form <content-form
v-model="newValues.description" v-model="newValues.description"

View File

@ -49,7 +49,7 @@ const statusInfo = computed(() => {
const step = ref(1) const step = ref(1)
const isLoading = ref(false) const isLoading = ref(false)
const dropdownOpen = ref(false) const open = ref(false)
</script> </script>
<template> <template>
@ -59,16 +59,16 @@ const dropdownOpen = ref(false)
> >
<h4 class="header"> <h4 class="header">
<span v-if="step === 1"> <span v-if="step === 1">
{{ $t('components.channels.UploadModal.header.publish') }} {{ t('components.channels.UploadModal.header.publish') }}
</span> </span>
<span v-else-if="step === 2"> <span v-else-if="step === 2">
{{ $t('components.channels.UploadModal.header.uploadFiles') }} {{ t('components.channels.UploadModal.header.uploadFiles') }}
</span> </span>
<span v-else-if="step === 3"> <span v-else-if="step === 3">
{{ $t('components.channels.UploadModal.header.uploadDetails') }} {{ t('components.channels.UploadModal.header.uploadDetails') }}
</span> </span>
<span v-else-if="step === 4"> <span v-else-if="step === 4">
{{ $t('components.channels.UploadModal.header.processing') }} {{ t('components.channels.UploadModal.header.processing') }}
</span> </span>
</h4> </h4>
<div class="scrolling content"> <div class="scrolling content">
@ -87,7 +87,7 @@ const dropdownOpen = ref(false)
</template> </template>
<div class="ui very small hidden divider" /> <div class="ui very small hidden divider" />
<template v-if="statusData && statusData.quotaStatus"> <template v-if="statusData && statusData.quotaStatus">
{{ $t('components.channels.UploadModal.meta.quota', humanSize((statusData.quotaStatus.remaining - statusData.uploadedSize) * 1000 * 1000)) }} {{ t('components.channels.UploadModal.meta.quota', humanSize((statusData.quotaStatus.remaining - statusData.uploadedSize) * 1000 * 1000)) }}
</template> </template>
</div> </div>
<div class="ui hidden clearing divider mobile-only" /> <div class="ui hidden clearing divider mobile-only" />
@ -97,7 +97,7 @@ const dropdownOpen = ref(false)
variant="outline" variant="outline"
@click="update(false)" @click="update(false)"
> >
{{ $t('components.channels.UploadModal.button.cancel') }} {{ t('components.channels.UploadModal.button.cancel') }}
</Button> </Button>
<Button <Button
v-else-if="step < 3" v-else-if="step < 3"
@ -105,21 +105,21 @@ const dropdownOpen = ref(false)
variant="outline" variant="outline"
@click.stop.prevent="uploadForm.step -= 1" @click.stop.prevent="uploadForm.step -= 1"
> >
{{ $t('components.channels.UploadModal.button.previous') }} {{ t('components.channels.UploadModal.button.previous') }}
</Button> </Button>
<Button <Button
v-else-if="step === 3" v-else-if="step === 3"
color="secondary" color="secondary"
@click.stop.prevent="uploadForm.step -= 1" @click.stop.prevent="uploadForm.step -= 1"
> >
{{ $t('components.channels.UploadModal.button.update') }} {{ t('components.channels.UploadModal.button.update') }}
</Button> </Button>
<Button <Button
v-if="step === 1" v-if="step === 1"
color="secondary" color="secondary"
@click.stop.prevent="uploadForm.step += 1" @click.stop.prevent="uploadForm.step += 1"
> >
{{ $t('components.channels.UploadModal.button.next') }} {{ t('components.channels.UploadModal.button.next') }}
</Button> </Button>
<div class="ui primary buttons"> <div class="ui primary buttons">
<Button <Button
@ -128,10 +128,10 @@ const dropdownOpen = ref(false)
:disabled="!statusData?.canSubmit" :disabled="!statusData?.canSubmit"
@click.prevent.stop="uploadForm.publish" @click.prevent.stop="uploadForm.publish"
> >
{{ $t('components.channels.UploadModal.button.publish') }} {{ t('components.channels.UploadModal.button.publish') }}
</Button> </Button>
<Popover v-model:open="dropdownOpen"> <Popover v-model:open="open">
<template #default="{ toggleOpen }"> <template #default="{ toggleOpen }">
<Button <Button
color="primary" color="primary"
@ -142,7 +142,7 @@ const dropdownOpen = ref(false)
</template> </template>
<template #items> <template #items>
<PopoverItem @click="update(false)"> <PopoverItem @click="update(false)">
{{ $t('components.channels.UploadModal.button.finishLater') }} {{ t('components.channels.UploadModal.button.finishLater') }}
</PopoverItem> </PopoverItem>
</template> </template>
</Popover> </Popover>
@ -153,7 +153,7 @@ const dropdownOpen = ref(false)
color="secondary" color="secondary"
@click="update(false)" @click="update(false)"
> >
{{ $t('components.channels.UploadModal.button.close') }} {{ t('components.channels.UploadModal.button.close') }}
</Button> </Button>
</div> </div>
</semantic-modal> </semantic-modal>

View File

@ -167,7 +167,7 @@ const launchAction = async () => {
class="right floated" class="right floated"
> >
<span v-if="needsRefresh"> <span v-if="needsRefresh">
{{ $t('components.common.ActionTable.message.needsRefresh') }} {{ t('components.common.ActionTable.message.needsRefresh') }}
</span> </span>
<button <button
class="ui basic icon button" class="ui basic icon button"
@ -185,7 +185,7 @@ const launchAction = async () => {
> >
<div class="ui inline fields"> <div class="ui inline fields">
<div class="field"> <div class="field">
<label for="actions-select">{{ $t('components.common.ActionTable.label.actions') }}</label> <label for="actions-select">{{ t('components.common.ActionTable.label.actions') }}</label>
<select <select
id="actions-select" id="actions-select"
v-model="currentActionName" v-model="currentActionName"
@ -208,11 +208,11 @@ const launchAction = async () => {
:aria-label="labels.performAction" :aria-label="labels.performAction"
@confirm="launchAction" @confirm="launchAction"
> >
{{ $t('components.common.ActionTable.button.go') }} {{ t('components.common.ActionTable.button.go') }}
<template #modal-header> <template #modal-header>
<p> <p>
<span key="1"> <span key="1">
{{ $t('components.common.ActionTable.modal.performAction.header', { action: currentActionName }, affectedObjectsCount) }} {{ t('components.common.ActionTable.modal.performAction.header', { action: currentActionName }, affectedObjectsCount) }}
</span> </span>
</p> </p>
</template> </template>
@ -222,13 +222,13 @@ const launchAction = async () => {
{{ currentAction?.confirmationMessage }} {{ currentAction?.confirmationMessage }}
</template> </template>
<span v-else> <span v-else>
{{ $t('components.common.ActionTable.modal.performAction.content.warning') }} {{ t('components.common.ActionTable.modal.performAction.content.warning') }}
</span> </span>
</p> </p>
</template> </template>
<template #modal-confirm> <template #modal-confirm>
<div :aria-label="labels.performAction"> <div :aria-label="labels.performAction">
{{ $t('components.common.ActionTable.button.launch') }} {{ t('components.common.ActionTable.button.launch') }}
</div> </div>
</template> </template>
</dangerous-button> </dangerous-button>
@ -239,15 +239,15 @@ const launchAction = async () => {
:class="['ui', {disabled: checked.length === 0}, {'loading': isLoading}, 'button']" :class="['ui', {disabled: checked.length === 0}, {'loading': isLoading}, 'button']"
@click="launchAction" @click="launchAction"
> >
{{ $t('components.common.ActionTable.button.go') }} {{ t('components.common.ActionTable.button.go') }}
</button> </button>
</div> </div>
<div class="count field"> <div class="count field">
<span v-if="selectAll"> <span v-if="selectAll">
{{ $t('components.common.ActionTable.button.allSelected', objectsData.count) }} {{ t('components.common.ActionTable.button.allSelected', objectsData.count) }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.common.ActionTable.button.selected', { total: objectsData.count }, checked.length) }} {{ t('components.common.ActionTable.button.selected', { total: objectsData.count }, checked.length) }}
</span> </span>
<template v-if="currentAction?.allowAll && checkable.length > 0 && checkable.length === checked.length"> <template v-if="currentAction?.allowAll && checkable.length > 0 && checkable.length === checked.length">
<a <a
@ -256,7 +256,7 @@ const launchAction = async () => {
@click.prevent="selectAll = true" @click.prevent="selectAll = true"
> >
<span key="3"> <span key="3">
{{ $t('components.common.ActionTable.button.selectElement', objectsData.count) }} {{ t('components.common.ActionTable.button.selectElement', objectsData.count) }}
</span> </span>
</a> </a>
<a <a
@ -265,7 +265,7 @@ const launchAction = async () => {
@click.prevent="selectAll = false" @click.prevent="selectAll = false"
> >
<span key="4"> <span key="4">
{{ $t('components.common.ActionTable.button.selectCurrentPage') }} {{ t('components.common.ActionTable.button.selectCurrentPage') }}
</span> </span>
</a> </a>
</template> </template>
@ -277,7 +277,7 @@ const launchAction = async () => {
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.common.ActionTable.header.error') }} {{ t('components.common.ActionTable.header.error') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -294,7 +294,7 @@ const launchAction = async () => {
> >
<p> <p>
<span> <span>
{{ $t('components.common.ActionTable.message.success', { action: result.action }, result.updated) }} {{ t('components.common.ActionTable.message.success', { action: result.action }, result.updated) }}
</span> </span>
</p> </p>

View File

@ -5,6 +5,7 @@ import axios from 'axios'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { reactive, ref, watch } from 'vue' import { reactive, ref, watch } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import useFormData from '~/composables/useFormData' import useFormData from '~/composables/useFormData'
interface Events { interface Events {
@ -20,6 +21,8 @@ interface Props {
initialValue?: string | undefined initialValue?: string | undefined
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
imageClass: '', imageClass: '',
@ -107,7 +110,7 @@ const getAttachmentUrl = (uuid: string) => {
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.common.AttachmentInput.header.failure') }} {{ t('components.common.AttachmentInput.header.failure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -144,7 +147,7 @@ const getAttachmentUrl = (uuid: string) => {
<div class="eleven wide column"> <div class="eleven wide column">
<div class="file-input"> <div class="file-input">
<label :for="attachmentId"> <label :for="attachmentId">
{{ $t('components.common.AttachmentInput.label.upload') }} {{ t('components.common.AttachmentInput.label.upload') }}
</label> </label>
<input <input
:id="attachmentId" :id="attachmentId"
@ -159,21 +162,21 @@ const getAttachmentUrl = (uuid: string) => {
</div> </div>
<div class="ui very small hidden divider" /> <div class="ui very small hidden divider" />
<p> <p>
{{ $t('components.common.AttachmentInput.help.upload') }} {{ t('components.common.AttachmentInput.help.upload') }}
</p> </p>
<button <button
v-if="value" v-if="value"
class="ui basic tiny button" class="ui basic tiny button"
@click.stop.prevent="remove(value as string)" @click.stop.prevent="remove(value as string)"
> >
{{ $t('components.common.AttachmentInput.button.remove') }} {{ t('components.common.AttachmentInput.button.remove') }}
</button> </button>
<div <div
v-if="isLoading" v-if="isLoading"
class="ui active inverted dimmer" class="ui active inverted dimmer"
> >
<div class="ui indeterminate text loader"> <div class="ui indeterminate text loader">
{{ $t('components.common.AttachmentInput.loader.uploading') }} {{ t('components.common.AttachmentInput.loader.uploading') }}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
interface Events { interface Events {
(e: 'update:modelValue', value: boolean): void (e: 'update:modelValue', value: boolean): void
@ -9,6 +10,8 @@ interface Props {
modelValue: boolean modelValue: boolean
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = defineProps<Props>() const props = defineProps<Props>()
const value = useVModel(props, 'modelValue', emit) const value = useVModel(props, 'modelValue', emit)
@ -21,10 +24,10 @@ const value = useVModel(props, 'modelValue', emit)
@click.prevent="value = !value" @click.prevent="value = !value"
> >
<span v-if="value"> <span v-if="value">
{{ $t('components.common.CollapseLink.button.expand') }} {{ t('components.common.CollapseLink.button.expand') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.common.CollapseLink.button.collapse') }} {{ t('components.common.CollapseLink.button.collapse') }}
</span> </span>
<i :class="[{ down: !value, right: value }, 'angle', 'icon']" /> <i :class="[{ down: !value, right: value }, 'angle', 'icon']" />
</a> </a>

View File

@ -90,13 +90,13 @@ onMounted(async () => {
:class="[{active: !isPreviewing}, 'item']" :class="[{active: !isPreviewing}, 'item']"
@click.prevent="isPreviewing = false" @click.prevent="isPreviewing = false"
> >
{{ $t('components.common.ContentForm.button.write') }} {{ t('components.common.ContentForm.button.write') }}
</button> </button>
<button <button
:class="[{active: isPreviewing}, 'item']" :class="[{active: isPreviewing}, 'item']"
@click.prevent="isPreviewing = true" @click.prevent="isPreviewing = true"
> >
{{ $t('components.common.ContentForm.button.preview') }} {{ t('components.common.ContentForm.button.preview') }}
</button> </button>
</div> </div>
<template v-if="isPreviewing"> <template v-if="isPreviewing">
@ -112,7 +112,7 @@ onMounted(async () => {
</div> </div>
</div> </div>
<p v-else-if="!preview"> <p v-else-if="!preview">
{{ $t('components.common.ContentForm.empty.noContent') }} {{ t('components.common.ContentForm.empty.noContent') }}
</p> </p>
<sanitized-html <sanitized-html
v-else v-else
@ -139,7 +139,7 @@ onMounted(async () => {
{{ remainingChars }} {{ remainingChars }}
</span> </span>
<p> <p>
{{ $t('components.common.ContentForm.help.markdown') }} {{ t('components.common.ContentForm.help.markdown') }}
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { toRefs, useClipboard } from '@vueuse/core' import { toRefs, useClipboard } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
interface Props { interface Props {
value: string value: string
@ -12,6 +13,8 @@ const props = withDefaults(defineProps<Props>(), {
id: 'copy-input' id: 'copy-input'
}) })
const { t } = useI18n()
const { value } = toRefs(props) const { value } = toRefs(props)
const { copy, isSupported: canCopy, copied } = useClipboard({ source: value, copiedDuring: 5000 }) const { copy, isSupported: canCopy, copied } = useClipboard({ source: value, copiedDuring: 5000 })
</script> </script>
@ -22,7 +25,7 @@ const { copy, isSupported: canCopy, copied } = useClipboard({ source: value, cop
v-if="copied" v-if="copied"
class="message" class="message"
> >
{{ $t('components.common.CopyInput.message.success') }} {{ t('components.common.CopyInput.message.success') }}
</p> </p>
<input <input
:id="id" :id="id"
@ -37,7 +40,7 @@ const { copy, isSupported: canCopy, copied } = useClipboard({ source: value, cop
@click="copy()" @click="copy()"
> >
<i class="copy icon" /> <i class="copy icon" />
{{ $t('components.common.CopyInput.button.copy') }} {{ t('components.common.CopyInput.button.copy') }}
</button> </button>
</div> </div>
</template> </template>

View File

@ -2,6 +2,7 @@
import SemanticModal from '~/components/semantic/Modal.vue' import SemanticModal from '~/components/semantic/Modal.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import { ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
interface Events { interface Events {
(e: 'confirm'): void (e: 'confirm'): void
@ -13,6 +14,8 @@ interface Props {
confirmColor?: 'destructive' | 'primary' confirmColor?: 'destructive' | 'primary'
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
action: () => undefined, action: () => undefined,
@ -44,7 +47,7 @@ const confirm = () => {
> >
<h4 class="header"> <h4 class="header">
<slot name="modal-header"> <slot name="modal-header">
{{ $t('components.common.DangerousButton.header.confirm') }} {{ t('components.common.DangerousButton.header.confirm') }}
</slot> </slot>
</h4> </h4>
<div class="scrolling content"> <div class="scrolling content">
@ -58,14 +61,14 @@ const confirm = () => {
variant="outline" variant="outline"
@click="showModal = false" @click="showModal = false"
> >
{{ $t('components.common.DangerousButton.button.cancel') }} {{ t('components.common.DangerousButton.button.cancel') }}
</Button> </Button>
<Button <Button
:color="confirmColor" :color="confirmColor"
@click="confirm" @click="confirm"
> >
<slot name="modal-confirm"> <slot name="modal-confirm">
{{ $t('components.common.DangerousButton.button.confirm') }} {{ t('components.common.DangerousButton.button.confirm') }}
</slot> </slot>
</Button> </Button>
</div> </div>

View File

@ -1,11 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import moment from 'moment' import moment from 'moment'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
interface Props { interface Props {
seconds?: number seconds?: number
} }
const { t } = useI18n()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
seconds: 0 seconds: 0
}) })
@ -19,10 +22,10 @@ const duration = computed(() => {
<template> <template>
<span> <span>
<span v-if="duration.hours > 0"> <span v-if="duration.hours > 0">
{{ $t('components.common.Duration.meta.hours', duration) }} {{ t('components.common.Duration.meta.hours', duration) }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.common.Duration.meta.minutes', duration) }} {{ t('components.common.Duration.meta.minutes', duration) }}
</span> </span>
</span> </span>
</template> </template>

View File

@ -1,4 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'
interface Events { interface Events {
(e: 'refresh'): void (e: 'refresh'): void
} }
@ -7,6 +10,8 @@ interface Props {
refresh?: boolean refresh?: boolean
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {
refresh: false refresh: false
@ -19,7 +24,7 @@ withDefaults(defineProps<Props>(), {
<div class="content"> <div class="content">
<slot name="title"> <slot name="title">
<i class="search icon" /> <i class="search icon" />
{{ $t('components.common.EmptyState.header.noResults') }} {{ t('components.common.EmptyState.header.noResults') }}
</slot> </slot>
</div> </div>
</h4> </h4>
@ -30,7 +35,7 @@ withDefaults(defineProps<Props>(), {
class="ui button" class="ui button"
@click="emit('refresh')" @click="emit('refresh')"
> >
{{ $t('components.common.EmptyState.button.refresh') }} {{ t('components.common.EmptyState.button.refresh') }}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useToggle } from '@vueuse/core' import { useToggle } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
interface Props { interface Props {
content: string content: string
@ -11,6 +12,8 @@ const props = withDefaults(defineProps<Props>(), {
length: 150 length: 150
}) })
const { t } = useI18n()
const [expanded, toggleExpanded] = useToggle(false) const [expanded, toggleExpanded] = useToggle(false)
const truncated = computed(() => props.content.slice(0, props.length)) const truncated = computed(() => props.content.slice(0, props.length))
</script> </script>
@ -27,10 +30,10 @@ const truncated = computed(() => props.content.slice(0, props.length))
> >
<br> <br>
<span v-if="expanded"> <span v-if="expanded">
{{ $t('components.common.ExpandableDiv.button.less') }} {{ t('components.common.ExpandableDiv.button.less') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.common.ExpandableDiv.button.more') }} {{ t('components.common.ExpandableDiv.button.more') }}
</span> </span>
</a> </a>
</div> </div>

View File

@ -42,7 +42,7 @@ const search = () => {
for="search-query" for="search-query"
class="hidden" class="hidden"
> >
{{ $t('components.common.InlineSearchBar.label.search') }} {{ t('components.common.InlineSearchBar.label.search') }}
</label> </label>
<input <input
id="search-query" id="search-query"

View File

@ -5,6 +5,7 @@ import type { Cover } from '~/types'
import SemanticModal from '~/components/semantic/Modal.vue' import SemanticModal from '~/components/semantic/Modal.vue'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
interface Props { interface Props {
nextRoute: RouteLocationRaw nextRoute: RouteLocationRaw
@ -14,6 +15,8 @@ interface Props {
defineProps<Props>() defineProps<Props>()
const store = useStore()
const show = ref(false) const show = ref(false)
const { t } = useI18n() const { t } = useI18n()
@ -66,7 +69,7 @@ const labels = computed(() => ({
{{ labels.login }} {{ labels.login }}
</router-link> </router-link>
<router-link <router-link
v-if="$store.state.instance.settings.users.registration_enabled.value" v-if="store.state.instance.settings.users.registration_enabled.value"
:to="{path: '/signup'}" :to="{path: '/signup'}"
class="ui labeled icon button" class="ui labeled icon button"
> >

View File

@ -3,6 +3,7 @@ import type { BackendError } from '~/types'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { whenever } from '@vueuse/core' import { whenever } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
import clip from 'text-clipper' import clip from 'text-clipper'
@ -21,6 +22,8 @@ interface Props {
truncateLength?: number truncateLength?: number
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
content: null, content: null,
@ -91,19 +94,19 @@ const submit = async () => {
href="" href=""
@click.stop.prevent="showMore = true" @click.stop.prevent="showMore = true"
> >
{{ $t('components.common.RenderedDescription.button.more') }} {{ t('components.common.RenderedDescription.button.more') }}
</a> </a>
<a <a
v-else v-else
href="" href=""
@click.stop.prevent="showMore = false" @click.stop.prevent="showMore = false"
> >
{{ $t('components.common.RenderedDescription.button.less') }} {{ t('components.common.RenderedDescription.button.less') }}
</a> </a>
</template> </template>
</template> </template>
<p v-else-if="!isUpdating"> <p v-else-if="!isUpdating">
{{ $t('components.common.RenderedDescription.empty.noDescription') }} {{ t('components.common.RenderedDescription.empty.noDescription') }}
</p> </p>
<template v-if="!isUpdating && canUpdate && updateUrl"> <template v-if="!isUpdating && canUpdate && updateUrl">
<div class="ui hidden divider" /> <div class="ui hidden divider" />
@ -112,7 +115,7 @@ const submit = async () => {
@click="isUpdating = true" @click="isUpdating = true"
> >
<i class="pencil icon" /> <i class="pencil icon" />
{{ $t('components.common.RenderedDescription.button.edit') }} {{ t('components.common.RenderedDescription.button.edit') }}
</span> </span>
</template> </template>
<form <form
@ -126,7 +129,7 @@ const submit = async () => {
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.common.RenderedDescription.header.failure') }} {{ t('components.common.RenderedDescription.header.failure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -145,14 +148,14 @@ const submit = async () => {
class="left floated" class="left floated"
@click.prevent="isUpdating = false" @click.prevent="isUpdating = false"
> >
{{ $t('components.common.RenderedDescription.button.cancel') }} {{ t('components.common.RenderedDescription.button.cancel') }}
</a> </a>
<button <button
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
type="submit" type="submit"
:disabled="isLoading" :disabled="isLoading"
> >
{{ $t('components.common.RenderedDescription.button.update') }} {{ t('components.common.RenderedDescription.button.update') }}
</button> </button>
<div class="ui clearing hidden divider" /> <div class="ui clearing hidden divider" />
</form> </form>

View File

@ -3,12 +3,17 @@ import type { User } from '~/types'
import { hashCode, intToRGB } from '~/utils/color' import { hashCode, intToRGB } from '~/utils/color'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
interface Props { interface Props {
user: User user: User
avatar?: boolean avatar?: boolean
} }
const store = useStore()
const { t } = useI18n()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
avatar: true avatar: true
}) })
@ -22,7 +27,7 @@ const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${userColor.valu
<template v-if="avatar"> <template v-if="avatar">
<img <img
v-if="user.avatar && user.avatar.urls.medium_square_crop" v-if="user.avatar && user.avatar.urls.medium_square_crop"
v-lazy="$store.getters['instance/absoluteUrl'](user.avatar.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](user.avatar.urls.medium_square_crop)"
class="ui tiny circular avatar" class="ui tiny circular avatar"
alt="" alt=""
> >
@ -33,6 +38,6 @@ const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${userColor.valu
>{{ user.username[0] }}</span> >{{ user.username[0] }}</span>
&nbsp; &nbsp;
</template> </template>
{{ $t('components.common.UserLink.link.username', {username: user.username}) }} {{ t('components.common.UserLink.link.username', {username: user.username}) }}
</span> </span>
</template> </template>

View File

@ -2,6 +2,8 @@
import { SUPPORTED_LOCALES, setI18nLanguage } from '~/init/locale' import { SUPPORTED_LOCALES, setI18nLanguage } from '~/init/locale'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { computed } from 'vue' import { computed } from 'vue'
import { useStore } from '~/store'
import { useRoute } from 'vue-router'
import useThemeList from '~/composables/useThemeList' import useThemeList from '~/composables/useThemeList'
import useTheme from '~/composables/useTheme' import useTheme from '~/composables/useTheme'
@ -10,9 +12,13 @@ interface Events {
(e: 'show:shortcuts-modal'): void (e: 'show:shortcuts-modal'): void
} }
const route = useRoute()
const store = useStore()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const { t } = useI18n() const { t, locale } = useI18n()
const themes = useThemeList() const themes = useThemeList()
const { theme } = useTheme() const { theme } = useTheme()
@ -48,7 +54,7 @@ const labels = computed(() => ({
<a <a
v-for="(language, key) in SUPPORTED_LOCALES" v-for="(language, key) in SUPPORTED_LOCALES"
:key="key" :key="key"
:class="[{'active': $i18n.locale === key},'item']" :class="[{'active': locale === key},'item']"
:value="key" :value="key"
@click="setI18nLanguage(key)" @click="setI18nLanguage(key)"
>{{ language }}</a> >{{ language }}</a>
@ -74,27 +80,27 @@ const labels = computed(() => ({
</a> </a>
</div> </div>
</div> </div>
<template v-if="$store.state.auth.authenticated"> <template v-if="store.state.auth.authenticated">
<div class="divider" /> <div class="divider" />
<router-link <router-link
class="item" class="item"
:to="{name: 'profile.overview', params: { username: $store.state.auth.username },}" :to="{name: 'profile.overview', params: { username: store.state.auth.username },}"
> >
<i class="user icon" /> <i class="user icon" />
{{ labels.profile }} {{ labels.profile }}
</router-link> </router-link>
<router-link <router-link
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
class="item" class="item"
:to="{name: 'notifications'}" :to="{name: 'notifications'}"
> >
<i class="bell icon" /> <i class="bell icon" />
<div <div
v-if="$store.state.ui.notifications.inbox > 0" v-if="store.state.ui.notifications.inbox > 0"
:title="labels.notifications" :title="labels.notifications"
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']" :class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']"
> >
{{ $store.state.ui.notifications.inbox }} {{ store.state.ui.notifications.inbox }}
</div> </div>
{{ labels.notifications }} {{ labels.notifications }}
</router-link> </router-link>
@ -155,14 +161,14 @@ const labels = computed(() => ({
{{ labels.shortcuts }} {{ labels.shortcuts }}
</a> </a>
<router-link <router-link
v-if="$route.path != '/about'" v-if="route.path != '/about'"
class="item" class="item"
:to="{ name: 'about' }" :to="{ name: 'about' }"
> >
<i class="question circle outline icon" /> <i class="question circle outline icon" />
{{ labels.about }} {{ labels.about }}
</router-link> </router-link>
<template v-if="$store.state.auth.authenticated && $route.path != '/logout'"> <template v-if="store.state.auth.authenticated && route.path != '/logout'">
<div class="divider" /> <div class="divider" />
<router-link <router-link
class="item" class="item"
@ -173,7 +179,7 @@ const labels = computed(() => ({
{{ labels.logout }} {{ labels.logout }}
</router-link> </router-link>
</template> </template>
<template v-if="!$store.state.auth.authenticated"> <template v-if="!store.state.auth.authenticated">
<div class="divider" /> <div class="divider" />
<router-link <router-link
class="item" class="item"
@ -183,7 +189,7 @@ const labels = computed(() => ({
{{ labels.login }} {{ labels.login }}
</router-link> </router-link>
</template> </template>
<template v-if="!$store.state.auth.authenticated && $store.state.instance.settings.users.registration_enabled.value"> <template v-if="!store.state.auth.authenticated && store.state.instance.settings.users.registration_enabled.value">
<router-link <router-link
class="item" class="item"
:to="{ name: 'signup' }" :to="{ name: 'signup' }"

View File

@ -8,6 +8,8 @@ import useTheme from '~/composables/useTheme'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import { useRouter } from 'vue-router'
import { SUPPORTED_LOCALES } from '~/init/locale' import { SUPPORTED_LOCALES } from '~/init/locale'
@ -21,6 +23,9 @@ interface Props {
show: boolean show: boolean
} }
const router = useRouter()
const store = useStore()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = defineProps<Props>() const props = defineProps<Props>()
@ -62,18 +67,18 @@ const locale = computed(() => SUPPORTED_LOCALES[i18nLocale.value as SupportedLan
:fullscreen="false" :fullscreen="false"
> >
<div <div
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
class="header" class="header"
> >
<img <img
v-if="$store.state.auth.profile?.avatar && $store.state.auth.profile?.avatar.urls.medium_square_crop" v-if="store.state.auth.profile?.avatar && store.state.auth.profile?.avatar.urls.medium_square_crop"
v-lazy="$store.getters['instance/absoluteUrl']($store.state.auth.profile?.avatar.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](store.state.auth.profile?.avatar.urls.medium_square_crop)"
alt="" alt=""
class="ui centered small circular image" class="ui centered small circular image"
> >
<actor-avatar <actor-avatar
v-else v-else
:actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username,}" :actor="{preferred_username: store.state.auth.username, full_username: store.state.auth.username,}"
/> />
<h3 class="user-modal title"> <h3 class="user-modal title">
{{ labels.header }} {{ labels.header }}
@ -124,12 +129,12 @@ const locale = computed(() => SUPPORTED_LOCALES[i18nLocale.value as SupportedLan
</div> </div>
</div> </div>
<div class="ui divider" /> <div class="ui divider" />
<template v-if="$store.state.auth.authenticated"> <template v-if="store.state.auth.authenticated">
<div class="row"> <div class="row">
<div <div
class="column" class="column"
role="button" role="button"
@click.prevent.exact="$router.push({name: 'profile.overview', params: { username: $store.state.auth.username }})" @click.prevent.exact="router.push({name: 'profile.overview', params: { username: store.state.auth.username }})"
> >
<i class="user icon user-modal list-icon" /> <i class="user icon user-modal list-icon" />
<span class="user-modal list-item">{{ labels.profile }}</span> <span class="user-modal list-item">{{ labels.profile }}</span>
@ -137,7 +142,7 @@ const locale = computed(() => SUPPORTED_LOCALES[i18nLocale.value as SupportedLan
</div> </div>
<div class="row"> <div class="row">
<router-link <router-link
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
v-slot="{ navigate }" v-slot="{ navigate }"
custom custom
:to="{ name: 'notifications' }" :to="{ name: 'notifications' }"
@ -212,7 +217,7 @@ const locale = computed(() => SUPPORTED_LOCALES[i18nLocale.value as SupportedLan
<div class="ui divider" /> <div class="ui divider" />
<router-link <router-link
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
v-slot="{ navigate }" v-slot="{ navigate }"
custom custom
:to="{ name: 'logout' }" :to="{ name: 'logout' }"
@ -244,7 +249,7 @@ const locale = computed(() => SUPPORTED_LOCALES[i18nLocale.value as SupportedLan
</div> </div>
</router-link> </router-link>
<router-link <router-link
v-if="!$store.state.auth.authenticated && $store.state.instance.settings.users.registration_enabled.value" v-if="!store.state.auth.authenticated && store.state.instance.settings.users.registration_enabled.value"
v-slot="{ navigate }" v-slot="{ navigate }"
custom custom
:to="{ name: 'signup' }" :to="{ name: 'signup' }"

View File

@ -112,7 +112,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
<section class="ui vertical center aligned stripe segment"> <section class="ui vertical center aligned stripe segment">
<div :class="['ui', { 'active': isLoading }, 'inverted', 'dimmer']"> <div :class="['ui', { 'active': isLoading }, 'inverted', 'dimmer']">
<div class="ui text loader"> <div class="ui text loader">
{{ $t('components.favorites.List.loader.loading') }} {{ t('components.favorites.List.loader.loading') }}
</div> </div>
</div> </div>
<h2 <h2
@ -120,7 +120,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
class="ui center aligned icon header" class="ui center aligned icon header"
> >
<i class="circular inverted heart pink icon" /> <i class="circular inverted heart pink icon" />
{{ $t('components.favorites.List.header.favorites', $store.state.favorites.count) }} {{ t('components.favorites.List.header.favorites', $store.state.favorites.count) }}
</h2> </h2>
<radio-button <radio-button
v-if="$store.state.favorites.count > 0" v-if="$store.state.favorites.count > 0"
@ -135,7 +135,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<label for="favorites-ordering"> <label for="favorites-ordering">
{{ $t('components.favorites.List.ordering.label') }} {{ t('components.favorites.List.ordering.label') }}
</label> </label>
<select <select
id="favorites-ordering" id="favorites-ordering"
@ -153,7 +153,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
</div> </div>
<div class="field"> <div class="field">
<label for="favorites-ordering-direction"> <label for="favorites-ordering-direction">
{{ $t('components.favorites.List.ordering.direction.label') }} {{ t('components.favorites.List.ordering.direction.label') }}
</label> </label>
<select <select
id="favorites-ordering-direction" id="favorites-ordering-direction"
@ -161,16 +161,16 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
class="ui dropdown" class="ui dropdown"
> >
<option value="+"> <option value="+">
{{ $t('components.favorites.List.ordering.direction.ascending') }} {{ t('components.favorites.List.ordering.direction.ascending') }}
</option> </option>
<option value="-"> <option value="-">
{{ $t('components.favorites.List.ordering.direction.descending') }} {{ t('components.favorites.List.ordering.direction.descending') }}
</option> </option>
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label for="favorites-results"> <label for="favorites-results">
{{ $t('components.favorites.List.pagination.results') }} {{ t('components.favorites.List.pagination.results') }}
</label> </label>
<select <select
id="favorites-results" id="favorites-results"
@ -209,14 +209,14 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
> >
<div class="ui icon header"> <div class="ui icon header">
<i class="broken heart icon" /> <i class="broken heart icon" />
{{ $t('components.favorites.List.empty.noFavorites') }} {{ t('components.favorites.List.empty.noFavorites') }}
</div> </div>
<router-link <router-link
:to="'/library'" :to="'/library'"
class="ui success labeled icon button" class="ui success labeled icon button"
> >
<i class="headphones icon" /> <i class="headphones icon" />
{{ $t('components.favorites.List.link.library') }} {{ t('components.favorites.List.link.library') }}
</router-link> </router-link>
</div> </div>
</main> </main>

View File

@ -36,10 +36,10 @@ const title = computed(() => isFavorite.value
> >
<i class="heart icon" /> <i class="heart icon" />
<span v-if="isFavorite"> <span v-if="isFavorite">
{{ $t('components.favorites.TrackFavoriteIcon.label.inFavorites') }} {{ t('components.favorites.TrackFavoriteIcon.label.inFavorites') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.favorites.TrackFavoriteIcon.button.add') }} {{ t('components.favorites.TrackFavoriteIcon.button.add') }}
</span> </span>
</button> </button>
<button <button

View File

@ -6,6 +6,10 @@ import SemanticModal from '~/components/semantic/Modal.vue'
import { useTimeoutFn } from '@vueuse/core' import { useTimeoutFn } from '@vueuse/core'
import { ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
interface Events { interface Events {
(e: 'refresh'): void (e: 'refresh'): void
} }
@ -80,7 +84,7 @@ const { start: startPolling } = useTimeoutFn(poll, 1000, { immediate: false })
class="small" class="small"
> >
<h3 class="header"> <h3 class="header">
{{ $t('components.federation.FetchButton.header.refresh') }} {{ t('components.federation.FetchButton.header.refresh') }}
</h3> </h3>
<div class="scrolling content"> <div class="scrolling content">
<template v-if="data && data.status != 'pending'"> <template v-if="data && data.status != 'pending'">
@ -89,10 +93,10 @@ const { start: startPolling } = useTimeoutFn(poll, 1000, { immediate: false })
class="ui message" class="ui message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.federation.FetchButton.header.skipped') }} {{ t('components.federation.FetchButton.header.skipped') }}
</h4> </h4>
<p> <p>
{{ $t('components.federation.FetchButton.description.skipped') }} {{ t('components.federation.FetchButton.description.skipped') }}
</p> </p>
</div> </div>
<div <div
@ -100,10 +104,10 @@ const { start: startPolling } = useTimeoutFn(poll, 1000, { immediate: false })
class="ui success message" class="ui success message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.federation.FetchButton.header.success') }} {{ t('components.federation.FetchButton.header.success') }}
</h4> </h4>
<p> <p>
{{ $t('components.federation.FetchButton.description.success') }} {{ t('components.federation.FetchButton.description.success') }}
</p> </p>
</div> </div>
<div <div
@ -111,16 +115,16 @@ const { start: startPolling } = useTimeoutFn(poll, 1000, { immediate: false })
class="ui error message" class="ui error message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.federation.FetchButton.header.failure') }} {{ t('components.federation.FetchButton.header.failure') }}
</h4> </h4>
<p> <p>
{{ $t('components.federation.FetchButton.description.failure') }} {{ t('components.federation.FetchButton.description.failure') }}
</p> </p>
<table class="ui very basic collapsing celled table"> <table class="ui very basic collapsing celled table">
<tbody> <tbody>
<tr> <tr>
<td> <td>
{{ $t('components.federation.FetchButton.table.error.label.type') }} {{ t('components.federation.FetchButton.table.error.label.type') }}
</td> </td>
<td> <td>
{{ data.detail.error_code }} {{ data.detail.error_code }}
@ -128,32 +132,32 @@ const { start: startPolling } = useTimeoutFn(poll, 1000, { immediate: false })
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.federation.FetchButton.table.error.label.detail') }} {{ t('components.federation.FetchButton.table.error.label.detail') }}
</td> </td>
<td> <td>
<span v-if="data.detail.error_code === 'http' && data.detail.status_code"> <span v-if="data.detail.error_code === 'http' && data.detail.status_code">
{{ $t('components.federation.FetchButton.table.error.value.httpStatus', {status: data.detail.status_code}) }} {{ t('components.federation.FetchButton.table.error.value.httpStatus', {status: data.detail.status_code}) }}
</span> </span>
<span v-else-if="['http', 'request'].includes(data.detail.error_code)"> <span v-else-if="['http', 'request'].includes(data.detail.error_code)">
{{ $t('components.federation.FetchButton.table.error.value.httpError') }} {{ t('components.federation.FetchButton.table.error.value.httpError') }}
</span> </span>
<span v-else-if="data.detail.error_code === 'timeout'"> <span v-else-if="data.detail.error_code === 'timeout'">
{{ $t('components.federation.FetchButton.table.error.value.timeoutError') }} {{ t('components.federation.FetchButton.table.error.value.timeoutError') }}
</span> </span>
<span v-else-if="data.detail.error_code === 'connection'"> <span v-else-if="data.detail.error_code === 'connection'">
{{ $t('components.federation.FetchButton.table.error.value.connectionError') }} {{ t('components.federation.FetchButton.table.error.value.connectionError') }}
</span> </span>
<span v-else-if="['invalid_json', 'invalid_jsonld', 'missing_jsonld_type'].includes(data.detail.error_code)"> <span v-else-if="['invalid_json', 'invalid_jsonld', 'missing_jsonld_type'].includes(data.detail.error_code)">
{{ $t('components.federation.FetchButton.table.error.value.invalidJsonError') }} {{ t('components.federation.FetchButton.table.error.value.invalidJsonError') }}
</span> </span>
<span v-else-if="data.detail.error_code === 'validation'"> <span v-else-if="data.detail.error_code === 'validation'">
{{ $t('components.federation.FetchButton.table.error.value.invalidAttributesError') }} {{ t('components.federation.FetchButton.table.error.value.invalidAttributesError') }}
</span> </span>
<span v-else-if="data.detail.error_code === 'unhandled'"> <span v-else-if="data.detail.error_code === 'unhandled'">
{{ $t('components.federation.FetchButton.table.error.value.unknownError') }} {{ t('components.federation.FetchButton.table.error.value.unknownError') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.federation.FetchButton.table.error.value.unknownError') }} {{ t('components.federation.FetchButton.table.error.value.unknownError') }}
</span> </span>
</td> </td>
</tr> </tr>
@ -166,7 +170,7 @@ const { start: startPolling } = useTimeoutFn(poll, 1000, { immediate: false })
class="ui active inverted dimmer" class="ui active inverted dimmer"
> >
<div class="ui text loader"> <div class="ui text loader">
{{ $t('components.federation.FetchButton.loader.fetchRequest') }} {{ t('components.federation.FetchButton.loader.fetchRequest') }}
</div> </div>
</div> </div>
<div <div
@ -174,7 +178,7 @@ const { start: startPolling } = useTimeoutFn(poll, 1000, { immediate: false })
class="ui active inverted dimmer" class="ui active inverted dimmer"
> >
<div class="ui text loader"> <div class="ui text loader">
{{ $t('components.federation.FetchButton.loader.awaitingResult') }} {{ t('components.federation.FetchButton.loader.awaitingResult') }}
</div> </div>
</div> </div>
<div <div
@ -183,7 +187,7 @@ const { start: startPolling } = useTimeoutFn(poll, 1000, { immediate: false })
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.federation.FetchButton.header.saveFailure') }} {{ t('components.federation.FetchButton.header.saveFailure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -200,23 +204,23 @@ const { start: startPolling } = useTimeoutFn(poll, 1000, { immediate: false })
class="ui warning message" class="ui warning message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.federation.FetchButton.header.pending') }} {{ t('components.federation.FetchButton.header.pending') }}
</h4> </h4>
<p> <p>
{{ $t('components.federation.FetchButton.description.pending') }} {{ t('components.federation.FetchButton.description.pending') }}
</p> </p>
</div> </div>
</div> </div>
<div class="actions"> <div class="actions">
<button class="ui basic cancel button"> <button class="ui basic cancel button">
{{ $t('components.federation.FetchButton.button.close') }} {{ t('components.federation.FetchButton.button.close') }}
</button> </button>
<button <button
v-if="data && data.status === 'finished'" v-if="data && data.status === 'finished'"
class="ui confirm success button" class="ui confirm success button"
@click.prevent="showModal = false; emit('refresh')" @click.prevent="showModal = false; emit('refresh')"
> >
{{ $t('components.federation.FetchButton.button.reload') }} {{ t('components.federation.FetchButton.button.reload') }}
</button> </button>
</div> </div>
</semantic-modal> </semantic-modal>

View File

@ -2,6 +2,8 @@
import type { Library } from '~/types' import type { Library } from '~/types'
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
@ -17,6 +19,9 @@ interface Props {
url: string url: string
} }
const { t } = useI18n()
const store = useStore()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = defineProps<Props>() const props = defineProps<Props>()
@ -64,7 +69,7 @@ fetchData()
v-if="!isLoading && libraries.length === 0" v-if="!isLoading && libraries.length === 0"
class="ui subtitle" class="ui subtitle"
> >
{{ $t('components.federation.LibraryWidget.empty.noMatch') }} {{ t('components.federation.LibraryWidget.empty.noMatch') }}
</p> </p>
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<div class="ui cards"> <div class="ui cards">
@ -78,7 +83,7 @@ fetchData()
v-for="library in libraries" v-for="library in libraries"
:key="library.uuid" :key="library.uuid"
:display-scan="false" :display-scan="false"
:display-follow="$store.state.auth.authenticated && library.actor.full_username != $store.state.auth.fullUsername" :display-follow="store.state.auth.authenticated && library.actor.full_username != store.state.auth.fullUsername"
:initial-library="library" :initial-library="library"
:display-copy-fid="true" :display-copy-fid="true"
/> />
@ -90,7 +95,7 @@ fetchData()
:class="['ui', 'basic', 'button']" :class="['ui', 'basic', 'button']"
@click="fetchData(nextPage)" @click="fetchData(nextPage)"
> >
{{ $t('components.federation.LibraryWidget.button.showMore') }} {{ t('components.federation.LibraryWidget.button.showMore') }}
</button> </button>
</template> </template>
</div> </div>

View File

@ -4,8 +4,9 @@ import type { Track, Album, Artist, Library, ArtistCredit } from '~/types'
import { momentFormat } from '~/utils/filters' import { momentFormat } from '~/utils/filters'
import { computed, reactive, ref, watch } from 'vue' import { computed, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { sum } from 'lodash-es' import { sum } from 'lodash-es'
import { useStore } from '~/store'
import axios from 'axios' import axios from 'axios'
@ -25,6 +26,8 @@ interface Props {
id: number id: number
} }
const store = useStore()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = defineProps<Props>() const props = defineProps<Props>()
@ -116,6 +119,8 @@ const fetchTracks = async () => {
watch(() => props.id, fetchData, { immediate: true }) watch(() => props.id, fetchData, { immediate: true })
const router = useRouter() const router = useRouter()
const route = useRoute()
const remove = async () => { const remove = async () => {
isLoading.value = true isLoading.value = true
try { try {
@ -156,7 +161,7 @@ const remove = async () => {
<div class="large two-images"> <div class="large two-images">
<img <img
v-if="object.cover && object.cover.urls.original" v-if="object.cover && object.cover.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)"
alt="" alt=""
class="channel-image" class="channel-image"
> >
@ -168,7 +173,7 @@ const remove = async () => {
> >
<img <img
v-if="object.cover && object.cover.urls.original" v-if="object.cover && object.cover.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)"
alt="" alt=""
class="channel-image" class="channel-image"
> >
@ -193,10 +198,10 @@ const remove = async () => {
<template v-if="totalTracks > 0"> <template v-if="totalTracks > 0">
<div class="ui hidden very small divider" /> <div class="ui hidden very small divider" />
<span v-if="isSerie"> <span v-if="isSerie">
{{ $t('components.library.AlbumBase.meta.episodes', totalTracks) }} {{ t('components.library.AlbumBase.meta.episodes', totalTracks) }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.library.AlbumBase.meta.tracks', totalTracks) }} {{ t('components.library.AlbumBase.meta.tracks', totalTracks) }}
</span> </span>
</template> </template>
<div class="ui small hidden divider" /> <div class="ui small hidden divider" />
@ -238,7 +243,7 @@ const remove = async () => {
> >
<img <img
v-if="object.cover && object.cover.urls.original" v-if="object.cover && object.cover.urls.original"
v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)"
alt="" alt=""
class="channel-image" class="channel-image"
> >
@ -271,10 +276,10 @@ const remove = async () => {
</template> </template>
<template v-if="totalTracks > 0"> <template v-if="totalTracks > 0">
<span v-if="isSerie"> <span v-if="isSerie">
{{ $t('components.library.AlbumBase.meta.episodes', totalTracks) }} {{ t('components.library.AlbumBase.meta.episodes', totalTracks) }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.library.AlbumBase.meta.tracks', totalTracks) }} {{ t('components.library.AlbumBase.meta.tracks', totalTracks) }}
</span> </span>
<span class="middle middledot symbol" /> <span class="middle middledot symbol" />
</template> </template>
@ -299,7 +304,7 @@ const remove = async () => {
:artist-credit="artistCredit" :artist-credit="artistCredit"
@remove="remove" @remove="remove"
/> />
<div v-if="(object.tags && object.tags.length > 0) || object.description || $store.state.auth.authenticated && object.is_local"> <div v-if="(object.tags && object.tags.length > 0) || object.description || store.state.auth.authenticated && object.is_local">
<div class="ui small hidden divider" /> <div class="ui small hidden divider" />
<div class="ui divider" /> <div class="ui divider" />
<div class="ui small hidden divider" /> <div class="ui small hidden divider" />
@ -313,11 +318,11 @@ const remove = async () => {
:can-update="false" :can-update="false"
/> />
<router-link <router-link
v-else-if="$store.state.auth.authenticated && object.is_local" v-else-if="store.state.auth.authenticated && object.is_local"
:to="{name: 'library.albums.edit', params: {id: object.id }}" :to="{name: 'library.albums.edit', params: {id: object.id }}"
> >
<i class="pencil icon" /> <i class="pencil icon" />
{{ $t('components.library.AlbumBase.link.addDescription') }} {{ t('components.library.AlbumBase.link.addDescription') }}
</router-link> </router-link>
</div> </div>
</div> </div>
@ -329,18 +334,18 @@ const remove = async () => {
:can-update="false" :can-update="false"
/> />
<router-link <router-link
v-else-if="$store.state.auth.authenticated && object.is_local" v-else-if="store.state.auth.authenticated && object.is_local"
:to="{name: 'library.albums.edit', params: {id: object.id }}" :to="{name: 'library.albums.edit', params: {id: object.id }}"
> >
<i class="pencil icon" /> <i class="pencil icon" />
{{ $t('components.library.AlbumBase.link.addDescription') }} {{ t('components.library.AlbumBase.link.addDescription') }}
</router-link> </router-link>
</template> </template>
</div> </div>
<div class="nine wide column"> <div class="nine wide column">
<router-view <router-view
v-if="object" v-if="object"
:key="$route.fullPath" :key="route.fullPath"
:paginate-by="paginateBy" :paginate-by="paginateBy"
:total-tracks="totalTracks" :total-tracks="totalTracks"
:is-serie="isSerie" :is-serie="isSerie"

View File

@ -7,6 +7,7 @@ import TrackTable from '~/components/audio/track/Table.vue'
import PlayButton from '~/components/audio/PlayButton.vue' import PlayButton from '~/components/audio/PlayButton.vue'
import Pagination from '~/components/vui/Pagination.vue' import Pagination from '~/components/vui/Pagination.vue'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
interface Events { interface Events {
(e: 'libraries-loaded', libraries: Library[]): void (e: 'libraries-loaded', libraries: Library[]): void
@ -24,6 +25,7 @@ interface Props {
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = defineProps<Props>() const props = defineProps<Props>()
const { t } = useI18n()
const getDiscKey = (disc: Track[]) => disc?.map(track => track.id).join('|') ?? '' const getDiscKey = (disc: Track[]) => disc?.map(track => track.id).join('|') ?? ''
const page = ref(1) const page = ref(1)
@ -58,10 +60,10 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
> >
<h2 class="ui header"> <h2 class="ui header">
<span v-if="isSerie"> <span v-if="isSerie">
{{ $t('components.library.AlbumDetail.header.episodes') }} {{ t('components.library.AlbumDetail.header.episodes') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.library.AlbumDetail.header.tracks') }} {{ t('components.library.AlbumDetail.header.tracks') }}
</span> </span>
</h2> </h2>
@ -85,7 +87,7 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
:tracks="discs[index]" :tracks="discs[index]"
/> />
<h3> <h3>
{{ $t('components.library.AlbumDetail.meta.volume', {number: tracks[0].disc_number}) }} {{ t('components.library.AlbumDetail.meta.volume', {number: tracks[0].disc_number}) }}
</h3> </h3>
<track-table <track-table
:is-album="true" :is-album="true"
@ -125,13 +127,13 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
<template v-if="artistCredit && !artistCredit[0]?.artist.channel && !isSerie"> <template v-if="artistCredit && !artistCredit[0]?.artist.channel && !isSerie">
<h2> <h2>
{{ $t('components.library.AlbumDetail.header.libraries') }} {{ t('components.library.AlbumDetail.header.libraries') }}
</h2> </h2>
<library-widget <library-widget
:url="'albums/' + object.id + '/libraries/'" :url="'albums/' + object.id + '/libraries/'"
@loaded="emit('libraries-loaded', $event)" @loaded="emit('libraries-loaded', $event)"
> >
{{ $t('components.library.AlbumDetail.description.libraries') }} {{ t('components.library.AlbumDetail.description.libraries') }}
</library-widget> </library-widget>
</template> </template>
</div> </div>

View File

@ -3,6 +3,7 @@ import type { Album, ArtistCredit, Library } from '~/types'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import { getDomain } from '~/utils' import { getDomain } from '~/utils'
@ -29,6 +30,7 @@ interface Props {
isSerie: boolean isSerie: boolean
} }
const store = useStore()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = defineProps<Props>() const props = defineProps<Props>()
const { report, getReportableObjects } = useReport() const { report, getReportableObjects } = useReport()
@ -59,7 +61,7 @@ const open = ref(false)
v-model:show="showEmbedModal" v-model:show="showEmbedModal"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.library.AlbumDropdown.modal.embed.header') }} {{ t('components.library.AlbumDropdown.modal.embed.header') }}
</h4> </h4>
<div class="scrolling content"> <div class="scrolling content">
<div class="description"> <div class="description">
@ -76,7 +78,7 @@ const open = ref(false)
variant="outline" variant="outline"
@click="showEmbedModal = false" @click="showEmbedModal = false"
> >
{{ $t('components.library.AlbumDropdown.button.cancel') }} {{ t('components.library.AlbumDropdown.button.cancel') }}
</Button> </Button>
</div> </div>
</semantic-modal> </semantic-modal>
@ -94,12 +96,12 @@ const open = ref(false)
<template #items> <template #items>
<PopoverItem <PopoverItem
v-if="domain != $store.getters['instance/domain']" v-if="domain != store.getters['instance/domain']"
:href="object.fid" :href="object.fid"
target="_blank" target="_blank"
> >
<i class="bi bi-box-arrow-up-right" /> <i class="bi bi-box-arrow-up-right" />
{{ $t('components.library.AlbumDropdown.link.domain') }} {{ t('components.library.AlbumDropdown.link.domain') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
@ -107,7 +109,7 @@ const open = ref(false)
@click="showEmbedModal = !showEmbedModal" @click="showEmbedModal = !showEmbedModal"
> >
<i class="bi bi-code" /> <i class="bi bi-code" />
{{ $t('components.library.AlbumDropdown.button.embed') }} {{ t('components.library.AlbumDropdown.button.embed') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
@ -117,7 +119,7 @@ const open = ref(false)
rel="noreferrer noopener" rel="noreferrer noopener"
> >
<i class="bi bi-box-arrow-up-right" /> <i class="bi bi-box-arrow-up-right" />
{{ $t('components.library.AlbumDropdown.link.musicbrainz') }} {{ t('components.library.AlbumDropdown.link.musicbrainz') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
@ -127,7 +129,7 @@ const open = ref(false)
rel="noreferrer noopener" rel="noreferrer noopener"
> >
<i class="bi bi-box-arrow-up-right" /> <i class="bi bi-box-arrow-up-right" />
{{ $t('components.library.AlbumDropdown.link.discogs') }} {{ t('components.library.AlbumDropdown.link.discogs') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
@ -135,18 +137,18 @@ const open = ref(false)
:to="{name: 'library.albums.edit', params: {id: object.id }}" :to="{name: 'library.albums.edit', params: {id: object.id }}"
> >
<i class="bi bi-pencil" /> <i class="bi bi-pencil" />
{{ $t('components.library.AlbumDropdown.button.edit') }} {{ t('components.library.AlbumDropdown.button.edit') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
v-if="artistCredit[0] && $store.state.auth.authenticated && artistCredit[0].artist.channel && artistCredit[0].artist.attributed_to?.full_username === $store.state.auth.fullUsername" v-if="artistCredit[0] && store.state.auth.authenticated && artistCredit[0].artist.channel && artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername"
> >
<DangerousButton <DangerousButton
:is-loading="isLoading" :is-loading="isLoading"
@confirm="remove()" @confirm="remove()"
> >
<i class="bi bi-trash" /> <i class="bi bi-trash" />
{{ $t('components.library.AlbumDropdown.button.delete') }} {{ t('components.library.AlbumDropdown.button.delete') }}
</DangerousButton> </DangerousButton>
</PopoverItem> </PopoverItem>
@ -164,21 +166,21 @@ const open = ref(false)
<hr> <hr>
<PopoverItem <PopoverItem
v-if="$store.state.auth.availablePermissions['library']" v-if="store.state.auth.availablePermissions['library']"
:to="{name: 'manage.library.albums.detail', params: {id: object.id}}" :to="{name: 'manage.library.albums.detail', params: {id: object.id}}"
> >
<i class="bi bi-wrench" /> <i class="bi bi-wrench" />
{{ $t('components.library.AlbumDropdown.link.moderation') }} {{ t('components.library.AlbumDropdown.link.moderation') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
v-if="$store.state.auth.profile && $store.state.auth.profile?.is_superuser" v-if="store.state.auth.profile && store.state.auth.profile?.is_superuser"
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/album/${object.id}`)" :href="store.getters['instance/absoluteUrl'](`/api/admin/music/album/${object.id}`)"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<i class="bi bi-wrench" /> <i class="bi bi-wrench" />
{{ $t('components.library.AlbumDropdown.link.django') }} {{ t('components.library.AlbumDropdown.link.django') }}
</PopoverItem> </PopoverItem>
</template> </template>
</Popover> </Popover>

View File

@ -3,6 +3,7 @@ import type { EditObjectType } from '~/composables/moderation/useEditConfigs'
import type { Album, Library, Actor } from '~/types' import type { Album, Library, Actor } from '~/types'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import EditForm from '~/components/library/EditForm.vue' import EditForm from '~/components/library/EditForm.vue'
@ -14,6 +15,7 @@ interface Props {
defineProps<Props>() defineProps<Props>()
const { t } = useI18n()
const store = useStore() const store = useStore()
const canEdit = store.state.auth.availablePermissions.library const canEdit = store.state.auth.availablePermissions.library
</script> </script>
@ -23,17 +25,17 @@ const canEdit = store.state.auth.availablePermissions.library
<div class="ui text container"> <div class="ui text container">
<h2> <h2>
<span v-if="canEdit"> <span v-if="canEdit">
{{ $t('components.library.AlbumEdit.header.edit') }} {{ t('components.library.AlbumEdit.header.edit') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.library.AlbumEdit.header.suggest') }} {{ t('components.library.AlbumEdit.header.suggest') }}
</span> </span>
</h2> </h2>
<div <div
v-if="!object.is_local" v-if="!object.is_local"
class="ui message" class="ui message"
> >
{{ $t('components.library.AlbumEdit.message.remote') }} {{ t('components.library.AlbumEdit.message.remote') }}
</div> </div>
<edit-form <edit-form
v-else v-else

View File

@ -122,7 +122,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
<section class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
/front/src/components/library/Albums.vue /front/src/components/library/Albums.vue
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.library.Albums.header.browse') }} {{ t('components.library.Albums.header.browse') }}
</h2> </h2>
<form <form
:class="['ui', {'loading': isLoading}, 'form']" :class="['ui', {'loading': isLoading}, 'form']"
@ -131,7 +131,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<label for="albums-search"> <label for="albums-search">
{{ $t('components.library.Albums.label.search') }} {{ t('components.library.Albums.label.search') }}
</label> </label>
<div class="ui action input"> <div class="ui action input">
<input <input
@ -151,11 +151,11 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label for="tags-search">{{ $t('components.library.Albums.label.tags') }}</label> <label for="tags-search">{{ t('components.library.Albums.label.tags') }}</label>
<tags-selector v-model="tags" /> <tags-selector v-model="tags" />
</div> </div>
<div class="field"> <div class="field">
<label for="album-ordering">{{ $t('components.library.Albums.ordering.label') }}</label> <label for="album-ordering">{{ t('components.library.Albums.ordering.label') }}</label>
<select <select
id="album-ordering" id="album-ordering"
v-model="ordering" v-model="ordering"
@ -171,22 +171,22 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label for="album-ordering-direction">{{ $t('components.library.Albums.ordering.direction.label') }}</label> <label for="album-ordering-direction">{{ t('components.library.Albums.ordering.direction.label') }}</label>
<select <select
id="album-ordering-direction" id="album-ordering-direction"
v-model="orderingDirection" v-model="orderingDirection"
class="ui dropdown" class="ui dropdown"
> >
<option value="+"> <option value="+">
{{ $t('components.library.Albums.ordering.direction.ascending') }} {{ t('components.library.Albums.ordering.direction.ascending') }}
</option> </option>
<option value="-"> <option value="-">
{{ $t('components.library.Albums.ordering.direction.descending') }} {{ t('components.library.Albums.ordering.direction.descending') }}
</option> </option>
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label for="album-results">{{ $t('components.library.Albums.pagination.results') }}</label> <label for="album-results">{{ t('components.library.Albums.pagination.results') }}</label>
<select <select
id="album-results" id="album-results"
v-model="paginateBy" v-model="paginateBy"
@ -228,7 +228,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
> >
<div class="ui icon header"> <div class="ui icon header">
<i class="compact disc icon" /> <i class="compact disc icon" />
{{ $t('components.library.Albums.empty.noResults') }} {{ t('components.library.Albums.empty.noResults') }}
</div> </div>
<router-link <router-link
v-if="$store.state.auth.authenticated" v-if="$store.state.auth.authenticated"
@ -236,7 +236,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
class="ui success button labeled icon" class="ui success button labeled icon"
> >
<i class="upload icon" /> <i class="upload icon" />
{{ $t('components.library.Albums.link.addMusic') }} {{ t('components.library.Albums.link.addMusic') }}
</router-link> </router-link>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@ import type { Track, Album, Artist, Library } from '~/types'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { getDomain } from '~/utils' import { getDomain } from '~/utils'
import { useStore } from '~/store' import { useStore } from '~/store'
@ -41,6 +41,7 @@ const totalTracks = ref(0)
const logger = useLogger() const logger = useLogger()
const store = useStore() const store = useStore()
const router = useRouter() const router = useRouter()
const route = useRoute()
const domain = computed(() => getDomain(object.value?.fid ?? '')) const domain = computed(() => getDomain(object.value?.fid ?? ''))
const isPlayable = computed(() => !!object.value?.albums.some(album => album.is_playable)) const isPlayable = computed(() => !!object.value?.albums.some(album => album.is_playable))
@ -117,8 +118,8 @@ watch(() => props.id, fetchData, { immediate: true })
v-if="albums" v-if="albums"
class="sub header" class="sub header"
> >
{{ $t('components.library.ArtistBase.meta.tracks', totalTracks) }} {{ t('components.library.ArtistBase.meta.tracks', totalTracks) }}
{{ $t('components.library.ArtistBase.meta.albums', totalAlbums) }} {{ t('components.library.ArtistBase.meta.albums', totalAlbums) }}
</div> </div>
</div> </div>
</h2> </h2>
@ -140,7 +141,7 @@ watch(() => props.id, fetchData, { immediate: true })
class="vibrant" class="vibrant"
:artist="object" :artist="object"
> >
{{ $t('components.library.ArtistBase.button.play') }} {{ t('components.library.ArtistBase.button.play') }}
</play-button> </play-button>
</div> </div>
@ -149,7 +150,7 @@ watch(() => props.id, fetchData, { immediate: true })
v-model:show="showEmbedModal" v-model:show="showEmbedModal"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.library.ArtistBase.modal.embed.header') }} {{ t('components.library.ArtistBase.modal.embed.header') }}
</h4> </h4>
<div class="scrolling content"> <div class="scrolling content">
<div class="description"> <div class="description">
@ -161,7 +162,7 @@ watch(() => props.id, fetchData, { immediate: true })
</div> </div>
<div class="actions"> <div class="actions">
<button class="ui deny button"> <button class="ui deny button">
{{ $t('components.library.ArtistBase.button.cancel') }} {{ t('components.library.ArtistBase.button.cancel') }}
</button> </button>
</div> </div>
</semantic-modal> </semantic-modal>
@ -172,20 +173,20 @@ watch(() => props.id, fetchData, { immediate: true })
class="ui button" class="ui button"
@click="toggleOpen" @click="toggleOpen"
> >
{{ $t('components.library.ArtistBase.button.more') }} {{ t('components.library.ArtistBase.button.more') }}
<i class="dropdown icon" /> <i class="dropdown icon" />
</button> </button>
</template> </template>
<template #items> <template #items>
<PopoverItem <PopoverItem
v-if="domain != $store.getters['instance/domain']" v-if="domain != store.getters['instance/domain']"
:href="object.fid" :href="object.fid"
target="_blank" target="_blank"
class="funkwhale item" class="funkwhale item"
> >
<i class="external icon" /> <i class="external icon" />
{{ $t('components.library.ArtistBase.link.domain', {domain: domain}) }} {{ t('components.library.ArtistBase.link.domain', {domain: domain}) }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
@ -193,7 +194,7 @@ watch(() => props.id, fetchData, { immediate: true })
@click="showEmbedModal = !showEmbedModal" @click="showEmbedModal = !showEmbedModal"
> >
<i class="code icon" /> <i class="code icon" />
{{ $t('components.library.ArtistBase.button.embed') }} {{ t('components.library.ArtistBase.button.embed') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
@ -202,7 +203,7 @@ watch(() => props.id, fetchData, { immediate: true })
rel="noreferrer noopener" rel="noreferrer noopener"
> >
<i class="wikipedia w icon" /> <i class="wikipedia w icon" />
{{ $t('components.library.ArtistBase.link.wikipedia') }} {{ t('components.library.ArtistBase.link.wikipedia') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
@ -212,7 +213,7 @@ watch(() => props.id, fetchData, { immediate: true })
rel="noreferrer noopener" rel="noreferrer noopener"
> >
<i class="external icon" /> <i class="external icon" />
{{ $t('components.library.ArtistBase.link.musicbrainz') }} {{ t('components.library.ArtistBase.link.musicbrainz') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
@ -221,7 +222,7 @@ watch(() => props.id, fetchData, { immediate: true })
rel="noreferrer noopener" rel="noreferrer noopener"
> >
<i class="external icon" /> <i class="external icon" />
{{ $t('components.library.ArtistBase.link.discogs') }} {{ t('components.library.ArtistBase.link.discogs') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
@ -229,7 +230,7 @@ watch(() => props.id, fetchData, { immediate: true })
:to="{name: 'library.artists.edit', params: {id: object.id }}" :to="{name: 'library.artists.edit', params: {id: object.id }}"
> >
<i class="edit icon" /> <i class="edit icon" />
{{ $t('components.library.ArtistBase.button.edit') }} {{ t('components.library.ArtistBase.button.edit') }}
</PopoverItem> </PopoverItem>
<hr> <hr>
@ -245,21 +246,21 @@ watch(() => props.id, fetchData, { immediate: true })
<hr> <hr>
<PopoverItem <PopoverItem
v-if="$store.state.auth.availablePermissions['library']" v-if="store.state.auth.availablePermissions['library']"
:to="{name: 'manage.library.artists.detail', params: {id: object.id}}" :to="{name: 'manage.library.artists.detail', params: {id: object.id}}"
> >
<i class="wrench icon" /> <i class="wrench icon" />
{{ $t('components.library.ArtistBase.link.moderation') }} {{ t('components.library.ArtistBase.link.moderation') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" v-if="store.state.auth.profile && store.state.auth.profile.is_superuser"
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)" :href="store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<i class="wrench icon" /> <i class="wrench icon" />
{{ $t('components.library.ArtistBase.link.django') }} {{ t('components.library.ArtistBase.link.django') }}
</PopoverItem> </PopoverItem>
</template> </template>
</Popover> </Popover>
@ -267,7 +268,7 @@ watch(() => props.id, fetchData, { immediate: true })
</div> </div>
</section> </section>
<router-view <router-view
:key="$route.fullPath" :key="route.fullPath"
:tracks="tracks" :tracks="tracks"
:next-tracks-url="nextTracksUrl" :next-tracks-url="nextTracksUrl"
:next-albums-url="nextAlbumsUrl" :next-albums-url="nextAlbumsUrl"

View File

@ -4,6 +4,8 @@ import type { ContentFilter } from '~/store/moderation'
import { ref, computed, reactive } from 'vue' import { ref, computed, reactive } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
@ -26,6 +28,8 @@ interface Props {
nextAlbumsUrl?: string | null nextAlbumsUrl?: string | null
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
nextTracksUrl: null, nextTracksUrl: null,
@ -65,19 +69,19 @@ const loadMoreAlbums = async () => {
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<div class="ui message"> <div class="ui message">
<p> <p>
{{ $t('components.library.ArtistDetail.message.filter') }} {{ t('components.library.ArtistDetail.message.filter') }}
</p> </p>
<router-link <router-link
class="right floated" class="right floated"
:to="{name: 'settings'}" :to="{name: 'settings'}"
> >
{{ $t('components.library.ArtistDetail.link.filter') }} {{ t('components.library.ArtistDetail.link.filter') }}
</router-link> </router-link>
<button <button
class="ui basic tiny button" class="ui basic tiny button"
@click="$store.dispatch('moderation/deleteContentFilter', contentFilter.uuid)" @click="store.dispatch('moderation/deleteContentFilter', contentFilter.uuid)"
> >
{{ $t('components.library.ArtistDetail.button.filter') }} {{ t('components.library.ArtistDetail.button.filter') }}
</button> </button>
</div> </div>
</div> </div>
@ -92,7 +96,7 @@ const loadMoreAlbums = async () => {
class="ui vertical stripe segment" class="ui vertical stripe segment"
> >
<h2> <h2>
{{ $t('components.library.ArtistDetail.header.album') }} {{ t('components.library.ArtistDetail.header.album') }}
</h2> </h2>
<div class="ui cards app-cards"> <div class="ui cards app-cards">
<album-card <album-card
@ -107,7 +111,7 @@ const loadMoreAlbums = async () => {
:class="['ui', {loading: isLoadingMoreAlbums}, 'button']" :class="['ui', {loading: isLoadingMoreAlbums}, 'button']"
@click="loadMoreAlbums()" @click="loadMoreAlbums()"
> >
{{ $t('components.library.ArtistDetail.button.more') }} {{ t('components.library.ArtistDetail.button.more') }}
</button> </button>
</section> </section>
<section <section
@ -122,7 +126,7 @@ const loadMoreAlbums = async () => {
> >
<template #header> <template #header>
<h2> <h2>
{{ $t('components.library.ArtistDetail.header.track') }} {{ t('components.library.ArtistDetail.header.track') }}
</h2> </h2>
<div class="ui hidden divider" /> <div class="ui hidden divider" />
</template> </template>
@ -130,13 +134,13 @@ const loadMoreAlbums = async () => {
</section> </section>
<section class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2> <h2>
{{ $t('components.library.ArtistDetail.header.library') }} {{ t('components.library.ArtistDetail.header.library') }}
</h2> </h2>
<library-widget <library-widget
:url="'artists/' + object.id + '/libraries/'" :url="'artists/' + object.id + '/libraries/'"
@loaded="emit('libraries-loaded', $event)" @loaded="emit('libraries-loaded', $event)"
> >
{{ $t('components.library.ArtistDetail.description.library') }} {{ t('components.library.ArtistDetail.description.library') }}
</library-widget> </library-widget>
</section> </section>
</div> </div>

View File

@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { EditObjectType } from '~/composables/moderation/useEditConfigs' import type { EditObjectType } from '~/composables/moderation/useEditConfigs'
import type { Artist, Library } from '~/types' import type { Artist, Library } from '~/types'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'
@ -14,7 +16,9 @@ interface Props {
defineProps<Props>() defineProps<Props>()
const { t } = useI18n()
const store = useStore() const store = useStore()
const canEdit = store.state.auth.availablePermissions.library const canEdit = store.state.auth.availablePermissions.library
</script> </script>
@ -23,17 +27,17 @@ const canEdit = store.state.auth.availablePermissions.library
<div class="ui text container"> <div class="ui text container">
<h2> <h2>
<span v-if="canEdit"> <span v-if="canEdit">
{{ $t('components.library.ArtistEdit.header.edit') }} {{ t('components.library.ArtistEdit.header.edit') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.library.ArtistEdit.header.suggest') }} {{ t('components.library.ArtistEdit.header.suggest') }}
</span> </span>
</h2> </h2>
<div <div
v-if="!object.is_local" v-if="!object.is_local"
class="ui message" class="ui message"
> >
{{ $t('components.library.ArtistEdit.message.remote') }} {{ t('components.library.ArtistEdit.message.remote') }}
</div> </div>
<edit-form <edit-form
v-else v-else

View File

@ -123,7 +123,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
<section class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
/front/src/components/library/Artists.vue /front/src/components/library/Artists.vue
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.library.Artists.header.browse') }} {{ t('components.library.Artists.header.browse') }}
</h2> </h2>
<form <form
:class="['ui', {'loading': isLoading}, 'form']" :class="['ui', {'loading': isLoading}, 'form']"
@ -132,7 +132,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<label for="artist-search"> <label for="artist-search">
{{ $t('components.library.Artists.label.search') }} {{ t('components.library.Artists.label.search') }}
</label> </label>
<div class="ui action input"> <div class="ui action input">
<input <input
@ -152,11 +152,11 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label for="tags-search">{{ $t('components.library.Artists.label.tags') }}</label> <label for="tags-search">{{ t('components.library.Artists.label.tags') }}</label>
<tags-selector v-model="tags" /> <tags-selector v-model="tags" />
</div> </div>
<div class="field"> <div class="field">
<label for="artist-ordering">{{ $t('components.library.Artists.ordering.label') }}</label> <label for="artist-ordering">{{ t('components.library.Artists.ordering.label') }}</label>
<select <select
id="artist-ordering" id="artist-ordering"
v-model="ordering" v-model="ordering"
@ -172,22 +172,22 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label for="artist-ordering-direction">{{ $t('components.library.Artists.ordering.direction.label') }}</label> <label for="artist-ordering-direction">{{ t('components.library.Artists.ordering.direction.label') }}</label>
<select <select
id="artist-ordering-direction" id="artist-ordering-direction"
v-model="orderingDirection" v-model="orderingDirection"
class="ui dropdown" class="ui dropdown"
> >
<option value="+"> <option value="+">
{{ $t('components.library.Artists.ordering.direction.ascending') }} {{ t('components.library.Artists.ordering.direction.ascending') }}
</option> </option>
<option value="-"> <option value="-">
{{ $t('components.library.Artists.ordering.direction.descending') }} {{ t('components.library.Artists.ordering.direction.descending') }}
</option> </option>
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label for="artist-results">{{ $t('components.library.Artists.pagination.results') }}</label> <label for="artist-results">{{ t('components.library.Artists.pagination.results') }}</label>
<select <select
id="artist-results" id="artist-results"
v-model="paginateBy" v-model="paginateBy"
@ -203,7 +203,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<span id="excludeHeader">{{ $t('components.library.Artists.label.excludeCompilation') }}</span> <span id="excludeHeader">{{ t('components.library.Artists.label.excludeCompilation') }}</span>
<div <div
id="excludeCompilation" id="excludeCompilation"
class="ui toggle checkbox" class="ui toggle checkbox"
@ -218,7 +218,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
<label <label
for="exclude-compilation" for="exclude-compilation"
class="visually-hidden" class="visually-hidden"
>{{ $t('components.library.Artists.label.excludeCompilation') }}</label> >{{ t('components.library.Artists.label.excludeCompilation') }}</label>
</div> </div>
</div> </div>
</div> </div>
@ -247,7 +247,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
> >
<div class="ui icon header"> <div class="ui icon header">
<i class="compact disc icon" /> <i class="compact disc icon" />
{{ $t('components.library.Artists.empty.noResults') }} {{ t('components.library.Artists.empty.noResults') }}
</div> </div>
<router-link <router-link
v-if="$store.state.auth.authenticated" v-if="$store.state.auth.authenticated"
@ -255,7 +255,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
class="ui success button labeled icon" class="ui success button labeled icon"
> >
<i class="upload icon" /> <i class="upload icon" />
{{ $t('components.library.Artists.button.upload') }} {{ t('components.library.Artists.button.upload') }}
</router-link> </router-link>
</div> </div>
<div class="ui center aligned basic segment"> <div class="ui center aligned basic segment">

View File

@ -7,6 +7,8 @@ import { diffWordsWithSpace } from 'diff'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
@ -23,6 +25,8 @@ interface Props {
currentState?: ReviewState currentState?: ReviewState
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
currentState: () => ({}) currentState: () => ({})
@ -155,7 +159,7 @@ const approve = async (approved: boolean) => {
<div class="content"> <div class="content">
<h4 class="header"> <h4 class="header">
<router-link :to="detailUrl"> <router-link :to="detailUrl">
{{ $t('components.library.EditCard.header.modification', {id: obj.uuid.substring(0, 8)}) }} {{ t('components.library.EditCard.header.modification', {id: obj.uuid.substring(0, 8)}) }}
</router-link> </router-link>
</h4> </h4>
<div class="meta"> <div class="meta">
@ -164,7 +168,7 @@ const approve = async (approved: boolean) => {
:to="{name: 'library.tracks.detail', params: {id: obj.target.id }}" :to="{name: 'library.tracks.detail', params: {id: obj.target.id }}"
> >
<i class="music icon" /> <i class="music icon" />
{{ $t('components.library.EditCard.link.track', {id: obj.target.id, name: obj.target.repr}) }} {{ t('components.library.EditCard.link.track', {id: obj.target.id, name: obj.target.repr}) }}
</router-link> </router-link>
<br> <br>
<human-date <human-date
@ -175,19 +179,19 @@ const approve = async (approved: boolean) => {
<span class="right floated"> <span class="right floated">
<span v-if="obj.is_approved && obj.is_applied"> <span v-if="obj.is_approved && obj.is_applied">
<i class="success check icon" /> <i class="success check icon" />
{{ $t('components.library.EditCard.status.applied') }} {{ t('components.library.EditCard.status.applied') }}
</span> </span>
<span v-else-if="obj.is_approved"> <span v-else-if="obj.is_approved">
<i class="success check icon" /> <i class="success check icon" />
{{ $t('components.library.EditCard.status.approved') }} {{ t('components.library.EditCard.status.approved') }}
</span> </span>
<span v-else-if="obj.is_approved === null"> <span v-else-if="obj.is_approved === null">
<i class="warning hourglass icon" /> <i class="warning hourglass icon" />
{{ $t('components.library.EditCard.status.pending') }} {{ t('components.library.EditCard.status.pending') }}
</span> </span>
<span v-else-if="obj.is_approved === false"> <span v-else-if="obj.is_approved === false">
<i class="danger x icon" /> <i class="danger x icon" />
{{ $t('components.library.EditCard.status.rejected') }} {{ t('components.library.EditCard.status.rejected') }}
</span> </span>
</span> </span>
</div> </div>
@ -206,13 +210,13 @@ const approve = async (approved: boolean) => {
<thead> <thead>
<tr> <tr>
<th> <th>
{{ $t('components.library.EditCard.table.update.header.field') }} {{ t('components.library.EditCard.table.update.header.field') }}
</th> </th>
<th> <th>
{{ $t('components.library.EditCard.table.update.header.oldValue') }} {{ t('components.library.EditCard.table.update.header.oldValue') }}
</th> </th>
<th> <th>
{{ $t('components.library.EditCard.table.update.header.newValue') }} {{ t('components.library.EditCard.table.update.header.newValue') }}
</th> </th>
</tr> </tr>
</thead> </thead>
@ -228,7 +232,7 @@ const approve = async (approved: boolean) => {
<img <img
class="ui image" class="ui image"
alt="" alt=""
:src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.oldRepr}/proxy?next=medium_square_crop`)" :src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.oldRepr}/proxy?next=medium_square_crop`)"
> >
</template> </template>
<template v-else> <template v-else>
@ -242,7 +246,7 @@ const approve = async (approved: boolean) => {
</template> </template>
</td> </td>
<td v-else> <td v-else>
{{ $t('components.library.EditCard.table.update.notApplicable') }} {{ t('components.library.EditCard.table.update.notApplicable') }}
</td> </td>
<td <td
@ -253,7 +257,7 @@ const approve = async (approved: boolean) => {
<img <img
class="ui image" class="ui image"
alt="" alt=""
:src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)" :src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)"
> >
</template> </template>
<template v-else> <template v-else>
@ -274,7 +278,7 @@ const approve = async (approved: boolean) => {
<img <img
class="ui image" class="ui image"
alt="" alt=""
:src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)" :src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)"
> >
</template> </template>
<template v-else> <template v-else>
@ -300,36 +304,36 @@ const approve = async (approved: boolean) => {
:class="['ui', {loading: isLoading}, 'success', 'basic', 'button']" :class="['ui', {loading: isLoading}, 'success', 'basic', 'button']"
@click="approve(true)" @click="approve(true)"
> >
{{ $t('components.library.EditCard.button.approve') }} {{ t('components.library.EditCard.button.approve') }}
</button> </button>
<button <button
v-if="canApprove && obj.is_approved === null" v-if="canApprove && obj.is_approved === null"
:class="['ui', {loading: isLoading}, 'warning', 'basic', 'button']" :class="['ui', {loading: isLoading}, 'warning', 'basic', 'button']"
@click="approve(false)" @click="approve(false)"
> >
{{ $t('components.library.EditCard.button.reject') }} {{ t('components.library.EditCard.button.reject') }}
</button> </button>
<dangerous-button <dangerous-button
v-if="canDelete" v-if="canDelete"
:class="['ui', {loading: isLoading}, 'basic danger button']" :class="['ui', {loading: isLoading}, 'basic danger button']"
:action="remove" :action="remove"
> >
{{ $t('components.library.EditCard.button.delete') }} {{ t('components.library.EditCard.button.delete') }}
<template #modal-header> <template #modal-header>
<p> <p>
{{ $t('components.library.EditCard.modal.delete.header') }} {{ t('components.library.EditCard.modal.delete.header') }}
</p> </p>
</template> </template>
<template #modal-content> <template #modal-content>
<div> <div>
<p> <p>
{{ $t('components.library.EditCard.modal.content.warning') }} {{ t('components.library.EditCard.modal.content.warning') }}
</p> </p>
</div> </div>
</template> </template>
<template #modal-confirm> <template #modal-confirm>
<p> <p>
{{ $t('components.library.EditCard.button.delete') }} {{ t('components.library.EditCard.button.delete') }}
</p> </p>
</template> </template>
</dangerous-button> </dangerous-button>

View File

@ -148,7 +148,7 @@ const resetField = (fieldId: string) => {
<div v-if="submittedMutation"> <div v-if="submittedMutation">
<div class="ui positive message"> <div class="ui positive message">
<h4 class="header"> <h4 class="header">
{{ $t('components.library.EditForm.header.success') }} {{ t('components.library.EditForm.header.success') }}
</h4> </h4>
</div> </div>
<edit-card <edit-card
@ -159,7 +159,7 @@ const resetField = (fieldId: string) => {
class="ui button" class="ui button"
@click.prevent="submittedMutation = null" @click.prevent="submittedMutation = null"
> >
{{ $t('components.library.EditForm.button.new') }} {{ t('components.library.EditForm.button.new') }}
</button> </button>
</div> </div>
<div v-else> <div v-else>
@ -171,27 +171,27 @@ const resetField = (fieldId: string) => {
> >
<div> <div>
<template v-if="showPendingReview"> <template v-if="showPendingReview">
{{ $t('components.library.EditForm.header.unreviewed') }} {{ t('components.library.EditForm.header.unreviewed') }}
<button <button
class="ui tiny basic right floated button" class="ui tiny basic right floated button"
@click.prevent="showPendingReview = false" @click.prevent="showPendingReview = false"
> >
{{ $t('components.library.EditForm.button.showAll') }} {{ t('components.library.EditForm.button.showAll') }}
</button> </button>
</template> </template>
<template v-else> <template v-else>
{{ $t('components.library.EditForm.header.recentEdits') }} {{ t('components.library.EditForm.header.recentEdits') }}
<button <button
class="ui tiny basic right floated button" class="ui tiny basic right floated button"
@click.prevent="showPendingReview = true" @click.prevent="showPendingReview = true"
> >
{{ $t('components.library.EditForm.button.showUnreviewed') }} {{ t('components.library.EditForm.button.showUnreviewed') }}
</button> </button>
</template> </template>
</div> </div>
<template #empty-state> <template #empty-state>
<empty-state> <empty-state>
{{ $t('components.library.EditForm.empty.suggestEdit') }} {{ t('components.library.EditForm.empty.suggestEdit') }}
</empty-state> </empty-state>
</template> </template>
</edit-list> </edit-list>
@ -206,7 +206,7 @@ const resetField = (fieldId: string) => {
class="ui negative message" class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.library.EditForm.header.failure') }} {{ t('components.library.EditForm.header.failure') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li
@ -221,7 +221,7 @@ const resetField = (fieldId: string) => {
v-if="!canEdit" v-if="!canEdit"
class="ui message" class="ui message"
> >
{{ $t('components.library.EditForm.message.noPermission') }} {{ t('components.library.EditForm.message.noPermission') }}
</div> </div>
<template v-if="values"> <template v-if="values">
<div <div
@ -250,7 +250,7 @@ const resetField = (fieldId: string) => {
class="ui fluid search dropdown" class="ui fluid search dropdown"
> >
<option :value="null"> <option :value="null">
{{ $t('components.library.EditForm.notApplicable') }} {{ t('components.library.EditForm.notApplicable') }}
</option> </option>
<option <option
v-for="{ code, name } in licenses" v-for="{ code, name } in licenses"
@ -266,7 +266,7 @@ const resetField = (fieldId: string) => {
@click.prevent="values[fieldConfig.id] = null" @click.prevent="values[fieldConfig.id] = null"
> >
<i class="x icon" /> <i class="x icon" />
{{ $t('components.library.EditForm.button.clear') }} {{ t('components.library.EditForm.button.clear') }}
</button> </button>
</template> </template>
<template v-else-if="fieldConfig.type === 'content'"> <template v-else-if="fieldConfig.type === 'content'">
@ -303,7 +303,7 @@ const resetField = (fieldId: string) => {
@click.prevent="values[fieldConfig.id] = []" @click.prevent="values[fieldConfig.id] = []"
> >
<i class="x icon" /> <i class="x icon" />
{{ $t('components.library.EditForm.button.clear') }} {{ t('components.library.EditForm.button.clear') }}
</button> </button>
</template> </template>
<div v-if="fieldValuesChanged(fieldConfig.id)"> <div v-if="fieldValuesChanged(fieldConfig.id)">
@ -313,13 +313,13 @@ const resetField = (fieldId: string) => {
@click.prevent="resetField(fieldConfig.id)" @click.prevent="resetField(fieldConfig.id)"
> >
<i class="undo icon" /> <i class="undo icon" />
{{ $t('components.library.EditForm.button.reset') }} {{ t('components.library.EditForm.button.reset') }}
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<div class="field"> <div class="field">
<label for="summary">{{ $t('components.library.EditForm.label.summary') }}</label> <label for="summary">{{ t('components.library.EditForm.label.summary') }}</label>
<textarea <textarea
id="change-summary" id="change-summary"
v-model="summary" v-model="summary"
@ -333,7 +333,7 @@ const resetField = (fieldId: string) => {
class="ui left floated button" class="ui left floated button"
:to="{name: 'library.tracks.detail', params: {id: object.id }}" :to="{name: 'library.tracks.detail', params: {id: object.id }}"
> >
{{ $t('components.library.EditForm.button.cancel') }} {{ t('components.library.EditForm.button.cancel') }}
</router-link> </router-link>
<button <button
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']" :class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']"
@ -341,10 +341,10 @@ const resetField = (fieldId: string) => {
:disabled="isLoading || !mutationPayload" :disabled="isLoading || !mutationPayload"
> >
<span v-if="canEdit"> <span v-if="canEdit">
{{ $t('components.library.EditForm.button.submit') }} {{ t('components.library.EditForm.button.submit') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.library.EditForm.button.suggest') }} {{ t('components.library.EditForm.button.suggest') }}
</span> </span>
</button> </button>
</form> </form>

View File

@ -303,12 +303,12 @@ useEventListener(window, 'beforeunload', (event) => {
:class="['item', {active: currentTab === 'uploads'}]" :class="['item', {active: currentTab === 'uploads'}]"
@click.prevent="currentTab = 'uploads'" @click.prevent="currentTab = 'uploads'"
> >
{{ $t('components.library.FileUpload.link.uploading') }} {{ t('components.library.FileUpload.link.uploading') }}
<div <div
v-if="files.length === 0" v-if="files.length === 0"
class="ui label" class="ui label"
> >
{{ $t('components.library.FileUpload.empty.noFiles') }} {{ t('components.library.FileUpload.empty.noFiles') }}
</div> </div>
<div <div
v-else-if="files.length > uploadedFilesCount + erroredFilesCount" v-else-if="files.length > uploadedFilesCount + erroredFilesCount"
@ -332,12 +332,12 @@ useEventListener(window, 'beforeunload', (event) => {
:class="['item', {active: currentTab === 'processing'}]" :class="['item', {active: currentTab === 'processing'}]"
@click.prevent="currentTab = 'processing'" @click.prevent="currentTab = 'processing'"
> >
{{ $t('components.library.FileUpload.link.processing') }} {{ t('components.library.FileUpload.link.processing') }}
<div <div
v-if="processableFiles === 0" v-if="processableFiles === 0"
class="ui label" class="ui label"
> >
{{ $t('components.library.FileUpload.empty.noFiles') }} {{ t('components.library.FileUpload.empty.noFiles') }}
</div> </div>
<div <div
v-else-if="processableFiles > processedFilesCount" v-else-if="processableFiles > processedFilesCount"
@ -361,7 +361,7 @@ useEventListener(window, 'beforeunload', (event) => {
<div :class="['ui', {loading: isLoadingQuota}, 'container']"> <div :class="['ui', {loading: isLoadingQuota}, 'container']">
<div :class="['ui', {red: remainingSpace === 0}, {warning: remainingSpace > 0 && remainingSpace <= 50}, 'small', 'statistic']"> <div :class="['ui', {red: remainingSpace === 0}, {warning: remainingSpace > 0 && remainingSpace <= 50}, 'small', 'statistic']">
<div class="label"> <div class="label">
{{ $t('components.library.FileUpload.label.remainingSpace') }} {{ t('components.library.FileUpload.label.remainingSpace') }}
</div> </div>
<div class="value"> <div class="value">
{{ humanSize(remainingSpace * 1000 * 1000) }} {{ humanSize(remainingSpace * 1000 * 1000) }}
@ -369,25 +369,25 @@ useEventListener(window, 'beforeunload', (event) => {
</div> </div>
<div class="ui divider" /> <div class="ui divider" />
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.library.FileUpload.header.local') }} {{ t('components.library.FileUpload.header.local') }}
</h2> </h2>
<div class="ui message"> <div class="ui message">
<p> <p>
{{ $t('components.library.FileUpload.message.local.message') }} {{ t('components.library.FileUpload.message.local.message') }}
</p> </p>
<ul> <ul>
<li v-if="library.privacy_level != 'me'"> <li v-if="library.privacy_level != 'me'">
{{ $t('components.library.FileUpload.message.local.copyright') }} {{ t('components.library.FileUpload.message.local.copyright') }}
</li> </li>
<li> <li>
{{ $t('components.library.FileUpload.message.local.tag') }}&nbsp; {{ t('components.library.FileUpload.message.local.tag') }}&nbsp;
<a <a
href="http://picard.musicbrainz.org/" href="http://picard.musicbrainz.org/"
target="_blank" target="_blank"
>{{ $t('components.library.FileUpload.link.picard') }}</a> >{{ t('components.library.FileUpload.link.picard') }}</a>
</li> </li>
<li> <li>
{{ $t('components.library.FileUpload.message.local.format') }} {{ t('components.library.FileUpload.message.local.format') }}
</li> </li>
</ul> </ul>
</div> </div>
@ -399,11 +399,11 @@ useEventListener(window, 'beforeunload', (event) => {
@input-file="inputFile" @input-file="inputFile"
> >
<i class="upload icon" />&nbsp; <i class="upload icon" />&nbsp;
{{ $t('components.library.FileUpload.label.uploadWidget') }} {{ t('components.library.FileUpload.label.uploadWidget') }}
<br> <br>
<br> <br>
<i> <i>
{{ $t('components.library.FileUpload.label.extensions', {extensions: supportedExtensions.join(', ')}) }} {{ t('components.library.FileUpload.label.extensions', {extensions: supportedExtensions.join(', ')}) }}
</i> </i>
</file-upload-widget> </file-upload-widget>
</div> </div>
@ -416,16 +416,16 @@ useEventListener(window, 'beforeunload', (event) => {
<thead> <thead>
<tr> <tr>
<th class="ten wide"> <th class="ten wide">
{{ $t('components.library.FileUpload.table.upload.header.filename') }} {{ t('components.library.FileUpload.table.upload.header.filename') }}
</th> </th>
<th> <th>
{{ $t('components.library.FileUpload.table.upload.header.size') }} {{ t('components.library.FileUpload.table.upload.header.size') }}
</th> </th>
<th> <th>
{{ $t('components.library.FileUpload.table.upload.header.status') }} {{ t('components.library.FileUpload.table.upload.header.status') }}
</th> </th>
<th> <th>
{{ $t('components.library.FileUpload.table.upload.header.actions') }} {{ t('components.library.FileUpload.table.upload.header.actions') }}
</th> </th>
</tr> </tr>
<tr v-if="retryableFiles.length > 1"> <tr v-if="retryableFiles.length > 1">
@ -437,7 +437,7 @@ useEventListener(window, 'beforeunload', (event) => {
class="ui right floated small basic button" class="ui right floated small basic button"
@click.prevent="retry(retryableFiles)" @click.prevent="retry(retryableFiles)"
> >
{{ $t('components.library.FileUpload.button.retry') }} {{ t('components.library.FileUpload.button.retry') }}
</button> </button>
</th> </th>
</tr> </tr>
@ -466,7 +466,7 @@ useEventListener(window, 'beforeunload', (event) => {
class="ui success label" class="ui success label"
> >
<span key="1"> <span key="1">
{{ $t('components.library.FileUpload.table.upload.status.uploaded') }} {{ t('components.library.FileUpload.table.upload.status.uploaded') }}
</span> </span>
</span> </span>
<span <span
@ -474,17 +474,17 @@ useEventListener(window, 'beforeunload', (event) => {
class="ui warning label" class="ui warning label"
> >
<span key="2"> <span key="2">
{{ $t('components.library.FileUpload.table.upload.status.uploading') }} {{ t('components.library.FileUpload.table.upload.status.uploading') }}
</span> </span>
{{ $t('components.library.FileUpload.table.upload.progress', {percent: parseFloat(file.progress ?? '0.00')}) }} {{ t('components.library.FileUpload.table.upload.progress', {percent: parseFloat(file.progress ?? '0.00')}) }}
</span> </span>
<span <span
v-else v-else
class="ui label" class="ui label"
> >
<span key="3"> <span key="3">
{{ $t('components.library.FileUpload.table.upload.status.pending') }} {{ t('components.library.FileUpload.table.upload.status.pending') }}
</span> </span>
</span> </span>
</td> </td>
@ -514,7 +514,7 @@ useEventListener(window, 'beforeunload', (event) => {
</div> </div>
<div class="ui divider" /> <div class="ui divider" />
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.library.FileUpload.header.server') }} {{ t('components.library.FileUpload.header.server') }}
</h2> </h2>
<div <div
v-if="fsErrors.length > 0" v-if="fsErrors.length > 0"
@ -522,7 +522,7 @@ useEventListener(window, 'beforeunload', (event) => {
class="ui negative message" class="ui negative message"
> >
<h3 class="header"> <h3 class="header">
{{ $t('components.library.FileUpload.header.failure') }} {{ t('components.library.FileUpload.header.failure') }}
</h3> </h3>
<ul class="list"> <ul class="list">
<li <li
@ -541,13 +541,13 @@ useEventListener(window, 'beforeunload', (event) => {
/> />
<template v-if="fsStatus && fsStatus.import"> <template v-if="fsStatus && fsStatus.import">
<h3 class="ui header"> <h3 class="ui header">
{{ $t('components.library.FileUpload.header.status') }} {{ t('components.library.FileUpload.header.status') }}
</h3> </h3>
<p v-if="fsStatus.import.reference !== importReference"> <p v-if="fsStatus.import.reference !== importReference">
{{ $t('components.library.FileUpload.description.previousImport') }} {{ t('components.library.FileUpload.description.previousImport') }}
</p> </p>
<p v-else> <p v-else>
{{ $t('components.library.FileUpload.description.import') }} {{ t('components.library.FileUpload.description.import') }}
</p> </p>
<button <button
@ -555,7 +555,7 @@ useEventListener(window, 'beforeunload', (event) => {
class="ui button" class="ui button"
@click="cancelFsScan" @click="cancelFsScan"
> >
{{ $t('components.library.FileUpload.button.cancel') }} {{ t('components.library.FileUpload.button.cancel') }}
</button> </button>
<fs-logs :data="fsStatus.import" /> <fs-logs :data="fsStatus.import" />
</template> </template>

View File

@ -2,6 +2,8 @@
import type { FileSystem, FSEntry } from '~/types' import type { FileSystem, FSEntry } from '~/types'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
interface Events { interface Events {
(e: 'update:modelValue', value: string[]): void (e: 'update:modelValue', value: string[]): void
@ -14,6 +16,8 @@ interface Props {
modelValue: string[] modelValue: string[]
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = defineProps<Props>() const props = defineProps<Props>()
@ -42,7 +46,7 @@ const handleClick = (entry: FSEntry) => {
class="ui button" class="ui button"
@click.prevent="emit('import')" @click.prevent="emit('import')"
> >
{{ $t('components.library.FsBrowser.button.import') }} {{ t('components.library.FsBrowser.button.import') }}
</button> </button>
</div> </div>
<div class="ui list component-fs-browser"> <div class="ui list component-fs-browser">

View File

@ -1,11 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FSLogs } from '~/types' import type { FSLogs } from '~/types'
import { useI18n } from 'vue-i18n'
interface Props { interface Props {
data: FSLogs data: FSLogs
} }
defineProps<Props>() defineProps<Props>()
const { t } = useI18n()
</script> </script>
<template> <template>
@ -15,7 +19,7 @@ defineProps<Props>()
class="ui active dimmer" class="ui active dimmer"
> >
<div class="ui text loader"> <div class="ui text loader">
{{ $t('components.library.FsLogs.empty.notStarted') }} {{ t('components.library.FsLogs.empty.notStarted') }}
</div> </div>
</div> </div>
<template v-else> <template v-else>

View File

@ -2,6 +2,7 @@
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useRoute } from 'vue-router'
import axios from 'axios' import axios from 'axios'
@ -22,6 +23,7 @@ withDefaults(defineProps<Props>(), {
}) })
const store = useStore() const store = useStore()
const route = useRoute()
const qualityFilters = computed(() => store.getters['instance/qualityFilters']) const qualityFilters = computed(() => store.getters['instance/qualityFilters'])
const artists = ref([]) const artists = ref([])
@ -59,7 +61,7 @@ fetchData()
<template> <template>
<main <main
:key="$route?.name ?? undefined" :key="route?.name ?? undefined"
v-title="labels.title" v-title="labels.title"
> >
<section class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
@ -71,7 +73,7 @@ fetchData()
:websocket-handlers="['Listen']" :websocket-handlers="['Listen']"
> >
<template #title> <template #title>
{{ $t('components.library.Home.header.recentlyListened') }} {{ t('components.library.Home.header.recentlyListened') }}
</template> </template>
</track-widget> </track-widget>
</div> </div>
@ -81,7 +83,7 @@ fetchData()
:filters="{scope: scope, ordering: '-creation_date'}" :filters="{scope: scope, ordering: '-creation_date'}"
> >
<template #title> <template #title>
{{ $t('components.library.Home.header.recentlyFavorited') }} {{ t('components.library.Home.header.recentlyFavorited') }}
</template> </template>
</track-widget> </track-widget>
</div> </div>
@ -92,7 +94,7 @@ fetchData()
:filters="{scope: scope, playable: true, ordering: '-modification_date'}" :filters="{scope: scope, playable: true, ordering: '-modification_date'}"
> >
<template #title> <template #title>
{{ $t('components.library.Home.header.playlists') }} {{ t('components.library.Home.header.playlists') }}
</template> </template>
</playlist-widget> </playlist-widget>
</div> </div>
@ -102,14 +104,14 @@ fetchData()
<div class="column"> <div class="column">
<album-widget :filters="{scope: scope, playable: true, ordering: '-creation_date', ...qualityFilters}"> <album-widget :filters="{scope: scope, playable: true, ordering: '-creation_date', ...qualityFilters}">
<template #title> <template #title>
{{ $t('components.library.Home.header.recentlyAdded') }} {{ t('components.library.Home.header.recentlyAdded') }}
</template> </template>
</album-widget> </album-widget>
</div> </div>
</div> </div>
<template v-if="scope === 'all'"> <template v-if="scope === 'all'">
<h3 class="ui header"> <h3 class="ui header">
{{ $t('components.library.Home.header.newChannels') }} {{ t('components.library.Home.header.newChannels') }}
</h3> </h3>
<channels-widget <channels-widget
:show-modification-date="true" :show-modification-date="true"

View File

@ -77,7 +77,7 @@ const getErrorData = (upload: Upload) => {
<template> <template>
<semantic-modal v-model:show="show"> <semantic-modal v-model:show="show">
<h4 class="header"> <h4 class="header">
{{ $t('components.library.ImportStatusModal.header.importDetail') }} {{ t('components.library.ImportStatusModal.header.importDetail') }}
</h4> </h4>
<div <div
v-if="Object.keys(upload).length > 0" v-if="Object.keys(upload).length > 0"
@ -88,33 +88,33 @@ const getErrorData = (upload: Upload) => {
v-if="upload.import_status === 'pending'" v-if="upload.import_status === 'pending'"
class="ui message" class="ui message"
> >
{{ $t('components.library.ImportStatusModal.message.importDetail') }} {{ t('components.library.ImportStatusModal.message.importDetail') }}
</div> </div>
<div <div
v-if="upload.import_status === 'finished'" v-if="upload.import_status === 'finished'"
class="ui success message" class="ui success message"
> >
{{ $t('components.library.ImportStatusModal.message.importSuccess') }} {{ t('components.library.ImportStatusModal.message.importSuccess') }}
</div> </div>
<div <div
v-if="upload.import_status === 'skipped'" v-if="upload.import_status === 'skipped'"
role="alert" role="alert"
class="ui warning message" class="ui warning message"
> >
{{ $t('components.library.ImportStatusModal.warning.importSkipped') }} {{ t('components.library.ImportStatusModal.warning.importSkipped') }}
</div> </div>
<div <div
v-if="upload.import_status === 'errored'" v-if="upload.import_status === 'errored'"
class="ui error message" class="ui error message"
> >
{{ $t('components.library.ImportStatusModal.error.importFailure') }} {{ t('components.library.ImportStatusModal.error.importFailure') }}
</div> </div>
<template v-if="upload.import_status === 'errored'"> <template v-if="upload.import_status === 'errored'">
<table class="ui very basic collapsing celled table"> <table class="ui very basic collapsing celled table">
<tbody> <tbody>
<tr> <tr>
<td> <td>
{{ $t('components.library.ImportStatusModal.table.error.errorType') }} {{ t('components.library.ImportStatusModal.table.error.errorType') }}
</td> </td>
<td> <td>
{{ getErrorData(upload).label }} {{ getErrorData(upload).label }}
@ -122,7 +122,7 @@ const getErrorData = (upload: Upload) => {
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.library.ImportStatusModal.table.error.errorDetail') }} {{ t('components.library.ImportStatusModal.table.error.errorDetail') }}
</td> </td>
<td> <td>
{{ getErrorData(upload).detail }} {{ getErrorData(upload).detail }}
@ -140,7 +140,7 @@ const getErrorData = (upload: Upload) => {
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.library.ImportStatusModal.table.error.help') }} {{ t('components.library.ImportStatusModal.table.error.help') }}
</td> </td>
<td> <td>
<ul> <ul>
@ -149,7 +149,7 @@ const getErrorData = (upload: Upload) => {
:href="getErrorData(upload).documentationUrl" :href="getErrorData(upload).documentationUrl"
target="_blank" target="_blank"
> >
{{ $t('components.library.ImportStatusModal.link.documentation') }} {{ t('components.library.ImportStatusModal.link.documentation') }}
</a> </a>
</li> </li>
<li> <li>
@ -157,7 +157,7 @@ const getErrorData = (upload: Upload) => {
:href="getErrorData(upload).supportUrl" :href="getErrorData(upload).supportUrl"
target="_blank" target="_blank"
> >
{{ $t('components.library.ImportStatusModal.link.support') }} {{ t('components.library.ImportStatusModal.link.support') }}
</a> </a>
</li> </li>
</ul> </ul>
@ -165,7 +165,7 @@ const getErrorData = (upload: Upload) => {
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.library.ImportStatusModal.table.error.debug') }} {{ t('components.library.ImportStatusModal.table.error.debug') }}
</td> </td>
<td> <td>
<div class="ui form"> <div class="ui form">
@ -184,7 +184,7 @@ const getErrorData = (upload: Upload) => {
</div> </div>
<div class="actions"> <div class="actions">
<button class="ui deny button"> <button class="ui deny button">
{{ $t('components.library.ImportStatusModal.button.close') }} {{ t('components.library.ImportStatusModal.button.close') }}
</button> </button>
</div> </div>
</semantic-modal> </semantic-modal>

View File

@ -123,7 +123,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
<main v-title="labels.title"> <main v-title="labels.title">
<section class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.library.Podcasts.header.browse') }} {{ t('components.library.Podcasts.header.browse') }}
</h2> </h2>
<form <form
:class="['ui', {'loading': isLoading}, 'form']" :class="['ui', {'loading': isLoading}, 'form']"
@ -132,7 +132,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<label for="artist-search"> <label for="artist-search">
{{ $t('components.library.Podcasts.label.search') }} {{ t('components.library.Podcasts.label.search') }}
</label> </label>
<div class="ui action input"> <div class="ui action input">
<input <input
@ -152,11 +152,11 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label for="tags-search">{{ $t('components.library.Podcasts.label.tags') }}</label> <label for="tags-search">{{ t('components.library.Podcasts.label.tags') }}</label>
<tags-selector v-model="tags" /> <tags-selector v-model="tags" />
</div> </div>
<div class="field"> <div class="field">
<label for="artist-ordering">{{ $t('components.library.Podcasts.ordering.label') }}</label> <label for="artist-ordering">{{ t('components.library.Podcasts.ordering.label') }}</label>
<select <select
id="artist-ordering" id="artist-ordering"
v-model="ordering" v-model="ordering"
@ -172,22 +172,22 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label for="artist-ordering-direction">{{ $t('components.library.Podcasts.ordering.direction.label') }}</label> <label for="artist-ordering-direction">{{ t('components.library.Podcasts.ordering.direction.label') }}</label>
<select <select
id="artist-ordering-direction" id="artist-ordering-direction"
v-model="orderingDirection" v-model="orderingDirection"
class="ui dropdown" class="ui dropdown"
> >
<option value="+"> <option value="+">
{{ $t('components.library.Podcasts.ordering.direction.ascending') }} {{ t('components.library.Podcasts.ordering.direction.ascending') }}
</option> </option>
<option value="-"> <option value="-">
{{ $t('components.library.Podcasts.ordering.direction.descending') }} {{ t('components.library.Podcasts.ordering.direction.descending') }}
</option> </option>
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label for="artist-results">{{ $t('components.library.Podcasts.pagination.results') }}</label> <label for="artist-results">{{ t('components.library.Podcasts.pagination.results') }}</label>
<select <select
id="artist-results" id="artist-results"
v-model="paginateBy" v-model="paginateBy"
@ -228,7 +228,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
> >
<div class="ui icon header"> <div class="ui icon header">
<i class="podcast icon" /> <i class="podcast icon" />
{{ $t('components.library.Podcasts.empty.noResults') }} {{ t('components.library.Podcasts.empty.noResults') }}
</div> </div>
<router-link <router-link
v-if="$store.state.auth.authenticated" v-if="$store.state.auth.authenticated"
@ -236,7 +236,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
class="ui success button labeled icon" class="ui success button labeled icon"
> >
<i class="upload icon" /> <i class="upload icon" />
{{ $t('components.library.Podcasts.button.channel') }} {{ t('components.library.Podcasts.button.channel') }}
</router-link> </router-link>
<h1 <h1
v-if="$store.state.auth.authenticated" v-if="$store.state.auth.authenticated"
@ -245,7 +245,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
<div class="actions"> <div class="actions">
<a @click.stop.prevent="showSubscribeModal = true"> <a @click.stop.prevent="showSubscribeModal = true">
<i class="plus icon" /> <i class="plus icon" />
{{ $t('components.library.Podcasts.button.feed') }} {{ t('components.library.Podcasts.button.feed') }}
</a> </a>
</div> </div>
</h1> </h1>
@ -265,7 +265,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
:fullscreen="false" :fullscreen="false"
> >
<h2 class="header"> <h2 class="header">
{{ $t('components.library.Podcasts.modal.subscription.header') }} {{ t('components.library.Podcasts.modal.subscription.header') }}
</h2> </h2>
<div <div
ref="modalContent" ref="modalContent"
@ -281,7 +281,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</div> </div>
<div class="actions"> <div class="actions">
<button class="ui basic deny button"> <button class="ui basic deny button">
{{ $t('components.library.Podcasts.button.cancel') }} {{ t('components.library.Podcasts.button.cancel') }}
</button> </button>
<button <button
form="remote-search" form="remote-search"
@ -289,7 +289,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
class="ui primary button" class="ui primary button"
> >
<i class="bookmark icon" /> <i class="bookmark icon" />
{{ $t('components.library.Podcasts.button.subscribe') }} {{ t('components.library.Podcasts.button.subscribe') }}
</button> </button>
</div> </div>
</semantic-modal> </semantic-modal>

View File

@ -112,12 +112,12 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
<main v-title="labels.title"> <main v-title="labels.title">
<section class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.library.Radios.header.browse') }} {{ t('components.library.Radios.header.browse') }}
</h2> </h2>
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<div class="ui row"> <div class="ui row">
<h3 class="ui header"> <h3 class="ui header">
{{ $t('components.library.Radios.header.instance') }} {{ t('components.library.Radios.header.instance') }}
</h3> </h3>
<div class="ui cards"> <div class="ui cards">
<radio-card <radio-card
@ -151,14 +151,14 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<h3 class="ui header"> <h3 class="ui header">
{{ $t('components.library.Radios.header.user') }} {{ t('components.library.Radios.header.user') }}
</h3> </h3>
<router-link <router-link
v-if="isAuthenticated" v-if="isAuthenticated"
class="ui success button" class="ui success button"
to="/library/radios/build" to="/library/radios/build"
> >
{{ $t('components.library.Radios.button.create') }} {{ t('components.library.Radios.button.create') }}
</router-link> </router-link>
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<form <form
@ -167,7 +167,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
> >
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<label for="radios-search">{{ $t('components.library.Radios.label.search') }}</label> <label for="radios-search">{{ t('components.library.Radios.label.search') }}</label>
<div class="ui action input"> <div class="ui action input">
<input <input
id="radios-search" id="radios-search"
@ -186,7 +186,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label for="radios-ordering">{{ $t('components.library.Radios.ordering.label') }}</label> <label for="radios-ordering">{{ t('components.library.Radios.ordering.label') }}</label>
<select <select
id="radios-ordering" id="radios-ordering"
v-model="ordering" v-model="ordering"
@ -202,22 +202,22 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label for="radios-ordering-direction">{{ $t('components.library.Radios.ordering.direction.label') }}</label> <label for="radios-ordering-direction">{{ t('components.library.Radios.ordering.direction.label') }}</label>
<select <select
id="radios-ordering-direction" id="radios-ordering-direction"
v-model="orderingDirection" v-model="orderingDirection"
class="ui dropdown" class="ui dropdown"
> >
<option value="+"> <option value="+">
{{ $t('components.library.Radios.ordering.direction.ascending') }} {{ t('components.library.Radios.ordering.direction.ascending') }}
</option> </option>
<option value="-"> <option value="-">
{{ $t('components.library.Radios.ordering.direction.descending') }} {{ t('components.library.Radios.ordering.direction.descending') }}
</option> </option>
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label for="radios-results">{{ $t('components.library.Radios.pagination.results') }}</label> <label for="radios-results">{{ t('components.library.Radios.pagination.results') }}</label>
<select <select
id="radios-results" id="radios-results"
v-model="paginateBy" v-model="paginateBy"
@ -241,7 +241,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
> >
<div class="ui icon header"> <div class="ui icon header">
<i class="feed icon" /> <i class="feed icon" />
{{ $t('components.library.Radios.empty.noResults') }} {{ t('components.library.Radios.empty.noResults') }}
</div> </div>
<router-link <router-link
v-if="$store.state.auth.authenticated" v-if="$store.state.auth.authenticated"
@ -249,7 +249,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
class="ui success button labeled icon" class="ui success button labeled icon"
> >
<i class="rss icon" /> <i class="rss icon" />
{{ $t('components.library.Radios.button.add') }} {{ t('components.library.Radios.button.add') }}
</router-link> </router-link>
</div> </div>
<div <div

View File

@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import ChannelsWidget from '~/components/audio/ChannelsWidget.vue' import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
import TrackWidget from '~/components/audio/track/Widget.vue' import TrackWidget from '~/components/audio/track/Widget.vue'
@ -11,6 +13,9 @@ interface Props {
id: string id: string
} }
const store = useStore()
const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const labels = computed(() => ({ const labels = computed(() => ({
@ -31,12 +36,12 @@ const labels = computed(() => ({
:object-id="id" :object-id="id"
/> />
<router-link <router-link
v-if="$store.state.auth.availablePermissions['library']" v-if="store.state.auth.availablePermissions['library']"
class="ui right floated button" class="ui right floated button"
:to="{name: 'manage.library.tags.detail', params: {id: id}}" :to="{name: 'manage.library.tags.detail', params: {id: id}}"
> >
<i class="wrench icon" /> <i class="wrench icon" />
{{ $t('components.library.TagDetail.link.moderation') }} {{ t('components.library.TagDetail.link.moderation') }}
</router-link> </router-link>
<div class="ui hidden divider" /> <div class="ui hidden divider" />
@ -48,14 +53,14 @@ const labels = computed(() => ({
> >
<template #title> <template #title>
<router-link :to="{name: 'library.artists.browse', query: {tag: id}}"> <router-link :to="{name: 'library.artists.browse', query: {tag: id}}">
{{ $t('components.library.TagDetail.link.artists') }} {{ t('components.library.TagDetail.link.artists') }}
</router-link> </router-link>
</template> </template>
</artist-widget> </artist-widget>
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<h3 class="ui header"> <h3 class="ui header">
{{ $t('components.library.TagDetail.header.channels') }} {{ t('components.library.TagDetail.header.channels') }}
</h3> </h3>
<channels-widget <channels-widget
:key="'channels' + id" :key="'channels' + id"
@ -73,7 +78,7 @@ const labels = computed(() => ({
> >
<template #title> <template #title>
<router-link :to="{name: 'library.albums.browse', query: {tag: id}}"> <router-link :to="{name: 'library.albums.browse', query: {tag: id}}">
{{ $t('components.library.TagDetail.link.albums') }} {{ t('components.library.TagDetail.link.albums') }}
</router-link> </router-link>
</template> </template>
</album-widget> </album-widget>
@ -89,7 +94,7 @@ const labels = computed(() => ({
:filters="{playable: true, ordering: '-creation_date', tag: id}" :filters="{playable: true, ordering: '-creation_date', tag: id}"
> >
<template #title> <template #title>
{{ $t('components.library.TagDetail.header.tracks') }} {{ t('components.library.TagDetail.header.tracks') }}
</template> </template>
</track-widget> </track-widget>
<div class="ui clearing hidden divider" /> <div class="ui clearing hidden divider" />

View File

@ -4,6 +4,8 @@ import type { Tag } from '~/types'
import { ref, watch, onMounted, nextTick } from 'vue' import { ref, watch, onMounted, nextTick } from 'vue'
import { isEqual } from 'lodash-es' import { isEqual } from 'lodash-es'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import $ from 'jquery' import $ from 'jquery'
@ -15,6 +17,8 @@ interface Props {
modelValue: string[] modelValue: string[]
} }
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = defineProps<Props>() const props = defineProps<Props>()
@ -99,7 +103,7 @@ onMounted(async () => {
class="search" class="search"
> >
<div class="default text"> <div class="default text">
{{ $t('components.library.TagSelector.placeholder.search') }} {{ t('components.library.TagSelector.placeholder.search') }}
</div> </div>
</div> </div>
</template> </template>

View File

@ -4,7 +4,7 @@ import type { Track, Artist, Library } from '~/types'
import { momentFormat } from '~/utils/filters' import { momentFormat } from '~/utils/filters'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { getDomain } from '~/utils' import { getDomain } from '~/utils'
import { useStore } from '~/store' import { useStore } from '~/store'
@ -15,6 +15,10 @@ import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
import EmbedWizard from '~/components/audio/EmbedWizard.vue' import EmbedWizard from '~/components/audio/EmbedWizard.vue'
import SemanticModal from '~/components/semantic/Modal.vue' import SemanticModal from '~/components/semantic/Modal.vue'
import PlayButton from '~/components/audio/PlayButton.vue' import PlayButton from '~/components/audio/PlayButton.vue'
import Button from '~/components/ui/Button.vue'
import Popover from '~/components/ui/Popover.vue'
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
import DangerousButton from '~/components/common/DangerousButton.vue'
import updateQueryString from '~/composables/updateQueryString' import updateQueryString from '~/composables/updateQueryString'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
@ -41,6 +45,7 @@ const libraries = ref([] as Library[])
const logger = useLogger() const logger = useLogger()
const router = useRouter() const router = useRouter()
const route = useRoute()
const store = useStore() const store = useStore()
const domain = computed(() => getDomain(track.value?.fid ?? '')) const domain = computed(() => getDomain(track.value?.fid ?? ''))
@ -100,6 +105,8 @@ const remove = async () => {
isLoading.value = false isLoading.value = false
} }
const open = ref(false)
</script> </script>
<template> <template>
@ -158,16 +165,16 @@ const remove = async () => {
class="vibrant" class="vibrant"
:track="track" :track="track"
> >
{{ $t('components.library.TrackBase.button.play') }} {{ t('components.library.TrackBase.button.play') }}
</play-button> </play-button>
&nbsp; &nbsp;
<track-favorite-icon <track-favorite-icon
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
:border="true" :border="true"
:track="track" :track="track"
/> />
<track-playlist-icon <track-playlist-icon
v-if="$store.state.auth.authenticated" v-if="store.state.auth.authenticated"
class="circular" class="circular"
:border="true" :border="true"
:track="track" :track="track"
@ -188,7 +195,7 @@ const remove = async () => {
v-model:show="showEmbedModal" v-model:show="showEmbedModal"
> >
<h4 class="header"> <h4 class="header">
{{ $t('components.library.TrackBase.modal.embed.header') }} {{ t('components.library.TrackBase.modal.embed.header') }}
</h4> </h4>
<div class="scrolling content"> <div class="scrolling content">
<div class="description"> <div class="description">
@ -200,7 +207,7 @@ const remove = async () => {
</div> </div>
<div class="actions"> <div class="actions">
<button class="ui basic deny button"> <button class="ui basic deny button">
{{ $t('components.library.TrackBase.button.cancel') }} {{ t('components.library.TrackBase.button.cancel') }}
</button> </button>
</div> </div>
</semantic-modal> </semantic-modal>
@ -215,13 +222,13 @@ const remove = async () => {
style="right: 0; left: auto" style="right: 0; left: auto"
> >
<a <a
v-if="domain != $store.getters['instance/domain']" v-if="domain != store.getters['instance/domain']"
:href="track.fid" :href="track.fid"
target="_blank" target="_blank"
class="basic item" class="basic item"
> >
<i class="external icon" /> <i class="external icon" />
{{ $t('components.library.TrackBase.link.domain', {domain: domain}) }} {{ t('components.library.TrackBase.link.domain', {domain: domain}) }}
</a> </a>
<div <div
v-if="isEmbedable" v-if="isEmbedable"
@ -230,7 +237,7 @@ const remove = async () => {
@click="showEmbedModal = !showEmbedModal" @click="showEmbedModal = !showEmbedModal"
> >
<i class="code icon" /> <i class="code icon" />
{{ $t('components.library.TrackBase.button.embed') }} {{ t('components.library.TrackBase.button.embed') }}
</div> </div>
<a <a
:href="wikipediaUrl" :href="wikipediaUrl"
@ -239,7 +246,7 @@ const remove = async () => {
class="basic item" class="basic item"
> >
<i class="wikipedia w icon" /> <i class="wikipedia w icon" />
{{ $t('components.library.TrackBase.link.wikipedia') }} {{ t('components.library.TrackBase.link.wikipedia') }}
</a> </a>
<a <a
v-if="discogsUrl" v-if="discogsUrl"
@ -249,7 +256,7 @@ const remove = async () => {
class="basic item" class="basic item"
> >
<i class="external icon" /> <i class="external icon" />
{{ $t('components.library.TrackBase.link.discogs') }} {{ t('components.library.TrackBase.link.discogs') }}
</a> </a>
<router-link <router-link
v-if="track.is_local" v-if="track.is_local"
@ -257,30 +264,30 @@ const remove = async () => {
class="basic item" class="basic item"
> >
<i class="edit icon" /> <i class="edit icon" />
{{ $t('components.library.TrackBase.button.edit') }} {{ t('components.library.TrackBase.button.edit') }}
</router-link> </router-link>
<dangerous-button <dangerous-button
v-if="artist && $store.state.auth.authenticated && artist.channel && artist.attributed_to.full_username === $store.state.auth.fullUsername" v-if="artist && store.state.auth.authenticated && artist.channel && artist.attributed_to.full_username === store.state.auth.fullUsername"
:class="['ui', {loading: isLoading}, 'item']" :class="['ui', {loading: isLoading}, 'item']"
@confirm="remove()" @confirm="remove()"
> >
<i class="ui trash icon" /> <i class="ui trash icon" />
{{ $t('components.library.TrackBase.button.delete') }} {{ t('components.library.TrackBase.button.delete') }}
<template #modal-header> <template #modal-header>
<p> <p>
{{ $t('components.library.TrackBase.modal.delete.header') }} {{ t('components.library.TrackBase.modal.delete.header') }}
</p> </p>
</template> </template>
<template #modal-content> <template #modal-content>
<div> <div>
<p> <p>
{{ $t('components.library.TrackBase.modal.delete.content.warning') }} {{ t('components.library.TrackBase.modal.delete.content.warning') }}
</p> </p>
</div> </div>
</template> </template>
<template #modal-confirm> <template #modal-confirm>
<p> <p>
{{ $t('components.library.TrackBase.button.delete') }} {{ t('components.library.TrackBase.button.delete') }}
</p> </p>
</template> </template>
</dangerous-button> </dangerous-button>
@ -296,22 +303,22 @@ const remove = async () => {
</div> </div>
<div class="divider" /> <div class="divider" />
<router-link <router-link
v-if="$store.state.auth.availablePermissions['library']" v-if="store.state.auth.availablePermissions['library']"
class="basic item" class="basic item"
:to="{name: 'manage.library.tracks.detail', params: {id: track.id}}" :to="{name: 'manage.library.tracks.detail', params: {id: track.id}}"
> >
<i class="wrench icon" /> <i class="wrench icon" />
{{ $t('components.library.TrackBase.link.moderation') }} {{ t('components.library.TrackBase.link.moderation') }}
</router-link> </router-link>
<a <a
v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" v-if="store.state.auth.profile && store.state.auth.profile.is_superuser"
class="basic item" class="basic item"
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/track/${track.id}`)" :href="store.getters['instance/absoluteUrl'](`/api/admin/music/track/${track.id}`)"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<i class="wrench icon" /> <i class="wrench icon" />
{{ $t('components.library.TrackBase.link.django') }} {{ t('components.library.TrackBase.link.django') }}
</a> </a>
</div> </div>
</button> </button>
@ -321,7 +328,7 @@ const remove = async () => {
</section> </section>
<router-view <router-view
v-if="track" v-if="track"
:key="$route.fullPath" :key="route.fullPath"
:track="track" :track="track"
:object="track" :object="track"
object-type="track" object-type="track"

View File

@ -3,6 +3,8 @@ import type { Track, Library } from '~/types'
import { humanSize, momentFormat, truncate } from '~/utils/filters' import { humanSize, momentFormat, truncate } from '~/utils/filters'
import { computed, ref, watchEffect } from 'vue' import { computed, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import time from '~/utils/time' import time from '~/utils/time'
import axios from 'axios' import axios from 'axios'
@ -21,6 +23,9 @@ interface Props {
track: Track track: Track
} }
const store = useStore()
const { t } = useI18n()
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = defineProps<Props>() const props = defineProps<Props>()
@ -59,13 +64,13 @@ watchEffect(() => {
<template v-if="upload"> <template v-if="upload">
<img <img
v-if="track.cover && track.cover.urls.large_square_crop" v-if="track.cover && track.cover.urls.large_square_crop"
v-lazy="$store.getters['instance/absoluteUrl'](track.cover.urls.large_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](track.cover.urls.large_square_crop)"
alt="Cover Image" alt="Cover Image"
class="ui fluid image track-cover-image" class="ui fluid image track-cover-image"
> >
<img <img
v-else-if="track.album && track.album.cover && track.album.cover.urls.large_square_crop" v-else-if="track.album && track.album.cover && track.album.cover.urls.large_square_crop"
v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.urls.large_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](track.album.cover.urls.large_square_crop)"
alt="Cover Image" alt="Cover Image"
class="ui fluid image track-cover-image" class="ui fluid image track-cover-image"
> >
@ -77,69 +82,69 @@ watchEffect(() => {
> >
<h3 class="ui header"> <h3 class="ui header">
<span v-if="track.artist_credit?.[0].artist?.content_category === 'music'"> <span v-if="track.artist_credit?.[0].artist?.content_category === 'music'">
{{ $t('components.library.TrackDetail.header.track') }} {{ t('components.library.TrackDetail.header.track') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.library.TrackDetail.header.episode') }} {{ t('components.library.TrackDetail.header.episode') }}
</span> </span>
</h3> </h3>
<table class="ui basic table"> <table class="ui basic table">
<tbody> <tbody>
<tr> <tr>
<td> <td>
{{ $t('components.library.TrackDetail.table.track.duration') }} {{ t('components.library.TrackDetail.table.track.duration') }}
</td> </td>
<td class="right aligned"> <td class="right aligned">
<template v-if="upload.duration"> <template v-if="upload.duration">
{{ time.parse(upload.duration) }} {{ time.parse(upload.duration) }}
</template> </template>
<span v-else> <span v-else>
{{ $t('components.library.TrackDetail.notApplicable') }} {{ t('components.library.TrackDetail.notApplicable') }}
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.library.TrackDetail.table.track.size') }} {{ t('components.library.TrackDetail.table.track.size') }}
</td> </td>
<td class="right aligned"> <td class="right aligned">
<template v-if="upload.size"> <template v-if="upload.size">
{{ humanSize(upload.size) }} {{ humanSize(upload.size) }}
</template> </template>
<span v-else> <span v-else>
{{ $t('components.library.TrackDetail.notApplicable') }} {{ t('components.library.TrackDetail.notApplicable') }}
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.library.TrackDetail.table.track.codec') }} {{ t('components.library.TrackDetail.table.track.codec') }}
</td> </td>
<td class="right aligned"> <td class="right aligned">
<template v-if="upload.extension"> <template v-if="upload.extension">
{{ upload.extension }} {{ upload.extension }}
</template> </template>
<span v-else> <span v-else>
{{ $t('components.library.TrackDetail.notApplicable') }} {{ t('components.library.TrackDetail.notApplicable') }}
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.library.TrackDetail.table.track.bitrate.label') }} {{ t('components.library.TrackDetail.table.track.bitrate.label') }}
</td> </td>
<td class="right aligned"> <td class="right aligned">
<template v-if="upload.bitrate"> <template v-if="upload.bitrate">
{{ $t('components.library.TrackDetail.table.track.bitrate.value', {bitrate: humanSize(upload.bitrate)}) }} {{ t('components.library.TrackDetail.table.track.bitrate.value', {bitrate: humanSize(upload.bitrate)}) }}
</template> </template>
<span v-else> <span v-else>
{{ $t('components.library.TrackDetail.notApplicable') }} {{ t('components.library.TrackDetail.notApplicable') }}
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.library.TrackDetail.table.track.downloads') }} {{ t('components.library.TrackDetail.table.track.downloads') }}
</td> </td>
<td class="right aligned"> <td class="right aligned">
{{ track.downloads_count }} {{ track.downloads_count }}
@ -160,13 +165,13 @@ watchEffect(() => {
:can-update="false" :can-update="false"
/> />
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.library.TrackDetail.header.release') }} {{ t('components.library.TrackDetail.header.release') }}
</h2> </h2>
<table class="ui basic table ellipsis-rows"> <table class="ui basic table ellipsis-rows">
<tbody> <tbody>
<tr> <tr>
<td> <td>
{{ $t('components.library.TrackDetail.table.release.artist') }} {{ t('components.library.TrackDetail.table.release.artist') }}
</td> </td>
<td class="right aligned"> <td class="right aligned">
<template <template
@ -187,10 +192,10 @@ watchEffect(() => {
<tr v-if="track.album"> <tr v-if="track.album">
<td> <td>
<span v-if="track.album.artist_credit?.[0].artist.content_category === 'music'"> <span v-if="track.album.artist_credit?.[0].artist.content_category === 'music'">
{{ $t('components.library.TrackDetail.table.release.album') }} {{ t('components.library.TrackDetail.table.release.album') }}
</span> </span>
<span v-else> <span v-else>
{{ $t('components.library.TrackDetail.table.release.series') }} {{ t('components.library.TrackDetail.table.release.series') }}
</span> </span>
</td> </td>
<td class="right aligned"> <td class="right aligned">
@ -201,20 +206,20 @@ watchEffect(() => {
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.library.TrackDetail.table.release.year') }} {{ t('components.library.TrackDetail.table.release.year') }}
</td> </td>
<td class="right aligned"> <td class="right aligned">
<template v-if="track.album && track.album.release_date"> <template v-if="track.album && track.album.release_date">
{{ momentFormat(new Date(track.album.release_date), 'Y') }} {{ momentFormat(new Date(track.album.release_date), 'Y') }}
</template> </template>
<template v-else> <template v-else>
{{ $t('components.library.TrackDetail.notApplicable') }} {{ t('components.library.TrackDetail.notApplicable') }}
</template> </template>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.library.TrackDetail.table.release.copyright') }} {{ t('components.library.TrackDetail.table.release.copyright') }}
</td> </td>
<td class="right aligned"> <td class="right aligned">
<span <span
@ -222,13 +227,13 @@ watchEffect(() => {
:title="track.copyright" :title="track.copyright"
>{{ truncate(track.copyright, 50) }}</span> >{{ truncate(track.copyright, 50) }}</span>
<template v-else> <template v-else>
{{ $t('components.library.TrackDetail.notApplicable') }} {{ t('components.library.TrackDetail.notApplicable') }}
</template> </template>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
{{ $t('components.library.TrackDetail.table.release.license') }} {{ t('components.library.TrackDetail.table.release.license') }}
</td> </td>
<td class="right aligned"> <td class="right aligned">
<a <a
@ -238,13 +243,13 @@ watchEffect(() => {
rel="noopener noreferrer" rel="noopener noreferrer"
>{{ license.name }}</a> >{{ license.name }}</a>
<span v-else> <span v-else>
{{ $t('components.library.TrackDetail.notApplicable') }} {{ t('components.library.TrackDetail.notApplicable') }}
</span> </span>
</td> </td>
</tr> </tr>
<tr v-if="!track.is_local"> <tr v-if="!track.is_local">
<td> <td>
{{ $t('components.library.TrackDetail.table.release.url') }} {{ t('components.library.TrackDetail.table.release.url') }}
</td> </td>
<td :title="track.fid"> <td :title="track.fid">
<a <a
@ -265,10 +270,10 @@ watchEffect(() => {
rel="noreferrer noopener" rel="noreferrer noopener"
> >
<i class="external icon" /> <i class="external icon" />
{{ $t('components.library.TrackDetail.link.musicbrainz') }} {{ t('components.library.TrackDetail.link.musicbrainz') }}
</a> </a>
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.library.TrackDetail.header.playlists') }} {{ t('components.library.TrackDetail.header.playlists') }}
</h2> </h2>
<playlist-widget <playlist-widget
:url="'playlists/'" :url="'playlists/'"
@ -276,13 +281,13 @@ watchEffect(() => {
/> />
<h2 class="ui header"> <h2 class="ui header">
{{ $t('components.library.TrackDetail.header.library') }} {{ t('components.library.TrackDetail.header.library') }}
</h2> </h2>
<library-widget <library-widget
:url="`tracks/${track.id}/libraries/`" :url="`tracks/${track.id}/libraries/`"
@loaded="emit('libraries-loaded', $event)" @loaded="emit('libraries-loaded', $event)"
> >
{{ $t('components.library.TrackDetail.description.library') }} {{ t('components.library.TrackDetail.description.library') }}
</library-widget> </library-widget>
</div> </div>
</div> </div>

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