chore(format): [WIP] run eslint and fix most stylistic/formatting errors
This commit is contained in:
parent
b82643d1a2
commit
8bb0adf700
|
@ -6,8 +6,7 @@ module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
'plugin:@intlify/vue-i18n/recommended',
|
'plugin:@intlify/vue-i18n/recommended',
|
||||||
'plugin:vue/vue3-recommended',
|
'plugin:vue/vue3-recommended',
|
||||||
'@vue/typescript/recommended',
|
'@vue/typescript/recommended'
|
||||||
'@vue/standard'
|
|
||||||
],
|
],
|
||||||
globals: {
|
globals: {
|
||||||
SharedArrayBuffer: 'readonly',
|
SharedArrayBuffer: 'readonly',
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
"universal-cookie": "4.0.4",
|
"universal-cookie": "4.0.4",
|
||||||
"vite-plugin-pwa": "0.14.4",
|
"vite-plugin-pwa": "0.14.4",
|
||||||
"vue": "3.5.13",
|
"vue": "3.5.13",
|
||||||
|
"vue-dompurify-html": "5.2.0",
|
||||||
"vue-gettext": "2.1.12",
|
"vue-gettext": "2.1.12",
|
||||||
"vue-i18n": "9.9.1",
|
"vue-i18n": "9.9.1",
|
||||||
"vue-router": "4.2.5",
|
"vue-router": "4.2.5",
|
||||||
|
|
|
@ -21,6 +21,7 @@ import ShortcutsModal from '~/ui/modals/Shortcuts.vue'
|
||||||
import LanguagesModal from '~/ui/modals/Language.vue'
|
import LanguagesModal from '~/ui/modals/Language.vue'
|
||||||
import SearchModal from '~/ui/modals/Search.vue'
|
import SearchModal from '~/ui/modals/Search.vue'
|
||||||
import UploadModal from '~/ui/modals/Upload.vue'
|
import UploadModal from '~/ui/modals/Upload.vue'
|
||||||
|
import Loader from '~/components/ui/Loader.vue'
|
||||||
|
|
||||||
// Fake content
|
// Fake content
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
@ -93,7 +94,7 @@ store.dispatch('auth/fetchUser')
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
FALLBACK
|
<Loader />
|
||||||
</template>
|
</template>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
|
|
|
@ -147,7 +147,7 @@ const federationEnabled = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-if="!(store.state.auth.authenticated || openRegistrations)"
|
||||||
class="signup-form content"
|
class="signup-form content"
|
||||||
>
|
>
|
||||||
<h3 class="header">
|
<h3 class="header">
|
||||||
|
@ -219,14 +219,14 @@ const federationEnabled = computed(() => {
|
||||||
<span class="statistics-figure ui text">
|
<span class="statistics-figure ui text">
|
||||||
<span class="ui big text"><strong>{{ stats.users?.toLocaleString(store.state.ui.momentLocale) }}</strong></span>
|
<span class="ui big text"><strong>{{ stats.users?.toLocaleString(store.state.ui.momentLocale) }}</strong></span>
|
||||||
<br>
|
<br>
|
||||||
{{ stats.users ? t('components.About.stat.activeUsers', stats.users) : "—" }}
|
{{ stats.users ? t('components.About.stat.activeUsers', stats.users) : "" }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<span class="statistics-figure ui text">
|
<span class="statistics-figure ui text">
|
||||||
<span class="ui big text"><strong>{{ stats.hours ? stats.hours.toLocaleString(store.state.ui.momentLocale) : "—" }}</strong></span>
|
<span class="ui big text"><strong>{{ stats.hours ? stats.hours.toLocaleString(store.state.ui.momentLocale) : "" }}</strong></span>
|
||||||
<br>
|
<br>
|
||||||
{{ stats.hours ? t('components.About.stat.hoursOfMusic', stats.hours) : "—" }}
|
{{ stats.hours ? t('components.About.stat.hoursOfMusic', stats.hours) : "" }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -246,8 +246,8 @@ if (!isWebGLSupported) {
|
||||||
@click="enter"
|
@click="enter"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
secondary
|
|
||||||
v-else
|
v-else
|
||||||
|
secondary
|
||||||
:aria-label="labels.exitFullscreen"
|
:aria-label="labels.exitFullscreen"
|
||||||
:title="labels.exitFullscreen"
|
:title="labels.exitFullscreen"
|
||||||
icon="bi-fullscreen-exit"
|
icon="bi-fullscreen-exit"
|
||||||
|
@ -309,7 +309,7 @@ if (!isWebGLSupported) {
|
||||||
<img
|
<img
|
||||||
v-if="ac.artist.cover"
|
v-if="ac.artist.cover"
|
||||||
v-lazy="ac.artist.cover?.urls.small_square_crop"
|
v-lazy="ac.artist.cover?.urls.small_square_crop"
|
||||||
/>
|
>
|
||||||
<i
|
<i
|
||||||
v-else
|
v-else
|
||||||
class="bi bi-person-circle"
|
class="bi bi-person-circle"
|
||||||
|
@ -348,8 +348,14 @@ if (!isWebGLSupported) {
|
||||||
<i class="loading spinner icon" />
|
<i class="loading spinner icon" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Spacer :size="16" class="desktop-and-below" />
|
<Spacer
|
||||||
<Layout flex class="additional-controls desktop-and-below">
|
:size="16"
|
||||||
|
class="desktop-and-below"
|
||||||
|
/>
|
||||||
|
<Layout
|
||||||
|
flex
|
||||||
|
class="additional-controls desktop-and-below"
|
||||||
|
>
|
||||||
<track-favorite-icon
|
<track-favorite-icon
|
||||||
v-if="store.state.auth.authenticated"
|
v-if="store.state.auth.authenticated"
|
||||||
:track="currentTrack"
|
:track="currentTrack"
|
||||||
|
@ -435,7 +441,10 @@ if (!isWebGLSupported) {
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
<span class="middle pipe symbol" />
|
<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">
|
<span :title="labels.duration">
|
||||||
{{ endsIn }}
|
{{ endsIn }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -25,8 +25,8 @@ const props = defineProps<Props>()
|
||||||
|
|
||||||
const { album } = props
|
const { album } = props
|
||||||
|
|
||||||
const artist_credit = album.artist_credit || []
|
const artistCredit = album.artist_credit || []
|
||||||
const firstArtist = artist_credit.length > 0 ? artist_credit[0].artist : null
|
const firstArtist = artistCredit.length > 0 ? artistCredit[0].artist : null
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const imageUrl = computed(() => props.album.cover?.urls.original
|
const imageUrl = computed(() => props.album.cover?.urls.original
|
||||||
|
@ -53,7 +53,6 @@ const imageUrl = computed(() => props.album.cover?.urls.original
|
||||||
|
|
||||||
<template
|
<template
|
||||||
v-for="ac in album.artist_credit"
|
v-for="ac in album.artist_credit"
|
||||||
#default
|
|
||||||
:key="ac.artist.id"
|
:key="ac.artist.id"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
|
|
|
@ -25,7 +25,8 @@ interface Props {
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
showCount: false,
|
showCount: false,
|
||||||
search: false,
|
search: false,
|
||||||
limit: 12
|
limit: 12,
|
||||||
|
title: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
|
@ -47,7 +47,7 @@ const { artist } = props
|
||||||
v-lazy="artist.cover.urls.medium_square_crop"
|
v-lazy="artist.cover.urls.medium_square_crop"
|
||||||
:alt="artist.name"
|
:alt="artist.name"
|
||||||
class="channel-image"
|
class="channel-image"
|
||||||
/>
|
>
|
||||||
<i
|
<i
|
||||||
v-else
|
v-else
|
||||||
class="bi bi-person-circle"
|
class="bi bi-person-circle"
|
||||||
|
@ -70,7 +70,6 @@ const { artist } = props
|
||||||
discrete
|
discrete
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,8 @@ interface Props {
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
search: false,
|
search: false,
|
||||||
header: true,
|
header: true,
|
||||||
limit: 12
|
limit: 12,
|
||||||
|
title: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
|
@ -37,7 +37,7 @@ const getRoute = (ac: ArtistCredit) => {
|
||||||
v-if="ac.artist.cover && ac.artist.cover.urls.original"
|
v-if="ac.artist.cover && ac.artist.cover.urls.original"
|
||||||
v-lazy="store.getters['instance/absoluteUrl'](ac.artist.cover.urls.small_square_crop)"
|
v-lazy="store.getters['instance/absoluteUrl'](ac.artist.cover.urls.small_square_crop)"
|
||||||
:alt="ac.artist.name"
|
:alt="ac.artist.name"
|
||||||
/>
|
>
|
||||||
<i
|
<i
|
||||||
v-else
|
v-else
|
||||||
class="bi bi-person-circle"
|
class="bi bi-person-circle"
|
||||||
|
|
|
@ -65,7 +65,7 @@ const updatedAgo = computed(() => moment(props.object.artist?.modification_date)
|
||||||
v-lazy="imageUrl"
|
v-lazy="imageUrl"
|
||||||
:alt="object.artist?.name"
|
:alt="object.artist?.name"
|
||||||
class="channel-image"
|
class="channel-image"
|
||||||
/>
|
>
|
||||||
<i
|
<i
|
||||||
v-else
|
v-else
|
||||||
class="bi bi-person-circle"
|
class="bi bi-person-circle"
|
||||||
|
@ -74,7 +74,7 @@ const updatedAgo = computed(() => moment(props.object.artist?.modification_date)
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default>
|
<template #default>
|
||||||
<Spacer :size="8"/>
|
<Spacer :size="8" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
@ -93,7 +93,10 @@ const updatedAgo = computed(() => moment(props.object.artist?.modification_date)
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ t('components.audio.ChannelCard.meta.tracks', object.artist?.tracks_count ?? 0) }}
|
{{ t('components.audio.ChannelCard.meta.tracks', object.artist?.tracks_count ?? 0) }}
|
||||||
</span>
|
</span>
|
||||||
<Spacer h grow />
|
<Spacer
|
||||||
|
h
|
||||||
|
grow
|
||||||
|
/>
|
||||||
<PlayButton
|
<PlayButton
|
||||||
:dropdown-only="true"
|
:dropdown-only="true"
|
||||||
:is-playable="true"
|
:is-playable="true"
|
||||||
|
|
|
@ -20,13 +20,14 @@ interface Events {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
filters: object
|
filters: object
|
||||||
limit?: number,
|
limit?: number
|
||||||
title?: string,
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<Events>()
|
const emit = defineEmits<Events>()
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
limit: 5
|
limit: 5,
|
||||||
|
title: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = ref<PaginatedChannelList>()
|
const result = ref<PaginatedChannelList>()
|
||||||
|
|
|
@ -72,7 +72,6 @@ const labels = computed(() => ({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
/front/src/components/audio/Search.vue
|
|
||||||
<h2>
|
<h2>
|
||||||
{{ t('components.audio.Search.header.search') }}
|
{{ t('components.audio.Search.header.search') }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
|
@ -15,7 +15,6 @@ import PlayButton from '~/components/audio/PlayButton.vue'
|
||||||
|
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
|
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
interface Props extends PlayOptionsProps {
|
interface Props extends PlayOptionsProps {
|
||||||
|
@ -67,10 +66,10 @@ const hover = ref(false)
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="[{ active }, 'track-row row', $style.row]"
|
:class="[{ active }, 'track-row row', $style.row]"
|
||||||
|
style="display: contents;"
|
||||||
@dblclick="activateTrack(track, index)"
|
@dblclick="activateTrack(track, index)"
|
||||||
@mousemove="hover = true"
|
@mousemove="hover = true"
|
||||||
@mouseout="hover = false"
|
@mouseout="hover = false"
|
||||||
style="display: contents;"
|
|
||||||
>
|
>
|
||||||
<!-- 1. column: Play button or track position -->
|
<!-- 1. column: Play button or track position -->
|
||||||
|
|
||||||
|
@ -143,59 +142,57 @@ const hover = ref(false)
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="content ellipsis column left floated column"
|
class="content ellipsis column left floated column"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
@click="activateTrack(track, index)"
|
||||||
>
|
>
|
||||||
<a
|
{{ track.title }}
|
||||||
@click="activateTrack(track, index)"
|
</a>
|
||||||
>
|
</div>
|
||||||
{{ track.title }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="content ellipsis left floated column"
|
class="content ellipsis left floated column"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-if="showAlbum"
|
||||||
|
:to="{ name: 'library.albums.detail', params: { id: track.album?.id } }"
|
||||||
|
>
|
||||||
|
{{ track.album?.title }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="content ellipsis left floated column"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="ac in (showArtist ? track.artist_credit : [])"
|
||||||
|
:key="ac.artist.id"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="showAlbum"
|
class="artist link"
|
||||||
:to="{ name: 'library.albums.detail', params: { id: track.album?.id } }"
|
:to="{
|
||||||
|
name: 'library.artists.detail',
|
||||||
|
params: { id: ac.artist?.id },
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
{{ track.album?.title }}
|
{{ ac.credit }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
<span>{{ ac.joinphrase }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="content ellipsis left floated column"
|
class="meta right floated column"
|
||||||
>
|
>
|
||||||
<template
|
<track-favorite-icon
|
||||||
v-if="showArtist"
|
v-if="store.state.auth.authenticated"
|
||||||
v-for="ac in track.artist_credit"
|
ghost
|
||||||
:key="ac.artist.id"
|
:track="track"
|
||||||
>
|
/>
|
||||||
<router-link
|
</div>
|
||||||
class="artist link"
|
|
||||||
:to="{
|
|
||||||
name: 'library.artists.detail',
|
|
||||||
params: { id: ac.artist?.id },
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ ac.credit }}
|
|
||||||
</router-link>
|
|
||||||
<span>{{ ac.joinphrase }}</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="meta right floated column"
|
|
||||||
>
|
|
||||||
<track-favorite-icon
|
|
||||||
v-if="store.state.auth.authenticated"
|
|
||||||
ghost
|
|
||||||
:track="track"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="meta right floated column"
|
class="meta right floated column"
|
||||||
|
|
|
@ -41,7 +41,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
isActivity: true,
|
isActivity: true,
|
||||||
limit: 9,
|
limit: 9,
|
||||||
itemClasses: '',
|
itemClasses: '',
|
||||||
websocketHandlers: () => []
|
websocketHandlers: () => [],
|
||||||
|
title: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
@ -92,7 +93,7 @@ watch(count, (to) => emit('count', to))
|
||||||
|
|
||||||
watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
||||||
if (to) {
|
if (to) {
|
||||||
useWebSocketHandler('Listen', (event) => {
|
useWebSocketHandler('Listen', (event: unknown) => {
|
||||||
// Handle WebSocket events for "Listen"
|
// Handle WebSocket events for "Listen"
|
||||||
|
|
||||||
// Add the event to `objects` reactively
|
// Add the event to `objects` reactively
|
||||||
|
@ -102,10 +103,6 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
||||||
if (objects.length > props.limit) {
|
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)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
@ -134,8 +131,7 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
||||||
</Alert>
|
</Alert>
|
||||||
<!-- TODO: Use activity.vue -->
|
<!-- TODO: Use activity.vue -->
|
||||||
<div
|
<div
|
||||||
v-for="object in objects"
|
v-for="object in (count > 0 ? objects : [])"
|
||||||
v-if="count > 0"
|
|
||||||
:key="object.id"
|
:key="object.id"
|
||||||
class="funkwhale activity"
|
class="funkwhale activity"
|
||||||
:class="['item', itemClasses]"
|
:class="['item', itemClasses]"
|
||||||
|
|
|
@ -185,7 +185,7 @@ fetchInstanceSettings()
|
||||||
field-id="password-field"
|
field-id="password-field"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
v-if="!store.state.instance.settings.users.registration_enabled.value"
|
v-if="!store.state.instance.settings.users.registration_enabled.value && payload.invitation"
|
||||||
id="invitation-code"
|
id="invitation-code"
|
||||||
v-model="payload.invitation"
|
v-model="payload.invitation"
|
||||||
:label="t('components.auth.SignupForm.label.invitation')"
|
:label="t('components.auth.SignupForm.label.invitation')"
|
||||||
|
@ -195,23 +195,26 @@ fetchInstanceSettings()
|
||||||
:placeholder="labels.placeholder"
|
:placeholder="labels.placeholder"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-for="(field, idx) in formCustomization?.fields"
|
v-for="(field, idx) in
|
||||||
v-if="signupRequiresApproval && (formCustomization?.fields.length ?? 0) > 0"
|
( signupRequiresApproval && formCustomization && (formCustomization.fields.length ?? 0) > 0
|
||||||
|
? formCustomization.fields
|
||||||
|
: []
|
||||||
|
)"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
:class="[{required: field.required}, 'field']"
|
:class="[{required: field.required}, 'field']"
|
||||||
>
|
>
|
||||||
<Textarea
|
<Textarea
|
||||||
v-if="field.input_type === 'long_text'"
|
v-if="field.input_type === 'long_text'"
|
||||||
:id="`custom-field-${idx}`"
|
:id="`custom-field-${idx}`"
|
||||||
v-model="payload.request_fields[field.label]"
|
v-model="payload.request_fields[field.label] as string"
|
||||||
:label="field.label"
|
:label="field.label"
|
||||||
:required="field.required"
|
:required="field.required || undefined"
|
||||||
rows="5"
|
rows="5"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
v-else
|
v-else
|
||||||
:id="`custom-field-${idx}`"
|
:id="`custom-field-${idx}`"
|
||||||
v-model="payload.request_fields[field.label]"
|
v-model="payload.request_fields[field.label] as string"
|
||||||
:label="field.label"
|
:label="field.label"
|
||||||
type="text"
|
type="text"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
|
|
|
@ -15,7 +15,7 @@ import Input from '~/components/ui/Input.vue'
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const channel = defineModel<Channel>({ required: true })
|
const channel = defineModel<Channel>({ required: true })
|
||||||
defineEmits(['created'])
|
const emit = defineEmits(['created'])
|
||||||
const newAlbumTitle = ref<string>('')
|
const newAlbumTitle = ref<string>('')
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
@ -58,7 +58,7 @@ const submit = async () => {
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
v-model="show"
|
v-model="show"
|
||||||
:title="t(channel?.artist?.content_category === 'podcast' ? 'components.channels.AlbumModal.header.newSeries' : 'components.channels.AlbumModal.header.newAlbum')"
|
:title="channel?.artist?.content_category === 'podcast' ? t('components.channels.AlbumModal.header.newSeries') : t('components.channels.AlbumModal.header.newAlbum')"
|
||||||
class="small"
|
class="small"
|
||||||
:cancel="t('components.channels.AlbumModal.button.cancel')"
|
:cancel="t('components.channels.AlbumModal.button.cancel')"
|
||||||
>
|
>
|
||||||
|
|
|
@ -27,7 +27,6 @@ const fetchAlbums = async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('I found another album with artist', model.value.channel.artist.name, ':', response.data.results)
|
|
||||||
albums.value = response.data.results
|
albums.value = response.data.results
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,10 +129,8 @@ const createEmptyChannel = async () => {
|
||||||
'channels/',
|
'channels/',
|
||||||
(emptyChannelCreateRequest satisfies operations['create_channel_2']['requestBody']['content']['application/json'])
|
(emptyChannelCreateRequest satisfies operations['create_channel_2']['requestBody']['content']['application/json'])
|
||||||
)
|
)
|
||||||
console.log('Created Channel: ', response.data)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.value = (error as BackendError).backendErrors
|
errors.value = (error as BackendError).backendErrors
|
||||||
console.log('Error:', error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,21 +147,16 @@ const fetchChannels = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Albums
|
// Albums
|
||||||
const albumSelection = ref<{channel: Channel, albumId: Album['id'] | '', albums: Album[]}>
|
const albumSelection = ref<{channel: Channel, albumId: Album['id'] | '', albums: Album[]}>()
|
||||||
|
|
||||||
watch(selectedChannel, (channel) =>
|
watch(selectedChannel, channel => {
|
||||||
albumSelection.value
|
if (!channel) return
|
||||||
= {
|
albumSelection.value = {
|
||||||
channel,
|
channel,
|
||||||
albumId: '',
|
albumId: '',
|
||||||
albums: []
|
albums: []
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
const channelChange = async (channelId) => {
|
|
||||||
selectedChannel.value = channelId
|
|
||||||
await fetchAlbums(channelId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quota and space
|
// Quota and space
|
||||||
//
|
//
|
||||||
|
@ -412,7 +405,6 @@ const labels = computed(() => ({
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const publish = async () => {
|
const publish = async () => {
|
||||||
console.log('starting publish...')
|
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
errors.value = []
|
errors.value = []
|
||||||
|
@ -435,24 +427,17 @@ const publish = async () => {
|
||||||
// headers: { 'Authorization': `Bearer ${store.state.auth.oauth}` }
|
// headers: { 'Authorization': `Bearer ${store.state.auth.oauth}` }
|
||||||
// })
|
// })
|
||||||
|
|
||||||
console.log('Channels Store Before: ', store.state.channels)
|
|
||||||
|
|
||||||
// Tell the store that the uploaded files are pending import
|
// Tell the store that the uploaded files are pending import
|
||||||
store.commit('channels/publish', {
|
store.commit('channels/publish', {
|
||||||
uploads: uploadedFiles.value.map((file) => ({ ...file.response, import_status: 'pending' })),
|
uploads: uploadedFiles.value.map((file) => ({ ...file.response, import_status: 'pending' })),
|
||||||
channel: selectedChannel.value
|
channel: selectedChannel.value
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Channels Store After: ', store.state.channels)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// TODO: Use inferred error type instead of typecasting
|
// TODO: Use inferred error type instead of typecasting
|
||||||
errors.value = (error as BackendError).backendErrors
|
errors.value = (error as BackendError).backendErrors
|
||||||
console.log('Error:', error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
|
|
||||||
console.log('...finished publish')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
@ -552,7 +537,7 @@ ChannelCreateRequest: {
|
||||||
v-if="availableChannels.length === 1"
|
v-if="availableChannels.length === 1"
|
||||||
for="channel-dropdown"
|
for="channel-dropdown"
|
||||||
>
|
>
|
||||||
{{ t('components.channels.UploadForm.label.channel') }}: {{ selectedChannel?.artist.name }}
|
{{ `${t('components.channels.UploadForm.label.channel')}: ${selectedChannel?.artist.name}` }}
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
v-else
|
v-else
|
||||||
|
@ -567,15 +552,16 @@ ChannelCreateRequest: {
|
||||||
class="dropdown"
|
class="dropdown"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="channel in availableChannels"
|
v-for="availableChannel in availableChannels"
|
||||||
:value="channel.artist.id"
|
:key="availableChannel.artist.id"
|
||||||
|
:value="availableChannel.artist.id"
|
||||||
>
|
>
|
||||||
{{ channel.artist.name }}
|
{{ availableChannel.artist.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<album-select
|
<album-select
|
||||||
v-if="selectedChannel"
|
v-if="selectedChannel && albumSelection"
|
||||||
v-model="albumSelection"
|
v-model="albumSelection"
|
||||||
:class="['ui', 'field']"
|
:class="['ui', 'field']"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -18,6 +18,8 @@ const update = (value: boolean) => store.commit('channels/showUploadModal', { sh
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { filter } = defineProps<{ filter: 'podcast' | 'music' }>()
|
||||||
|
|
||||||
const uploadForm = ref()
|
const uploadForm = ref()
|
||||||
|
|
||||||
const statusData = ref()
|
const statusData = ref()
|
||||||
|
@ -50,17 +52,27 @@ const step = ref(1)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
|
|
||||||
|
const title = computed(() =>
|
||||||
|
[t('components.channels.UploadModal.header'),
|
||||||
|
t('components.channels.UploadModal.header.publish'),
|
||||||
|
t('components.channels.UploadModal.header.uploadFiles'),
|
||||||
|
t('components.channels.UploadModal.header.uploadDetails'),
|
||||||
|
t('components.channels.UploadModal.header.processing')
|
||||||
|
][step.value]
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
v-model="store.state.channels.showUploadModal"
|
v-model="store.state.channels.showUploadModal"
|
||||||
:title="t(`components.channels.UploadModal.header.${['', 'publish', 'uploadFiles', 'uploadDetails', 'processing'][step]}`)"
|
:title="title"
|
||||||
class="small"
|
class="small"
|
||||||
>
|
>
|
||||||
<div class="scrolling content">
|
<div class="scrolling content">
|
||||||
<channel-upload-form
|
<channel-upload-form
|
||||||
ref="uploadForm"
|
ref="uploadForm"
|
||||||
|
:filter="filter"
|
||||||
:channel="store.state.channels.uploadModalConfig.channel ?? null"
|
:channel="store.state.channels.uploadModalConfig.channel ?? null"
|
||||||
@step="step = $event"
|
@step="step = $event"
|
||||||
@loading="isLoading = $event"
|
@loading="isLoading = $event"
|
||||||
|
|
|
@ -114,8 +114,12 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Layout main stack no-gap align-left
|
<Layout
|
||||||
v-title="labels.title"
|
v-title="labels.title"
|
||||||
|
main
|
||||||
|
stack
|
||||||
|
no-gap
|
||||||
|
align-left
|
||||||
>
|
>
|
||||||
<Header :h1="labels.title">
|
<Header :h1="labels.title">
|
||||||
<template #action>
|
<template #action>
|
||||||
|
@ -126,7 +130,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
||||||
</template>
|
</template>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<Loader v-if="isLoading"/>
|
<Loader v-if="isLoading" />
|
||||||
<Layout
|
<Layout
|
||||||
v-if="store.state.favorites.count > 0"
|
v-if="store.state.favorites.count > 0"
|
||||||
form
|
form
|
||||||
|
@ -134,8 +138,16 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
||||||
:class="['ui', { 'loading': isLoading }, 'form']"
|
:class="['ui', { 'loading': isLoading }, 'form']"
|
||||||
>
|
>
|
||||||
<Spacer :size="16" />
|
<Spacer :size="16" />
|
||||||
<Layout flex style="justify-content: flex-end;">
|
<Layout
|
||||||
<Layout stack noGap label for="favorites-ordering">
|
flex
|
||||||
|
style="justify-content: flex-end;"
|
||||||
|
>
|
||||||
|
<Layout
|
||||||
|
stack
|
||||||
|
no-gap
|
||||||
|
label
|
||||||
|
for="favorites-ordering"
|
||||||
|
>
|
||||||
<span class="label">
|
<span class="label">
|
||||||
{{ t('components.favorites.List.ordering.label') }}
|
{{ t('components.favorites.List.ordering.label') }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -153,7 +165,12 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout stack noGap label for="favorites-ordering-direction">
|
<Layout
|
||||||
|
stack
|
||||||
|
no-gap
|
||||||
|
label
|
||||||
|
for="favorites-ordering-direction"
|
||||||
|
>
|
||||||
<span class="label">
|
<span class="label">
|
||||||
{{ t('components.favorites.List.ordering.direction.label') }}
|
{{ t('components.favorites.List.ordering.direction.label') }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -170,7 +187,12 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout stack noGap label for="favorites-results">
|
<Layout
|
||||||
|
stack
|
||||||
|
no-gap
|
||||||
|
label
|
||||||
|
for="favorites-results"
|
||||||
|
>
|
||||||
<span class="label">
|
<span class="label">
|
||||||
{{ t('components.favorites.List.pagination.results') }}
|
{{ t('components.favorites.List.pagination.results') }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -203,13 +225,18 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
||||||
:tracks="results"
|
:tracks="results"
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Alert blue align-items="center"
|
<Alert
|
||||||
v-else-if="!isLoading"
|
v-else-if="!isLoading"
|
||||||
|
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 />
|
<Spacer />
|
||||||
{{ t('components.favorites.List.empty.noFavorites') }}
|
{{ t('components.favorites.List.empty.noFavorites') }}
|
||||||
<Spacer :size="32"/>
|
<Spacer :size="32" />
|
||||||
<Link
|
<Link
|
||||||
to="/library"
|
to="/library"
|
||||||
solid
|
solid
|
||||||
|
|
|
@ -69,7 +69,7 @@ const labels = computed(() => ({
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isShuffled,
|
isShuffled,
|
||||||
shuffle,
|
shuffle
|
||||||
} = useQueue()
|
} = useQueue()
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
@ -150,7 +150,10 @@ const remove = async () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Layout stack main>
|
<Layout
|
||||||
|
stack
|
||||||
|
main
|
||||||
|
>
|
||||||
<Loader
|
<Loader
|
||||||
v-if="isLoading"
|
v-if="isLoading"
|
||||||
v-title="labels.title"
|
v-title="labels.title"
|
||||||
|
@ -162,105 +165,122 @@ const remove = async () => {
|
||||||
v-lazy="store.getters['instance/absoluteUrl'](object.cover.urls.large_square_crop)"
|
v-lazy="store.getters['instance/absoluteUrl'](object.cover.urls.large_square_crop)"
|
||||||
:alt="object.title"
|
:alt="object.title"
|
||||||
class="channel-image"
|
class="channel-image"
|
||||||
/>
|
>
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
alt=""
|
alt=""
|
||||||
class="channel-image"
|
class="channel-image"
|
||||||
src="../../assets/audio/default-cover.png"
|
src="../../assets/audio/default-cover.png"
|
||||||
/>
|
>
|
||||||
<Layout stack style="flex: 1; gap: 8px;">
|
<Layout
|
||||||
<h1 style="margin-top: 64px; margin-bottom: 8px;">{{ object.title }}</h1>
|
stack
|
||||||
<artist-credit-label
|
style="flex: 1; gap: 8px;"
|
||||||
v-if="artistCredit"
|
>
|
||||||
:artist-credit="artistCredit"
|
<h1 style="margin-top: 64px; margin-bottom: 8px;">
|
||||||
|
{{ object.title }}
|
||||||
|
</h1>
|
||||||
|
<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>
|
||||||
|
<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>
|
</Layout>
|
||||||
|
|
||||||
<div style="flex 1;">
|
<div style="flex 1;">
|
||||||
<router-view
|
<router-view
|
||||||
v-if="object"
|
v-if="object"
|
||||||
:key="route.fullPath"
|
:key="route.fullPath"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total-tracks="totalTracks"
|
:total-tracks="totalTracks"
|
||||||
:is-serie="isSerie"
|
:is-serie="isSerie"
|
||||||
:artist-credit="artistCredit"
|
:artist-credit="artistCredit"
|
||||||
:object="object"
|
:object="object"
|
||||||
:is-loading-tracks="isLoadingTracks"
|
:is-loading-tracks="isLoadingTracks"
|
||||||
object-type="album"
|
object-type="album"
|
||||||
@libraries-loaded="libraries = $event"
|
@libraries-loaded="libraries = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Layout>
|
</Layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -79,7 +79,7 @@ const fetchData = async () => {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
page_size: paginateBy.value,
|
page_size: paginateBy.value,
|
||||||
q: query.value,
|
q: query.value,
|
||||||
// @ts-ignore
|
// @ts-expect-error TODO: add strict types to useOrdering
|
||||||
ordering: orderingString.value,
|
ordering: orderingString.value,
|
||||||
playable: true,
|
playable: true,
|
||||||
tag: tags.value,
|
tag: tags.value,
|
||||||
|
|
|
@ -25,7 +25,6 @@ import Layout from '~/components/ui/Layout.vue'
|
||||||
import Modal from '~/components/ui/Modal.vue'
|
import Modal from '~/components/ui/Modal.vue'
|
||||||
import Spacer from '~/components/ui/Spacer.vue'
|
import Spacer from '~/components/ui/Spacer.vue'
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: number
|
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 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 publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
|
||||||
|
|
||||||
|
const cover = computed(() => {
|
||||||
const cover = computed(() => {
|
|
||||||
const artistCover: Cover | undefined = object.value?.cover
|
const artistCover: Cover | undefined = object.value?.cover
|
||||||
|
|
||||||
const albumCover: Cover | undefined = object.value?.albums
|
const albumCover: Cover | undefined = object.value?.albums
|
||||||
|
@ -68,12 +66,12 @@ const cover = computed(() => {
|
||||||
)?.cover
|
)?.cover
|
||||||
|
|
||||||
const fallback : Cover = {
|
const fallback : Cover = {
|
||||||
uuid: '',
|
uuid: '',
|
||||||
urls: {
|
urls: {
|
||||||
original: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
|
original: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
|
||||||
medium_square_crop: `${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`
|
large_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return artistCover
|
return artistCover
|
||||||
|
@ -122,7 +120,11 @@ watch(() => props.id, fetchData, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Layout stack main v-title="labels.title">
|
<Layout
|
||||||
|
v-title="labels.title"
|
||||||
|
stack
|
||||||
|
main
|
||||||
|
>
|
||||||
<Loader v-if="isLoading" />
|
<Loader v-if="isLoading" />
|
||||||
<template v-if="object && !isLoading">
|
<template v-if="object && !isLoading">
|
||||||
<Layout flex>
|
<Layout flex>
|
||||||
|
@ -131,9 +133,18 @@ watch(() => props.id, fetchData, { immediate: true })
|
||||||
:alt="object.name"
|
:alt="object.name"
|
||||||
class="channel-image"
|
class="channel-image"
|
||||||
>
|
>
|
||||||
<Layout stack style="flex: 1; gap: 8px;">
|
<Layout
|
||||||
<h1 style="margin-top: 64px; margin-bottom: 8px;">{{ object.name }}</h1>
|
stack
|
||||||
<Layout flex class="meta" style="gap: 0;">
|
style="flex: 1; gap: 8px;"
|
||||||
|
>
|
||||||
|
<h1 style="margin-top: 64px; margin-bottom: 8px;">
|
||||||
|
{{ object.name }}
|
||||||
|
</h1>
|
||||||
|
<Layout
|
||||||
|
flex
|
||||||
|
class="meta"
|
||||||
|
style="gap: 0;"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="albums"
|
v-if="albums"
|
||||||
>
|
>
|
||||||
|
@ -181,8 +192,8 @@ watch(() => props.id, fetchData, { immediate: true })
|
||||||
|
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-if="publicLibraries.length > 0"
|
v-if="publicLibraries.length > 0"
|
||||||
@click="showEmbedModal = true"
|
|
||||||
icon="bi-code-square"
|
icon="bi-code-square"
|
||||||
|
@click="showEmbedModal = true"
|
||||||
>
|
>
|
||||||
{{ t('components.library.ArtistBase.button.embed') }}
|
{{ t('components.library.ArtistBase.button.embed') }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
|
@ -223,7 +234,7 @@ watch(() => props.id, fetchData, { immediate: true })
|
||||||
{{ t('components.library.ArtistBase.button.edit') }}
|
{{ t('components.library.ArtistBase.button.edit') }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
|
|
||||||
<hr v-for="obj in getReportableObjects({artist: object})">
|
<hr v-if="getReportableObjects({artist: object}).length>0">
|
||||||
|
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-for="obj in getReportableObjects({artist: object})"
|
v-for="obj in getReportableObjects({artist: object})"
|
||||||
|
@ -234,7 +245,7 @@ watch(() => props.id, fetchData, { immediate: true })
|
||||||
{{ obj.label }}
|
{{ obj.label }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
|
|
||||||
<hr>
|
<hr v-if="getReportableObjects({artist: object}).length>0">
|
||||||
|
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-if="store.state.auth.availablePermissions['library']"
|
v-if="store.state.auth.availablePermissions['library']"
|
||||||
|
|
|
@ -231,8 +231,7 @@ const resetField = (fieldId: string) => {
|
||||||
{{ t('components.library.EditForm.message.noPermission') }}
|
{{ t('components.library.EditForm.message.noPermission') }}
|
||||||
</Alert>
|
</Alert>
|
||||||
<Layout
|
<Layout
|
||||||
v-for="fieldConfig in config.fields"
|
v-for="fieldConfig in (values ? config.fields : [])"
|
||||||
v-if="values"
|
|
||||||
:key="fieldConfig.id"
|
:key="fieldConfig.id"
|
||||||
stack
|
stack
|
||||||
gap-8
|
gap-8
|
||||||
|
|
|
@ -80,7 +80,7 @@ const attributedToUrl = computed(() => router.resolve({
|
||||||
|
|
||||||
const artistCredit = track.value?.artist_credit
|
const artistCredit = track.value?.artist_credit
|
||||||
|
|
||||||
const totalDuration = computed(() => track.value?.uploads?.[0]?.duration ?? 0)
|
const totalDuration = computed(() => track.value?.uploads?.[0]?.duration ?? 0)
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const labels = computed(() => ({
|
const labels = computed(() => ({
|
||||||
|
@ -142,253 +142,276 @@ watch(showDeleteModal, (newValue) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Layout stack main>
|
<Layout
|
||||||
<Loader
|
stack
|
||||||
v-if="isLoading"
|
main
|
||||||
v-title="labels.title"
|
>
|
||||||
/>
|
<Loader
|
||||||
<template v-if="track">
|
v-if="isLoading"
|
||||||
<Layout flex>
|
v-title="labels.title"
|
||||||
<img
|
/>
|
||||||
v-if="track.cover"
|
<template v-if="track">
|
||||||
v-lazy="store.getters['instance/absoluteUrl'](track.cover.urls.large_square_crop)"
|
<Layout flex>
|
||||||
alt=""
|
<img
|
||||||
class="channel-image"
|
v-if="track.cover"
|
||||||
>
|
v-lazy="store.getters['instance/absoluteUrl'](track.cover.urls.large_square_crop)"
|
||||||
<img
|
alt=""
|
||||||
v-if="track.album && track.album.cover"
|
class="channel-image"
|
||||||
v-lazy="store.getters['instance/absoluteUrl'](track.album.cover.urls.large_square_crop)"
|
>
|
||||||
alt=""
|
<img
|
||||||
class="channel-image"
|
v-if="track.album && track.album.cover"
|
||||||
>
|
v-lazy="store.getters['instance/absoluteUrl'](track.album.cover.urls.large_square_crop)"
|
||||||
<img
|
alt=""
|
||||||
v-else
|
class="channel-image"
|
||||||
alt=""
|
>
|
||||||
class="channel-image"
|
<img
|
||||||
src="../../assets/audio/default-cover.png"
|
v-else
|
||||||
>
|
alt=""
|
||||||
|
class="channel-image"
|
||||||
|
src="../../assets/audio/default-cover.png"
|
||||||
|
>
|
||||||
|
|
||||||
<Layout stack style="flex: 1; gap: 8px;">
|
<Layout
|
||||||
<Layout flex no-gap style="align-items: baseline;">
|
stack
|
||||||
<h1 style="margin-top: 64px; margin-bottom: 8px;">{{ track.title }}</h1>
|
style="flex: 1; gap: 8px;"
|
||||||
<Spacer grow />
|
>
|
||||||
<Button
|
<Layout
|
||||||
v-if="upload"
|
flex
|
||||||
:aria-label="labels.download"
|
no-gap
|
||||||
:to="downloadUrl"
|
style="align-items: baseline;"
|
||||||
target="_blank"
|
|
||||||
primary
|
|
||||||
icon="bi-download"
|
|
||||||
:title="labels.download"
|
|
||||||
>
|
>
|
||||||
{{ labels.download }}
|
<h1 style="margin-top: 64px; margin-bottom: 8px;">
|
||||||
</Button>
|
{{ track.title }}
|
||||||
</Layout>
|
</h1>
|
||||||
<artist-credit-label
|
<Spacer grow />
|
||||||
:artist-credit="track.artist_credit"
|
<Button
|
||||||
/>
|
v-if="upload"
|
||||||
<div class="meta">
|
:aria-label="labels.download"
|
||||||
<span>{{ t('components.library.TrackBase.title') }}</span>
|
:to="downloadUrl"
|
||||||
<i class="bi bi-dot" />
|
target="_blank"
|
||||||
<span>{{ track.album.title }}</span>
|
primary
|
||||||
<i v-if="totalDuration > 0" class="bi bi-dot" />
|
icon="bi-download"
|
||||||
<human-duration
|
:title="labels.download"
|
||||||
v-if="totalDuration > 0"
|
>
|
||||||
:duration="totalDuration"
|
{{ labels.download }}
|
||||||
|
</Button>
|
||||||
|
</Layout>
|
||||||
|
<artist-credit-label
|
||||||
|
:artist-credit="track.artist_credit"
|
||||||
/>
|
/>
|
||||||
</div>
|
<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"
|
||||||
|
/>
|
||||||
|
<human-duration
|
||||||
|
v-if="totalDuration > 0"
|
||||||
|
:duration="totalDuration"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Layout flex>
|
<Layout flex>
|
||||||
<PlayButton
|
<PlayButton
|
||||||
:is-playable="track.is_playable"
|
:is-playable="track.is_playable"
|
||||||
class="vibrant"
|
class="vibrant"
|
||||||
split
|
split
|
||||||
:track="track"
|
:track="track"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacer h grow />
|
<Spacer
|
||||||
|
h
|
||||||
|
grow
|
||||||
|
/>
|
||||||
|
|
||||||
<TrackFavoriteIcon v-if="store.state.auth.authenticated" :track="track" />
|
<TrackFavoriteIcon
|
||||||
<TrackPlaylistIcon v-if="store.state.auth.authenticated" :track="track" />
|
v-if="store.state.auth.authenticated"
|
||||||
<Popover v-model:open="open">
|
:track="track"
|
||||||
<template #default="{ toggleOpen }">
|
/>
|
||||||
<OptionsButton @click="toggleOpen" />
|
<TrackPlaylistIcon
|
||||||
</template>
|
v-if="store.state.auth.authenticated"
|
||||||
<template #items>
|
:track="track"
|
||||||
<PopoverItem
|
/>
|
||||||
v-if="domain != store.getters['instance/domain']"
|
<Popover v-model:open="open">
|
||||||
:to="track.fid"
|
<template #default="{ toggleOpen }">
|
||||||
target="_blank"
|
<OptionsButton @click="toggleOpen" />
|
||||||
icon="bi-box-arrow-up-right"
|
</template>
|
||||||
>
|
<template #items>
|
||||||
{{ t('components.library.TrackBase.link.domain', { domain }) }}
|
<PopoverItem
|
||||||
</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
|
<PopoverItem
|
||||||
v-if="isEmbedable"
|
v-if="isEmbedable"
|
||||||
@click="showEmbedModal = !showEmbedModal"
|
icon="bi-code-slash"
|
||||||
icon="bi-code-slash"
|
@click="showEmbedModal = !showEmbedModal"
|
||||||
>
|
>
|
||||||
{{ t('components.library.TrackBase.button.embed') }}
|
{{ t('components.library.TrackBase.button.embed') }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
|
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
:to="wikipediaUrl"
|
:to="wikipediaUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
icon="bi-wikipedia"
|
icon="bi-wikipedia"
|
||||||
>
|
>
|
||||||
{{ t('components.library.TrackBase.link.wikipedia') }}
|
{{ t('components.library.TrackBase.link.wikipedia') }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
|
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-if="discogsUrl"
|
v-if="discogsUrl"
|
||||||
:to="discogsUrl"
|
:to="discogsUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
icon="bi-box-arrow-up-right"
|
icon="bi-box-arrow-up-right"
|
||||||
>
|
>
|
||||||
{{ t('components.library.TrackBase.link.discogs') }}
|
{{ t('components.library.TrackBase.link.discogs') }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
|
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-if="track.is_local"
|
v-if="track.is_local"
|
||||||
icon="bi-pencil-fill"
|
icon="bi-pencil-fill"
|
||||||
:to="{ name: 'library.tracks.edit', params: { id: track.id } }"
|
:to="{ name: 'library.tracks.edit', params: { id: track.id } }"
|
||||||
>
|
>
|
||||||
{{ t('components.library.TrackBase.button.edit') }}
|
{{ t('components.library.TrackBase.button.edit') }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
|
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-if="artist &&
|
v-if="artist &&
|
||||||
store.state.auth.authenticated &&
|
store.state.auth.authenticated &&
|
||||||
artist.channel &&
|
artist.channel &&
|
||||||
artist.attributed_to.full_username === store.state.auth.fullUsername"
|
artist.attributed_to.full_username === store.state.auth.fullUsername"
|
||||||
@click="showDeleteModal = true"
|
icon="bi-trash"
|
||||||
icon="bi-trash"
|
@click="showDeleteModal = true"
|
||||||
>
|
>
|
||||||
{{ t('components.library.TrackBase.button.delete') }}
|
{{ t('components.library.TrackBase.button.delete') }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-for="obj in getReportableObjects({ track })"
|
v-for="obj in getReportableObjects({ track })"
|
||||||
:key="obj.target.type + obj.target.id"
|
:key="obj.target.type + obj.target.id"
|
||||||
@click="report(obj)"
|
icon="bi-flag"
|
||||||
icon="bi-flag"
|
@click="report(obj)"
|
||||||
>
|
>
|
||||||
{{ obj.label }}
|
{{ obj.label }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-if="store.state.auth.availablePermissions['library']"
|
v-if="store.state.auth.availablePermissions['library']"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'manage.library.tracks.detail',
|
name: 'manage.library.tracks.detail',
|
||||||
params: { id: track.id }
|
params: { id: track.id }
|
||||||
}"
|
}"
|
||||||
icon="bi-wrench"
|
icon="bi-wrench"
|
||||||
>
|
>
|
||||||
{{ t('components.library.TrackBase.link.moderation') }}
|
{{ t('components.library.TrackBase.link.moderation') }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
|
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-if="store.state.auth.profile?.is_superuser"
|
v-if="store.state.auth.profile?.is_superuser"
|
||||||
:to="store.getters['instance/absoluteUrl'](`/api/admin/music/track/${track.id}`)"
|
:to="store.getters['instance/absoluteUrl'](`/api/admin/music/track/${track.id}`)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
icon="bi-wrench"
|
icon="bi-wrench"
|
||||||
>
|
>
|
||||||
{{ t('components.library.TrackBase.link.django') }}
|
{{ t('components.library.TrackBase.link.django') }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<Layout flex>
|
<Layout flex>
|
||||||
<div>
|
<div>
|
||||||
<span v-if="track.attributed_to">
|
<span v-if="track.attributed_to">
|
||||||
{{ t('components.library.TrackBase.subtitle.with-uploader') }}
|
{{ t('components.library.TrackBase.subtitle.with-uploader') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ t('components.library.TrackBase.subtitle.without-uploader') }}
|
{{ t('components.library.TrackBase.subtitle.without-uploader') }}
|
||||||
</span>
|
</span>
|
||||||
<ActorLink
|
<ActorLink
|
||||||
v-if="track.attributed_to"
|
v-if="track.attributed_to"
|
||||||
:actor="track.attributed_to"
|
:actor="track.attributed_to"
|
||||||
:avatar="false"
|
: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
|
<template #actions>
|
||||||
:title="track.creation_date"
|
<Button
|
||||||
:datetime="track.creation_date"
|
secondary
|
||||||
>
|
@click="showEmbedModal = false"
|
||||||
{{ momentFormat(new Date(track.creation_date), 'LL') }}
|
>
|
||||||
</time>
|
{{ t('components.library.TrackBase.button.cancel') }}
|
||||||
</div>
|
</Button>
|
||||||
</Layout>
|
</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
|
<template #actions>
|
||||||
v-if="isEmbedable"
|
<Button
|
||||||
v-model="showEmbedModal"
|
secondary
|
||||||
:title="t('components.library.TrackBase.modal.embed.header')"
|
@click="showDeleteModal = false"
|
||||||
>
|
>
|
||||||
<embed-wizard
|
{{ t('components.library.TrackBase.button.cancel') }}
|
||||||
:id="track.id"
|
</Button>
|
||||||
type="track"
|
<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 #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>
|
|
||||||
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -53,19 +53,18 @@ const fetchLicense = async (licenseId: string) => {
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (props.track.license) {
|
if (props.track.license) {
|
||||||
// @ts-expect-error For some reason, track.license is id instead of License here
|
|
||||||
fetchLicense(props.track.license)
|
fetchLicense(props.track.license)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const release_details: {
|
const releaseDetails: {
|
||||||
label: string;
|
label: string;
|
||||||
release_value: string;
|
releaseValue: string;
|
||||||
link?: { name: string; params: { id: number } };
|
link?: { name: string; params: { id: number } };
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
label: t('components.library.TrackDetail.table.release.artist'),
|
label: t('components.library.TrackDetail.table.release.artist'),
|
||||||
release_value: props.track.artist_credit.map(ac => ac.credit).join(', '),
|
releaseValue: props.track.artist_credit.map(ac => ac.credit).join(', '),
|
||||||
link: props.track.artist_credit.length > 0
|
link: props.track.artist_credit.length > 0
|
||||||
? {
|
? {
|
||||||
name: 'library.artists.detail',
|
name: 'library.artists.detail',
|
||||||
|
@ -78,7 +77,7 @@ const release_details: {
|
||||||
props.track.album?.artist_credit?.[0].artist.content_category === 'music'
|
props.track.album?.artist_credit?.[0].artist.content_category === 'music'
|
||||||
? t('components.library.TrackDetail.table.release.album')
|
? t('components.library.TrackDetail.table.release.album')
|
||||||
: t('components.library.TrackDetail.table.release.series'),
|
: t('components.library.TrackDetail.table.release.series'),
|
||||||
release_value: props.track.album?.title || t('components.library.TrackDetail.notApplicable'),
|
releaseValue: props.track.album?.title || t('components.library.TrackDetail.notApplicable'),
|
||||||
link: props.track.album
|
link: props.track.album
|
||||||
? {
|
? {
|
||||||
name: 'library.albums.detail',
|
name: 'library.albums.detail',
|
||||||
|
@ -88,48 +87,48 @@ const release_details: {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('components.library.TrackDetail.table.release.year'),
|
label: t('components.library.TrackDetail.table.release.year'),
|
||||||
release_value: props.track.album?.release_date
|
releaseValue: props.track.album?.release_date
|
||||||
? momentFormat(new Date(props.track.album.release_date), 'Y')
|
? momentFormat(new Date(props.track.album.release_date), 'Y')
|
||||||
: t('components.library.TrackDetail.notApplicable')
|
: t('components.library.TrackDetail.notApplicable')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('components.library.TrackDetail.table.release.copyright'),
|
label: t('components.library.TrackDetail.table.release.copyright'),
|
||||||
release_value: props.track.copyright || t('components.library.TrackDetail.notApplicable')
|
releaseValue: props.track.copyright || t('components.library.TrackDetail.notApplicable')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('components.library.TrackDetail.table.release.license'),
|
label: t('components.library.TrackDetail.table.release.license'),
|
||||||
release_value: license.value?.name || t('components.library.TrackDetail.notApplicable')
|
releaseValue: license.value?.name || t('components.library.TrackDetail.notApplicable')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const track_details: {
|
const trackDetails: {
|
||||||
label: string;
|
label: string;
|
||||||
track_value: string | number;
|
trackValue: string | number;
|
||||||
link?: { name: string; params: { id: number } };
|
link?: { name: string; params: { id: number } };
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
label: t('components.library.TrackDetail.table.track.duration'),
|
label: t('components.library.TrackDetail.table.track.duration'),
|
||||||
track_value: upload?.value.duration ? time.parse(upload.value.duration) : t('components.library.TrackDetail.notApplicable')
|
trackValue: upload?.value.duration ? time.parse(upload.value.duration) : t('components.library.TrackDetail.notApplicable')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label:
|
label:
|
||||||
t('components.library.TrackDetail.table.track.size'),
|
t('components.library.TrackDetail.table.track.size'),
|
||||||
track_value: upload?.value.size ? humanSize(upload.value.size) : t('components.library.TrackDetail.notApplicable')
|
trackValue: upload?.value.size ? humanSize(upload.value.size) : t('components.library.TrackDetail.notApplicable')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('components.library.TrackDetail.table.track.codec'),
|
label: t('components.library.TrackDetail.table.track.codec'),
|
||||||
track_value: upload?.value.extension || t('components.library.TrackDetail.notApplicable')
|
trackValue: upload?.value.extension || t('components.library.TrackDetail.notApplicable')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label:
|
label:
|
||||||
t('components.library.TrackDetail.table.track.bitrate.label'),
|
t('components.library.TrackDetail.table.track.bitrate.label'),
|
||||||
track_value: upload?.value.bitrate
|
trackValue: 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')
|
: t('components.library.TrackDetail.notApplicable')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('components.library.TrackDetail.table.track.downloads'),
|
label: t('components.library.TrackDetail.table.track.downloads'),
|
||||||
track_value: props.track.downloads_count
|
trackValue: props.track.downloads_count
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
@ -156,15 +155,15 @@ const track_details: {
|
||||||
<Section
|
<Section
|
||||||
align-left
|
align-left
|
||||||
h2="Release Details"
|
h2="Release Details"
|
||||||
:action="{
|
:action="musicbrainzUrl ? {
|
||||||
text:'View on MusicBrainz',
|
text: 'View on MusicBrainz',
|
||||||
to: musicbrainzUrl
|
to: musicbrainzUrl
|
||||||
}"
|
} : undefined"
|
||||||
icon="bi-box-arrow-up-right"
|
icon="bi-box-arrow-up-right"
|
||||||
/>
|
/>
|
||||||
<Layout
|
<Layout
|
||||||
v-for="item in release_details"
|
v-for="item in releaseDetails"
|
||||||
key="label"
|
:key="item.label"
|
||||||
flex
|
flex
|
||||||
class="details"
|
class="details"
|
||||||
style="min-width: 120px;"
|
style="min-width: 120px;"
|
||||||
|
@ -179,12 +178,12 @@ const track_details: {
|
||||||
class="value"
|
class="value"
|
||||||
:to="item.link"
|
:to="item.link"
|
||||||
>
|
>
|
||||||
{{ item.release_value }}
|
{{ item.releaseValue }}
|
||||||
</Link>
|
</Link>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="value"
|
class="value"
|
||||||
>{{ item.release_value }}</span>
|
>{{ item.releaseValue }}</span>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
@ -197,8 +196,8 @@ const track_details: {
|
||||||
h2="Track Details"
|
h2="Track Details"
|
||||||
/>
|
/>
|
||||||
<Layout
|
<Layout
|
||||||
v-for="item in track_details"
|
v-for="item in trackDetails"
|
||||||
key="label"
|
:key="item.label"
|
||||||
flex
|
flex
|
||||||
class="details"
|
class="details"
|
||||||
style="min-width: 120px;"
|
style="min-width: 120px;"
|
||||||
|
@ -213,12 +212,12 @@ const track_details: {
|
||||||
class="value"
|
class="value"
|
||||||
:to="item.link"
|
:to="item.link"
|
||||||
>
|
>
|
||||||
{{ item.track_value }}
|
{{ item.trackValue }}
|
||||||
</Link>
|
</Link>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="value"
|
class="value"
|
||||||
>{{ item.track_value }}</span>
|
>{{ item.trackValue }}</span>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -102,7 +102,6 @@ const labels = computed(() => ({
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="ui inline form">
|
<div class="ui inline form">
|
||||||
/front/src/components/manage/library/AlbumsTable.vue
|
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="ui six wide field">
|
<div class="ui six wide field">
|
||||||
<label for="albums-search">{{ t('components.manage.library.AlbumsTable.label.search') }}</label>
|
<label for="albums-search">{{ t('components.manage.library.AlbumsTable.label.search') }}</label>
|
||||||
|
|
|
@ -12,14 +12,10 @@ defineProps<Props>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Layout
|
<!-- TODO: Remove this module -->
|
||||||
v-if="playlists.length > 0"
|
<PlaylistsCard
|
||||||
grid
|
v-for="playlist in playlists"
|
||||||
>
|
:key="playlist.id"
|
||||||
<PlaylistsCard
|
:playlist="playlist"
|
||||||
v-for="playlist in playlists"
|
/>
|
||||||
:key="playlist.id"
|
|
||||||
:playlist="playlist"
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -48,7 +48,7 @@ const tags = computed(() => {
|
||||||
:class="props.labelClasses"
|
:class="props.labelClasses"
|
||||||
>
|
>
|
||||||
<Pill raised>
|
<Pill raised>
|
||||||
#{{ truncate(tag, props.truncateSize) }}
|
{{ `#${truncate(tag, props.truncateSize)}` }}
|
||||||
</Pill>
|
</Pill>
|
||||||
</router-link>
|
</router-link>
|
||||||
<Pill
|
<Pill
|
||||||
|
|
|
@ -14,8 +14,17 @@ const emit = defineEmits<{ play: [track: Track] }>()
|
||||||
|
|
||||||
const { track, user } = defineProps<{ track: Track, user: User }>()
|
const { track, user } = defineProps<{ track: Track, user: User }>()
|
||||||
|
|
||||||
const artist_credit = track.artist_credit || []
|
const router = useRouter()
|
||||||
const firstArtist = artist_credit.length > 0 ? artist_credit[0].artist : null
|
|
||||||
|
const navigate = (to: 'track' | 'artist' | 'user') =>
|
||||||
|
to === 'track'
|
||||||
|
? router.push({ name: 'library.tracks.detail', params: { id: track.id } })
|
||||||
|
: to === 'artist'
|
||||||
|
? router.push({ name: 'library.artists.detail', params: { id: track.artist.id } })
|
||||||
|
: router.push({ name: 'profile.full', params: profileParams.value })
|
||||||
|
|
||||||
|
const artistCredit = track.artist_credit || []
|
||||||
|
const firstArtist = artistCredit.length > 0 ? artistCredit[0].artist : null
|
||||||
|
|
||||||
const profileParams = computed(() => {
|
const profileParams = computed(() => {
|
||||||
const [username, domain] = user.full_username.split('@')
|
const [username, domain] = user.full_username.split('@')
|
||||||
|
|
|
@ -39,7 +39,14 @@ const props = defineProps<{
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
|
||||||
// TODO: Refactor this once upload button progress indicator can be tested (in Sidebar.vue)
|
// TODO: Refactor this once upload button progress indicator can be tested (in Sidebar.vue)
|
||||||
const isIconOnly = computed(() => !!props.icon && (!slots.default || 'square' in props && props.square || 'squareSmall' in props && props.squareSmall))
|
const isIconOnly = computed(() =>
|
||||||
|
!!props.icon
|
||||||
|
&& (!slots.default
|
||||||
|
|| ('square' in props && props.square)
|
||||||
|
|| ('squareSmall' in props && props.squareSmall)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const isSplitIconOnly = computed(() => !!props.splitIcon && !props.splitTitle)
|
const isSplitIconOnly = computed(() => !!props.splitIcon && !props.splitTitle)
|
||||||
|
|
||||||
const internalLoader = ref(false)
|
const internalLoader = ref(false)
|
||||||
|
|
|
@ -21,15 +21,17 @@ const isOpen = defineModel<boolean>({ default: false })
|
||||||
const previouslyFocusedElement = ref()
|
const previouslyFocusedElement = ref()
|
||||||
|
|
||||||
// Handle focus and inertness of the elements behind the modal
|
// Handle focus and inertness of the elements behind the modal
|
||||||
watchEffect(() =>
|
watchEffect(() => {
|
||||||
isOpen.value
|
if (isOpen.value) {
|
||||||
? (previouslyFocusedElement.value = document.activeElement,
|
previouslyFocusedElement.value = document.activeElement
|
||||||
document.querySelector('#app')?.setAttribute('inert', 'true'))
|
document.querySelector('#app')?.setAttribute('inert', 'true')
|
||||||
: (nextTick(() => previouslyFocusedElement.value?.focus()),
|
} else {
|
||||||
document.querySelector('#app')?.removeAttribute('inert'))
|
nextTick(() => previouslyFocusedElement.value?.focus())
|
||||||
)
|
document.querySelector('#app')?.removeAttribute('inert')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onKeyboardShortcut('escape', () => isOpen.value = false)
|
onKeyboardShortcut('escape', () => { isOpen.value = false })
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// When overflowing content: Add inset shadow to indicate scrollability
|
// When overflowing content: Add inset shadow to indicate scrollability
|
||||||
|
|
|
@ -52,12 +52,11 @@ const setPage = () => {
|
||||||
page.value = pageFromInput(goTo.value)
|
page.value = pageFromInput(goTo.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(goTo, (potentiallyWrongValue) =>
|
watch(goTo, potentiallyWrongValue => {
|
||||||
goTo.value
|
goTo.value = typeof potentiallyWrongValue === 'string'
|
||||||
= typeof potentiallyWrongValue === 'string'
|
? ''
|
||||||
? ''
|
: pageFromInput(potentiallyWrongValue)
|
||||||
: pageFromInput(potentiallyWrongValue)
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const pageFromInput = (input: string | number): number =>
|
const pageFromInput = (input: string | number): number =>
|
||||||
input === 'NaN'
|
input === 'NaN'
|
||||||
|
@ -98,8 +97,7 @@ watch(page, (_) => {
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<template
|
<template
|
||||||
v-for="(i, index) in renderPages"
|
v-for="(i, index) in (isSmall ? [] : renderPages)"
|
||||||
v-if="!isSmall"
|
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
|
@ -114,10 +112,10 @@ watch(page, (_) => {
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="i + 1 < renderPages[index + 1]">
|
<li v-if="i + 1 < renderPages[index + 1]">
|
||||||
…
|
{{ (() => '…')() }}
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-if="isSmall">
|
||||||
<li>
|
<li>
|
||||||
<Button
|
<Button
|
||||||
square-small
|
square-small
|
||||||
|
@ -125,11 +123,11 @@ watch(page, (_) => {
|
||||||
:secondary="page !== 1"
|
:secondary="page !== 1"
|
||||||
@click="page = 1"
|
@click="page = 1"
|
||||||
>
|
>
|
||||||
1
|
{{ (() => '1')() }}
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="page === 1 || page === pages">
|
<li v-if="page === 1 || page === pages">
|
||||||
…
|
{{ (() => '…')() }}
|
||||||
</li>
|
</li>
|
||||||
<li v-else>
|
<li v-else>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -19,12 +19,16 @@ onMounted(() => {
|
||||||
if (props.autofocus) input.value?.focus()
|
if (props.autofocus) input.value?.focus()
|
||||||
})
|
})
|
||||||
|
|
||||||
const sanitize = () =>
|
const sanitize = () => {
|
||||||
model.value = (console.log("SANITIZE"), model.value?.replace(',', '')?.trim())
|
console.log('SANITIZE')
|
||||||
|
model.value = model.value?.replace(',', '')?.trim()
|
||||||
const sanitizeAndBlur = () =>
|
}
|
||||||
sanitize() || input.value?.blur()
|
|
||||||
|
|
||||||
|
const sanitizeAndBlur = () => {
|
||||||
|
sanitize()
|
||||||
|
input.value?.blur()
|
||||||
|
/* and bubble a confirmation event */
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -32,7 +36,6 @@ const sanitizeAndBlur = () =>
|
||||||
:is="model ? 'label' : 'button'"
|
:is="model ? 'label' : 'button'"
|
||||||
class="funkwhale pill"
|
class="funkwhale pill"
|
||||||
:class="props.noUnderline && 'no-underline'"
|
:class="props.noUnderline && 'no-underline'"
|
||||||
:is="model ? 'label' : 'button'"
|
|
||||||
:type="model ? undefined : 'button'"
|
:type="model ? undefined : 'button'"
|
||||||
v-bind="color(props, ['interactive', 'secondary'])()"
|
v-bind="color(props, ['interactive', 'secondary'])()"
|
||||||
@click.stop="handleClick"
|
@click.stop="handleClick"
|
||||||
|
|
|
@ -50,36 +50,39 @@ whenInteractive(() =>
|
||||||
|
|
||||||
// Pill clicked --> edit or unselect label
|
// Pill clicked --> edit or unselect label
|
||||||
|
|
||||||
const pillClicked = (value: string) =>
|
const pillClicked = (value: string) => {
|
||||||
model.value.custom?.includes(value)
|
model.value.custom?.includes(value)
|
||||||
? edit(value)
|
? edit(value)
|
||||||
: unselect(value)
|
: unselect(value)
|
||||||
|
}
|
||||||
|
|
||||||
const edit = (value: string) =>
|
const edit = (value: string) => {
|
||||||
editingValue.value = (console.log('edit', value), value)
|
editingValue.value = value
|
||||||
|
}
|
||||||
|
|
||||||
const unselect = (value: string) =>
|
const unselect = (value: string) => {
|
||||||
model.value
|
model.value = {
|
||||||
= {
|
...model.value,
|
||||||
...model.value,
|
current: model.value.current.filter(v => v !== value),
|
||||||
current: model.value.current.filter(v => v !== value),
|
others: [value, ...(model.value.others || [])]
|
||||||
others: [value, ...(model.value.others || [])]
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Editing value changed --> remove, add or replace a label
|
// Editing value changed --> remove, add or replace a label
|
||||||
|
|
||||||
const remove = (value: string) =>
|
const remove = (value: string) => {
|
||||||
model.value = {
|
model.value = {
|
||||||
...model.value,
|
...model.value,
|
||||||
current: model.value.current.filter(v => v !== (console.log('remove', value), value)),
|
current: model.value.current.filter(v => v !== value),
|
||||||
custom: model.value.custom?.filter(v => v !== value)
|
custom: model.value.custom?.filter(v => v !== value)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const add = (value: string) => {
|
const add = (value: string) => {
|
||||||
if (model.value.current.includes(value)) return
|
if (model.value.current.includes(value)) return
|
||||||
model.value = {
|
model.value = {
|
||||||
...model.value,
|
...model.value,
|
||||||
current: [...model.value.current, (console.log('add', value), value)],
|
current: [...model.value.current, value],
|
||||||
custom: [...(model.value.custom || []), value]
|
custom: [...(model.value.custom || []), value]
|
||||||
}
|
}
|
||||||
additionalValue.value = ''
|
additionalValue.value = ''
|
||||||
|
@ -90,7 +93,7 @@ const add = (value: string) => {
|
||||||
|
|
||||||
const replace = (value: string) => {
|
const replace = (value: string) => {
|
||||||
model.value = {
|
model.value = {
|
||||||
...(console.log('replace', value), model.value),
|
...model.value,
|
||||||
current: model.value.current.map(v => v === value ? editingValue.value : v),
|
current: model.value.current.map(v => v === value ? editingValue.value : v),
|
||||||
custom: model.value.custom?.map(v => v === value ? editingValue.value : v)
|
custom: model.value.custom?.map(v => v === value ? editingValue.value : v)
|
||||||
}
|
}
|
||||||
|
@ -160,17 +163,21 @@ watch(model, () => {
|
||||||
v-model="selectedLabel"
|
v-model="selectedLabel"
|
||||||
name="dropdown"
|
name="dropdown"
|
||||||
:class="$style.dropdown"
|
:class="$style.dropdown"
|
||||||
@change="e => e.target.value='+'"
|
@change="e => { (e.target as HTMLInputElement).value='+' }"
|
||||||
>
|
>
|
||||||
<option value="+" />
|
<option value="+" />
|
||||||
<option
|
<option
|
||||||
v-for="value in model.others"
|
v-for="value in model.others"
|
||||||
|
:key="value"
|
||||||
:value="value"
|
:value="value"
|
||||||
>
|
>
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<template v-for="value in model.current">
|
<template
|
||||||
|
v-for="value in model.current"
|
||||||
|
:key="value"
|
||||||
|
>
|
||||||
<!-- List of current pills -->
|
<!-- List of current pills -->
|
||||||
|
|
||||||
<Pill
|
<Pill
|
||||||
|
@ -200,11 +207,12 @@ watch(model, () => {
|
||||||
|
|
||||||
<!-- TODO: Add error state (or mitigation) if new label is already in `custom[]` -->
|
<!-- TODO: Add error state (or mitigation) if new label is already in `custom[]` -->
|
||||||
|
|
||||||
<Pill solid no-underline
|
<Pill
|
||||||
v-if="model.custom"
|
v-if="model.custom"
|
||||||
:key="componentKey"
|
:key="componentKey"
|
||||||
v-model="additionalValue"
|
v-model="additionalValue"
|
||||||
:key = "componentKey"
|
solid
|
||||||
|
no-underline
|
||||||
style="margin-right: 40px; height:32px; flex-grow: 1;"
|
style="margin-right: 40px; height:32px; flex-grow: 1;"
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { type ColorProps, type DefaultProps, type RaisedProps, color } from '~/c
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const open = defineModel('open', { default: false })
|
const open = defineModel<boolean>({ default: false })
|
||||||
|
|
||||||
const { positioning = 'vertical', ...colorProps } = defineProps<{
|
const { positioning = 'vertical', ...colorProps } = defineProps<{
|
||||||
positioning?:'horizontal' | 'vertical'
|
positioning?:'horizontal' | 'vertical'
|
||||||
|
@ -21,6 +21,7 @@ const { positioning = 'vertical', ...colorProps } = defineProps<{
|
||||||
// Template refs
|
// Template refs
|
||||||
const popover = ref()
|
const popover = ref()
|
||||||
const slot = ref()
|
const slot = ref()
|
||||||
|
const inSlot = ref()
|
||||||
|
|
||||||
// Click outside
|
// Click outside
|
||||||
const mobileClickOutside = (event: MouseEvent) => {
|
const mobileClickOutside = (event: MouseEvent) => {
|
||||||
|
@ -122,10 +123,11 @@ watch(open, (isOpen) => {
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="slot"
|
ref="slot"
|
||||||
:class="['funkwhale popover-container', { 'split-button': $slots.default?.$el?.classList?.contains('button-group') }]"
|
:class="['funkwhale popover-container', { 'split-button': inSlot?.classList?.contains('button-group') }]"
|
||||||
:style="$slots.default?.$el?.classList?.contains('button-group') ? 'display: inline-flex' : 'display: contents'"
|
:style="inSlot?.classList?.contains('button-group') ? 'display: inline-flex' : 'display: contents'"
|
||||||
>
|
>
|
||||||
<slot
|
<slot
|
||||||
|
ref="inSlot"
|
||||||
:is-open="open"
|
:is-open="open"
|
||||||
:toggle-open="() => open = !open"
|
:toggle-open="() => open = !open"
|
||||||
:open="() => open = true"
|
:open="() => open = true"
|
||||||
|
|
|
@ -1,22 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DOMPurify from 'dompurify'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const { as = 'div', html: rawHtml } = defineProps<{ as?:string, html:string }>()
|
const { as = 'div', html: rawHtml } = defineProps<{ as?:string, html:string }>()
|
||||||
|
|
||||||
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
|
|
||||||
// set all elements owning target to target=_blank
|
|
||||||
if ('target' in node) {
|
|
||||||
node.setAttribute('target', '_blank')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const html = computed(() => DOMPurify.sanitize(rawHtml))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<component
|
<component
|
||||||
:is="as"
|
:is="as"
|
||||||
v-html="html"
|
v-dompurify-html="rawHtml"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -20,9 +20,7 @@ const index = computed({
|
||||||
return keys.value.indexOf(model.value)
|
return keys.value.indexOf(model.value)
|
||||||
},
|
},
|
||||||
set (newIndex) {
|
set (newIndex) {
|
||||||
console.log('NEW', newIndex)
|
|
||||||
model.value = keys.value[newIndex]
|
model.value = keys.value[newIndex]
|
||||||
console.log('=', keys.value[newIndex])
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -66,6 +64,7 @@ onMounted(() => {
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-for="key in keys"
|
v-for="key in keys"
|
||||||
|
:key="key"
|
||||||
:class="[$style.key, {[$style.current]: key===model}]"
|
:class="[$style.key, {[$style.current]: key===model}]"
|
||||||
style="flex-basis:var(--step-size)"
|
style="flex-basis:var(--step-size)"
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -99,6 +98,7 @@ onMounted(() => {
|
||||||
<span style="display: inline-flex; margin-right: -100%; width: 100%; visibility: hidden;">
|
<span style="display: inline-flex; margin-right: -100%; width: 100%; visibility: hidden;">
|
||||||
<span
|
<span
|
||||||
v-for="key in keys"
|
v-for="key in keys"
|
||||||
|
:key="key"
|
||||||
:class="$style.description"
|
:class="$style.description"
|
||||||
:style="`margin-right: -20%; --current-step: 0; color: magenta;`"
|
:style="`margin-right: -20%; --current-step: 0; color: magenta;`"
|
||||||
><Markdown :md="options[key]" /></span>
|
><Markdown :md="options[key]" /></span>
|
||||||
|
|
|
@ -38,7 +38,8 @@ watch(() => tabs.length, (_, from) => {
|
||||||
<div class="tabs-header">
|
<div class="tabs-header">
|
||||||
<component
|
<component
|
||||||
:is="tab.to ? Link : Button"
|
:is="tab.to ? Link : Button"
|
||||||
v-for="(tab, _) in tabs"
|
v-for="tab in tabs"
|
||||||
|
:key="tab.title"
|
||||||
ghost
|
ghost
|
||||||
:class="{ 'is-active': actualCurrentTitle === tab.title }"
|
:class="{ 'is-active': actualCurrentTitle === tab.title }"
|
||||||
v-bind="tab"
|
v-bind="tab"
|
||||||
|
|
|
@ -83,7 +83,7 @@ const splice = async (start: number, deleteCount: number, items?: string) => {
|
||||||
textarea.value.setSelectionRange(selectionStart, selectionEnd)
|
textarea.value.setSelectionRange(selectionStart, selectionEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
const newLineOperations = new Map<RegExp, (event: KeyboardEvent, line: string, groups: string[]) => void>()
|
const newLineOperations = new Map<RegExp, ((event: KeyboardEvent, line: string, groups: string[]) => void)>()
|
||||||
const newline = async (event: KeyboardEvent) => {
|
const newline = async (event: KeyboardEvent) => {
|
||||||
const line = currentLine.value
|
const line = currentLine.value
|
||||||
for (const regexp of newLineOperations.keys()) {
|
for (const regexp of newLineOperations.keys()) {
|
||||||
|
@ -217,20 +217,20 @@ onMounted(() => {
|
||||||
class="preview"
|
class="preview"
|
||||||
/>
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
ref="textarea"
|
|
||||||
:maxlength="charLimit"
|
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
|
id="textarea_id"
|
||||||
|
ref="textarea"
|
||||||
|
v-model="model"
|
||||||
|
:maxlength="charLimit"
|
||||||
:autofocus="autofocus || undefined"
|
:autofocus="autofocus || undefined"
|
||||||
:required="required"
|
:required="required"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@click="updateLineNumber"
|
|
||||||
v-model="model"
|
|
||||||
@mousedown.stop
|
|
||||||
id="textarea_id"
|
|
||||||
@mouseup.stop
|
|
||||||
:rows="initialLines"
|
:rows="initialLines"
|
||||||
@keydown.left="updateLineNumber"
|
|
||||||
:style="`min-height:${((typeof(initialLines) === 'string' ? parseInt(initialLines) : (initialLines ?? 3)) + 1.2) * 1.5}rem`"
|
:style="`min-height:${((typeof(initialLines) === 'string' ? parseInt(initialLines) : (initialLines ?? 3)) + 1.2) * 1.5}rem`"
|
||||||
|
@click="updateLineNumber"
|
||||||
|
@mousedown.stop
|
||||||
|
@mouseup.stop
|
||||||
|
@keydown.left="updateLineNumber"
|
||||||
@keydown.right="updateLineNumber"
|
@keydown.right="updateLineNumber"
|
||||||
@keydown.up="updateLineNumber"
|
@keydown.up="updateLineNumber"
|
||||||
@keydown.down="updateLineNumber"
|
@keydown.down="updateLineNumber"
|
||||||
|
@ -350,7 +350,7 @@ onMounted(() => {
|
||||||
:aria-pressed="preview || undefined"
|
:aria-pressed="preview || undefined"
|
||||||
@click="preview = !preview"
|
@click="preview = !preview"
|
||||||
>
|
>
|
||||||
Preview
|
{{ t('components.common.ContentForm.button.preview') }}
|
||||||
</Button>
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Button from '../Button.vue'
|
import Button from '../Button.vue'
|
||||||
|
|
||||||
const play = defineEmits()
|
const play = defineEmits(['play'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -43,8 +43,7 @@ const getStyle = (props : Partial<AlignmentProps>) => ([key, value]: Entry<Align
|
||||||
(
|
(
|
||||||
typeof styles[key] === 'function'
|
typeof styles[key] === 'function'
|
||||||
? (styles[key](
|
? (styles[key](
|
||||||
// We know that props[key] is a value accepted by styles[key]. The ts compiler is not so smart.
|
// @ts-expect-error We know that props[key] is a value accepted by styles[key]. The ts compiler is not so smart.
|
||||||
// @ts-ignore
|
|
||||||
(key in props && props[key])
|
(key in props && props[key])
|
||||||
? props[((props[key]), (key))]
|
? props[((props[key]), (key))]
|
||||||
: value
|
: value
|
||||||
|
|
|
@ -50,7 +50,7 @@ function useWebSocketHandler (eventName: 'mutation.created', handler: (event: Pe
|
||||||
function useWebSocketHandler (eventName: 'mutation.updated', handler: (event: PendingReviewEdits) => void): stopFn
|
function useWebSocketHandler (eventName: 'mutation.updated', handler: (event: PendingReviewEdits) => void): stopFn
|
||||||
function useWebSocketHandler (eventName: 'import.status_updated', handler: (event: ImportStatusWS) => void): stopFn
|
function useWebSocketHandler (eventName: 'import.status_updated', handler: (event: ImportStatusWS) => void): stopFn
|
||||||
function useWebSocketHandler (eventName: 'user_request.created', handler: (event: PendingReviewRequests) => void): stopFn
|
function useWebSocketHandler (eventName: 'user_request.created', handler: (event: PendingReviewRequests) => void): stopFn
|
||||||
function useWebSocketHandler (eventName: 'Listen', handler: (event: ListenWS) => void): stopFn
|
function useWebSocketHandler (eventName: 'Listen', handler: (event: unknown) => void): stopFn
|
||||||
|
|
||||||
function useWebSocketHandler (eventName: string, handler: (event: any) => void): stopFn {
|
function useWebSocketHandler (eventName: string, handler: (event: any) => void): stopFn {
|
||||||
const id = `${+new Date() + Math.random()}`
|
const id = `${+new Date() + Math.random()}`
|
||||||
|
|
|
@ -43,12 +43,12 @@ const styles = {
|
||||||
} as const satisfies Record<Key, string | ((w: string) => string)>
|
} as const satisfies Record<Key, string | ((w: string) => string)>
|
||||||
|
|
||||||
const getStyle = (props: Partial<WidthProps>) => (key: Key): string =>
|
const getStyle = (props: Partial<WidthProps>) => (key: Key): string =>
|
||||||
// @ts-ignore
|
// @ts-expect-error Typescript is hard
|
||||||
typeof styles[key] === 'function' && key in props
|
typeof styles[key] === 'function' && key in props
|
||||||
// @ts-ignore
|
// @ts-expect-error Typescript is hard
|
||||||
? styles[key](
|
? styles[key](
|
||||||
// TODO: Make the typescript compiler understand `key in props`
|
// TODO: Make the typescript compiler understand `key in props`
|
||||||
// @ts-ignore
|
// @ts-expect-error Typescript is hard
|
||||||
props[key]
|
props[key]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import type { InitModule, InitModuleContext } from '~/types'
|
import type { InitModule, InitModuleContext } from '~/types'
|
||||||
|
|
||||||
|
import VueDOMPurifyHTML from 'vue-dompurify-html'
|
||||||
|
|
||||||
import store, { key } from '~/store'
|
import store, { key } from '~/store'
|
||||||
import router from '~/router'
|
import router from '~/router'
|
||||||
|
|
||||||
|
@ -43,6 +45,7 @@ const pinia = createPinia()
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
app.use(store, key)
|
app.use(store, key)
|
||||||
|
app.use(VueDOMPurifyHTML)
|
||||||
|
|
||||||
const modules: Record<string | 'axios', { install?: InitModule }> = import.meta.glob('./init/*.ts', { eager: true })
|
const modules: Record<string | 'axios', { install?: InitModule }> = import.meta.glob('./init/*.ts', { eager: true })
|
||||||
const moduleContext: InitModuleContext = {
|
const moduleContext: InitModuleContext = {
|
||||||
|
|
|
@ -32,7 +32,7 @@ defineProps<{
|
||||||
<div class="track-title">
|
<div class="track-title">
|
||||||
{{ track.metadata.tags.title }}
|
{{ track.metadata.tags.title }}
|
||||||
</div>
|
</div>
|
||||||
{{ track.metadata.tags.artist }} / {{ track.metadata.tags.album }}
|
{{ `${track.metadata.tags.artist} / ${track.metadata.tags.album}` }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
@ -84,9 +84,9 @@ defineProps<{
|
||||||
v-else
|
v-else
|
||||||
class="track-progress"
|
class="track-progress"
|
||||||
>
|
>
|
||||||
{{ bytesToHumanSize(track.file.size / 100 * track.progress) }}
|
{{ `${bytesToHumanSize(track.file.size / 100 * track.progress)}
|
||||||
/ {{ bytesToHumanSize(track.file.size) }}
|
/ ${bytesToHumanSize(track.file.size)}
|
||||||
⋅ {{ track.progress }}%
|
⋅ ${track.progress}%` }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FwButton
|
<FwButton
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useUploadsStore } from '~/ui/stores/upload'
|
||||||
import { bytesToHumanSize } from '~/ui/composables/bytes'
|
import { bytesToHumanSize } from '~/ui/composables/bytes'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import UploadList from '~/ui/components/UploadList.vue'
|
import UploadList from '~/ui/components/UploadList.vue'
|
||||||
import Alert from '~/components/ui/Alert.vue'
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
|
@ -11,6 +12,8 @@ import Modal from '~/components/ui/Modal.vue'
|
||||||
import Input from '~/components/ui/Input.vue'
|
import Input from '~/components/ui/Input.vue'
|
||||||
import FileUploadWidget from '~/components/library/FileUploadWidget.vue'
|
import FileUploadWidget from '~/components/library/FileUploadWidget.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const uploads = useUploadsStore()
|
const uploads = useUploadsStore()
|
||||||
|
|
||||||
const libraryOpen = computed({
|
const libraryOpen = computed({
|
||||||
|
@ -95,15 +98,10 @@ const isOpen = computed({
|
||||||
v-model="isOpen"
|
v-model="isOpen"
|
||||||
title="Upload..."
|
title="Upload..."
|
||||||
>
|
>
|
||||||
<template #alert="closeAlert">
|
<template #alert>
|
||||||
<Alert yellow>
|
<Alert yellow>
|
||||||
Before uploading, please ensure your files are tagged properly.
|
{{ `${t('components.library.FileUpload.message.tag')}
|
||||||
We recommend using Picard for that purpose.
|
${t('components.library.FileUpload.link.picard')}` }}
|
||||||
<template #actions>
|
|
||||||
<Button @click="closeAlert">
|
|
||||||
Got it
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,6 @@ const labels = computed(() => ({
|
||||||
<PopoverItem v-if="store.state.ui.notifications.inbox /* TODO: Check: + additionalNotifications */ > 0">
|
<PopoverItem v-if="store.state.ui.notifications.inbox /* TODO: Check: + additionalNotifications */ > 0">
|
||||||
<Link :to="{name: 'notifications'}">
|
<Link :to="{name: 'notifications'}">
|
||||||
<i class="bi bi-inbox-fill" />
|
<i class="bi bi-inbox-fill" />
|
||||||
>
|
|
||||||
{{ store.state.ui.notifications.inbox /* TODO: Check: + additionalNotifications */ }}
|
{{ store.state.ui.notifications.inbox /* TODO: Check: + additionalNotifications */ }}
|
||||||
{{ labels.notifications }}
|
{{ labels.notifications }}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -100,7 +99,7 @@ const labels = computed(() => ({
|
||||||
<hr v-if="store.state.auth.authenticated">
|
<hr v-if="store.state.auth.authenticated">
|
||||||
<PopoverItem :to="useModal('language').to">
|
<PopoverItem :to="useModal('language').to">
|
||||||
<i class="bi bi-translate" />
|
<i class="bi bi-translate" />
|
||||||
{{ labels.language }}...
|
{{ `${labels.language}...` }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
<PopoverSubmenu>
|
<PopoverSubmenu>
|
||||||
<i class="bi bi-palette-fill" />
|
<i class="bi bi-palette-fill" />
|
||||||
|
|
|
@ -71,14 +71,13 @@ const createForm = ref()
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
v-model="isOpen"
|
v-model="isOpen"
|
||||||
:title="t(`views.auth.ProfileOverview.modal.createChannel.${
|
:title="
|
||||||
step === 1 ?
|
step === 1
|
||||||
'header' :
|
? t('views.auth.ProfileOverview.modal.createChannel.header')
|
||||||
category === 'podcast' ?
|
: category === 'podcast'
|
||||||
'podcast.header'
|
? t('views.auth.ProfileOverview.modal.createChannel.podcast.header')
|
||||||
:
|
: t('views.auth.ProfileOverview.modal.createChannel.artist.header')
|
||||||
'artist.header'
|
"
|
||||||
}`)"
|
|
||||||
>
|
>
|
||||||
<channel-form
|
<channel-form
|
||||||
ref="createForm"
|
ref="createForm"
|
||||||
|
|
|
@ -150,8 +150,10 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Layout stack main
|
<Layout
|
||||||
v-title="labels.title"
|
v-title="labels.title"
|
||||||
|
stack
|
||||||
|
main
|
||||||
>
|
>
|
||||||
<Loader v-if="isLoading" />
|
<Loader v-if="isLoading" />
|
||||||
<template v-if="object && !isLoading">
|
<template v-if="object && !isLoading">
|
||||||
|
@ -164,18 +166,28 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
alt=""
|
alt=""
|
||||||
class="huge channel-image"
|
class="huge channel-image"
|
||||||
:src="store.getters['instance/absoluteUrl'](object.artist.cover.urls.large_square_crop)"
|
:src="store.getters['instance/absoluteUrl'](object.artist.cover.urls.large_square_crop)"
|
||||||
/>
|
>
|
||||||
<i
|
<i
|
||||||
v-else
|
v-else
|
||||||
class="bi bi-person-circle"
|
class="bi bi-person-circle"
|
||||||
style="font-size: 300px; margin-top: -32px;"
|
style="font-size: 300px; margin-top: -32px;"
|
||||||
/>
|
/>
|
||||||
<Layout stack style="flex: 1; gap: 16px;">
|
<Layout
|
||||||
|
stack
|
||||||
|
style="flex: 1; gap: 16px;"
|
||||||
|
>
|
||||||
<h1 style="margin-top: 64px; margin-bottom: 8px;">
|
<h1 style="margin-top: 64px; margin-bottom: 8px;">
|
||||||
{{ object.artist?.name }}
|
{{ object.artist?.name }}
|
||||||
</h1>
|
</h1>
|
||||||
<Layout stack class="meta" style="gap: 8px;">
|
<Layout
|
||||||
<Layout flex noGap>
|
stack
|
||||||
|
class="meta"
|
||||||
|
style="gap: 8px;"
|
||||||
|
>
|
||||||
|
<Layout
|
||||||
|
flex
|
||||||
|
no-gap
|
||||||
|
>
|
||||||
<template v-if="totalTracks > 0">
|
<template v-if="totalTracks > 0">
|
||||||
<span
|
<span
|
||||||
v-if="object.artist?.content_category === 'podcast'"
|
v-if="object.artist?.content_category === 'podcast'"
|
||||||
|
@ -203,7 +215,10 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout flex noGap>
|
<Layout
|
||||||
|
flex
|
||||||
|
no-gap
|
||||||
|
>
|
||||||
<template v-if="object.artist?.content_category === 'podcast'">
|
<template v-if="object.artist?.content_category === 'podcast'">
|
||||||
<span>
|
<span>
|
||||||
{{ t('views.channels.DetailBase.header.podcastChannel') }}
|
{{ t('views.channels.DetailBase.header.podcastChannel') }}
|
||||||
|
@ -231,7 +246,10 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
<i class="bi bi-dot" />
|
<i class="bi bi-dot" />
|
||||||
{{ t('views.library.LibraryBase.link.owner') }}
|
{{ t('views.library.LibraryBase.link.owner') }}
|
||||||
</span>
|
</span>
|
||||||
<Spacer h :size="4" />
|
<Spacer
|
||||||
|
h
|
||||||
|
:size="4"
|
||||||
|
/>
|
||||||
<ActorLink
|
<ActorLink
|
||||||
v-if="object.actor"
|
v-if="object.actor"
|
||||||
discrete
|
discrete
|
||||||
|
@ -241,13 +259,16 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
<rendered-description
|
<rendered-description
|
||||||
:content="object.artist?.description"
|
:content="object.artist?.description"
|
||||||
:update-url="`channels/${object.uuid}/`"
|
:update-url="`channels/${object.uuid}/`"
|
||||||
:can-update="false"
|
:can-update="false"
|
||||||
@updated="object = $event"
|
@updated="object = $event"
|
||||||
/>
|
/>
|
||||||
<Layout flex class="header-buttons">
|
<Layout
|
||||||
|
flex
|
||||||
|
class="header-buttons"
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
v-if="isOwner"
|
v-if="isOwner"
|
||||||
solid
|
solid
|
||||||
|
@ -295,8 +316,8 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
<hr>
|
<hr>
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-for="obj in getReportableObjects({account: object.attributed_to, channel: object})"
|
v-for="obj in getReportableObjects({account: object.attributed_to, channel: object})"
|
||||||
icon="bi-share"
|
|
||||||
:key="obj.target.type + obj.target.id"
|
:key="obj.target.type + obj.target.id"
|
||||||
|
icon="bi-share"
|
||||||
@click.stop.prevent="report(obj)"
|
@click.stop.prevent="report(obj)"
|
||||||
>
|
>
|
||||||
{{ obj.label }}
|
{{ obj.label }}
|
||||||
|
@ -317,7 +338,8 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
icon="bi-trash"
|
icon="bi-trash"
|
||||||
@confirm="remove()"
|
@confirm="remove()"
|
||||||
> {{ t('views.channels.DetailBase.button.confirm') }}
|
>
|
||||||
|
{{ t('views.channels.DetailBase.button.confirm') }}
|
||||||
<template #modal-content>
|
<template #modal-content>
|
||||||
{{ t('views.channels.DetailBase.modal.delete.content.warning') }}
|
{{ t('views.channels.DetailBase.modal.delete.content.warning') }}
|
||||||
</template>
|
</template>
|
||||||
|
@ -339,7 +361,10 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Spacer h grow />
|
<Spacer
|
||||||
|
h
|
||||||
|
grow
|
||||||
|
/>
|
||||||
<subscribe-button
|
<subscribe-button
|
||||||
:channel="object"
|
:channel="object"
|
||||||
@subscribed="updateSubscriptionCount(1)"
|
@subscribed="updateSubscriptionCount(1)"
|
||||||
|
@ -347,9 +372,9 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
:title="t('views.channels.DetailBase.modal.embed.header')"
|
|
||||||
v-if="totalTracks > 0"
|
v-if="totalTracks > 0"
|
||||||
v-model="showEmbedModal"
|
v-model="showEmbedModal"
|
||||||
|
:title="t('views.channels.DetailBase.modal.embed.header')"
|
||||||
:cancel="t('views.channels.DetailBase.button.cancel')"
|
:cancel="t('views.channels.DetailBase.button.cancel')"
|
||||||
>
|
>
|
||||||
<div class="scrolling content">
|
<div class="scrolling content">
|
||||||
|
@ -367,11 +392,13 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
:title="t(`views.channels.DetailBase.header.${
|
|
||||||
object.artist?.content_category === 'podcast' ? 'podcastChannel' : 'artistChannel'
|
|
||||||
}`)"
|
|
||||||
v-if="isOwner"
|
v-if="isOwner"
|
||||||
v-model="showEditModal"
|
v-model="showEditModal"
|
||||||
|
:title="
|
||||||
|
object.artist?.content_category === 'podcast'
|
||||||
|
? t('views.channels.DetailBase.header.podcastChannel')
|
||||||
|
: t('views.channels.DetailBase.header.artistChannel')
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div class="scrolling content">
|
<div class="scrolling content">
|
||||||
<channel-form
|
<channel-form
|
||||||
|
@ -387,7 +414,7 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
<Button
|
<Button
|
||||||
primary
|
primary
|
||||||
autofocus
|
autofocus
|
||||||
:isLoading="edit.loading"
|
:is-loading="edit.loading"
|
||||||
:disabled="!edit.submittable"
|
:disabled="!edit.submittable"
|
||||||
@click.stop="editForm?.submit"
|
@click.stop="editForm?.submit"
|
||||||
>
|
>
|
||||||
|
@ -401,8 +428,8 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
@click.stop.prevent="showSubscribeModal = true"
|
@click.stop.prevent="showSubscribeModal = true"
|
||||||
/>
|
/>
|
||||||
<Modal
|
<Modal
|
||||||
:title="t('views.channels.DetailBase.modal.subscribe.header')"
|
|
||||||
v-model:show="showSubscribeModal"
|
v-model:show="showSubscribeModal"
|
||||||
|
:title="t('views.channels.DetailBase.modal.subscribe.header')"
|
||||||
class="tiny"
|
class="tiny"
|
||||||
:cancel="t('views.channels.DetailBase.button.cancel')"
|
:cancel="t('views.channels.DetailBase.button.cancel')"
|
||||||
>
|
>
|
||||||
|
@ -453,7 +480,7 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
v-if="object.artist?.tags && object.artist?.tags.length > 0"
|
v-if="object.artist?.tags && object.artist?.tags.length > 0"
|
||||||
:tags="object.artist.tags"
|
:tags="object.artist.tags"
|
||||||
:limit="5"
|
:limit="5"
|
||||||
showMore="true"
|
show-more="true"
|
||||||
/>
|
/>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab
|
<Tab
|
||||||
|
|
|
@ -306,14 +306,13 @@ const showCreateModal = ref(false)
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
v-model="showCreateModal"
|
v-model="showCreateModal"
|
||||||
:title="t(`views.auth.ProfileOverview.modal.createChannel.${
|
:title="
|
||||||
step === 1 ?
|
step === 1
|
||||||
'header' :
|
? t('views.auth.ProfileOverview.modal.createChannel.header')
|
||||||
category === 'podcast' ?
|
: category === 'podcast'
|
||||||
'podcast.header'
|
? t('views.auth.ProfileOverview.modal.createChannel.podcast.header')
|
||||||
:
|
: t('views.auth.ProfileOverview.modal.createChannel.artist.header')
|
||||||
'artist.header'
|
"
|
||||||
}`)"
|
|
||||||
>
|
>
|
||||||
<channel-form
|
<channel-form
|
||||||
ref="createForm"
|
ref="createForm"
|
||||||
|
|
|
@ -20,7 +20,7 @@ const defaultQuota = computed(() => humanSize(quota.value * 1e6))
|
||||||
v-title="labels.title"
|
v-title="labels.title"
|
||||||
class="ui vertical aligned stripe segment"
|
class="ui vertical aligned stripe segment"
|
||||||
>
|
>
|
||||||
front/src/views/content/Home.vue
|
<!-- TODO: Remove this module -->
|
||||||
<div class="ui text container">
|
<div class="ui text container">
|
||||||
<h1>{{ labels.title }}</h1>
|
<h1>{{ labels.title }}</h1>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -45,7 +45,7 @@ const libraryCreated = (library: Library) => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="ui vertical aligned stripe segment">
|
<section class="ui vertical aligned stripe segment">
|
||||||
front/src/views/content/libraries/Home.vue
|
<!-- TODO: Modernise to new Ui -->
|
||||||
<div
|
<div
|
||||||
v-if="isLoading"
|
v-if="isLoading"
|
||||||
:class="['ui', {'active': isLoading}, 'inverted', 'dimmer']"
|
:class="['ui', {'active': isLoading}, 'inverted', 'dimmer']"
|
||||||
|
|
|
@ -102,6 +102,7 @@ const openModal = (object_: Library | Channel) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- TODO: Remove this module -->
|
||||||
<section
|
<section
|
||||||
v-title="labels.title"
|
v-title="labels.title"
|
||||||
class="ui vertical aligned stripe segment"
|
class="ui vertical aligned stripe segment"
|
||||||
|
|
|
@ -131,6 +131,8 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
||||||
v-else
|
v-else
|
||||||
:h1="t('views.playlists.List.header.browse')"
|
:h1="t('views.playlists.List.header.browse')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Search-bar -->
|
||||||
<Layout
|
<Layout
|
||||||
form
|
form
|
||||||
flex
|
flex
|
||||||
|
@ -215,42 +217,46 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
||||||
</select>
|
</select>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
<template
|
|
||||||
v-if="result && result.results.length > 0"
|
<Spacer v-if="result && result.results.length > 0" />
|
||||||
style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;"
|
|
||||||
>
|
<!-- Search results -->
|
||||||
|
<Section small-items>
|
||||||
<Loader v-if="isLoading" />
|
<Loader v-if="isLoading" />
|
||||||
|
<Alert
|
||||||
|
v-if="result && result.results.length === 0"
|
||||||
|
yellow
|
||||||
|
style="grid-column: 1 / -1;"
|
||||||
|
>
|
||||||
|
{{ t('views.playlists.List.empty.noResults') }}
|
||||||
|
<Button
|
||||||
|
v-if="store.state.auth.authenticated"
|
||||||
|
icon="bi-list"
|
||||||
|
primary
|
||||||
|
@click="store.commit('playlists/showModal', true)"
|
||||||
|
>
|
||||||
|
{{ t('views.playlists.List.button.create') }}
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
v-model:page="page"
|
v-model:page="page"
|
||||||
|
style="grid-column: 1 / -1;"
|
||||||
:pages="Math.ceil(result.count/paginateBy)"
|
:pages="Math.ceil(result.count/paginateBy)"
|
||||||
/>
|
/>
|
||||||
<playlist-card-list
|
<PlaylistsCard
|
||||||
v-if="result && result.results.length > 0"
|
v-for="playlist in (result && result.results.length > 0 ? result.results : [])"
|
||||||
:playlists="result.results"
|
:key="playlist.id"
|
||||||
|
:playlist="playlist"
|
||||||
/>
|
/>
|
||||||
</template>
|
<Spacer grow />
|
||||||
<Layout
|
<Pagination
|
||||||
v-else-if="result && result.results.length === 0"
|
v-if="result && result.count > paginateBy"
|
||||||
stack
|
v-model:page="page"
|
||||||
>
|
style="grid-column: 1 / -1;"
|
||||||
<Alert yellow>
|
:pages="Math.ceil(result.count/paginateBy)"
|
||||||
{{ t('views.playlists.List.empty.noResults') }}
|
/>
|
||||||
</Alert>
|
</Section>
|
||||||
<Button
|
|
||||||
v-if="store.state.auth.authenticated"
|
|
||||||
icon="bi-list"
|
|
||||||
primary
|
|
||||||
@click="store.commit('playlists/showModal', true)"
|
|
||||||
>
|
|
||||||
{{ t('views.playlists.List.button.create') }}
|
|
||||||
</Button>
|
|
||||||
</Layout>
|
|
||||||
<Spacer grow />
|
|
||||||
<Pagination
|
|
||||||
v-if="result && result.count > paginateBy"
|
|
||||||
v-model:page="page"
|
|
||||||
:pages="Math.ceil(result.count/paginateBy)"
|
|
||||||
/>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -3448,7 +3448,7 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.4.tgz#041143ba4a918e8f080f8b0ffbe3d4cb514e2315"
|
resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.4.tgz#041143ba4a918e8f080f8b0ffbe3d4cb514e2315"
|
||||||
integrity sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==
|
integrity sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==
|
||||||
|
|
||||||
"@types/trusted-types@*", "@types/trusted-types@^2.0.2":
|
"@types/trusted-types@*", "@types/trusted-types@^2.0.2", "@types/trusted-types@^2.0.7":
|
||||||
version "2.0.7"
|
version "2.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||||
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
||||||
|
@ -6277,6 +6277,13 @@ dompurify@3.0.8:
|
||||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.8.tgz#e0021ab1b09184bc8af7e35c7dd9063f43a8a437"
|
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.8.tgz#e0021ab1b09184bc8af7e35c7dd9063f43a8a437"
|
||||||
integrity sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ==
|
integrity sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ==
|
||||||
|
|
||||||
|
dompurify@^3.2.1:
|
||||||
|
version "3.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e"
|
||||||
|
integrity sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==
|
||||||
|
optionalDependencies:
|
||||||
|
"@types/trusted-types" "^2.0.7"
|
||||||
|
|
||||||
domutils@^3.1.0:
|
domutils@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
|
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
|
||||||
|
@ -11456,6 +11463,13 @@ vue-demi@^0.12.5:
|
||||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.12.5.tgz#8eeed566a7d86eb090209a11723f887d28aeb2d1"
|
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.12.5.tgz#8eeed566a7d86eb090209a11723f887d28aeb2d1"
|
||||||
integrity sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==
|
integrity sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==
|
||||||
|
|
||||||
|
vue-dompurify-html@5.2.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-dompurify-html/-/vue-dompurify-html-5.2.0.tgz#a8bb6cbee94f6e8581fd42c57096bde5185dcaf2"
|
||||||
|
integrity sha512-GX+BStkKEJ8wu/+hU1EK2nu/gzXWhb4XzBu6aowpsuU/3nkvXvZ2jx4nZ9M3jtS/Vu7J7MtFXjc7x3cWQ+zbVQ==
|
||||||
|
dependencies:
|
||||||
|
dompurify "^3.2.1"
|
||||||
|
|
||||||
vue-eslint-parser@^9.0.0, vue-eslint-parser@^9.3.1, vue-eslint-parser@^9.4.2:
|
vue-eslint-parser@^9.0.0, vue-eslint-parser@^9.3.1, vue-eslint-parser@^9.4.2:
|
||||||
version "9.4.2"
|
version "9.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz#02ffcce82042b082292f2d1672514615f0d95b6d"
|
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz#02ffcce82042b082292f2d1672514615f0d95b6d"
|
||||||
|
|
Loading…
Reference in New Issue