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