Rewrite queue
This commit is contained in:
parent
02f8f37824
commit
8c11b6d0ea
|
@ -1,3 +1,115 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
import { nextTick, onMounted, ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import time from '~/utils/time'
|
||||||
|
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
||||||
|
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||||
|
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
||||||
|
import Draggable, { } from 'vuedraggable'
|
||||||
|
import { whenever, useTimeoutFn, useWindowScroll, useWindowSize } from '@vueuse/core'
|
||||||
|
import { useGettext } from "vue3-gettext"
|
||||||
|
import useQueue from '~/composables/useQueue'
|
||||||
|
import usePlayer from '~/composables/usePlayer'
|
||||||
|
|
||||||
|
const queueModal = ref()
|
||||||
|
|
||||||
|
const { activate } = useFocusTrap(queueModal, { allowOutsideClick: true })
|
||||||
|
activate()
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const queue = computed(() => store.state.queue)
|
||||||
|
const currentIndex = computed(() => store.state.queue.currentIndex)
|
||||||
|
|
||||||
|
const { y: pageYOffset } = useWindowScroll()
|
||||||
|
const { height: windowHeight } = useWindowSize()
|
||||||
|
const scrollToCurrent = async () => {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const item = queueModal.value?.querySelector('.queue-item.active')
|
||||||
|
const { top } = item?.getBoundingClientRect() ?? { top: 0 }
|
||||||
|
|
||||||
|
window.scrollTo({
|
||||||
|
top: top + pageYOffset.value - windowHeight.value / 2,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO (wvffle): Add useVirtualList to speed up the queue rendering and potentially resolve #1471
|
||||||
|
// Each item has 49px height on desktop and 50.666px on tablet(?) and down
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
// NOTE: delay is to let transition work
|
||||||
|
useTimeoutFn(scrollToCurrent, 400)
|
||||||
|
})
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
|
||||||
|
const {
|
||||||
|
playing,
|
||||||
|
loading: isLoadingAudio,
|
||||||
|
errored,
|
||||||
|
focused: playerFocused,
|
||||||
|
duration,
|
||||||
|
durationFormatted,
|
||||||
|
currentTimeFormatted,
|
||||||
|
progress,
|
||||||
|
bufferProgress,
|
||||||
|
pause,
|
||||||
|
resume,
|
||||||
|
} = usePlayer()
|
||||||
|
|
||||||
|
const {
|
||||||
|
focused: queueFocused,
|
||||||
|
currentTrack,
|
||||||
|
hasNext,
|
||||||
|
isEmpty: emptyQueue,
|
||||||
|
tracks,
|
||||||
|
reorder: reorderTracks,
|
||||||
|
endsIn: timeLeft,
|
||||||
|
removeTrack,
|
||||||
|
clear,
|
||||||
|
next,
|
||||||
|
previous
|
||||||
|
} = useQueue()
|
||||||
|
|
||||||
|
const reorder = (event: { oldIndex: number, newIndex: number }) => reorderTracks(event.oldIndex, event.newIndex)
|
||||||
|
|
||||||
|
const labels = computed(() => ({
|
||||||
|
queue: $pgettext('*/*/*', 'Queue'),
|
||||||
|
duration: $pgettext('*/*/*', 'Duration'),
|
||||||
|
addArtistContentFilter: $pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Hide content from this artist…'),
|
||||||
|
restart: $pgettext('*/*/*', 'Restart track'),
|
||||||
|
previous: $pgettext('*/*/*', 'Previous track'),
|
||||||
|
next: $pgettext('*/*/*', 'Next track'),
|
||||||
|
pause: $pgettext('*/*/*', 'Pause'),
|
||||||
|
play: $pgettext('*/*/*', 'Play'),
|
||||||
|
remove: $pgettext('*/*/*', 'Remove'),
|
||||||
|
selectTrack: $pgettext('*/*/*', 'Select track')
|
||||||
|
}))
|
||||||
|
|
||||||
|
whenever(queueFocused, scrollToCurrent, { immediate: true })
|
||||||
|
whenever(currentTrack, scrollToCurrent, { immediate: true })
|
||||||
|
|
||||||
|
whenever(
|
||||||
|
() => tracks.value.length === 0,
|
||||||
|
() => store.commit('ui/queueFocused', null),
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
router.beforeEach(() => store.commit('ui/queueFocused', null))
|
||||||
|
|
||||||
|
// TODO (wvffle): move setCurrentTime to usePlayer
|
||||||
|
const emit = defineEmits(['touch-progress'])
|
||||||
|
|
||||||
|
const progressBar = ref()
|
||||||
|
const touchProgress = (event: MouseEvent) => {
|
||||||
|
const time = (event.clientX / progressBar.value.offsetWidth) * duration.value
|
||||||
|
emit('touch-progress', time)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
ref="queueModal"
|
ref="queueModal"
|
||||||
|
@ -107,7 +219,7 @@
|
||||||
class="progress-area"
|
class="progress-area"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="progress"
|
ref="progressBar"
|
||||||
:class="['ui', 'small', 'vibrant', {'indicating': isLoadingAudio}, 'progress']"
|
:class="['ui', 'small', 'vibrant', {'indicating': isLoadingAudio}, 'progress']"
|
||||||
@click="touchProgress"
|
@click="touchProgress"
|
||||||
>
|
>
|
||||||
|
@ -141,7 +253,7 @@
|
||||||
href=""
|
href=""
|
||||||
:aria-label="labels.restart"
|
:aria-label="labels.restart"
|
||||||
class="left floated timer discrete start"
|
class="left floated timer discrete start"
|
||||||
@click.prevent="setCurrentTime(0)"
|
@click.prevent="emit('touch-progress', 0)"
|
||||||
>{{ currentTimeFormatted }}</a>
|
>{{ currentTimeFormatted }}</a>
|
||||||
<span class="right floated timer total">{{ durationFormatted }}</span>
|
<span class="right floated timer total">{{ durationFormatted }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
@ -154,11 +266,11 @@
|
||||||
<div class="player-controls tablet-and-below">
|
<div class="player-controls tablet-and-below">
|
||||||
<span
|
<span
|
||||||
role="button"
|
role="button"
|
||||||
:title="labels.previousTrack"
|
:title="labels.previous"
|
||||||
:aria-label="labels.previousTrack"
|
:aria-label="labels.previous"
|
||||||
class="control"
|
class="control"
|
||||||
:disabled="emptyQueue || null"
|
:disabled="emptyQueue || null"
|
||||||
@click.prevent.stop="$store.dispatch('queue/previous')"
|
@click.prevent.stop="previous"
|
||||||
>
|
>
|
||||||
<i :class="['ui', 'backward step', {'disabled': emptyQueue}, 'icon']" />
|
<i :class="['ui', 'backward step', {'disabled': emptyQueue}, 'icon']" />
|
||||||
</span>
|
</span>
|
||||||
|
@ -169,7 +281,7 @@
|
||||||
:title="labels.play"
|
:title="labels.play"
|
||||||
:aria-label="labels.play"
|
:aria-label="labels.play"
|
||||||
class="control"
|
class="control"
|
||||||
@click.prevent.stop="resumePlayback"
|
@click.prevent.stop="resume"
|
||||||
>
|
>
|
||||||
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'icon']" />
|
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'icon']" />
|
||||||
</span>
|
</span>
|
||||||
|
@ -179,7 +291,7 @@
|
||||||
:title="labels.pause"
|
:title="labels.pause"
|
||||||
:aria-label="labels.pause"
|
:aria-label="labels.pause"
|
||||||
class="control"
|
class="control"
|
||||||
@click.prevent.stop="pausePlayback"
|
@click.prevent.stop="pause"
|
||||||
>
|
>
|
||||||
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'icon']" />
|
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'icon']" />
|
||||||
</span>
|
</span>
|
||||||
|
@ -189,7 +301,7 @@
|
||||||
:aria-label="labels.next"
|
:aria-label="labels.next"
|
||||||
class="control"
|
class="control"
|
||||||
:disabled="hasNext || null"
|
:disabled="hasNext || null"
|
||||||
@click.prevent.stop="$store.dispatch('queue/next')"
|
@click.prevent.stop="next"
|
||||||
>
|
>
|
||||||
<i :class="['ui', {'disabled': !hasNext}, 'forward step', 'icon']" />
|
<i :class="['ui', {'disabled': !hasNext}, 'forward step', 'icon']" />
|
||||||
</span>
|
</span>
|
||||||
|
@ -211,7 +323,7 @@
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="ui right floated basic button danger"
|
class="ui right floated basic button danger"
|
||||||
@click="$store.dispatch('queue/clean')"
|
@click="clear"
|
||||||
>
|
>
|
||||||
<translate translate-context="*/Queue/*/Verb">
|
<translate translate-context="*/Queue/*/Verb">
|
||||||
Clear
|
Clear
|
||||||
|
@ -225,7 +337,8 @@
|
||||||
:translate-params="{index: currentIndex + 1, length: queue.tracks.length}"
|
:translate-params="{index: currentIndex + 1, length: queue.tracks.length}"
|
||||||
>
|
>
|
||||||
Track %{ index } of %{ length }
|
Track %{ index } of %{ length }
|
||||||
</translate><template v-if="!$store.state.radios.running">
|
</translate>
|
||||||
|
<template v-if="!$store.state.radios.running">
|
||||||
-
|
-
|
||||||
<span :title="labels.duration">
|
<span :title="labels.duration">
|
||||||
{{ timeLeft }}
|
{{ timeLeft }}
|
||||||
|
@ -300,10 +413,10 @@
|
||||||
<i class="pink heart icon" />
|
<i class="pink heart icon" />
|
||||||
</template>
|
</template>
|
||||||
<button
|
<button
|
||||||
:aria-label="labels.removeFromQueue"
|
:aria-label="labels.remove"
|
||||||
:title="labels.removeFromQueue"
|
:title="labels.remove"
|
||||||
:class="['ui', 'really', 'tiny', 'basic', 'circular', 'icon', 'button']"
|
:class="['ui', 'really', 'tiny', 'basic', 'circular', 'icon', 'button']"
|
||||||
@click.stop="cleanTrack(index)"
|
@click.stop="removeTrack(index)"
|
||||||
>
|
>
|
||||||
<i class="x icon" />
|
<i class="x icon" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -344,193 +457,3 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
|
||||||
import { useStore } from '~/store'
|
|
||||||
import { mapState, mapGetters, mapActions } from 'vuex'
|
|
||||||
import { nextTick, onMounted, ref, computed } from 'vue'
|
|
||||||
import moment from 'moment'
|
|
||||||
import { sum } from 'lodash-es'
|
|
||||||
import time from '~/utils/time'
|
|
||||||
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
|
||||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
|
||||||
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
|
||||||
import draggable from 'vuedraggable'
|
|
||||||
import { useTimeoutFn, useWindowScroll, useWindowSize } from '@vueuse/core'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
TrackFavoriteIcon,
|
|
||||||
TrackPlaylistIcon,
|
|
||||||
draggable
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const queueModal = ref()
|
|
||||||
|
|
||||||
const { activate } = useFocusTrap(queueModal, { allowOutsideClick: true })
|
|
||||||
activate()
|
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
const queue = store.state.queue
|
|
||||||
const currentIndex = computed(() => store.state.queue.currentIndex)
|
|
||||||
|
|
||||||
const { y: pageYOffset } = useWindowScroll()
|
|
||||||
const { height: windowHeight } = useWindowSize()
|
|
||||||
const scrollToCurrent = async () => {
|
|
||||||
await nextTick()
|
|
||||||
const item = queueModal.value?.querySelector('.queue-item.active')
|
|
||||||
const { top } = item?.getBoundingClientRect() ?? { top: 0 }
|
|
||||||
window.scrollTo({
|
|
||||||
top: top + pageYOffset.value - windowHeight.value / 2,
|
|
||||||
behavior: 'smooth'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await nextTick()
|
|
||||||
// delay is to let transition work
|
|
||||||
useTimeoutFn(scrollToCurrent, 400)
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO (wvffle): Add useVirtualList to speed up the queue rendering and potentially resolve #1471
|
|
||||||
// Each item has 49px height on desktop and 50.666px on tablet(?) and down
|
|
||||||
return { queueModal, scrollToCurrent, queue, currentIndex }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
showVolume: false,
|
|
||||||
isShuffling: false,
|
|
||||||
tracksChangeBuffer: null,
|
|
||||||
time
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState({
|
|
||||||
playing: state => state.player.playing,
|
|
||||||
isLoadingAudio: state => state.player.isLoadingAudio,
|
|
||||||
volume: state => state.player.volume,
|
|
||||||
looping: state => state.player.looping,
|
|
||||||
duration: state => state.player.duration,
|
|
||||||
bufferProgress: state => state.player.bufferProgress,
|
|
||||||
errored: state => state.player.errored,
|
|
||||||
currentTime: state => state.player.currentTime
|
|
||||||
}),
|
|
||||||
...mapGetters({
|
|
||||||
currentTrack: 'queue/currentTrack',
|
|
||||||
hasNext: 'queue/hasNext',
|
|
||||||
emptyQueue: 'queue/isEmpty',
|
|
||||||
durationFormatted: 'player/durationFormatted',
|
|
||||||
currentTimeFormatted: 'player/currentTimeFormatted',
|
|
||||||
progress: 'player/progress'
|
|
||||||
}),
|
|
||||||
tracks: {
|
|
||||||
get () {
|
|
||||||
return this.$store.state.queue.tracks
|
|
||||||
},
|
|
||||||
set (value) {
|
|
||||||
this.tracksChangeBuffer = value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
queue: this.$pgettext('*/*/*', 'Queue'),
|
|
||||||
duration: this.$pgettext('*/*/*', 'Duration'),
|
|
||||||
addArtistContentFilter: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Hide content from this artist…'),
|
|
||||||
restart: this.$pgettext('*/*/*', 'Restart track')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
timeLeft () {
|
|
||||||
const seconds = sum(
|
|
||||||
this.queue.tracks.slice(this.queue.currentIndex).map((t) => {
|
|
||||||
return (t.uploads || []).map((u) => {
|
|
||||||
return u.duration || 0
|
|
||||||
})[0] || 0
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return moment(this.$store.state.ui.lastDate).add(seconds, 'seconds').fromNow(true)
|
|
||||||
},
|
|
||||||
sliderVolume: {
|
|
||||||
get () {
|
|
||||||
return this.volume
|
|
||||||
},
|
|
||||||
set (v) {
|
|
||||||
this.$store.commit('player/volume', v)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
playerFocused () {
|
|
||||||
return this.$store.state.ui.queueFocused === 'player'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'$store.state.ui.queueFocused': {
|
|
||||||
handler (v) {
|
|
||||||
if (v === 'queue') {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollToCurrent()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
},
|
|
||||||
'$store.state.queue.currentIndex': {
|
|
||||||
handler () {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollToCurrent()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'$store.state.queue.tracks': {
|
|
||||||
handler (v) {
|
|
||||||
if (!v || v.length === 0) {
|
|
||||||
this.$store.commit('ui/queueFocused', null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
immediate: true,
|
|
||||||
deep: true
|
|
||||||
},
|
|
||||||
'$route.fullPath' () {
|
|
||||||
this.$store.commit('ui/queueFocused', null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions({
|
|
||||||
cleanTrack: 'queue/cleanTrack',
|
|
||||||
mute: 'player/mute',
|
|
||||||
unmute: 'player/unmute',
|
|
||||||
clean: 'queue/clean',
|
|
||||||
toggleMute: 'player/toggleMute',
|
|
||||||
resumePlayback: 'player/resumePlayback',
|
|
||||||
pausePlayback: 'player/pausePlayback'
|
|
||||||
}),
|
|
||||||
reorder: function (event) {
|
|
||||||
this.$store.commit('queue/reorder', {
|
|
||||||
tracks: this.tracksChangeBuffer,
|
|
||||||
oldIndex: event.oldIndex,
|
|
||||||
newIndex: event.newIndex
|
|
||||||
})
|
|
||||||
},
|
|
||||||
touchProgress (e) {
|
|
||||||
const target = this.$refs.progress
|
|
||||||
const time = (e.layerX / target.offsetWidth) * this.duration
|
|
||||||
this.$emit('touch-progress', time)
|
|
||||||
},
|
|
||||||
shuffle () {
|
|
||||||
const disabled = this.queue.tracks.length === 0
|
|
||||||
if (this.isShuffling || disabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const self = this
|
|
||||||
const msg = this.$pgettext('Content/Queue/Message', 'Queue shuffled!')
|
|
||||||
this.isShuffling = true
|
|
||||||
setTimeout(() => {
|
|
||||||
self.$store.dispatch('queue/shuffle', () => {
|
|
||||||
self.isShuffling = false
|
|
||||||
self.$store.commit('ui/addMessage', {
|
|
||||||
content: msg,
|
|
||||||
date: new Date()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -334,6 +334,7 @@ import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
||||||
import { useThrottleFn, useTimeoutFn, useToggle } from '@vueuse/core'
|
import { useThrottleFn, useTimeoutFn, useToggle } from '@vueuse/core'
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
import { useGettext } from 'vue3-gettext'
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import useQueue from '~/composables/useQueue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -357,17 +358,7 @@ export default {
|
||||||
store.commit('ui/queueFocused', ['queue', 'player'].indexOf(store.state.ui.queueFocused) > -1 ? null : 'player')
|
store.commit('ui/queueFocused', ['queue', 'player'].indexOf(store.state.ui.queueFocused) > -1 ? null : 'player')
|
||||||
}
|
}
|
||||||
|
|
||||||
const shuffledMessage = $pgettext('Content/Queue/Message', 'Queue shuffled!')
|
const { shuffle } = useQueue()
|
||||||
const shuffle = useThrottleFn(() => {
|
|
||||||
if (queueIsEmpty.value) return
|
|
||||||
useTimeoutFn(async () => {
|
|
||||||
await store.dispatch('queue/shuffle')
|
|
||||||
store.commit('ui/addMessage', {
|
|
||||||
content: shuffledMessage,
|
|
||||||
date: new Date()
|
|
||||||
})
|
|
||||||
}, 100)
|
|
||||||
}, 101, false)
|
|
||||||
|
|
||||||
const seek = (step) => {
|
const seek = (step) => {
|
||||||
if (step > 0) {
|
if (step > 0) {
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { useStore } from "~/store"
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const store = useStore()
|
||||||
|
const looping = computed(() => store.state.player.looping)
|
||||||
|
const playing = computed(() => store.state.player.playing)
|
||||||
|
const loading = computed(() => store.state.player.isLoadingAudio)
|
||||||
|
const errored = computed(() => store.state.player.errored)
|
||||||
|
const focused = computed(() => store.state.ui.queueFocused === 'player')
|
||||||
|
|
||||||
|
const volume = computed(() => store.state.player.volume)
|
||||||
|
|
||||||
|
const duration = computed(() => store.state.player.duration)
|
||||||
|
const currentTime = computed(() => store.state.player.currentTime)
|
||||||
|
|
||||||
|
const durationFormatted = computed(() => store.getters['player/durationFormatted'])
|
||||||
|
const currentTimeFormatted = computed(() => store.getters['player/currentTimeFormatted'])
|
||||||
|
|
||||||
|
const progress = computed(() => store.getters['player/progress'])
|
||||||
|
const bufferProgress = computed(() => store.state.player.bufferProgress)
|
||||||
|
|
||||||
|
const pause = () => store.dispatch('player/pausePlayback')
|
||||||
|
const resume = () => store.dispatch('player/resumePlayback')
|
||||||
|
|
||||||
|
return {
|
||||||
|
looping,
|
||||||
|
playing,
|
||||||
|
loading,
|
||||||
|
errored,
|
||||||
|
focused,
|
||||||
|
|
||||||
|
volume,
|
||||||
|
|
||||||
|
duration,
|
||||||
|
currentTime,
|
||||||
|
|
||||||
|
durationFormatted,
|
||||||
|
currentTimeFormatted,
|
||||||
|
|
||||||
|
progress,
|
||||||
|
bufferProgress,
|
||||||
|
|
||||||
|
pause,
|
||||||
|
resume
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { useTimeoutFn, useThrottleFn } from "@vueuse/core"
|
||||||
|
import { useTimeAgo, useNow } from '@vueuse/core'
|
||||||
|
import { useGettext } from "vue3-gettext"
|
||||||
|
import { useStore } from "~/store"
|
||||||
|
import { ref, computed } from "vue"
|
||||||
|
import { Track } from "~/types"
|
||||||
|
import { sum } from 'lodash-es'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const store = useStore()
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
|
||||||
|
const currentTrack = computed(() => store.getters['queue/currentTrack'])
|
||||||
|
const currentIndex = computed(() => store.state.queue.currentIndex)
|
||||||
|
const hasNext = computed(() => store.getters['queue/hasNext'])
|
||||||
|
const isEmpty = computed(() => store.getters['queue/isEmpty'])
|
||||||
|
|
||||||
|
const removeTrack = (index: number) => store.dispatch('queue/cleanTrack', index)
|
||||||
|
const clear = () => store.dispatch('queue/clean')
|
||||||
|
|
||||||
|
const next = () => store.dispatch('queue/next')
|
||||||
|
const previous = () => store.dispatch('queue/previous')
|
||||||
|
|
||||||
|
const focused = computed(() => store.state.ui.queueFocused === 'queue')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Track list
|
||||||
|
//
|
||||||
|
const tracksChangeBuffer = ref<Track[] | null>(null)
|
||||||
|
const tracks = computed<Track[]>({
|
||||||
|
get: () => store.state.queue.tracks,
|
||||||
|
set: (value) => (tracksChangeBuffer.value = value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const reorder = (oldIndex: number, newIndex: number) => {
|
||||||
|
store.commit('queue/reorder', {
|
||||||
|
tracks: tracksChangeBuffer.value ?? tracks.value,
|
||||||
|
oldIndex,
|
||||||
|
newIndex
|
||||||
|
})
|
||||||
|
|
||||||
|
tracksChangeBuffer.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Shuffle
|
||||||
|
//
|
||||||
|
const isShuffling = ref(false)
|
||||||
|
|
||||||
|
const forceShuffle = useThrottleFn(() => {
|
||||||
|
isShuffling.value = true
|
||||||
|
|
||||||
|
useTimeoutFn(async () => {
|
||||||
|
await store.dispatch('queue/shuffle')
|
||||||
|
store.commit('ui/addMessage', {
|
||||||
|
content: $pgettext('Content/Queue/Message', 'Queue shuffled!'),
|
||||||
|
date: new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
isShuffling.value = false
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
const shuffle = useThrottleFn(() => {
|
||||||
|
if (isShuffling.value || isEmpty.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return forceShuffle()
|
||||||
|
}, 101, false)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Time left
|
||||||
|
//
|
||||||
|
const now = useNow()
|
||||||
|
const endsIn = useTimeAgo(computed(() => {
|
||||||
|
const seconds = sum(
|
||||||
|
tracks.value
|
||||||
|
.slice(currentIndex.value)
|
||||||
|
.map((track) => track.uploads?.[0]?.duration ?? 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
const date = new Date(now.value)
|
||||||
|
date.setSeconds(date.getSeconds() + seconds)
|
||||||
|
return date
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentTrack,
|
||||||
|
hasNext,
|
||||||
|
isEmpty,
|
||||||
|
|
||||||
|
removeTrack,
|
||||||
|
clear,
|
||||||
|
next,
|
||||||
|
previous,
|
||||||
|
|
||||||
|
tracks,
|
||||||
|
reorder,
|
||||||
|
|
||||||
|
shuffle,
|
||||||
|
forceShuffle,
|
||||||
|
|
||||||
|
endsIn,
|
||||||
|
focused
|
||||||
|
}
|
||||||
|
}
|
|
@ -111,9 +111,11 @@ const store: Module<State, RootState> = {
|
||||||
cleanTrack ({ state, dispatch, commit }, index) {
|
cleanTrack ({ state, dispatch, commit }, index) {
|
||||||
// are we removing current playin track
|
// are we removing current playin track
|
||||||
const current = index === state.currentIndex
|
const current = index === state.currentIndex
|
||||||
|
|
||||||
if (current) {
|
if (current) {
|
||||||
dispatch('player/stop', null, { root: true })
|
dispatch('player/stop', null, { root: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
commit('splice', { start: index, size: 1 })
|
commit('splice', { start: index, size: 1 })
|
||||||
if (index < state.currentIndex) {
|
if (index < state.currentIndex) {
|
||||||
commit('currentIndex', state.currentIndex - 1)
|
commit('currentIndex', state.currentIndex - 1)
|
||||||
|
@ -127,6 +129,7 @@ const store: Module<State, RootState> = {
|
||||||
// we play next track, which now have the same index
|
// we play next track, which now have the same index
|
||||||
commit('currentIndex', index)
|
commit('currentIndex', index)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.currentIndex + 1 === state.tracks.length) {
|
if (state.currentIndex + 1 === state.tracks.length) {
|
||||||
dispatch('radios/populateQueue', null, { root: true })
|
dispatch('radios/populateQueue', null, { root: true })
|
||||||
}
|
}
|
||||||
|
@ -171,14 +174,11 @@ const store: Module<State, RootState> = {
|
||||||
// so we replay automatically on next track append
|
// so we replay automatically on next track append
|
||||||
commit('ended', true)
|
commit('ended', true)
|
||||||
},
|
},
|
||||||
async shuffle ({ dispatch, state }, callback) {
|
async shuffle ({ dispatch, state }) {
|
||||||
const shuffled = shuffle(state.tracks)
|
const shuffled = shuffle(state.tracks)
|
||||||
state.tracks.length = 0
|
state.tracks.length = 0
|
||||||
const params: { tracks: Track[], callback?: () => unknown } = { tracks: shuffled }
|
|
||||||
if (callback) {
|
await dispatch('appendMany', { tracks: shuffled })
|
||||||
params.callback = callback
|
|
||||||
}
|
|
||||||
await dispatch('appendMany', params)
|
|
||||||
await dispatch('currentIndex', 0)
|
await dispatch('currentIndex', 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,7 @@ export interface Track {
|
||||||
copyright?: string
|
copyright?: string
|
||||||
license?: License
|
license?: License
|
||||||
tags: string[]
|
tags: string[]
|
||||||
|
uploads: Upload[]
|
||||||
|
|
||||||
album?: Album
|
album?: Album
|
||||||
artist?: Artist
|
artist?: Artist
|
||||||
|
@ -169,6 +170,7 @@ export interface Upload {
|
||||||
filename?: string
|
filename?: string
|
||||||
source?: string
|
source?: string
|
||||||
uuid: string
|
uuid: string
|
||||||
|
duration?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileSystem Logs
|
// FileSystem Logs
|
||||||
|
|
Loading…
Reference in New Issue