fix(front): [WIP] use generated types to make the CI (`lint:tsc`) happy

This commit is contained in:
upsiflu 2025-04-03 17:02:39 +02:00
parent 2b3831d4d3
commit a5098c0952
30 changed files with 102 additions and 189 deletions

View File

@ -133,5 +133,6 @@
"workbox-precaching": "6.5.4", "workbox-precaching": "6.5.4",
"workbox-routing": "6.5.4", "workbox-routing": "6.5.4",
"workbox-strategies": "6.5.4" "workbox-strategies": "6.5.4"
} },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View File

@ -4,8 +4,6 @@ import { useStore } from '~/store'
import { get } from 'lodash-es' import { get } from 'lodash-es'
import { computed } from 'vue' import { computed } from 'vue'
import type { components } from '~/generated/types.ts'
import useMarkdown from '~/composables/useMarkdown' import useMarkdown from '~/composables/useMarkdown'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@ -40,24 +38,14 @@ const federationEnabled = computed(() => {
const onDesktop = computed(() => window.innerWidth > 800) const onDesktop = computed(() => window.innerWidth > 800)
const stats = computed(() => { const stats = computed(() => ({
const info = nodeinfo.value ?? {} as components['schemas']['NodeInfo20'] users: nodeinfo.value?.usage.users.activeMonth,
hours: nodeinfo.value?.metadata.content.local.hoursOfContent,
const data = { artists: nodeinfo.value?.metadata.content.local.artists,
users: get(info, 'usage.users.activeMonth', null), albums: nodeinfo.value?.metadata.content.local.releases, // TODO: Check where to get 'metadata.content.local.albums.total'
hours: get(info, 'metadata.content.local.hoursOfContent', null), tracks: nodeinfo.value?.metadata.content.local.recordings, // TODO: 'metadata.content.local.tracks.total'
artists: get(info, 'metadata.content.local.artists.total', null), listenings: nodeinfo.value?.metadata.usage?.listenings.total
albums: get(info, 'metadata.content.local.albums.total', null), }))
tracks: get(info, 'metadata.content.local.tracks.total', null),
listenings: get(info, 'metadata.usage.listenings.total', null)
}
if (data.users === null || data.artists === null) {
return data
}
return data
})
const headerStyle = computed(() => { const headerStyle = computed(() => {
if (!banner.value) { if (!banner.value) {
@ -341,7 +329,7 @@ const headerStyle = computed(() => {
class="statistics-statistic" class="statistics-statistic"
> >
<span class="statistics-figure ui text"> <span class="statistics-figure ui text">
<span class="ui big text"><strong>{{ stats.hours.toLocaleString(store.state.ui.momentLocale) }}</strong></span> <span class="ui big text"><strong>{{ stats.hours?.toLocaleString(store.state.ui.momentLocale) }}</strong></span>
<br> <br>
{{ t('components.AboutPod.stat.hoursOfMusic', stats.hours) }} {{ t('components.AboutPod.stat.hoursOfMusic', stats.hours) }}
</span> </span>
@ -351,7 +339,7 @@ const headerStyle = computed(() => {
class="statistics-statistic" class="statistics-statistic"
> >
<span class="statistics-figure ui text"> <span class="statistics-figure ui text">
<span class="ui big text"><strong>{{ stats.artists.toLocaleString(store.state.ui.momentLocale) }}</strong></span> <span class="ui big text"><strong>{{ stats.artists?.toLocaleString(store.state.ui.momentLocale) }}</strong></span>
<br> <br>
{{ t('components.AboutPod.stat.artistsCount', stats.artists) }} {{ t('components.AboutPod.stat.artistsCount', stats.artists) }}
</span> </span>
@ -361,7 +349,7 @@ const headerStyle = computed(() => {
class="statistics-statistic" class="statistics-statistic"
> >
<span class="statistics-figure ui text"> <span class="statistics-figure ui text">
<span class="ui big text"><strong>{{ stats.albums.toLocaleString(store.state.ui.momentLocale) }}</strong></span> <span class="ui big text"><strong>{{ stats.albums?.toLocaleString(store.state.ui.momentLocale) }}</strong></span>
<br> <br>
{{ t('components.AboutPod.stat.albumsCount', stats.albums) }} {{ t('components.AboutPod.stat.albumsCount', stats.albums) }}
</span> </span>
@ -371,7 +359,7 @@ const headerStyle = computed(() => {
class="statistics-statistic" class="statistics-statistic"
> >
<span class="statistics-figure ui text"> <span class="statistics-figure ui text">
<span class="ui big text"><strong>{{ stats.tracks.toLocaleString(store.state.ui.momentLocale) }}</strong></span> <span class="ui big text"><strong>{{ stats.tracks?.toLocaleString(store.state.ui.momentLocale) }}</strong></span>
<br> <br>
{{ t('components.AboutPod.stat.tracksCount', stats.tracks) }} {{ t('components.AboutPod.stat.tracksCount', stats.tracks) }}
</span> </span>

View File

@ -107,6 +107,8 @@ const save = async () => {
</script> </script>
<template> <template>
<!-- TODO: type the different values in `settings` (use generics) -->
<!-- eslint-disable vue/valid-v-model -->
<Section <Section
align-left align-left
:h2="group.label" :h2="group.label"
@ -135,33 +137,30 @@ const save = async () => {
v-bind="setting.fieldParams" v-bind="setting.fieldParams"
v-model="values[setting.identifier]" v-model="values[setting.identifier]"
/> />
<!-- eslint-disable vue/valid-v-model -->
<signup-form-builder <signup-form-builder
v-else-if="setting.fieldType === 'formBuilder'" v-else-if="setting.fieldType === 'formBuilder'"
v-model="values[setting.identifier] as Form" v-model="values[setting.identifier] as Form"
:signup-approval-enabled="!!values.moderation__signup_approval_enabled" :signup-approval-enabled="!!values.moderation__signup_approval_enabled"
/> />
<!-- eslint-enable vue/valid-v-model -->
<Input <Input
v-else-if="setting.field.widget.class === 'PasswordInput'" v-else-if="setting.field.widget.class === 'PasswordInput'"
v-model="values[setting.identifier]" v-model="values[setting.identifier] as string"
password password
type="password" type="password"
class="ui input" class="ui input"
/> />
<Input <Input
v-else-if="setting.field.widget.class === 'TextInput'" v-else-if="setting.field.widget.class === 'TextInput'"
v-model="values[setting.identifier]" v-model="values[setting.identifier] as string"
type="text" type="text"
class="ui input" class="ui input"
/> />
<Input <Input
v-else-if="setting.field.class === 'IntegerField'" v-else-if="setting.field.class === 'IntegerField'"
v-model.number="values[setting.identifier]" v-model.number="values[setting.identifier] as number"
type="number" type="number"
class="ui input" class="ui input"
/> />
<!-- eslint-disable vue/valid-v-model -->
<textarea <textarea
v-else-if="setting.field.widget.class === 'Textarea'" v-else-if="setting.field.widget.class === 'Textarea'"
v-model="values[setting.identifier] as string" v-model="values[setting.identifier] as string"
@ -172,13 +171,11 @@ const save = async () => {
<div <div
v-else-if="setting.field.widget.class === 'CheckboxInput'" v-else-if="setting.field.widget.class === 'CheckboxInput'"
> >
<!-- eslint-disable vue/valid-v-model -->
<Toggle <Toggle
v-model="values[setting.identifier] as boolean" v-model="values[setting.identifier] as boolean"
big big
:label="setting.verbose_name" :label="setting.verbose_name"
/> />
<!-- eslint-enable vue/valid-v-model -->
<Spacer :size="8" /> <Spacer :size="8" />
<p v-if="setting.help_text"> <p v-if="setting.help_text">
{{ setting.help_text }} {{ setting.help_text }}
@ -215,11 +212,15 @@ const save = async () => {
</option> </option>
</select> </select>
<div v-else-if="setting.field.widget.class === 'ImageWidget'"> <div v-else-if="setting.field.widget.class === 'ImageWidget'">
<!-- TODO: Implement image input -->
<!-- @vue-ignore -->
<Input <Input
:id="setting.identifier" :id="setting.identifier"
:ref="setFileRef(setting.identifier)" :ref="setFileRef(setting.identifier)"
type="file" type="file"
/> />
<div v-if="values[setting.identifier]"> <div v-if="values[setting.identifier]">
<h3 class="ui header"> <h3 class="ui header">
{{ t('components.admin.SettingsGroup.header.image') }} {{ t('components.admin.SettingsGroup.header.image') }}
@ -271,6 +272,7 @@ const save = async () => {
</Section> </Section>
<hr :class="$style.separator"> <hr :class="$style.separator">
<Spacer size-64 /> <Spacer size-64 />
<!-- eslint-enable vue/valid-v-model -->
</template> </template>
<style module> <style module>

View File

@ -13,8 +13,6 @@ import Spacer from '~/components/ui/Spacer.vue'
import { type Album } from '~/types' import { type Album } from '~/types'
const play = defineEmit<[album: Album]>()
interface Props { interface Props {
album: Album; album: Album;
} }

View File

@ -1,16 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { components, operations } from '~/generated/types.ts' import type { components } from '~/generated/types.ts'
import PlayButton from '~/components/audio/PlayButton.vue' import PlayButton from '~/components/audio/PlayButton.vue'
import Card from '~/components/ui/Card.vue' import Card from '~/components/ui/Card.vue'
import Spacer from '~/components/ui/Spacer.vue' import Spacer from '~/components/ui/Spacer.vue'
import type { Artist, Cover, Track, Album } from '~/types' import type { Artist, Album } from '~/types'
const albums = ref([] as Album[]) const albums = ref([] as Album[])
const tracks = ref([] as Track[])
interface Props { interface Props {
artist: Artist | components['schemas']['ArtistWithAlbums']; artist: Artist | components['schemas']['ArtistWithAlbums'];

View File

@ -79,7 +79,7 @@ watch(
<template> <template>
<Section <Section
align-left align-left
columns-per-item="1" :columns-per-item="1"
:h2="title" :h2="title"
> >
<Loader <Loader

View File

@ -14,6 +14,9 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
// TODO: Fix getRoute // TODO: Fix getRoute
// TODO: check if still needed:
/*
const getRoute = (ac: ArtistCredit) => { const getRoute = (ac: ArtistCredit) => {
return { return {
name: ac.artist.channel ? 'channels.detail' : 'library.artists.detail', name: ac.artist.channel ? 'channels.detail' : 'library.artists.detail',
@ -22,6 +25,7 @@ const getRoute = (ac: ArtistCredit) => {
} }
} }
} }
*/
</script> </script>
<template> <template>

View File

@ -13,7 +13,7 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const route = computed(() => props.artist.channel const route = computed(() => props.artist.channel
? { name: 'channels.detail', params: { id: props.artist.channel.uuid } } ? { name: 'channels.detail', params: { id: props.artist.channel } }
: { name: 'library.artists.detail', params: { id: props.artist.id } } : { name: 'library.artists.detail', params: { id: props.artist.id } }
) )
</script> </script>

View File

@ -5,7 +5,6 @@ import { momentFormat } from '~/utils/filters'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'
import { computed } from 'vue' import { computed } from 'vue'
import { useRouter } from 'vue-router'
import moment from 'moment' import moment from 'moment'
@ -20,7 +19,6 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const store = useStore() const store = useStore()
const router = useRouter()
const imageUrl = computed(() => props.object.artist?.cover const imageUrl = computed(() => props.object.artist?.cover
? store.getters['instance/absoluteUrl'](props.object.artist.cover.urls.medium_square_crop) ? store.getters['instance/absoluteUrl'](props.object.artist.cover.urls.medium_square_crop)

View File

@ -79,6 +79,8 @@ interface MetadataChoices {
const metadataChoices = ref({ itunes_category: null } as MetadataChoices) const metadataChoices = ref({ itunes_category: null } as MetadataChoices)
const itunesSubcategories = computed(() => { const itunesSubcategories = computed(() => {
for (const element of metadataChoices.value.itunes_category ?? []) { for (const element of metadataChoices.value.itunes_category ?? []) {
// TODO: Backend: Define schema for `metadata` field
// @ts-expect-error No types defined by backend schema for `metadata` field
if (element.value === newValues.metadata.itunes_category) { if (element.value === newValues.metadata.itunes_category) {
return element.children ?? [] return element.children ?? []
} }
@ -94,6 +96,7 @@ const labels = computed(() => ({
const submittable = computed(() => !!( const submittable = computed(() => !!(
newValues.content_category === 'podcast' newValues.content_category === 'podcast'
// @ts-expect-error No types defined by backend schema for `metadata` field
? newValues.name && newValues.username && newValues.metadata.itunes_category && newValues.metadata.language ? newValues.name && newValues.username && newValues.metadata.itunes_category && newValues.metadata.language
: newValues.name && newValues.username : newValues.name && newValues.username
)) ))
@ -104,13 +107,16 @@ watch(() => newValues.name, (name) => {
} }
}) })
// @ts-expect-error No types defined by backend schema for `metadata` field
watch(() => newValues.metadata.itunes_category, () => { watch(() => newValues.metadata.itunes_category, () => {
// @ts-expect-error No types defined by backend schema for `metadata` field
newValues.metadata.itunes_subcategory = null newValues.metadata.itunes_subcategory = null
}) })
const isLoading = ref(false) const isLoading = ref(false)
const errors = ref([] as string[]) const errors = ref([] as string[])
// @ts-expect-error Re-check emits
watchEffect(() => emit('category', newValues.content_category)) watchEffect(() => emit('category', newValues.content_category))
watchEffect(() => emit('loading', isLoading.value)) watchEffect(() => emit('loading', isLoading.value))
watchEffect(() => emit('submittable', submittable.value)) watchEffect(() => emit('submittable', submittable.value))
@ -265,6 +271,8 @@ defineExpose({
<label for="channel-language"> <label for="channel-language">
{{ t('components.audio.ChannelForm.label.language') }} {{ t('components.audio.ChannelForm.label.language') }}
</label> </label>
<!-- @vue-ignore -->
<select <select
id="channel-language" id="channel-language"
v-model="newValues.metadata.language" v-model="newValues.metadata.language"
@ -295,6 +303,8 @@ defineExpose({
<label for="channel-itunes-category"> <label for="channel-itunes-category">
{{ t('components.audio.ChannelForm.label.category') }} {{ t('components.audio.ChannelForm.label.category') }}
</label> </label>
<!-- @vue-ignore -->
<select <select
id="itunes-category" id="itunes-category"
v-model="newValues.metadata.itunes_category" v-model="newValues.metadata.itunes_category"
@ -315,6 +325,8 @@ defineExpose({
<label for="channel-itunes-category"> <label for="channel-itunes-category">
{{ t('components.audio.ChannelForm.label.subcategory') }} {{ t('components.audio.ChannelForm.label.subcategory') }}
</label> </label>
<!-- @vue-ignore -->
<select <select
id="itunes-category" id="itunes-category"
v-model="newValues.metadata.itunes_subcategory" v-model="newValues.metadata.itunes_subcategory"
@ -342,6 +354,7 @@ defineExpose({
</span> </span>
</Alert> </Alert>
<div class="ui field"> <div class="ui field">
<!-- @vue-ignore -->
<Input <Input
id="channel-itunes-email" id="channel-itunes-email"
v-model="newValues.metadata.owner_email" v-model="newValues.metadata.owner_email"
@ -351,6 +364,7 @@ defineExpose({
/> />
</div> </div>
<div class="ui field"> <div class="ui field">
<!-- @vue-ignore -->
<Input <Input
id="channel-itunes-name" id="channel-itunes-name"
v-model="newValues.metadata.owner_name" v-model="newValues.metadata.owner_name"

View File

@ -18,7 +18,10 @@ interface Props {
const { t } = useI18n() const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const width = ref(null)
// TODO: This used to be `null`. Is `0` correct?
const width = ref(0)
const height = ref(150) const height = ref(150)
const minHeight = ref(100) const minHeight = ref(100)

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Track, SimpleArtist as Artist, Album, Playlist, Library, Channel, Actor } from '~/types' import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types'
import type { components } from '~/generated/types'
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions' import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
@ -29,12 +30,12 @@ interface Props extends PlayOptionsProps {
isPlayable?: boolean isPlayable?: boolean
tracks?: Track[] tracks?: Track[]
track?: Track | null track?: Track | null
artist?: Artist | null artist?: Artist | components["schemas"]["SimpleChannelArtist"] | components['schemas']['ArtistWithAlbums'] | null
album?: Album | null album?: Album | null
playlist?: Playlist | null playlist?: Playlist | null
library?: Library | null library?: Library | null
channel?: Channel | null channel?: Channel | null
account?: Actor | null account?: Actor | components['schemas']['APIActor'] | null
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -201,7 +202,7 @@ const isOpen = ref(false)
</span> </span>
</PopoverItem> </PopoverItem>
<hr v-if="filterableArtist || Object.keys(getReportableObjects({track, album, artist, playlist, account, channel})).length > 0"> <hr v-if="filterableArtist || Object.keys(getReportableObjects({ track, album, artist, playlist, account, channel })).length > 0">
<PopoverItem <PopoverItem
v-if="filterableArtist" v-if="filterableArtist"
@ -214,7 +215,7 @@ const isOpen = ref(false)
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
v-for="obj in getReportableObjects({track, album, artist, playlist, account, channel})" v-for="obj in getReportableObjects({ track, album, artist, playlist, account, channel })"
:key="obj.target.type + obj.target.id" :key="obj.target.type + obj.target.id"
icon="bi-exclamation-triangle-fill" icon="bi-exclamation-triangle-fill"
@click.stop.prevent="report(obj)" @click.stop.prevent="report(obj)"

View File

@ -119,17 +119,18 @@ const loopingTitle = computed(() => {
: t('components.audio.Player.label.loopingWholeQueue') : t('components.audio.Player.label.loopingWholeQueue')
}) })
const hideArtist = () => { // TODO: check if still useful for filtering
if (currentTrack.value.artistId !== -1 && currentTrack.value.artistCredit) { // const hideArtist = () => {
return store.dispatch('moderation/hide', { // if (currentTrack.value.artistId !== -1 && currentTrack.value.artistCredit) {
type: 'artist', // return store.dispatch('moderation/hide', {
target: { // type: 'artist',
id: currentTrack.value.artistCredit[0].artist.id, // target: {
name: currentTrack.value.artistCredit[0].artist.name // id: currentTrack.value.artistCredit[0].artist.id,
} // name: currentTrack.value.artistCredit[0].artist.name
}) // }
} // })
} // }
// }
</script> </script>
<template> <template>

View File

@ -7,7 +7,8 @@ import { useQueue } from '~/composables/audio/queue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
const { playPrevious, hasNext, playNext, currentTrack } = useQueue() // TODO: Check if we want to use `currentTrack` from useQueue() in order to disable some icon. Or not.
const { playPrevious, hasNext, playNext } = useQueue()
const { isPlaying } = usePlayer() const { isPlaying } = usePlayer()
const { t } = useI18n() const { t } = useI18n()

View File

@ -1,49 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Artist, Track, Album, Tag } from '~/types'
import type { RouteRecordName, RouteLocationNamedRaw } from 'vue-router'
import jQuery from 'jquery' import { useFocus } from '@vueuse/core'
import { trim } from 'lodash-es'
import { useFocus, useCurrentElement } from '@vueuse/core'
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useStore } from '~/store'
import { generateTrackCreditString } from '~/utils/utils'
import onKeyboardShortcut from '~/composables/onKeyboardShortcut' import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
interface Events {
(e: 'search'): void
}
type CategoryCode = 'federation' | 'podcasts' | 'artists' | 'albums' | 'tracks' | 'tags' | 'more'
interface Category {
code: CategoryCode,
name: string,
route: RouteRecordName
getId: (obj: unknown) => number
getTitle: (obj: unknown) => string
getDescription: (obj: unknown) => string
}
type SimpleCategory = Partial<Category> & Pick<Category, 'code' | 'name'>
const isCategoryGuard = (object: Category | SimpleCategory): object is Category => typeof object.route === 'string'
interface Results {
name: string,
results: Result[]
}
interface Result {
title: string
id?: number
description?: string
routerUrl: RouteLocationNamedRaw
}
const emit = defineEmits<Events>()
const search = ref() const search = ref()
const { focused } = useFocus(search) const { focused } = useFocus(search)
onKeyboardShortcut(['shift', 'f'], () => (focused.value = true), true) onKeyboardShortcut(['shift', 'f'], () => (focused.value = true), true)
@ -60,8 +23,6 @@ const labels = computed(() => ({
})) }))
const router = useRouter() const router = useRouter()
const store = useStore()
const el = useCurrentElement()
const query = ref() const query = ref()
const enter = () => { const enter = () => {
@ -76,66 +37,6 @@ const blur = () => {
search.value.blur() search.value.blur()
} }
const categories = computed(() => [
{
code: 'federation',
name: t('components.audio.SearchBar.label.category.federation')
},
{
code: 'podcasts',
name: t('components.audio.SearchBar.label.category.podcasts')
},
{
code: 'artists',
route: 'library.artists.detail',
name: labels.value.artist,
getId: (obj: Artist) => obj.id,
getTitle: (obj: Artist) => obj.name,
getDescription: () => ''
},
{
code: 'albums',
route: 'library.albums.detail',
name: labels.value.album,
getId: (obj: Album) => obj.id,
getTitle: (obj: Album) => obj.title,
getDescription: (obj: Album) => generateTrackCreditString(obj)
},
{
code: 'tracks',
route: 'library.tracks.detail',
name: labels.value.track,
getId: (obj: Track) => obj.id,
getTitle: (obj: Track) => obj.title,
getDescription: (track: Track) => {
const album = track.album ?? null
return generateTrackCreditString(album) ?? generateTrackCreditString(track) ?? ''
}
},
{
code: 'tags',
route: 'library.tags.detail',
name: labels.value.tag,
getId: (obj: Tag) => obj.name,
getTitle: (obj: Tag) => `#${obj.name}`,
getDescription: (obj: Tag) => ''
},
{
code: 'more',
name: ''
}
] as (Category | SimpleCategory)[])
const objectId = computed(() => {
const trimmedQuery = trim(trim(query.value), '@')
if (trimmedQuery.startsWith('http://') || trimmedQuery.startsWith('https://') || trimmedQuery.includes('@')) {
return query.value
}
return null
})
onMounted(() => { onMounted(() => {
// TODO: Find out what jQuery version supports `search` // TODO: Find out what jQuery version supports `search`
// jQuery(el.value).search({ // jQuery(el.value).search({

View File

@ -18,7 +18,7 @@ const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const cover = computed(() => !props.artist.cover?.urls.original const cover = computed(() => !props.artist.cover?.urls.original
? props.artist.albums.find(album => !!album.cover?.urls.original)?.cover ? undefined // TODO: Also check Albums. Like in props.artist.albums.find(album => !!album.cover?.urls.original)?.cover
: props.artist.cover : props.artist.cover
) )
@ -41,7 +41,7 @@ const imageUrl = computed(() => cover.value?.urls.original
> >
<play-button <play-button
:icon-only="true" :icon-only="true"
:is-playable="artist.is_playable" :is-playable="true /* TODO: check if artist.is_playable exists instead */"
:button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']" :button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']"
:artist="artist" :artist="artist"
/> />
@ -67,15 +67,15 @@ const imageUrl = computed(() => cover.value?.urls.original
</div> </div>
<div class="extra content"> <div class="extra content">
<span v-if="artist.content_category === 'music'"> <span v-if="artist.content_category === 'music'">
{{ t('components.audio.artist.Card.meta.tracks', artist.tracks_count) }} {{ t('components.audio.artist.Card.meta.tracks', (0 /* TODO: check where artist.tracks_count exists */)) }}
</span> </span>
<span v-else> <span v-else>
{{ t('components.audio.artist.Card.meta.episodes', artist.tracks_count) }} {{ t('components.audio.artist.Card.meta.episodes', (0 /* TODO: check where artist.tracks_count exists */)) }}
</span> </span>
<play-button <play-button
class="right floated basic icon" class="right floated basic icon"
:dropdown-only="true" :dropdown-only="true"
:is-playable="artist.is_playable" :is-playable="true /* TODO: check if is_playable can be derived from the data */"
:dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']"
:artist="artist" :artist="artist"
/> />

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Track, SimpleArtist as Artist, Album, Playlist, Library, Channel, Actor } from '~/types' import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types'
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions' import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Track, SimpleArtist as Artist, Album, Playlist, Library, Channel, Actor } from '~/types' import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types'
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions' import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
import { useStore } from '~/store' import { useStore } from '~/store'

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Track, Album, Playlist, Library, Channel, Actor, Cover, ArtistCredit } from '~/types' import type { Track, Album, Playlist, Library, Channel, Actor, Cover, ArtistCredit } from '~/types'
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions' import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
import { getArtistCoverUrl } from '~/utils/utils'
import { ref } from 'vue' import { ref } from 'vue'

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Track, SimpleArtist as Artist, Album, Playlist, Library, Channel, Actor } from '~/types' import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types'
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions' import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
@ -13,7 +13,7 @@ import usePlayOptions from '~/composables/audio/usePlayOptions'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue' import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import TrackModal from '~/components/audio/track/Modal.vue' import TrackModal from '~/components/audio/track/Modal.vue'
import { generateTrackCreditString, getArtistCoverUrl } from '~/utils/utils' import { generateTrackCreditString } from '~/utils/utils'
interface Props extends PlayOptionsProps { interface Props extends PlayOptionsProps {
track: Track track: Track

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Track, SimpleArtist as Artist, Album, Playlist, Library, Channel, Actor } from '~/types' import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types'
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions' import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
import usePlayOptions from '~/composables/audio/usePlayOptions' import usePlayOptions from '~/composables/audio/usePlayOptions'
import useReport from '~/composables/moderation/useReport' import useReport from '~/composables/moderation/useReport'

View File

@ -2,9 +2,8 @@
import type { Track } from '~/types' import type { Track } from '~/types'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { clone, uniqBy, sortedUniq } from 'lodash-es' import { clone, uniqBy } from 'lodash-es'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useStore } from '~/store'
import axios from 'axios' import axios from 'axios'
@ -91,7 +90,6 @@ const allTracks = computed(() => {
const paginateResults = computed(() => props.paginateResults && allTracks.value.length < props.paginateBy) const paginateResults = computed(() => props.paginateResults && allTracks.value.length < props.paginateBy)
const { t } = useI18n() const { t } = useI18n()
const store = useStore()
const labels = computed(() => ({ const labels = computed(() => ({
title: t('components.audio.track.Table.table.header.title'), title: t('components.audio.track.Table.table.header.title'),

View File

@ -2,7 +2,7 @@
import type { BackendError } from '~/types' import type { BackendError } from '~/types'
import { onBeforeRouteLeave, type RouteLocationRaw, useRouter } from 'vue-router' import { onBeforeRouteLeave, type RouteLocationRaw, useRouter } from 'vue-router'
import { ref, reactive, computed, onMounted, nextTick } from 'vue' import { ref, reactive, computed } from 'vue'
import { useEventListener } from '@vueuse/core' import { useEventListener } from '@vueuse/core'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'

View File

@ -13,7 +13,6 @@ import Alert from '~/components/ui/Alert.vue'
import Input from '~/components/ui/Input.vue' import Input from '~/components/ui/Input.vue'
import Textarea from '~/components/ui/Textarea.vue' import Textarea from '~/components/ui/Textarea.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'

View File

@ -33,7 +33,7 @@ const fetchAlbums = async () => {
watch(() => model.value.channel, fetchAlbums, { immediate: true }) watch(() => model.value.channel, fetchAlbums, { immediate: true })
watch(albums, (value) => { watch(albums, (value) => {
if (value.length === 1) { selectedAlbumId.value = albums.value[0].id } if (value.length === 1) { model.value.albumId = albums.value[0].id }
}) })
</script> </script>

View File

@ -2,12 +2,11 @@
import type { BackendError, Channel, Upload, Track, Album } from '~/types' import type { BackendError, Channel, Upload, Track, Album } from '~/types'
import type { VueUploadItem } from 'vue-upload-component' import type { VueUploadItem } from 'vue-upload-component'
import { computed, ref, reactive, watchEffect, watch, onMounted } from 'vue' import { computed, ref, reactive, watchEffect, watch } from 'vue'
import { whenever } from '@vueuse/core' import { whenever } from '@vueuse/core'
import { humanSize } from '~/utils/filters' import { humanSize } from '~/utils/filters'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useModal } from '~/ui/composables/useModal.ts'
import axios from 'axios' import axios from 'axios'
import { type paths, type operations, type components } from '~/generated/types.ts' import { type paths, type operations, type components } from '~/generated/types.ts'
@ -16,14 +15,12 @@ import UploadMetadataForm from '~/components/channels/UploadMetadataForm.vue'
import FileUploadWidget from '~/components/library/FileUploadWidget.vue' import FileUploadWidget from '~/components/library/FileUploadWidget.vue'
import LicenseSelect from '~/components/channels/LicenseSelect.vue' import LicenseSelect from '~/components/channels/LicenseSelect.vue'
import AlbumSelect from '~/components/channels/AlbumSelect.vue' import AlbumSelect from '~/components/channels/AlbumSelect.vue'
import AlbumModal from '~/components/channels/AlbumModal.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
import Alert from '~/components/ui/Alert.vue' import Alert from '~/components/ui/Alert.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import Link from '~/components/ui/Link.vue'
import Loader from '~/components/ui/Loader.vue' import Loader from '~/components/ui/Loader.vue'
import Spacer from '~/components/ui/Spacer.vue' import Spacer from '~/components/ui/Spacer.vue'

View File

@ -25,6 +25,7 @@ const props = withDefaults(defineProps<Props>(), {
}) })
const newValues = reactive<Values>({ const newValues = reactive<Values>({
position: 0,
description: '', description: '',
title: '', title: '',
tags: [], tags: [],

View File

@ -1,15 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Actor } from '~/types' import type { Actor } from '~/types'
import type { components } from '~/generated/types'
import { toRefs } from '@vueuse/core' import { toRefs } from '@vueuse/core'
import { computed } from 'vue' import { computed } from 'vue'
import { truncate } from '~/utils/filters' import { truncate } from '~/utils/filters'
import Link from '~/components/ui/Link.vue'
import Pill from '~/components/ui/Pill.vue' import Pill from '~/components/ui/Pill.vue'
interface Props { interface Props {
actor: Actor actor: Actor | components['schemas']['APIActor']
avatar?: boolean avatar?: boolean
admin?: boolean admin?: boolean
displayName?: boolean displayName?: boolean
@ -32,7 +32,7 @@ const repr = computed(() => {
? actor.value.preferred_username ? actor.value.preferred_username
: actor.value.full_username : actor.value.full_username
return truncate(name, truncateLength.value) return truncate(name || '', truncateLength.value)
}) })
const url = computed(() => { const url = computed(() => {

View File

@ -1,4 +1,5 @@
import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types' import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types'
import type { components } from '~/generated/types'
import type { ContentFilter } from '~/store/moderation' import type { ContentFilter } from '~/store/moderation'
import { useCurrentElement } from '@vueuse/core' import { useCurrentElement } from '@vueuse/core'
@ -15,12 +16,12 @@ export interface PlayOptionsProps {
isPlayable?: boolean isPlayable?: boolean
tracks?: Track[] tracks?: Track[]
track?: Track | null track?: Track | null
artist?: Artist | null artist?: Artist | components["schemas"]["SimpleChannelArtist"] | components['schemas']['ArtistWithAlbums'] | null
album?: Album | null album?: Album | null
playlist?: Playlist | null playlist?: Playlist | null
library?: Library | null library?: Library | null
channel?: Channel | null channel?: Channel | null
account?: Actor | null account?: Actor | components['schemas']['APIActor'] | null
} }
export default (props: PlayOptionsProps) => { export default (props: PlayOptionsProps) => {
@ -36,8 +37,12 @@ export default (props: PlayOptionsProps) => {
if (props.track) { if (props.track) {
return props.track.uploads?.length > 0 return props.track.uploads?.length > 0
} else if (props.artist) { } else if (props.artist) {
// TODO: Find out how to get tracks, album from Artist
/*
return props.artist.tracks_count > 0 return props.artist.tracks_count > 0
|| props.artist?.albums?.some((album) => album.is_playable === true) || props.artist?.albums?.some((album) => album.is_playable === true)
*/
} else if (props.tracks) { } else if (props.tracks) {
return props.tracks?.some((track) => (track.uploads?.length ?? 0) > 0) return props.tracks?.some((track) => (track.uploads?.length ?? 0) > 0)
} }

View File

@ -1,4 +1,5 @@
import type { Track, Artist, Album, Playlist, Library, Channel, Actor, ArtistCredit } from '~/types' import type { Track, Artist, Album, Playlist, Library, Channel, Actor, ArtistCredit } from '~/types'
import type { components } from '~/generated/types'
import { i18n } from '~/init/locale' import { i18n } from '~/init/locale'
@ -8,11 +9,11 @@ const { t } = i18n.global
interface Objects { interface Objects {
track?: Track | null track?: Track | null
album?: Album | null album?: Album | components['schemas']['TrackAlbum'] | null
artist?: Artist | null artist?: Artist | components['schemas']['ArtistWithAlbums'] | components["schemas"]["SimpleChannelArtist"] | null
artistCredit?: ArtistCredit[] | null artistCredit?: ArtistCredit[] | null
playlist?: Playlist | null playlist?: Playlist | null
account?: Actor | null account?: Actor | components['schemas']['APIActor'] | null
library?: Library | null library?: Library | null
channel?: Channel | null channel?: Channel | null
} }