kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
				
				
				
			WIP Rewrite queue
							rodzic
							
								
									15f5056a59
								
							
						
					
					
						commit
						bef0d1dec4
					
				| 
						 | 
				
			
			@ -33,6 +33,7 @@
 | 
			
		|||
    "focus-trap": "7.0.0",
 | 
			
		||||
    "fomantic-ui-css": "2.8.8",
 | 
			
		||||
    "howler": "2.2.3",
 | 
			
		||||
    "idb-keyval": "^6.2.0",
 | 
			
		||||
    "js-logger": "1.6.1",
 | 
			
		||||
    "lodash-es": "4.17.21",
 | 
			
		||||
    "moment": "2.29.4",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,9 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import type { Track, QueueItemSource } from '~/types'
 | 
			
		||||
import type { QueueItemSource } from '~/types'
 | 
			
		||||
 | 
			
		||||
import { useStore } from '~/store'
 | 
			
		||||
import { nextTick, ref, computed, watchEffect, onMounted } 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'
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +15,8 @@ import usePlayer from '~/composables/audio/usePlayer'
 | 
			
		|||
import VirtualList from '~/components/vui/list/VirtualList.vue'
 | 
			
		||||
import QueueItem from '~/components/QueueItem.vue'
 | 
			
		||||
 | 
			
		||||
import { queue } from '~/composables/audio/queue'
 | 
			
		||||
 | 
			
		||||
const queueModal = ref()
 | 
			
		||||
const { activate, deactivate } = useFocusTrap(queueModal, { allowOutsideClick: true, preventScroll: true })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -114,24 +115,14 @@ const play = (index: unknown) => {
 | 
			
		|||
  resume()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getCover = (track: Track) => {
 | 
			
		||||
  return store.getters['instance/absoluteUrl'](
 | 
			
		||||
    track.cover?.urls.medium_square_crop
 | 
			
		||||
        ?? track.album?.cover?.urls.medium_square_crop
 | 
			
		||||
        ?? new URL('../assets/audio/default-cover.png', import.meta.url).href
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const queueItems = computed(() => tracks.value.map((track, index) => ({
 | 
			
		||||
const queueItems = computed(() => queue.value.map((track, index) => ({
 | 
			
		||||
  ...track,
 | 
			
		||||
  id: `${index}-${track.id}`,
 | 
			
		||||
  track,
 | 
			
		||||
  coverUrl: getCover(track),
 | 
			
		||||
  labels: {
 | 
			
		||||
    remove: $pgettext('*/*/*', 'Remove'),
 | 
			
		||||
    selectTrack: $pgettext('*/*/*', 'Select track'),
 | 
			
		||||
    favorite: $pgettext('*/*/*', 'Favorite track')
 | 
			
		||||
  },
 | 
			
		||||
  duration: time.durationFormatted(track.uploads[0]?.duration ?? 0) ?? ''
 | 
			
		||||
  }
 | 
			
		||||
}) as QueueItemSource))
 | 
			
		||||
 | 
			
		||||
const reorderTracks = async (from: number, to: number) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,17 +36,17 @@ defineProps<Props>()
 | 
			
		|||
    <div @click="$emit('play', index)">
 | 
			
		||||
      <button
 | 
			
		||||
        class="title reset ellipsis"
 | 
			
		||||
        :title="source.track.title"
 | 
			
		||||
        :title="source.title"
 | 
			
		||||
        :aria-label="source.labels.selectTrack"
 | 
			
		||||
      >
 | 
			
		||||
        <strong>{{ source.track.title }}</strong><br>
 | 
			
		||||
        <strong>{{ source.title }}</strong><br>
 | 
			
		||||
        <span>
 | 
			
		||||
          {{ source.track.artist?.name }}
 | 
			
		||||
          {{ source.artistName }}
 | 
			
		||||
        </span>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="duration-cell">
 | 
			
		||||
      <template v-if="source.track.uploads.length > 0">
 | 
			
		||||
      <template v-if="source.sources.length > 0">
 | 
			
		||||
        {{ source.duration }}
 | 
			
		||||
      </template>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -55,10 +55,10 @@ defineProps<Props>()
 | 
			
		|||
        :aria-label="source.labels.favorite"
 | 
			
		||||
        :title="source.labels.favorite"
 | 
			
		||||
        class="ui really basic circular icon button"
 | 
			
		||||
        @click.stop="$store.dispatch('favorites/toggle', source.track.id)"
 | 
			
		||||
        @click.stop="$store.dispatch('favorites/toggle', source.id)"
 | 
			
		||||
      >
 | 
			
		||||
        <i
 | 
			
		||||
          :class="$store.getters['favorites/isFavorite'](source.track.id) ? 'pink' : ''"
 | 
			
		||||
          :class="$store.getters['favorites/isFavorite'](source.id) ? 'pink' : ''"
 | 
			
		||||
          class="heart icon"
 | 
			
		||||
        />
 | 
			
		||||
      </button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ import {
 | 
			
		|||
  playPrevious,
 | 
			
		||||
  hasNext,
 | 
			
		||||
  playNext,
 | 
			
		||||
  tracks,
 | 
			
		||||
  queue,
 | 
			
		||||
  currentIndex,
 | 
			
		||||
  currentTrack,
 | 
			
		||||
  shuffle
 | 
			
		||||
| 
						 | 
				
			
			@ -35,8 +35,8 @@ import { useStore } from '~/store'
 | 
			
		|||
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
 | 
			
		||||
import time from '~/utils/time'
 | 
			
		||||
 | 
			
		||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
 | 
			
		||||
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
 | 
			
		||||
// import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
 | 
			
		||||
// import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
 | 
			
		||||
import VolumeControl from './VolumeControl.vue'
 | 
			
		||||
 | 
			
		||||
const store = useStore()
 | 
			
		||||
| 
						 | 
				
			
			@ -152,21 +152,9 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
 | 
			
		|||
            @click.stop.prevent="$router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})"
 | 
			
		||||
          >
 | 
			
		||||
            <img
 | 
			
		||||
              v-if="currentTrack.cover && currentTrack.cover.urls.original"
 | 
			
		||||
              ref="cover"
 | 
			
		||||
              alt=""
 | 
			
		||||
              :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)"
 | 
			
		||||
            >
 | 
			
		||||
            <img
 | 
			
		||||
              v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls && currentTrack.album.cover.urls.original"
 | 
			
		||||
              ref="cover"
 | 
			
		||||
              alt=""
 | 
			
		||||
              :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.medium_square_crop)"
 | 
			
		||||
            >
 | 
			
		||||
            <img
 | 
			
		||||
              v-else
 | 
			
		||||
              alt=""
 | 
			
		||||
              src="../../assets/audio/default-cover.png"
 | 
			
		||||
              :src="$store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
 | 
			
		||||
            >
 | 
			
		||||
          </div>
 | 
			
		||||
          <div
 | 
			
		||||
| 
						 | 
				
			
			@ -185,19 +173,19 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
 | 
			
		|||
            <div class="meta">
 | 
			
		||||
              <router-link
 | 
			
		||||
                class="discrete link"
 | 
			
		||||
                :to="{name: 'library.artists.detail', params: {id: currentTrack.artist?.id }}"
 | 
			
		||||
                :to="{name: 'library.artists.detail', params: {id: currentTrack.artistId }}"
 | 
			
		||||
                @click.stop.prevent=""
 | 
			
		||||
              >
 | 
			
		||||
                {{ currentTrack.artist?.name }}
 | 
			
		||||
                {{ currentTrack.artistName }}
 | 
			
		||||
              </router-link>
 | 
			
		||||
              <template v-if="currentTrack.album">
 | 
			
		||||
              <template v-if="currentTrack.albumId !== -1">
 | 
			
		||||
                /
 | 
			
		||||
                <router-link
 | 
			
		||||
                  class="discrete link"
 | 
			
		||||
                  :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}"
 | 
			
		||||
                  :to="{name: 'library.albums.detail', params: {id: currentTrack.albumId }}"
 | 
			
		||||
                  @click.stop.prevent=""
 | 
			
		||||
                >
 | 
			
		||||
                  {{ currentTrack.album.title }}
 | 
			
		||||
                  {{ currentTrack.albumTitle }}
 | 
			
		||||
                </router-link>
 | 
			
		||||
              </template>
 | 
			
		||||
            </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -206,21 +194,9 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
 | 
			
		|||
        <div class="controls track-controls queue-not-focused desktop-and-below">
 | 
			
		||||
          <div class="ui tiny image">
 | 
			
		||||
            <img
 | 
			
		||||
              v-if="currentTrack.cover && currentTrack.cover.urls.original"
 | 
			
		||||
              ref="cover"
 | 
			
		||||
              alt=""
 | 
			
		||||
              :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)"
 | 
			
		||||
            >
 | 
			
		||||
            <img
 | 
			
		||||
              v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.original"
 | 
			
		||||
              ref="cover"
 | 
			
		||||
              alt=""
 | 
			
		||||
              :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.medium_square_crop)"
 | 
			
		||||
            >
 | 
			
		||||
            <img
 | 
			
		||||
              v-else
 | 
			
		||||
              alt=""
 | 
			
		||||
              src="../../assets/audio/default-cover.png"
 | 
			
		||||
              :src="$store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
 | 
			
		||||
            >
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="middle aligned content ellipsis">
 | 
			
		||||
| 
						 | 
				
			
			@ -228,8 +204,9 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
 | 
			
		|||
              {{ currentTrack.title }}
 | 
			
		||||
            </strong>
 | 
			
		||||
            <div class="meta">
 | 
			
		||||
              {{ currentTrack.artist?.name }}<template v-if="currentTrack.album">
 | 
			
		||||
                / {{ currentTrack.album.title }}
 | 
			
		||||
              {{ currentTrack.artistName }}
 | 
			
		||||
              <template v-if="currentTrack.albumId !== -1">
 | 
			
		||||
                / {{ currentTrack.albumTitle }}
 | 
			
		||||
              </template>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -238,7 +215,8 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
 | 
			
		|||
          v-if="$store.state.auth.authenticated"
 | 
			
		||||
          class="controls desktop-and-up fluid align-right"
 | 
			
		||||
        >
 | 
			
		||||
          <track-favorite-icon
 | 
			
		||||
          <!-- TODO (wvffle): Uncomment -->
 | 
			
		||||
          <!-- <track-favorite-icon
 | 
			
		||||
            class="control white"
 | 
			
		||||
            :track="currentTrack"
 | 
			
		||||
          />
 | 
			
		||||
| 
						 | 
				
			
			@ -253,7 +231,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
 | 
			
		|||
            @click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
 | 
			
		||||
          >
 | 
			
		||||
            <i :class="['eye slash outline', 'basic', 'icon']" />
 | 
			
		||||
          </button>
 | 
			
		||||
          </button> -->
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="player-controls controls queue-not-focused">
 | 
			
		||||
          <button
 | 
			
		||||
| 
						 | 
				
			
			@ -332,12 +310,12 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
 | 
			
		|||
 | 
			
		||||
            <button
 | 
			
		||||
              class="circular control button"
 | 
			
		||||
              :disabled="tracks.length === 0"
 | 
			
		||||
              :disabled="queue.length === 0"
 | 
			
		||||
              :title="labels.shuffle"
 | 
			
		||||
              :aria-label="labels.shuffle"
 | 
			
		||||
              @click.prevent.stop="shuffle()"
 | 
			
		||||
            >
 | 
			
		||||
              <i :class="['ui', 'random', {'disabled': tracks.length === 0}, 'icon']" />
 | 
			
		||||
              <i :class="['ui', 'random', {'disabled': queue.length === 0}, 'icon']" />
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="group">
 | 
			
		||||
| 
						 | 
				
			
			@ -350,7 +328,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
 | 
			
		|||
                <i class="stream icon" />
 | 
			
		||||
                <translate
 | 
			
		||||
                  translate-context="Sidebar/Queue/Text"
 | 
			
		||||
                  :translate-params="{index: currentIndex + 1, length: tracks.length}"
 | 
			
		||||
                  :translate-params="{index: currentIndex + 1, length: queue.length}"
 | 
			
		||||
                >
 | 
			
		||||
                  %{ index } of %{ length }
 | 
			
		||||
                </translate>
 | 
			
		||||
| 
						 | 
				
			
			@ -362,7 +340,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
 | 
			
		|||
                <i class="stream icon" />
 | 
			
		||||
                <translate
 | 
			
		||||
                  translate-context="Sidebar/Queue/Text"
 | 
			
		||||
                  :translate-params="{index: currentIndex + 1, length: tracks.length}"
 | 
			
		||||
                  :translate-params="{index: currentIndex + 1, length: queue.length}"
 | 
			
		||||
                >
 | 
			
		||||
                  %{ index } of %{ length }
 | 
			
		||||
                </translate>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import { tryOnMounted, useIntervalFn, useRafFn, useStorage, whenever } from '@vueuse/core'
 | 
			
		||||
import { currentTrack, currentIndex } 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'
 | 
			
		||||
| 
						 | 
				
			
			@ -6,10 +7,6 @@ import { setGain } from './audio-api'
 | 
			
		|||
import store from '~/store'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
 | 
			
		||||
import useQueue from '~/composables/audio/useQueue'
 | 
			
		||||
 | 
			
		||||
const { currentIndex, currentTrack } = useQueue()
 | 
			
		||||
 | 
			
		||||
export const isPlaying = ref(false)
 | 
			
		||||
 | 
			
		||||
watch(isPlaying, (playing) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,46 +1,110 @@
 | 
			
		|||
import type { Track } from '~/types'
 | 
			
		||||
import type { Track, Upload } from '~/types'
 | 
			
		||||
 | 
			
		||||
import { isPlaying, looping, LoopingMode } from '~/composables/audio/player'
 | 
			
		||||
import { currentSound } from '~/composables/audio/tracks'
 | 
			
		||||
import { toReactive, useStorage } from '@vueuse/core'
 | 
			
		||||
import { shuffle as shuffleArray } from 'lodash-es'
 | 
			
		||||
import { computedAsync, useStorage } from '@vueuse/core'
 | 
			
		||||
import { shuffle as shuffleArray, uniq } from 'lodash-es'
 | 
			
		||||
import { getMany, setMany } from 'idb-keyval'
 | 
			
		||||
import { useClamp } from '@vueuse/math'
 | 
			
		||||
import { computed } from 'vue'
 | 
			
		||||
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
 | 
			
		||||
// import useWebWorker from '~/composables/useWebWorker'
 | 
			
		||||
 | 
			
		||||
// const { post, onMessageReceived } = useWebWorker('queue')
 | 
			
		||||
 | 
			
		||||
// Queue
 | 
			
		||||
export const tracks = toReactive(useStorage('queue:tracks', [] as Track[]))
 | 
			
		||||
export const queue = computed(() => {
 | 
			
		||||
  if (isShuffled.value) {
 | 
			
		||||
    const tracksById = tracks.reduce((acc, track) => {
 | 
			
		||||
      acc[track.id] = track
 | 
			
		||||
      return acc
 | 
			
		||||
    }, {} as Record<number, Track>)
 | 
			
		||||
export interface QueueTrackSource {
 | 
			
		||||
  uuid: string
 | 
			
		||||
  mimetype: string
 | 
			
		||||
  bitrate?: number
 | 
			
		||||
  url: string
 | 
			
		||||
  duration?: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    return shuffledIds.value.map(id => tracksById[id])
 | 
			
		||||
export interface QueueTrack {
 | 
			
		||||
  id: number
 | 
			
		||||
  title: string
 | 
			
		||||
  artistName: string
 | 
			
		||||
  albumTitle: string
 | 
			
		||||
 | 
			
		||||
  // TODO: Add urls for those
 | 
			
		||||
  coverUrl: string
 | 
			
		||||
  artistId: number
 | 
			
		||||
  albumId: number
 | 
			
		||||
 | 
			
		||||
  sources: QueueTrackSource[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Queue
 | 
			
		||||
export const tracks = useStorage('queue:tracks', [] as number[])
 | 
			
		||||
const tracksById = computedAsync(async () => {
 | 
			
		||||
  const trackObjects = await getMany(uniq(tracks.value))
 | 
			
		||||
  return trackObjects.reduce((acc, track) => {
 | 
			
		||||
    acc[track.id] = track
 | 
			
		||||
    return acc
 | 
			
		||||
  }, {}) as Record<number, QueueTrack>
 | 
			
		||||
}, {})
 | 
			
		||||
 | 
			
		||||
export const queue = computed(() => {
 | 
			
		||||
  const indexedTracks = tracksById.value
 | 
			
		||||
 | 
			
		||||
  if (isShuffled.value) {
 | 
			
		||||
    return shuffledIds.value.map(id => indexedTracks[id]).filter(i => i)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return tracks
 | 
			
		||||
  return tracks.value.map(id => indexedTracks[id]).filter(i => i)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const enqueue = (...newTracks: Track[]) => {
 | 
			
		||||
  tracks.push(...newTracks)
 | 
			
		||||
const createQueueTrack = async (track: Track): Promise<QueueTrack> => {
 | 
			
		||||
  if (track.uploads.length === 0) {
 | 
			
		||||
    // we don't have any information for this track, we need to fetch it
 | 
			
		||||
    const { uploads } = await axios.get(`tracks/${track.id}/`)
 | 
			
		||||
      .then(response => response.data as Track, () => ({ uploads: [] as Upload[] }))
 | 
			
		||||
 | 
			
		||||
    track.uploads = uploads
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    id: track.id,
 | 
			
		||||
    title: track.title,
 | 
			
		||||
    // TODO (wvffle): i18n
 | 
			
		||||
    artistName: track.artist?.name ?? 'Unknown artist',
 | 
			
		||||
    // TODO (wvffle): i18n
 | 
			
		||||
    albumTitle: track.album?.title ?? 'Unknown album',
 | 
			
		||||
    artistId: track.artist?.id ?? -1,
 | 
			
		||||
    albumId: track.album?.id ?? -1,
 | 
			
		||||
    coverUrl: (track.cover?.urls ?? track.album?.cover?.urls ?? track.artist?.cover?.urls)?.original
 | 
			
		||||
      ?? new URL('~/assets/audio/default-cover.png', import.meta.url).href,
 | 
			
		||||
    sources: track.uploads.map(upload => ({
 | 
			
		||||
      uuid: upload.uuid,
 | 
			
		||||
      mimetype: upload.mimetype,
 | 
			
		||||
      bitrate: upload.bitrate,
 | 
			
		||||
      url: upload.listen_url
 | 
			
		||||
    }))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const enqueue = async (...newTracks: Track[]) => {
 | 
			
		||||
  const queueTracks = await Promise.all(newTracks.map(createQueueTrack))
 | 
			
		||||
  await setMany(queueTracks.map(track => [track.id, track]))
 | 
			
		||||
 | 
			
		||||
  const ids = queueTracks.map(track => track.id)
 | 
			
		||||
  tracks.value.push(...ids)
 | 
			
		||||
 | 
			
		||||
  // Shuffle new tracks
 | 
			
		||||
  if (isShuffled.value) {
 | 
			
		||||
    shuffledIds.value.push(...shuffleIds(newTracks))
 | 
			
		||||
    shuffledIds.value.push(...shuffleArray(ids))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Current Index
 | 
			
		||||
export const currentIndex = useClamp(useStorage('queue:index', 0), 0, () => tracks.length)
 | 
			
		||||
export const currentTrack = computed(() => tracks[currentIndex.value])
 | 
			
		||||
export const currentIndex = useClamp(useStorage('queue:index', 0), 0, () => tracks.value.length)
 | 
			
		||||
export const currentTrack = computed(() => queue.value[currentIndex.value])
 | 
			
		||||
 | 
			
		||||
// Play track
 | 
			
		||||
export const playTrack = async (trackIndex: number, force = false) => {
 | 
			
		||||
  const { currentSound } = await import('~/composables/audio/tracks')
 | 
			
		||||
  const { isPlaying } = await import('~/composables/audio/player')
 | 
			
		||||
 | 
			
		||||
  if (isPlaying.value) currentSound.value?.pause()
 | 
			
		||||
 | 
			
		||||
  if (force && currentIndex.value === trackIndex) {
 | 
			
		||||
| 
						 | 
				
			
			@ -54,25 +118,29 @@ export const playTrack = async (trackIndex: number, force = false) => {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// Previous track
 | 
			
		||||
export const hasPrevious = computed(() => looping.value === LoopingMode.LoopQueue || currentIndex.value !== 0)
 | 
			
		||||
export const hasPrevious = computed(() => /* looping.value === LoopingMode.LoopQueue || */ currentIndex.value !== 0)
 | 
			
		||||
export const playPrevious = async () => {
 | 
			
		||||
  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) {
 | 
			
		||||
    // Loop track programmatically if it is the only track in the queue
 | 
			
		||||
    if (tracks.length === 1) return playTrack(currentIndex.value, true)
 | 
			
		||||
    return playTrack(tracks.length - 1)
 | 
			
		||||
    if (tracks.value.length === 1) return playTrack(currentIndex.value, true)
 | 
			
		||||
    return playTrack(tracks.value.length - 1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return playTrack(currentIndex.value - 1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Next track
 | 
			
		||||
export const hasNext = computed(() => looping.value === LoopingMode.LoopQueue || currentIndex.value !== tracks.length - 1)
 | 
			
		||||
export const hasNext = computed(() => /* looping.value === LoopingMode.LoopQueue || */ currentIndex.value !== tracks.value.length - 1)
 | 
			
		||||
export const playNext = async () => {
 | 
			
		||||
  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.length - 1) {
 | 
			
		||||
  if (looping.value === LoopingMode.LoopQueue && currentIndex.value === tracks.value.length - 1) {
 | 
			
		||||
    // Loop track programmatically if it is the only track in the queue
 | 
			
		||||
    if (tracks.length === 1) return playTrack(currentIndex.value, true)
 | 
			
		||||
    if (tracks.value.length === 1) return playTrack(currentIndex.value, true)
 | 
			
		||||
    return playTrack(0)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -80,8 +148,7 @@ export const playNext = async () => {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// Shuffle
 | 
			
		||||
const shuffleIds = (tracks: Track[]) => shuffleArray(tracks.map(track => track.id))
 | 
			
		||||
const shuffledIds = useStorage('queue:shuffled-ids', [] as number[])
 | 
			
		||||
const shuffledIds = useStorage('queue:tracks:shuffled', [] as number[])
 | 
			
		||||
export const isShuffled = computed(() => shuffledIds.value.length !== 0)
 | 
			
		||||
export const shuffle = () => {
 | 
			
		||||
  if (isShuffled.value) {
 | 
			
		||||
| 
						 | 
				
			
			@ -89,5 +156,5 @@ export const shuffle = () => {
 | 
			
		|||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  shuffledIds.value = shuffleIds(tracks)
 | 
			
		||||
  shuffledIds.value = shuffleArray(tracks.value)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,13 @@
 | 
			
		|||
import type { Sound, SoundSource } from '~/api/player'
 | 
			
		||||
import type { Track, Upload } from '~/types'
 | 
			
		||||
import type { QueueTrack, QueueTrackSource } from '~/composables/audio/queue'
 | 
			
		||||
import type { Sound } from '~/api/player'
 | 
			
		||||
 | 
			
		||||
import { connectAudioSource } from '~/composables/audio/audio-api'
 | 
			
		||||
import { isPlaying } from '~/composables/audio/player'
 | 
			
		||||
import { soundImplementation } from '~/api/player'
 | 
			
		||||
import { computed, shallowReactive } from 'vue'
 | 
			
		||||
import { playNext, tracks, currentIndex } from '~/composables/audio/queue'
 | 
			
		||||
 | 
			
		||||
import { playNext, queue, currentTrack, currentIndex } from '~/composables/audio/queue'
 | 
			
		||||
import { connectAudioSource } from '~/composables/audio/audio-api'
 | 
			
		||||
import { isPlaying } from '~/composables/audio/player'
 | 
			
		||||
import store from '~/store'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
 | 
			
		||||
const ALLOWED_PLAY_TYPES: (CanPlayTypeResult | undefined)[] = ['maybe', 'probably']
 | 
			
		||||
const AUDIO_ELEMENT = document.createElement('audio')
 | 
			
		||||
| 
						 | 
				
			
			@ -16,42 +15,31 @@ const AUDIO_ELEMENT = document.createElement('audio')
 | 
			
		|||
const soundPromises = new Map<number, Promise<Sound>>()
 | 
			
		||||
const soundCache = shallowReactive(new Map<number, Sound>())
 | 
			
		||||
 | 
			
		||||
const getUploadSources = (uploads: Upload[]): SoundSource[] => {
 | 
			
		||||
  const sources = uploads
 | 
			
		||||
const getTrackSources = (track: QueueTrack): QueueTrackSource[] => {
 | 
			
		||||
  const sources: QueueTrackSource[] = track.sources
 | 
			
		||||
    // NOTE: Filter out repeating and unplayable media types
 | 
			
		||||
    .filter(({ mimetype }, index, array) => array.findIndex((upload) => upload.mimetype === mimetype) === index)
 | 
			
		||||
    .filter(({ mimetype, bitrate }, index, array) => array.findIndex((upload) => upload.mimetype + upload.bitrate === mimetype + bitrate) === index)
 | 
			
		||||
    .filter(({ mimetype }) => ALLOWED_PLAY_TYPES.includes(AUDIO_ELEMENT.canPlayType(`${mimetype}`)))
 | 
			
		||||
    .map((upload): SoundSource => ({
 | 
			
		||||
      ...upload,
 | 
			
		||||
      url: store.getters['instance/absoluteUrl'](upload.listen_url) as string
 | 
			
		||||
    .map((source) => ({
 | 
			
		||||
      ...source,
 | 
			
		||||
      url: store.getters['instance/absoluteUrl'](source.url) as string
 | 
			
		||||
    }))
 | 
			
		||||
 | 
			
		||||
  // NOTE: Add a transcoded MP3 src at the end for browsers
 | 
			
		||||
  //       that do not support other codecs to be able to play it :)
 | 
			
		||||
  if (sources.length > 0 && !sources.some(({ mimetype }) => mimetype === 'audio/mpeg')) {
 | 
			
		||||
    const url = new URL(sources[0].url)
 | 
			
		||||
  if (sources.length > 0) {
 | 
			
		||||
    const original = sources[0]
 | 
			
		||||
    const url = new URL(original.url)
 | 
			
		||||
    url.searchParams.set('to', 'mp3')
 | 
			
		||||
    sources.push({ uuid: 'transcoded', mimetype: 'audio/mpeg', url: url.toString() })
 | 
			
		||||
 | 
			
		||||
    const bitrate = Math.min(320000, original.bitrate ?? Infinity)
 | 
			
		||||
    sources.push({ uuid: 'transcoded', mimetype: 'audio/mpeg', url: url.toString(), bitrate })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return sources
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getTrackSources = async (track: Track): Promise<SoundSource[]> => {
 | 
			
		||||
  if (track === undefined) return []
 | 
			
		||||
 | 
			
		||||
  if (track.uploads.length === 0) {
 | 
			
		||||
    // we don't have any information for this track, we need to fetch it
 | 
			
		||||
    const { uploads } = await axios.get(`tracks/${track.id}/`)
 | 
			
		||||
      .then(response => response.data as Track, () => ({ uploads: [] as Upload[] } as Track))
 | 
			
		||||
 | 
			
		||||
    track.uploads = uploads
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return getUploadSources(track.uploads)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const createSound = async (track: Track): Promise<Sound> => {
 | 
			
		||||
export const createSound = async (track: QueueTrack): Promise<Sound> => {
 | 
			
		||||
  if (soundCache.has(track.id)) {
 | 
			
		||||
    return soundCache.get(track.id) as Sound
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +49,7 @@ export const createSound = async (track: Track): Promise<Sound> => {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  const createSoundPromise = async () => {
 | 
			
		||||
    const sources = await getTrackSources(track)
 | 
			
		||||
    const sources = getTrackSources(track)
 | 
			
		||||
 | 
			
		||||
    const SoundImplementation = soundImplementation.value
 | 
			
		||||
    const sound = new SoundImplementation(sources)
 | 
			
		||||
| 
						 | 
				
			
			@ -85,10 +73,10 @@ export const createSound = async (track: Track): Promise<Sound> => {
 | 
			
		|||
 | 
			
		||||
// Create track from queue
 | 
			
		||||
export const createTrack = async (index: number) => {
 | 
			
		||||
  if (tracks.length <= index || index === -1) return
 | 
			
		||||
  if (queue.value.length <= index || index === -1) return
 | 
			
		||||
  console.log('LOADING TRACK')
 | 
			
		||||
 | 
			
		||||
  const track = tracks[index]
 | 
			
		||||
  const track = queue.value[index]
 | 
			
		||||
  if (!soundPromises.has(track.id) && !soundCache.has(track.id)) {
 | 
			
		||||
    // TODO (wvffle): Resolve race condition - is it still here after adding soundPromises?
 | 
			
		||||
    console.log('NO TRACK IN CACHE, CREATING')
 | 
			
		||||
| 
						 | 
				
			
			@ -105,12 +93,12 @@ export const createTrack = async (index: number) => {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  // NOTE: Preload next track
 | 
			
		||||
  if (index === currentIndex.value && index + 1 < tracks.length) {
 | 
			
		||||
  if (index === currentIndex.value && index + 1 < queue.value.length) {
 | 
			
		||||
    setTimeout(async () => {
 | 
			
		||||
      const sound = await createSound(tracks[index + 1])
 | 
			
		||||
      const sound = await createSound(queue.value[index + 1])
 | 
			
		||||
      await sound.preload()
 | 
			
		||||
    }, 100)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const currentSound = computed(() => soundCache.get(tracks[currentIndex.value]?.id ?? -1))
 | 
			
		||||
export const currentSound = computed(() => soundCache.get(currentTrack.value?.id ?? -1))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,17 @@
 | 
			
		|||
import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types'
 | 
			
		||||
import type { ContentFilter } from '~/store/moderation'
 | 
			
		||||
 | 
			
		||||
import { useStore } from '~/store'
 | 
			
		||||
import { useGettext } from 'vue3-gettext'
 | 
			
		||||
import { computed, markRaw, ref } from 'vue'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import usePlayer from '~/composables/audio/usePlayer'
 | 
			
		||||
import useQueue from '~/composables/audio/useQueue'
 | 
			
		||||
import { useCurrentElement } from '@vueuse/core'
 | 
			
		||||
import { computed, markRaw, ref } from 'vue'
 | 
			
		||||
import { useGettext } from 'vue3-gettext'
 | 
			
		||||
import { useStore } from '~/store'
 | 
			
		||||
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import jQuery from 'jquery'
 | 
			
		||||
 | 
			
		||||
import { enqueue as addToQueue, currentTrack } from '~/composables/audio/queue'
 | 
			
		||||
import { isPlaying } from '~/composables/audio/player'
 | 
			
		||||
 | 
			
		||||
export interface PlayOptionsProps {
 | 
			
		||||
  isPlayable?: boolean
 | 
			
		||||
  tracks?: Track[]
 | 
			
		||||
| 
						 | 
				
			
			@ -24,8 +26,6 @@ export interface PlayOptionsProps {
 | 
			
		|||
 | 
			
		||||
export default (props: PlayOptionsProps) => {
 | 
			
		||||
  const store = useStore()
 | 
			
		||||
  const { resume, pause, playing } = usePlayer()
 | 
			
		||||
  const { currentTrack } = useQueue()
 | 
			
		||||
 | 
			
		||||
  const playable = computed(() => {
 | 
			
		||||
    if (props.isPlayable) {
 | 
			
		||||
| 
						 | 
				
			
			@ -134,7 +134,7 @@ export default (props: PlayOptionsProps) => {
 | 
			
		|||
    jQuery(el.value).find('.ui.dropdown').dropdown('hide')
 | 
			
		||||
 | 
			
		||||
    const tracks = await getPlayableTracks()
 | 
			
		||||
    await store.dispatch('queue/appendMany', { tracks })
 | 
			
		||||
    await addToQueue(...tracks)
 | 
			
		||||
    addMessage(tracks)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -148,40 +148,33 @@ export default (props: PlayOptionsProps) => {
 | 
			
		|||
 | 
			
		||||
    if (next && !wasEmpty) {
 | 
			
		||||
      await store.dispatch('queue/next')
 | 
			
		||||
      resume()
 | 
			
		||||
      isPlaying.value = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addMessage(tracks)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const replacePlay = async () => {
 | 
			
		||||
    store.dispatch('queue/clean')
 | 
			
		||||
    const { tracks, playTrack } = await import('~/composables/audio/queue')
 | 
			
		||||
    tracks.value.length = 0
 | 
			
		||||
 | 
			
		||||
    jQuery(el.value).find('.ui.dropdown').dropdown('hide')
 | 
			
		||||
 | 
			
		||||
    const tracks = await getPlayableTracks()
 | 
			
		||||
    await store.dispatch('queue/appendMany', { tracks })
 | 
			
		||||
    const tracksToPlay = await getPlayableTracks()
 | 
			
		||||
    await addToQueue(...tracksToPlay)
 | 
			
		||||
 | 
			
		||||
    if (props.track && props.tracks?.length) {
 | 
			
		||||
      // set queue position to selected track
 | 
			
		||||
      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)
 | 
			
		||||
    }
 | 
			
		||||
    const trackIndex = props.tracks?.findIndex(track => track.id === props.track?.id && track.position === props.track?.position) ?? 0
 | 
			
		||||
    await playTrack(trackIndex)
 | 
			
		||||
 | 
			
		||||
    resume()
 | 
			
		||||
    addMessage(tracks)
 | 
			
		||||
    isPlaying.value = true
 | 
			
		||||
    playTrack(0, true)
 | 
			
		||||
    addMessage(tracksToPlay)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const activateTrack = (track: Track, index: number) => {
 | 
			
		||||
    // 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()
 | 
			
		||||
      isPlaying.value = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    replacePlay()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ import type { RootState } from '~/store'
 | 
			
		|||
 | 
			
		||||
// eslint-disable-next-line
 | 
			
		||||
import type { ComponentPublicInstance } from '@vue/runtime-core'
 | 
			
		||||
import type { QueueTrack } from './composables/audio/queue'
 | 
			
		||||
 | 
			
		||||
export type FunctionRef = Element | ComponentPublicInstance | null
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -18,11 +19,8 @@ export interface InitModuleContext {
 | 
			
		|||
 | 
			
		||||
export type InitModule = (ctx: InitModuleContext) => void | Promise<void>
 | 
			
		||||
 | 
			
		||||
export interface QueueItemSource {
 | 
			
		||||
export interface QueueItemSource extends Omit<QueueTrack, 'id'> {
 | 
			
		||||
  id: string
 | 
			
		||||
  track: Track
 | 
			
		||||
  duration: string
 | 
			
		||||
  coverUrl: string
 | 
			
		||||
 | 
			
		||||
  labels: {
 | 
			
		||||
    remove: string
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3621,6 +3621,13 @@ iconv-lite@0.6.3:
 | 
			
		|||
  dependencies:
 | 
			
		||||
    safer-buffer ">= 2.1.2 < 3.0.0"
 | 
			
		||||
 | 
			
		||||
idb-keyval@^6.2.0:
 | 
			
		||||
  version "6.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.0.tgz#3af94a3cc0689d6ee0bc9e045d2a3340ea897173"
 | 
			
		||||
  integrity sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    safari-14-idb-fix "^3.0.0"
 | 
			
		||||
 | 
			
		||||
idb@^7.0.1:
 | 
			
		||||
  version "7.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.0.tgz#2cc886be57738419e57f9aab58f647e5e2160270"
 | 
			
		||||
| 
						 | 
				
			
			@ -4780,6 +4787,11 @@ run-parallel@^1.1.9:
 | 
			
		|||
  dependencies:
 | 
			
		||||
    queue-microtask "^1.2.2"
 | 
			
		||||
 | 
			
		||||
safari-14-idb-fix@^3.0.0:
 | 
			
		||||
  version "3.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz#450fc049b996ec7f3fd9ca2f89d32e0761583440"
 | 
			
		||||
  integrity sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog==
 | 
			
		||||
 | 
			
		||||
safe-buffer@^5.1.0:
 | 
			
		||||
  version "5.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Ładowanie…
	
		Reference in New Issue