kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
Allow displaying multiple same tracks in track list
Well, there was some error with `@mouseleave` not firing in some cases for some weird reason, so I decided to handle the `hover` prop in the containerenvironments/review-front-deve-otr6gc/deployments/13419
rodzic
3e5a772027
commit
a37835a9c2
|
@ -9,10 +9,9 @@ import PlayButton from '~/components/audio/PlayButton.vue'
|
||||||
import usePlayOptions from '~/composables/audio/usePlayOptions'
|
import usePlayOptions from '~/composables/audio/usePlayOptions'
|
||||||
import useQueue from '~/composables/audio/useQueue'
|
import useQueue from '~/composables/audio/useQueue'
|
||||||
import usePlayer from '~/composables/audio/usePlayer'
|
import usePlayer from '~/composables/audio/usePlayer'
|
||||||
import { ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
interface Props extends PlayOptionsProps {
|
interface Props extends PlayOptionsProps {
|
||||||
tracks: Track[]
|
|
||||||
track: Track
|
track: Track
|
||||||
index: number
|
index: number
|
||||||
|
|
||||||
|
@ -23,7 +22,10 @@ interface Props extends PlayOptionsProps {
|
||||||
showPosition?: boolean
|
showPosition?: boolean
|
||||||
displayActions?: boolean
|
displayActions?: boolean
|
||||||
|
|
||||||
|
hover: boolean
|
||||||
|
|
||||||
// TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
|
// TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
|
||||||
|
tracks: Track[]
|
||||||
isPlayable?: boolean
|
isPlayable?: boolean
|
||||||
artist?: Artist | null
|
artist?: Artist | null
|
||||||
album?: Album | null
|
album?: Album | null
|
||||||
|
@ -42,22 +44,16 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
displayActions: true
|
displayActions: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const hover = ref<string | null>(null)
|
const { playing, loading } = usePlayer()
|
||||||
|
|
||||||
const { playing } = usePlayer()
|
|
||||||
const { currentTrack } = useQueue()
|
const { currentTrack } = useQueue()
|
||||||
const { activateTrack } = usePlayOptions(props)
|
const { activateTrack } = usePlayOptions(props)
|
||||||
|
|
||||||
|
const active = computed(() => props.track.id === currentTrack.value?.id && props.track.position === currentTrack.value?.position)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="[
|
:class="[{ active }, 'track-row row']"
|
||||||
{ active: currentTrack && track.id === currentTrack.id },
|
|
||||||
'track-row row',
|
|
||||||
]"
|
|
||||||
@mouseover="hover = track.id"
|
|
||||||
@mouseleave="hover = null"
|
|
||||||
@dblclick="activateTrack(track, index)"
|
@dblclick="activateTrack(track, index)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -67,37 +63,34 @@ const { activateTrack } = usePlayOptions(props)
|
||||||
>
|
>
|
||||||
<play-indicator
|
<play-indicator
|
||||||
v-if="
|
v-if="
|
||||||
!$store.state.player.isLoadingAudio &&
|
!loading &&
|
||||||
currentTrack &&
|
|
||||||
playing &&
|
playing &&
|
||||||
track.id === currentTrack.id &&
|
active &&
|
||||||
!(track.id == hover)
|
!hover
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-else-if="
|
v-else-if="
|
||||||
currentTrack &&
|
|
||||||
!playing &&
|
!playing &&
|
||||||
track.id === currentTrack.id &&
|
active &&
|
||||||
track.id !== hover
|
!hover
|
||||||
"
|
"
|
||||||
class="ui really tiny basic icon button play-button paused"
|
class="ui really tiny basic icon button play-button paused"
|
||||||
>
|
>
|
||||||
<i class="pause icon" />
|
<i class="play icon" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else-if="
|
v-else-if="
|
||||||
currentTrack &&
|
|
||||||
playing &&
|
playing &&
|
||||||
track.id === currentTrack.id &&
|
active &&
|
||||||
track.id == hover
|
hover
|
||||||
"
|
"
|
||||||
class="ui really tiny basic icon button play-button"
|
class="ui really tiny basic icon button play-button"
|
||||||
>
|
>
|
||||||
<i class="pause icon" />
|
<i class="pause icon" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else-if="track.id == hover"
|
v-else-if="hover"
|
||||||
class="ui really tiny basic icon button play-button"
|
class="ui really tiny basic icon button play-button"
|
||||||
>
|
>
|
||||||
<i class="play icon" />
|
<i class="play icon" />
|
||||||
|
|
|
@ -1,3 +1,145 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Track } from '~/types'
|
||||||
|
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import { clone, uniqBy } from 'lodash-es'
|
||||||
|
import { useElementByPoint, useMouse } from '@vueuse/core'
|
||||||
|
import axios from 'axios'
|
||||||
|
import TrackRow from '~/components/audio/track/Row.vue'
|
||||||
|
import TrackMobileRow from '~/components/audio/track/MobileRow.vue'
|
||||||
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tracks?: Track[]
|
||||||
|
|
||||||
|
showAlbum?: boolean
|
||||||
|
showArtist?: boolean
|
||||||
|
showPosition?: boolean
|
||||||
|
showArt?: boolean
|
||||||
|
showDuration?: boolean
|
||||||
|
search?: boolean
|
||||||
|
displayActions?: boolean
|
||||||
|
isArtist?: boolean
|
||||||
|
isAlbum?: boolean
|
||||||
|
isPodcast?: boolean
|
||||||
|
|
||||||
|
// TODO (wvffle): Find correct type
|
||||||
|
filters?: object
|
||||||
|
|
||||||
|
nextUrl?: string | null
|
||||||
|
|
||||||
|
paginateResults?: boolean
|
||||||
|
total?: number
|
||||||
|
page?: number
|
||||||
|
paginateBy?: number,
|
||||||
|
|
||||||
|
unique?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
tracks: () => [],
|
||||||
|
|
||||||
|
showAlbum: true,
|
||||||
|
showArtist: true,
|
||||||
|
showPosition: false,
|
||||||
|
showArt: true,
|
||||||
|
showDuration: true,
|
||||||
|
search: false,
|
||||||
|
displayActions: true,
|
||||||
|
isArtist: false,
|
||||||
|
isAlbum: false,
|
||||||
|
isPodcast: false,
|
||||||
|
|
||||||
|
filters: () => ({}),
|
||||||
|
nextUrl: null,
|
||||||
|
|
||||||
|
paginateResults: true,
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
paginateBy: 25,
|
||||||
|
|
||||||
|
unique: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const { x, y } = useMouse({ type: 'client' })
|
||||||
|
const { element } = useElementByPoint({ x, y })
|
||||||
|
const hover = computed(() => {
|
||||||
|
const row = element.value?.closest('.track-row') ?? null
|
||||||
|
return row && allTracks.value.find(track => {
|
||||||
|
return `${track.id}` === row.getAttribute('data-track-id') && `${track.position}` === row.getAttribute('data-track-position')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentPage = ref(props.page)
|
||||||
|
const totalTracks = ref(props.total)
|
||||||
|
const fetchDataUrl = ref(props.nextUrl)
|
||||||
|
const additionalTracks = ref([] as Track[])
|
||||||
|
const query = ref('')
|
||||||
|
|
||||||
|
const allTracks = computed(() => {
|
||||||
|
const tracks = [...props.tracks, ...additionalTracks.value]
|
||||||
|
return props.unique
|
||||||
|
? uniqBy(tracks, 'id')
|
||||||
|
: tracks
|
||||||
|
})
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
title: $pgettext('*/*/*/Noun', 'Title'),
|
||||||
|
album: $pgettext('*/*/*/Noun', 'Album'),
|
||||||
|
artist: $pgettext('*/*/*/Noun', 'Artist')
|
||||||
|
}))
|
||||||
|
|
||||||
|
const emit = defineEmits(['fetched', 'page-changed'])
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
...clone(props.filters),
|
||||||
|
page_size: props.paginateBy,
|
||||||
|
page: currentPage.value,
|
||||||
|
include_channels: true,
|
||||||
|
q: query.value
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('tracks/', { params })
|
||||||
|
|
||||||
|
// TODO (wvffle): Fetch continously?
|
||||||
|
fetchDataUrl.value = response.data.next
|
||||||
|
additionalTracks.value = response.data.results
|
||||||
|
totalTracks.value = response.data.count
|
||||||
|
emit('fetched')
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const performSearch = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
additionalTracks.value = []
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.tracks.length === 0) {
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePage = (page: number) => {
|
||||||
|
if (props.tracks.length === 0) {
|
||||||
|
currentPage.value = page
|
||||||
|
fetchData()
|
||||||
|
} else {
|
||||||
|
emit('page-changed', page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- Show the search bar if search is true -->
|
<!-- Show the search bar if search is true -->
|
||||||
|
@ -19,7 +161,7 @@
|
||||||
>
|
>
|
||||||
<empty-state
|
<empty-state
|
||||||
:refresh="true"
|
:refresh="true"
|
||||||
@refresh="fetchData('tracks/')"
|
@refresh="fetchData()"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
@ -85,7 +227,9 @@
|
||||||
|
|
||||||
<track-row
|
<track-row
|
||||||
v-for="(track, index) in allTracks"
|
v-for="(track, index) in allTracks"
|
||||||
:key="track.id"
|
:data-track-id="track.id"
|
||||||
|
:data-track-position="track.position"
|
||||||
|
:key="track.id + track.position"
|
||||||
:track="track"
|
:track="track"
|
||||||
:index="index"
|
:index="index"
|
||||||
:tracks="allTracks"
|
:tracks="allTracks"
|
||||||
|
@ -96,6 +240,7 @@
|
||||||
:display-actions="displayActions"
|
:display-actions="displayActions"
|
||||||
:show-duration="showDuration"
|
:show-duration="showDuration"
|
||||||
:is-podcast="isPodcast"
|
:is-podcast="isPodcast"
|
||||||
|
:hover="hover === track"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -144,7 +289,7 @@
|
||||||
v-if="paginateResults && totalTracks > paginateBy"
|
v-if="paginateResults && totalTracks > paginateBy"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="totalTracks"
|
:total="totalTracks"
|
||||||
:current="tracks.length > 0 ? page : {currentPage}"
|
:current="tracks.length > 0 ? page : currentPage"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
@page-changed="updatePage"
|
@page-changed="updatePage"
|
||||||
/>
|
/>
|
||||||
|
@ -152,112 +297,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import { clone, uniqBy } from 'lodash-es'
|
|
||||||
import axios from 'axios'
|
|
||||||
import TrackRow from '~/components/audio/track/Row.vue'
|
|
||||||
import TrackMobileRow from '~/components/audio/track/MobileRow.vue'
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
TrackRow,
|
|
||||||
TrackMobileRow,
|
|
||||||
Pagination
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
tracks: { type: Array, default: () => { return [] } },
|
|
||||||
showAlbum: { type: Boolean, required: false, default: true },
|
|
||||||
showArtist: { type: Boolean, required: false, default: true },
|
|
||||||
showPosition: { type: Boolean, required: false, default: false },
|
|
||||||
showArt: { type: Boolean, required: false, default: true },
|
|
||||||
search: { type: Boolean, required: false, default: false },
|
|
||||||
filters: { type: Object, required: false, default: () => { return {} } },
|
|
||||||
nextUrl: { type: String, required: false, default: null },
|
|
||||||
displayActions: { type: Boolean, required: false, default: true },
|
|
||||||
showDuration: { type: Boolean, required: false, default: true },
|
|
||||||
isArtist: { type: Boolean, required: false, default: false },
|
|
||||||
isAlbum: { type: Boolean, required: false, default: false },
|
|
||||||
isPodcast: { type: Boolean, required: false, default: false },
|
|
||||||
paginateResults: { type: Boolean, required: false, default: true },
|
|
||||||
total: { type: Number, required: false, default: 0 },
|
|
||||||
page: { type: Number, required: false, default: 1 },
|
|
||||||
paginateBy: { type: Number, required: false, default: 25 }
|
|
||||||
},
|
|
||||||
|
|
||||||
setup () {
|
|
||||||
const performSearch = () => {
|
|
||||||
this.currentPage = 1
|
|
||||||
this.additionalTracks.length = 0
|
|
||||||
this.fetchData('tracks/')
|
|
||||||
}
|
|
||||||
|
|
||||||
return { performSearch }
|
|
||||||
},
|
|
||||||
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
fetchDataUrl: this.nextUrl,
|
|
||||||
isLoading: false,
|
|
||||||
additionalTracks: [],
|
|
||||||
query: '',
|
|
||||||
totalTracks: this.total,
|
|
||||||
currentPage: this.page
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
allTracks () {
|
|
||||||
const tracks = (this.tracks || []).concat(this.additionalTracks)
|
|
||||||
return uniqBy(tracks, 'id')
|
|
||||||
},
|
|
||||||
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
title: this.$pgettext('*/*/*/Noun', 'Title'),
|
|
||||||
album: this.$pgettext('*/*/*/Noun', 'Album'),
|
|
||||||
artist: this.$pgettext('*/*/*/Noun', 'Artist')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
if (this.tracks.length === 0) {
|
|
||||||
this.fetchData('tracks/')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async fetchData (url) {
|
|
||||||
if (!url) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.isLoading = true
|
|
||||||
const self = this
|
|
||||||
const params = clone(this.filters)
|
|
||||||
params.page_size = this.paginateBy
|
|
||||||
params.page = this.currentPage
|
|
||||||
params.include_channels = true
|
|
||||||
params.q = this.query
|
|
||||||
const tracksPromise = await axios.get(url, { params: params })
|
|
||||||
try {
|
|
||||||
self.fetchDataUrl = tracksPromise.data.next
|
|
||||||
self.additionalTracks = tracksPromise.data.results
|
|
||||||
self.totalTracks = tracksPromise.data.count
|
|
||||||
self.$emit('fetched', tracksPromise.data)
|
|
||||||
self.isLoading = false
|
|
||||||
} catch (e) {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = e.backendErrors
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updatePage: function (page) {
|
|
||||||
if (this.tracks.length === 0) {
|
|
||||||
this.currentPage = page
|
|
||||||
this.fetchData('tracks/')
|
|
||||||
} else {
|
|
||||||
this.$emit('page-changed', page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { BackendError, Playlist, APIErrorResponse } from '~/types'
|
||||||
import { filter, sortBy, flow } from 'lodash-es'
|
import { filter, sortBy, flow } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useGettext } from 'vue3-gettext'
|
import { useGettext } from 'vue3-gettext'
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
import PlaylistForm from '~/components/playlists/Form.vue'
|
import PlaylistForm from '~/components/playlists/Form.vue'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
|
@ -79,7 +79,7 @@ store.dispatch('playlists/fetchOwn')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<semantic-modal
|
||||||
v-model:show="$store.state.playlists.showModal"
|
v-model:show="$store.state.playlists.showModal"
|
||||||
>
|
>
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
|
@ -268,5 +268,5 @@ store.dispatch('playlists/fetchOwn')
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -168,7 +168,7 @@ export default (props: PlayOptionsProps) => {
|
||||||
|
|
||||||
if (props.track && props.tracks?.length) {
|
if (props.track && props.tracks?.length) {
|
||||||
// set queue position to selected track
|
// set queue position to selected track
|
||||||
const trackIndex = props.tracks.findIndex(track => track.id === props.track?.id)
|
const trackIndex = props.tracks.findIndex(track => track.id === props.track?.id && track.position === props.track?.position)
|
||||||
store.dispatch('queue/currentIndex', trackIndex)
|
store.dispatch('queue/currentIndex', trackIndex)
|
||||||
} else {
|
} else {
|
||||||
store.dispatch('queue/currentIndex', 0)
|
store.dispatch('queue/currentIndex', 0)
|
||||||
|
@ -179,13 +179,16 @@ export default (props: PlayOptionsProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const activateTrack = (track: Track, index: number) => {
|
const activateTrack = (track: Track, index: number) => {
|
||||||
if (playing.value && track.id === currentTrack.value?.id) {
|
// TODO (wvffle): Check if position checking did not break anything
|
||||||
pause()
|
if (track.id === currentTrack.value?.id && track.position === currentTrack.value?.position) {
|
||||||
} else if (!playing.value && track.id === currentTrack.value?.id) {
|
if (playing.value) {
|
||||||
resume()
|
return pause()
|
||||||
} else {
|
}
|
||||||
replacePlay()
|
|
||||||
|
return resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replacePlay()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -221,6 +221,7 @@ const deletePlaylist = async () => {
|
||||||
<track-table
|
<track-table
|
||||||
:display-position="true"
|
:display-position="true"
|
||||||
:tracks="tracks"
|
:tracks="tracks"
|
||||||
|
:unique="false"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
|
|
Ładowanie…
Reference in New Issue