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 useQueue from '~/composables/audio/useQueue'
|
||||
import usePlayer from '~/composables/audio/usePlayer'
|
||||
import { ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props extends PlayOptionsProps {
|
||||
tracks: Track[]
|
||||
track: Track
|
||||
index: number
|
||||
|
||||
|
@ -23,7 +22,10 @@ interface Props extends PlayOptionsProps {
|
|||
showPosition?: boolean
|
||||
displayActions?: boolean
|
||||
|
||||
hover: boolean
|
||||
|
||||
// TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
|
||||
tracks: Track[]
|
||||
isPlayable?: boolean
|
||||
artist?: Artist | null
|
||||
album?: Album | null
|
||||
|
@ -42,22 +44,16 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
displayActions: true
|
||||
})
|
||||
|
||||
const hover = ref<string | null>(null)
|
||||
|
||||
const { playing } = usePlayer()
|
||||
const { playing, loading } = usePlayer()
|
||||
const { currentTrack } = useQueue()
|
||||
const { activateTrack } = usePlayOptions(props)
|
||||
|
||||
const active = computed(() => props.track.id === currentTrack.value?.id && props.track.position === currentTrack.value?.position)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
{ active: currentTrack && track.id === currentTrack.id },
|
||||
'track-row row',
|
||||
]"
|
||||
@mouseover="hover = track.id"
|
||||
@mouseleave="hover = null"
|
||||
:class="[{ active }, 'track-row row']"
|
||||
@dblclick="activateTrack(track, index)"
|
||||
>
|
||||
<div
|
||||
|
@ -67,37 +63,34 @@ const { activateTrack } = usePlayOptions(props)
|
|||
>
|
||||
<play-indicator
|
||||
v-if="
|
||||
!$store.state.player.isLoadingAudio &&
|
||||
currentTrack &&
|
||||
!loading &&
|
||||
playing &&
|
||||
track.id === currentTrack.id &&
|
||||
!(track.id == hover)
|
||||
active &&
|
||||
!hover
|
||||
"
|
||||
/>
|
||||
<button
|
||||
v-else-if="
|
||||
currentTrack &&
|
||||
!playing &&
|
||||
track.id === currentTrack.id &&
|
||||
track.id !== hover
|
||||
active &&
|
||||
!hover
|
||||
"
|
||||
class="ui really tiny basic icon button play-button paused"
|
||||
>
|
||||
<i class="pause icon" />
|
||||
<i class="play icon" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="
|
||||
currentTrack &&
|
||||
playing &&
|
||||
track.id === currentTrack.id &&
|
||||
track.id == hover
|
||||
active &&
|
||||
hover
|
||||
"
|
||||
class="ui really tiny basic icon button play-button"
|
||||
>
|
||||
<i class="pause icon" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="track.id == hover"
|
||||
v-else-if="hover"
|
||||
class="ui really tiny basic icon button play-button"
|
||||
>
|
||||
<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>
|
||||
<div>
|
||||
<!-- Show the search bar if search is true -->
|
||||
|
@ -19,7 +161,7 @@
|
|||
>
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData('tracks/')"
|
||||
@refresh="fetchData()"
|
||||
/>
|
||||
</slot>
|
||||
<div v-else>
|
||||
|
@ -85,7 +227,9 @@
|
|||
|
||||
<track-row
|
||||
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"
|
||||
:index="index"
|
||||
:tracks="allTracks"
|
||||
|
@ -96,6 +240,7 @@
|
|||
:display-actions="displayActions"
|
||||
:show-duration="showDuration"
|
||||
:is-podcast="isPodcast"
|
||||
:hover="hover === track"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
@ -144,7 +289,7 @@
|
|||
v-if="paginateResults && totalTracks > paginateBy"
|
||||
:paginate-by="paginateBy"
|
||||
:total="totalTracks"
|
||||
:current="tracks.length > 0 ? page : {currentPage}"
|
||||
:current="tracks.length > 0 ? page : currentPage"
|
||||
:compact="true"
|
||||
@page-changed="updatePage"
|
||||
/>
|
||||
|
@ -152,112 +297,3 @@
|
|||
</div>
|
||||
</div>
|
||||
</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 axios from 'axios'
|
||||
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 useLogger from '~/composables/useLogger'
|
||||
import { useStore } from '~/store'
|
||||
|
@ -79,7 +79,7 @@ store.dispatch('playlists/fetchOwn')
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<modal
|
||||
<semantic-modal
|
||||
v-model:show="$store.state.playlists.showModal"
|
||||
>
|
||||
<h4 class="header">
|
||||
|
@ -268,5 +268,5 @@ store.dispatch('playlists/fetchOwn')
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
</template>
|
||||
|
|
|
@ -168,7 +168,7 @@ export default (props: PlayOptionsProps) => {
|
|||
|
||||
if (props.track && props.tracks?.length) {
|
||||
// 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)
|
||||
} else {
|
||||
store.dispatch('queue/currentIndex', 0)
|
||||
|
@ -179,13 +179,16 @@ export default (props: PlayOptionsProps) => {
|
|||
}
|
||||
|
||||
const activateTrack = (track: Track, index: number) => {
|
||||
if (playing.value && track.id === currentTrack.value?.id) {
|
||||
pause()
|
||||
} else if (!playing.value && track.id === currentTrack.value?.id) {
|
||||
resume()
|
||||
} else {
|
||||
replacePlay()
|
||||
// TODO (wvffle): Check if position checking did not break anything
|
||||
if (track.id === currentTrack.value?.id && track.position === currentTrack.value?.position) {
|
||||
if (playing.value) {
|
||||
return pause()
|
||||
}
|
||||
|
||||
return resume()
|
||||
}
|
||||
|
||||
replacePlay()
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -221,6 +221,7 @@ const deletePlaylist = async () => {
|
|||
<track-table
|
||||
:display-position="true"
|
||||
:tracks="tracks"
|
||||
:unique="false"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
|
|
Ładowanie…
Reference in New Issue