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

This commit is contained in:
upsiflu 2025-04-04 10:08:46 +02:00
parent a5098c0952
commit 864ab4a758
58 changed files with 313 additions and 273 deletions

View File

@ -18,7 +18,6 @@ import { useI18n } from 'vue-i18n'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
import PlayerControls from '~/components/audio/PlayerControls.vue'
import ActorLink from '~/components/audio/ArtistCreditLabel.vue'
import VirtualList from '~/components/vui/list/VirtualList.vue'
import QueueItem from '~/components/QueueItem.vue'
@ -27,7 +26,6 @@ import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Link from '~/components/ui/Link.vue'
import Button from '~/components/ui/Button.vue'
import Pill from '~/components/ui/Pill.vue'
import ArtistCreditLabel from '~/components/audio/ArtistCreditLabel.vue'
const MilkDrop = defineAsyncComponent(() => import('~/components/audio/visualizer/MilkDrop.vue'))
@ -299,7 +297,8 @@ if (!isWebGLSupported) {
</h2>
<span>
<ArtistCreditLabel
:artist-credit="currentTrack.artistCredit || undefined"
v-if="currentTrack.artistCredit"
:artist-credit="currentTrack.artistCredit"
/>
</span>
<div

View File

@ -24,6 +24,7 @@ const props = withDefaults(defineProps<Props>(), {
values: null
})
// TODO: check if `position: 0` is a good default
const newValues = reactive<Values>({
position: 0,
description: '',

View File

@ -1,11 +1,8 @@
<script setup lang="ts">
import axios from 'axios'
import { useVModel, watchDebounced, useTextareaAutosize, syncRef } from '@vueuse/core'
import { ref, computed, watchEffect, onMounted, nextTick, watch } from 'vue'
import { useVModel, useTextareaAutosize, syncRef } from '@vueuse/core'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import useLogger from '~/composables/useLogger'
import Textarea from '~/components/ui/Textarea.vue'
interface Events {
@ -30,17 +27,11 @@ const props = withDefaults(defineProps<Props>(), {
required: false
})
const logger = useLogger()
const { t } = useI18n()
const { textarea, input } = useTextareaAutosize()
const value = useVModel(props, 'modelValue', emit)
syncRef(value, input)
const isPreviewing = ref(false)
const preview = ref()
const isLoadingPreview = ref(false)
const labels = computed(() => ({
placeholder: props.placeholder ?? t('components.common.ContentForm.placeholder.input')
}))

View File

@ -28,11 +28,6 @@ const labels = computed(() => ({
searchPlaceholder: t('components.common.InlineSearchBar.placeholder.search'),
clear: t('components.common.InlineSearchBar.button.clear')
}))
const search = () => {
value.value = ''
emit('search', value.value)
}
</script>
<template>

View File

@ -1,10 +1,9 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useClipboard, useVModel } from '@vueuse/core'
import { useStore } from '~/store'
import Button from '~/components/ui/Button.vue'
import Input from '~/components/ui/Input.vue'
interface Events {

View File

@ -68,7 +68,6 @@ const labels = computed(() => ({
}))
const {
isShuffled,
shuffle
} = useQueue()
@ -221,7 +220,7 @@ const remove = async () => {
:is-playable="object.is_playable"
/>
<Button
v-if="object.tracks.length > 2"
v-if="object?.tracks?.length && object?.tracks?.length > 2"
primary
icon="bi-shuffle"
:aria-label="labels.shuffle"
@ -232,8 +231,11 @@ const remove = async () => {
<DangerousButton
v-if="artistCredit[0] &&
store.state.auth.authenticated &&
artistCredit[0].artist.channel &&
artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername"
artistCredit[0].artist.channel
/* attributed_to is a number
TODO: Re-implement the intention behind
&& artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername
*/"
:is-loading="isLoading"
icon="bi-trash"
@confirm="remove()"

View File

@ -34,22 +34,20 @@ const { t } = useI18n()
const getDiscKey = (disc: Track[]) => disc?.map(track => track.id).join('|') ?? ''
const page = ref(1)
const discCount = computed(() => props.object.tracks.reduce((acc, track) => {
const discCount = computed(() => props.object?.tracks?.reduce((acc, track) => {
acc.add(track.disc_number)
return acc
}, new Set()).size)
const discs = computed(() => props.object.tracks
.reduce((acc: Track[][], track: Track) => {
const discNumber = track.disc_number - (props.object.tracks[0]?.disc_number ?? 1)
acc[discNumber].push(track)
return acc
}, Array(discCount.value).fill(undefined).map(() => []))
)
const discs = computed(() => props.object?.tracks?.reduce((acc: Track[][], track: Track) => {
const discNumber = track.disc_number - (props.object?.tracks?.[0]?.disc_number ?? 1)
acc[discNumber].push(track)
return acc
}, Array(discCount.value).fill(undefined).map(() => [])))
const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy * (page.value - 1), props.paginateBy * page.value)
const paginatedDiscs = computed(() => props.object?.tracks?.slice(props.paginateBy * (page.value - 1), props.paginateBy * page.value)
.reduce((acc: Track[][], track: Track) => {
const discNumber = track.disc_number - (props.object.tracks[0]?.disc_number ?? 1)
const discNumber = track.disc_number - (props.object?.tracks?.[0]?.disc_number ?? 1)
acc[discNumber].push(track)
return acc
}, Array(discCount.value).fill(undefined).map(() => []))
@ -72,9 +70,10 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
<channel-entries
v-if="artistCredit && artistCredit[0].artist.channel && isSerie"
:default-cover="null"
:is-podcast="isSerie"
:limit="50"
:filters="{channel: artistCredit[0].artist.channel.uuid, album: object.id, ordering: '-creation_date'}"
:filters="{channel: artistCredit[0].artist.channel, album: object.id, ordering: '-creation_date'}"
/>
<Loader v-if="isLoadingTracks" />
@ -85,7 +84,7 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
style="margin-top: -16px;"
:tags="object.tags"
/>
<template v-if="discCount > 1">
<template v-if="(discCount || 0) > 1">
<div
v-for="tracks, index in paginatedDiscs"
:key="index + getDiscKey(tracks)"
@ -94,10 +93,10 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
<div class="ui hidden divider" />
<PlayButton
class="right floated mini inverted vibrant"
:tracks="discs[index]"
:tracks="discs ? discs[index] : []"
/>
<h3>
{{ t('components.library.AlbumDetail.meta.volume', {number: tracks[0].disc_number}) }}
{{ t('components.library.AlbumDetail.meta.volume', { number: tracks[0].disc_number }) }}
</h3>
<track-table
:is-album="true"
@ -111,7 +110,7 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
</template>
</div>
</template>
<template v-else>
<template v-else-if="object.tracks">
<track-table
:is-album="true"
:tracks="object.tracks"

View File

@ -132,8 +132,14 @@ const open = ref(false)
<PopoverItem
v-if="artistCredit[0] &&
store.state.auth.authenticated &&
artistCredit[0].artist.channel &&
artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername"
artistCredit[0].artist.channel
/*
TODO: Re-implement what was the intention behind the following line:
&& artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername
*/"
>
<DangerousButton
:is-loading="isLoading"
@ -148,8 +154,14 @@ const open = ref(false)
<PopoverItem
v-for="obj in getReportableObjects({
album: object,
channel: artistCredit[0]?.artist.channel
album: object
/*
TODO: The type of the following field has changed to number.
Find out if we want to load the corresponding channel instead.
, channel: artistCredit[0]?.artist.channel
*/
})"
:key="obj.target.type + obj.target.id"
icon="bi-flag"

View File

@ -1,22 +1,20 @@
<script setup lang="ts">
import type { OrderingProps } from '~/composables/navigation/useOrdering'
import type { Album, PaginatedAlbumList } from '~/types'
import { type operations } from '~/generated/types.ts'
import type { PaginatedAlbumList } from '~/types'
import type { operations } from '~/generated/types.ts'
import type { RouteRecordName } from 'vue-router'
import type { OrderingField } from '~/store/ui'
import { computed, ref, watch, onMounted } from 'vue'
import { computed, ref, watch } from 'vue'
import { useRouteQuery } from '@vueuse/router'
import { useI18n } from 'vue-i18n'
import { syncRef } from '@vueuse/core'
import { sortedUniq } from 'lodash-es'
import { useStore } from '~/store'
import { useModal } from '~/ui/composables/useModal.ts'
import { useTags } from '~/ui/composables/useTags.ts'
import axios from 'axios'
import TagsSelector from '~/components/library/TagsSelector.vue'
import Pagination from '~/components/ui/Pagination.vue'
import Card from '~/components/ui/Card.vue'
import AlbumCard from '~/components/album/Card.vue'

View File

@ -49,17 +49,21 @@ const router = useRouter()
const route = useRoute()
const domain = computed(() => getDomain(object.value?.fid ?? ''))
const isPlayable = computed(() => !!object.value?.albums.some(album => album.is_playable))
// TODO: Re-implement `!!object.value?.albums.some(album => album.is_playable)` instead of `true`
const isPlayable = computed(() => true)
const wikipediaUrl = computed(() => `https://en.wikipedia.org/w/index.php?search=${encodeURI(object.value?.name ?? '')}`)
const musicbrainzUrl = computed(() => object.value?.mbid ? `https://musicbrainz.org/artist/${object.value.mbid}` : null)
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') ?? [])
// TODO: This is cover logic. We use it a lot. Should all go into a single, smart, parametrised function.
// Something like `useCover.ts`!
const cover = computed(() => {
const artistCover: Cover | undefined = object.value?.cover
const artistCover = object.value?.cover
const albumCover: Cover | undefined = object.value?.albums
.find(album => album.cover?.urls.large_square_crop)?.cover
// const albumCover: Cover | null = object.value?.albums
// .find(album => album.cover?.urls.large_square_crop)?.cover
const trackCover = tracks.value?.find(
(track: Track) => track.cover
@ -67,15 +71,19 @@ const cover = computed(() => {
const fallback : Cover = {
uuid: '',
mimetype: 'jpeg',
creation_date: '',
size: 0,
urls: {
original: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
small_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
medium_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
large_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`
}
}
return artistCover
|| albumCover
// || albumCover
|| trackCover
|| fallback
})
@ -182,7 +190,7 @@ watch(() => props.id, fetchData, { immediate: true })
<template #items>
<PopoverItem
v-if="domain != store.getters['instance/domain']"
v-if="object.fid && domain != store.getters['instance/domain']"
:to="object.fid"
target="_blank"
icon="bi-box-arrow-up-right"

View File

@ -100,11 +100,9 @@ const loadMoreAlbums = async () => {
<Loader v-if="isLoadingAlbums" />
<template v-else-if="albums && albums.length > 0">
<Heading
h2
:h2="t('components.library.ArtistDetail.header.album')"
section-heading
>
{{ t('components.library.ArtistDetail.header.album') }}
</Heading>
/>
<Layout flex>
<album-card
v-for="album in allAlbums"
@ -127,11 +125,9 @@ const loadMoreAlbums = async () => {
</template>
<template v-if="tracks.length > 0">
<Heading
h2
:h2="t('components.library.ArtistDetail.header.track')"
section-heading
>
{{ t('components.library.ArtistDetail.header.track') }}
</Heading>
/>
<TrackTable
:is-artist="true"
:show-position="false"
@ -140,11 +136,9 @@ const loadMoreAlbums = async () => {
/>
</template>
<Heading
h2
:h2="t('components.library.ArtistDetail.header.library')"
section-heading
>
{{ t('components.library.ArtistDetail.header.library') }}
</Heading>
/>
<LibraryWidget
:url="'artists/' + object.id + '/libraries/'"
@loaded="emit('libraries-loaded', $event)"

View File

@ -41,10 +41,11 @@ const canEdit = store.state.auth.availablePermissions.library
>
{{ t('components.library.ArtistEdit.message.remote') }}
</Alert>
<!-- TODO: Check if we need to load the corresponding Actor (field: `attributed_to`) -->
<edit-form
v-else
:object-type="objectType"
:object="object"
:object="{ ...object, attributed_to: undefined }"
/>
</Layout>
</template>

View File

@ -160,10 +160,10 @@ const approve = async (approved: boolean) => {
const alertProps = computed(() => {
return {
green: props.obj.is_approved && props.obj.is_applied,
red: props.obj.is_approved === false,
yellow: props.obj.is_applied === false
}
green: props.obj.is_approved && props.obj.is_applied || undefined,
red: props.obj.is_approved === false || undefined,
yellow: props.obj.is_applied === false || undefined
} as const
})
</script>
@ -177,8 +177,8 @@ const alertProps = computed(() => {
<div class="meta">
<router-link
v-if="obj.target && obj.target.type === 'track'"
:to="{name: 'library.tracks.detail', params: {id: obj.target.id }}"
:class="isInteractive"
:to="{ name: 'library.tracks.detail', params: { id: obj.target.id } }"
:class="/* TODO: find out: what is isInteractive? */ undefined"
>
<i class="bi bi-file-music-fill" />
{{ t('components.library.EditCard.link.track', {id: obj.target.id, name: obj.target.repr}) }}

View File

@ -47,7 +47,6 @@ const { t } = useI18n()
const store = useStore()
const upload = ref()
const currentTab = ref('uploads')
const supportedExtensions = computed(() => store.state.ui.supportedExtensions)
const labels = computed(() => ({
@ -82,7 +81,7 @@ const options = {
everyone: sharedLabels.fields.privacy_level.choices.everyone
} as const satisfies Record<PrivacyLevel, string>
const privacyLevel = defineModel<keyof typeof options>({ required: true })
const privacyLevel = defineModel<PrivacyLevel | undefined>({ required: true })
const library = ref<Library>()
@ -105,12 +104,12 @@ watch(privacyLevel, async (newValue) => {
try {
const response = await axios.get<paths['/api/v2/libraries/']['get']['responses']['200']['content']['application/json']>('libraries/', {
params: {
privacy_level: privacyLevel.value,
privacy_level: newValue,
scope: 'me'
}
})
library.value = response.data.results.find(({ name }) => name === privacyLevel.value)
library.value = response.data.results.find(({ name }) => name === newValue)
} catch (error) {
useErrorHandler(error as Error)
}

View File

@ -4,7 +4,6 @@ import { useVModel } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
import Modal from '~/components/ui/Modal.vue'
import Button from '~/components/ui/Button.vue'
interface ErrorEntry {
key: string

View File

@ -24,7 +24,6 @@ import useErrorHandler from '~/composables/useErrorHandler'
import usePage from '~/composables/navigation/usePage'
import useLogger from '~/composables/useLogger'
import { useRouter } from 'vue-router'
import { useTags } from '~/ui/composables/useTags.ts'
import Layout from '~/components/ui/Layout.vue'
@ -140,7 +139,7 @@ const labels = computed(() => ({
const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value].sort((a, b) => a - b)))
const { isOpen: subscribeIsOpen, to: subscribe } = useModal('subscribe')
const { isOpen: channelIsOpen, to: channel } = useModal('channel')
const { isOpen: channelIsOpen } = useModal('channel')
const { to: upload } = useModal('upload')
</script>

View File

@ -25,14 +25,20 @@ const store = useStore()
const dropdown = ref()
watch(() => props.modelValue, (value) => {
// TODO: root out jquery and all its malevolent offspring!
// @ts-expect-error drop up and down and up again
const current = $(dropdown.value).dropdown('get value').split(',').sort()
if (!isEqual([...value].sort(), current)) {
// TODO: root out jquery and all its malevolent offspring!
// @ts-expect-error drop up and down and up again
$(dropdown.value).dropdown('set exactly', value)
}
})
const handleUpdate = () => {
// TODO: root out jquery and all its malevolent offspring!
// @ts-expect-error drop up and down and up again
const value = $(dropdown.value).dropdown('get value').split(',')
emit('update:modelValue', value)
return value
@ -41,6 +47,8 @@ const handleUpdate = () => {
onMounted(async () => {
await nextTick()
// TODO: root out jquery and all its malevolent offspring!
// @ts-expect-error drop up and down and up again
$(dropdown.value).dropdown({
keys: { delimiter: 32 },
forceSelection: false,
@ -49,12 +57,14 @@ onMounted(async () => {
preserveHTML: false,
apiSettings: {
url: store.getters['instance/absoluteUrl']('/api/v1/tags/?name__startswith={query}&ordering=length&page_size=5'),
// @ts-expect-error I'm not curious to research what xhr is but I'm sure it served its purpose well
beforeXHR: function (xhrObject) {
if (store.state.auth.oauth.accessToken) {
xhrObject.setRequestHeader('Authorization', store.getters['auth/header'])
}
return xhrObject
},
// @ts-expect-error yes, semantic-ui has a large API.
onResponse (response) {
response = { results: [], ...response }
@ -85,6 +95,8 @@ onMounted(async () => {
onChange: handleUpdate
})
// TODO: root out jquery and all its malevolent offspring!
// @ts-expect-error drop up and down and up again
$(dropdown.value).dropdown('set exactly', props.modelValue)
})
</script>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { Track, Artist, Library } from '~/types'
import type { Operations } from '~/generated/types'
import type { Track, Library } from '~/types'
import type { operations, components } from '~/generated/types'
import { momentFormat } from '~/utils/filters'
import { computed, ref, watch } from 'vue'
@ -47,7 +47,7 @@ const props = defineProps<Props>()
const { report, getReportableObjects } = useReport()
const track = ref<Track | null>(null)
const artist = ref<Artist | null>(null)
const artist = ref<components['schemas']['ArtistWithAlbums'] | null>(null)
const showEmbedModal = ref(false)
const showDeleteModal = ref(false)
const libraries = ref([] as Library[])
@ -58,8 +58,15 @@ const route = useRoute()
const store = useStore()
const domain = computed(() => getDomain(track.value?.fid ?? ''))
const publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
const isEmbedable = computed(() => artist.value?.channel?.actor || publicLibraries.value.length)
// TODO: Why is nobody using the public libraries?
// const publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
// TODO: Make it make sense:
// const isEmbedable = computed(() => artist.value?.channel?.actor || publicLibraries.value.length)
const isEmbedable = computed(() => false)
const upload = computed(() => track.value?.uploads?.[0] ?? null)
const wikipediaUrl = computed(() => `https://en.wikipedia.org/w/index.php?search=${encodeURI(`${track.value?.title ?? ''} ${track.value?.artist_credit?.[0].artist?.name ?? ''}`)}`)
const discogsUrl = computed(() => `https://discogs.com/search/?type=release&title=${encodeURI(track.value?.album?.title ?? '')}&artist=${encodeURI(track.value?.artist_credit?.[0].artist?.name ?? '')}&title=${encodeURI(track.value?.title ?? '')}`)
@ -70,15 +77,17 @@ const downloadUrl = computed(() => {
: url
})
const attributedToUrl = computed(() => router.resolve({
name: 'profile.full.overview',
params: {
username: track.value?.attributed_to?.preferred_username,
domain: track.value?.attributed_to?.domain
}
})?.href)
// TODO: Still needed?:
const artistCredit = track.value?.artist_credit
// const attributedToUrl = computed(() => router.resolve({
// name: 'profile.full.overview',
// params: {
// username: track.value?.attributed_to?.preferred_username,
// domain: track.value?.attributed_to?.domain
// }
// })?.href)
// const artistCredit = track.value?.artist_credit
const totalDuration = computed(() => track.value?.uploads?.[0]?.duration ?? 0)
@ -89,15 +98,17 @@ const labels = computed(() => ({
more: t('components.library.TrackBase.button.more')
}))
type TrackResponse = Operations['get_tracks_2']['responses']['200']['content']['application/json']
type TrackParams = Operations['get_tracks_2']['parameters']['query']
// Note: Mind the singular!
type ArtistResponse = Operations['get_artists_2']['responses']['200']['content']['application/json']
type TrackResponse = operations['get_track_2']['responses']['200']['content']['application/json']
type ArtistResponse = operations['get_artist_2']['responses']['200']['content']['application/json']
/* Too bad the following is just wrong now:
const params: TrackParams = {
refresh: 'true'
// TypeScript will now show all available parameters with their types
}
*/
const isLoading = ref(false)
const fetchData = async () => {
@ -287,7 +298,7 @@ watch(showDeleteModal, (newValue) => {
v-if="artist &&
store.state.auth.authenticated &&
artist.channel &&
artist.attributed_to.full_username === store.state.auth.fullUsername"
artist.attributed_to?.full_username === store.state.auth.fullUsername"
icon="bi-trash"
@click="showDeleteModal = true"
>

View File

@ -4,7 +4,6 @@ import type { Track, Library } from '~/types'
import { humanSize, momentFormat } from '~/utils/filters'
import { computed, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import time from '~/utils/time'
import axios from 'axios'

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, reactive, watch, onMounted } from 'vue'
import { computed, ref, reactive, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'

View File

@ -55,8 +55,11 @@ const el = useCurrentElement()
onMounted(() => {
for (const field of data.value.filter.fields) {
// @ts-expect-error We threw out Semantic UI types
const settings: SemanticUI.DropdownSettings = {
// @ts-expect-error value? any!
onChange (value) {
// @ts-expect-error dropdown? any!
value = $(this).dropdown('get value').split(',')
if (field.type === 'list' && field.subtype === 'number') {
@ -75,11 +78,11 @@ onMounted(() => {
if (field.autocomplete) {
selector += '.autocomplete'
// @ts-expect-error Semantic UI types are incomplete
settings.fields = field.autocomplete_fields
settings.minCharacters = 1
settings.apiSettings = {
url: store.getters['instance/absoluteUrl'](`${field.autocomplete}?${field.autocomplete_qs}`),
// @ts-expect-error xhr? any!
beforeXHR (xhrObject) {
if (store.state.auth.oauth.accessToken) {
xhrObject.setRequestHeader('Authorization', store.getters['auth/header'])
@ -87,6 +90,7 @@ onMounted(() => {
return xhrObject
},
// @ts-expect-error initialResponse? any!
onResponse (initialResponse) {
return !settings.fields?.remoteValues
? { results: initialResponse.results }
@ -95,6 +99,7 @@ onMounted(() => {
}
}
// @ts-expect-error jquery lives!
$(el.value).find(selector).dropdown(settings)
}
})

View File

@ -56,7 +56,15 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
const { t } = useI18n()
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
const actions = computed(() => [
const actions = computed<{
name: string
label: string
isDangerous?: boolean
allowAll?: boolean
confirmColor?: 'success' | 'danger'
confirmationMessage?: string
filterChackable?: (item: any) => boolean
}[]>(() => [
{
name: 'delete',
label: t('components.manage.library.AlbumsTable.action.delete.label'),

View File

@ -54,16 +54,14 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
]
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
const actions = computed(() => [
{
const actions = computed(() => [{
name: 'delete',
label: t('components.manage.library.ArtistsTable.action.delete.label'),
confirmationMessage: t('components.manage.library.ArtistsTable.action.delete.warning'),
isDangerous: true,
allowAll: false,
confirmColor: 'danger'
}
])
} as const])
const isLoading = ref(false)
const fetchData = async () => {

View File

@ -56,16 +56,14 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
const { t } = useI18n()
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
const actions = computed(() => [
{
name: 'delete',
label: t('components.manage.library.LibrariesTable.action.delete.label'),
confirmationMessage: t('components.manage.library.LibrariesTable.action.delete.warning'),
isDangerous: true,
allowAll: false,
confirmColor: 'danger'
}
])
const actions = computed(() => [{
name: 'delete',
label: t('components.manage.library.LibrariesTable.action.delete.label'),
confirmationMessage: t('components.manage.library.LibrariesTable.action.delete.warning'),
isDangerous: true,
allowAll: false,
confirmColor: 'danger'
} as const ])
const isLoading = ref(false)
const fetchData = async () => {

View File

@ -59,16 +59,14 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
const { t } = useI18n()
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
const actions = computed(() => [
{
name: 'delete',
label: t('components.manage.library.TagsTable.action.delete.label'),
confirmationMessage: t('components.manage.library.TagsTable.action.delete.warning'),
isDangerous: true,
allowAll: false,
confirmColor: 'danger'
}
])
const actions = computed(() => [{
name: 'delete',
label: t('components.manage.library.TagsTable.action.delete.label'),
confirmationMessage: t('components.manage.library.TagsTable.action.delete.warning'),
isDangerous: true,
allowAll: false,
confirmColor: 'danger'
} as const ])
const isLoading = ref(false)
const fetchData = async () => {

View File

@ -54,16 +54,14 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
const { t } = useI18n()
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
const actions = computed(() => [
{
name: 'delete',
label: t('components.manage.library.TracksTable.action.delete.label'),
confirmationMessage: t('components.manage.library.TracksTable.action.delete.warning'),
isDangerous: true,
allowAll: false,
confirmColor: 'danger'
}
])
const actions = computed(() => [{
name: 'delete',
label: t('components.manage.library.TracksTable.action.delete.label'),
confirmationMessage: t('components.manage.library.TracksTable.action.delete.warning'),
isDangerous: true,
allowAll: false,
confirmColor: 'danger'
} as const ])
const isLoading = ref(false)
const fetchData = async () => {

View File

@ -232,8 +232,9 @@ const labels = computed(() => ({
<div>
<Pagination
v-if="result && result.count > paginateBy"
v-model:current="page"
v-model:page="page"
:paginate-by="paginateBy"
:pages="result.count"
/>
<span v-if="result && result.results.length > 0">

View File

@ -263,7 +263,8 @@ const labels = computed(() => ({
<div>
<pagination
v-if="result && result.count > paginateBy"
v-model:current="page"
v-model:page="page"
v-model:pages="result.count"
:compact="true"
:paginate-by="paginateBy"
:total="result.count"

View File

@ -231,7 +231,8 @@ const labels = computed(() => ({
<div>
<Pagination
v-if="result && result.count > paginateBy"
v-model:current="page"
v-model:page="page"
v-model:pages="result.count"
:paginate-by="paginateBy"
/>

View File

@ -269,7 +269,8 @@ const labels = computed(() => ({
<div>
<Pagination
v-if="result && result.count > paginateBy"
v-model:current="page"
v-model:page="page"
v-model:pages="result.count"
:paginate-by="paginateBy"
/>

View File

@ -10,7 +10,9 @@ import type { Playlist } from '~/types'
const { t } = useI18n()
const play = defineEmit<[playlist: Playlist]>()
// TODO: Check if the following emit is ever caught anywhere
// const play = defineEmit<[playlist: Playlist]>()
const { playlist } = defineProps<{playlist: Playlist}>()
const covers = computed(() => playlist.album_covers

View File

@ -4,7 +4,7 @@ import type { Playlist } from '~/types'
import PlayButton from '~/components/audio/PlayButton.vue'
import defaultCover from '~/assets/audio/default-cover.png'
import { momentFormat } from '~/utils/filters'
import { ref, computed, reactive } from 'vue'
import { ref, computed } from 'vue'
import { useStore } from '~/store'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
@ -54,9 +54,10 @@ function shuffleArray (array: string[]): string[] {
const randomizedColors = computed(() => shuffleArray(bgcolors.value))
const goToPlaylist = () => {
router.push({ name: 'library.playlists.detail', params: { id: props.playlist.id } })
}
// TODO: Chseck if the following function has a use
// const goToPlaylist = () => {
// router.push({ name: 'library.playlists.detail', params: { id: props.playlist.id } })
// }
const updatedTitle = computed(() => {
const date = momentFormat(new Date(props.playlist.modification_date ?? '1970-01-01'))

View File

@ -2,7 +2,6 @@
import type { Playlist } from '~/types'
import PlaylistsCard from '~/components/playlists/Card.vue'
import Layout from '~/components/ui/Layout.vue'
interface Props {
playlists: Playlist[]

View File

@ -1,8 +1,9 @@
<script setup lang="ts">
import type { Playlist, PrivacyLevel, BackendError } from '~/types'
import type { Playlist, BackendError } from '~/types'
import type { components } from '~/generated/types'
import { useVModels, useCurrentElement } from '@vueuse/core'
import { ref, computed, onMounted, nextTick } from 'vue'
import { useVModels } from '@vueuse/core'
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
@ -16,7 +17,6 @@ import Alert from '~/components/ui/Alert.vue'
import Button from '~/components/ui/Button.vue'
import Input from '~/components/ui/Input.vue'
import Slider from '~/components/ui/Slider.vue'
import Textarea from '~/components/ui/Textarea.vue'
interface Events {
(e: 'update:playlist', value: Playlist): void
@ -56,10 +56,9 @@ const sharedLabels = useSharedLabels()
const privacyLevelChoices = {
me: sharedLabels.fields.privacy_level.choices.me,
instance: sharedLabels.fields.privacy_level.choices.instance,
followers: sharedLabels.fields.privacy_level.choices.followers,
everyone: sharedLabels.fields.privacy_level.choices.everyone
} as const satisfies Record<PrivacyLevel, string>
const el = useCurrentElement()
} as const satisfies Record<components['schemas']['PrivacyLevelEnum'], string>
const isLoading = ref(false)
const submit = async () => {

View File

@ -15,7 +15,6 @@ import { generateTrackCreditString } from '~/utils/utils'
import Button from '~/components/ui/Button.vue'
import Link from '~/components/ui/Link.vue'
import Alert from '~/components/ui/Alert.vue'
import Input from '~/components/ui/Input.vue'
import Spacer from '~/components/ui/Spacer.vue'
const logger = useLogger()

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import { useTimeAgo } from '@vueuse/core'
import { useRouter } from 'vue-router'
import type { Channel } from '~/types'
import OptionsButton from '~/components/ui/button/Options.vue'
@ -13,17 +12,12 @@ const timeAgo = useTimeAgo(new Date(podcast.artist?.modification_date ?? new Dat
<template>
<Card
:title="podcast.uuid"
:title="podcast.uuid || '' /* TODO: This is probably not what we want as a title? */"
:image="podcast.artist?.cover?.urls.original"
class="podcast-card"
@click="navigate"
:to="/* TODO: where should it link? */ ''"
>
<a
class="funkwhale link"
@click.stop="navigate"
>
{{ podcast.artist?.name }}
</a>
{{ podcast.artist?.name }}
<template #footer>
{{ timeAgo }}

View File

@ -1,30 +1,27 @@
<script setup lang="ts">
import { usePastel } from '~/composables/color'
import { FwCard, FwPlayButton } from '~/components'
import { useRouter } from 'vue-router'
import type { ComponentProps } from 'vue-component-type-helpers'
import type { Radio } from '~/types'
import { useI18n } from 'vue-i18n'
import type { PastelProps } from '~/types/common-props'
import type { Radio } from '~/types/models'
import Card from '~/components/ui/Card.vue'
import PlayButton from '~/components/audio/PlayButton.vue'
const { t } = useI18n()
interface Props {
const play = defineEmit<[radio: Radio]>()
const { radio, small, ...cardProps } = defineProps<{
radio: Radio
small?: boolean
}
const play = defineEmit<[radio: Radio]>()
const props = defineProps<Props & PastelProps>()
const pastel = usePastel(() => props.color)
} & ComponentProps<typeof Card>>()
</script>
<template>
<fw-card
<Card
v-bind="cardProps"
:title="radio.name"
:class="pastel"
class="radio-card"
@click="navigate"
:to="/* TODO: get correct route here */ ''"
>
<template #image>
<div class="cover-name">
@ -32,7 +29,7 @@ const pastel = usePastel(() => props.color)
</div>
</template>
<fw-play-button @play="play(props.radio)" />
<PlayButton @play="play(radio)" />
<div
v-if="!small"
@ -40,7 +37,7 @@ const pastel = usePastel(() => props.color)
>
{{ radio.description }}
</div>
</fw-card>
</Card>
</template>
<style lang="scss">

View File

@ -16,15 +16,10 @@ const { track, user } = defineProps<{ track: Track, user: User }>()
const router = useRouter()
const navigate = (to: 'track' | 'artist' | 'user') =>
const navigate = (to: 'track' | '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
: router.push({ name: 'profile.full', params: profileParams.value })
const profileParams = computed(() => {
const [username, domain] = user.full_username.split('@')
@ -50,10 +45,15 @@ const profileParams = computed(() => {
{{ track.title }}
</div>
<a
v-for="{ artist } in track.artist_credit"
:key="artist.id"
class="funkwhale link artist"
@click.stop="navigate('artist')"
@click.stop="router.push({
name: 'library.artists.detail',
params: { id: artist.id }
})"
>
{{ firstArtist?.name /* TODO: Multi-Artist! */ }}
{{ artist.name }}
</a>
<a
class="funkwhale link user"

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, onMounted, ref, useSlots } from 'vue'
import { type RouterLinkProps, RouterLink, useLink } from 'vue-router'
import { type RouterLinkProps, RouterLink } from 'vue-router'
import { type ColorProps, type DefaultProps, type VariantProps, color, isNoColors } from '~/composables/color'
import { type WidthProps, width } from '~/composables/width'
import { type AlignmentProps, align } from '~/composables/alignment'

View File

@ -6,7 +6,7 @@ import showdown from 'showdown'
import SanitizedHtml from './SanitizedHtml.vue'
interface Props {
md: string | null
md: string
}
const props = defineProps<Props>()

View File

@ -62,6 +62,8 @@ const props = defineProps<{
@click="() => expand ? expand() : collapse ? collapse() : (() => { return })()"
>
<slot name="topleft" />
<!-- @vue-ignore -->
<Heading v-bind="props" />
</Button>
<i

View File

@ -163,9 +163,6 @@ onMounted(() => {
pointer-events: none;
opacity: var(--slider-opacity);
}
input:focus~.range {
// focused style
}
input[type=range]::-moz-range-thumb {
background-color: var(--fw-primary);
transition: all .1s;

View File

@ -1,15 +1,10 @@
<script setup lang="ts">
import Button from '../Button.vue'
interface Props {
defineProps<{
isSquare?: boolean
isGhost?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isSquare: false,
isGhost: false
})
}>()
</script>
<template>

View File

@ -1,4 +1,5 @@
import type { PrivacyLevel, ImportStatus } from '~/types'
import type { ImportStatus } from '~/types'
import type { components } from '~/generated/types'
import type { ScopeId } from '~/composables/auth/useScopes'
import { i18n } from '~/init/locale'
@ -13,13 +14,15 @@ export default () => ({
choices: {
me: t('composables.locale.useSharedLabels.fields.privacyLevel.choices.private'),
instance: t('composables.locale.useSharedLabels.fields.privacyLevel.choices.instance'),
followers: t('composables.locale.useSharedLabels.fields.privacyLevel.choices.followers'),
everyone: t('composables.locale.useSharedLabels.fields.privacyLevel.choices.public')
} as Record<PrivacyLevel, string>,
} satisfies Record<components['schemas']['PrivacyLevelEnum'], string>,
shortChoices: {
me: t('composables.locale.useSharedLabels.fields.privacyLevel.shortChoices.private'),
instance: t('composables.locale.useSharedLabels.fields.privacyLevel.shortChoices.instance'),
followers: t('composables.locale.useSharedLabels.fields.privacyLevel.choices.followers'),
everyone: t('composables.locale.useSharedLabels.fields.privacyLevel.shortChoices.public')
} as Record<PrivacyLevel, string>
} satisfies Record<components['schemas']['PrivacyLevelEnum'], string>
},
import_status: {
label: t('composables.locale.useSharedLabels.fields.importStatus.label'),

View File

@ -16,7 +16,7 @@ export interface EditableConfigField extends ConfigField {
id: EditObjectType
}
export type EditObject = (Partial<Artist> | Partial<Album> | Partial<Track>) & { attributed_to: Actor }
export type EditObject = (Partial<Artist> | Partial<Album> | Partial<Track>) & { attributed_to?: Actor }
export type EditObjectType = 'artist' | 'album' | 'track'
type Configs = Record<EditObjectType, { fields: (EditableConfigField|ConfigField)[] }>

View File

@ -42,13 +42,15 @@ const styles = {
...widths, ...sizes
} as const satisfies Record<Key, string | ((w: string) => string)>
const getStyle = (props: Partial<WidthProps>) => (key: Key) =>
typeof styles[key] === 'function' && key in props
? styles[key](
// @ts-expect-error Typescript is hard. Make the typescript compiler understand `key in props`
props[key]
)
: styles[key]
const getStyle = (props: Partial<WidthProps>) => (key: Key):string =>
key in props
? typeof styles[key] === 'function'
? styles[key](
// @ts-expect-error Typescript is hard. Make the typescript compiler understand `key in props`
props[key]
)
: styles[key] as string
: ''
// All keys are exclusive
const conflicts: Set<Key>[] = [

View File

@ -5,7 +5,6 @@ import { parseAPIErrors } from '~/utils'
import { i18n } from './locale'
import createAuthRefreshInterceptor from 'axios-auth-refresh'
import moment from 'moment'
import axios from 'axios'
import useLogger from '~/composables/useLogger'
@ -61,23 +60,31 @@ export const install: InitModule = ({ store, router }) => {
break
case 429: {
let message
let message = ''
// TODO: Find out if the following fields are still relevant
const rateLimitStatus: RateLimitStatus = {
limit: error.response?.headers['x-ratelimit-limit'],
scope: error.response?.headers['x-ratelimit-scope'],
// scope: error.response?.headers['x-ratelimit-scope'],
remaining: error.response?.headers['x-ratelimit-remaining'],
duration: error.response?.headers['x-ratelimit-duration'],
availableSeconds: parseInt(error.response?.headers['retry-after'] ?? '60'),
// availableSeconds: parseInt(error.response?.headers['retry-after'] ?? '60'),
reset: error.response?.headers['x-ratelimit-reset'],
resetSeconds: error.response?.headers['x-ratelimit-resetseconds']
// resetSeconds: error.response?.headers['x-ratelimit-resetseconds']
// The following fields were missing:
id: '',
rate: '',
description: '',
available: 0,
available_seconds: 0,
reset_seconds: 0
}
if (rateLimitStatus.availableSeconds) {
const tryAgain = moment().add(rateLimitStatus.availableSeconds, 's').toNow(true)
message = t('init.axios.rateLimitDelay', { delay: tryAgain })
} else {
message = t('init.axios.rateLimitLater')
}
message = t('init.axios.rateLimitLater')
// if (rateLimitStatus.availableSeconds) {
// const tryAgain = moment().add(rateLimitStatus.availableSeconds, 's').toNow(true)
// message = t('init.axios.rateLimitDelay', { delay: tryAgain })
// }
error.backendErrors.push(message)
error.isHandled = true

View File

@ -3017,6 +3017,7 @@
"privacyLevel": {
"choices": {
"instance": "Everyone on this instance",
"followers": "My followers",
"private": "Nobody except me",
"public": "Everyone, across all instances"
},
@ -3024,7 +3025,8 @@
"label": "Activity visibility",
"shortChoices": {
"instance": "Instance",
"private": "Private",
"followers": "Followers",
"private": "Nobody except me",
"public": "Everyone"
}
},

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import type { paths, components } from '~/generated/types.ts'
import type { paths } from '~/generated/types.ts'
import type { RadioConfig } from '~/store/radios'
import axios from 'axios'
import { ref, watch, computed } from 'vue'

View File

@ -1,25 +1,18 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
import { useModal } from '~/ui/composables/useModal.ts'
import Modal from '~/components/ui/Modal.vue'
import Button from '~/components/ui/Button.vue'
import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Alert from '~/components/ui/Alert.vue'
import Card from '~/components/ui/Card.vue'
import type { Actor, Channel, PrivacyLevel } from '~/types'
import type { paths } from '~/generated/types'
import axios from 'axios'
import FileUploadWidget from '~/components/library/FileUploadWidget.vue'
import type { VueUploadItem } from 'vue-upload-component'
import type { Channel, PrivacyLevel } from '~/types'
import ChannelUpload from '~/components/channels/UploadForm.vue'
import LibraryUpload from '~/components/library/FileUpload.vue'
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
const { t } = useI18n()
const store = useStore()

View File

@ -1,4 +1,5 @@
import type { Track, SimpleArtist, Album, ArtistCredit, QueueItemSource } from '~/types'
import type { Track, Album, ArtistCredit, QueueItemSource } from '~/types'
import type { components } from '~/generated/types'
import { useStore } from '~/store'
import type { QueueTrack } from '~/composables/audio/queue'
@ -39,7 +40,7 @@ export function getArtistCoverUrl (artistCredits: ArtistCredit[]): string | unde
return undefined
}
const getSimpleArtistCover = (artist: SimpleArtist) =>
const getSimpleArtistCover = (artist: components['schemas']['SimpleChannelArtist'] | components['schemas']['Artist'] | components['schemas']['ArtistWithAlbums']) =>
(field: 'original' | 'small_square_crop' | 'medium_square_crop' | 'large_square_crop') =>
artist.cover
? (field in artist.cover ? artist.cover.urls[field] : null)
@ -50,5 +51,5 @@ const getSimpleArtistCover = (artist: SimpleArtist) =>
* @param artist: a simple artist
* @param field: the size you want
*/
export const getSimpleArtistCoverUrl = (artist: SimpleArtist, field: 'original' | 'small_square_crop' | 'medium_square_crop' | 'large_square_crop') =>
export const getSimpleArtistCoverUrl = (artist: components['schemas']['SimpleChannelArtist'] | components['schemas']['Artist'] | components['schemas']['ArtistWithAlbums'], field: 'original' | 'small_square_crop' | 'medium_square_crop' | 'large_square_crop') =>
store.getters['instance/absoluteUrl'](getSimpleArtistCover(artist)(field))

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import { onMounted } from 'vue'
interface Props {
@ -12,6 +13,7 @@ const props = defineProps<Props>()
const router = useRouter()
const store = useStore()
const { t } = useI18n()
onMounted(async () => {
await store.dispatch('auth/handleOauthCallback', props.code)

View File

@ -57,7 +57,7 @@ const isOver = computed(() => pendingUploads.length === processedUploads.value.l
const isSuccessfull = computed(() => pendingUploads.length === finishedUploads.value.length)
watch(() => store.state.channels.latestPublication, (value) => {
if (value?.channel.uuid === props.object.uuid && value.uploads.length > 0) {
if (value?.channel.uuid === props.object.uuid && value?.uploads && value?.uploads.length > 0) {
pendingUploads.push(...value.uploads)
}
})

View File

@ -1,7 +1,6 @@
<script setup lang="ts">
import type { Library, PrivacyLevel } from '~/types'
import { humanSize } from '~/utils/filters'
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
@ -24,7 +23,8 @@ const { t } = useI18n()
const sharedLabels = useSharedLabels()
const sizeLabel = computed(() => t('views.content.libraries.Card.label.size'))
// TODO: Check if this is still needed:
// const sizeLabel = computed(() => t('views.content.libraries.Card.label.size'))
const privacyTooltips = (level: PrivacyLevel) => `Visibility: ${sharedLabels.fields.privacy_level.choices[level].toLowerCase()}`
</script>
@ -59,10 +59,12 @@ const privacyTooltips = (level: PrivacyLevel) => `Visibility: ${sharedLabels.fie
</span>
</h4>
<div class="description">
<!-- TODO: Does library have a description? Its schema has not. -->
<!-- <div class="description">
{{ library.description }}
<div class="ui hidden divider" />
</div>
</div> -->
<div class="content">
<i class="music icon" />
{{ t('views.content.libraries.Card.meta.tracks', library.uploads_count) }}

View File

@ -1,20 +1,17 @@
<script setup lang="ts">
import { humanSize } from '~/utils/filters'
import { useI18n } from 'vue-i18n'
import { computed, ref, type Ref, defineAsyncComponent } from 'vue'
import { computed, ref, type Ref } from 'vue'
import { useStore } from '~/store'
// LIBRARIES BEGIN
import type { Library, Channel } from '~/types'
import { useRouter } from 'vue-router'
import axios from 'axios'
import LibraryForm from '../libraries/Form.vue'
import LibraryCard from '../libraries/CardUpload.vue'
import ChannelCard from '../channels/CardUpload.vue'
import Quota from '../libraries/Quota.vue'
import Upload from '~/ui/pages/upload.vue'
// import UploadModal from '~/ui/pages/upload.vue'
// const ChannelUploadModal = defineAsyncComponent(() => import('~/components/channels/UploadModal.vue'))
@ -25,8 +22,6 @@ import useErrorHandler from '~/composables/useErrorHandler'
const { t } = useI18n()
const router = useRouter()
const libraries = ref([] as Library[])
const channels = ref([] as Channel[])
@ -69,9 +64,10 @@ const fetchChannels = async () => {
fetchLibraries()
fetchChannels()
const libraryCreated = (library: Library) => {
router.push({ name: 'library.detail', params: { id: library.uuid } })
}
// TODO: Check if this is needed:
// const libraryCreated = (library: Library) => {
// router.push({ name: 'library.detail', params: { id: library.uuid } })
// }
// LIBRARIES END
@ -122,9 +118,11 @@ const openModal = (object_: Library | Channel) => {
:title="'New Library'"
/>
<!-- TODO: Check what value `new` should be -->
<library-card
v-for="library in libraries"
:key="library.uuid"
:new="false"
:library="library"
/>

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { Library } from '~/types'
import { onBeforeRouteLeave } from 'vue-router'
@ -15,8 +16,10 @@ interface Props {
defaultImportReference?: string
}
const privacyLevel= computed(() => props.object.privacy_level)
const emit = defineEmits<Events>()
withDefaults(defineProps<Props>(), {
const props = withDefaults(defineProps<Props>(), {
defaultImportReference: ''
})
@ -39,6 +42,7 @@ onBeforeRouteLeave((to, from, next) => {
<section>
<file-upload
ref="fileupload"
v-model="privacyLevel"
:default-import-reference="defaultImportReference"
:library="object"
@uploads-finished="emit('uploads-finished', $event)"

View File

@ -1,11 +1,9 @@
<script setup lang="ts">
import type { PlaylistTrack, Playlist, Library } from '~/types'
import type { PlaylistTrack, Playlist, Track } from '~/types'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { ref, computed } from 'vue'
import { useStore } from '~/store'
import { momentFormat } from '~/utils/filters'
import axios from 'axios'
@ -28,9 +26,10 @@ import PlaylistDropdown from '~/components/playlists/PlaylistDropdown.vue'
import useErrorHandler from '~/composables/useErrorHandler'
interface Events {
(e: 'libraries-loaded', libraries: Library[]): void
}
// TODO: Is this event ever caught somewhere?
// interface Events {
// (e: 'libraries-loaded', libraries: Library[]): void
// }
interface Props {
id: number
@ -42,7 +41,6 @@ const props = withDefaults(defineProps<Props>(), {
})
const store = useStore()
const router = useRouter()
const edit = ref(props.defaultEdit)
const playlist = ref<Playlist | null>(null)
@ -50,7 +48,13 @@ const playlistTracks = ref<PlaylistTrack[]>([])
const showEmbedModal = ref(false)
const tracks = computed(() => playlistTracks.value.map(({ track }, index) => ({ ...track, position: index + 1 })))
// TODO: Compute `tracks` with `track`. In the new types, `track` is just a string.
// We probably have to load each track before loading it into `tracks`.
// Original line: const tracks = computed(() => playlistTracks.value.map(({ track }, index) => ({ ...track, position: index + 1 })))
const tracks = computed(() => playlistTracks.value.map(({ track }, index) => (
// @i-would-expect-ts-to-error because this typecasting is evil
{ position: index + 1 } as Track
)))
const { t } = useI18n()
const labels = computed(() => ({
@ -108,20 +112,25 @@ function shuffleArray (array: string[]): string[] {
const randomizedColors = computed(() => shuffleArray(bgcolors.value))
const updatedTitle = computed(() => {
const date = momentFormat(new Date(playlist.value.modification_date ?? '1970-01-01'))
return t('components.audio.ChannelCard.title', { date })
})
// TODO: Check if this ref is still needed
// const updatedTitle = computed(() => {
// const date = momentFormat(new Date(playlist.value?.modification_date ?? '1970-01-01'))
// return t('components.audio.ChannelCard.title', { date })
// })
const deletePlaylist = async () => {
try {
await axios.delete(`playlists/${props.id}/`)
store.dispatch('playlists/fetchOwn')
return router.push({ path: '/library' })
} catch (error) {
useErrorHandler(error as Error)
}
}
// TODO: Check if this function is still needed
// const deletePlaylist = async () => {
// try {
// await axios.delete(`playlists/${props.id}/`)
// store.dispatch('playlists/fetchOwn')
// return router.push({ path: '/library' })
// } catch (error) {
// useErrorHandler(error as Error)
// }
// }
// TODO: Implement shuffle
const shuffle = () => {}
</script>
<template>

View File

@ -12,8 +12,13 @@ import VueMacros from 'unplugin-vue-macros/vite'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import vueDevTools from 'vite-plugin-vue-devtools'
// We don't use port but, magically, it is necessary to set it here.
const port = +(process.env.VUE_PORT ?? 8080)
// To prevent a linter warning, here is a partial Haiku:
export const exPort = port
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
envPrefix: ['VUE_', 'TAURI_', 'FUNKWHALE_SENTRY_'],