fix(front): [WIP] use generated types to make the CI (`lint:tsc`) happy
This commit is contained in:
parent
a5098c0952
commit
864ab4a758
|
@ -18,7 +18,6 @@ import { useI18n } from 'vue-i18n'
|
||||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||||
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
||||||
import PlayerControls from '~/components/audio/PlayerControls.vue'
|
import PlayerControls from '~/components/audio/PlayerControls.vue'
|
||||||
import ActorLink from '~/components/audio/ArtistCreditLabel.vue'
|
|
||||||
|
|
||||||
import VirtualList from '~/components/vui/list/VirtualList.vue'
|
import VirtualList from '~/components/vui/list/VirtualList.vue'
|
||||||
import QueueItem from '~/components/QueueItem.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 Spacer from '~/components/ui/Spacer.vue'
|
||||||
import Link from '~/components/ui/Link.vue'
|
import Link from '~/components/ui/Link.vue'
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
import Pill from '~/components/ui/Pill.vue'
|
|
||||||
import ArtistCreditLabel from '~/components/audio/ArtistCreditLabel.vue'
|
import ArtistCreditLabel from '~/components/audio/ArtistCreditLabel.vue'
|
||||||
|
|
||||||
const MilkDrop = defineAsyncComponent(() => import('~/components/audio/visualizer/MilkDrop.vue'))
|
const MilkDrop = defineAsyncComponent(() => import('~/components/audio/visualizer/MilkDrop.vue'))
|
||||||
|
@ -299,7 +297,8 @@ if (!isWebGLSupported) {
|
||||||
</h2>
|
</h2>
|
||||||
<span>
|
<span>
|
||||||
<ArtistCreditLabel
|
<ArtistCreditLabel
|
||||||
:artist-credit="currentTrack.artistCredit || undefined"
|
v-if="currentTrack.artistCredit"
|
||||||
|
:artist-credit="currentTrack.artistCredit"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -24,6 +24,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
values: null
|
values: null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: check if `position: 0` is a good default
|
||||||
const newValues = reactive<Values>({
|
const newValues = reactive<Values>({
|
||||||
position: 0,
|
position: 0,
|
||||||
description: '',
|
description: '',
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import axios from 'axios'
|
import { useVModel, useTextareaAutosize, syncRef } from '@vueuse/core'
|
||||||
import { useVModel, watchDebounced, useTextareaAutosize, syncRef } from '@vueuse/core'
|
import { computed } from 'vue'
|
||||||
import { ref, computed, watchEffect, onMounted, nextTick, watch } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import useLogger from '~/composables/useLogger'
|
|
||||||
|
|
||||||
import Textarea from '~/components/ui/Textarea.vue'
|
import Textarea from '~/components/ui/Textarea.vue'
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
|
@ -30,17 +27,11 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
required: false
|
required: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const logger = useLogger()
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { textarea, input } = useTextareaAutosize()
|
const { textarea, input } = useTextareaAutosize()
|
||||||
const value = useVModel(props, 'modelValue', emit)
|
const value = useVModel(props, 'modelValue', emit)
|
||||||
syncRef(value, input)
|
syncRef(value, input)
|
||||||
|
|
||||||
const isPreviewing = ref(false)
|
|
||||||
const preview = ref()
|
|
||||||
const isLoadingPreview = ref(false)
|
|
||||||
|
|
||||||
const labels = computed(() => ({
|
const labels = computed(() => ({
|
||||||
placeholder: props.placeholder ?? t('components.common.ContentForm.placeholder.input')
|
placeholder: props.placeholder ?? t('components.common.ContentForm.placeholder.input')
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -28,11 +28,6 @@ const labels = computed(() => ({
|
||||||
searchPlaceholder: t('components.common.InlineSearchBar.placeholder.search'),
|
searchPlaceholder: t('components.common.InlineSearchBar.placeholder.search'),
|
||||||
clear: t('components.common.InlineSearchBar.button.clear')
|
clear: t('components.common.InlineSearchBar.button.clear')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const search = () => {
|
|
||||||
value.value = ''
|
|
||||||
emit('search', value.value)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useClipboard, useVModel } from '@vueuse/core'
|
import { useClipboard, useVModel } from '@vueuse/core'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
import Button from '~/components/ui/Button.vue'
|
|
||||||
import Input from '~/components/ui/Input.vue'
|
import Input from '~/components/ui/Input.vue'
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
|
|
|
@ -68,7 +68,6 @@ const labels = computed(() => ({
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isShuffled,
|
|
||||||
shuffle
|
shuffle
|
||||||
} = useQueue()
|
} = useQueue()
|
||||||
|
|
||||||
|
@ -221,7 +220,7 @@ const remove = async () => {
|
||||||
:is-playable="object.is_playable"
|
:is-playable="object.is_playable"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="object.tracks.length > 2"
|
v-if="object?.tracks?.length && object?.tracks?.length > 2"
|
||||||
primary
|
primary
|
||||||
icon="bi-shuffle"
|
icon="bi-shuffle"
|
||||||
:aria-label="labels.shuffle"
|
:aria-label="labels.shuffle"
|
||||||
|
@ -232,8 +231,11 @@ const remove = async () => {
|
||||||
<DangerousButton
|
<DangerousButton
|
||||||
v-if="artistCredit[0] &&
|
v-if="artistCredit[0] &&
|
||||||
store.state.auth.authenticated &&
|
store.state.auth.authenticated &&
|
||||||
artistCredit[0].artist.channel &&
|
artistCredit[0].artist.channel
|
||||||
artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername"
|
/* 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"
|
:is-loading="isLoading"
|
||||||
icon="bi-trash"
|
icon="bi-trash"
|
||||||
@confirm="remove()"
|
@confirm="remove()"
|
||||||
|
|
|
@ -34,22 +34,20 @@ const { t } = useI18n()
|
||||||
const getDiscKey = (disc: Track[]) => disc?.map(track => track.id).join('|') ?? ''
|
const getDiscKey = (disc: Track[]) => disc?.map(track => track.id).join('|') ?? ''
|
||||||
const page = ref(1)
|
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)
|
acc.add(track.disc_number)
|
||||||
return acc
|
return acc
|
||||||
}, new Set()).size)
|
}, new Set()).size)
|
||||||
|
|
||||||
const discs = computed(() => props.object.tracks
|
const discs = computed(() => props.object?.tracks?.reduce((acc: Track[][], track: Track) => {
|
||||||
.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)
|
||||||
acc[discNumber].push(track)
|
return acc
|
||||||
return acc
|
}, Array(discCount.value).fill(undefined).map(() => [])))
|
||||||
}, 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) => {
|
.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)
|
acc[discNumber].push(track)
|
||||||
return acc
|
return acc
|
||||||
}, Array(discCount.value).fill(undefined).map(() => []))
|
}, Array(discCount.value).fill(undefined).map(() => []))
|
||||||
|
@ -72,9 +70,10 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
|
||||||
|
|
||||||
<channel-entries
|
<channel-entries
|
||||||
v-if="artistCredit && artistCredit[0].artist.channel && isSerie"
|
v-if="artistCredit && artistCredit[0].artist.channel && isSerie"
|
||||||
|
:default-cover="null"
|
||||||
:is-podcast="isSerie"
|
:is-podcast="isSerie"
|
||||||
:limit="50"
|
: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" />
|
<Loader v-if="isLoadingTracks" />
|
||||||
|
@ -85,7 +84,7 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
|
||||||
style="margin-top: -16px;"
|
style="margin-top: -16px;"
|
||||||
:tags="object.tags"
|
:tags="object.tags"
|
||||||
/>
|
/>
|
||||||
<template v-if="discCount > 1">
|
<template v-if="(discCount || 0) > 1">
|
||||||
<div
|
<div
|
||||||
v-for="tracks, index in paginatedDiscs"
|
v-for="tracks, index in paginatedDiscs"
|
||||||
:key="index + getDiscKey(tracks)"
|
:key="index + getDiscKey(tracks)"
|
||||||
|
@ -94,10 +93,10 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
|
||||||
<div class="ui hidden divider" />
|
<div class="ui hidden divider" />
|
||||||
<PlayButton
|
<PlayButton
|
||||||
class="right floated mini inverted vibrant"
|
class="right floated mini inverted vibrant"
|
||||||
:tracks="discs[index]"
|
:tracks="discs ? discs[index] : []"
|
||||||
/>
|
/>
|
||||||
<h3>
|
<h3>
|
||||||
{{ t('components.library.AlbumDetail.meta.volume', {number: tracks[0].disc_number}) }}
|
{{ t('components.library.AlbumDetail.meta.volume', { number: tracks[0].disc_number }) }}
|
||||||
</h3>
|
</h3>
|
||||||
<track-table
|
<track-table
|
||||||
:is-album="true"
|
:is-album="true"
|
||||||
|
@ -111,7 +110,7 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else-if="object.tracks">
|
||||||
<track-table
|
<track-table
|
||||||
:is-album="true"
|
:is-album="true"
|
||||||
:tracks="object.tracks"
|
:tracks="object.tracks"
|
||||||
|
|
|
@ -132,8 +132,14 @@ const open = ref(false)
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-if="artistCredit[0] &&
|
v-if="artistCredit[0] &&
|
||||||
store.state.auth.authenticated &&
|
store.state.auth.authenticated &&
|
||||||
artistCredit[0].artist.channel &&
|
artistCredit[0].artist.channel
|
||||||
artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername"
|
/*
|
||||||
|
|
||||||
|
TODO: Re-implement what was the intention behind the following line:
|
||||||
|
|
||||||
|
&& artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername
|
||||||
|
|
||||||
|
*/"
|
||||||
>
|
>
|
||||||
<DangerousButton
|
<DangerousButton
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
|
@ -148,8 +154,14 @@ const open = ref(false)
|
||||||
|
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-for="obj in getReportableObjects({
|
v-for="obj in getReportableObjects({
|
||||||
album: object,
|
album: object
|
||||||
channel: artistCredit[0]?.artist.channel
|
/*
|
||||||
|
|
||||||
|
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"
|
:key="obj.target.type + obj.target.id"
|
||||||
icon="bi-flag"
|
icon="bi-flag"
|
||||||
|
|
|
@ -1,22 +1,20 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { OrderingProps } from '~/composables/navigation/useOrdering'
|
import type { OrderingProps } from '~/composables/navigation/useOrdering'
|
||||||
import type { Album, PaginatedAlbumList } from '~/types'
|
import type { PaginatedAlbumList } from '~/types'
|
||||||
import { type operations } from '~/generated/types.ts'
|
import type { operations } from '~/generated/types.ts'
|
||||||
import type { RouteRecordName } from 'vue-router'
|
import type { RouteRecordName } from 'vue-router'
|
||||||
import type { OrderingField } from '~/store/ui'
|
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 { useRouteQuery } from '@vueuse/router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { syncRef } from '@vueuse/core'
|
import { syncRef } from '@vueuse/core'
|
||||||
import { sortedUniq } from 'lodash-es'
|
import { sortedUniq } from 'lodash-es'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
import { useModal } from '~/ui/composables/useModal.ts'
|
import { useModal } from '~/ui/composables/useModal.ts'
|
||||||
import { useTags } from '~/ui/composables/useTags.ts'
|
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import TagsSelector from '~/components/library/TagsSelector.vue'
|
|
||||||
import Pagination from '~/components/ui/Pagination.vue'
|
import Pagination from '~/components/ui/Pagination.vue'
|
||||||
import Card from '~/components/ui/Card.vue'
|
import Card from '~/components/ui/Card.vue'
|
||||||
import AlbumCard from '~/components/album/Card.vue'
|
import AlbumCard from '~/components/album/Card.vue'
|
||||||
|
|
|
@ -49,17 +49,21 @@ const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const domain = computed(() => getDomain(object.value?.fid ?? ''))
|
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 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 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 discogsUrl = computed(() => `https://discogs.com/search/?type=artist&title=${encodeURI(object.value?.name ?? '')}`)
|
||||||
const publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
|
const publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
|
||||||
|
|
||||||
|
// 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 cover = computed(() => {
|
||||||
const artistCover: Cover | undefined = object.value?.cover
|
const artistCover = object.value?.cover
|
||||||
|
|
||||||
const albumCover: Cover | undefined = object.value?.albums
|
// const albumCover: Cover | null = object.value?.albums
|
||||||
.find(album => album.cover?.urls.large_square_crop)?.cover
|
// .find(album => album.cover?.urls.large_square_crop)?.cover
|
||||||
|
|
||||||
const trackCover = tracks.value?.find(
|
const trackCover = tracks.value?.find(
|
||||||
(track: Track) => track.cover
|
(track: Track) => track.cover
|
||||||
|
@ -67,15 +71,19 @@ const cover = computed(() => {
|
||||||
|
|
||||||
const fallback : Cover = {
|
const fallback : Cover = {
|
||||||
uuid: '',
|
uuid: '',
|
||||||
|
mimetype: 'jpeg',
|
||||||
|
creation_date: '',
|
||||||
|
size: 0,
|
||||||
urls: {
|
urls: {
|
||||||
original: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
|
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`,
|
medium_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
|
||||||
large_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`
|
large_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return artistCover
|
return artistCover
|
||||||
|| albumCover
|
// || albumCover
|
||||||
|| trackCover
|
|| trackCover
|
||||||
|| fallback
|
|| fallback
|
||||||
})
|
})
|
||||||
|
@ -182,7 +190,7 @@ watch(() => props.id, fetchData, { immediate: true })
|
||||||
|
|
||||||
<template #items>
|
<template #items>
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-if="domain != store.getters['instance/domain']"
|
v-if="object.fid && domain != store.getters['instance/domain']"
|
||||||
:to="object.fid"
|
:to="object.fid"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
icon="bi-box-arrow-up-right"
|
icon="bi-box-arrow-up-right"
|
||||||
|
|
|
@ -100,11 +100,9 @@ const loadMoreAlbums = async () => {
|
||||||
<Loader v-if="isLoadingAlbums" />
|
<Loader v-if="isLoadingAlbums" />
|
||||||
<template v-else-if="albums && albums.length > 0">
|
<template v-else-if="albums && albums.length > 0">
|
||||||
<Heading
|
<Heading
|
||||||
h2
|
:h2="t('components.library.ArtistDetail.header.album')"
|
||||||
section-heading
|
section-heading
|
||||||
>
|
/>
|
||||||
{{ t('components.library.ArtistDetail.header.album') }}
|
|
||||||
</Heading>
|
|
||||||
<Layout flex>
|
<Layout flex>
|
||||||
<album-card
|
<album-card
|
||||||
v-for="album in allAlbums"
|
v-for="album in allAlbums"
|
||||||
|
@ -127,11 +125,9 @@ const loadMoreAlbums = async () => {
|
||||||
</template>
|
</template>
|
||||||
<template v-if="tracks.length > 0">
|
<template v-if="tracks.length > 0">
|
||||||
<Heading
|
<Heading
|
||||||
h2
|
:h2="t('components.library.ArtistDetail.header.track')"
|
||||||
section-heading
|
section-heading
|
||||||
>
|
/>
|
||||||
{{ t('components.library.ArtistDetail.header.track') }}
|
|
||||||
</Heading>
|
|
||||||
<TrackTable
|
<TrackTable
|
||||||
:is-artist="true"
|
:is-artist="true"
|
||||||
:show-position="false"
|
:show-position="false"
|
||||||
|
@ -140,11 +136,9 @@ const loadMoreAlbums = async () => {
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<Heading
|
<Heading
|
||||||
h2
|
:h2="t('components.library.ArtistDetail.header.library')"
|
||||||
section-heading
|
section-heading
|
||||||
>
|
/>
|
||||||
{{ t('components.library.ArtistDetail.header.library') }}
|
|
||||||
</Heading>
|
|
||||||
<LibraryWidget
|
<LibraryWidget
|
||||||
:url="'artists/' + object.id + '/libraries/'"
|
:url="'artists/' + object.id + '/libraries/'"
|
||||||
@loaded="emit('libraries-loaded', $event)"
|
@loaded="emit('libraries-loaded', $event)"
|
||||||
|
|
|
@ -41,10 +41,11 @@ const canEdit = store.state.auth.availablePermissions.library
|
||||||
>
|
>
|
||||||
{{ t('components.library.ArtistEdit.message.remote') }}
|
{{ t('components.library.ArtistEdit.message.remote') }}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
<!-- TODO: Check if we need to load the corresponding Actor (field: `attributed_to`) -->
|
||||||
<edit-form
|
<edit-form
|
||||||
v-else
|
v-else
|
||||||
:object-type="objectType"
|
:object-type="objectType"
|
||||||
:object="object"
|
:object="{ ...object, attributed_to: undefined }"
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -160,10 +160,10 @@ const approve = async (approved: boolean) => {
|
||||||
|
|
||||||
const alertProps = computed(() => {
|
const alertProps = computed(() => {
|
||||||
return {
|
return {
|
||||||
green: props.obj.is_approved && props.obj.is_applied,
|
green: props.obj.is_approved && props.obj.is_applied || undefined,
|
||||||
red: props.obj.is_approved === false,
|
red: props.obj.is_approved === false || undefined,
|
||||||
yellow: props.obj.is_applied === false
|
yellow: props.obj.is_applied === false || undefined
|
||||||
}
|
} as const
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -177,8 +177,8 @@ const alertProps = computed(() => {
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="obj.target && obj.target.type === 'track'"
|
v-if="obj.target && obj.target.type === 'track'"
|
||||||
:to="{name: 'library.tracks.detail', params: {id: obj.target.id }}"
|
:to="{ name: 'library.tracks.detail', params: { id: obj.target.id } }"
|
||||||
:class="isInteractive"
|
:class="/* TODO: find out: what is isInteractive? */ undefined"
|
||||||
>
|
>
|
||||||
<i class="bi bi-file-music-fill" />
|
<i class="bi bi-file-music-fill" />
|
||||||
{{ t('components.library.EditCard.link.track', {id: obj.target.id, name: obj.target.repr}) }}
|
{{ t('components.library.EditCard.link.track', {id: obj.target.id, name: obj.target.repr}) }}
|
||||||
|
|
|
@ -47,7 +47,6 @@ const { t } = useI18n()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
const upload = ref()
|
const upload = ref()
|
||||||
const currentTab = ref('uploads')
|
|
||||||
const supportedExtensions = computed(() => store.state.ui.supportedExtensions)
|
const supportedExtensions = computed(() => store.state.ui.supportedExtensions)
|
||||||
|
|
||||||
const labels = computed(() => ({
|
const labels = computed(() => ({
|
||||||
|
@ -82,7 +81,7 @@ const options = {
|
||||||
everyone: sharedLabels.fields.privacy_level.choices.everyone
|
everyone: sharedLabels.fields.privacy_level.choices.everyone
|
||||||
} as const satisfies Record<PrivacyLevel, string>
|
} 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>()
|
const library = ref<Library>()
|
||||||
|
|
||||||
|
@ -105,12 +104,12 @@ watch(privacyLevel, async (newValue) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get<paths['/api/v2/libraries/']['get']['responses']['200']['content']['application/json']>('libraries/', {
|
const response = await axios.get<paths['/api/v2/libraries/']['get']['responses']['200']['content']['application/json']>('libraries/', {
|
||||||
params: {
|
params: {
|
||||||
privacy_level: privacyLevel.value,
|
privacy_level: newValue,
|
||||||
scope: 'me'
|
scope: 'me'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
library.value = response.data.results.find(({ name }) => name === privacyLevel.value)
|
library.value = response.data.results.find(({ name }) => name === newValue)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
useErrorHandler(error as Error)
|
useErrorHandler(error as Error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { useVModel } from '@vueuse/core'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import Modal from '~/components/ui/Modal.vue'
|
import Modal from '~/components/ui/Modal.vue'
|
||||||
import Button from '~/components/ui/Button.vue'
|
|
||||||
|
|
||||||
interface ErrorEntry {
|
interface ErrorEntry {
|
||||||
key: string
|
key: string
|
||||||
|
|
|
@ -24,7 +24,6 @@ import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
import usePage from '~/composables/navigation/usePage'
|
import usePage from '~/composables/navigation/usePage'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useTags } from '~/ui/composables/useTags.ts'
|
|
||||||
|
|
||||||
|
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
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 paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value].sort((a, b) => a - b)))
|
||||||
|
|
||||||
const { isOpen: subscribeIsOpen, to: subscribe } = useModal('subscribe')
|
const { isOpen: subscribeIsOpen, to: subscribe } = useModal('subscribe')
|
||||||
const { isOpen: channelIsOpen, to: channel } = useModal('channel')
|
const { isOpen: channelIsOpen } = useModal('channel')
|
||||||
const { to: upload } = useModal('upload')
|
const { to: upload } = useModal('upload')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -25,14 +25,20 @@ const store = useStore()
|
||||||
const dropdown = ref()
|
const dropdown = ref()
|
||||||
|
|
||||||
watch(() => props.modelValue, (value) => {
|
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()
|
const current = $(dropdown.value).dropdown('get value').split(',').sort()
|
||||||
|
|
||||||
if (!isEqual([...value].sort(), current)) {
|
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)
|
$(dropdown.value).dropdown('set exactly', value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleUpdate = () => {
|
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(',')
|
const value = $(dropdown.value).dropdown('get value').split(',')
|
||||||
emit('update:modelValue', value)
|
emit('update:modelValue', value)
|
||||||
return value
|
return value
|
||||||
|
@ -41,6 +47,8 @@ const handleUpdate = () => {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
|
// TODO: root out jquery and all its malevolent offspring!
|
||||||
|
// @ts-expect-error drop up and down and up again
|
||||||
$(dropdown.value).dropdown({
|
$(dropdown.value).dropdown({
|
||||||
keys: { delimiter: 32 },
|
keys: { delimiter: 32 },
|
||||||
forceSelection: false,
|
forceSelection: false,
|
||||||
|
@ -49,12 +57,14 @@ onMounted(async () => {
|
||||||
preserveHTML: false,
|
preserveHTML: false,
|
||||||
apiSettings: {
|
apiSettings: {
|
||||||
url: store.getters['instance/absoluteUrl']('/api/v1/tags/?name__startswith={query}&ordering=length&page_size=5'),
|
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) {
|
beforeXHR: function (xhrObject) {
|
||||||
if (store.state.auth.oauth.accessToken) {
|
if (store.state.auth.oauth.accessToken) {
|
||||||
xhrObject.setRequestHeader('Authorization', store.getters['auth/header'])
|
xhrObject.setRequestHeader('Authorization', store.getters['auth/header'])
|
||||||
}
|
}
|
||||||
return xhrObject
|
return xhrObject
|
||||||
},
|
},
|
||||||
|
// @ts-expect-error yes, semantic-ui has a large API.
|
||||||
onResponse (response) {
|
onResponse (response) {
|
||||||
response = { results: [], ...response }
|
response = { results: [], ...response }
|
||||||
|
|
||||||
|
@ -85,6 +95,8 @@ onMounted(async () => {
|
||||||
onChange: handleUpdate
|
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)
|
$(dropdown.value).dropdown('set exactly', props.modelValue)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Track, Artist, Library } from '~/types'
|
import type { Track, Library } from '~/types'
|
||||||
import type { Operations } from '~/generated/types'
|
import type { operations, components } from '~/generated/types'
|
||||||
|
|
||||||
import { momentFormat } from '~/utils/filters'
|
import { momentFormat } from '~/utils/filters'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
@ -47,7 +47,7 @@ const props = defineProps<Props>()
|
||||||
const { report, getReportableObjects } = useReport()
|
const { report, getReportableObjects } = useReport()
|
||||||
|
|
||||||
const track = ref<Track | null>(null)
|
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 showEmbedModal = ref(false)
|
||||||
const showDeleteModal = ref(false)
|
const showDeleteModal = ref(false)
|
||||||
const libraries = ref([] as Library[])
|
const libraries = ref([] as Library[])
|
||||||
|
@ -58,8 +58,15 @@ const route = useRoute()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
const domain = computed(() => getDomain(track.value?.fid ?? ''))
|
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 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 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 ?? '')}`)
|
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
|
: url
|
||||||
})
|
})
|
||||||
|
|
||||||
const attributedToUrl = computed(() => router.resolve({
|
// TODO: Still needed?:
|
||||||
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 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)
|
const totalDuration = computed(() => track.value?.uploads?.[0]?.duration ?? 0)
|
||||||
|
|
||||||
|
@ -89,15 +98,17 @@ const labels = computed(() => ({
|
||||||
more: t('components.library.TrackBase.button.more')
|
more: t('components.library.TrackBase.button.more')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
type TrackResponse = Operations['get_tracks_2']['responses']['200']['content']['application/json']
|
// Note: Mind the singular!
|
||||||
type TrackParams = Operations['get_tracks_2']['parameters']['query']
|
|
||||||
|
|
||||||
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 = {
|
const params: TrackParams = {
|
||||||
refresh: 'true'
|
refresh: 'true'
|
||||||
// TypeScript will now show all available parameters with their types
|
// TypeScript will now show all available parameters with their types
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
@ -287,7 +298,7 @@ watch(showDeleteModal, (newValue) => {
|
||||||
v-if="artist &&
|
v-if="artist &&
|
||||||
store.state.auth.authenticated &&
|
store.state.auth.authenticated &&
|
||||||
artist.channel &&
|
artist.channel &&
|
||||||
artist.attributed_to.full_username === store.state.auth.fullUsername"
|
artist.attributed_to?.full_username === store.state.auth.fullUsername"
|
||||||
icon="bi-trash"
|
icon="bi-trash"
|
||||||
@click="showDeleteModal = true"
|
@click="showDeleteModal = true"
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,7 +4,6 @@ import type { Track, Library } from '~/types'
|
||||||
import { humanSize, momentFormat } from '~/utils/filters'
|
import { humanSize, momentFormat } from '~/utils/filters'
|
||||||
import { computed, ref, watchEffect } from 'vue'
|
import { computed, ref, watchEffect } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useStore } from '~/store'
|
|
||||||
|
|
||||||
import time from '~/utils/time'
|
import time from '~/utils/time'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<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 { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
|
|
@ -55,8 +55,11 @@ const el = useCurrentElement()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
for (const field of data.value.filter.fields) {
|
for (const field of data.value.filter.fields) {
|
||||||
|
// @ts-expect-error We threw out Semantic UI types
|
||||||
const settings: SemanticUI.DropdownSettings = {
|
const settings: SemanticUI.DropdownSettings = {
|
||||||
|
// @ts-expect-error value? any!
|
||||||
onChange (value) {
|
onChange (value) {
|
||||||
|
// @ts-expect-error dropdown? any!
|
||||||
value = $(this).dropdown('get value').split(',')
|
value = $(this).dropdown('get value').split(',')
|
||||||
|
|
||||||
if (field.type === 'list' && field.subtype === 'number') {
|
if (field.type === 'list' && field.subtype === 'number') {
|
||||||
|
@ -75,11 +78,11 @@ onMounted(() => {
|
||||||
if (field.autocomplete) {
|
if (field.autocomplete) {
|
||||||
selector += '.autocomplete'
|
selector += '.autocomplete'
|
||||||
|
|
||||||
// @ts-expect-error Semantic UI types are incomplete
|
|
||||||
settings.fields = field.autocomplete_fields
|
settings.fields = field.autocomplete_fields
|
||||||
settings.minCharacters = 1
|
settings.minCharacters = 1
|
||||||
settings.apiSettings = {
|
settings.apiSettings = {
|
||||||
url: store.getters['instance/absoluteUrl'](`${field.autocomplete}?${field.autocomplete_qs}`),
|
url: store.getters['instance/absoluteUrl'](`${field.autocomplete}?${field.autocomplete_qs}`),
|
||||||
|
// @ts-expect-error xhr? any!
|
||||||
beforeXHR (xhrObject) {
|
beforeXHR (xhrObject) {
|
||||||
if (store.state.auth.oauth.accessToken) {
|
if (store.state.auth.oauth.accessToken) {
|
||||||
xhrObject.setRequestHeader('Authorization', store.getters['auth/header'])
|
xhrObject.setRequestHeader('Authorization', store.getters['auth/header'])
|
||||||
|
@ -87,6 +90,7 @@ onMounted(() => {
|
||||||
|
|
||||||
return xhrObject
|
return xhrObject
|
||||||
},
|
},
|
||||||
|
// @ts-expect-error initialResponse? any!
|
||||||
onResponse (initialResponse) {
|
onResponse (initialResponse) {
|
||||||
return !settings.fields?.remoteValues
|
return !settings.fields?.remoteValues
|
||||||
? { results: initialResponse.results }
|
? { results: initialResponse.results }
|
||||||
|
@ -95,6 +99,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error jquery lives!
|
||||||
$(el.value).find(selector).dropdown(settings)
|
$(el.value).find(selector).dropdown(settings)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -56,7 +56,15 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
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',
|
name: 'delete',
|
||||||
label: t('components.manage.library.AlbumsTable.action.delete.label'),
|
label: t('components.manage.library.AlbumsTable.action.delete.label'),
|
||||||
|
|
|
@ -54,16 +54,14 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
const actions = computed(() => [
|
const actions = computed(() => [{
|
||||||
{
|
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
label: t('components.manage.library.ArtistsTable.action.delete.label'),
|
label: t('components.manage.library.ArtistsTable.action.delete.label'),
|
||||||
confirmationMessage: t('components.manage.library.ArtistsTable.action.delete.warning'),
|
confirmationMessage: t('components.manage.library.ArtistsTable.action.delete.warning'),
|
||||||
isDangerous: true,
|
isDangerous: true,
|
||||||
allowAll: false,
|
allowAll: false,
|
||||||
confirmColor: 'danger'
|
confirmColor: 'danger'
|
||||||
}
|
} as const])
|
||||||
])
|
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
|
|
@ -56,16 +56,14 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
const actions = computed(() => [
|
const actions = computed(() => [{
|
||||||
{
|
name: 'delete',
|
||||||
name: 'delete',
|
label: t('components.manage.library.LibrariesTable.action.delete.label'),
|
||||||
label: t('components.manage.library.LibrariesTable.action.delete.label'),
|
confirmationMessage: t('components.manage.library.LibrariesTable.action.delete.warning'),
|
||||||
confirmationMessage: t('components.manage.library.LibrariesTable.action.delete.warning'),
|
isDangerous: true,
|
||||||
isDangerous: true,
|
allowAll: false,
|
||||||
allowAll: false,
|
confirmColor: 'danger'
|
||||||
confirmColor: 'danger'
|
} as const ])
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
|
|
@ -59,16 +59,14 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
const actions = computed(() => [
|
const actions = computed(() => [{
|
||||||
{
|
name: 'delete',
|
||||||
name: 'delete',
|
label: t('components.manage.library.TagsTable.action.delete.label'),
|
||||||
label: t('components.manage.library.TagsTable.action.delete.label'),
|
confirmationMessage: t('components.manage.library.TagsTable.action.delete.warning'),
|
||||||
confirmationMessage: t('components.manage.library.TagsTable.action.delete.warning'),
|
isDangerous: true,
|
||||||
isDangerous: true,
|
allowAll: false,
|
||||||
allowAll: false,
|
confirmColor: 'danger'
|
||||||
confirmColor: 'danger'
|
} as const ])
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
|
|
@ -54,16 +54,14 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
const actions = computed(() => [
|
const actions = computed(() => [{
|
||||||
{
|
name: 'delete',
|
||||||
name: 'delete',
|
label: t('components.manage.library.TracksTable.action.delete.label'),
|
||||||
label: t('components.manage.library.TracksTable.action.delete.label'),
|
confirmationMessage: t('components.manage.library.TracksTable.action.delete.warning'),
|
||||||
confirmationMessage: t('components.manage.library.TracksTable.action.delete.warning'),
|
isDangerous: true,
|
||||||
isDangerous: true,
|
allowAll: false,
|
||||||
allowAll: false,
|
confirmColor: 'danger'
|
||||||
confirmColor: 'danger'
|
} as const ])
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
|
|
@ -232,8 +232,9 @@ const labels = computed(() => ({
|
||||||
<div>
|
<div>
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
v-model:current="page"
|
v-model:page="page"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
|
:pages="result.count"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span v-if="result && result.results.length > 0">
|
<span v-if="result && result.results.length > 0">
|
||||||
|
|
|
@ -263,7 +263,8 @@ const labels = computed(() => ({
|
||||||
<div>
|
<div>
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
v-model:current="page"
|
v-model:page="page"
|
||||||
|
v-model:pages="result.count"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
|
|
|
@ -231,7 +231,8 @@ const labels = computed(() => ({
|
||||||
<div>
|
<div>
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
v-model:current="page"
|
v-model:page="page"
|
||||||
|
v-model:pages="result.count"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -269,7 +269,8 @@ const labels = computed(() => ({
|
||||||
<div>
|
<div>
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
v-model:current="page"
|
v-model:page="page"
|
||||||
|
v-model:pages="result.count"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,9 @@ import type { Playlist } from '~/types'
|
||||||
|
|
||||||
const { t } = useI18n()
|
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 { playlist } = defineProps<{playlist: Playlist}>()
|
||||||
|
|
||||||
const covers = computed(() => playlist.album_covers
|
const covers = computed(() => playlist.album_covers
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { Playlist } from '~/types'
|
||||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||||
import defaultCover from '~/assets/audio/default-cover.png'
|
import defaultCover from '~/assets/audio/default-cover.png'
|
||||||
import { momentFormat } from '~/utils/filters'
|
import { momentFormat } from '~/utils/filters'
|
||||||
import { ref, computed, reactive } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
@ -54,9 +54,10 @@ function shuffleArray (array: string[]): string[] {
|
||||||
|
|
||||||
const randomizedColors = computed(() => shuffleArray(bgcolors.value))
|
const randomizedColors = computed(() => shuffleArray(bgcolors.value))
|
||||||
|
|
||||||
const goToPlaylist = () => {
|
// TODO: Chseck if the following function has a use
|
||||||
router.push({ name: 'library.playlists.detail', params: { id: props.playlist.id } })
|
// const goToPlaylist = () => {
|
||||||
}
|
// router.push({ name: 'library.playlists.detail', params: { id: props.playlist.id } })
|
||||||
|
// }
|
||||||
|
|
||||||
const updatedTitle = computed(() => {
|
const updatedTitle = computed(() => {
|
||||||
const date = momentFormat(new Date(props.playlist.modification_date ?? '1970-01-01'))
|
const date = momentFormat(new Date(props.playlist.modification_date ?? '1970-01-01'))
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import type { Playlist } from '~/types'
|
import type { Playlist } from '~/types'
|
||||||
|
|
||||||
import PlaylistsCard from '~/components/playlists/Card.vue'
|
import PlaylistsCard from '~/components/playlists/Card.vue'
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
playlists: Playlist[]
|
playlists: Playlist[]
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script setup lang="ts">
|
<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 { useVModels } from '@vueuse/core'
|
||||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
|
@ -16,7 +17,6 @@ import Alert from '~/components/ui/Alert.vue'
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
import Input from '~/components/ui/Input.vue'
|
import Input from '~/components/ui/Input.vue'
|
||||||
import Slider from '~/components/ui/Slider.vue'
|
import Slider from '~/components/ui/Slider.vue'
|
||||||
import Textarea from '~/components/ui/Textarea.vue'
|
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'update:playlist', value: Playlist): void
|
(e: 'update:playlist', value: Playlist): void
|
||||||
|
@ -56,10 +56,9 @@ const sharedLabels = useSharedLabels()
|
||||||
const privacyLevelChoices = {
|
const privacyLevelChoices = {
|
||||||
me: sharedLabels.fields.privacy_level.choices.me,
|
me: sharedLabels.fields.privacy_level.choices.me,
|
||||||
instance: sharedLabels.fields.privacy_level.choices.instance,
|
instance: sharedLabels.fields.privacy_level.choices.instance,
|
||||||
|
followers: sharedLabels.fields.privacy_level.choices.followers,
|
||||||
everyone: sharedLabels.fields.privacy_level.choices.everyone
|
everyone: sharedLabels.fields.privacy_level.choices.everyone
|
||||||
} as const satisfies Record<PrivacyLevel, string>
|
} as const satisfies Record<components['schemas']['PrivacyLevelEnum'], string>
|
||||||
|
|
||||||
const el = useCurrentElement()
|
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
|
|
|
@ -15,7 +15,6 @@ import { generateTrackCreditString } from '~/utils/utils'
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
import Link from '~/components/ui/Link.vue'
|
import Link from '~/components/ui/Link.vue'
|
||||||
import Alert from '~/components/ui/Alert.vue'
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
import Input from '~/components/ui/Input.vue'
|
|
||||||
import Spacer from '~/components/ui/Spacer.vue'
|
import Spacer from '~/components/ui/Spacer.vue'
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTimeAgo } from '@vueuse/core'
|
import { useTimeAgo } from '@vueuse/core'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import type { Channel } from '~/types'
|
import type { Channel } from '~/types'
|
||||||
|
|
||||||
import OptionsButton from '~/components/ui/button/Options.vue'
|
import OptionsButton from '~/components/ui/button/Options.vue'
|
||||||
|
@ -13,17 +12,12 @@ const timeAgo = useTimeAgo(new Date(podcast.artist?.modification_date ?? new Dat
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card
|
<Card
|
||||||
:title="podcast.uuid"
|
:title="podcast.uuid || '' /* TODO: This is probably not what we want as a title? */"
|
||||||
:image="podcast.artist?.cover?.urls.original"
|
:image="podcast.artist?.cover?.urls.original"
|
||||||
class="podcast-card"
|
class="podcast-card"
|
||||||
@click="navigate"
|
:to="/* TODO: where should it link? */ ''"
|
||||||
>
|
>
|
||||||
<a
|
{{ podcast.artist?.name }}
|
||||||
class="funkwhale link"
|
|
||||||
@click.stop="navigate"
|
|
||||||
>
|
|
||||||
{{ podcast.artist?.name }}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
{{ timeAgo }}
|
{{ timeAgo }}
|
||||||
|
|
|
@ -1,30 +1,27 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePastel } from '~/composables/color'
|
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||||
import { FwCard, FwPlayButton } from '~/components'
|
import type { Radio } from '~/types'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
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()
|
const { t } = useI18n()
|
||||||
|
|
||||||
interface Props {
|
const play = defineEmit<[radio: Radio]>()
|
||||||
|
const { radio, small, ...cardProps } = defineProps<{
|
||||||
radio: Radio
|
radio: Radio
|
||||||
small?: boolean
|
small?: boolean
|
||||||
}
|
} & ComponentProps<typeof Card>>()
|
||||||
|
|
||||||
const play = defineEmit<[radio: Radio]>()
|
|
||||||
const props = defineProps<Props & PastelProps>()
|
|
||||||
|
|
||||||
const pastel = usePastel(() => props.color)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<fw-card
|
<Card
|
||||||
|
v-bind="cardProps"
|
||||||
:title="radio.name"
|
:title="radio.name"
|
||||||
:class="pastel"
|
|
||||||
class="radio-card"
|
class="radio-card"
|
||||||
@click="navigate"
|
:to="/* TODO: get correct route here */ ''"
|
||||||
>
|
>
|
||||||
<template #image>
|
<template #image>
|
||||||
<div class="cover-name">
|
<div class="cover-name">
|
||||||
|
@ -32,7 +29,7 @@ const pastel = usePastel(() => props.color)
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<fw-play-button @play="play(props.radio)" />
|
<PlayButton @play="play(radio)" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!small"
|
v-if="!small"
|
||||||
|
@ -40,7 +37,7 @@ const pastel = usePastel(() => props.color)
|
||||||
>
|
>
|
||||||
{{ radio.description }}
|
{{ radio.description }}
|
||||||
</div>
|
</div>
|
||||||
</fw-card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -16,15 +16,10 @@ const { track, user } = defineProps<{ track: Track, user: User }>()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const navigate = (to: 'track' | 'artist' | 'user') =>
|
const navigate = (to: 'track' | 'user') =>
|
||||||
to === 'track'
|
to === 'track'
|
||||||
? router.push({ name: 'library.tracks.detail', params: { id: track.id } })
|
? router.push({ name: 'library.tracks.detail', params: { id: track.id } })
|
||||||
: to === 'artist'
|
: router.push({ name: 'profile.full', params: profileParams.value })
|
||||||
? router.push({ name: 'library.artists.detail', params: { id: track.artist.id } })
|
|
||||||
: router.push({ name: 'profile.full', params: profileParams.value })
|
|
||||||
|
|
||||||
const artistCredit = track.artist_credit || []
|
|
||||||
const firstArtist = artistCredit.length > 0 ? artistCredit[0].artist : null
|
|
||||||
|
|
||||||
const profileParams = computed(() => {
|
const profileParams = computed(() => {
|
||||||
const [username, domain] = user.full_username.split('@')
|
const [username, domain] = user.full_username.split('@')
|
||||||
|
@ -50,10 +45,15 @@ const profileParams = computed(() => {
|
||||||
{{ track.title }}
|
{{ track.title }}
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
|
v-for="{ artist } in track.artist_credit"
|
||||||
|
:key="artist.id"
|
||||||
class="funkwhale link artist"
|
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>
|
||||||
<a
|
<a
|
||||||
class="funkwhale link user"
|
class="funkwhale link user"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, useSlots } from 'vue'
|
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 ColorProps, type DefaultProps, type VariantProps, color, isNoColors } from '~/composables/color'
|
||||||
import { type WidthProps, width } from '~/composables/width'
|
import { type WidthProps, width } from '~/composables/width'
|
||||||
import { type AlignmentProps, align } from '~/composables/alignment'
|
import { type AlignmentProps, align } from '~/composables/alignment'
|
||||||
|
|
|
@ -6,7 +6,7 @@ import showdown from 'showdown'
|
||||||
import SanitizedHtml from './SanitizedHtml.vue'
|
import SanitizedHtml from './SanitizedHtml.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
md: string | null
|
md: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
|
@ -62,6 +62,8 @@ const props = defineProps<{
|
||||||
@click="() => expand ? expand() : collapse ? collapse() : (() => { return })()"
|
@click="() => expand ? expand() : collapse ? collapse() : (() => { return })()"
|
||||||
>
|
>
|
||||||
<slot name="topleft" />
|
<slot name="topleft" />
|
||||||
|
|
||||||
|
<!-- @vue-ignore -->
|
||||||
<Heading v-bind="props" />
|
<Heading v-bind="props" />
|
||||||
</Button>
|
</Button>
|
||||||
<i
|
<i
|
||||||
|
|
|
@ -163,9 +163,6 @@ onMounted(() => {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: var(--slider-opacity);
|
opacity: var(--slider-opacity);
|
||||||
}
|
}
|
||||||
input:focus~.range {
|
|
||||||
// focused style
|
|
||||||
}
|
|
||||||
input[type=range]::-moz-range-thumb {
|
input[type=range]::-moz-range-thumb {
|
||||||
background-color: var(--fw-primary);
|
background-color: var(--fw-primary);
|
||||||
transition: all .1s;
|
transition: all .1s;
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Button from '../Button.vue'
|
import Button from '../Button.vue'
|
||||||
|
|
||||||
interface Props {
|
defineProps<{
|
||||||
isSquare?: boolean
|
isSquare?: boolean
|
||||||
isGhost?: boolean
|
isGhost?: boolean
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
isSquare: false,
|
|
||||||
isGhost: false
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -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 type { ScopeId } from '~/composables/auth/useScopes'
|
||||||
|
|
||||||
import { i18n } from '~/init/locale'
|
import { i18n } from '~/init/locale'
|
||||||
|
@ -13,13 +14,15 @@ export default () => ({
|
||||||
choices: {
|
choices: {
|
||||||
me: t('composables.locale.useSharedLabels.fields.privacyLevel.choices.private'),
|
me: t('composables.locale.useSharedLabels.fields.privacyLevel.choices.private'),
|
||||||
instance: t('composables.locale.useSharedLabels.fields.privacyLevel.choices.instance'),
|
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')
|
everyone: t('composables.locale.useSharedLabels.fields.privacyLevel.choices.public')
|
||||||
} as Record<PrivacyLevel, string>,
|
} satisfies Record<components['schemas']['PrivacyLevelEnum'], string>,
|
||||||
shortChoices: {
|
shortChoices: {
|
||||||
me: t('composables.locale.useSharedLabels.fields.privacyLevel.shortChoices.private'),
|
me: t('composables.locale.useSharedLabels.fields.privacyLevel.shortChoices.private'),
|
||||||
instance: t('composables.locale.useSharedLabels.fields.privacyLevel.shortChoices.instance'),
|
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')
|
everyone: t('composables.locale.useSharedLabels.fields.privacyLevel.shortChoices.public')
|
||||||
} as Record<PrivacyLevel, string>
|
} satisfies Record<components['schemas']['PrivacyLevelEnum'], string>
|
||||||
},
|
},
|
||||||
import_status: {
|
import_status: {
|
||||||
label: t('composables.locale.useSharedLabels.fields.importStatus.label'),
|
label: t('composables.locale.useSharedLabels.fields.importStatus.label'),
|
||||||
|
|
|
@ -16,7 +16,7 @@ export interface EditableConfigField extends ConfigField {
|
||||||
id: EditObjectType
|
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'
|
export type EditObjectType = 'artist' | 'album' | 'track'
|
||||||
type Configs = Record<EditObjectType, { fields: (EditableConfigField|ConfigField)[] }>
|
type Configs = Record<EditObjectType, { fields: (EditableConfigField|ConfigField)[] }>
|
||||||
|
|
||||||
|
|
|
@ -42,13 +42,15 @@ const styles = {
|
||||||
...widths, ...sizes
|
...widths, ...sizes
|
||||||
} as const satisfies Record<Key, string | ((w: string) => string)>
|
} as const satisfies Record<Key, string | ((w: string) => string)>
|
||||||
|
|
||||||
const getStyle = (props: Partial<WidthProps>) => (key: Key) =>
|
const getStyle = (props: Partial<WidthProps>) => (key: Key):string =>
|
||||||
typeof styles[key] === 'function' && key in props
|
key in props
|
||||||
? styles[key](
|
? typeof styles[key] === 'function'
|
||||||
// @ts-expect-error Typescript is hard. Make the typescript compiler understand `key in props`
|
? styles[key](
|
||||||
props[key]
|
// @ts-expect-error Typescript is hard. Make the typescript compiler understand `key in props`
|
||||||
)
|
props[key]
|
||||||
: styles[key]
|
)
|
||||||
|
: styles[key] as string
|
||||||
|
: ''
|
||||||
|
|
||||||
// All keys are exclusive
|
// All keys are exclusive
|
||||||
const conflicts: Set<Key>[] = [
|
const conflicts: Set<Key>[] = [
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { parseAPIErrors } from '~/utils'
|
||||||
import { i18n } from './locale'
|
import { i18n } from './locale'
|
||||||
|
|
||||||
import createAuthRefreshInterceptor from 'axios-auth-refresh'
|
import createAuthRefreshInterceptor from 'axios-auth-refresh'
|
||||||
import moment from 'moment'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
@ -61,23 +60,31 @@ export const install: InitModule = ({ store, router }) => {
|
||||||
break
|
break
|
||||||
|
|
||||||
case 429: {
|
case 429: {
|
||||||
let message
|
let message = ''
|
||||||
|
|
||||||
|
// TODO: Find out if the following fields are still relevant
|
||||||
const rateLimitStatus: RateLimitStatus = {
|
const rateLimitStatus: RateLimitStatus = {
|
||||||
limit: error.response?.headers['x-ratelimit-limit'],
|
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'],
|
remaining: error.response?.headers['x-ratelimit-remaining'],
|
||||||
duration: error.response?.headers['x-ratelimit-duration'],
|
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'],
|
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) {
|
message = t('init.axios.rateLimitLater')
|
||||||
const tryAgain = moment().add(rateLimitStatus.availableSeconds, 's').toNow(true)
|
// if (rateLimitStatus.availableSeconds) {
|
||||||
message = t('init.axios.rateLimitDelay', { delay: tryAgain })
|
// const tryAgain = moment().add(rateLimitStatus.availableSeconds, 's').toNow(true)
|
||||||
} else {
|
// message = t('init.axios.rateLimitDelay', { delay: tryAgain })
|
||||||
message = t('init.axios.rateLimitLater')
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
error.backendErrors.push(message)
|
error.backendErrors.push(message)
|
||||||
error.isHandled = true
|
error.isHandled = true
|
||||||
|
|
|
@ -3017,6 +3017,7 @@
|
||||||
"privacyLevel": {
|
"privacyLevel": {
|
||||||
"choices": {
|
"choices": {
|
||||||
"instance": "Everyone on this instance",
|
"instance": "Everyone on this instance",
|
||||||
|
"followers": "My followers",
|
||||||
"private": "Nobody except me",
|
"private": "Nobody except me",
|
||||||
"public": "Everyone, across all instances"
|
"public": "Everyone, across all instances"
|
||||||
},
|
},
|
||||||
|
@ -3024,7 +3025,8 @@
|
||||||
"label": "Activity visibility",
|
"label": "Activity visibility",
|
||||||
"shortChoices": {
|
"shortChoices": {
|
||||||
"instance": "Instance",
|
"instance": "Instance",
|
||||||
"private": "Private",
|
"followers": "Followers",
|
||||||
|
"private": "Nobody except me",
|
||||||
"public": "Everyone"
|
"public": "Everyone"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<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 type { RadioConfig } from '~/store/radios'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { ref, watch, computed } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
|
|
|
@ -1,25 +1,18 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
|
||||||
import { useModal } from '~/ui/composables/useModal.ts'
|
import { useModal } from '~/ui/composables/useModal.ts'
|
||||||
|
|
||||||
import Modal from '~/components/ui/Modal.vue'
|
import Modal from '~/components/ui/Modal.vue'
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
import Layout from '~/components/ui/Layout.vue'
|
||||||
import Spacer from '~/components/ui/Spacer.vue'
|
import Spacer from '~/components/ui/Spacer.vue'
|
||||||
import Alert from '~/components/ui/Alert.vue'
|
|
||||||
import Card from '~/components/ui/Card.vue'
|
import Card from '~/components/ui/Card.vue'
|
||||||
|
|
||||||
import type { Actor, Channel, PrivacyLevel } from '~/types'
|
import type { 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 ChannelUpload from '~/components/channels/UploadForm.vue'
|
import ChannelUpload from '~/components/channels/UploadForm.vue'
|
||||||
import LibraryUpload from '~/components/library/FileUpload.vue'
|
import LibraryUpload from '~/components/library/FileUpload.vue'
|
||||||
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
|
@ -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 { useStore } from '~/store'
|
||||||
import type { QueueTrack } from '~/composables/audio/queue'
|
import type { QueueTrack } from '~/composables/audio/queue'
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@ export function getArtistCoverUrl (artistCredits: ArtistCredit[]): string | unde
|
||||||
return undefined
|
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') =>
|
(field: 'original' | 'small_square_crop' | 'medium_square_crop' | 'large_square_crop') =>
|
||||||
artist.cover
|
artist.cover
|
||||||
? (field in artist.cover ? artist.cover.urls[field] : null)
|
? (field in artist.cover ? artist.cover.urls[field] : null)
|
||||||
|
@ -50,5 +51,5 @@ const getSimpleArtistCover = (artist: SimpleArtist) =>
|
||||||
* @param artist: a simple artist
|
* @param artist: a simple artist
|
||||||
* @param field: the size you want
|
* @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))
|
store.getters['instance/absoluteUrl'](getSimpleArtistCover(artist)(field))
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -12,6 +13,7 @@ const props = defineProps<Props>()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await store.dispatch('auth/handleOauthCallback', props.code)
|
await store.dispatch('auth/handleOauthCallback', props.code)
|
||||||
|
|
|
@ -57,7 +57,7 @@ const isOver = computed(() => pendingUploads.length === processedUploads.value.l
|
||||||
const isSuccessfull = computed(() => pendingUploads.length === finishedUploads.value.length)
|
const isSuccessfull = computed(() => pendingUploads.length === finishedUploads.value.length)
|
||||||
|
|
||||||
watch(() => store.state.channels.latestPublication, (value) => {
|
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)
|
pendingUploads.push(...value.uploads)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Library, PrivacyLevel } from '~/types'
|
import type { Library, PrivacyLevel } from '~/types'
|
||||||
|
|
||||||
import { humanSize } from '~/utils/filters'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
@ -24,7 +23,8 @@ const { t } = useI18n()
|
||||||
|
|
||||||
const sharedLabels = useSharedLabels()
|
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()}`
|
const privacyTooltips = (level: PrivacyLevel) => `Visibility: ${sharedLabels.fields.privacy_level.choices[level].toLowerCase()}`
|
||||||
</script>
|
</script>
|
||||||
|
@ -59,10 +59,12 @@ const privacyTooltips = (level: PrivacyLevel) => `Visibility: ${sharedLabels.fie
|
||||||
</span>
|
</span>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="description">
|
<!-- TODO: Does library have a description? Its schema has not. -->
|
||||||
|
<!-- <div class="description">
|
||||||
{{ library.description }}
|
{{ library.description }}
|
||||||
<div class="ui hidden divider" />
|
<div class="ui hidden divider" />
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<i class="music icon" />
|
<i class="music icon" />
|
||||||
{{ t('views.content.libraries.Card.meta.tracks', library.uploads_count) }}
|
{{ t('views.content.libraries.Card.meta.tracks', library.uploads_count) }}
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { humanSize } from '~/utils/filters'
|
import { humanSize } from '~/utils/filters'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { computed, ref, type Ref, defineAsyncComponent } from 'vue'
|
import { computed, ref, type Ref } from 'vue'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
// LIBRARIES BEGIN
|
// LIBRARIES BEGIN
|
||||||
|
|
||||||
import type { Library, Channel } from '~/types'
|
import type { Library, Channel } from '~/types'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import LibraryForm from '../libraries/Form.vue'
|
|
||||||
import LibraryCard from '../libraries/CardUpload.vue'
|
import LibraryCard from '../libraries/CardUpload.vue'
|
||||||
import ChannelCard from '../channels/CardUpload.vue'
|
import ChannelCard from '../channels/CardUpload.vue'
|
||||||
import Quota from '../libraries/Quota.vue'
|
|
||||||
import Upload from '~/ui/pages/upload.vue'
|
import Upload from '~/ui/pages/upload.vue'
|
||||||
// import UploadModal from '~/ui/pages/upload.vue'
|
// import UploadModal from '~/ui/pages/upload.vue'
|
||||||
// const ChannelUploadModal = defineAsyncComponent(() => import('~/components/channels/UploadModal.vue'))
|
// const ChannelUploadModal = defineAsyncComponent(() => import('~/components/channels/UploadModal.vue'))
|
||||||
|
@ -25,8 +22,6 @@ import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const libraries = ref([] as Library[])
|
const libraries = ref([] as Library[])
|
||||||
const channels = ref([] as Channel[])
|
const channels = ref([] as Channel[])
|
||||||
|
|
||||||
|
@ -69,9 +64,10 @@ const fetchChannels = async () => {
|
||||||
fetchLibraries()
|
fetchLibraries()
|
||||||
fetchChannels()
|
fetchChannels()
|
||||||
|
|
||||||
const libraryCreated = (library: Library) => {
|
// TODO: Check if this is needed:
|
||||||
router.push({ name: 'library.detail', params: { id: library.uuid } })
|
// const libraryCreated = (library: Library) => {
|
||||||
}
|
// router.push({ name: 'library.detail', params: { id: library.uuid } })
|
||||||
|
// }
|
||||||
|
|
||||||
// LIBRARIES END
|
// LIBRARIES END
|
||||||
|
|
||||||
|
@ -122,9 +118,11 @@ const openModal = (object_: Library | Channel) => {
|
||||||
:title="'New Library'"
|
:title="'New Library'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- TODO: Check what value `new` should be -->
|
||||||
<library-card
|
<library-card
|
||||||
v-for="library in libraries"
|
v-for="library in libraries"
|
||||||
:key="library.uuid"
|
:key="library.uuid"
|
||||||
|
:new="false"
|
||||||
:library="library"
|
:library="library"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
import type { Library } from '~/types'
|
import type { Library } from '~/types'
|
||||||
|
|
||||||
import { onBeforeRouteLeave } from 'vue-router'
|
import { onBeforeRouteLeave } from 'vue-router'
|
||||||
|
@ -15,8 +16,10 @@ interface Props {
|
||||||
defaultImportReference?: string
|
defaultImportReference?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const privacyLevel= computed(() => props.object.privacy_level)
|
||||||
|
|
||||||
const emit = defineEmits<Events>()
|
const emit = defineEmits<Events>()
|
||||||
withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
defaultImportReference: ''
|
defaultImportReference: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -39,6 +42,7 @@ onBeforeRouteLeave((to, from, next) => {
|
||||||
<section>
|
<section>
|
||||||
<file-upload
|
<file-upload
|
||||||
ref="fileupload"
|
ref="fileupload"
|
||||||
|
v-model="privacyLevel"
|
||||||
:default-import-reference="defaultImportReference"
|
:default-import-reference="defaultImportReference"
|
||||||
:library="object"
|
:library="object"
|
||||||
@uploads-finished="emit('uploads-finished', $event)"
|
@uploads-finished="emit('uploads-finished', $event)"
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PlaylistTrack, Playlist, Library } from '~/types'
|
import type { PlaylistTrack, Playlist, Track } from '~/types'
|
||||||
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
import { momentFormat } from '~/utils/filters'
|
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
|
@ -28,9 +26,10 @@ import PlaylistDropdown from '~/components/playlists/PlaylistDropdown.vue'
|
||||||
|
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
|
||||||
interface Events {
|
// TODO: Is this event ever caught somewhere?
|
||||||
(e: 'libraries-loaded', libraries: Library[]): void
|
// interface Events {
|
||||||
}
|
// (e: 'libraries-loaded', libraries: Library[]): void
|
||||||
|
// }
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: number
|
id: number
|
||||||
|
@ -42,7 +41,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
})
|
})
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const edit = ref(props.defaultEdit)
|
const edit = ref(props.defaultEdit)
|
||||||
const playlist = ref<Playlist | null>(null)
|
const playlist = ref<Playlist | null>(null)
|
||||||
|
@ -50,7 +48,13 @@ const playlistTracks = ref<PlaylistTrack[]>([])
|
||||||
|
|
||||||
const showEmbedModal = ref(false)
|
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 { t } = useI18n()
|
||||||
const labels = computed(() => ({
|
const labels = computed(() => ({
|
||||||
|
@ -108,20 +112,25 @@ function shuffleArray (array: string[]): string[] {
|
||||||
|
|
||||||
const randomizedColors = computed(() => shuffleArray(bgcolors.value))
|
const randomizedColors = computed(() => shuffleArray(bgcolors.value))
|
||||||
|
|
||||||
const updatedTitle = computed(() => {
|
// TODO: Check if this ref is still needed
|
||||||
const date = momentFormat(new Date(playlist.value.modification_date ?? '1970-01-01'))
|
// const updatedTitle = computed(() => {
|
||||||
return t('components.audio.ChannelCard.title', { date })
|
// const date = momentFormat(new Date(playlist.value?.modification_date ?? '1970-01-01'))
|
||||||
})
|
// return t('components.audio.ChannelCard.title', { date })
|
||||||
|
// })
|
||||||
|
|
||||||
const deletePlaylist = async () => {
|
// TODO: Check if this function is still needed
|
||||||
try {
|
// const deletePlaylist = async () => {
|
||||||
await axios.delete(`playlists/${props.id}/`)
|
// try {
|
||||||
store.dispatch('playlists/fetchOwn')
|
// await axios.delete(`playlists/${props.id}/`)
|
||||||
return router.push({ path: '/library' })
|
// store.dispatch('playlists/fetchOwn')
|
||||||
} catch (error) {
|
// return router.push({ path: '/library' })
|
||||||
useErrorHandler(error as Error)
|
// } catch (error) {
|
||||||
}
|
// useErrorHandler(error as Error)
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO: Implement shuffle
|
||||||
|
const shuffle = () => {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -12,8 +12,13 @@ import VueMacros from 'unplugin-vue-macros/vite'
|
||||||
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
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)
|
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/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
envPrefix: ['VUE_', 'TAURI_', 'FUNKWHALE_SENTRY_'],
|
envPrefix: ['VUE_', 'TAURI_', 'FUNKWHALE_SENTRY_'],
|
||||||
|
|
Loading…
Reference in New Issue