funkwhale/front/src/components/audio/Player.vue

364 wiersze
12 KiB
Vue
Czysty Zwykły widok Historia

<script setup lang="ts">
2022-10-28 07:34:24 +00:00
import { usePlayer } from '~/composables/audio/player'
import { useQueue } from '~/composables/audio/queue'
import { useMouse, useWindowSize } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useStore } from '~/store'
2022-09-08 14:32:45 +00:00
import { useI18n } from 'vue-i18n'
2022-10-28 07:34:24 +00:00
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
import time from '~/utils/time'
2022-10-28 13:24:25 +00:00
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
2022-10-28 07:34:24 +00:00
import PlayerControls from './PlayerControls.vue'
2022-09-08 14:32:45 +00:00
import VolumeControl from './VolumeControl.vue'
2022-10-28 07:34:24 +00:00
const {
2022-10-18 21:48:47 +00:00
LoopingMode,
initializeFirstTrack,
isPlaying,
mute,
volume,
toggleLooping,
looping,
seekBy,
seekTo,
currentTime,
duration,
progress,
bufferProgress,
2022-10-28 07:34:24 +00:00
loading: isLoadingAudio
} = usePlayer()
2022-10-18 21:48:47 +00:00
2022-10-28 07:34:24 +00:00
const {
2022-10-20 08:51:41 +00:00
playPrevious,
playNext,
2022-10-23 07:41:38 +00:00
queue,
2022-10-20 08:51:41 +00:00
currentIndex,
currentTrack,
2022-10-28 12:59:54 +00:00
isShuffled,
2022-10-28 16:35:21 +00:00
shuffle,
clear
2022-10-28 07:34:24 +00:00
} = useQueue()
const store = useStore()
2022-09-08 14:32:45 +00:00
const { t } = useI18n()
const toggleMobilePlayer = () => {
store.commit('ui/queueFocused', ['queue', 'player'].includes(store.state.ui.queueFocused as string) ? null : 'player')
}
// Key binds
onKeyboardShortcut('e', toggleMobilePlayer)
2022-10-18 21:48:47 +00:00
onKeyboardShortcut('p', () => { isPlaying.value = !isPlaying.value })
onKeyboardShortcut('s', shuffle)
2022-10-28 16:35:21 +00:00
onKeyboardShortcut('q', clear)
2022-10-18 21:48:47 +00:00
onKeyboardShortcut('m', mute)
onKeyboardShortcut('l', toggleLooping)
onKeyboardShortcut('f', () => store.dispatch('favorites/toggle', currentTrack.value?.id))
onKeyboardShortcut('escape', () => store.commit('ui/queueFocused', null))
2022-10-18 21:48:47 +00:00
onKeyboardShortcut(['shift', 'up'], () => (volume.value += 0.1), true)
onKeyboardShortcut(['shift', 'down'], () => (volume.value -= 0.1), true)
2022-10-18 21:48:47 +00:00
onKeyboardShortcut('right', () => seekBy(5), true)
onKeyboardShortcut(['shift', 'right'], () => seekBy(30), true)
onKeyboardShortcut('left', () => seekBy(-5), true)
onKeyboardShortcut(['shift', 'left'], () => seekBy(-30), true)
2022-10-20 08:51:41 +00:00
onKeyboardShortcut(['ctrl', 'shift', 'left'], playPrevious, true)
onKeyboardShortcut(['ctrl', 'shift', 'right'], playNext, true)
const labels = computed(() => ({
2022-09-18 23:12:39 +00:00
audioPlayer: t('components.audio.Player.label.audioPlayer'),
previous: t('components.audio.Player.label.previousTrack'),
play: t('components.audio.Player.label.play'),
pause: t('components.audio.Player.label.pause'),
next: t('components.audio.Player.label.nextTrack'),
unmute: t('components.audio.Player.label.unmute'),
mute: t('components.audio.Player.label.mute'),
expandQueue: t('components.audio.Player.label.expandQueue'),
shuffle: t('components.audio.Player.label.shuffleQueue'),
clear: t('components.audio.Player.label.clearQueue'),
addArtistContentFilter: t('components.audio.Player.label.addArtistContentFilter')
}))
const switchTab = () => {
store.commit('ui/queueFocused', store.state.ui.queueFocused === 'player' ? 'queue' : 'player')
}
2022-07-21 15:05:24 +00:00
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
2022-10-18 21:48:47 +00:00
seekTo(time)
2022-07-21 15:05:24 +00:00
}
2022-07-25 02:05:33 +00:00
2022-10-18 21:48:47 +00:00
const { x } = useMouse({ type: 'client' })
const { width: screenWidth } = useWindowSize({ includeScrollbar: false })
initializeFirstTrack()
2022-10-18 21:48:47 +00:00
const loopingTitle = computed(() => {
const mode = looping.value
return mode === LoopingMode.None
2022-11-27 12:15:43 +00:00
? t('components.audio.Player.label.loopingDisabled')
2022-10-18 21:48:47 +00:00
: mode === LoopingMode.LoopTrack
2022-11-27 12:15:43 +00:00
? t('components.audio.Player.label.loopingSingle')
: t('components.audio.Player.label.loopingWholeQueue')
2022-10-18 21:48:47 +00:00
})
2022-10-28 13:24:25 +00:00
const hideArtist = () => {
if (currentTrack.value.artistId !== -1) {
return store.dispatch('moderation/hide', {
type: 'artist',
target: {
id: currentTrack.value.artistId,
name: currentTrack.value.artistName
}
})
}
}
</script>
<template>
2021-12-06 10:35:20 +00:00
<section
v-if="currentTrack"
role="complementary"
class="player-wrapper ui bottom-player component-player"
aria-labelledby="player-label"
>
<h1
id="player-label"
class="visually-hidden"
>
2022-09-18 23:12:39 +00:00
{{ $t('components.audio.Player.header.player') }}
</h1>
2021-12-06 10:35:20 +00:00
<div
class="ui inverted segment fixed-controls"
@click.prevent.stop="toggleMobilePlayer"
>
<div
2022-07-21 15:05:24 +00:00
ref="progressBar"
2021-12-06 10:35:20 +00:00
:class="['ui', 'top attached', 'small', 'inverted', {'indicating': isLoadingAudio}, 'progress']"
2022-07-21 15:05:24 +00:00
@click.prevent.stop="touchProgress"
2021-12-06 10:35:20 +00:00
>
<div
class="buffer bar"
:style="{ 'transform': `translateX(${bufferProgress - 100}%)` }"
/>
2021-12-06 10:35:20 +00:00
<div
class="position bar"
2022-10-18 21:48:47 +00:00
:style="{ 'transform': `translateX(${progress - 100}%)` }"
2021-12-06 10:35:20 +00:00
/>
2022-07-25 02:05:33 +00:00
<div
class="seek bar"
:style="{ 'transform': `translateX(${x / screenWidth * 100 - 100}%)` }"
2022-07-25 02:05:33 +00:00
/>
</div>
<div class="controls-row">
<div class="controls track-controls queue-not-focused desktop-and-up">
2021-12-06 10:35:20 +00:00
<div
class="ui tiny image"
@click.stop.prevent="$router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})"
>
<img
ref="cover"
alt=""
2022-10-23 07:41:38 +00:00
:src="$store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
2021-12-06 10:35:20 +00:00
>
</div>
2021-12-06 10:35:20 +00:00
<div
class="middle aligned content ellipsis"
@click.stop.prevent=""
>
<strong>
2021-12-06 10:35:20 +00:00
<router-link
class="small header discrete link track"
:to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}"
@click.stop.prevent=""
>
{{ currentTrack.title }}
</router-link>
</strong>
<div class="meta">
2021-12-06 10:35:20 +00:00
<router-link
class="discrete link"
2022-10-23 07:41:38 +00:00
:to="{name: 'library.artists.detail', params: {id: currentTrack.artistId }}"
2021-12-06 10:35:20 +00:00
@click.stop.prevent=""
>
2022-10-23 07:41:38 +00:00
{{ currentTrack.artistName }}
2021-12-06 10:35:20 +00:00
</router-link>
2022-10-23 07:41:38 +00:00
<template v-if="currentTrack.albumId !== -1">
2022-09-18 20:57:41 +00:00
<span class="middle slash symbol" />
2021-12-06 10:35:20 +00:00
<router-link
class="discrete link"
2022-10-23 07:41:38 +00:00
:to="{name: 'library.albums.detail', params: {id: currentTrack.albumId }}"
2021-12-06 10:35:20 +00:00
@click.stop.prevent=""
>
2022-10-23 07:41:38 +00:00
{{ currentTrack.albumTitle }}
2021-12-06 10:35:20 +00:00
</router-link>
2021-01-03 16:26:09 +00:00
</template>
</div>
</div>
</div>
<div class="controls track-controls queue-not-focused desktop-and-below">
<div class="ui tiny image">
2021-12-06 10:35:20 +00:00
<img
ref="cover"
alt=""
2022-10-23 07:41:38 +00:00
:src="$store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
2021-12-06 10:35:20 +00:00
>
</div>
<div class="middle aligned content ellipsis">
<strong>
{{ currentTrack.title }}
</strong>
<div class="meta">
2022-10-23 07:41:38 +00:00
{{ currentTrack.artistName }}
<template v-if="currentTrack.albumId !== -1">
2022-09-18 20:57:41 +00:00
<span class="middle slash symbol" />
{{ currentTrack.albumTitle }}
2021-12-06 10:35:20 +00:00
</template>
</div>
</div>
</div>
2021-12-06 10:35:20 +00:00
<div
v-if="$store.state.auth.authenticated"
class="controls desktop-and-up fluid align-right"
>
2022-10-28 13:24:25 +00:00
<track-favorite-icon
class="control white"
2021-12-06 10:35:20 +00:00
:track="currentTrack"
/>
<track-playlist-icon
class="control white"
2021-12-06 10:35:20 +00:00
:track="currentTrack"
/>
<button
:class="['ui', 'really', 'basic', 'circular', 'icon', 'button', 'control']"
:aria-label="labels.addArtistContentFilter"
2021-12-06 10:35:20 +00:00
:title="labels.addArtistContentFilter"
2022-10-28 13:24:25 +00:00
@click="hideArtist"
2021-12-06 10:35:20 +00:00
>
<i :class="['eye slash outline', 'basic', 'icon']" />
2022-10-28 13:24:25 +00:00
</button>
</div>
2022-10-25 19:07:36 +00:00
<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">
2021-12-06 10:35:20 +00:00
<span
class="start"
2022-10-18 21:48:47 +00:00
@click.stop.prevent="seekTo(0)"
2022-07-03 21:36:27 +00:00
>
2022-10-25 19:07:36 +00:00
{{ time.parse(Math.round(currentTime)) }}
2022-07-03 21:36:27 +00:00
</span>
2022-09-18 20:57:41 +00:00
<span class="middle pipe symbol" />
2022-10-18 21:48:47 +00:00
<span class="total">{{ time.parse(Math.round(duration)) }}</span>
</template>
</div>
</div>
<div class="controls queue-controls when-queue-focused align-right">
<div class="group">
<volume-control class="expandable" />
<button
2021-12-06 10:35:20 +00:00
class="circular control button"
2022-10-18 21:48:47 +00:00
:class="{ looping: looping !== LoopingMode.None }"
:title="loopingTitle"
:aria-label="loopingTitle"
:disabled="!currentTrack"
2022-10-18 21:48:47 +00:00
@click.prevent.stop="toggleLooping"
2021-12-06 10:35:20 +00:00
>
2022-10-18 21:48:47 +00:00
<i class="repeat icon">
<span
v-if="looping !== LoopingMode.None"
class="ui circular tiny vibrant label"
>
2022-11-27 12:15:43 +00:00
<span
v-if="looping === LoopingMode.LoopTrack"
class="symbol single"
/>
<span
v-else-if="looping === LoopingMode.LoopQueue"
class="infinity symbol"
/>
2022-10-18 21:48:47 +00:00
</span>
</i>
</button>
2022-10-18 21:48:47 +00:00
<button
class="circular control button"
2022-10-23 07:41:38 +00:00
:disabled="queue.length === 0"
:title="labels.shuffle"
:aria-label="labels.shuffle"
@click.prevent.stop="shuffle()"
2021-12-06 10:35:20 +00:00
>
2022-10-28 12:59:54 +00:00
<i :class="['ui', 'random', { disabled: queue.length === 0, shuffling: isShuffled }, 'icon']" />
</button>
</div>
<div class="group">
<div class="fake-dropdown">
2021-12-06 10:35:20 +00:00
<button
class="position circular control button desktop-and-up"
aria-expanded="true"
@click.stop="toggleMobilePlayer"
>
<i class="stream icon" />
2022-09-15 23:01:21 +00:00
<span>
2022-09-18 23:12:39 +00:00
{{ $t('components.audio.Player.meta.position', { index: currentIndex + 1, length: queue.length }) }}
2022-09-15 23:01:21 +00:00
</span>
</button>
2021-12-06 10:35:20 +00:00
<button
class="position circular control button desktop-and-below"
2021-12-06 10:35:20 +00:00
@click.stop="switchTab"
>
<i class="stream icon" />
2022-09-17 02:14:35 +00:00
<span>
2022-09-18 23:12:39 +00:00
{{ $t('components.audio.Player.meta.position', { index: currentIndex + 1, length: queue.length }) }}
2022-09-17 02:14:35 +00:00
</span>
</button>
<button
v-if="$store.state.ui.queueFocused"
2021-12-06 10:35:20 +00:00
class="circular control button close-control desktop-and-up"
@click.stop="toggleMobilePlayer"
>
<i class="large down angle icon" />
</button>
<button
v-else
2021-12-06 10:35:20 +00:00
class="circular control button desktop-and-up"
@click.stop="toggleMobilePlayer"
>
<i class="large up angle icon" />
</button>
<button
v-if="$store.state.ui.queueFocused === 'player'"
class="circular control button close-control desktop-and-below"
2021-12-06 10:35:20 +00:00
@click.stop="switchTab"
>
<i class="large up angle icon" />
</button>
<button
v-if="$store.state.ui.queueFocused === 'queue'"
class="circular control button desktop-and-below"
2021-12-06 10:35:20 +00:00
@click.stop="switchTab"
>
<i class="large down angle icon" />
</button>
</div>
<button
class="circular control button close-control desktop-and-below"
2021-12-06 10:35:20 +00:00
@click.stop="$store.commit('ui/queueFocused', null)"
>
<i class="x icon" />
</button>
</div>
</div>
</div>
</div>
</section>
</template>