chore(format): [WIP] run eslint and fix most stylistic/formatting errors

This commit is contained in:
upsiflu 2025-02-24 15:17:18 +01:00
parent b82643d1a2
commit 8bb0adf700
57 changed files with 818 additions and 673 deletions

View File

@ -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',

View File

@ -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",

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -25,7 +25,8 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
showCount: false,
search: false,
limit: 12
limit: 12,
title: undefined
})
const store = useStore()

View File

@ -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>

View File

@ -24,7 +24,8 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
search: false,
header: true,
limit: 12
limit: 12,
title: undefined
})
const store = useStore()

View File

@ -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"

View File

@ -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"

View File

@ -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>()

View File

@ -72,7 +72,6 @@ const labels = computed(() => ({
<template>
<div>
/front/src/components/audio/Search.vue
<h2>
{{ t('components.audio.Search.header.search') }}
</h2>

View File

@ -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"

View File

@ -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]"

View File

@ -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"

View File

@ -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')"
>

View File

@ -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
}

View File

@ -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']"
/>

View File

@ -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"

View File

@ -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

View File

@ -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>

View File

@ -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,

View File

@ -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']"

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -48,7 +48,7 @@ const tags = computed(() => {
:class="props.labelClasses"
>
<Pill raised>
#{{ truncate(tag, props.truncateSize) }}
{{ `#${truncate(tag, props.truncateSize)}` }}
</Pill>
</router-link>
<Pill

View File

@ -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('@')

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import Button from '../Button.vue'
const play = defineEmits()
const play = defineEmits(['play'])
</script>
<template>

View File

@ -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

View File

@ -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()}`

View File

@ -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]
)

View File

@ -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 = {

View File

@ -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

View File

@ -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>

View File

@ -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" />

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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>

View File

@ -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']"

View File

@ -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"

View File

@ -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>

View File

@ -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"