chore(eslint): apply automatic fixes to format front/src
This commit is contained in:
parent
d333b79ee1
commit
8b2a5e54ce
|
@ -4,8 +4,8 @@ import { watchEffect, computed, onMounted, nextTick } from 'vue'
|
|||
import { type QueueTrack, useQueue } from '~/composables/audio/queue'
|
||||
import { useStore } from '~/store'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
import { useStyleTag, useIntervalFn} from '@vueuse/core'
|
||||
import { color } from '~/composables/color';
|
||||
import { useStyleTag, useIntervalFn } from '@vueuse/core'
|
||||
import { color } from '~/composables/color'
|
||||
|
||||
import { generateTrackCreditStringFromQueue } from '~/utils/utils'
|
||||
|
||||
|
@ -80,8 +80,15 @@ store.dispatch('auth/fetchUser')
|
|||
<template>
|
||||
<div class="funkwhale responsive">
|
||||
<Sidebar />
|
||||
<RouterView v-bind="color({}, ['default', 'solid'])()" v-slot="{ Component }">
|
||||
<Transition v-if="Component" name="main" mode="out-in">
|
||||
<RouterView
|
||||
v-slot="{ Component }"
|
||||
v-bind="color({}, ['default', 'solid'])()"
|
||||
>
|
||||
<Transition
|
||||
v-if="Component"
|
||||
name="main"
|
||||
mode="out-in"
|
||||
>
|
||||
<KeepAlive :max="10">
|
||||
<Suspense>
|
||||
<component :is="Component" />
|
||||
|
|
|
@ -6,7 +6,6 @@ import { useQueue } from '~/composables/audio/queue'
|
|||
import { useStore } from '~/store'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
@ -91,9 +90,9 @@ const { width } = useWindowSize()
|
|||
</keep-alive>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Display a proper 404 page or error message -->
|
||||
<h1>404 - Page Not Found</h1>
|
||||
</template>
|
||||
<!-- Display a proper 404 page or error message -->
|
||||
<h1>404 - Page Not Found</h1>
|
||||
</template>
|
||||
</router-view>
|
||||
|
||||
<audio-player />
|
||||
|
|
|
@ -24,7 +24,7 @@ const labels = computed(() => ({
|
|||
title: t('components.About.title')
|
||||
}))
|
||||
|
||||
const podName = computed(() => (n => n === "" ? "No name" : n ?? 'Funkwhale')(get(nodeinfo.value, 'metadata.nodeName')))
|
||||
const podName = computed(() => (n => n === '' ? 'No name' : n ?? 'Funkwhale')(get(nodeinfo.value, 'metadata.nodeName')))
|
||||
|
||||
const banner = computed(() => get(nodeinfo.value, 'metadata.banner'))
|
||||
const shortDescription = computed(() => get(nodeinfo.value, 'metadata.shortDescription'))
|
||||
|
@ -84,14 +84,21 @@ const federationEnabled = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout stack main
|
||||
<Layout
|
||||
v-title="labels.title"
|
||||
stack
|
||||
main
|
||||
style="align-items: center;"
|
||||
>
|
||||
<!-- About funkwhale -->
|
||||
|
||||
<Link to="/" width="full" alignText="stretch" style="width:min(480px, 100%)">
|
||||
<logo-text/>
|
||||
<Link
|
||||
to="/"
|
||||
width="full"
|
||||
align-text="stretch"
|
||||
style="width:min(480px, 100%)"
|
||||
>
|
||||
<logo-text />
|
||||
</Link>
|
||||
|
||||
<h2 class="header">
|
||||
|
@ -102,13 +109,15 @@ const federationEnabled = computed(() => {
|
|||
{{ t('components.About.description.funkwhale') }}
|
||||
</p>
|
||||
|
||||
<Layout flex style="justify-content: center;">
|
||||
|
||||
<Card :title="t('components.About.header.signup')"
|
||||
<Layout
|
||||
flex
|
||||
style="justify-content: center;"
|
||||
>
|
||||
<Card
|
||||
v-if="!store.state.auth.authenticated"
|
||||
:title="t('components.About.header.signup')"
|
||||
width="256px"
|
||||
>
|
||||
|
||||
<template v-if="openRegistrations">
|
||||
<p>
|
||||
{{ t('components.About.description.signup') }}
|
||||
|
@ -155,23 +164,27 @@ const federationEnabled = computed(() => {
|
|||
</div>
|
||||
</Card>
|
||||
|
||||
<Card :title="t('components.About.message.greeting', {username: store.state.auth.username})"
|
||||
<Card
|
||||
v-else
|
||||
width= "256px"
|
||||
:title="t('components.About.message.greeting', {username: store.state.auth.username})"
|
||||
width="256px"
|
||||
>
|
||||
|
||||
<p v-if="defaultUploadQuota">
|
||||
{{ t('components.About.description.quota', {quota: defaultUploadQuota}) }}
|
||||
</p>
|
||||
|
||||
<template #action>
|
||||
<Button full disabled>
|
||||
<Button
|
||||
full
|
||||
disabled
|
||||
>
|
||||
{{ t('components.About.message.loggedIn') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
<Card :title="podName"
|
||||
<Card
|
||||
:title="podName"
|
||||
width="256px"
|
||||
>
|
||||
<section
|
||||
|
@ -222,20 +235,22 @@ const federationEnabled = computed(() => {
|
|||
</div>
|
||||
|
||||
<template #action>
|
||||
<Link alignText="center"
|
||||
<Link
|
||||
align-text="center"
|
||||
to="/about/pod"
|
||||
>
|
||||
{{ t('components.About.link.learnMore') }}
|
||||
</Link>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
</Layout>
|
||||
|
||||
|
||||
<Layout flex style="justify-content: center;">
|
||||
|
||||
<Card width="256px"
|
||||
<Layout
|
||||
flex
|
||||
style="justify-content: center;"
|
||||
>
|
||||
<Card
|
||||
width="256px"
|
||||
to="/"
|
||||
:title="t('components.About.header.publicContent')"
|
||||
icon="bi-box-arrow-up-right"
|
||||
|
@ -244,26 +259,25 @@ const federationEnabled = computed(() => {
|
|||
{{ t('components.About.description.publicContent') }}
|
||||
</Card>
|
||||
|
||||
<Card width="256px"
|
||||
<Card
|
||||
width="256px"
|
||||
:title="t('components.About.link.findOtherPod')"
|
||||
to="https://funkwhale.audio/#get-started"
|
||||
icon="bi-box-arrow-up-right"
|
||||
>
|
||||
{{ t('components.About.description.publicContent') }}
|
||||
{{ t('components.About.description.publicContent') }}
|
||||
</Card>
|
||||
|
||||
<Card width="256px"
|
||||
<Card
|
||||
width="256px"
|
||||
:title="t('components.About.header.findApp')"
|
||||
to="https://funkwhale.audio/apps"
|
||||
icon="bi-box-arrow-up-right"
|
||||
>
|
||||
{{ t('components.About.description.findApp') }}
|
||||
{{ t('components.About.description.findApp') }}
|
||||
</Card>
|
||||
|
||||
|
||||
</Layout>
|
||||
|
||||
|
||||
<section
|
||||
:class="['ui', 'head', {'with-background': banner}, 'vertical', 'center', 'aligned', 'stripe', 'segment']"
|
||||
:style="headerStyle"
|
||||
|
@ -274,7 +288,6 @@ const federationEnabled = computed(() => {
|
|||
</h1>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- About Pod -->
|
||||
<div class="about-pod-info-container">
|
||||
<div class="about-pod-info-toc">
|
||||
|
|
|
@ -246,8 +246,8 @@ if (!isWebGLSupported) {
|
|||
@click="enter"
|
||||
/>
|
||||
<Button
|
||||
secondary
|
||||
v-else
|
||||
secondary
|
||||
:aria-label="labels.exitFullscreen"
|
||||
:title="labels.exitFullscreen"
|
||||
icon="bi-fullscreen-exit"
|
||||
|
@ -305,7 +305,10 @@ if (!isWebGLSupported) {
|
|||
@click.stop.prevent=""
|
||||
>
|
||||
<Pill>
|
||||
<template #image v-if="ac.artist.cover">
|
||||
<template
|
||||
v-if="ac.artist.cover"
|
||||
#image
|
||||
>
|
||||
<img v-lazy="ac.artist.cover?.urls.medium_square_crop">
|
||||
</template>
|
||||
{{ ac.credit ?? t('components.Queue.meta.unknownArtist') }}
|
||||
|
@ -341,8 +344,14 @@ if (!isWebGLSupported) {
|
|||
<i class="loading spinner icon" />
|
||||
</p>
|
||||
</div>
|
||||
<Spacer :size="16" class="desktop-and-below" />
|
||||
<Layout flex class="additional-controls desktop-and-below">
|
||||
<Spacer
|
||||
:size="16"
|
||||
class="desktop-and-below"
|
||||
/>
|
||||
<Layout
|
||||
flex
|
||||
class="additional-controls desktop-and-below"
|
||||
>
|
||||
<track-favorite-icon
|
||||
v-if="store.state.auth.authenticated"
|
||||
:track="currentTrack"
|
||||
|
@ -428,7 +437,10 @@ if (!isWebGLSupported) {
|
|||
</template>
|
||||
</i18n-t>
|
||||
<span class="middle pipe symbol" />
|
||||
<span style="margin-right: 8px;" v-t="'components.Queue.meta.end'" />
|
||||
<span
|
||||
v-t="'components.Queue.meta.end'"
|
||||
style="margin-right: 8px;"
|
||||
/>
|
||||
<span :title="labels.duration">
|
||||
{{ endsIn }}
|
||||
</span>
|
||||
|
|
|
@ -170,8 +170,9 @@ watch(() => props.initialId, () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout stack
|
||||
<Layout
|
||||
v-if="type === 'both'"
|
||||
stack
|
||||
>
|
||||
<Button
|
||||
secondary
|
||||
|
@ -188,18 +189,23 @@ watch(() => props.initialId, () => {
|
|||
{{ t('components.RemoteSearchForm.button.rss') }}
|
||||
</Button>
|
||||
</Layout>
|
||||
<Layout form
|
||||
<Layout
|
||||
v-else
|
||||
id="remote-search"
|
||||
form
|
||||
:class="['ui', {loading: isLoading}, 'form']"
|
||||
@submit.stop.prevent="submit"
|
||||
>
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="errors.length > 0"
|
||||
red
|
||||
role="alert"
|
||||
title="t('components.RemoteSearchForm.header.fetchFailed')"
|
||||
>
|
||||
<ul class="list" v-if="errors.length > 1">
|
||||
<ul
|
||||
v-if="errors.length > 1"
|
||||
class="list"
|
||||
>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
|
@ -207,7 +213,9 @@ watch(() => props.initialId, () => {
|
|||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else>{{ errors[0] }}</p>
|
||||
<p v-else>
|
||||
{{ errors[0] }}
|
||||
</p>
|
||||
</Alert>
|
||||
<p v-if="type === 'rss'">
|
||||
{{ t('components.RemoteSearchForm.description.rss') }}
|
||||
|
@ -216,7 +224,6 @@ watch(() => props.initialId, () => {
|
|||
{{ t('components.RemoteSearchForm.description.fediverse') }}
|
||||
</p>
|
||||
|
||||
|
||||
<Input
|
||||
id="object-id"
|
||||
v-model="id"
|
||||
|
@ -237,8 +244,9 @@ watch(() => props.initialId, () => {
|
|||
{{ t('components.RemoteSearchForm.button.search') }}
|
||||
</Button>
|
||||
</Layout>
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="!isLoading && obj?.status === 'finished' && !redirectRoute"
|
||||
red
|
||||
>
|
||||
{{ t('components.RemoteSearchForm.warning.unsupported') }}
|
||||
</Alert>
|
||||
|
|
|
@ -65,8 +65,9 @@ const checkAndSwitch = async (url: string) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="t('views.ChooseInstance.header.chooseInstance')"
|
||||
<Modal
|
||||
v-model="show"
|
||||
:title="t('views.ChooseInstance.header.chooseInstance')"
|
||||
@update="isError = false"
|
||||
>
|
||||
<h3 class="header">
|
||||
|
|
|
@ -273,9 +273,9 @@ onMounted(() => {
|
|||
@show-language-modal-event="isLanguageModalOpen=true"
|
||||
/>
|
||||
<Modal
|
||||
:title="labels.language"
|
||||
ref="languageModal"
|
||||
v-model="isLanguageModalOpen"
|
||||
:title="labels.language"
|
||||
:fullscreen="false"
|
||||
>
|
||||
<!-- TODO: Is this actually a popover menu, not a modal? -->
|
||||
|
@ -306,9 +306,9 @@ onMounted(() => {
|
|||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
:title="labels.theme"
|
||||
ref="themeModal"
|
||||
v-model:show="isThemeModalOpen"
|
||||
:title="labels.theme"
|
||||
:fullscreen="false"
|
||||
>
|
||||
<i
|
||||
|
|
|
@ -192,8 +192,8 @@ const move = (idx: number, increment: number) => {
|
|||
</table>
|
||||
<div class="ui hidden divider" />
|
||||
<Button
|
||||
color = "primary"
|
||||
v-if="value.fields?.length < maxFields"
|
||||
color="primary"
|
||||
@click.stop.prevent="addField"
|
||||
>
|
||||
{{ t('components.admin.SignupFormBuilder.button.add') }}
|
||||
|
|
|
@ -51,8 +51,9 @@ const imageUrl = computed(() => props.album.cover?.urls.original
|
|||
/>
|
||||
</template>
|
||||
|
||||
<template #default
|
||||
<template
|
||||
v-for="ac in album.artist_credit"
|
||||
#default
|
||||
:key="ac.artist.id"
|
||||
>
|
||||
<Link
|
||||
|
@ -68,11 +69,14 @@ const imageUrl = computed(() => props.album.cover?.urls.original
|
|||
<span v-if="album.release_date">
|
||||
{{ momentFormat(new Date(album.release_date), 'Y') }}
|
||||
</span>
|
||||
<i class="bi bi-dot"/>
|
||||
<i class="bi bi-dot" />
|
||||
<span>
|
||||
{{ t('components.audio.album.Card.meta.tracks', album.tracks_count) }}
|
||||
</span>
|
||||
<Spacer h grow />
|
||||
<Spacer
|
||||
h
|
||||
grow
|
||||
/>
|
||||
<PlayButton
|
||||
:dropdown-only="true"
|
||||
discrete
|
||||
|
|
|
@ -14,7 +14,6 @@ import Section from '~/components/ui/Section.vue'
|
|||
import Loader from '~/components/ui/Loader.vue'
|
||||
import Pagination from '~/components/ui/Pagination.vue'
|
||||
|
||||
|
||||
interface Props {
|
||||
filters: Record<string, string | boolean>
|
||||
showCount?: boolean
|
||||
|
@ -82,17 +81,17 @@ watch(
|
|||
>
|
||||
<inline-search-bar
|
||||
v-if="search"
|
||||
style="grid-column: 1 / -1;"
|
||||
v-model="query"
|
||||
style="grid-column: 1 / -1;"
|
||||
@search="performSearch"
|
||||
/>
|
||||
<Loader v-if="isLoading" />
|
||||
<template v-if="!isLoading && albums.length > 0">
|
||||
<album-card
|
||||
v-for="album in albums"
|
||||
:key="album.id"
|
||||
:album="album"
|
||||
/>
|
||||
<album-card
|
||||
v-for="album in albums"
|
||||
:key="album.id"
|
||||
:album="album"
|
||||
/>
|
||||
</template>
|
||||
<slot
|
||||
v-if="!isLoading && albums.length === 0"
|
||||
|
@ -100,8 +99,8 @@ watch(
|
|||
>
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData"
|
||||
style="grid-column: 1 / -1;"
|
||||
@refresh="fetchData"
|
||||
/>
|
||||
</slot>
|
||||
<Pagination
|
||||
|
|
|
@ -67,7 +67,7 @@ const cover = computed(() => {
|
|||
v-lazy="cover.urls.medium_square_crop"
|
||||
:alt="artist.name"
|
||||
class="channel-image"
|
||||
/>
|
||||
>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
|
@ -85,7 +85,6 @@ const cover = computed(() => {
|
|||
discrete
|
||||
/>
|
||||
</template>
|
||||
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import type { Artist } from '~/types'
|
|||
import { reactive, ref, watch, onMounted } from 'vue'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
|
@ -14,7 +13,6 @@ import ArtistCard from '~/components/artist/Card.vue'
|
|||
import Section from '~/components/ui/Section.vue'
|
||||
import Pagination from '~/components/ui/Pagination.vue'
|
||||
|
||||
|
||||
interface Props {
|
||||
filters: Record<string, string | boolean>
|
||||
search?: boolean
|
||||
|
@ -97,14 +95,14 @@ watch(
|
|||
<inline-search-bar
|
||||
v-if="!isLoading && search"
|
||||
v-model="query"
|
||||
@search="performSearch"
|
||||
style="grid-column: 1 / -1;"
|
||||
@search="performSearch"
|
||||
/>
|
||||
<artist-card
|
||||
v-for="artist in artists"
|
||||
:key="artist.id"
|
||||
:artist="artist"
|
||||
/>
|
||||
<artist-card
|
||||
v-for="artist in artists"
|
||||
:key="artist.id"
|
||||
:artist="artist"
|
||||
/>
|
||||
<Pagination
|
||||
v-if="artists && count > limit"
|
||||
v-model:page="page"
|
||||
|
|
|
@ -28,7 +28,11 @@ const getRoute = (ac: ArtistCredit) => {
|
|||
v-for="ac in props.artistCredit"
|
||||
:key="ac.artist.id"
|
||||
>
|
||||
<Link solid secondary round min-content
|
||||
<Link
|
||||
solid
|
||||
secondary
|
||||
round
|
||||
min-content
|
||||
:to="getRoute(ac)"
|
||||
>
|
||||
<img
|
||||
|
|
|
@ -66,11 +66,11 @@ const updatedAgo = computed(() => moment(props.object.artist?.modification_date)
|
|||
v-lazy="imageUrl"
|
||||
:alt="object.artist?.name"
|
||||
class="channel-image"
|
||||
/>
|
||||
>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<Spacer :size="8"/>
|
||||
<Spacer :size="8" />
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
|
@ -89,7 +89,10 @@ const updatedAgo = computed(() => moment(props.object.artist?.modification_date)
|
|||
<span v-else>
|
||||
{{ t('components.audio.ChannelCard.meta.tracks', object.artist?.tracks_count ?? 0) }}
|
||||
</span>
|
||||
<Spacer h grow />
|
||||
<Spacer
|
||||
h
|
||||
grow
|
||||
/>
|
||||
<PlayButton
|
||||
:dropdown-only="true"
|
||||
:is-playable="true"
|
||||
|
|
|
@ -9,7 +9,7 @@ import axios from 'axios'
|
|||
import PodcastTable from '~/components/audio/podcast/Table.vue'
|
||||
import TrackTable from '~/components/audio/track/Table.vue'
|
||||
|
||||
import Loader from'~/components/ui/Loader.vue'
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
|
||||
interface Events {
|
||||
(e: 'fetched', data: BackendResponse<Track[]>): void
|
||||
|
@ -66,44 +66,44 @@ watch(page, fetchData, { immediate: true })
|
|||
<div>
|
||||
<slot />
|
||||
<Loader v-if="isLoading" />
|
||||
</div>
|
||||
<podcast-table
|
||||
v-if="isPodcast"
|
||||
v-model:page="page"
|
||||
:paginate-by="limit"
|
||||
:default-cover="defaultCover"
|
||||
:is-podcast="isPodcast"
|
||||
:show-art="true"
|
||||
:show-position="false"
|
||||
:tracks="channels"
|
||||
:show-artist="false"
|
||||
:show-album="false"
|
||||
:paginate-results="true"
|
||||
:total="count"
|
||||
/>
|
||||
<track-table
|
||||
v-else
|
||||
v-model:page="page"
|
||||
:default-cover="defaultCover"
|
||||
:is-podcast="isPodcast"
|
||||
:show-art="true"
|
||||
:show-position="false"
|
||||
:tracks="channels"
|
||||
:show-artist="false"
|
||||
:show-album="false"
|
||||
:paginate-results="true"
|
||||
:total="count"
|
||||
:paginate-by="limit"
|
||||
:filters="filters"
|
||||
/>
|
||||
<template v-if="!isLoading && channels.length === 0">
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData()"
|
||||
>
|
||||
<p>
|
||||
{{ t('components.audio.ChannelEntries.help.subscribe') }}
|
||||
</p>
|
||||
</empty-state>
|
||||
</template>
|
||||
</div>
|
||||
<podcast-table
|
||||
v-if="isPodcast"
|
||||
v-model:page="page"
|
||||
:paginate-by="limit"
|
||||
:default-cover="defaultCover"
|
||||
:is-podcast="isPodcast"
|
||||
:show-art="true"
|
||||
:show-position="false"
|
||||
:tracks="channels"
|
||||
:show-artist="false"
|
||||
:show-album="false"
|
||||
:paginate-results="true"
|
||||
:total="count"
|
||||
/>
|
||||
<track-table
|
||||
v-else
|
||||
v-model:page="page"
|
||||
:default-cover="defaultCover"
|
||||
:is-podcast="isPodcast"
|
||||
:show-art="true"
|
||||
:show-position="false"
|
||||
:tracks="channels"
|
||||
:show-artist="false"
|
||||
:show-album="false"
|
||||
:paginate-results="true"
|
||||
:total="count"
|
||||
:paginate-by="limit"
|
||||
:filters="filters"
|
||||
/>
|
||||
<template v-if="!isLoading && channels.length === 0">
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData()"
|
||||
>
|
||||
<p>
|
||||
{{ t('components.audio.ChannelEntries.help.subscribe') }}
|
||||
</p>
|
||||
</empty-state>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
@ -8,7 +8,6 @@ import { useQueue } from '~/composables/audio/queue'
|
|||
import { useStore } from '~/store'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
|
||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||
|
||||
|
|
|
@ -164,7 +164,8 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout form
|
||||
<Layout
|
||||
form
|
||||
class="ui form"
|
||||
@submit.prevent.stop="submit"
|
||||
>
|
||||
|
|
|
@ -76,22 +76,22 @@ fetchData()
|
|||
</template>
|
||||
<template v-if="nextPage">
|
||||
<Button
|
||||
secondary
|
||||
v-if="nextPage"
|
||||
secondary
|
||||
@click="fetchData(nextPage)"
|
||||
>
|
||||
{{ t('components.audio.ChannelSeries.button.showMore') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Layout>
|
||||
<template v-if="!isLoading && albums.length === 0">
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData()"
|
||||
>
|
||||
<p>
|
||||
{{ t('components.audio.ChannelSeries.help.subscribe') }}
|
||||
</p>
|
||||
</empty-state>
|
||||
</template>
|
||||
<template v-if="!isLoading && albums.length === 0">
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData()"
|
||||
>
|
||||
<p>
|
||||
{{ t('components.audio.ChannelSeries.help.subscribe') }}
|
||||
</p>
|
||||
</empty-state>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
@ -26,7 +26,7 @@ interface Props {
|
|||
|
||||
const emit = defineEmits<Events>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
limit: 5,
|
||||
limit: 5
|
||||
})
|
||||
|
||||
const result = ref<PaginatedChannelList>()
|
||||
|
@ -43,7 +43,7 @@ const fetchData = async (url = 'channels/') => {
|
|||
const params: operations['get_channels_2']['parameters']['query'] = {
|
||||
...clone(props.filters),
|
||||
page: page.value,
|
||||
page_size: props.limit,
|
||||
page_size: props.limit
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -75,14 +75,17 @@ watch([() => props.filters, page],
|
|||
small-items
|
||||
:h2="title || undefined"
|
||||
>
|
||||
<Loader v-if="isLoading" style="grid-column: 1 / -1;" />
|
||||
<Loader
|
||||
v-if="isLoading"
|
||||
style="grid-column: 1 / -1;"
|
||||
/>
|
||||
<template
|
||||
v-if="!isLoading && result?.count === 0"
|
||||
>
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData('channels/')"
|
||||
style="grid-column: 1 / -1;"
|
||||
@refresh="fetchData('channels/')"
|
||||
/>
|
||||
</template>
|
||||
<Pagination
|
||||
|
|
|
@ -14,7 +14,6 @@ import OptionsButton from '~/components/ui/button/Options.vue'
|
|||
import Popover from '~/components/ui/Popover.vue'
|
||||
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
|
||||
|
||||
|
||||
interface Props extends PlayOptionsProps {
|
||||
split?: boolean
|
||||
dropdownIconClasses?: string[]
|
||||
|
@ -110,8 +109,8 @@ const isOpen = ref(false)
|
|||
<OptionsButton
|
||||
v-if="dropdownOnly"
|
||||
v-bind="$attrs"
|
||||
:is-ghost="discrete"
|
||||
@click="isOpen = !isOpen"
|
||||
:isGhost="discrete"
|
||||
/>
|
||||
<Button
|
||||
v-else
|
||||
|
@ -124,7 +123,7 @@ const isOpen = ref(false)
|
|||
:aria-label="labels.replacePlay"
|
||||
:class="[...buttonClasses, 'play-button']"
|
||||
:isloading="isLoading"
|
||||
:dropdownOnly="dropdownOnly"
|
||||
:dropdown-only="dropdownOnly"
|
||||
@click.stop.prevent="replacePlay()"
|
||||
@split-click="isOpen = !isOpen"
|
||||
>
|
||||
|
@ -137,7 +136,9 @@ const isOpen = ref(false)
|
|||
v-else
|
||||
:class="['bi', playIconClass]"
|
||||
/>
|
||||
<template v-if="!discrete && !iconOnly"> <slot>{{ t('components.audio.PlayButton.button.discretePlay') }}</slot></template>
|
||||
<template v-if="!discrete && !iconOnly">
|
||||
<slot>{{ t('components.audio.PlayButton.button.discretePlay') }}</slot>
|
||||
</template>
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
|
@ -200,7 +201,7 @@ const isOpen = ref(false)
|
|||
</span>
|
||||
</PopoverItem>
|
||||
|
||||
<hr v-if="filterableArtists || Object.keys(getReportableObjects({track, album, artist, playlist, account, channel})).length > 0" />
|
||||
<hr v-if="filterableArtists || Object.keys(getReportableObjects({track, album, artist, playlist, account, channel})).length > 0">
|
||||
|
||||
<PopoverItem
|
||||
v-if="filterableArtists"
|
||||
|
|
|
@ -51,9 +51,11 @@ const { t } = useI18n()
|
|||
/** Toggle between null and player */
|
||||
const togglePlayer = () => {
|
||||
store.commit('ui/queueFocused',
|
||||
store.state.ui.queueFocused === 'queue' ? null
|
||||
: store.state.ui.queueFocused === 'player' ? null
|
||||
: 'player'
|
||||
store.state.ui.queueFocused === 'queue'
|
||||
? null
|
||||
: store.state.ui.queueFocused === 'player'
|
||||
? null
|
||||
: 'player'
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -166,11 +168,11 @@ const hideArtist = () => {
|
|||
class="ui tiny image"
|
||||
@click.stop.prevent="router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})"
|
||||
>
|
||||
<!-- TODO: Use smaller covers -->
|
||||
<!-- TODO: Use smaller covers -->
|
||||
<img
|
||||
ref="cover"
|
||||
alt=""
|
||||
v-lazy="store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
|
||||
alt=""
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
|
@ -217,11 +219,11 @@ const hideArtist = () => {
|
|||
</div>
|
||||
<div class="controls track-controls queue-not-focused desktop-and-below">
|
||||
<div class="ui tiny image">
|
||||
<!-- TODO: Use smaller covers -->
|
||||
<!-- TODO: Use smaller covers -->
|
||||
<img
|
||||
ref="cover"
|
||||
alt=""
|
||||
v-lazy="store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
|
||||
alt=""
|
||||
>
|
||||
</div>
|
||||
<div class="middle aligned content ellipsis">
|
||||
|
@ -358,8 +360,7 @@ const hideArtist = () => {
|
|||
class="close-control desktop-and-below"
|
||||
icon="bi-x"
|
||||
@click.stop="store.commit('ui/queueFocused', null)"
|
||||
>
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -72,7 +72,7 @@ const labels = computed(() => ({
|
|||
|
||||
<template>
|
||||
<div>
|
||||
/front/src/components/audio/Search.vue
|
||||
/front/src/components/audio/Search.vue
|
||||
<h2>
|
||||
{{ t('components.audio.Search.header.search') }}
|
||||
</h2>
|
||||
|
|
|
@ -9,7 +9,6 @@ import { usePlayer } from '~/composables/audio/player'
|
|||
import { useQueue } from '~/composables/audio/queue'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
|
||||
import usePlayOptions from '~/composables/audio/usePlayOptions'
|
||||
|
||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||
|
|
|
@ -93,9 +93,9 @@ const labels = computed(() => ({
|
|||
|
||||
<template>
|
||||
<Modal
|
||||
:title="track.title"
|
||||
ref="modal"
|
||||
v-model="show"
|
||||
:title="track.title"
|
||||
:scrolling="true"
|
||||
class="scrolling-track-options"
|
||||
>
|
||||
|
|
|
@ -9,7 +9,6 @@ import { usePlayer } from '~/composables/audio/player'
|
|||
import { useQueue } from '~/composables/audio/queue'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
|
||||
import usePlayOptions from '~/composables/audio/usePlayOptions'
|
||||
|
||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||
|
|
|
@ -93,9 +93,10 @@ const labels = computed(() => ({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="track.title"
|
||||
<Modal
|
||||
ref="modal"
|
||||
v-model="show"
|
||||
:title="track.title"
|
||||
:scrolling="true"
|
||||
class="scrolling-track-options"
|
||||
>
|
||||
|
|
|
@ -13,7 +13,6 @@ import { usePlayer } from '~/composables/audio/player'
|
|||
import { useQueue } from '~/composables/audio/queue'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
|
||||
const store = useStore()
|
||||
|
||||
interface Props extends PlayOptionsProps {
|
||||
|
@ -65,10 +64,10 @@ const hover = ref(false)
|
|||
<template>
|
||||
<div
|
||||
:class="[{ active }, 'track-row row', $style.row]"
|
||||
style="display: contents;"
|
||||
@dblclick="activateTrack(track, index)"
|
||||
@mousemove="hover = true"
|
||||
@mouseout="hover = false"
|
||||
style="display: contents;"
|
||||
>
|
||||
<!-- 1. column: Play button or track position -->
|
||||
|
||||
|
@ -153,63 +152,62 @@ const hover = ref(false)
|
|||
</div>
|
||||
|
||||
<!-- third column: title ! -->
|
||||
<div
|
||||
tabindex="0"
|
||||
class="content ellipsis column"
|
||||
<div
|
||||
tabindex="0"
|
||||
class="content ellipsis column"
|
||||
>
|
||||
<a
|
||||
@click="activateTrack(track, index)"
|
||||
>
|
||||
<a
|
||||
@click="activateTrack(track, index)"
|
||||
>
|
||||
{{ track.title }}
|
||||
</a>
|
||||
</div>
|
||||
{{ track.title }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 4. column: album link -->
|
||||
<!-- 4. column: album link -->
|
||||
|
||||
<div
|
||||
class="content ellipsis column"
|
||||
<div
|
||||
class="content ellipsis column"
|
||||
>
|
||||
<router-link
|
||||
v-if="showAlbum"
|
||||
:to="{ name: 'library.albums.detail', params: { id: track.album?.id } }"
|
||||
>
|
||||
{{ track.album?.title }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 5. column: artist link -->
|
||||
<div
|
||||
class="content ellipsis column"
|
||||
>
|
||||
<template
|
||||
v-for="ac in track.artist_credit"
|
||||
v-if="showArtist"
|
||||
:key="ac.artist.id"
|
||||
>
|
||||
<router-link
|
||||
v-if="showAlbum"
|
||||
:to="{ name: 'library.albums.detail', params: { id: track.album?.id } }"
|
||||
class="artist link"
|
||||
:to="{
|
||||
name: 'library.artists.detail',
|
||||
params: { id: ac.artist?.id },
|
||||
}"
|
||||
>
|
||||
{{ track.album?.title }}
|
||||
{{ ac.credit }}
|
||||
</router-link>
|
||||
</div>
|
||||
<span>{{ ac.joinphrase }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 5. column: artist link -->
|
||||
<div
|
||||
class="content ellipsis column"
|
||||
>
|
||||
<template
|
||||
v-if="showArtist"
|
||||
v-for="ac in track.artist_credit"
|
||||
:key="ac.artist.id"
|
||||
>
|
||||
<router-link
|
||||
class="artist link"
|
||||
:to="{
|
||||
name: 'library.artists.detail',
|
||||
params: { id: ac.artist?.id },
|
||||
}"
|
||||
>
|
||||
{{ ac.credit }}
|
||||
</router-link>
|
||||
<span>{{ ac.joinphrase }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 6. column: favorite icon -->
|
||||
<div
|
||||
class="meta column"
|
||||
>
|
||||
<track-favorite-icon
|
||||
v-if="store.state.auth.authenticated"
|
||||
ghost
|
||||
:track="track"
|
||||
/>
|
||||
</div>
|
||||
<!-- 6. column: favorite icon -->
|
||||
<div
|
||||
class="meta column"
|
||||
>
|
||||
<track-favorite-icon
|
||||
v-if="store.state.auth.authenticated"
|
||||
ghost
|
||||
:track="track"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 7. column: duration -->
|
||||
<div
|
||||
|
|
|
@ -2,11 +2,9 @@
|
|||
import type { Track } from '~/types'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { clone, uniqBy } from 'lodash-es'
|
||||
import { clone, uniqBy, sortedUniq } from 'lodash-es'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useStore } from '~/store'
|
||||
import { sortedUniq } from 'lodash-es'
|
||||
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
|
@ -151,15 +149,15 @@ const updatePage = (page: number) => {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<!-- Show the search bar if search is true -->
|
||||
|
||||
<Input search
|
||||
<Input
|
||||
v-if="search"
|
||||
v-model="query"
|
||||
@search="performSearch"
|
||||
search
|
||||
autofocus
|
||||
:placeholder="labels.searchPlaceholder"
|
||||
@search="performSearch"
|
||||
/>
|
||||
<Spacer v-if="search" />
|
||||
|
||||
|
@ -206,7 +204,10 @@ const updatePage = (page: number) => {
|
|||
</label>
|
||||
<label />
|
||||
<label>
|
||||
<i v-if="showDuration" class="bi bi-clock" />
|
||||
<i
|
||||
v-if="showDuration"
|
||||
class="bi bi-clock"
|
||||
/>
|
||||
</label>
|
||||
<label />
|
||||
</template>
|
||||
|
@ -225,7 +226,6 @@ const updatePage = (page: number) => {
|
|||
:show-duration="showDuration"
|
||||
:is-podcast="isPodcast"
|
||||
/>
|
||||
|
||||
</Table>
|
||||
|
||||
<!-- Pagination -->
|
||||
|
|
|
@ -22,7 +22,6 @@ import Pagination from '~/components/ui/Pagination.vue'
|
|||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
|
||||
|
||||
interface Events {
|
||||
(e: 'count', count: number): void
|
||||
}
|
||||
|
@ -97,19 +96,19 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
|||
// Handle WebSocket events for "Listen"
|
||||
|
||||
// Add the event to `objects` reactively
|
||||
objects.unshift(event as Listening);
|
||||
objects.unshift(event as Listening)
|
||||
|
||||
// Keep the array size within limits (e.g., remove the last item if needed)
|
||||
if (objects.length > props.limit) {
|
||||
objects.pop();
|
||||
objects.pop()
|
||||
}
|
||||
|
||||
// Recompute coverUrl for the updated `objects`
|
||||
console.log('WebSocket event received:', event);
|
||||
console.log('Updated cover URL:', coverUrl.value);
|
||||
});
|
||||
console.log('WebSocket event received:', event)
|
||||
console.log('Updated cover URL:', coverUrl.value)
|
||||
})
|
||||
}
|
||||
}, { immediate: true });
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -118,7 +117,8 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
|||
medium-items
|
||||
align-left
|
||||
>
|
||||
<Loader v-if="isLoading"
|
||||
<Loader
|
||||
v-if="isLoading"
|
||||
style="grid-column: 1 / -1;"
|
||||
/>
|
||||
<Alert
|
||||
|
@ -133,11 +133,13 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
|||
</h4>
|
||||
</Alert>
|
||||
<!-- TODO: Use activity.vue -->
|
||||
<div class="funkwhale activity"
|
||||
v-if="count > 0"
|
||||
<div
|
||||
v-for="object in objects"
|
||||
v-if="count > 0"
|
||||
:key="object.id"
|
||||
:class="['item', itemClasses]">
|
||||
class="funkwhale activity"
|
||||
:class="['item', itemClasses]"
|
||||
>
|
||||
<div class="activity-image">
|
||||
<img
|
||||
v-if="object.track.album && object.track.album.cover"
|
||||
|
@ -161,13 +163,16 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
|||
<!-- TODO: Add Playbutton overlay -->
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<router-link
|
||||
class="funkwhale link artist"
|
||||
:to="{name: 'library.tracks.detail', params: {id: object.track.id}}"
|
||||
>
|
||||
<Heading :h3="object.track.title" title />
|
||||
</router-link>
|
||||
<Spacer :size="2"/>
|
||||
<router-link
|
||||
class="funkwhale link artist"
|
||||
:to="{name: 'library.tracks.detail', params: {id: object.track.id}}"
|
||||
>
|
||||
<Heading
|
||||
:h3="object.track.title"
|
||||
title
|
||||
/>
|
||||
</router-link>
|
||||
<Spacer :size="2" />
|
||||
<div
|
||||
v-if="object.track.artist_credit"
|
||||
class="funkwhale link artist"
|
||||
|
@ -192,7 +197,7 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
|||
:show-more="false"
|
||||
:tags="object.track.tags"
|
||||
/>
|
||||
<Spacer :size="4"/>
|
||||
<Spacer :size="4" />
|
||||
<div
|
||||
v-if="isActivity"
|
||||
class="extra"
|
||||
|
@ -222,7 +227,6 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
|||
</Section>
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.funkwhale {
|
||||
&.activity {
|
||||
|
|
|
@ -114,8 +114,9 @@ whenever(() => props.clientId, fetchApplication, { immediate: true })
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<layout main
|
||||
<layout
|
||||
v-title="labels.title"
|
||||
main
|
||||
class="main"
|
||||
>
|
||||
<section class="ui vertical stripe segment">
|
||||
|
@ -123,8 +124,9 @@ whenever(() => props.clientId, fetchApplication, { immediate: true })
|
|||
<h2>
|
||||
<i class="bi bi-unlock-fill" />{{ t('components.auth.Authorize.header.authorize') }}
|
||||
</h2>
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="errors.length > 0"
|
||||
red
|
||||
role="alert"
|
||||
>
|
||||
<h4
|
||||
|
|
|
@ -75,72 +75,85 @@ const submit = async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout form stack
|
||||
<Layout
|
||||
form
|
||||
stack
|
||||
style="max-width: 600px"
|
||||
@submit.prevent="submit()"
|
||||
>
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="errors.length > 0"
|
||||
red
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.auth.LoginForm.header.loginFailure') }}
|
||||
</h4>
|
||||
<component :is="errors.length>1 ? 'ul' : 'div'" class="list">
|
||||
<component :is="errors.length>1 ? 'li' : 'div'"
|
||||
<component
|
||||
:is="errors.length>1 ? 'ul' : 'div'"
|
||||
class="list"
|
||||
>
|
||||
<component
|
||||
:is="errors.length>1 ? 'li' : 'div'"
|
||||
v-if="errors[0] == 'invalid_credentials' && store.state.instance.settings.moderation.signup_approval_enabled.value"
|
||||
>
|
||||
{{ t('components.auth.LoginForm.help.approvalRequired') }}
|
||||
</component>
|
||||
<component :is="errors.length>1 ? 'li' : 'div'"
|
||||
v-else-if="errors[0] == 'invalid_credentials'">
|
||||
<component
|
||||
:is="errors.length>1 ? 'li' : 'div'"
|
||||
v-else-if="errors[0] == 'invalid_credentials'"
|
||||
>
|
||||
{{ t('components.auth.LoginForm.help.invalidCredentials') }}
|
||||
</component>
|
||||
<component :is="errors.length>1 ? 'li' : 'div'" v-else>
|
||||
<component
|
||||
:is="errors.length>1 ? 'li' : 'div'"
|
||||
v-else
|
||||
>
|
||||
{{ errors[0] }}
|
||||
</component>
|
||||
</component>
|
||||
</Alert>
|
||||
<Spacer />
|
||||
<template v-if="domain === store.getters['instance/domain']">
|
||||
<Input
|
||||
id="username-field"
|
||||
ref="username"
|
||||
autocomplete="username"
|
||||
v-model="credentials.username"
|
||||
required
|
||||
name="username"
|
||||
type="text"
|
||||
autofocus
|
||||
:placeholder="labels.usernamePlaceholder"
|
||||
>
|
||||
<template #label>
|
||||
{{ t('components.auth.LoginForm.label.username') }}
|
||||
<template v-if="showSignup">
|
||||
<span class="middle pipe symbol" />
|
||||
<router-link :to="{ path: '/signup' }">
|
||||
{{ t('components.auth.LoginForm.link.createAccount') }}
|
||||
</router-link>
|
||||
</template>
|
||||
</template>
|
||||
</Input>
|
||||
<Input password
|
||||
name="password-field"
|
||||
autocomplete="current-password"
|
||||
v-model="credentials.password"
|
||||
field-id="password-field"
|
||||
required
|
||||
>
|
||||
<template #label>
|
||||
{{ t('components.auth.LoginForm.label.password') }}
|
||||
<Input
|
||||
id="username-field"
|
||||
ref="username"
|
||||
v-model="credentials.username"
|
||||
autocomplete="username"
|
||||
required
|
||||
name="username"
|
||||
type="text"
|
||||
autofocus
|
||||
:placeholder="labels.usernamePlaceholder"
|
||||
>
|
||||
<template #label>
|
||||
{{ t('components.auth.LoginForm.label.username') }}
|
||||
<template v-if="showSignup">
|
||||
<span class="middle pipe symbol" />
|
||||
<router-link
|
||||
tabindex="1"
|
||||
:to="{ name: 'auth.password-reset', query: { email: credentials.username } }"
|
||||
>
|
||||
{{ t('components.auth.LoginForm.link.resetPassword') }}
|
||||
<router-link :to="{ path: '/signup' }">
|
||||
{{ t('components.auth.LoginForm.link.createAccount') }}
|
||||
</router-link>
|
||||
</template>
|
||||
</Input>
|
||||
</template>
|
||||
</Input>
|
||||
<Input
|
||||
v-model="credentials.password"
|
||||
password
|
||||
name="password-field"
|
||||
autocomplete="current-password"
|
||||
field-id="password-field"
|
||||
required
|
||||
>
|
||||
<template #label>
|
||||
{{ t('components.auth.LoginForm.label.password') }}
|
||||
<span class="middle pipe symbol" />
|
||||
<router-link
|
||||
tabindex="1"
|
||||
:to="{ name: 'auth.password-reset', query: { email: credentials.username } }"
|
||||
>
|
||||
{{ t('components.auth.LoginForm.link.resetPassword') }}
|
||||
</router-link>
|
||||
</template>
|
||||
</Input>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>
|
||||
|
|
|
@ -40,8 +40,9 @@ const labels = computed(() => ({
|
|||
{{ t('components.auth.Logout.button.logout') }}
|
||||
</Button>
|
||||
</div>
|
||||
<Alert yellow
|
||||
<Alert
|
||||
v-else
|
||||
yellow
|
||||
>
|
||||
<h2>
|
||||
{{ t('components.auth.Logout.header.unauthenticated') }}
|
||||
|
@ -49,7 +50,7 @@ const labels = computed(() => ({
|
|||
<Link
|
||||
solid
|
||||
secondary
|
||||
buttonWidth
|
||||
button-width
|
||||
to="/login"
|
||||
>
|
||||
{{ t('components.auth.Logout.link.login') }}
|
||||
|
|
|
@ -272,23 +272,27 @@ fetchOwnedApps()
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout main stack
|
||||
<Layout
|
||||
v-title="labels.title"
|
||||
main
|
||||
stack
|
||||
>
|
||||
<Header :h1="t('components.auth.Settings.header.accountSettings')">
|
||||
</Header>
|
||||
<Layout form
|
||||
<Header :h1="t('components.auth.Settings.header.accountSettings')" />
|
||||
<Layout
|
||||
form
|
||||
@submit.prevent="submitSettings()"
|
||||
>
|
||||
<Alert green
|
||||
<Alert
|
||||
v-if="settings.success"
|
||||
green
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.auth.Settings.header.settingsUpdated') }}
|
||||
</h4>
|
||||
</Alert>
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="settings.errors.length > 0"
|
||||
red
|
||||
role="alert"
|
||||
>
|
||||
<h4 class="header">
|
||||
|
@ -340,466 +344,472 @@ fetchOwnedApps()
|
|||
{{ t('components.auth.Settings.button.updateSettings') }}
|
||||
</Button>
|
||||
</Layout>
|
||||
<section class="ui text container">
|
||||
<h2 class="ui header">
|
||||
{{ t('components.auth.Settings.header.avatar') }}
|
||||
</h2>
|
||||
<Layout form>
|
||||
<Alert red
|
||||
v-if="avatarErrors.length > 0"
|
||||
role="alert"
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.auth.Settings.header.avatarFailure') }}
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li
|
||||
v-for="(error, key) in avatarErrors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
<attachment-input
|
||||
v-model="avatar.uuid"
|
||||
:initial-value="initialAvatar"
|
||||
@update:model-value="submitAvatar($event)"
|
||||
@delete="avatar = {uuid: null}"
|
||||
>
|
||||
{{ t('components.auth.Settings.label.avatar') }}
|
||||
</attachment-input>
|
||||
</Layout>
|
||||
</section>
|
||||
|
||||
<section class="ui text container">
|
||||
<h2 class="ui header">
|
||||
{{ t('components.auth.Settings.header.changePassword') }}
|
||||
</h2>
|
||||
<div class="ui message">
|
||||
{{ t('components.auth.Settings.description.changePassword.paragraph1') }} {{ t('components.auth.Settings.description.changePassword.paragraph2') }}
|
||||
</div>
|
||||
<Layout form
|
||||
@submit.prevent="submitPassword()"
|
||||
>
|
||||
<Alert
|
||||
v-if="passwordError"
|
||||
role="alert"
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.auth.Settings.header.passwordFailure') }}
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-if="passwordError == 'invalid_credentials'">
|
||||
{{ t('components.auth.Settings.help.changePassword') }}
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
<div class="field">
|
||||
<label for="old-password-field">{{ t('components.auth.Settings.label.currentPassword') }}</label>
|
||||
<password-input
|
||||
v-model="credentials.oldPassword"
|
||||
field-id="old-password-field"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="new-password-field">{{ t('components.auth.Settings.label.newPassword') }}</label>
|
||||
<password-input
|
||||
v-model="credentials.newPassword"
|
||||
field-id="new-password-field"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<dangerous-button
|
||||
:class="['ui', {'loading': isLoadingPassword}, {disabled: !credentials.newPassword || !credentials.oldPassword}, 'warning', 'button']"
|
||||
:action="submitPassword"
|
||||
:title="t('components.auth.Settings.modal.changePassword.header')"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.password') }}
|
||||
<template #modal-content>
|
||||
<div>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.modal.changePassword.content.warning') }}
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
{{ t('components.auth.Settings.modal.changePassword.content.logout') }}
|
||||
</li>
|
||||
<li>
|
||||
{{ t('components.auth.Settings.modal.changePassword.content.subsonic') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<template #modal-confirm>
|
||||
<div>
|
||||
{{ t('components.auth.Settings.button.disableSubsonic') }}
|
||||
</div>
|
||||
</template>
|
||||
</dangerous-button>
|
||||
</Layout>
|
||||
<div class="ui hidden divider" />
|
||||
<subsonic-token-form />
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="content-filters"
|
||||
class="ui text container"
|
||||
>
|
||||
<h2 class="ui header">
|
||||
<i class="bi bi-eye-slash" />
|
||||
<div class="content">
|
||||
{{ t('components.auth.Settings.header.contentFilters') }}
|
||||
</div>
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.description.contentFilters') }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
primary
|
||||
icon="bi-arrow-clockwise"
|
||||
@click="store.dispatch('moderation/fetchContentFilters')"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.refresh') }}
|
||||
</Button>
|
||||
<h3 class="ui header">
|
||||
{{ t('components.auth.Settings.header.hiddenArtists') }}
|
||||
</h3>
|
||||
<table class="ui compact very basic unstackable table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.artists.header.name') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.artists.header.creationDate') }}
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="filter in store.getters['moderation/artistFilters']()"
|
||||
:key="filter.uuid"
|
||||
>
|
||||
<td>
|
||||
<router-link :to="{name: 'library.artists.detail', params: {id: filter.target.id }}">
|
||||
{{ filter.target.name }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td>
|
||||
<human-date :date="filter.creation_date" />
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
secondary
|
||||
@click="store.dispatch('moderation/deleteContentFilter', filter.uuid)"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.delete') }}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section
|
||||
id="grants"
|
||||
class="ui text container"
|
||||
>
|
||||
<div class="ui hidden divider" />
|
||||
<h2 class="ui header">
|
||||
<i class="bi bi-unlock-fill" />
|
||||
<div class="content">
|
||||
{{ t('components.auth.Settings.header.authorizedApps') }}
|
||||
</div>
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.description.authorizedApps') }}
|
||||
</p>
|
||||
<Button
|
||||
primary
|
||||
icon="bi-arrow-clockwise"
|
||||
:is-loading="isLoadingApps"
|
||||
@click="fetchApps()"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.refresh') }}
|
||||
</Button>
|
||||
<table
|
||||
v-if="apps.length > 0"
|
||||
class="ui compact very basic unstackable table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.authorizedApps.header.application') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.authorizedApps.header.permissions') }}
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="app in apps"
|
||||
:key="app.client_id"
|
||||
>
|
||||
<td>
|
||||
{{ app.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ app.scopes }}
|
||||
</td>
|
||||
<td>
|
||||
<dangerous-button
|
||||
:class="['ui', 'tiny', 'danger', { loading: isRevoking.has(app.client_id) }, 'button']"
|
||||
@confirm="revokeApp(app.client_id)"
|
||||
:title="t('components.auth.Settings.modal.revokeApp.header', {app: app.name})"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.revoke') }}
|
||||
<template #modal-content>
|
||||
{{ t('components.auth.Settings.modal.revokeApp.content.warning') }}
|
||||
</template>
|
||||
<template #modal-confirm>
|
||||
{{ t('components.auth.Settings.button.revokeAccess') }}
|
||||
</template>
|
||||
</dangerous-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<empty-state v-else>
|
||||
<template #title>
|
||||
{{ t('components.auth.Settings.header.noApps') }}
|
||||
</template>
|
||||
{{ t('components.auth.Settings.help.noApps') }}
|
||||
</empty-state>
|
||||
</section>
|
||||
<section
|
||||
id="apps"
|
||||
class="ui text container"
|
||||
>
|
||||
<div class="ui hidden divider" />
|
||||
<h2 class="ui header">
|
||||
<i class="bi bi-code-slash" />
|
||||
<div class="content">
|
||||
{{ t('components.auth.Settings.header.yourApps') }}
|
||||
</div>
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.description.yourApps') }}
|
||||
</p>
|
||||
<Link
|
||||
class="ui success button"
|
||||
:to="{name: 'settings.applications.new'}"
|
||||
>
|
||||
{{ t('components.auth.Settings.link.newApp') }}
|
||||
</Link>
|
||||
<table
|
||||
v-if="ownedApps.length > 0"
|
||||
class="ui compact very basic unstackable table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.yourApps.header.application') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.yourApps.header.scopes') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.yourApps.header.creationDate') }}
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="app in ownedApps"
|
||||
:key="app.client_id"
|
||||
>
|
||||
<td>
|
||||
<router-link :to="{name: 'settings.applications.edit', params: {id: app.client_id}}">
|
||||
{{ app.name }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td>
|
||||
{{ app.scopes }}
|
||||
</td>
|
||||
<td>
|
||||
<human-date :date="app.created" />
|
||||
</td>
|
||||
<td>
|
||||
<Link
|
||||
class="ui tiny success button"
|
||||
:to="{name: 'settings.applications.edit', params: {id: app.client_id}}"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.edit') }}
|
||||
</Link>
|
||||
<DangerousButton
|
||||
:is-loading="isDeleting.has(app.client_id)"
|
||||
class="tiny"
|
||||
@confirm="deleteApp(app.client_id)"
|
||||
:title="t('components.auth.Settings.modal.deleteApp.header', {app: app.name})"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.remove') }}
|
||||
<template #modal-content>
|
||||
{{ t('components.auth.Settings.modal.deleteApp.content.warning') }}
|
||||
</template>
|
||||
<template #modal-confirm>
|
||||
{{ t('components.auth.Settings.button.removeApp') }}
|
||||
</template>
|
||||
</DangerousButton>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<empty-state v-else>
|
||||
<template #title>
|
||||
{{ t('components.auth.Settings.header.noPersonalApps') }}
|
||||
</template>
|
||||
{{ t('components.auth.Settings.help.noPersonalApps') }}
|
||||
</empty-state>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="plugins"
|
||||
class="ui text container"
|
||||
>
|
||||
<div class="ui hidden divider" />
|
||||
<h2 class="ui header">
|
||||
<i class="bi bi-code" />
|
||||
<div class="content">
|
||||
{{ t('components.auth.Settings.header.plugins') }}
|
||||
</div>
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.description.plugins') }}
|
||||
</p>
|
||||
<Button
|
||||
primary
|
||||
:to="{name: 'settings.plugins'}"
|
||||
>
|
||||
{{ t('components.auth.Settings.link.managePlugins') }}
|
||||
</Button>
|
||||
</section>
|
||||
<section class="ui text container">
|
||||
<div class="ui hidden divider" />
|
||||
<h2 class="ui header">
|
||||
<i class="bi bi-envelope-at" />
|
||||
<div class="content">
|
||||
{{ t('components.auth.Settings.header.changeEmail') }}
|
||||
</div>
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.description.changeEmail') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.message.currentEmail', { email: store.state.auth.profile?.email }) }}
|
||||
</p>
|
||||
<Layout form
|
||||
@submit.prevent="changeEmail"
|
||||
>
|
||||
<Alert red
|
||||
v-if="changeEmailErrors.length > 0"
|
||||
role="alert"
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.auth.Settings.header.emailFailure') }}
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li
|
||||
v-for="(error, key) in changeEmailErrors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
<div class="field">
|
||||
<label for="new-email">{{ t('components.auth.Settings.label.newEmail') }}</label>
|
||||
<Input
|
||||
id="new-email"
|
||||
v-model="newEmail"
|
||||
required
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="current-password-field-email">{{ t('components.auth.Settings.label.password') }}</label>
|
||||
<password-input
|
||||
v-model="emailPassword"
|
||||
field-id="current-password-field-email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
primary
|
||||
type="submit"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.update') }}
|
||||
</Button>
|
||||
</Layout>
|
||||
</section>
|
||||
<section class="ui text container">
|
||||
<div class="ui hidden divider" />
|
||||
<h2 class="ui header">
|
||||
<i class="bi bi-trash" />
|
||||
<div class="content">
|
||||
{{ t('components.auth.Settings.header.deleteAccount') }}
|
||||
</div>
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.description.deleteAccount') }}
|
||||
</p>
|
||||
<Alert yellow
|
||||
<section class="ui text container">
|
||||
<h2 class="ui header">
|
||||
{{ t('components.auth.Settings.header.avatar') }}
|
||||
</h2>
|
||||
<Layout form>
|
||||
<Alert
|
||||
v-if="avatarErrors.length > 0"
|
||||
red
|
||||
role="alert"
|
||||
>
|
||||
{{ t('components.auth.Settings.warning.deleteAccount') }}
|
||||
<h4 class="header">
|
||||
{{ t('components.auth.Settings.header.avatarFailure') }}
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li
|
||||
v-for="(error, key) in avatarErrors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
<Layout form>
|
||||
<Alert red
|
||||
v-if="accountDeleteErrors.length > 0"
|
||||
role="alert"
|
||||
<attachment-input
|
||||
v-model="avatar.uuid"
|
||||
:initial-value="initialAvatar"
|
||||
@update:model-value="submitAvatar($event)"
|
||||
@delete="avatar = {uuid: null}"
|
||||
>
|
||||
{{ t('components.auth.Settings.label.avatar') }}
|
||||
</attachment-input>
|
||||
</Layout>
|
||||
</section>
|
||||
|
||||
<section class="ui text container">
|
||||
<h2 class="ui header">
|
||||
{{ t('components.auth.Settings.header.changePassword') }}
|
||||
</h2>
|
||||
<div class="ui message">
|
||||
{{ t('components.auth.Settings.description.changePassword.paragraph1') }} {{ t('components.auth.Settings.description.changePassword.paragraph2') }}
|
||||
</div>
|
||||
<Layout
|
||||
form
|
||||
@submit.prevent="submitPassword()"
|
||||
>
|
||||
<Alert
|
||||
v-if="passwordError"
|
||||
role="alert"
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.auth.Settings.header.passwordFailure') }}
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-if="passwordError == 'invalid_credentials'">
|
||||
{{ t('components.auth.Settings.help.changePassword') }}
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
<div class="field">
|
||||
<label for="old-password-field">{{ t('components.auth.Settings.label.currentPassword') }}</label>
|
||||
<password-input
|
||||
v-model="credentials.oldPassword"
|
||||
field-id="old-password-field"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="new-password-field">{{ t('components.auth.Settings.label.newPassword') }}</label>
|
||||
<password-input
|
||||
v-model="credentials.newPassword"
|
||||
field-id="new-password-field"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<dangerous-button
|
||||
:class="['ui', {'loading': isLoadingPassword}, {disabled: !credentials.newPassword || !credentials.oldPassword}, 'warning', 'button']"
|
||||
:action="submitPassword"
|
||||
:title="t('components.auth.Settings.modal.changePassword.header')"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.password') }}
|
||||
<template #modal-content>
|
||||
<div>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.modal.changePassword.content.warning') }}
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
{{ t('components.auth.Settings.modal.changePassword.content.logout') }}
|
||||
</li>
|
||||
<li>
|
||||
{{ t('components.auth.Settings.modal.changePassword.content.subsonic') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<template #modal-confirm>
|
||||
<div>
|
||||
{{ t('components.auth.Settings.button.disableSubsonic') }}
|
||||
</div>
|
||||
</template>
|
||||
</dangerous-button>
|
||||
</Layout>
|
||||
<div class="ui hidden divider" />
|
||||
<subsonic-token-form />
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="content-filters"
|
||||
class="ui text container"
|
||||
>
|
||||
<h2 class="ui header">
|
||||
<i class="bi bi-eye-slash" />
|
||||
<div class="content">
|
||||
{{ t('components.auth.Settings.header.contentFilters') }}
|
||||
</div>
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.description.contentFilters') }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
primary
|
||||
icon="bi-arrow-clockwise"
|
||||
@click="store.dispatch('moderation/fetchContentFilters')"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.refresh') }}
|
||||
</Button>
|
||||
<h3 class="ui header">
|
||||
{{ t('components.auth.Settings.header.hiddenArtists') }}
|
||||
</h3>
|
||||
<table class="ui compact very basic unstackable table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.artists.header.name') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.artists.header.creationDate') }}
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="filter in store.getters['moderation/artistFilters']()"
|
||||
:key="filter.uuid"
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.auth.Settings.header.accountFailure') }}
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li
|
||||
v-for="(error, key) in accountDeleteErrors"
|
||||
:key="key"
|
||||
<td>
|
||||
<router-link :to="{name: 'library.artists.detail', params: {id: filter.target.id }}">
|
||||
{{ filter.target.name }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td>
|
||||
<human-date :date="filter.creation_date" />
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
secondary
|
||||
@click="store.dispatch('moderation/deleteContentFilter', filter.uuid)"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
<div class="field">
|
||||
<label for="current-password-field">{{ t('components.auth.Settings.label.currentPassword') }}</label>
|
||||
<password-input
|
||||
v-model="deleteAccountPassword"
|
||||
field-id="current-password-field"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<dangerous-button
|
||||
:is-loading="isDeletingAccount"
|
||||
:disabled="!deleteAccountPassword || undefined"
|
||||
:class="{danger: deleteAccountPassword}"
|
||||
:action="deleteAccount"
|
||||
:title="t('components.auth.Settings.modal.deleteAccount.header')"
|
||||
{{ t('components.auth.Settings.button.delete') }}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section
|
||||
id="grants"
|
||||
class="ui text container"
|
||||
>
|
||||
<div class="ui hidden divider" />
|
||||
<h2 class="ui header">
|
||||
<i class="bi bi-unlock-fill" />
|
||||
<div class="content">
|
||||
{{ t('components.auth.Settings.header.authorizedApps') }}
|
||||
</div>
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.description.authorizedApps') }}
|
||||
</p>
|
||||
<Button
|
||||
primary
|
||||
icon="bi-arrow-clockwise"
|
||||
:is-loading="isLoadingApps"
|
||||
@click="fetchApps()"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.refresh') }}
|
||||
</Button>
|
||||
<table
|
||||
v-if="apps.length > 0"
|
||||
class="ui compact very basic unstackable table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.authorizedApps.header.application') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.authorizedApps.header.permissions') }}
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="app in apps"
|
||||
:key="app.client_id"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.deleteAccount') }}
|
||||
<template #modal-content>
|
||||
{{ t('components.auth.Settings.modal.deleteAccount.content.warning') }}
|
||||
</template>
|
||||
<template #modal-confirm>
|
||||
{{ t('components.auth.Settings.button.deleteAccountConfirm') }}
|
||||
</template>
|
||||
</dangerous-button>
|
||||
</Layout>
|
||||
</section>
|
||||
<td>
|
||||
{{ app.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ app.scopes }}
|
||||
</td>
|
||||
<td>
|
||||
<dangerous-button
|
||||
:class="['ui', 'tiny', 'danger', { loading: isRevoking.has(app.client_id) }, 'button']"
|
||||
:title="t('components.auth.Settings.modal.revokeApp.header', {app: app.name})"
|
||||
@confirm="revokeApp(app.client_id)"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.revoke') }}
|
||||
<template #modal-content>
|
||||
{{ t('components.auth.Settings.modal.revokeApp.content.warning') }}
|
||||
</template>
|
||||
<template #modal-confirm>
|
||||
{{ t('components.auth.Settings.button.revokeAccess') }}
|
||||
</template>
|
||||
</dangerous-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<empty-state v-else>
|
||||
<template #title>
|
||||
{{ t('components.auth.Settings.header.noApps') }}
|
||||
</template>
|
||||
{{ t('components.auth.Settings.help.noApps') }}
|
||||
</empty-state>
|
||||
</section>
|
||||
<section
|
||||
id="apps"
|
||||
class="ui text container"
|
||||
>
|
||||
<div class="ui hidden divider" />
|
||||
<h2 class="ui header">
|
||||
<i class="bi bi-code-slash" />
|
||||
<div class="content">
|
||||
{{ t('components.auth.Settings.header.yourApps') }}
|
||||
</div>
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.description.yourApps') }}
|
||||
</p>
|
||||
<Link
|
||||
class="ui success button"
|
||||
:to="{name: 'settings.applications.new'}"
|
||||
>
|
||||
{{ t('components.auth.Settings.link.newApp') }}
|
||||
</Link>
|
||||
<table
|
||||
v-if="ownedApps.length > 0"
|
||||
class="ui compact very basic unstackable table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.yourApps.header.application') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.yourApps.header.scopes') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.auth.Settings.table.yourApps.header.creationDate') }}
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="app in ownedApps"
|
||||
:key="app.client_id"
|
||||
>
|
||||
<td>
|
||||
<router-link :to="{name: 'settings.applications.edit', params: {id: app.client_id}}">
|
||||
{{ app.name }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td>
|
||||
{{ app.scopes }}
|
||||
</td>
|
||||
<td>
|
||||
<human-date :date="app.created" />
|
||||
</td>
|
||||
<td>
|
||||
<Link
|
||||
class="ui tiny success button"
|
||||
:to="{name: 'settings.applications.edit', params: {id: app.client_id}}"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.edit') }}
|
||||
</Link>
|
||||
<DangerousButton
|
||||
:is-loading="isDeleting.has(app.client_id)"
|
||||
class="tiny"
|
||||
:title="t('components.auth.Settings.modal.deleteApp.header', {app: app.name})"
|
||||
@confirm="deleteApp(app.client_id)"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.remove') }}
|
||||
<template #modal-content>
|
||||
{{ t('components.auth.Settings.modal.deleteApp.content.warning') }}
|
||||
</template>
|
||||
<template #modal-confirm>
|
||||
{{ t('components.auth.Settings.button.removeApp') }}
|
||||
</template>
|
||||
</DangerousButton>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<empty-state v-else>
|
||||
<template #title>
|
||||
{{ t('components.auth.Settings.header.noPersonalApps') }}
|
||||
</template>
|
||||
{{ t('components.auth.Settings.help.noPersonalApps') }}
|
||||
</empty-state>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="plugins"
|
||||
class="ui text container"
|
||||
>
|
||||
<div class="ui hidden divider" />
|
||||
<h2 class="ui header">
|
||||
<i class="bi bi-code" />
|
||||
<div class="content">
|
||||
{{ t('components.auth.Settings.header.plugins') }}
|
||||
</div>
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.description.plugins') }}
|
||||
</p>
|
||||
<Button
|
||||
primary
|
||||
:to="{name: 'settings.plugins'}"
|
||||
>
|
||||
{{ t('components.auth.Settings.link.managePlugins') }}
|
||||
</Button>
|
||||
</section>
|
||||
<section class="ui text container">
|
||||
<div class="ui hidden divider" />
|
||||
<h2 class="ui header">
|
||||
<i class="bi bi-envelope-at" />
|
||||
<div class="content">
|
||||
{{ t('components.auth.Settings.header.changeEmail') }}
|
||||
</div>
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.description.changeEmail') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.message.currentEmail', { email: store.state.auth.profile?.email }) }}
|
||||
</p>
|
||||
<Layout
|
||||
form
|
||||
@submit.prevent="changeEmail"
|
||||
>
|
||||
<Alert
|
||||
v-if="changeEmailErrors.length > 0"
|
||||
red
|
||||
role="alert"
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.auth.Settings.header.emailFailure') }}
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li
|
||||
v-for="(error, key) in changeEmailErrors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
<div class="field">
|
||||
<label for="new-email">{{ t('components.auth.Settings.label.newEmail') }}</label>
|
||||
<Input
|
||||
id="new-email"
|
||||
v-model="newEmail"
|
||||
required
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="current-password-field-email">{{ t('components.auth.Settings.label.password') }}</label>
|
||||
<password-input
|
||||
v-model="emailPassword"
|
||||
field-id="current-password-field-email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
primary
|
||||
type="submit"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.update') }}
|
||||
</Button>
|
||||
</Layout>
|
||||
</section>
|
||||
<section class="ui text container">
|
||||
<div class="ui hidden divider" />
|
||||
<h2 class="ui header">
|
||||
<i class="bi bi-trash" />
|
||||
<div class="content">
|
||||
{{ t('components.auth.Settings.header.deleteAccount') }}
|
||||
</div>
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.auth.Settings.description.deleteAccount') }}
|
||||
</p>
|
||||
<Alert
|
||||
yellow
|
||||
role="alert"
|
||||
>
|
||||
{{ t('components.auth.Settings.warning.deleteAccount') }}
|
||||
</Alert>
|
||||
<Layout form>
|
||||
<Alert
|
||||
v-if="accountDeleteErrors.length > 0"
|
||||
red
|
||||
role="alert"
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.auth.Settings.header.accountFailure') }}
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li
|
||||
v-for="(error, key) in accountDeleteErrors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
<div class="field">
|
||||
<label for="current-password-field">{{ t('components.auth.Settings.label.currentPassword') }}</label>
|
||||
<password-input
|
||||
v-model="deleteAccountPassword"
|
||||
field-id="current-password-field"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<dangerous-button
|
||||
:is-loading="isDeletingAccount"
|
||||
:disabled="!deleteAccountPassword || undefined"
|
||||
:class="{danger: deleteAccountPassword}"
|
||||
:action="deleteAccount"
|
||||
:title="t('components.auth.Settings.modal.deleteAccount.header')"
|
||||
>
|
||||
{{ t('components.auth.Settings.button.deleteAccount') }}
|
||||
<template #modal-content>
|
||||
{{ t('components.auth.Settings.modal.deleteAccount.content.warning') }}
|
||||
</template>
|
||||
<template #modal-confirm>
|
||||
{{ t('components.auth.Settings.button.deleteAccountConfirm') }}
|
||||
</template>
|
||||
</dangerous-button>
|
||||
</Layout>
|
||||
</section>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
|
@ -92,10 +92,16 @@ fetchInstanceSettings()
|
|||
|
||||
<template>
|
||||
<div v-if="submitted">
|
||||
<Alert yellow v-if="signupRequiresApproval">
|
||||
<Alert
|
||||
v-if="signupRequiresApproval"
|
||||
yellow
|
||||
>
|
||||
{{ t('components.auth.SignupForm.message.awaitingReview') }}
|
||||
</Alert>
|
||||
<Alert green v-else>
|
||||
<Alert
|
||||
v-else
|
||||
green
|
||||
>
|
||||
{{ t('components.auth.SignupForm.message.accountCreated') }}
|
||||
</Alert>
|
||||
<h2>
|
||||
|
@ -107,18 +113,22 @@ fetchInstanceSettings()
|
|||
:show-signup="false"
|
||||
/>
|
||||
</div>
|
||||
<Layout form stack
|
||||
<Layout
|
||||
v-else
|
||||
form
|
||||
stack
|
||||
style="max-width: 600px"
|
||||
@submit.prevent="submit()"
|
||||
>
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="!store.state.instance.settings.users.registration_enabled.value"
|
||||
red
|
||||
>
|
||||
{{ t('components.auth.SignupForm.message.registrationClosed') }}
|
||||
</Alert>
|
||||
<Alert yellow
|
||||
<Alert
|
||||
v-else-if="signupRequiresApproval"
|
||||
yellow
|
||||
>
|
||||
{{ t('components.auth.SignupForm.message.requiresReview') }}
|
||||
</Alert>
|
||||
|
@ -129,8 +139,9 @@ fetchInstanceSettings()
|
|||
:permissive="true"
|
||||
/>
|
||||
</template>
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="errors.length > 0"
|
||||
red
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.auth.SignupForm.header.signupFailure') }}
|
||||
|
@ -144,65 +155,68 @@ fetchInstanceSettings()
|
|||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
<Input
|
||||
id="username-field"
|
||||
:label = "t('components.auth.SignupForm.label.username')"
|
||||
ref="username"
|
||||
v-model="payload.username"
|
||||
name="username"
|
||||
required
|
||||
type="text"
|
||||
autofocus
|
||||
:placeholder="labels.usernamePlaceholder"
|
||||
<Input
|
||||
id="username-field"
|
||||
ref="username"
|
||||
v-model="payload.username"
|
||||
:label="t('components.auth.SignupForm.label.username')"
|
||||
name="username"
|
||||
required
|
||||
type="text"
|
||||
autofocus
|
||||
:placeholder="labels.usernamePlaceholder"
|
||||
/>
|
||||
<Input
|
||||
id="email-field"
|
||||
ref="email"
|
||||
v-model="payload.email"
|
||||
:label="t('components.auth.SignupForm.label.email')"
|
||||
autocomplete="email"
|
||||
name="email"
|
||||
required
|
||||
type="email"
|
||||
:placeholder="labels.emailPlaceholder"
|
||||
/>
|
||||
<Input
|
||||
v-model="payload.password1"
|
||||
password
|
||||
autocomplete="new-password"
|
||||
:label="t('components.auth.SignupForm.label.password')"
|
||||
field-id="password-field"
|
||||
/>
|
||||
<Input
|
||||
v-if="!store.state.instance.settings.users.registration_enabled.value"
|
||||
id="invitation-code"
|
||||
v-model="payload.invitation"
|
||||
:label="t('components.auth.SignupForm.label.invitation')"
|
||||
required
|
||||
type="text"
|
||||
name="invitation"
|
||||
:placeholder="labels.placeholder"
|
||||
/>
|
||||
<div
|
||||
v-for="(field, idx) in formCustomization?.fields"
|
||||
v-if="signupRequiresApproval && (formCustomization?.fields.length ?? 0) > 0"
|
||||
:key="idx"
|
||||
:class="[{required: field.required}, 'field']"
|
||||
>
|
||||
<Textarea
|
||||
v-if="field.input_type === 'long_text'"
|
||||
:id="`custom-field-${idx}`"
|
||||
v-model="payload.request_fields[field.label]"
|
||||
:label="field.label"
|
||||
:required="field.required"
|
||||
rows="5"
|
||||
/>
|
||||
<Input
|
||||
:label="t('components.auth.SignupForm.label.email')"
|
||||
id="email-field"
|
||||
autocomplete="email"
|
||||
ref="email"
|
||||
v-model="payload.email"
|
||||
name="email"
|
||||
required
|
||||
type="email"
|
||||
:placeholder="labels.emailPlaceholder"
|
||||
/>
|
||||
<Input password
|
||||
autocomplete="new-password"
|
||||
:label="t('components.auth.SignupForm.label.password')"
|
||||
v-model="payload.password1"
|
||||
field-id="password-field"
|
||||
/>
|
||||
<Input v-if="!store.state.instance.settings.users.registration_enabled.value"
|
||||
:label="t('components.auth.SignupForm.label.invitation')"
|
||||
id="invitation-code"
|
||||
v-model="payload.invitation"
|
||||
required
|
||||
v-else
|
||||
:id="`custom-field-${idx}`"
|
||||
v-model="payload.request_fields[field.label]"
|
||||
:label="field.label"
|
||||
type="text"
|
||||
name="invitation"
|
||||
:placeholder="labels.placeholder"
|
||||
:required="field.required"
|
||||
/>
|
||||
<div v-if="signupRequiresApproval && (formCustomization?.fields.length ?? 0) > 0"
|
||||
v-for="(field, idx) in formCustomization?.fields"
|
||||
:key="idx"
|
||||
:class="[{required: field.required}, 'field']"
|
||||
>
|
||||
<Textarea
|
||||
:label="field.label"
|
||||
v-if="field.input_type === 'long_text'"
|
||||
:id="`custom-field-${idx}`"
|
||||
v-model="payload.request_fields[field.label]"
|
||||
:required="field.required"
|
||||
rows="5"
|
||||
/>
|
||||
<Input
|
||||
v-else
|
||||
:id="`custom-field-${idx}`"
|
||||
:label="field.label"
|
||||
v-model="payload.request_fields[field.label]"
|
||||
type="text"
|
||||
:required="field.required"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
primary
|
||||
auto
|
||||
|
|
|
@ -108,15 +108,17 @@ fetchToken()
|
|||
{{ t('components.auth.SubsonicTokenForm.link.apps') }}
|
||||
</a>
|
||||
</p>
|
||||
<Alert green
|
||||
<Alert
|
||||
v-if="success"
|
||||
green
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ successMessage }}
|
||||
</h4>
|
||||
</Alert>
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="subsonicEnabled && errors.length > 0"
|
||||
red
|
||||
role="alert"
|
||||
>
|
||||
<h4 class="header">
|
||||
|
@ -164,8 +166,8 @@ fetchToken()
|
|||
</template>
|
||||
</DangerousButton>
|
||||
<Button
|
||||
primary
|
||||
v-else
|
||||
primary
|
||||
:is-loading="isLoading"
|
||||
@click="requestNewToken"
|
||||
>
|
||||
|
|
|
@ -15,7 +15,7 @@ interface Events {
|
|||
(e: 'created'): void
|
||||
}
|
||||
|
||||
const channel = defineModel<Channel>({required: true})
|
||||
const channel = defineModel<Channel>({ required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
@ -53,7 +53,8 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout form
|
||||
<Layout
|
||||
form
|
||||
:class="['ui', {loading: isLoading}, 'form']"
|
||||
@submit.stop.prevent
|
||||
>
|
||||
|
|
|
@ -14,7 +14,7 @@ import Input from '~/components/ui/Input.vue'
|
|||
|
||||
const { t } = useI18n()
|
||||
|
||||
const channel = defineModel<Channel>({required: true})
|
||||
const channel = defineModel<Channel>({ required: true })
|
||||
defineEmits(['created'])
|
||||
const newAlbumTitle = ref<string>('')
|
||||
|
||||
|
@ -22,7 +22,7 @@ const isLoading = ref(false)
|
|||
const submittable = ref(false)
|
||||
const errors = ref<string[]>([])
|
||||
|
||||
const {isOpen:show} = useModal('album')
|
||||
const { isOpen: show } = useModal('album')
|
||||
|
||||
watch(show, () => {
|
||||
isLoading.value = false
|
||||
|
@ -56,8 +56,9 @@ const submit = async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="t(channel?.artist?.content_category === 'podcast' ? 'components.channels.AlbumModal.header.newSeries' : 'components.channels.AlbumModal.header.newAlbum')"
|
||||
<Modal
|
||||
v-model="show"
|
||||
:title="t(channel?.artist?.content_category === 'podcast' ? 'components.channels.AlbumModal.header.newSeries' : 'components.channels.AlbumModal.header.newAlbum')"
|
||||
class="small"
|
||||
:cancel="t('components.channels.AlbumModal.button.cancel')"
|
||||
>
|
||||
|
@ -80,7 +81,8 @@ const submit = async () => {
|
|||
</Alert>
|
||||
</template>
|
||||
|
||||
<Layout form
|
||||
<Layout
|
||||
form
|
||||
:class="['ui', {loading: isLoading}, 'form']"
|
||||
@submit.stop.prevent
|
||||
>
|
||||
|
|
|
@ -27,15 +27,14 @@ const fetchAlbums = async () => {
|
|||
}
|
||||
})
|
||||
|
||||
console.log("I found another album with artist", model.value.channel.artist.name, ":", response.data.results)
|
||||
console.log('I found another album with artist', model.value.channel.artist.name, ':', response.data.results)
|
||||
albums.value = response.data.results
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
watch(() => model.value.channel, fetchAlbums, { immediate: true })
|
||||
watch(albums, (value) => {
|
||||
if (value.length === 1)
|
||||
selectedAlbumId.value = albums.value[0].id
|
||||
if (value.length === 1) { selectedAlbumId.value = albums.value[0].id }
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -62,7 +61,7 @@ watch(albums, (value) => {
|
|||
:value="album.id"
|
||||
>
|
||||
{{ album.title }}
|
||||
{{ t('components.channels.AlbumSelect.meta.tracks', album.tracks_count) }}
|
||||
{{ t('components.channels.AlbumSelect.meta.tracks', album.tracks_count) }}
|
||||
</option>
|
||||
</select>
|
||||
<Layout stack>
|
||||
|
@ -73,11 +72,11 @@ watch(albums, (value) => {
|
|||
icon="bi-plus"
|
||||
:to="useModal('album').to"
|
||||
>
|
||||
Add Album
|
||||
<AlbumModal
|
||||
v-model="model.channel"
|
||||
@created="fetchAlbums"
|
||||
/>
|
||||
Add Album
|
||||
<AlbumModal
|
||||
v-model="model.channel"
|
||||
@created="fetchAlbums"
|
||||
/>
|
||||
</Link>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
|
@ -6,11 +6,11 @@ import { computed, ref } from 'vue'
|
|||
import { useStore } from '~/store'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
import LoginModal from '~/components/common/LoginModal.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
interface Events {
|
||||
(e: 'unsubscribed'): void
|
||||
(e: 'subscribed'): void
|
||||
|
|
|
@ -27,7 +27,6 @@ import Link from '~/components/ui/Link.vue'
|
|||
import Loader from '~/components/ui/Loader.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
|
||||
|
||||
interface Events {
|
||||
(e: 'status', status: UploadStatus): void
|
||||
}
|
||||
|
@ -106,11 +105,14 @@ albums
|
|||
const channelDropdownId = ref<Channel['artist']['id'] | null>(null)
|
||||
const isLoading = ref(false)
|
||||
|
||||
const selectedChannel = computed(()=>
|
||||
props.channel ? props.channel
|
||||
: availableChannels.value.length===0 ? (createEmptyChannel(), null)
|
||||
: availableChannels.value.length===1 ? availableChannels.value[0]
|
||||
: availableChannels.value.find(({artist}) => artist.id === channelDropdownId.value)
|
||||
const selectedChannel = computed(() =>
|
||||
props.channel
|
||||
? props.channel
|
||||
: availableChannels.value.length === 0
|
||||
? (createEmptyChannel(), null)
|
||||
: availableChannels.value.length === 1
|
||||
? availableChannels.value[0]
|
||||
: availableChannels.value.find(({ artist }) => artist.id === channelDropdownId.value)
|
||||
)
|
||||
|
||||
const emptyChannelCreateRequest:components['schemas']['ChannelCreateRequest'] = {
|
||||
|
@ -118,7 +120,7 @@ const emptyChannelCreateRequest:components['schemas']['ChannelCreateRequest'] =
|
|||
username: store.state.auth.username,
|
||||
description: null,
|
||||
tags: [],
|
||||
content_category: 'music',
|
||||
content_category: 'music'
|
||||
}
|
||||
|
||||
const createEmptyChannel = async () => {
|
||||
|
@ -127,10 +129,10 @@ const createEmptyChannel = async () => {
|
|||
'channels/',
|
||||
(emptyChannelCreateRequest satisfies operations['create_channel_2']['requestBody']['content']['application/json'])
|
||||
)
|
||||
console.log("Created Channel: ", response.data)
|
||||
console.log('Created Channel: ', response.data)
|
||||
} catch (error) {
|
||||
errors.value = (error as BackendError).backendErrors
|
||||
console.log("Error:", error)
|
||||
console.log('Error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,12 +152,13 @@ const fetchChannels = async () => {
|
|||
const albumSelection = ref<{channel: Channel, albumId: Album['id'] | '', albums: Album[]}>
|
||||
|
||||
watch(selectedChannel, (channel) =>
|
||||
albumSelection.value =
|
||||
{ channel: channel,
|
||||
albumId: '',
|
||||
albums: []
|
||||
}
|
||||
)
|
||||
albumSelection.value
|
||||
= {
|
||||
channel,
|
||||
albumId: '',
|
||||
albums: []
|
||||
}
|
||||
)
|
||||
|
||||
const channelChange = async (channelId) => {
|
||||
selectedChannel.value = channelId
|
||||
|
@ -409,7 +412,7 @@ const labels = computed(() => ({
|
|||
}))
|
||||
|
||||
const publish = async () => {
|
||||
console.log("starting publish...")
|
||||
console.log('starting publish...')
|
||||
isLoading.value = true
|
||||
|
||||
errors.value = []
|
||||
|
@ -432,8 +435,7 @@ const publish = async () => {
|
|||
// headers: { 'Authorization': `Bearer ${store.state.auth.oauth}` }
|
||||
// })
|
||||
|
||||
|
||||
console.log("Channels Store Before: ", store.state.channels)
|
||||
console.log('Channels Store Before: ', store.state.channels)
|
||||
|
||||
// Tell the store that the uploaded files are pending import
|
||||
store.commit('channels/publish', {
|
||||
|
@ -441,17 +443,16 @@ const publish = async () => {
|
|||
channel: selectedChannel.value
|
||||
})
|
||||
|
||||
console.log("Channels Store After: ", store.state.channels)
|
||||
|
||||
console.log('Channels Store After: ', store.state.channels)
|
||||
} catch (error) {
|
||||
// TODO: Use inferred error type instead of typecasting
|
||||
errors.value = (error as BackendError).backendErrors
|
||||
console.log("Error:", error)
|
||||
console.log('Error:', error)
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
|
||||
console.log("...finished publish")
|
||||
console.log('...finished publish')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
@ -520,7 +521,6 @@ ChannelCreateRequest: {
|
|||
*/
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<Layout
|
||||
form
|
||||
|
@ -548,19 +548,28 @@ ChannelCreateRequest: {
|
|||
<!-- Select Album and License -->
|
||||
|
||||
<div :class="['ui', 'required', 'field']">
|
||||
<label v-if="availableChannels.length === 1" for="channel-dropdown">
|
||||
<label
|
||||
v-if="availableChannels.length === 1"
|
||||
for="channel-dropdown"
|
||||
>
|
||||
{{ t('components.channels.UploadForm.label.channel') }}: {{ selectedChannel?.artist.name }}
|
||||
</label>
|
||||
<label v-else for="channel-dropdown">
|
||||
<label
|
||||
v-else
|
||||
for="channel-dropdown"
|
||||
>
|
||||
{{ t('components.channels.UploadForm.label.channel') }}
|
||||
</label>
|
||||
<select
|
||||
v-if="availableChannels.length > 1"
|
||||
v-model="channelDropdownId"
|
||||
id="channel-dropdown"
|
||||
v-model="channelDropdownId"
|
||||
class="dropdown"
|
||||
>
|
||||
<option v-for="channel in availableChannels" :value="channel.artist.id">
|
||||
<option
|
||||
v-for="channel in availableChannels"
|
||||
:value="channel.artist.id"
|
||||
>
|
||||
{{ channel.artist.name }}
|
||||
</option>
|
||||
</select>
|
||||
|
@ -575,13 +584,13 @@ ChannelCreateRequest: {
|
|||
v-model="values.license"
|
||||
:class="['ui', 'field']"
|
||||
/>
|
||||
<div class="content">
|
||||
<p>
|
||||
<i class="copyright icon" />
|
||||
{{ t('components.channels.UploadForm.help.license') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>
|
||||
<i class="copyright icon" />
|
||||
{{ t('components.channels.UploadForm.help.license') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Files to upload -->
|
||||
<template v-if="remainingSpace === 0">
|
||||
|
@ -690,10 +699,14 @@ ChannelCreateRequest: {
|
|||
v-model:values="uploadImportData[selectedUploadId]"
|
||||
:upload="selectedUpload"
|
||||
/>
|
||||
<Alert blue
|
||||
<Alert
|
||||
blue
|
||||
class="ui message"
|
||||
>
|
||||
<Layout flex gap-8>
|
||||
<Layout
|
||||
flex
|
||||
gap-8
|
||||
>
|
||||
<i class="bi bi-info-circle-fill" />
|
||||
{{ t('components.channels.UploadForm.description.extensions', {extensions: store.state.ui.supportedExtensions.join(', ')}) }}
|
||||
</Layout>
|
||||
|
@ -710,10 +723,16 @@ ChannelCreateRequest: {
|
|||
{{ t('components.channels.UploadForm.message.dragAndDrop') }}
|
||||
</div>
|
||||
<div class="ui very small divider" />
|
||||
<Button primary icon="bi-folder2-open">
|
||||
<Button
|
||||
primary
|
||||
icon="bi-folder2-open"
|
||||
>
|
||||
{{ t('components.channels.UploadForm.label.openBrowser') }}
|
||||
</Button>
|
||||
<Spacer class="divider" :size="32" />
|
||||
<Spacer
|
||||
class="divider"
|
||||
:size="32"
|
||||
/>
|
||||
</file-upload-widget>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
|
@ -54,8 +54,8 @@ const open = ref(false)
|
|||
|
||||
<template>
|
||||
<Modal
|
||||
:title="t(`components.channels.UploadModal.header.${['', 'publish', 'uploadFiles', 'uploadDetails', 'processing'][step]}`)"
|
||||
v-model="store.state.channels.showUploadModal"
|
||||
:title="t(`components.channels.UploadModal.header.${['', 'publish', 'uploadFiles', 'uploadDetails', 'processing'][step]}`)"
|
||||
class="small"
|
||||
>
|
||||
<div class="scrolling content">
|
||||
|
|
|
@ -206,11 +206,11 @@ const launchAction = async () => {
|
|||
<dangerous-button
|
||||
v-if="selectAll || currentAction?.isDangerous"
|
||||
:disabled="checked.length === 0 || undefined"
|
||||
:isLoading="isLoading"
|
||||
:is-loading="isLoading"
|
||||
:confirm-color="currentAction?.confirmColor ?? 'success'"
|
||||
:aria-label="labels.performAction"
|
||||
@confirm="launchAction"
|
||||
:title="t('components.common.ActionTable.modal.performAction.header', { action: currentActionName }, affectedObjectsCount)"
|
||||
@confirm="launchAction"
|
||||
>
|
||||
{{ t('components.common.ActionTable.button.go') }}
|
||||
<template #modal-content>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { hashCode, intToRGB } from '~/utils/color'
|
|||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
actor: { full_username : string; preferred_username?:string; icon?:Actor["icon"] }
|
||||
actor: { full_username : string; preferred_username?:string; icon?:Actor['icon'] }
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
|
|
@ -107,8 +107,9 @@ const getAttachmentUrl = (uuid: string) => {
|
|||
|
||||
<template>
|
||||
<div class="ui form">
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="errors.length > 0"
|
||||
red
|
||||
role="alert"
|
||||
>
|
||||
<h4 class="header">
|
||||
|
@ -160,7 +161,7 @@ const getAttachmentUrl = (uuid: string) => {
|
|||
type="file"
|
||||
accept="image/png,image/jpeg"
|
||||
@change="submit"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div class="ui very small hidden divider" />
|
||||
<p>
|
||||
|
|
|
@ -55,7 +55,7 @@ const remainingChars = computed(() => props.charLimit - props.modelValue.length)
|
|||
v-model="value"
|
||||
:required="required || undefined"
|
||||
:placeholder="labels.placeholder"
|
||||
:autofocus = "autofocus || undefined"
|
||||
:autofocus="autofocus || undefined"
|
||||
/>
|
||||
<span
|
||||
v-if="charLimit"
|
||||
|
|
|
@ -25,33 +25,33 @@ const { copy, isSupported: canCopy, copied } = useClipboard({ source: value, cop
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
v-if="copied"
|
||||
class="message"
|
||||
>
|
||||
{{ t('components.common.CopyInput.message.success') }}
|
||||
</p>
|
||||
<Input
|
||||
:id="id"
|
||||
:value="value"
|
||||
readonly
|
||||
:name="id"
|
||||
type="text"
|
||||
:label="label"
|
||||
>
|
||||
<template #input-right>
|
||||
<Button
|
||||
:class="['ui', buttonClasses, 'input-right']"
|
||||
min-content
|
||||
secondary
|
||||
:disabled="!canCopy || undefined"
|
||||
@click="copy()"
|
||||
>
|
||||
<p
|
||||
v-if="copied"
|
||||
class="message"
|
||||
>
|
||||
{{ t('components.common.CopyInput.message.success') }}
|
||||
</p>
|
||||
<Input
|
||||
:id="id"
|
||||
:value="value"
|
||||
readonly
|
||||
:name="id"
|
||||
type="text"
|
||||
:label="label"
|
||||
>
|
||||
<template #input-right>
|
||||
<Button
|
||||
:class="['ui', buttonClasses, 'input-right']"
|
||||
min-content
|
||||
secondary
|
||||
:disabled="!canCopy || undefined"
|
||||
@click="copy()"
|
||||
>
|
||||
<i class="bi bi-copy" />
|
||||
{{ t('components.common.CopyInput.button.copy') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Input>
|
||||
{{ t('components.common.CopyInput.button.copy') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Input>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -13,7 +13,7 @@ interface Events {
|
|||
// Note that properties such as [disabled] and 'destructive' | 'primary' are inherited.
|
||||
const props = defineProps<{
|
||||
title?: string
|
||||
action?: () => void,
|
||||
action?:() => void,
|
||||
confirmColor?:'success' | 'danger',
|
||||
popoverItem?: boolean
|
||||
}>()
|
||||
|
@ -32,16 +32,17 @@ const confirm = () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="props.popoverItem ? PopoverItem : Button"
|
||||
<component
|
||||
:is="props.popoverItem ? PopoverItem : Button"
|
||||
destructive
|
||||
@click.prevent.stop="showModal = true"
|
||||
v-bind="$attrs"
|
||||
@click.prevent.stop="showModal = true"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<Modal
|
||||
destructive
|
||||
v-model="showModal"
|
||||
destructive
|
||||
:title="title || t('components.common.DangerousButton.header.confirm')"
|
||||
:cancel="t('components.common.DangerousButton.button.cancel')"
|
||||
>
|
||||
|
|
|
@ -22,7 +22,10 @@ withDefaults(defineProps<Props>(), {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Alert blue align-items="center">
|
||||
<Alert
|
||||
blue
|
||||
align-items="center"
|
||||
>
|
||||
<h4 class="ui header">
|
||||
<div class="content">
|
||||
<slot name="title">
|
||||
|
@ -33,10 +36,10 @@ withDefaults(defineProps<Props>(), {
|
|||
</h4>
|
||||
<div class="inline center aligned text">
|
||||
<slot />
|
||||
<Spacer :size="16"/>
|
||||
<Spacer :size="16" />
|
||||
<Button
|
||||
primary
|
||||
v-if="refresh"
|
||||
primary
|
||||
@click="emit('refresh')"
|
||||
>
|
||||
{{ t('components.common.EmptyState.button.refresh') }}
|
||||
|
|
|
@ -36,7 +36,8 @@ const search = () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout form
|
||||
<Layout
|
||||
form
|
||||
@submit.stop.prevent="emit('search', value)"
|
||||
>
|
||||
<div :class="['ui', 'action', {icon: value}, 'input']">
|
||||
|
|
|
@ -32,7 +32,10 @@ const labels = computed(() => ({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model="show" :title="labels.header">
|
||||
<Modal
|
||||
v-model="show"
|
||||
:title="labels.header"
|
||||
>
|
||||
<div
|
||||
v-if="cover"
|
||||
class="image content"
|
||||
|
|
|
@ -119,8 +119,9 @@ const submit = async () => {
|
|||
v-if="isUpdating"
|
||||
@submit.prevent="submit()"
|
||||
>
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="errors.length > 0"
|
||||
red
|
||||
title="{{ t('components.common.RenderedDescription.header.failure') }}"
|
||||
role="alert"
|
||||
>
|
||||
|
@ -139,9 +140,9 @@ const submit = async () => {
|
|||
/>
|
||||
<Button
|
||||
class="left floated"
|
||||
@click.prevent="isUpdating = false"
|
||||
solid
|
||||
secondary
|
||||
@click.prevent="isUpdating = false"
|
||||
>
|
||||
{{ t('components.common.RenderedDescription.button.cancel') }}
|
||||
</Button>
|
||||
|
|
|
@ -107,8 +107,12 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout main stack no-gap align-left
|
||||
<Layout
|
||||
v-title="labels.title"
|
||||
main
|
||||
stack
|
||||
no-gap
|
||||
align-left
|
||||
>
|
||||
<Header :h1="labels.title">
|
||||
<template #action>
|
||||
|
@ -119,69 +123,87 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
|||
</template>
|
||||
</Header>
|
||||
|
||||
<Loader v-if="isLoading"/>
|
||||
<Loader v-if="isLoading" />
|
||||
<Layout
|
||||
v-if="store.state.favorites.count > 0"
|
||||
form
|
||||
stack
|
||||
:class="['ui', { 'loading': isLoading }, 'form']"
|
||||
>
|
||||
<Spacer :size="16" />
|
||||
<Layout flex style="justify-content: flex-end;">
|
||||
<Layout stack noGap label for="favorites-ordering">
|
||||
<span class="label">
|
||||
{{ t('components.favorites.List.ordering.label') }}
|
||||
</span>
|
||||
<select
|
||||
id="favorites-ordering"
|
||||
v-model="ordering"
|
||||
class="dropdown"
|
||||
<Spacer :size="16" />
|
||||
<Layout
|
||||
flex
|
||||
style="justify-content: flex-end;"
|
||||
>
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="favorites-ordering"
|
||||
>
|
||||
<option
|
||||
v-for="option in orderingOptions"
|
||||
:key="option[0]"
|
||||
:value="option[0]"
|
||||
<span class="label">
|
||||
{{ t('components.favorites.List.ordering.label') }}
|
||||
</span>
|
||||
<select
|
||||
id="favorites-ordering"
|
||||
v-model="ordering"
|
||||
class="dropdown"
|
||||
>
|
||||
{{ sharedLabels.filters[option[1]] }}
|
||||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout stack noGap label for="favorites-ordering-direction">
|
||||
<span class="label">
|
||||
{{ t('components.favorites.List.ordering.direction.label') }}
|
||||
</span>
|
||||
<select
|
||||
id="favorites-ordering-direction"
|
||||
v-model="orderingDirection"
|
||||
class="dropdown"
|
||||
<option
|
||||
v-for="option in orderingOptions"
|
||||
:key="option[0]"
|
||||
:value="option[0]"
|
||||
>
|
||||
{{ sharedLabels.filters[option[1]] }}
|
||||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="favorites-ordering-direction"
|
||||
>
|
||||
<option value="+">
|
||||
{{ t('components.favorites.List.ordering.direction.ascending') }}
|
||||
</option>
|
||||
<option value="-">
|
||||
{{ t('components.favorites.List.ordering.direction.descending') }}
|
||||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout stack noGap label for="favorites-results">
|
||||
<span class="label">
|
||||
{{ t('components.favorites.List.pagination.results') }}
|
||||
</span>
|
||||
<select
|
||||
id="favorites-results"
|
||||
v-model="paginateBy"
|
||||
class="dropdown"
|
||||
>
|
||||
<option
|
||||
v-for="opt in paginateOptions"
|
||||
:key="opt"
|
||||
:value="opt"
|
||||
<span class="label">
|
||||
{{ t('components.favorites.List.ordering.direction.label') }}
|
||||
</span>
|
||||
<select
|
||||
id="favorites-ordering-direction"
|
||||
v-model="orderingDirection"
|
||||
class="dropdown"
|
||||
>
|
||||
{{ opt }}
|
||||
</option>
|
||||
</select>
|
||||
<option value="+">
|
||||
{{ t('components.favorites.List.ordering.direction.ascending') }}
|
||||
</option>
|
||||
<option value="-">
|
||||
{{ t('components.favorites.List.ordering.direction.descending') }}
|
||||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="favorites-results"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.favorites.List.pagination.results') }}
|
||||
</span>
|
||||
<select
|
||||
id="favorites-results"
|
||||
v-model="paginateBy"
|
||||
class="dropdown"
|
||||
>
|
||||
<option
|
||||
v-for="opt in paginateOptions"
|
||||
:key="opt"
|
||||
:value="opt"
|
||||
>
|
||||
{{ opt }}
|
||||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<TrackTable
|
||||
v-if="results"
|
||||
:search="true"
|
||||
|
@ -190,13 +212,18 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
|||
:tracks="results"
|
||||
/>
|
||||
</Layout>
|
||||
<Alert blue align-items="center"
|
||||
<Alert
|
||||
v-else
|
||||
blue
|
||||
align-items="center"
|
||||
>
|
||||
<i class="bi bi-heartbreak-fill" style="font-size: 100px;" />
|
||||
<i
|
||||
class="bi bi-heartbreak-fill"
|
||||
style="font-size: 100px;"
|
||||
/>
|
||||
<Spacer />
|
||||
{{ t('components.favorites.List.empty.noFavorites') }}
|
||||
<Spacer :size="32"/>
|
||||
<Spacer :size="32" />
|
||||
<Link
|
||||
to="/library"
|
||||
solid
|
||||
|
|
|
@ -55,6 +55,5 @@ const title = computed(() => isFavorite.value
|
|||
:aria-label="title"
|
||||
:title="title"
|
||||
@click.stop="store.dispatch('favorites/toggle', track.id)"
|
||||
>
|
||||
</Button>
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -80,8 +80,9 @@ const { start: startPolling } = useTimeoutFn(poll, 1000, { immediate: false })
|
|||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
<Modal :title="t('components.federation.FetchButton.header.refresh')"
|
||||
<Modal
|
||||
v-model="showModal"
|
||||
:title="t('components.federation.FetchButton.header.refresh')"
|
||||
class="small"
|
||||
:cancel="t('components.federation.FetchButton.button.close')"
|
||||
>
|
||||
|
|
|
@ -69,9 +69,13 @@ watch(() => props.url, () => {
|
|||
align-left
|
||||
small-items
|
||||
>
|
||||
<Loader v-if="isLoading" style="grid-column: 1 / -1;" />
|
||||
<Alert blue
|
||||
<Loader
|
||||
v-if="isLoading"
|
||||
style="grid-column: 1 / -1;"
|
||||
/>
|
||||
<Alert
|
||||
v-if="!isLoading && libraries.length === 0"
|
||||
blue
|
||||
style="grid-column: 1 / -1;"
|
||||
>
|
||||
{{ t('components.federation.LibraryWidget.empty.noMatch') }}
|
||||
|
|
|
@ -45,9 +45,10 @@ const copyPassword = () => {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<Input password
|
||||
<Input
|
||||
:id="fieldId"
|
||||
v-model="value"
|
||||
password
|
||||
required
|
||||
>
|
||||
<template #input-right>
|
||||
|
|
|
@ -69,7 +69,7 @@ const labels = computed(() => ({
|
|||
|
||||
const {
|
||||
isShuffled,
|
||||
shuffle,
|
||||
shuffle
|
||||
} = useQueue()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
@ -150,7 +150,10 @@ const remove = async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout stack main>
|
||||
<Layout
|
||||
stack
|
||||
main
|
||||
>
|
||||
<Loader
|
||||
v-if="isLoading"
|
||||
v-title="labels.title"
|
||||
|
@ -162,114 +165,129 @@ const remove = async () => {
|
|||
v-lazy="store.getters['instance/absoluteUrl'](object.cover.urls.large_square_crop)"
|
||||
:alt="object.title"
|
||||
class="channel-image"
|
||||
/>
|
||||
>
|
||||
<img
|
||||
v-else-if="object.artist_credit && object.artist_credit[0] && object.artist_credit[0].artist.cover"
|
||||
v-lazy="object.artist_credit[0].artist.cover.urls.large_square_crop"
|
||||
:alt="object.artist_credit[0].artist.name"
|
||||
class="channel-image"
|
||||
v-lazy="object.artist_credit[0].artist.cover.urls.large_square_crop"
|
||||
/>
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="channel-image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
/>
|
||||
>
|
||||
<!-- ({target}) => target -->
|
||||
<!-- Header (TODO: Put into Header component) Hint: Header is heavier fontweight than h1! -->
|
||||
<Layout stack style="flex: 1; gap: 8px;">
|
||||
<h1>{{ object.title }}</h1>
|
||||
<!-- <Header :h1="object.title" /> -->
|
||||
<artist-credit-label
|
||||
v-if="artistCredit"
|
||||
:artist-credit="artistCredit"
|
||||
<Layout
|
||||
stack
|
||||
style="flex: 1; gap: 8px;"
|
||||
>
|
||||
<h1>{{ object.title }}</h1>
|
||||
<!-- <Header :h1="object.title" /> -->
|
||||
<artist-credit-label
|
||||
v-if="artistCredit"
|
||||
:artist-credit="artistCredit"
|
||||
/>
|
||||
<!-- Metadata: -->
|
||||
<div class="meta">
|
||||
<template v-if="object.release_date">
|
||||
{{ momentFormat(new Date(object.release_date ?? '1970-01-01'), 'Y') }}
|
||||
<i class="bi bi-dot" />
|
||||
</template>
|
||||
<template v-if="totalTracks > 0">
|
||||
<span v-if="isSerie">
|
||||
{{ t('components.library.AlbumBase.meta.episodes', totalTracks) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('components.library.AlbumBase.meta.tracks', totalTracks) }}
|
||||
</span>
|
||||
</template>
|
||||
<i
|
||||
v-if="totalDuration > 0"
|
||||
class="bi bi-dot"
|
||||
/>
|
||||
<human-duration
|
||||
v-if="totalDuration > 0"
|
||||
:duration="totalDuration"
|
||||
/>
|
||||
<!--TODO: License -->
|
||||
</div>
|
||||
<Layout flex>
|
||||
<rendered-description
|
||||
v-if="object.description"
|
||||
:content="object.description"
|
||||
:can-update="true"
|
||||
/>
|
||||
<!-- Metadata: -->
|
||||
<div class="meta">
|
||||
<template v-if="object.release_date">
|
||||
{{ momentFormat(new Date(object.release_date ?? '1970-01-01'), 'Y') }}
|
||||
<i class="bi bi-dot" />
|
||||
</template>
|
||||
<template v-if="totalTracks > 0">
|
||||
<span v-if="isSerie">
|
||||
{{ t('components.library.AlbumBase.meta.episodes', totalTracks) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('components.library.AlbumBase.meta.tracks', totalTracks) }}
|
||||
</span>
|
||||
</template>
|
||||
<i v-if="totalDuration > 0" class="bi bi-dot" />
|
||||
<human-duration
|
||||
v-if="totalDuration > 0"
|
||||
:duration="totalDuration"
|
||||
/>
|
||||
<!--TODO: License -->
|
||||
</div>
|
||||
<Layout flex>
|
||||
<rendered-description
|
||||
v-if="object.description"
|
||||
:content="object.description"
|
||||
:can-update="true"
|
||||
/>
|
||||
</Layout>
|
||||
<Layout flex>
|
||||
<PlayButton
|
||||
split
|
||||
:tracks="object.tracks"
|
||||
:is-playable="object.is_playable"
|
||||
/>
|
||||
<Button
|
||||
v-if="object.tracks.length > 2"
|
||||
primary
|
||||
icon="bi-shuffle"
|
||||
:aria-label="labels.shuffle"
|
||||
@click.prevent.stop="shuffle()"
|
||||
>
|
||||
{{ labels.shuffle }}
|
||||
</Button>
|
||||
<DangerousButton
|
||||
v-if="artistCredit[0] &&
|
||||
store.state.auth.authenticated &&
|
||||
artistCredit[0].artist.channel &&
|
||||
artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername"
|
||||
:is-loading="isLoading"
|
||||
@confirm="remove()"
|
||||
icon="bi-trash"
|
||||
>
|
||||
{{ t('components.library.AlbumDropdown.button.delete') }}
|
||||
</DangerousButton>
|
||||
<Spacer h grow />
|
||||
<TrackFavoriteIcon v-if="store.state.auth.authenticated" :album="object" />
|
||||
<TrackPlaylistIcon v-if="store.state.auth.authenticated" :album="object" />
|
||||
<!-- TODO: Share Button -->
|
||||
<album-dropdown
|
||||
:object="object"
|
||||
:public-libraries="publicLibraries"
|
||||
:is-loading="isLoading"
|
||||
:is-album="isAlbum"
|
||||
:is-serie="isSerie"
|
||||
:is-channel="isChannel"
|
||||
:artist-credit="artistCredit"
|
||||
@remove="remove"
|
||||
/>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<Layout flex>
|
||||
<PlayButton
|
||||
split
|
||||
:tracks="object.tracks"
|
||||
:is-playable="object.is_playable"
|
||||
/>
|
||||
<Button
|
||||
v-if="object.tracks.length > 2"
|
||||
primary
|
||||
icon="bi-shuffle"
|
||||
:aria-label="labels.shuffle"
|
||||
@click.prevent.stop="shuffle()"
|
||||
>
|
||||
{{ labels.shuffle }}
|
||||
</Button>
|
||||
<DangerousButton
|
||||
v-if="artistCredit[0] &&
|
||||
store.state.auth.authenticated &&
|
||||
artistCredit[0].artist.channel &&
|
||||
artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername"
|
||||
:is-loading="isLoading"
|
||||
icon="bi-trash"
|
||||
@confirm="remove()"
|
||||
>
|
||||
{{ t('components.library.AlbumDropdown.button.delete') }}
|
||||
</DangerousButton>
|
||||
<Spacer
|
||||
h
|
||||
grow
|
||||
/>
|
||||
<TrackFavoriteIcon
|
||||
v-if="store.state.auth.authenticated"
|
||||
:album="object"
|
||||
/>
|
||||
<TrackPlaylistIcon
|
||||
v-if="store.state.auth.authenticated"
|
||||
:album="object"
|
||||
/>
|
||||
<!-- TODO: Share Button -->
|
||||
<album-dropdown
|
||||
:object="object"
|
||||
:public-libraries="publicLibraries"
|
||||
:is-loading="isLoading"
|
||||
:is-album="isAlbum"
|
||||
:is-serie="isSerie"
|
||||
:is-channel="isChannel"
|
||||
:artist-credit="artistCredit"
|
||||
@remove="remove"
|
||||
/>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
<div style="flex 1;">
|
||||
<router-view
|
||||
v-if="object"
|
||||
:key="route.fullPath"
|
||||
:paginate-by="paginateBy"
|
||||
:total-tracks="totalTracks"
|
||||
:is-serie="isSerie"
|
||||
:artist-credit="artistCredit"
|
||||
:object="object"
|
||||
:is-loading-tracks="isLoadingTracks"
|
||||
object-type="album"
|
||||
@libraries-loaded="libraries = $event"
|
||||
/>
|
||||
</div>
|
||||
<div style="flex 1;">
|
||||
<router-view
|
||||
v-if="object"
|
||||
:key="route.fullPath"
|
||||
:paginate-by="paginateBy"
|
||||
:total-tracks="totalTracks"
|
||||
:is-serie="isSerie"
|
||||
:artist-credit="artistCredit"
|
||||
:object="object"
|
||||
:is-loading-tracks="isLoadingTracks"
|
||||
object-type="album"
|
||||
@libraries-loaded="libraries = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
|
@ -139,8 +139,8 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
|
|||
<Spacer />
|
||||
<library-widget
|
||||
:url="'albums/' + object.id + '/libraries/'"
|
||||
@loaded="emit('libraries-loaded', $event)"
|
||||
:title="t('components.library.AlbumDetail.header.libraries')"
|
||||
@loaded="emit('libraries-loaded', $event)"
|
||||
>
|
||||
{{ t('components.library.AlbumDetail.description.libraries') }}
|
||||
</library-widget>
|
||||
|
|
|
@ -16,7 +16,7 @@ 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 OptionsButton from "~/components/ui/button/Options.vue"
|
||||
import OptionsButton from '~/components/ui/button/Options.vue'
|
||||
|
||||
interface Events {
|
||||
(e: 'remove'): void
|
||||
|
@ -60,9 +60,10 @@ const open = ref(false)
|
|||
|
||||
<template>
|
||||
<span>
|
||||
<Modal :title="t('components.library.AlbumDropdown.modal.embed.header')"
|
||||
<Modal
|
||||
v-if="isEmbedable"
|
||||
v-model="showEmbedModal"
|
||||
:title="t('components.library.AlbumDropdown.modal.embed.header')"
|
||||
:cancel="t('components.library.AlbumDropdown.button.cancel')"
|
||||
>
|
||||
<div class="scrolling content">
|
||||
|
@ -78,13 +79,13 @@ const open = ref(false)
|
|||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton
|
||||
:title="labels.more"
|
||||
isSquare
|
||||
is-square
|
||||
@click="toggleOpen()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #items>
|
||||
<PopoverItem
|
||||
<PopoverItem
|
||||
v-if="domain != store.getters['instance/domain']"
|
||||
:to="object.fid"
|
||||
icon="bi-box-arrow-up-right"
|
||||
|
@ -95,8 +96,8 @@ const open = ref(false)
|
|||
|
||||
<PopoverItem
|
||||
v-if="isEmbedable"
|
||||
@click="showEmbedModal = !showEmbedModal"
|
||||
icon="bi-code"
|
||||
@click="showEmbedModal = !showEmbedModal"
|
||||
>
|
||||
{{ t('components.library.AlbumDropdown.button.embed') }}
|
||||
</PopoverItem>
|
||||
|
@ -131,14 +132,14 @@ const open = ref(false)
|
|||
|
||||
<PopoverItem
|
||||
v-if="artistCredit[0] &&
|
||||
store.state.auth.authenticated &&
|
||||
artistCredit[0].artist.channel &&
|
||||
artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername"
|
||||
store.state.auth.authenticated &&
|
||||
artistCredit[0].artist.channel &&
|
||||
artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername"
|
||||
>
|
||||
<DangerousButton
|
||||
:is-loading="isLoading"
|
||||
@confirm="remove()"
|
||||
icon="bi-trash"
|
||||
@confirm="remove()"
|
||||
>
|
||||
{{ t('components.library.AlbumDropdown.button.delete') }}
|
||||
</DangerousButton>
|
||||
|
@ -152,8 +153,8 @@ const open = ref(false)
|
|||
channel: artistCredit[0]?.artist.channel
|
||||
})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
@click="report(obj)"
|
||||
icon="bi-flag"
|
||||
@click="report(obj)"
|
||||
>
|
||||
{{ obj.label }}
|
||||
</PopoverItem>
|
||||
|
|
|
@ -131,27 +131,38 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout stack main v-title="labels.title">
|
||||
<Layout
|
||||
v-title="labels.title"
|
||||
stack
|
||||
main
|
||||
>
|
||||
<Header :h1="t('components.library.Albums.header.browse')" />
|
||||
<Layout form flex
|
||||
<Layout
|
||||
form
|
||||
flex
|
||||
:class="['ui', {'loading': isLoading}, 'form']"
|
||||
@submit.prevent="search"
|
||||
>
|
||||
<Input search
|
||||
<Input
|
||||
id="album-search"
|
||||
v-model="query"
|
||||
search
|
||||
name="search"
|
||||
:label="t('components.library.Albums.label.search')"
|
||||
autofocus
|
||||
:placeholder="labels.searchPlaceholder"
|
||||
>
|
||||
</Input>
|
||||
/>
|
||||
<Pills
|
||||
v-model="tagList"
|
||||
:label="t('components.library.Albums.label.tags')"
|
||||
style="max-width: 150px;"
|
||||
/>
|
||||
<Layout stack noGap label for="album-ordering">
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="album-ordering"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.library.Albums.ordering.label') }}
|
||||
</span>
|
||||
|
@ -169,7 +180,12 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout stack noGap label for="album-ordering-direction">
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="album-ordering-direction"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.library.Albums.ordering.direction.label') }}
|
||||
</span>
|
||||
|
@ -186,7 +202,12 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout stack noGap label for="album-results">
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="album-results"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.library.Albums.pagination.results') }}
|
||||
</span>
|
||||
|
@ -205,14 +226,15 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
</select>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<Loader v-if="isLoading"/>
|
||||
<Loader v-if="isLoading" />
|
||||
<Pagination
|
||||
v-if="result && result.count > paginateBy"
|
||||
v-model:page="page"
|
||||
:pages="Math.ceil((result.count || 0)/paginateBy)"
|
||||
/>
|
||||
<Layout grid
|
||||
<Layout
|
||||
v-if="result && result.results.length > 0"
|
||||
grid
|
||||
style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;"
|
||||
>
|
||||
<AlbumCard
|
||||
|
@ -222,8 +244,8 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
/>
|
||||
</Layout>
|
||||
<Layout
|
||||
stack
|
||||
v-else-if="result && result.results.length === 0"
|
||||
stack
|
||||
>
|
||||
<Alert blue>
|
||||
<i class="bi bi-disc" />
|
||||
|
@ -239,7 +261,10 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
:to="useModal('upload').to"
|
||||
>
|
||||
<template #image>
|
||||
<i class="bi bi-upload" style="font-size: 100px; position: relative; top: 50px;" />
|
||||
<i
|
||||
class="bi bi-upload"
|
||||
style="font-size: 100px; position: relative; top: 50px;"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</Layout>
|
||||
|
|
|
@ -25,7 +25,6 @@ import Layout from '~/components/ui/Layout.vue'
|
|||
import Modal from '~/components/ui/Modal.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
|
||||
|
||||
interface Props {
|
||||
id: number
|
||||
}
|
||||
|
@ -56,8 +55,7 @@ const musicbrainzUrl = computed(() => object.value?.mbid ? `https://musicbrainz.
|
|||
const discogsUrl = computed(() => `https://discogs.com/search/?type=artist&title=${encodeURI(object.value?.name ?? '')}`)
|
||||
const publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
|
||||
|
||||
|
||||
const cover = computed(() => {
|
||||
const cover = computed(() => {
|
||||
const artistCover: Cover | undefined = object.value?.cover
|
||||
|
||||
const albumCover: Cover | undefined = object.value?.albums
|
||||
|
@ -68,12 +66,12 @@ const cover = computed(() => {
|
|||
)?.cover
|
||||
|
||||
const fallback : Cover = {
|
||||
uuid: '',
|
||||
urls: {
|
||||
original: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
|
||||
medium_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
|
||||
large_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`
|
||||
}
|
||||
uuid: '',
|
||||
urls: {
|
||||
original: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
|
||||
medium_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
|
||||
large_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`
|
||||
}
|
||||
}
|
||||
|
||||
return artistCover
|
||||
|
@ -122,7 +120,11 @@ watch(() => props.id, fetchData, { immediate: true })
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout stack main v-title="labels.title">
|
||||
<Layout
|
||||
v-title="labels.title"
|
||||
stack
|
||||
main
|
||||
>
|
||||
<Loader v-if="isLoading" />
|
||||
<template v-if="object && !isLoading">
|
||||
<Layout flex>
|
||||
|
@ -131,9 +133,16 @@ watch(() => props.id, fetchData, { immediate: true })
|
|||
:alt="object.name"
|
||||
class="channel-image"
|
||||
>
|
||||
<Layout stack style="flex: 1; gap: 8px;">
|
||||
<Layout
|
||||
stack
|
||||
style="flex: 1; gap: 8px;"
|
||||
>
|
||||
<h1>{{ object.name }}</h1>
|
||||
<Layout flex class="meta" style="gap: 0;">
|
||||
<Layout
|
||||
flex
|
||||
class="meta"
|
||||
style="gap: 0;"
|
||||
>
|
||||
<div
|
||||
v-if="albums"
|
||||
>
|
||||
|
@ -181,8 +190,8 @@ watch(() => props.id, fetchData, { immediate: true })
|
|||
|
||||
<PopoverItem
|
||||
v-if="publicLibraries.length > 0"
|
||||
@click="showEmbedModal = true"
|
||||
icon="bi-code-square"
|
||||
@click="showEmbedModal = true"
|
||||
>
|
||||
{{ t('components.library.ArtistBase.button.embed') }}
|
||||
</PopoverItem>
|
||||
|
|
|
@ -6,7 +6,6 @@ import { ref, computed, reactive } from 'vue'
|
|||
import { useStore } from '~/store'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
||||
|
@ -14,7 +13,7 @@ import TrackTable from '~/components/audio/track/Table.vue'
|
|||
import TagsList from '~/components/tags/List.vue'
|
||||
import AlbumCard from '~/components/album/Card.vue'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Heading from "~/components/ui/Heading.vue";
|
||||
import Heading from '~/components/ui/Heading.vue'
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
import Link from '~/components/ui/Link.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
|
@ -69,14 +68,18 @@ const loadMoreAlbums = async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout stack v-if="object">
|
||||
<Layout
|
||||
v-if="object"
|
||||
stack
|
||||
>
|
||||
<TagsList
|
||||
v-if="object.tags && object.tags.length > 0"
|
||||
style="margin-top: -16px;"
|
||||
:tags="object.tags"
|
||||
/>
|
||||
<Alert blue
|
||||
<Alert
|
||||
v-if="contentFilter"
|
||||
blue
|
||||
>
|
||||
<p>
|
||||
{{ t('components.library.ArtistDetail.message.filter') }}
|
||||
|
@ -96,7 +99,10 @@ const loadMoreAlbums = async () => {
|
|||
</Alert>
|
||||
<Loader v-if="isLoadingAlbums" />
|
||||
<template v-else-if="albums && albums.length > 0">
|
||||
<Heading h2 section-heading>
|
||||
<Heading
|
||||
h2
|
||||
section-heading
|
||||
>
|
||||
{{ t('components.library.ArtistDetail.header.album') }}
|
||||
</Heading>
|
||||
<Layout flex>
|
||||
|
@ -105,7 +111,10 @@ const loadMoreAlbums = async () => {
|
|||
:key="album.id"
|
||||
:album="album"
|
||||
/>
|
||||
<Spacer h grow />
|
||||
<Spacer
|
||||
h
|
||||
grow
|
||||
/>
|
||||
<Button
|
||||
v-if="loadMoreAlbumsUrl !== null"
|
||||
primary
|
||||
|
@ -117,7 +126,10 @@ const loadMoreAlbums = async () => {
|
|||
</Layout>
|
||||
</template>
|
||||
<template v-if="tracks.length > 0">
|
||||
<Heading h2 section-heading>
|
||||
<Heading
|
||||
h2
|
||||
section-heading
|
||||
>
|
||||
{{ t('components.library.ArtistDetail.header.track') }}
|
||||
</Heading>
|
||||
<TrackTable
|
||||
|
@ -127,7 +139,10 @@ const loadMoreAlbums = async () => {
|
|||
:tracks="tracks.slice(0,5)"
|
||||
/>
|
||||
</template>
|
||||
<Heading h2 section-heading>
|
||||
<Heading
|
||||
h2
|
||||
section-heading
|
||||
>
|
||||
{{ t('components.library.ArtistDetail.header.library') }}
|
||||
</Heading>
|
||||
<LibraryWidget
|
||||
|
|
|
@ -26,23 +26,26 @@ const canEdit = store.state.auth.availablePermissions.library
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout stack>
|
||||
<Spacer />
|
||||
<Section no-items alignLeft
|
||||
:h2="canEdit
|
||||
? t('components.library.ArtistEdit.header.edit')
|
||||
: t('components.library.ArtistEdit.header.suggest')
|
||||
"
|
||||
/>
|
||||
<Alert yellow
|
||||
v-if="!object.is_local"
|
||||
>
|
||||
{{ t('components.library.ArtistEdit.message.remote') }}
|
||||
</Alert>
|
||||
<edit-form
|
||||
v-else
|
||||
:object-type="objectType"
|
||||
:object="object"
|
||||
/>
|
||||
</Layout>
|
||||
<Layout stack>
|
||||
<Spacer />
|
||||
<Section
|
||||
no-items
|
||||
align-left
|
||||
:h2="canEdit
|
||||
? t('components.library.ArtistEdit.header.edit')
|
||||
: t('components.library.ArtistEdit.header.suggest')
|
||||
"
|
||||
/>
|
||||
<Alert
|
||||
v-if="!object.is_local"
|
||||
yellow
|
||||
>
|
||||
{{ t('components.library.ArtistEdit.message.remote') }}
|
||||
</Alert>
|
||||
<edit-form
|
||||
v-else
|
||||
:object-type="objectType"
|
||||
:object="object"
|
||||
/>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
|
@ -130,27 +130,38 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout stack main v-title="labels.title">
|
||||
<Layout
|
||||
v-title="labels.title"
|
||||
stack
|
||||
main
|
||||
>
|
||||
<Header :h1="t('components.library.Artists.header.browse')" />
|
||||
<Layout form flex
|
||||
<Layout
|
||||
form
|
||||
flex
|
||||
:class="['ui', {'loading': isLoading}, 'form']"
|
||||
@submit.prevent="search"
|
||||
>
|
||||
<Input search
|
||||
<Input
|
||||
id="artist-search"
|
||||
v-model="query"
|
||||
search
|
||||
name="search"
|
||||
:label="t('components.library.Artists.label.search')"
|
||||
autofocus
|
||||
:placeholder="labels.searchPlaceholder"
|
||||
>
|
||||
</Input>
|
||||
/>
|
||||
<Pills
|
||||
v-model="tagList"
|
||||
:label="t('components.library.Artists.label.tags')"
|
||||
style="max-width: 150px;"
|
||||
/>
|
||||
<Layout stack noGap label for="artist-ordering">
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="artist-ordering"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.library.Artists.ordering.label') }}
|
||||
</span>
|
||||
|
@ -168,7 +179,12 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout stack noGap label for="artist-ordering-direction">
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="artist-ordering-direction"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.library.Artists.ordering.direction.label') }}
|
||||
</span>
|
||||
|
@ -185,7 +201,12 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout stack noGap label for="artist-results">
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="artist-results"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.library.Artists.pagination.results') }}
|
||||
</span>
|
||||
|
@ -204,22 +225,23 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
</select>
|
||||
</Layout>
|
||||
<Toggle
|
||||
:label="t('components.library.Artists.label.excludeCompilation')"
|
||||
id="exclude-compilation"
|
||||
v-model="excludeCompilation"
|
||||
:label="t('components.library.Artists.label.excludeCompilation')"
|
||||
true-value="true"
|
||||
false-value="null"
|
||||
type="checkbox"
|
||||
/>
|
||||
</Layout>
|
||||
<Loader v-if="isLoading"/>
|
||||
<Loader v-if="isLoading" />
|
||||
<Pagination
|
||||
v-if="result && result.count > paginateBy"
|
||||
v-model:page="page"
|
||||
:pages="Math.ceil(result.count / paginateBy)"
|
||||
/>
|
||||
<Layout grid
|
||||
<Layout
|
||||
v-if="result && result.results.length > 0"
|
||||
grid
|
||||
style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;"
|
||||
>
|
||||
<ArtistCard
|
||||
|
@ -229,8 +251,8 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
/>
|
||||
</Layout>
|
||||
<Layout
|
||||
stack
|
||||
v-else-if="result && result.results.length === 0"
|
||||
stack
|
||||
>
|
||||
<Alert yellow>
|
||||
<i class="compact disc icon" />
|
||||
|
@ -246,7 +268,10 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
:to="useModal('upload').to"
|
||||
>
|
||||
<template #image>
|
||||
<i class="bi bi-upload" style="font-size: 100px; position: relative; top: 50px;" />
|
||||
<i
|
||||
class="bi bi-upload"
|
||||
style="font-size: 100px; position: relative; top: 50px;"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</Layout>
|
||||
|
|
|
@ -169,7 +169,7 @@ const alertProps = computed(() => {
|
|||
|
||||
<template>
|
||||
<Card
|
||||
:alertProps="alertProps"
|
||||
:alert-props="alertProps"
|
||||
:to="detailUrl"
|
||||
:title="t('components.library.EditCard.header.modification', {id: obj.uuid.substring(0, 8)})"
|
||||
>
|
||||
|
@ -183,7 +183,6 @@ const alertProps = computed(() => {
|
|||
<i class="bi bi-file-music-fill" />
|
||||
{{ t('components.library.EditCard.link.track', {id: obj.target.id, name: obj.target.repr}) }}
|
||||
</router-link>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -194,115 +193,115 @@ const alertProps = computed(() => {
|
|||
</div>
|
||||
|
||||
<template #alert>
|
||||
<span class="right floated">
|
||||
<span v-if="obj.is_approved && obj.is_applied">
|
||||
<i class="green bi bi-check"/>
|
||||
{{ t('components.library.EditCard.status.applied') }}
|
||||
</span>
|
||||
<span v-else-if="obj.is_approved">
|
||||
<i class="green bi bi-check" />
|
||||
{{ t('components.library.EditCard.status.approved') }}
|
||||
</span>
|
||||
<span v-else-if="obj.is_approved === null">
|
||||
<i class="yellow bi bi-hourglass" />
|
||||
{{ t('components.library.EditCard.status.pending') }}
|
||||
</span>
|
||||
<span v-else-if="obj.is_approved === false">
|
||||
<i class="destructive bi bi-x" />
|
||||
{{ t('components.library.EditCard.status.rejected') }}
|
||||
</span>
|
||||
<span class="right floated">
|
||||
<span v-if="obj.is_approved && obj.is_applied">
|
||||
<i class="green bi bi-check" />
|
||||
{{ t('components.library.EditCard.status.applied') }}
|
||||
</span>
|
||||
<table
|
||||
v-if="obj.type === 'update'"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{ t('components.library.EditCard.table.update.header.field') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.library.EditCard.table.update.header.oldValue') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.library.EditCard.table.update.header.newValue') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="field in updatedFields"
|
||||
:key="field.id"
|
||||
<span v-else-if="obj.is_approved">
|
||||
<i class="green bi bi-check" />
|
||||
{{ t('components.library.EditCard.status.approved') }}
|
||||
</span>
|
||||
<span v-else-if="obj.is_approved === null">
|
||||
<i class="yellow bi bi-hourglass" />
|
||||
{{ t('components.library.EditCard.status.pending') }}
|
||||
</span>
|
||||
<span v-else-if="obj.is_approved === false">
|
||||
<i class="destructive bi bi-x" />
|
||||
{{ t('components.library.EditCard.status.rejected') }}
|
||||
</span>
|
||||
</span>
|
||||
<table
|
||||
v-if="obj.type === 'update'"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{ t('components.library.EditCard.table.update.header.field') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.library.EditCard.table.update.header.oldValue') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.library.EditCard.table.update.header.newValue') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="field in updatedFields"
|
||||
:key="field.id"
|
||||
>
|
||||
<td>{{ field.id }}</td>
|
||||
|
||||
<td v-if="field.diff">
|
||||
<template v-if="field.config?.type === 'attachment' && field.oldRepr">
|
||||
<img
|
||||
class="image"
|
||||
alt=""
|
||||
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.oldRepr}/proxy?next=medium_square_crop`)"
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
v-for="(part, key) in field.diff.filter(p => !p.added)"
|
||||
:key="key"
|
||||
:class="['diff', {removed: part.removed}]"
|
||||
>
|
||||
{{ part.value }}
|
||||
</span>
|
||||
</template>
|
||||
</td>
|
||||
<td v-else>
|
||||
{{ t('components.library.EditCard.table.update.notApplicable') }}
|
||||
</td>
|
||||
|
||||
<td
|
||||
v-if="field.diff"
|
||||
:title="field.newRepr"
|
||||
>
|
||||
<td>{{ field.id }}</td>
|
||||
|
||||
<td v-if="field.diff">
|
||||
<template v-if="field.config?.type === 'attachment' && field.oldRepr">
|
||||
<img
|
||||
class="image"
|
||||
alt=""
|
||||
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.oldRepr}/proxy?next=medium_square_crop`)"
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
v-for="(part, key) in field.diff.filter(p => !p.added)"
|
||||
:key="key"
|
||||
:class="['diff', {removed: part.removed}]"
|
||||
>
|
||||
{{ part.value }}
|
||||
</span>
|
||||
</template>
|
||||
</td>
|
||||
<td v-else>
|
||||
{{ t('components.library.EditCard.table.update.notApplicable') }}
|
||||
</td>
|
||||
|
||||
<td
|
||||
v-if="field.diff"
|
||||
:title="field.newRepr"
|
||||
>
|
||||
<template v-if="field.config?.type === 'attachment' && field.newRepr">
|
||||
<img
|
||||
class="ui image"
|
||||
alt=""
|
||||
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)"
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
v-for="(part, key) in field.diff.filter(p => !p.removed)"
|
||||
:key="key"
|
||||
:class="['diff', {added: part.added}]"
|
||||
>
|
||||
{{ part.value }}
|
||||
</span>
|
||||
</template>
|
||||
</td>
|
||||
<td
|
||||
v-else
|
||||
:title="field.newRepr"
|
||||
>
|
||||
<template v-if="field.config?.type === 'attachment' && field.newRepr">
|
||||
<img
|
||||
class="ui image"
|
||||
alt=""
|
||||
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)"
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ field.newRepr }}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<template v-if="field.config?.type === 'attachment' && field.newRepr">
|
||||
<img
|
||||
class="ui image"
|
||||
alt=""
|
||||
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)"
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
v-for="(part, key) in field.diff.filter(p => !p.removed)"
|
||||
:key="key"
|
||||
:class="['diff', {added: part.added}]"
|
||||
>
|
||||
{{ part.value }}
|
||||
</span>
|
||||
</template>
|
||||
</td>
|
||||
<td
|
||||
v-else
|
||||
:title="field.newRepr"
|
||||
>
|
||||
<template v-if="field.config?.type === 'attachment' && field.newRepr">
|
||||
<img
|
||||
class="ui image"
|
||||
alt=""
|
||||
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)"
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ field.newRepr }}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="obj.created_by"
|
||||
class="extra content"
|
||||
>
|
||||
<Spacer :size="8"/>
|
||||
<Spacer :size="8" />
|
||||
<actor-link :actor="obj.created_by" />
|
||||
</div>
|
||||
|
||||
|
@ -313,13 +312,14 @@ const alertProps = computed(() => {
|
|||
/>
|
||||
</template>
|
||||
|
||||
<template #action
|
||||
<template
|
||||
v-if="canDelete || canApprove"
|
||||
#action
|
||||
>
|
||||
<Button
|
||||
v-if="canApprove && obj.is_approved !== true"
|
||||
primary
|
||||
:isLoading="isLoading"
|
||||
:is-loading="isLoading"
|
||||
@click="approve(true)"
|
||||
>
|
||||
{{ t('components.library.EditCard.button.approve') }}
|
||||
|
@ -327,7 +327,7 @@ const alertProps = computed(() => {
|
|||
<Button
|
||||
v-if="canApprove && obj.is_approved === null"
|
||||
destructive
|
||||
:isLoading="isLoading"
|
||||
:is-loading="isLoading"
|
||||
@click="approve(false)"
|
||||
>
|
||||
{{ t('components.library.EditCard.button.reject') }}
|
||||
|
@ -335,7 +335,7 @@ const alertProps = computed(() => {
|
|||
<!--TODO: Make Dangerous Button hand through isLoading prop -->
|
||||
<dangerous-button
|
||||
v-if="canDelete"
|
||||
:isLoading="isLoading"
|
||||
:is-loading="isLoading"
|
||||
:action="remove"
|
||||
:title="t('components.library.EditCard.modal.delete.header')"
|
||||
>
|
||||
|
|
|
@ -48,10 +48,10 @@ fetchData()
|
|||
|
||||
<template>
|
||||
<section :class="{ loading: isLoading }">
|
||||
<edit-card
|
||||
v-if="obj"
|
||||
:obj="obj"
|
||||
:current-state="currentState"
|
||||
/>
|
||||
<edit-card
|
||||
v-if="obj"
|
||||
:obj="obj"
|
||||
:current-state="currentState"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
|
|
@ -13,9 +13,9 @@ import Layout from '~/components/ui/Layout.vue'
|
|||
import Button from '~/components/ui/Button.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
import Input from '~/components/ui/Input.vue'
|
||||
import Textarea from "~/components/ui/Textarea.vue"
|
||||
import Pills from "~/components/ui/Pills.vue"
|
||||
import Alert from "~/components/ui/Alert.vue"
|
||||
import Textarea from '~/components/ui/Textarea.vue'
|
||||
import Pills from '~/components/ui/Pills.vue'
|
||||
import Alert from '~/components/ui/Alert.vue'
|
||||
|
||||
import AttachmentInput from '~/components/common/AttachmentInput.vue'
|
||||
import useEditConfigs from '~/composables/moderation/useEditConfigs'
|
||||
|
@ -137,23 +137,29 @@ const resetField = (fieldId: string) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Alert green v-if="submittedMutation">
|
||||
<h4 class="header">
|
||||
{{ t('components.library.EditForm.header.success') }}
|
||||
</h4>
|
||||
<Alert
|
||||
v-if="submittedMutation"
|
||||
green
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.library.EditForm.header.success') }}
|
||||
</h4>
|
||||
<edit-card
|
||||
:obj="submittedMutation"
|
||||
:current-state="currentState"
|
||||
/>
|
||||
<Button
|
||||
solid primary
|
||||
solid
|
||||
primary
|
||||
@click.prevent="submittedMutation = null"
|
||||
>
|
||||
{{ t('components.library.EditForm.button.new') }}
|
||||
</Button>
|
||||
</Alert>
|
||||
<Layout gap-32 v-else>
|
||||
|
||||
<Layout
|
||||
v-else
|
||||
gap-32
|
||||
>
|
||||
<!-- Previous edits -->
|
||||
|
||||
<edit-list
|
||||
|
@ -163,7 +169,7 @@ const resetField = (fieldId: string) => {
|
|||
:current-state="currentState"
|
||||
>
|
||||
<div>
|
||||
<!--TODO: Use Section component with conditional headlines and action buttons-->
|
||||
<!--TODO: Use Section component with conditional headlines and action buttons-->
|
||||
<template v-if="showPendingReview">
|
||||
{{ t('components.library.EditForm.header.unreviewed') }}
|
||||
<Button
|
||||
|
@ -196,11 +202,14 @@ const resetField = (fieldId: string) => {
|
|||
|
||||
<!-- Add new edits -->
|
||||
|
||||
<form class="ui form" style="display: contents;"
|
||||
<form
|
||||
class="ui form"
|
||||
style="display: contents;"
|
||||
@submit.prevent="submit()"
|
||||
>
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="errors.length > 0"
|
||||
red
|
||||
role="alert"
|
||||
>
|
||||
<h4 class="header">
|
||||
|
@ -215,15 +224,18 @@ const resetField = (fieldId: string) => {
|
|||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="!canEdit"
|
||||
red
|
||||
>
|
||||
{{ t('components.library.EditForm.message.noPermission') }}
|
||||
</Alert>
|
||||
<Layout stack gap-8
|
||||
v-if="values"
|
||||
<Layout
|
||||
v-for="fieldConfig in config.fields"
|
||||
v-if="values"
|
||||
:key="fieldConfig.id"
|
||||
stack
|
||||
gap-8
|
||||
class="ui field"
|
||||
>
|
||||
<template v-if="fieldConfig.type === 'text'">
|
||||
|
@ -259,10 +271,10 @@ const resetField = (fieldId: string) => {
|
|||
</template>
|
||||
<template v-else-if="fieldConfig.type === 'content'">
|
||||
<Textarea
|
||||
:label="fieldConfig.label"
|
||||
v-model="values[fieldConfig.id].text"
|
||||
:label="fieldConfig.label"
|
||||
:field-id="fieldConfig.id"
|
||||
initialLines="3"
|
||||
initial-lines="3"
|
||||
/>
|
||||
</template>
|
||||
<!-- TODO: Style Attachment Input -->
|
||||
|
@ -281,9 +293,9 @@ const resetField = (fieldId: string) => {
|
|||
<template v-else-if="fieldConfig.type === 'tags'">
|
||||
<Pills
|
||||
:id="fieldConfig.id"
|
||||
:label="fieldConfig.label"
|
||||
ref="tags"
|
||||
v-model="values[fieldConfig.id]"
|
||||
:label="fieldConfig.label"
|
||||
required="fieldConfig.required"
|
||||
>
|
||||
<Button
|
||||
|
@ -298,7 +310,7 @@ const resetField = (fieldId: string) => {
|
|||
<Button
|
||||
low-height
|
||||
secondary
|
||||
alignSelf="end"
|
||||
align-self="end"
|
||||
icon="bi-arrow-counterclockwise"
|
||||
form="noop"
|
||||
:disabled="fieldValuesChanged(fieldConfig.id) ? undefined : true"
|
||||
|
@ -307,12 +319,12 @@ const resetField = (fieldId: string) => {
|
|||
{{ t('components.library.EditForm.button.reset') }}
|
||||
</Button>
|
||||
</Layout>
|
||||
<Spacer/>
|
||||
<Spacer />
|
||||
<Textarea
|
||||
id="change-summary"
|
||||
v-model="summary"
|
||||
name="change-summary"
|
||||
initialLines="3"
|
||||
initial-lines="3"
|
||||
:label="t('components.library.EditForm.label.summary')"
|
||||
:placeholder="labels.summaryPlaceholder"
|
||||
>
|
||||
|
@ -327,7 +339,7 @@ const resetField = (fieldId: string) => {
|
|||
</Button>
|
||||
</Textarea>
|
||||
<Button
|
||||
:isLoading="isLoading"
|
||||
:is-loading="isLoading"
|
||||
primary
|
||||
:disabled="isLoading || !mutationPayload"
|
||||
>
|
||||
|
|
|
@ -61,11 +61,10 @@ watchEffect(() => fetchData())
|
|||
:disabled="!previousPage"
|
||||
primary
|
||||
round
|
||||
alignSelf="center"
|
||||
align-self="center"
|
||||
icon="bi-chevron-left"
|
||||
@click="fetchData(previousPage)"
|
||||
>
|
||||
</Button>
|
||||
/>
|
||||
<Loader v-if="isLoading" />
|
||||
<edit-card
|
||||
v-for="obj in objects"
|
||||
|
@ -78,12 +77,11 @@ watchEffect(() => fetchData())
|
|||
<Button
|
||||
v-if="nextPage || previousPage"
|
||||
:disabled="!nextPage"
|
||||
alignSelf="center"
|
||||
align-self="center"
|
||||
primary
|
||||
round
|
||||
icon="bi-chevron-right"
|
||||
@click="fetchData(nextPage)"
|
||||
>
|
||||
</Button>
|
||||
/>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
|
@ -97,19 +97,20 @@ const library = ref<Library>()
|
|||
|
||||
// Old implementation:
|
||||
|
||||
watch(privacyLevel, async(newValue) => { try {
|
||||
const response = await axios.get<paths['/api/v2/libraries/']['get']['responses']['200']['content']['application/json']>('libraries/', {
|
||||
params: {
|
||||
privacy_level: privacyLevel.value,
|
||||
scope: 'me'
|
||||
}
|
||||
})
|
||||
|
||||
library.value = response.data.results.find(({name})=>name===privacyLevel.value)
|
||||
} catch (error) {
|
||||
useErrorHandler(error as Error)
|
||||
}}, { immediate: true })
|
||||
watch(privacyLevel, async (newValue) => {
|
||||
try {
|
||||
const response = await axios.get<paths['/api/v2/libraries/']['get']['responses']['200']['content']['application/json']>('libraries/', {
|
||||
params: {
|
||||
privacy_level: privacyLevel.value,
|
||||
scope: 'me'
|
||||
}
|
||||
})
|
||||
|
||||
library.value = response.data.results.find(({ name }) => name === privacyLevel.value)
|
||||
} catch (error) {
|
||||
useErrorHandler(error as Error)
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
//
|
||||
// File counts
|
||||
|
@ -355,15 +356,22 @@ useEventListener(window, 'beforeunload', (event) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Slider :options="options" v-model="privacyLevel" :label="t('components.manage.library.UploadsTable.label.visibility')" />
|
||||
<Slider
|
||||
v-model="privacyLevel"
|
||||
:options="options"
|
||||
:label="t('components.manage.library.UploadsTable.label.visibility')"
|
||||
/>
|
||||
<file-upload-widget
|
||||
ref="upload"
|
||||
v-model="files"
|
||||
:data="uploadData"
|
||||
@input-file="inputFile"
|
||||
>
|
||||
<Button primary icon="bi bi-upload" >
|
||||
{{ t('components.library.FileUpload.label.uploadWidget') }}
|
||||
<Button
|
||||
primary
|
||||
icon="bi bi-upload"
|
||||
>
|
||||
{{ t('components.library.FileUpload.label.uploadWidget') }}
|
||||
</Button>
|
||||
<p>
|
||||
{{ t('components.library.FileUpload.label.extensions', {extensions: supportedExtensions.join(', ')}) }}
|
||||
|
@ -602,11 +610,10 @@ useEventListener(window, 'beforeunload', (event) => {
|
|||
<template v-else-if="!file.success">
|
||||
<Button
|
||||
tiny
|
||||
@click.prevent="upload.remove(file)"
|
||||
style="float: right;"
|
||||
icon="bi-trash-fill"
|
||||
>
|
||||
</Button>
|
||||
@click.prevent="upload.remove(file)"
|
||||
/>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -9,7 +9,6 @@ import Layout from '~/components/ui/Layout.vue'
|
|||
import Button from '~/components/ui/Button.vue'
|
||||
import Input from '~/components/ui/Input.vue'
|
||||
|
||||
|
||||
interface Events {
|
||||
(e: 'update:modelValue', value: string[]): void
|
||||
(e: 'import'): void
|
||||
|
@ -37,7 +36,7 @@ const handleClick = (entry: FSEntry) => {
|
|||
|
||||
value.value.push(entry.name)
|
||||
}
|
||||
const path = computed (() => props.data.root + '/' + value.value.join('/'))
|
||||
const path = computed(() => props.data.root + '/' + value.value.join('/'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import type { FSLogs } from '~/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
interface Props {
|
||||
data: FSLogs
|
||||
}
|
||||
|
|
|
@ -62,43 +62,45 @@ fetchData()
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout main stack
|
||||
<Layout
|
||||
:key="route?.name ?? undefined"
|
||||
v-title="labels.title"
|
||||
main
|
||||
stack
|
||||
>
|
||||
<Header :h1="t('components.Sidebar.header.explore')" />
|
||||
<playlist-widget
|
||||
:url="'playlists/'"
|
||||
:filters="{scope: scope, playable: true, ordering: '-modification_date'}"
|
||||
:title="t('components.library.Home.header.playlists')"
|
||||
:limit="12"
|
||||
/>
|
||||
<Spacer />
|
||||
<channels-widget
|
||||
v-if="scope === 'all'"
|
||||
:show-modification-date="true"
|
||||
:filters="{ordering: '-creation_date'}"
|
||||
:limit="8"
|
||||
:title="t('components.library.Home.header.newChannels')"
|
||||
/>
|
||||
<Spacer />
|
||||
<track-widget
|
||||
:title="t('components.library.Home.header.recentlyListened')"
|
||||
:url="'history/listenings/'"
|
||||
:filters="{ scope, ordering: '-creation_date', ...qualityFilters }"
|
||||
:websocket-handlers="['Listen']"
|
||||
/>
|
||||
<Spacer />
|
||||
<track-widget
|
||||
:title="t('components.library.Home.header.recentlyFavorited')"
|
||||
:url="'favorites/tracks/'"
|
||||
:filters="{scope: scope, ordering: '-creation_date'}"
|
||||
/>
|
||||
<Spacer />
|
||||
<album-widget
|
||||
:filters="{scope: scope, playable: true, ordering: '-creation_date', ...qualityFilters}"
|
||||
:limit="12"
|
||||
:title="t('components.library.Home.header.recentlyAdded')"
|
||||
/>
|
||||
<Header :h1="t('components.Sidebar.header.explore')" />
|
||||
<playlist-widget
|
||||
:url="'playlists/'"
|
||||
:filters="{scope: scope, playable: true, ordering: '-modification_date'}"
|
||||
:title="t('components.library.Home.header.playlists')"
|
||||
:limit="12"
|
||||
/>
|
||||
<Spacer />
|
||||
<channels-widget
|
||||
v-if="scope === 'all'"
|
||||
:show-modification-date="true"
|
||||
:filters="{ordering: '-creation_date'}"
|
||||
:limit="8"
|
||||
:title="t('components.library.Home.header.newChannels')"
|
||||
/>
|
||||
<Spacer />
|
||||
<track-widget
|
||||
:title="t('components.library.Home.header.recentlyListened')"
|
||||
:url="'history/listenings/'"
|
||||
:filters="{ scope, ordering: '-creation_date', ...qualityFilters }"
|
||||
:websocket-handlers="['Listen']"
|
||||
/>
|
||||
<Spacer />
|
||||
<track-widget
|
||||
:title="t('components.library.Home.header.recentlyFavorited')"
|
||||
:url="'favorites/tracks/'"
|
||||
:filters="{scope: scope, ordering: '-creation_date'}"
|
||||
/>
|
||||
<Spacer />
|
||||
<album-widget
|
||||
:filters="{scope: scope, playable: true, ordering: '-creation_date', ...qualityFilters}"
|
||||
:limit="12"
|
||||
:title="t('components.library.Home.header.recentlyAdded')"
|
||||
/>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
|
@ -76,8 +76,9 @@ const getErrorData = (upload: Upload) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="t('components.library.ImportStatusModal.header.importDetail')"
|
||||
<Modal
|
||||
v-model="show"
|
||||
:title="t('components.library.ImportStatusModal.header.importDetail')"
|
||||
:cancel="t('components.library.ImportStatusModal.button.close')"
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -128,34 +128,44 @@ const labels = computed(() => ({
|
|||
|
||||
const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value].sort((a, b) => a - b)))
|
||||
|
||||
const { isOpen:subscribeIsOpen, to:subscribe } = useModal('subscribe')
|
||||
const { to:upload } = useModal('upload')
|
||||
const { isOpen: subscribeIsOpen, to: subscribe } = useModal('subscribe')
|
||||
const { to: upload } = useModal('upload')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout stack main>
|
||||
<Layout
|
||||
stack
|
||||
main
|
||||
>
|
||||
<Header
|
||||
:h1="t('components.library.Podcasts.header.browse')"
|
||||
/>
|
||||
<Layout form flex
|
||||
<Layout
|
||||
form
|
||||
flex
|
||||
:class="['ui', {'loading': isLoading}, 'form']"
|
||||
@submit.prevent="search"
|
||||
>
|
||||
<Input search
|
||||
<Input
|
||||
id="artist-search"
|
||||
v-model="query"
|
||||
search
|
||||
name="search"
|
||||
:label="t('components.library.Podcasts.label.search')"
|
||||
autofocus
|
||||
:placeholder="labels.searchPlaceholder"
|
||||
>
|
||||
</Input>
|
||||
/>
|
||||
<Pills
|
||||
v-model="tagList"
|
||||
:label="t('components.library.Podcasts.label.tags')"
|
||||
style="max-width: 150px;"
|
||||
/>
|
||||
<Layout stack noGap label for="artist-ordering">
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="artist-ordering"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.library.Podcasts.ordering.label') }}
|
||||
</span>
|
||||
|
@ -173,7 +183,12 @@ const { to:upload } = useModal('upload')
|
|||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout stack noGap label for="artist-ordering-direction">
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="artist-ordering-direction"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.library.Podcasts.ordering.direction.label') }}
|
||||
</span>
|
||||
|
@ -190,7 +205,12 @@ const { to:upload } = useModal('upload')
|
|||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout stack noGap label for="artist-results">
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="artist-results"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.library.Podcasts.pagination.results') }}
|
||||
</span>
|
||||
|
@ -209,11 +229,12 @@ const { to:upload } = useModal('upload')
|
|||
</select>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<Layout grid
|
||||
<Layout
|
||||
v-if="result && result.results.length > 0"
|
||||
grid
|
||||
style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;"
|
||||
>
|
||||
<Loader v-if="isLoading"/>
|
||||
<Loader v-if="isLoading" />
|
||||
<artist-card
|
||||
v-for="artist in result.results"
|
||||
:key="artist.id"
|
||||
|
@ -221,8 +242,8 @@ const { to:upload } = useModal('upload')
|
|||
/>
|
||||
</Layout>
|
||||
<Layout
|
||||
stack
|
||||
v-else-if="result && result.results.length === 0"
|
||||
stack
|
||||
>
|
||||
<Alert yellow>
|
||||
{{ t('components.library.Podcasts.empty.noResults') }}
|
||||
|
@ -238,7 +259,10 @@ const { to:upload } = useModal('upload')
|
|||
:to="subscribe"
|
||||
>
|
||||
<template #image>
|
||||
<i class="bi bi-plus" style="font-size: 100px; position: relative; top: 50px;" />
|
||||
<i
|
||||
class="bi bi-plus"
|
||||
style="font-size: 100px; position: relative; top: 50px;"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
<Card
|
||||
|
@ -251,7 +275,10 @@ const { to:upload } = useModal('upload')
|
|||
:to="upload"
|
||||
>
|
||||
<template #image>
|
||||
<i class="bi bi-upload" style="font-size: 100px; position: relative; top: 50px;" />
|
||||
<i
|
||||
class="bi bi-upload"
|
||||
style="font-size: 100px; position: relative; top: 50px;"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</Layout>
|
||||
|
@ -262,35 +289,33 @@ const { to:upload } = useModal('upload')
|
|||
:page="page"
|
||||
:pages="Math.ceil((result?.results.length || 0)/paginateBy)"
|
||||
/>
|
||||
<Modal
|
||||
v-model="subscribeIsOpen"
|
||||
:title="t('components.library.Podcasts.modal.subscription.header')"
|
||||
:cancel="t('components.library.Podcasts.button.cancel')"
|
||||
>
|
||||
<div
|
||||
ref="modalContent"
|
||||
class="scrolling content"
|
||||
<Modal
|
||||
v-model="subscribeIsOpen"
|
||||
:title="t('components.library.Podcasts.modal.subscription.header')"
|
||||
:cancel="t('components.library.Podcasts.button.cancel')"
|
||||
>
|
||||
<remote-search-form
|
||||
initial-type="both"
|
||||
:show-submit="false"
|
||||
:standalone="false"
|
||||
:redirect="true"
|
||||
@subscribed="subscribeIsOpen = false; fetchData()"
|
||||
/>
|
||||
</div>
|
||||
<template #actions>
|
||||
<Button
|
||||
primary
|
||||
form="remote-search"
|
||||
type="submit"
|
||||
<div
|
||||
ref="modalContent"
|
||||
class="scrolling content"
|
||||
>
|
||||
<i class="bookmark icon" />
|
||||
{{ t('components.library.Podcasts.button.subscribe') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<remote-search-form
|
||||
initial-type="both"
|
||||
:show-submit="false"
|
||||
:standalone="false"
|
||||
:redirect="true"
|
||||
@subscribed="subscribeIsOpen = false; fetchData()"
|
||||
/>
|
||||
</div>
|
||||
<template #actions>
|
||||
<Button
|
||||
primary
|
||||
form="remote-search"
|
||||
type="submit"
|
||||
>
|
||||
<i class="bookmark icon" />
|
||||
{{ t('components.library.Podcasts.button.subscribe') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</Layout>
|
||||
|
||||
</template>
|
||||
|
|
|
@ -114,35 +114,42 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout main stack gap-64>
|
||||
<Layout
|
||||
main
|
||||
stack
|
||||
gap-64
|
||||
>
|
||||
<Header :h1="t('components.library.Radios.header.browse')" />
|
||||
<Section alignLeft :h2="t('components.library.Radios.header.instance')">
|
||||
<radio-card
|
||||
v-if="isAuthenticated"
|
||||
:type="'actor-content'"
|
||||
:object-id="store.state.auth.fullUsername"
|
||||
/>
|
||||
<radio-card
|
||||
v-if="isAuthenticated && hasFavorites"
|
||||
:type="'favorites'"
|
||||
/>
|
||||
<radio-card
|
||||
v-if="scope === 'all'"
|
||||
:type="'random'"
|
||||
/>
|
||||
<radio-card
|
||||
v-if="scope === 'me'"
|
||||
:type="'random_library'"
|
||||
/>
|
||||
<radio-card :type="'recently-added'" />
|
||||
<radio-card
|
||||
v-if="store.state.auth.authenticated && scope === 'all'"
|
||||
:type="'less-listened'"
|
||||
/>
|
||||
<radio-card
|
||||
v-if="store.state.auth.authenticated && scope === 'me'"
|
||||
:type="'less-listened_library'"
|
||||
/>
|
||||
<Section
|
||||
align-left
|
||||
:h2="t('components.library.Radios.header.instance')"
|
||||
>
|
||||
<radio-card
|
||||
v-if="isAuthenticated"
|
||||
:type="'actor-content'"
|
||||
:object-id="store.state.auth.fullUsername"
|
||||
/>
|
||||
<radio-card
|
||||
v-if="isAuthenticated && hasFavorites"
|
||||
:type="'favorites'"
|
||||
/>
|
||||
<radio-card
|
||||
v-if="scope === 'all'"
|
||||
:type="'random'"
|
||||
/>
|
||||
<radio-card
|
||||
v-if="scope === 'me'"
|
||||
:type="'random_library'"
|
||||
/>
|
||||
<radio-card :type="'recently-added'" />
|
||||
<radio-card
|
||||
v-if="store.state.auth.authenticated && scope === 'all'"
|
||||
:type="'less-listened'"
|
||||
/>
|
||||
<radio-card
|
||||
v-if="store.state.auth.authenticated && scope === 'me'"
|
||||
:type="'less-listened_library'"
|
||||
/>
|
||||
</Section>
|
||||
<h2>
|
||||
{{ t('components.library.Radios.header.user') }}
|
||||
|
@ -157,18 +164,26 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
|||
{{ t('components.library.Radios.button.create') }}
|
||||
</Link>
|
||||
</h2>
|
||||
<Layout flex form
|
||||
<Layout
|
||||
flex
|
||||
form
|
||||
:class="['ui', {'loading': isLoading}, 'form']"
|
||||
@submit.prevent="search"
|
||||
>
|
||||
<Input search
|
||||
<Input
|
||||
id="radios-search"
|
||||
v-model="query"
|
||||
search
|
||||
name="search"
|
||||
:label="t('components.library.Radios.label.search')"
|
||||
:placeholder="labels.searchPlaceholder"
|
||||
/>
|
||||
<Layout stack noGap label for="radios-ordering">
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="radios-ordering"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.library.Radios.ordering.label') }}
|
||||
</span>
|
||||
|
@ -186,7 +201,12 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
|||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout stack noGap label for="radios-ordering-direction">
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="radios-ordering-direction"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.library.Radios.ordering.direction.label') }}
|
||||
</span>
|
||||
|
@ -203,7 +223,12 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
|||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout stack noGap label for="radios-results">
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
label
|
||||
for="radios-results"
|
||||
>
|
||||
<span class="label">
|
||||
{{ t('components.library.Radios.pagination.results') }}
|
||||
</span>
|
||||
|
@ -223,11 +248,14 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
|||
</Layout>
|
||||
</Layout>
|
||||
<Alert
|
||||
v-if="result && result.results.length === 0"
|
||||
blue
|
||||
style="align-items: center;"
|
||||
v-if="result && result.results.length === 0"
|
||||
>
|
||||
<i class="bi bi-broadcast-pin" style="font-size: 80px" />
|
||||
<i
|
||||
class="bi bi-broadcast-pin"
|
||||
style="font-size: 80px"
|
||||
/>
|
||||
<Spacer />
|
||||
{{ t('components.library.Radios.empty.noResults') }}
|
||||
<Spacer />
|
||||
|
@ -241,8 +269,9 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
|||
{{ t('components.library.Radios.button.add') }}
|
||||
</Button>
|
||||
</Alert>
|
||||
<Layout flex
|
||||
<Layout
|
||||
v-if="result && result.results.length > 0"
|
||||
flex
|
||||
>
|
||||
<Pagination
|
||||
v-if="result && result.count > paginateBy"
|
||||
|
|
|
@ -27,15 +27,22 @@ const labels = computed(() => ({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout main stack v-title="labels.title">
|
||||
<Layout
|
||||
v-title="labels.title"
|
||||
main
|
||||
stack
|
||||
>
|
||||
<h1 class="ui header">
|
||||
<span class="funkwhale solid raised secondary pill">
|
||||
<span class="pill-content">
|
||||
{{ labels.title }}
|
||||
<span class="pill-content">
|
||||
{{ labels.title }}
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
<Layout flex class="buttons">
|
||||
<Layout
|
||||
flex
|
||||
class="buttons"
|
||||
>
|
||||
<radio-button
|
||||
type="tag"
|
||||
:object-id="id"
|
||||
|
|
|
@ -76,7 +76,7 @@ const attributedToUrl = computed(() => router.resolve({
|
|||
}
|
||||
})?.href)
|
||||
|
||||
const totalDuration = computed(() => track.value?.uploads[0]?.duration ?? 0)
|
||||
const totalDuration = computed(() => track.value?.uploads[0]?.duration ?? 0)
|
||||
|
||||
const { t } = useI18n()
|
||||
const labels = computed(() => ({
|
||||
|
@ -126,250 +126,271 @@ watch(showDeleteModal, (newValue) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout stack main>
|
||||
<Loader
|
||||
v-if="isLoading"
|
||||
v-title="labels.title"
|
||||
/>
|
||||
<template v-if="track">
|
||||
<Layout flex>
|
||||
<img
|
||||
v-if="track.cover"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](track.cover.urls.large_square_crop)"
|
||||
alt=""
|
||||
class="channel-image"
|
||||
>
|
||||
<img
|
||||
v-if="track.album && track.album.cover"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](track.album.cover.urls.large_square_crop)"
|
||||
alt=""
|
||||
class="channel-image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="channel-image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
>
|
||||
<Layout
|
||||
stack
|
||||
main
|
||||
>
|
||||
<Loader
|
||||
v-if="isLoading"
|
||||
v-title="labels.title"
|
||||
/>
|
||||
<template v-if="track">
|
||||
<Layout flex>
|
||||
<img
|
||||
v-if="track.cover"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](track.cover.urls.large_square_crop)"
|
||||
alt=""
|
||||
class="channel-image"
|
||||
>
|
||||
<img
|
||||
v-if="track.album && track.album.cover"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](track.album.cover.urls.large_square_crop)"
|
||||
alt=""
|
||||
class="channel-image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="channel-image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
>
|
||||
|
||||
<Layout stack style="flex: 1; gap: 8px;">
|
||||
<Layout flex no-gap style="align-items: baseline; margin-bottom: 24px;">
|
||||
<h1>{{ track.title }}</h1>
|
||||
<Spacer grow />
|
||||
<Button
|
||||
v-if="upload"
|
||||
:aria-label="labels.download"
|
||||
:to="downloadUrl"
|
||||
target="_blank"
|
||||
primary
|
||||
icon="bi-download"
|
||||
:title="labels.download"
|
||||
<Layout
|
||||
stack
|
||||
style="flex: 1; gap: 8px;"
|
||||
>
|
||||
<Layout
|
||||
flex
|
||||
no-gap
|
||||
style="align-items: baseline; margin-bottom: 24px;"
|
||||
>
|
||||
{{ labels.download }}
|
||||
</Button>
|
||||
</Layout>
|
||||
<div class="meta">
|
||||
<h1>{{ track.title }}</h1>
|
||||
<Spacer grow />
|
||||
<Button
|
||||
v-if="upload"
|
||||
:aria-label="labels.download"
|
||||
:to="downloadUrl"
|
||||
target="_blank"
|
||||
primary
|
||||
icon="bi-download"
|
||||
:title="labels.download"
|
||||
>
|
||||
{{ labels.download }}
|
||||
</Button>
|
||||
</Layout>
|
||||
<div class="meta">
|
||||
<span>{{ t('components.library.TrackBase.title') }}</span>
|
||||
<i class="bi bi-dot" />
|
||||
<span>{{ track.album.title }}</span>
|
||||
<i v-if="totalDuration > 0" class="bi bi-dot" />
|
||||
<i
|
||||
v-if="totalDuration > 0"
|
||||
class="bi bi-dot"
|
||||
/>
|
||||
<human-duration
|
||||
v-if="totalDuration > 0"
|
||||
:duration="totalDuration"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Layout flex>
|
||||
<PlayButton
|
||||
:is-playable="track.is_playable"
|
||||
class="vibrant"
|
||||
split
|
||||
:track="track"
|
||||
/>
|
||||
<Layout flex>
|
||||
<PlayButton
|
||||
:is-playable="track.is_playable"
|
||||
class="vibrant"
|
||||
split
|
||||
:track="track"
|
||||
/>
|
||||
|
||||
<Spacer h grow />
|
||||
<Spacer
|
||||
h
|
||||
grow
|
||||
/>
|
||||
|
||||
<TrackFavoriteIcon v-if="store.state.auth.authenticated" :track="track" />
|
||||
<TrackPlaylistIcon v-if="store.state.auth.authenticated" :track="track" />
|
||||
<Popover v-model:open="open">
|
||||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton @click="toggleOpen" />
|
||||
</template>
|
||||
<template #items>
|
||||
<PopoverItem
|
||||
v-if="domain != store.getters['instance/domain']"
|
||||
:to="track.fid"
|
||||
target="_blank"
|
||||
icon="bi-box-arrow-up-right"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.domain', { domain }) }}
|
||||
</PopoverItem>
|
||||
<TrackFavoriteIcon
|
||||
v-if="store.state.auth.authenticated"
|
||||
:track="track"
|
||||
/>
|
||||
<TrackPlaylistIcon
|
||||
v-if="store.state.auth.authenticated"
|
||||
:track="track"
|
||||
/>
|
||||
<Popover v-model:open="open">
|
||||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton @click="toggleOpen" />
|
||||
</template>
|
||||
<template #items>
|
||||
<PopoverItem
|
||||
v-if="domain != store.getters['instance/domain']"
|
||||
:to="track.fid"
|
||||
target="_blank"
|
||||
icon="bi-box-arrow-up-right"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.domain', { domain }) }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="isEmbedable"
|
||||
@click="showEmbedModal = !showEmbedModal"
|
||||
icon="bi-code-slash"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.embed') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-if="isEmbedable"
|
||||
icon="bi-code-slash"
|
||||
@click="showEmbedModal = !showEmbedModal"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.embed') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
:to="wikipediaUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
icon="bi-wikipedia"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.wikipedia') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
:to="wikipediaUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
icon="bi-wikipedia"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.wikipedia') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="discogsUrl"
|
||||
:to="discogsUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
icon="bi-box-arrow-up-right"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.discogs') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-if="discogsUrl"
|
||||
:to="discogsUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
icon="bi-box-arrow-up-right"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.discogs') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="track.is_local"
|
||||
icon="bi-pencil-fill"
|
||||
:to="{ name: 'library.tracks.edit', params: { id: track.id } }"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.edit') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-if="track.is_local"
|
||||
icon="bi-pencil-fill"
|
||||
:to="{ name: 'library.tracks.edit', params: { id: track.id } }"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.edit') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="artist &&
|
||||
store.state.auth.authenticated &&
|
||||
artist.channel &&
|
||||
artist.attributed_to.full_username === store.state.auth.fullUsername"
|
||||
@click="showDeleteModal = true"
|
||||
icon="bi-trash"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.delete') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-if="artist &&
|
||||
store.state.auth.authenticated &&
|
||||
artist.channel &&
|
||||
artist.attributed_to.full_username === store.state.auth.fullUsername"
|
||||
icon="bi-trash"
|
||||
@click="showDeleteModal = true"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.delete') }}
|
||||
</PopoverItem>
|
||||
|
||||
<hr>
|
||||
<hr>
|
||||
|
||||
<PopoverItem
|
||||
v-for="obj in getReportableObjects({ track })"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
@click="report(obj)"
|
||||
icon="bi-flag"
|
||||
>
|
||||
{{ obj.label }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-for="obj in getReportableObjects({ track })"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
icon="bi-flag"
|
||||
@click="report(obj)"
|
||||
>
|
||||
{{ obj.label }}
|
||||
</PopoverItem>
|
||||
|
||||
<hr>
|
||||
<hr>
|
||||
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.availablePermissions['library']"
|
||||
:to="{
|
||||
name: 'manage.library.tracks.detail',
|
||||
params: { id: track.id }
|
||||
}"
|
||||
icon="bi-wrench"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.moderation') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.availablePermissions['library']"
|
||||
:to="{
|
||||
name: 'manage.library.tracks.detail',
|
||||
params: { id: track.id }
|
||||
}"
|
||||
icon="bi-wrench"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.moderation') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.profile?.is_superuser"
|
||||
:to="store.getters['instance/absoluteUrl'](`/api/admin/music/track/${track.id}`)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
icon="bi-wrench"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.django') }}
|
||||
</PopoverItem>
|
||||
</template>
|
||||
</Popover>
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.profile?.is_superuser"
|
||||
:to="store.getters['instance/absoluteUrl'](`/api/admin/music/track/${track.id}`)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
icon="bi-wrench"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.django') }}
|
||||
</PopoverItem>
|
||||
</template>
|
||||
</Popover>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
<hr>
|
||||
<Layout flex>
|
||||
<div>
|
||||
<span v-if="track.attributed_to">
|
||||
{{ t('components.library.TrackBase.subtitle.with-uploader') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('components.library.TrackBase.subtitle.without-uploader') }}
|
||||
</span>
|
||||
<ActorLink
|
||||
v-if="track.attributed_to"
|
||||
:actor="track.attributed_to"
|
||||
:avatar="false"
|
||||
<hr>
|
||||
<Layout flex>
|
||||
<div>
|
||||
<span v-if="track.attributed_to">
|
||||
{{ t('components.library.TrackBase.subtitle.with-uploader') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('components.library.TrackBase.subtitle.without-uploader') }}
|
||||
</span>
|
||||
<ActorLink
|
||||
v-if="track.attributed_to"
|
||||
:actor="track.attributed_to"
|
||||
:avatar="false"
|
||||
/>
|
||||
|
||||
<time
|
||||
:title="track.creation_date"
|
||||
:datetime="track.creation_date"
|
||||
>
|
||||
{{ momentFormat(new Date(track.creation_date), 'LL') }}
|
||||
</time>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<Modal
|
||||
v-if="isEmbedable"
|
||||
v-model="showEmbedModal"
|
||||
:title="t('components.library.TrackBase.modal.embed.header')"
|
||||
>
|
||||
<embed-wizard
|
||||
:id="track.id"
|
||||
type="track"
|
||||
/>
|
||||
|
||||
<time
|
||||
:title="track.creation_date"
|
||||
:datetime="track.creation_date"
|
||||
>
|
||||
{{ momentFormat(new Date(track.creation_date), 'LL') }}
|
||||
</time>
|
||||
</div>
|
||||
</Layout>
|
||||
<template #actions>
|
||||
<Button
|
||||
secondary
|
||||
@click="showEmbedModal = false"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.cancel') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<Modal
|
||||
v-model="showDeleteModal"
|
||||
:title="t('components.library.TrackBase.modal.delete.header')"
|
||||
destructive
|
||||
>
|
||||
<template #alert>
|
||||
<Alert red>
|
||||
{{ t('components.library.TrackBase.modal.delete.content.warning') }}
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
<Modal
|
||||
v-if="isEmbedable"
|
||||
v-model="showEmbedModal"
|
||||
:title="t('components.library.TrackBase.modal.embed.header')"
|
||||
>
|
||||
<embed-wizard
|
||||
:id="track.id"
|
||||
type="track"
|
||||
<template #actions>
|
||||
<Button
|
||||
secondary
|
||||
@click="showDeleteModal = false"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.cancel') }}
|
||||
</Button>
|
||||
<Button
|
||||
destructive
|
||||
:is-loading="isLoading"
|
||||
@click="remove()"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<router-view
|
||||
v-if="track"
|
||||
:key="route.fullPath"
|
||||
:track="track"
|
||||
:object="track"
|
||||
object-type="track"
|
||||
@libraries-loaded="libraries = $event"
|
||||
/>
|
||||
|
||||
<template #actions>
|
||||
<Button
|
||||
secondary
|
||||
@click="showEmbedModal = false"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.cancel') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<Modal
|
||||
v-model="showDeleteModal"
|
||||
:title="t('components.library.TrackBase.modal.delete.header')"
|
||||
destructive
|
||||
>
|
||||
<template #alert>
|
||||
<Alert red>
|
||||
{{ t('components.library.TrackBase.modal.delete.content.warning') }}
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<Button
|
||||
secondary
|
||||
@click="showDeleteModal = false"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.cancel') }}
|
||||
</Button>
|
||||
<Button
|
||||
destructive
|
||||
:is-loading="isLoading"
|
||||
@click="remove()"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<router-view
|
||||
v-if="track"
|
||||
:key="route.fullPath"
|
||||
:track="track"
|
||||
:object="track"
|
||||
object-type="track"
|
||||
@libraries-loaded="libraries = $event"
|
||||
/>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ const release_details: {
|
|||
},
|
||||
{
|
||||
label: t('components.library.TrackDetail.table.release.license'),
|
||||
release_value: license?.name || t('components.library.TrackDetail.notApplicable'),
|
||||
release_value: license.value?.name || t('components.library.TrackDetail.notApplicable')
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -124,7 +124,7 @@ const track_details: {
|
|||
label:
|
||||
t('components.library.TrackDetail.table.track.bitrate.label'),
|
||||
track_value: upload?.value.bitrate
|
||||
? t('components.library.TrackDetail.table.track.bitrate.value', {bitrate: humanSize(upload.value.bitrate)})
|
||||
? t('components.library.TrackDetail.table.track.bitrate.value', { bitrate: humanSize(upload.value.bitrate) })
|
||||
: t('components.library.TrackDetail.notApplicable')
|
||||
},
|
||||
{
|
||||
|
@ -135,17 +135,26 @@ const track_details: {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout stack v-if="track">
|
||||
<Layout
|
||||
v-if="track"
|
||||
stack
|
||||
>
|
||||
<TagsList
|
||||
v-if="track.tags && track.tags.length > 0"
|
||||
style="margin-top: -16px;"
|
||||
:tags="track.tags"
|
||||
/>
|
||||
|
||||
<Layout flex style="gap: 24px;">
|
||||
<Layout stack style="flex: 1; gap: 0;">
|
||||
<Layout
|
||||
flex
|
||||
style="gap: 24px;"
|
||||
>
|
||||
<Layout
|
||||
stack
|
||||
style="flex: 1; gap: 0;"
|
||||
>
|
||||
<Section
|
||||
alignLeft
|
||||
align-left
|
||||
h2="Release Details"
|
||||
:action="{
|
||||
text:'View on MusicBrainz',
|
||||
|
@ -153,28 +162,63 @@ const track_details: {
|
|||
}"
|
||||
icon="bi-box-arrow-up-right"
|
||||
/>
|
||||
<Layout flex class="details" v-for="item in release_details" key="label"
|
||||
<Layout
|
||||
v-for="item in release_details"
|
||||
key="label"
|
||||
flex
|
||||
class="details"
|
||||
style="min-width: 120px;"
|
||||
>
|
||||
<span class="label">{{ item.label }}</span>
|
||||
<Spacer h grow />
|
||||
<Link v-if="item.link" class="value" :to="item.link">{{ item.release_value }}</Link>
|
||||
<span v-else class="value">{{ item.release_value }}</span>
|
||||
<Spacer
|
||||
h
|
||||
grow
|
||||
/>
|
||||
<Link
|
||||
v-if="item.link"
|
||||
class="value"
|
||||
:to="item.link"
|
||||
>
|
||||
{{ item.release_value }}
|
||||
</Link>
|
||||
<span
|
||||
v-else
|
||||
class="value"
|
||||
>{{ item.release_value }}</span>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
<Layout stack style="flex: 1; gap: 0;">
|
||||
<Layout
|
||||
stack
|
||||
style="flex: 1; gap: 0;"
|
||||
>
|
||||
<Section
|
||||
alignLeft
|
||||
align-left
|
||||
h2="Track Details"
|
||||
/>
|
||||
<Layout flex class="details" v-for="item in track_details" key="label"
|
||||
<Layout
|
||||
v-for="item in track_details"
|
||||
key="label"
|
||||
flex
|
||||
class="details"
|
||||
style="min-width: 120px;"
|
||||
>
|
||||
<span class="label">{{ item.label }}</span>
|
||||
<Spacer h grow />
|
||||
<Link v-if="item.link" class="value" :to="item.link">{{ item.track_value }}</Link>
|
||||
<span v-else class="value">{{ item.track_value }}</span>
|
||||
<Spacer
|
||||
h
|
||||
grow
|
||||
/>
|
||||
<Link
|
||||
v-if="item.link"
|
||||
class="value"
|
||||
:to="item.link"
|
||||
>
|
||||
{{ item.track_value }}
|
||||
</Link>
|
||||
<span
|
||||
v-else
|
||||
class="value"
|
||||
>{{ item.track_value }}</span>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
|
|
@ -5,7 +5,6 @@ import type { Library } from '~/types'
|
|||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
import store from '~/store'
|
||||
import axios from 'axios'
|
||||
|
||||
|
@ -50,8 +49,9 @@ fetchLicenses()
|
|||
|
||||
<template>
|
||||
<Header :h2="canEdit ? t('components.library.TrackEdit.header.edit') : t('components.library.TrackEdit.header.suggest')" />
|
||||
<Alert yellow
|
||||
<Alert
|
||||
v-if="!object.is_local"
|
||||
yellow
|
||||
>
|
||||
{{ t('components.library.TrackEdit.message.remote') }}
|
||||
</Alert>
|
||||
|
|
|
@ -195,9 +195,9 @@ const save = async () => {
|
|||
|
||||
<template>
|
||||
<Layout
|
||||
v-title="labels.title"
|
||||
stack
|
||||
main
|
||||
v-title="labels.title"
|
||||
>
|
||||
<section>
|
||||
<h1>
|
||||
|
|
|
@ -186,9 +186,10 @@ fetchCandidates()
|
|||
>
|
||||
{{ t('components.library.radios.Filter.matchingTracks', checkResult.candidates.count) }}
|
||||
</a>
|
||||
<Modal :title="t('components.library.radios.Filter.matchingTracksModalHeader')"
|
||||
<Modal
|
||||
v-if="checkResult"
|
||||
v-model:show="showCandidadesModal"
|
||||
:title="t('components.library.radios.Filter.matchingTracksModalHeader')"
|
||||
>
|
||||
<div class="content">
|
||||
<div class="description">
|
||||
|
|
|
@ -102,7 +102,7 @@ const labels = computed(() => ({
|
|||
<template>
|
||||
<div>
|
||||
<div class="ui inline form">
|
||||
/front/src/components/manage/library/AlbumsTable.vue
|
||||
/front/src/components/manage/library/AlbumsTable.vue
|
||||
<div class="fields">
|
||||
<div class="ui six wide field">
|
||||
<label for="albums-search">{{ t('components.manage.library.AlbumsTable.label.search') }}</label>
|
||||
|
|
|
@ -340,8 +340,7 @@ const getPrivacyLevelChoice = (privacyLevel: PrivacyLevel) => {
|
|||
icon="bi-question-circle"
|
||||
:title="sharedLabels.fields.import_status.label"
|
||||
@click="detailedUpload = scope.obj; showUploadDetailModal = true"
|
||||
>
|
||||
</Button>
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="scope.obj.size">{{ humanSize(scope.obj.size) }}</span>
|
||||
|
|
|
@ -241,8 +241,8 @@ const remove = async () => {
|
|||
<dangerous-button
|
||||
v-if="object"
|
||||
style="float: right;"
|
||||
@confirm="remove"
|
||||
:title="t('components.manage.moderation.InstancePolicyForm.modal.delete.header')"
|
||||
@confirm="remove"
|
||||
>
|
||||
{{ t('components.manage.moderation.InstancePolicyForm.button.delete') }}
|
||||
<template #modal-content>
|
||||
|
|
|
@ -30,7 +30,7 @@ const obj = computed(() => result.value?.results[0] ?? null)
|
|||
|
||||
const isLoading = ref(false)
|
||||
|
||||
watch (show, (newValue) => {
|
||||
watch(show, (newValue) => {
|
||||
if (newValue) fetchData()
|
||||
})
|
||||
|
||||
|
@ -72,8 +72,9 @@ const fetchData = async () => {
|
|||
<slot>
|
||||
{{ t('components.manage.moderation.InstancePolicyModal.button.show') }}
|
||||
</slot>
|
||||
<Modal :title="t('components.manage.moderation.InstancePolicyModal.modal.manage.header', {obj: target})"
|
||||
<Modal
|
||||
v-model="show"
|
||||
:title="t('components.manage.moderation.InstancePolicyModal.modal.manage.header', {obj: target})"
|
||||
:cancel="t('components.manage.moderation.InstancePolicyModal.button.close')"
|
||||
>
|
||||
<div class="content">
|
||||
|
|
|
@ -67,9 +67,9 @@ const remove = async (note: Note) => {
|
|||
<dangerous-button
|
||||
:is-loading="isLoading"
|
||||
low-height
|
||||
@confirm="remove(note)"
|
||||
icon="bi-trash"
|
||||
:title="t('components.manage.moderation.NotesThread.modal.delete.header')"
|
||||
@confirm="remove(note)"
|
||||
>
|
||||
{{ t('components.manage.moderation.NotesThread.button.delete') }}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import { useStore } from '~/store'
|
|||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
import NotesThread from '~/components/manage/moderation/NotesThread.vue'
|
||||
|
|
|
@ -60,14 +60,16 @@ const hide = async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model="show"
|
||||
<Modal
|
||||
v-model="show"
|
||||
:title="type==='artist' ? t('components.moderation.FilterModal.header.modal', {name: target?.name}) : errors.length > 0 ? t('components.moderation.FilterModal.header.failure') : ''"
|
||||
:cancel="t('components.moderation.FilterModal.button.cancel')"
|
||||
>
|
||||
<div class="scrolling content">
|
||||
<div class="description">
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="errors.length > 0"
|
||||
red
|
||||
>
|
||||
<ul class="list">
|
||||
<li
|
||||
|
|
|
@ -125,8 +125,9 @@ watchEffect(async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="target ? t('components.moderation.ReportModal.header.modal') : errors.length > 0 ? t('components.moderation.ReportModal.header.submissionFailure') : ''"
|
||||
<Modal
|
||||
v-model="show"
|
||||
:title="target ? t('components.moderation.ReportModal.header.modal') : errors.length > 0 ? t('components.moderation.ReportModal.header.submissionFailure') : ''"
|
||||
:cancel="t('components.moderation.ReportModal.button.cancel')"
|
||||
>
|
||||
<h2
|
||||
|
@ -141,8 +142,9 @@ watchEffect(async () => {
|
|||
</h2>
|
||||
<div class="scrolling content">
|
||||
<div class="description">
|
||||
<Alert red
|
||||
<Alert
|
||||
v-if="errors.length > 0"
|
||||
red
|
||||
>
|
||||
<ul class="list">
|
||||
<li
|
||||
|
@ -241,8 +243,9 @@ watchEffect(async () => {
|
|||
</div>
|
||||
</div>
|
||||
<template #actions>
|
||||
<Button destructive
|
||||
<Button
|
||||
v-if="canSubmit"
|
||||
destructive
|
||||
:is-loading="isLoading"
|
||||
type="submit"
|
||||
form="report-form"
|
||||
|
|
|
@ -11,7 +11,7 @@ import type { Playlist } from '~/types'
|
|||
const { t } = useI18n()
|
||||
|
||||
const play = defineEmit<[playlist: Playlist]>()
|
||||
const {playlist} = defineProps<{playlist: Playlist}>()
|
||||
const { playlist } = defineProps<{playlist: Playlist}>()
|
||||
|
||||
const covers = computed(() => playlist.album_covers
|
||||
.filter((src, index, array) => array.indexOf(src) === index)
|
||||
|
@ -36,15 +36,15 @@ if (import.meta.env.PROD) {
|
|||
<template>
|
||||
<Card
|
||||
:title="playlist.name"
|
||||
@click="navigate('playlist')"
|
||||
class="playlist-card"
|
||||
@click="navigate('playlist')"
|
||||
>
|
||||
<template #image>
|
||||
<img
|
||||
v-for="src in covers"
|
||||
:key="src"
|
||||
:src="src"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
v-for="i in Math.max(0, 4 - covers.length)"
|
||||
:key="i"
|
||||
|
@ -54,8 +54,8 @@ if (import.meta.env.PROD) {
|
|||
<PlayButton @play="play(playlist)" />
|
||||
|
||||
<a
|
||||
@click.stop="navigate('user')"
|
||||
class="funkwhale link"
|
||||
@click.stop="navigate('user')"
|
||||
>
|
||||
{{ t('vui.by-user', playlist.actor.full_username) }}
|
||||
</a>
|
||||
|
|
|
@ -45,16 +45,16 @@ const bgcolors = ref([
|
|||
'#292525',
|
||||
'#403a3b',
|
||||
'#322f2f'
|
||||
]);
|
||||
])
|
||||
|
||||
function shuffleArray(array: string[]): string[] {
|
||||
return [...array].sort(() => Math.random() - 0.5);
|
||||
function shuffleArray (array: string[]): string[] {
|
||||
return [...array].sort(() => Math.random() - 0.5)
|
||||
}
|
||||
|
||||
const randomizedColors = computed(() => shuffleArray(bgcolors.value));
|
||||
const randomizedColors = computed(() => shuffleArray(bgcolors.value))
|
||||
|
||||
const goToPlaylist = () => {
|
||||
router.push({name: 'library.playlists.detail', params: {id: props.playlist.id}})
|
||||
router.push({ name: 'library.playlists.detail', params: { id: props.playlist.id } })
|
||||
}
|
||||
|
||||
const updatedTitle = computed(() => {
|
||||
|
@ -72,7 +72,7 @@ const updatedAgo = computed(() => moment(props.playlist.modification_date).fromN
|
|||
>
|
||||
<template #topright>
|
||||
<PlayButton
|
||||
iconOnly
|
||||
icon-only
|
||||
:is-playable="playlist.is_playable"
|
||||
:playlist="playlist"
|
||||
/>
|
||||
|
@ -86,7 +86,7 @@ const updatedAgo = computed(() => moment(props.playlist.modification_date).fromN
|
|||
v-lazy="url"
|
||||
:alt="playlist.name"
|
||||
:style="{ backgroundColor: randomizedColors[idx % randomizedColors.length] }"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -12,7 +12,10 @@ defineProps<Props>()
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout grid v-if="playlists.length > 0">
|
||||
<Layout
|
||||
v-if="playlists.length > 0"
|
||||
grid
|
||||
>
|
||||
<PlaylistsCard
|
||||
v-for="playlist in playlists"
|
||||
:key="playlist.id"
|
||||
|
|
|
@ -176,143 +176,143 @@ const insertMany = async (insertedTracks: number[], allowDuplicates: boolean) =>
|
|||
<h3 class="ui top attached header">
|
||||
{{ t('components.playlists.Editor.header.editor') }}
|
||||
</h3>
|
||||
<template v-if="status === 'loading'">
|
||||
<div class="ui active tiny inline loader" />
|
||||
{{ t('components.playlists.Editor.loading.sync') }}
|
||||
</template>
|
||||
<template v-else-if="status === 'errored'">
|
||||
<i class="dangerclose icon" />
|
||||
{{ t('components.playlists.Editor.error.sync') }}
|
||||
<Alert
|
||||
red
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
>
|
||||
<ul class="list">
|
||||
<li
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
</template>
|
||||
<template v-if="status === 'loading'">
|
||||
<div class="ui active tiny inline loader" />
|
||||
{{ t('components.playlists.Editor.loading.sync') }}
|
||||
</template>
|
||||
<template v-else-if="status === 'errored'">
|
||||
<i class="dangerclose icon" />
|
||||
{{ t('components.playlists.Editor.error.sync') }}
|
||||
<Alert
|
||||
v-if="errors.length > 0"
|
||||
red
|
||||
v-else-if="status === 'confirmDuplicateAdd'"
|
||||
role="alert"
|
||||
>
|
||||
<p>
|
||||
{{ t('components.playlists.Editor.warning.duplicate') }}
|
||||
</p>
|
||||
<ul class="ui relaxed divided list duplicate-tracks-list">
|
||||
<ul class="list">
|
||||
<li
|
||||
v-for="track in duplicateTrackAddInfo?.tracks ?? []"
|
||||
:key="track"
|
||||
class="ui item"
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
>
|
||||
{{ track }}
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
<Button
|
||||
destructive
|
||||
@click="insertMany(queueTracks, true)"
|
||||
>
|
||||
{{ t('components.playlists.Editor.button.addDuplicate') }}
|
||||
</Button>
|
||||
</Alert>
|
||||
<Alert
|
||||
v-else-if="status === 'saved'"
|
||||
green
|
||||
align-content="center"
|
||||
</template>
|
||||
<Alert
|
||||
v-else-if="status === 'confirmDuplicateAdd'"
|
||||
red
|
||||
role="alert"
|
||||
>
|
||||
<p>
|
||||
{{ t('components.playlists.Editor.warning.duplicate') }}
|
||||
</p>
|
||||
<ul class="ui relaxed divided list duplicate-tracks-list">
|
||||
<li
|
||||
v-for="track in duplicateTrackAddInfo?.tracks ?? []"
|
||||
:key="track"
|
||||
class="ui item"
|
||||
>
|
||||
{{ track }}
|
||||
</li>
|
||||
</ul>
|
||||
<Button
|
||||
destructive
|
||||
@click="insertMany(queueTracks, true)"
|
||||
>
|
||||
<span>
|
||||
<i class="bi bi-check" />
|
||||
{{ t('components.playlists.Editor.message.sync') }}
|
||||
</span>
|
||||
</Alert>
|
||||
<Layout flex>
|
||||
<Button
|
||||
:disabled="queueTracks.length === 0"
|
||||
primary
|
||||
:class="['ui', {disabled: queueTracks.length === 0}, 'labeled', 'icon', 'button']"
|
||||
:title="labels.copyTitle"
|
||||
icon="bi-plus"
|
||||
@click="insertMany(queueTracks, false)"
|
||||
>
|
||||
{{ t('components.playlists.Editor.button.insertFromQueue', queueTracks.length) }}
|
||||
</Button>
|
||||
{{ t('components.playlists.Editor.button.addDuplicate') }}
|
||||
</Button>
|
||||
</Alert>
|
||||
<Alert
|
||||
v-else-if="status === 'saved'"
|
||||
green
|
||||
align-content="center"
|
||||
>
|
||||
<span>
|
||||
<i class="bi bi-check" />
|
||||
{{ t('components.playlists.Editor.message.sync') }}
|
||||
</span>
|
||||
</Alert>
|
||||
<Layout flex>
|
||||
<Button
|
||||
:disabled="queueTracks.length === 0"
|
||||
primary
|
||||
:class="['ui', {disabled: queueTracks.length === 0}, 'labeled', 'icon', 'button']"
|
||||
:title="labels.copyTitle"
|
||||
icon="bi-plus"
|
||||
@click="insertMany(queueTracks, false)"
|
||||
>
|
||||
{{ t('components.playlists.Editor.button.insertFromQueue', queueTracks.length) }}
|
||||
</Button>
|
||||
|
||||
<dangerous-button
|
||||
:disabled="tracks.length === 0"
|
||||
icon="bi-eraser-fill"
|
||||
style="float: right;"
|
||||
:action="clearPlaylist"
|
||||
:title="t('components.playlists.Editor.modal.clearPlaylist.header', { playlist: playlist?.name })"
|
||||
>
|
||||
<dangerous-button
|
||||
:disabled="tracks.length === 0"
|
||||
icon="bi-eraser-fill"
|
||||
style="float: right;"
|
||||
:action="clearPlaylist"
|
||||
:title="t('components.playlists.Editor.modal.clearPlaylist.header', { playlist: playlist?.name })"
|
||||
>
|
||||
{{ t('components.playlists.Editor.button.clear') }}
|
||||
<template #modal-content>
|
||||
{{ t('components.playlists.Editor.modal.clearPlaylist.content.warning') }}
|
||||
</template>
|
||||
<template #modal-confirm>
|
||||
{{ t('components.playlists.Editor.button.clear') }}
|
||||
<template #modal-content>
|
||||
{{ t('components.playlists.Editor.modal.clearPlaylist.content.warning') }}
|
||||
</template>
|
||||
<template #modal-confirm>
|
||||
{{ t('components.playlists.Editor.button.clear') }}
|
||||
</template>
|
||||
</dangerous-button>
|
||||
</Layout>
|
||||
<template v-if="tracks.length > 0">
|
||||
<p>
|
||||
{{ t('components.playlists.Editor.help.reorder') }}
|
||||
</p>
|
||||
<div class="table-wrapper">
|
||||
</template>
|
||||
</dangerous-button>
|
||||
</Layout>
|
||||
<template v-if="tracks.length > 0">
|
||||
<p>
|
||||
{{ t('components.playlists.Editor.help.reorder') }}
|
||||
</p>
|
||||
<div class="table-wrapper">
|
||||
<!-- TODO: Use activity.vue -->
|
||||
<table class="ui compact very basic unstackable table">
|
||||
<draggable
|
||||
v-model="tracks"
|
||||
tag="tbody"
|
||||
item-key="_id"
|
||||
@update="reorder"
|
||||
>
|
||||
<template #item="{ element: plt, index }">
|
||||
<tr>
|
||||
<td class="left aligned">
|
||||
{{ plt.index + 1 }}
|
||||
</td>
|
||||
<td class="center aligned">
|
||||
<img
|
||||
v-if="plt.track.album && plt.track.album.cover && plt.track.album.cover.urls.original"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](plt.track.album.cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
style="width: 40px;"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
style="width: 40px;"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
>
|
||||
</td>
|
||||
<td colspan="4">
|
||||
<strong>{{ plt.track.title }}</strong><br>
|
||||
{{ generateTrackCreditString(plt.track) }}
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<Button
|
||||
square-small
|
||||
round
|
||||
destructive
|
||||
@click.stop="removePlaylistTrack(index)"
|
||||
>
|
||||
<i
|
||||
class="bi bi-trash"
|
||||
/>
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</draggable>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<table class="ui compact very basic unstackable table">
|
||||
<draggable
|
||||
v-model="tracks"
|
||||
tag="tbody"
|
||||
item-key="_id"
|
||||
@update="reorder"
|
||||
>
|
||||
<template #item="{ element: plt, index }">
|
||||
<tr>
|
||||
<td class="left aligned">
|
||||
{{ plt.index + 1 }}
|
||||
</td>
|
||||
<td class="center aligned">
|
||||
<img
|
||||
v-if="plt.track.album && plt.track.album.cover && plt.track.album.cover.urls.original"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](plt.track.album.cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
style="width: 40px;"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
style="width: 40px;"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
>
|
||||
</td>
|
||||
<td colspan="4">
|
||||
<strong>{{ plt.track.title }}</strong><br>
|
||||
{{ generateTrackCreditString(plt.track) }}
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<Button
|
||||
square-small
|
||||
round
|
||||
destructive
|
||||
@click.stop="removePlaylistTrack(index)"
|
||||
>
|
||||
<i
|
||||
class="bi bi-trash"
|
||||
/>
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</draggable>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
|
@ -55,7 +55,7 @@ const privacyLevelChoices = {
|
|||
me: sharedLabels.fields.privacy_level.choices.me,
|
||||
instance: sharedLabels.fields.privacy_level.choices.instance,
|
||||
everyone: sharedLabels.fields.privacy_level.choices.everyone
|
||||
} as const satisfies Record<PrivacyLevel, string>;
|
||||
} as const satisfies Record<PrivacyLevel, string>
|
||||
|
||||
const el = useCurrentElement()
|
||||
|
||||
|
@ -95,7 +95,8 @@ const submit = async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout form
|
||||
<Layout
|
||||
form
|
||||
@submit.prevent="submit()"
|
||||
>
|
||||
<h3
|
||||
|
@ -145,7 +146,11 @@ const submit = async () => {
|
|||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<Slider :options="privacyLevelChoices" v-model="privacyLevel" :label="t('components.playlists.Form.label.visibility')" />
|
||||
<Slider
|
||||
v-model="privacyLevel"
|
||||
:options="privacyLevelChoices"
|
||||
:label="t('components.playlists.Form.label.visibility')"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span id="updatePlaylistLabel" />
|
||||
|
|
|
@ -102,42 +102,43 @@ const showDeleteModal = ref(false)
|
|||
</script>
|
||||
<template>
|
||||
<span>
|
||||
<Popover v-model:open="open">
|
||||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton @click="toggleOpen" />
|
||||
</template>
|
||||
<template #items>
|
||||
<PopoverItem
|
||||
v-if="playlist.privacy_level === 'everyone' && playlist.is_playable"
|
||||
icon="bi-code-slash"
|
||||
@click="showEmbedModal = !showEmbedModal"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.embed') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem destructive
|
||||
v-if="store.state.auth.profile && playlist.actor.full_username === store.state.auth.fullUsername"
|
||||
icon="bi-trash"
|
||||
:action="deletePlaylist"
|
||||
@click="showDeleteModal = !showDeleteModal"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.delete') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
icon="bi-download"
|
||||
@click="exportPlaylist"
|
||||
>
|
||||
{{ labels.export }}
|
||||
</PopoverItem>
|
||||
<Popover v-model:open="open">
|
||||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton @click="toggleOpen" />
|
||||
</template>
|
||||
<template #items>
|
||||
<PopoverItem
|
||||
v-if="playlist.privacy_level === 'everyone' && playlist.is_playable"
|
||||
icon="bi-code-slash"
|
||||
@click="showEmbedModal = !showEmbedModal"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.embed') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.profile && playlist.actor.full_username === store.state.auth.fullUsername"
|
||||
destructive
|
||||
icon="bi-trash"
|
||||
:action="deletePlaylist"
|
||||
@click="showDeleteModal = !showDeleteModal"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.delete') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
icon="bi-download"
|
||||
@click="exportPlaylist"
|
||||
>
|
||||
{{ labels.export }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.authenticated && playlist.actor.full_username === store.state.auth.fullUsername"
|
||||
icon="bi-upload"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
{{ labels.import }}
|
||||
</PopoverItem>
|
||||
</template>
|
||||
</Popover>
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.authenticated && playlist.actor.full_username === store.state.auth.fullUsername"
|
||||
icon="bi-upload"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
{{ labels.import }}
|
||||
</PopoverItem>
|
||||
</template>
|
||||
</Popover>
|
||||
|
||||
<!-- Hidden file input, triggered by the button click -->
|
||||
<input
|
||||
|
@ -182,7 +183,7 @@ const showDeleteModal = ref(false)
|
|||
</template>
|
||||
<template #actions>
|
||||
<Button @click="showDeleteModal = false">
|
||||
{{ t('views.playlists.Detail.button.cancel') }}
|
||||
{{ t('views.playlists.Detail.button.cancel') }}
|
||||
</Button>
|
||||
<Button
|
||||
destructive
|
||||
|
@ -192,5 +193,4 @@ const showDeleteModal = ref(false)
|
|||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
</template>
|
||||
|
|
|
@ -91,7 +91,7 @@ store.dispatch('playlists/fetchOwn')
|
|||
v-model="store.state.playlists.showModal"
|
||||
:title="t('components.playlists.PlaylistModal.header.addToPlaylist')"
|
||||
:cancel="t('components.playlists.PlaylistModal.button.cancel')"
|
||||
overPopover
|
||||
over-popover
|
||||
>
|
||||
<template v-if="track">
|
||||
<h3 class="ui header">
|
||||
|
@ -206,7 +206,7 @@ store.dispatch('playlists/fetchOwn')
|
|||
</tbody>
|
||||
</table>
|
||||
<template v-else>
|
||||
<Spacer />
|
||||
<Spacer />
|
||||
<Alert blue>
|
||||
<span>
|
||||
{{ t('components.playlists.PlaylistModal.header.noResults') }}
|
||||
|
|
|
@ -45,6 +45,5 @@ const labels = computed(() => ({
|
|||
:aria-label="labels.addToPlaylist"
|
||||
:title="labels.addToPlaylist"
|
||||
@click.stop="store.commit('playlists/chooseTrack', track)"
|
||||
>
|
||||
</Button>
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -5,7 +5,6 @@ import { ref, reactive, watch } from 'vue'
|
|||
import { useStore } from '~/store'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
|
@ -74,7 +73,10 @@ watch(
|
|||
small-items
|
||||
:h2="title"
|
||||
>
|
||||
<Loader v-if="isLoading" style="grid-column: 1 / -1;" />
|
||||
<Loader
|
||||
v-if="isLoading"
|
||||
style="grid-column: 1 / -1;"
|
||||
/>
|
||||
<Alert
|
||||
v-if="!isLoading && objects.length === 0"
|
||||
style="grid-column: 1 / -1;"
|
||||
|
|
|
@ -15,12 +15,12 @@ const timeAgo = useTimeAgo(new Date(podcast.artist?.modification_date ?? new Dat
|
|||
<Card
|
||||
:title="podcast.uuid"
|
||||
:image="podcast.artist?.cover?.urls.original"
|
||||
@click="navigate"
|
||||
class="podcast-card"
|
||||
@click="navigate"
|
||||
>
|
||||
<a
|
||||
@click.stop="navigate"
|
||||
class="funkwhale link"
|
||||
@click.stop="navigate"
|
||||
>
|
||||
{{ podcast.artist?.name }}
|
||||
</a>
|
||||
|
|
|
@ -23,8 +23,8 @@ const pastel = usePastel(() => props.color)
|
|||
<fw-card
|
||||
:title="radio.name"
|
||||
:class="pastel"
|
||||
@click="navigate"
|
||||
class="radio-card"
|
||||
@click="navigate"
|
||||
>
|
||||
<template #image>
|
||||
<div class="cover-name">
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue