Migrate queue component
This commit is contained in:
parent
bef0d1dec4
commit
ccb905b004
|
@ -16,14 +16,15 @@ export interface Sound {
|
|||
dispose(): void
|
||||
|
||||
readonly audioNode: IAudioNode<IAudioContext>
|
||||
readonly isErrored: Ref<boolean>
|
||||
readonly isLoaded: Ref<boolean>
|
||||
readonly currentTime: number
|
||||
readonly duration: number
|
||||
readonly buffered: number
|
||||
looping: boolean
|
||||
|
||||
play(): void | Promise<void>
|
||||
pause(): void | Promise<void>
|
||||
play(): void | Promise<void>
|
||||
|
||||
seekTo(seconds: number): void | Promise<void>
|
||||
seekBy(seconds: number): void | Promise<void>
|
||||
|
@ -46,6 +47,7 @@ export class HTMLSound implements Sound {
|
|||
#soundLoopEventHook = createEventHook<HTMLSound>()
|
||||
#soundEndEventHook = createEventHook<HTMLSound>()
|
||||
|
||||
readonly isErrored = ref(false)
|
||||
readonly isLoaded = ref(false)
|
||||
|
||||
audioNode = createAudioSource(this.#audio)
|
||||
|
@ -69,6 +71,11 @@ export class HTMLSound implements Sound {
|
|||
this.isLoaded.value = this.#audio.readyState >= 2
|
||||
})
|
||||
|
||||
useEventListener(this.#audio, 'error', () => {
|
||||
this.isErrored.value = true
|
||||
this.isLoaded.value = true
|
||||
})
|
||||
|
||||
this.onSoundLoop = this.#soundLoopEventHook.on
|
||||
this.onSoundEnd = this.#soundEndEventHook.on
|
||||
}
|
||||
|
|
|
@ -1,21 +1,43 @@
|
|||
<script setup lang="ts">
|
||||
import type { QueueItemSource } from '~/types'
|
||||
|
||||
import { useStore } from '~/store'
|
||||
import { nextTick, ref, computed, watchEffect, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
||||
import { whenever, watchDebounced, useCurrentElement, useScrollLock } from '@vueuse/core'
|
||||
import { nextTick, ref, computed, watchEffect, onMounted } from 'vue'
|
||||
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import useQueue from '~/composables/audio/useQueue'
|
||||
import usePlayer from '~/composables/audio/usePlayer'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
import time from '~/utils/time'
|
||||
|
||||
// import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||
// import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
||||
import PlayerControls from '~/components/audio/PlayerControls.vue'
|
||||
import VirtualList from '~/components/vui/list/VirtualList.vue'
|
||||
import QueueItem from '~/components/QueueItem.vue'
|
||||
|
||||
import { queue } from '~/composables/audio/queue'
|
||||
import {
|
||||
isPlaying,
|
||||
currentTime,
|
||||
duration,
|
||||
progress,
|
||||
bufferProgress,
|
||||
seekTo,
|
||||
loading as isLoadingAudio,
|
||||
errored
|
||||
} from '~/composables/audio/player'
|
||||
|
||||
import {
|
||||
hasNext,
|
||||
currentTrack,
|
||||
currentIndex,
|
||||
queue,
|
||||
tracks,
|
||||
dequeue,
|
||||
playTrack,
|
||||
reorder,
|
||||
endsIn as timeLeft
|
||||
} from '~/composables/audio/queue'
|
||||
|
||||
const queueModal = ref()
|
||||
const { activate, deactivate } = useFocusTrap(queueModal, { allowOutsideClick: true, preventScroll: true })
|
||||
|
@ -24,34 +46,6 @@ const { $pgettext } = useGettext()
|
|||
const scrollLock = useScrollLock(document.body)
|
||||
const store = useStore()
|
||||
|
||||
const {
|
||||
playing,
|
||||
loading: isLoadingAudio,
|
||||
errored,
|
||||
duration,
|
||||
durationFormatted,
|
||||
currentTimeFormatted,
|
||||
progress,
|
||||
bufferProgress,
|
||||
currentTime,
|
||||
pause,
|
||||
resume
|
||||
} = usePlayer()
|
||||
|
||||
const {
|
||||
currentTrack,
|
||||
hasNext,
|
||||
isEmpty: emptyQueue,
|
||||
tracks,
|
||||
reorder,
|
||||
endsIn: timeLeft,
|
||||
currentIndex,
|
||||
removeTrack,
|
||||
clear,
|
||||
next,
|
||||
previous
|
||||
} = useQueue()
|
||||
|
||||
const labels = computed(() => ({
|
||||
queue: $pgettext('*/*/*', 'Queue'),
|
||||
duration: $pgettext('*/*/*', 'Duration'),
|
||||
|
@ -96,7 +90,7 @@ const scrollLoop = () => {
|
|||
onMounted(scrollLoop)
|
||||
|
||||
whenever(
|
||||
() => tracks.value.length === 0,
|
||||
() => queue.value.length === 0,
|
||||
() => store.commit('ui/queueFocused', null),
|
||||
{ immediate: true }
|
||||
)
|
||||
|
@ -107,12 +101,12 @@ router.beforeEach(() => store.commit('ui/queueFocused', null))
|
|||
const progressBar = ref()
|
||||
const touchProgress = (event: MouseEvent) => {
|
||||
const time = ((event.clientX - ((event.target as Element).closest('.progress')?.getBoundingClientRect().left ?? 0)) / progressBar.value.offsetWidth) * duration.value
|
||||
currentTime.value = time
|
||||
seekTo(time)
|
||||
}
|
||||
|
||||
const play = (index: unknown) => {
|
||||
store.dispatch('queue/currentIndex', index as number)
|
||||
resume()
|
||||
const play = async (index: number) => {
|
||||
isPlaying.value = true
|
||||
return playTrack(index)
|
||||
}
|
||||
|
||||
const queueItems = computed(() => queue.value.map((track, index) => ({
|
||||
|
@ -152,22 +146,9 @@ const reorderTracks = async (from: number, to: number) => {
|
|||
<div class="cover-container">
|
||||
<div class="cover">
|
||||
<img
|
||||
v-if="currentTrack.cover && currentTrack.cover.urls.large_square_crop"
|
||||
ref="cover"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.large_square_crop)"
|
||||
>
|
||||
<img
|
||||
v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.large_square_crop"
|
||||
ref="cover"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.large_square_crop)"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
class="ui image"
|
||||
alt=""
|
||||
src="../assets/audio/default-cover.png"
|
||||
:src="$store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -182,17 +163,17 @@ const reorderTracks = async (from: number, to: number) => {
|
|||
<div class="sub header ellipsis">
|
||||
<router-link
|
||||
class="discrete link artist"
|
||||
:to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}"
|
||||
:to="{name: 'library.artists.detail', params: {id: currentTrack.artistId }}"
|
||||
>
|
||||
{{ currentTrack.artist.name }}
|
||||
{{ currentTrack.artistName }}
|
||||
</router-link>
|
||||
<template v-if="currentTrack.album">
|
||||
<template v-if="currentTrack.albumId !== -1">
|
||||
/
|
||||
<router-link
|
||||
class="discrete link album"
|
||||
:to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}"
|
||||
:to="{name: 'library.albums.detail', params: {id: currentTrack.albumId }}"
|
||||
>
|
||||
{{ currentTrack.album.title }}
|
||||
{{ currentTrack.albumTitle }}
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -207,7 +188,7 @@ const reorderTracks = async (from: number, to: number) => {
|
|||
The track cannot be loaded
|
||||
</translate>
|
||||
</h3>
|
||||
<p v-if="hasNext && playing && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors">
|
||||
<p v-if="hasNext && isPlaying && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors">
|
||||
<translate translate-context="Sidebar/Player/Error message.Paragraph">
|
||||
The next track will play automatically in a few seconds…
|
||||
</translate>
|
||||
|
@ -220,7 +201,8 @@ const reorderTracks = async (from: number, to: number) => {
|
|||
</p>
|
||||
</div>
|
||||
<div class="additional-controls desktop-and-below">
|
||||
<track-favorite-icon
|
||||
<!-- TODO (wvffle): Update props -->
|
||||
<!-- <track-favorite-icon
|
||||
v-if="$store.state.auth.authenticated"
|
||||
:track="currentTrack"
|
||||
/>
|
||||
|
@ -236,7 +218,7 @@ const reorderTracks = async (from: number, to: number) => {
|
|||
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
|
||||
>
|
||||
<i :class="['eye slash outline', 'basic', 'icon']" />
|
||||
</button>
|
||||
</button> -->
|
||||
</div>
|
||||
<div class="progress-wrapper">
|
||||
<div
|
||||
|
@ -277,8 +259,10 @@ const reorderTracks = async (from: number, to: number) => {
|
|||
:aria-label="labels.restart"
|
||||
class="left floated timer discrete start"
|
||||
@click.prevent="currentTime = 0"
|
||||
>{{ currentTimeFormatted }}</a>
|
||||
<span class="right floated timer total">{{ durationFormatted }}</span>
|
||||
>
|
||||
{{ time.parse(Math.round(currentTime)) }}
|
||||
</a>
|
||||
<span class="right floated timer total">{{ time.parse(Math.round(duration)) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="left floated timer">00:00</span>
|
||||
|
@ -286,49 +270,7 @@ const reorderTracks = async (from: number, to: number) => {
|
|||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player-controls desktop-and-below">
|
||||
<span
|
||||
role="button"
|
||||
:title="labels.previous"
|
||||
:aria-label="labels.previous"
|
||||
class="control"
|
||||
:disabled="emptyQueue || null"
|
||||
@click.prevent.stop="previous"
|
||||
>
|
||||
<i :class="['ui', 'backward step', {'disabled': emptyQueue}, 'icon']" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="!playing"
|
||||
role="button"
|
||||
:title="labels.play"
|
||||
:aria-label="labels.play"
|
||||
class="control"
|
||||
@click.prevent.stop="resume"
|
||||
>
|
||||
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'icon']" />
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
role="button"
|
||||
:title="labels.pause"
|
||||
:aria-label="labels.pause"
|
||||
class="control"
|
||||
@click.prevent.stop="pause"
|
||||
>
|
||||
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'icon']" />
|
||||
</span>
|
||||
<span
|
||||
role="button"
|
||||
:title="labels.next"
|
||||
:aria-label="labels.next"
|
||||
class="control"
|
||||
:disabled="hasNext || null"
|
||||
@click.prevent.stop="next"
|
||||
>
|
||||
<i :class="['ui', {'disabled': !hasNext}, 'forward step', 'icon']" />
|
||||
</span>
|
||||
</div>
|
||||
<player-controls class="desktop-and-below" />
|
||||
</template>
|
||||
</div>
|
||||
<div id="queue">
|
||||
|
@ -345,7 +287,7 @@ const reorderTracks = async (from: number, to: number) => {
|
|||
</button>
|
||||
<button
|
||||
class="ui right floated basic button danger"
|
||||
@click="clear"
|
||||
@click="tracks.length = 0"
|
||||
>
|
||||
<translate translate-context="*/Queue/*/Verb">
|
||||
Clear
|
||||
|
@ -356,7 +298,7 @@ const reorderTracks = async (from: number, to: number) => {
|
|||
<div>
|
||||
<translate
|
||||
translate-context="Sidebar/Queue/Text"
|
||||
:translate-params="{index: currentIndex + 1, length: tracks.length}"
|
||||
:translate-params="{index: currentIndex + 1, length: queue.length}"
|
||||
>
|
||||
Track %{ index } of %{ length }
|
||||
</translate>
|
||||
|
@ -387,7 +329,7 @@ const reorderTracks = async (from: number, to: number) => {
|
|||
:source="item"
|
||||
:class="[...classList, currentIndex === index && 'active']"
|
||||
@play="play"
|
||||
@remove="removeTrack"
|
||||
@remove="dequeue"
|
||||
/>
|
||||
</template>
|
||||
</virtual-list>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import type { QueueItemSource } from '~/types'
|
||||
|
||||
import time from '~/utils/time'
|
||||
|
||||
interface Events {
|
||||
(e: 'play', index: number): void
|
||||
(e: 'remove', index: number): void
|
||||
|
@ -47,7 +49,7 @@ defineProps<Props>()
|
|||
</div>
|
||||
<div class="duration-cell">
|
||||
<template v-if="source.sources.length > 0">
|
||||
{{ source.duration }}
|
||||
{{ time.parse(Math.round(source.sources[0].duration ?? 0)) }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="controls">
|
||||
|
|
|
@ -17,9 +17,7 @@ import {
|
|||
} from '~/composables/audio/player'
|
||||
|
||||
import {
|
||||
hasPrevious,
|
||||
playPrevious,
|
||||
hasNext,
|
||||
playNext,
|
||||
queue,
|
||||
currentIndex,
|
||||
|
@ -38,6 +36,7 @@ import time from '~/utils/time'
|
|||
// import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||
// import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
||||
import VolumeControl from './VolumeControl.vue'
|
||||
import PlayerControls from './PlayerControls.vue'
|
||||
|
||||
const store = useStore()
|
||||
const { $pgettext } = useGettext()
|
||||
|
@ -104,8 +103,6 @@ const loopingTitle = computed(() => {
|
|||
? $pgettext('Sidebar/Player/Icon.Tooltip', 'Looping on a single track. Click to switch to whole queue looping.')
|
||||
: $pgettext('Sidebar/Player/Icon.Tooltip', 'Looping on whole queue. Click to disable looping.')
|
||||
})
|
||||
|
||||
const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.value)))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -233,45 +230,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
|
|||
<i :class="['eye slash outline', 'basic', 'icon']" />
|
||||
</button> -->
|
||||
</div>
|
||||
<div class="player-controls controls queue-not-focused">
|
||||
<button
|
||||
:title="labels.previous"
|
||||
:aria-label="labels.previous"
|
||||
:disabled="!hasPrevious"
|
||||
class="circular button control tablet-and-up"
|
||||
@click.prevent.stop="playPrevious"
|
||||
>
|
||||
<i :class="['ui', 'large', {'disabled': !hasPrevious}, 'backward step', 'icon']" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!isPlaying"
|
||||
:title="labels.play"
|
||||
:aria-label="labels.play"
|
||||
class="circular button control"
|
||||
@click.prevent.stop="isPlaying = true"
|
||||
>
|
||||
<i :class="['ui', 'big', 'play', {'disabled': !currentTrack}, 'icon']" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
:title="labels.pause"
|
||||
:aria-label="labels.pause"
|
||||
class="circular button control"
|
||||
@click.prevent.stop="isPlaying = false"
|
||||
>
|
||||
<i :class="['ui', 'big', 'pause', {'disabled': !currentTrack}, 'icon']" />
|
||||
</button>
|
||||
<button
|
||||
:title="labels.next"
|
||||
:aria-label="labels.next"
|
||||
:disabled="!hasNext"
|
||||
class="circular button control"
|
||||
@click.prevent.stop="playNext"
|
||||
>
|
||||
<i :class="['ui', 'large', {'disabled': !hasNext}, 'forward step', 'icon']" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<player-controls class="controls queue-not-focused" />
|
||||
<div class="controls progress-controls queue-not-focused tablet-and-up small align-left">
|
||||
<div class="timer">
|
||||
<template v-if="!isLoadingAudio">
|
||||
|
@ -279,7 +238,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
|
|||
class="start"
|
||||
@click.stop.prevent="seekTo(0)"
|
||||
>
|
||||
{{ currentTimeFormatted }}
|
||||
{{ time.parse(Math.round(currentTime)) }}
|
||||
</span>
|
||||
|
|
||||
<span class="total">{{ time.parse(Math.round(duration)) }}</span>
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
<script setup lang="ts">
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { isPlaying } from '~/composables/audio/player'
|
||||
import {
|
||||
hasPrevious,
|
||||
playPrevious,
|
||||
hasNext,
|
||||
playNext,
|
||||
currentTrack
|
||||
} from '~/composables/audio/queue'
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
const labels = computed(() => ({
|
||||
previous: $pgettext('Sidebar/Player/Icon.Tooltip', 'Previous track'),
|
||||
play: $pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Play'),
|
||||
pause: $pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Pause'),
|
||||
next: $pgettext('Sidebar/Player/Icon.Tooltip', 'Next track')
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="player-controls">
|
||||
<button
|
||||
:title="labels.previous"
|
||||
:aria-label="labels.previous"
|
||||
:disabled="!hasPrevious"
|
||||
class="circular button control tablet-and-up"
|
||||
@click.prevent.stop="playPrevious()"
|
||||
>
|
||||
<i :class="['ui', 'large', {'disabled': !hasPrevious}, 'backward step', 'icon']" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!isPlaying"
|
||||
:title="labels.play"
|
||||
:aria-label="labels.play"
|
||||
class="circular button control"
|
||||
@click.prevent.stop="isPlaying = true"
|
||||
>
|
||||
<i :class="['ui', 'big', 'play', {'disabled': !currentTrack}, 'icon']" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
:title="labels.pause"
|
||||
:aria-label="labels.pause"
|
||||
class="circular button control"
|
||||
@click.prevent.stop="isPlaying = false"
|
||||
>
|
||||
<i :class="['ui', 'big', 'pause', {'disabled': !currentTrack}, 'icon']" />
|
||||
</button>
|
||||
<button
|
||||
:title="labels.next"
|
||||
:aria-label="labels.next"
|
||||
:disabled="!hasNext"
|
||||
class="circular button control"
|
||||
@click.prevent.stop="playNext()"
|
||||
>
|
||||
<i :class="['ui', 'large', {'disabled': !hasNext}, 'forward step', 'icon']" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -1,5 +1,5 @@
|
|||
import { tryOnMounted, useIntervalFn, useRafFn, useStorage, whenever } from '@vueuse/core'
|
||||
import { currentTrack, currentIndex } from '~/composables/audio/queue'
|
||||
import { tryOnMounted, useIntervalFn, useRafFn, useStorage, useTimeoutFn, whenever } from '@vueuse/core'
|
||||
import { currentTrack, currentIndex, playNext } from '~/composables/audio/queue'
|
||||
import { currentSound, createTrack } from '~/composables/audio/tracks'
|
||||
import { computed, ref, watch, watchEffect, type Ref } from 'vue'
|
||||
import { setGain } from './audio-api'
|
||||
|
@ -168,3 +168,14 @@ export const loading = computed(() => {
|
|||
if (!sound) return false
|
||||
return !sound.isLoaded.value
|
||||
})
|
||||
|
||||
// Errored
|
||||
export const errored = computed(() => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) return false
|
||||
return sound.isErrored.value
|
||||
})
|
||||
|
||||
const { start, stop } = useTimeoutFn(() => playNext(), 3000, { immediate: false })
|
||||
watch(currentIndex, stop)
|
||||
whenever(errored, start)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { Track, Upload } from '~/types'
|
||||
|
||||
import { computedAsync, useStorage } from '@vueuse/core'
|
||||
import { shuffle as shuffleArray, uniq } from 'lodash-es'
|
||||
import { computedAsync, useNow, useStorage, useTimeAgo } from '@vueuse/core'
|
||||
import { shuffle as shuffleArray, sum, uniq } from 'lodash-es'
|
||||
import { getMany, setMany } from 'idb-keyval'
|
||||
import { useClamp } from '@vueuse/math'
|
||||
import { computed } from 'vue'
|
||||
|
@ -76,6 +76,7 @@ const createQueueTrack = async (track: Track): Promise<QueueTrack> => {
|
|||
?? new URL('~/assets/audio/default-cover.png', import.meta.url).href,
|
||||
sources: track.uploads.map(upload => ({
|
||||
uuid: upload.uuid,
|
||||
duration: upload.duration,
|
||||
mimetype: upload.mimetype,
|
||||
bitrate: upload.bitrate,
|
||||
url: upload.listen_url
|
||||
|
@ -83,6 +84,7 @@ const createQueueTrack = async (track: Track): Promise<QueueTrack> => {
|
|||
}
|
||||
}
|
||||
|
||||
// Adding tracks
|
||||
export const enqueue = async (...newTracks: Track[]) => {
|
||||
const queueTracks = await Promise.all(newTracks.map(createQueueTrack))
|
||||
await setMany(queueTracks.map(track => [track.id, track]))
|
||||
|
@ -96,6 +98,21 @@ export const enqueue = async (...newTracks: Track[]) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Removing tracks
|
||||
export const dequeue = async (index: number) => {
|
||||
if (currentIndex.value === index) {
|
||||
await playNext(true)
|
||||
}
|
||||
|
||||
tracks.value.splice(index, 1)
|
||||
|
||||
if (index <= currentIndex.value) {
|
||||
currentIndex.value -= 1
|
||||
}
|
||||
|
||||
// TODO (wvffle): Check if removing last element works well
|
||||
}
|
||||
|
||||
// Current Index
|
||||
export const currentIndex = useClamp(useStorage('queue:index', 0), 0, () => tracks.value.length)
|
||||
export const currentTrack = computed(() => queue.value[currentIndex.value])
|
||||
|
@ -119,11 +136,11 @@ export const playTrack = async (trackIndex: number, force = false) => {
|
|||
|
||||
// Previous track
|
||||
export const hasPrevious = computed(() => /* looping.value === LoopingMode.LoopQueue || */ currentIndex.value !== 0)
|
||||
export const playPrevious = async () => {
|
||||
export const playPrevious = async (force = false) => {
|
||||
const { looping, LoopingMode } = await import('~/composables/audio/player')
|
||||
|
||||
// Loop entire queue / change track to the next one
|
||||
if (looping.value === LoopingMode.LoopQueue && currentIndex.value === 0) {
|
||||
if (looping.value === LoopingMode.LoopQueue && currentIndex.value === 0 && force !== true) {
|
||||
// Loop track programmatically if it is the only track in the queue
|
||||
if (tracks.value.length === 1) return playTrack(currentIndex.value, true)
|
||||
return playTrack(tracks.value.length - 1)
|
||||
|
@ -134,11 +151,11 @@ export const playPrevious = async () => {
|
|||
|
||||
// Next track
|
||||
export const hasNext = computed(() => /* looping.value === LoopingMode.LoopQueue || */ currentIndex.value !== tracks.value.length - 1)
|
||||
export const playNext = async () => {
|
||||
export const playNext = async (force = false) => {
|
||||
const { looping, LoopingMode } = await import('~/composables/audio/player')
|
||||
|
||||
// Loop entire queue / change track to the next one
|
||||
if (looping.value === LoopingMode.LoopQueue && currentIndex.value === tracks.value.length - 1) {
|
||||
if (looping.value === LoopingMode.LoopQueue && currentIndex.value === tracks.value.length - 1 && force !== true) {
|
||||
// Loop track programmatically if it is the only track in the queue
|
||||
if (tracks.value.length === 1) return playTrack(currentIndex.value, true)
|
||||
return playTrack(0)
|
||||
|
@ -147,6 +164,28 @@ export const playNext = async () => {
|
|||
return playTrack(currentIndex.value + 1)
|
||||
}
|
||||
|
||||
// Reorder
|
||||
export const reorder = (from: number, to: number) => {
|
||||
const [id] = tracks.value.splice(from, 1)
|
||||
tracks.value.splice(to, 0, id)
|
||||
|
||||
const current = currentIndex.value
|
||||
if (current === from) {
|
||||
currentIndex.value = to
|
||||
return
|
||||
}
|
||||
|
||||
if (from < current && to >= current) {
|
||||
// item before was moved after
|
||||
currentIndex.value -= 1
|
||||
}
|
||||
|
||||
if (from > current && to <= current) {
|
||||
// item after was moved before
|
||||
currentIndex.value += 1
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle
|
||||
const shuffledIds = useStorage('queue:tracks:shuffled', [] as number[])
|
||||
export const isShuffled = computed(() => shuffledIds.value.length !== 0)
|
||||
|
@ -158,3 +197,17 @@ export const shuffle = () => {
|
|||
|
||||
shuffledIds.value = shuffleArray(tracks.value)
|
||||
}
|
||||
|
||||
// Ends in
|
||||
const now = useNow()
|
||||
export const endsIn = useTimeAgo(computed(() => {
|
||||
const seconds = sum(
|
||||
queue.value
|
||||
.slice(currentIndex.value)
|
||||
.map((track) => track.sources[0]?.duration ?? 0)
|
||||
)
|
||||
|
||||
const date = new Date(now.value)
|
||||
date.setSeconds(date.getSeconds() + seconds)
|
||||
return date
|
||||
}))
|
||||
|
|
|
@ -207,6 +207,13 @@
|
|||
.icon {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
|
|
Loading…
Reference in New Issue