Fix #1796, Potentially fix #1471

This commit is contained in:
wvffle 2022-07-22 03:02:53 +00:00 committed by Georg Krause
parent 9234720710
commit 53d9015e17
4 changed files with 409 additions and 378 deletions

View File

@ -97,14 +97,14 @@ if (store.state.auth.authenticated) {
<set-instance-modal v-model:show="showSetInstanceModal" /> <set-instance-modal v-model:show="showSetInstanceModal" />
<service-messages /> <service-messages />
<transition name="queue"> <transition name="queue">
<queue v-if="store.state.ui.queueFocused" /> <queue v-show="store.state.ui.queueFocused" />
</transition> </transition>
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<template v-if="Component"> <template v-if="Component">
<keep-alive :max="1"> <keep-alive :max="1">
<Suspense v-if="!store.state.ui.queueFocused"> <Suspense>
<component :is="Component" /> <component :is="Component" v-show="!store.state.ui.queueFocused" />
<template #fallback> <template #fallback>
<!-- TODO (wvffle): Add loader --> <!-- TODO (wvffle): Add loader -->
Loading... Loading...

View File

@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { useStore } from '~/store' import { useStore } from '~/store'
import { nextTick, onMounted, ref, computed, onBeforeMount, onUnmounted } from 'vue' import { nextTick, ref, computed, onBeforeMount, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import time from '~/utils/time' import time from '~/utils/time'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap' import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue' import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue' import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { whenever, useTimeoutFn, useWindowScroll, useWindowSize } from '@vueuse/core' import { whenever, watchDebounced } from '@vueuse/core'
import { useGettext } from 'vue3-gettext' import { useGettext } from 'vue3-gettext'
import useQueue from '~/composables/audio/useQueue' import useQueue from '~/composables/audio/useQueue'
import usePlayer from '~/composables/audio/usePlayer' import usePlayer from '~/composables/audio/usePlayer'
@ -28,33 +28,18 @@ activate()
const store = useStore() const store = useStore()
const currentIndex = computed(() => store.state.queue.currentIndex) const currentIndex = computed(() => store.state.queue.currentIndex)
const { y: pageYOffset } = useWindowScroll()
const { height: windowHeight } = useWindowSize()
const scrollToCurrent = async () => { const scrollToCurrent = async () => {
await nextTick() await nextTick()
const item = queueModal.value?.querySelector('.queue-item.active') const item = queueModal.value?.querySelector('.queue-item.active')
const { top } = item?.getBoundingClientRect() ?? { top: 0 } item?.scrollIntoView({ behavior: store.state.ui.queueFocused ? 'smooth' : 'auto' })
window.scrollTo({
top: top + pageYOffset.value - windowHeight.value / 2,
behavior: 'smooth'
})
} }
onMounted(async () => {
await nextTick()
// NOTE: delay is to let transition work
useTimeoutFn(scrollToCurrent, 400)
})
const { $pgettext } = useGettext() const { $pgettext } = useGettext()
const { const {
playing, playing,
loading: isLoadingAudio, loading: isLoadingAudio,
errored, errored,
focused: playerFocused,
duration, duration,
durationFormatted, durationFormatted,
currentTimeFormatted, currentTimeFormatted,
@ -66,7 +51,6 @@ const {
} = usePlayer() } = usePlayer()
const { const {
focused: queueFocused,
currentTrack, currentTrack,
hasNext, hasNext,
isEmpty: emptyQueue, isEmpty: emptyQueue,
@ -94,7 +78,7 @@ const labels = computed(() => ({
selectTrack: $pgettext('*/*/*', 'Select track') selectTrack: $pgettext('*/*/*', 'Select track')
})) }))
whenever(queueFocused, scrollToCurrent, { immediate: true }) watchDebounced(() => store.state.ui.queueFocused, scrollToCurrent, { debounce: 400 })
whenever(currentTrack, scrollToCurrent, { immediate: true }) whenever(currentTrack, scrollToCurrent, { immediate: true })
whenever( whenever(
@ -124,339 +108,335 @@ const play = (index: number) => {
class="main with-background component-queue" class="main with-background component-queue"
:aria-label="labels.queue" :aria-label="labels.queue"
> >
<div :class="['ui vertical stripe queue segment', playerFocused ? 'player-focused' : '']"> <div id="queue-grid">
<div class="ui fluid container"> <div
<div id="player"
id="queue-grid" class="ui basic segment"
class="ui stackable grid" >
> <template v-if="currentTrack">
<div class="ui six wide column current-track"> <div class="cover-container">
<div <div class="cover">
id="player" <img
class="ui basic segment" 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"
>
</div>
</div>
<h1 class="ui header">
<div class="content ellipsis">
<router-link
class="small header discrete link track"
:to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}"
>
{{ currentTrack.title }}
</router-link>
<div class="sub header ellipsis">
<router-link
class="discrete link artist"
:to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}"
>
{{ currentTrack.artist.name }}
</router-link>
<template v-if="currentTrack.album">
/
<router-link
class="discrete link album"
:to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}"
>
{{ currentTrack.album.title }}
</router-link>
</template>
</div>
</div>
</h1>
<div
v-if="currentTrack && errored"
class="ui small warning message"
>
<h3 class="header">
<translate translate-context="Sidebar/Player/Error message.Title">
The track cannot be loaded
</translate>
</h3>
<p v-if="hasNext && playing && $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>
<i class="loading spinner icon" />
</p>
<p>
<translate translate-context="Sidebar/Player/Error message.Paragraph">
You may have a connectivity issue.
</translate>
</p>
</div>
<div class="additional-controls desktop-and-below">
<track-favorite-icon
v-if="$store.state.auth.authenticated"
:track="currentTrack"
/>
<track-playlist-icon
v-if="$store.state.auth.authenticated"
:track="currentTrack"
/>
<button
v-if="$store.state.auth.authenticated"
:class="['ui', 'really', 'basic', 'circular', 'icon', 'button']"
:aria-label="labels.addArtistContentFilter"
:title="labels.addArtistContentFilter"
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
> >
<template v-if="currentTrack"> <i :class="['eye slash outline', 'basic', 'icon']" />
<img </button>
v-if="currentTrack.cover && currentTrack.cover.urls.large_square_crop" </div>
ref="cover" <div class="progress-wrapper">
alt="" <div
:src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.large_square_crop)" v-if="currentTrack && !errored"
> class="progress-area"
<img >
v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.large_square_crop" <div
ref="cover" ref="progressBar"
alt="" :class="['ui', 'small', 'vibrant', {'indicating': isLoadingAudio}, 'progress']"
:src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.large_square_crop)" @click="touchProgress"
> >
<img
v-else
class="ui image"
alt=""
src="../assets/audio/default-cover.png"
>
<h1 class="ui header">
<div class="content ellipsis">
<router-link
class="small header discrete link track"
:to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}"
>
{{ currentTrack.title }}
</router-link>
<div class="sub header ellipsis">
<router-link
class="discrete link artist"
:to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}"
>
{{ currentTrack.artist.name }}
</router-link>
<template v-if="currentTrack.album">
/
<router-link
class="discrete link album"
:to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}"
>
{{ currentTrack.album.title }}
</router-link>
</template>
</div>
</div>
</h1>
<div <div
v-if="currentTrack && errored" class="buffer bar"
class="ui small warning message" :style="{ 'transform': `translateX(${bufferProgress - 100}%)` }"
> />
<h3 class="header"> <div
<translate translate-context="Sidebar/Player/Error message.Title"> class="position bar"
The track cannot be loaded :style="{ 'transform': `translateX(${progress - 100}%)` }"
</translate> />
</h3> </div>
<p v-if="hasNext && playing && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors"> </div>
<translate translate-context="Sidebar/Player/Error message.Paragraph"> <div
The next track will play automatically in a few seconds v-else
</translate> class="progress-area"
<i class="loading spinner icon" /> >
</p> <div
<p> ref="progress"
<translate translate-context="Sidebar/Player/Error message.Paragraph"> :class="['ui', 'small', 'vibrant', 'progress']"
You may have a connectivity issue. >
</translate> <div class="buffer bar" />
</p> <div class="position bar" />
</div> </div>
<div class="additional-controls tablet-and-below"> </div>
<track-favorite-icon <div class="progress">
v-if="$store.state.auth.authenticated" <template v-if="!isLoadingAudio">
:track="currentTrack" <a
/> href=""
<track-playlist-icon :aria-label="labels.restart"
v-if="$store.state.auth.authenticated" class="left floated timer discrete start"
:track="currentTrack" @click.prevent="currentTime = 0"
/> >{{ currentTimeFormatted }}</a>
<button <span class="right floated timer total">{{ durationFormatted }}</span>
v-if="$store.state.auth.authenticated" </template>
:class="['ui', 'really', 'basic', 'circular', 'icon', 'button']" <template v-else>
:aria-label="labels.addArtistContentFilter" <span class="left floated timer">00:00</span>
:title="labels.addArtistContentFilter" <span class="right floated timer">00:00</span>
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
>
<i :class="['eye slash outline', 'basic', 'icon']" />
</button>
</div>
<div class="progress-wrapper">
<div
v-if="currentTrack && !errored"
class="progress-area"
>
<div
ref="progressBar"
:class="['ui', 'small', 'vibrant', {'indicating': isLoadingAudio}, 'progress']"
@click="touchProgress"
>
<div
class="buffer bar"
:style="{ 'transform': `translateX(${bufferProgress - 100}%)` }"
/>
<div
class="position bar"
:style="{ 'transform': `translateX(${progress - 100}%)` }"
/>
</div>
</div>
<div
v-else
class="progress-area"
>
<div
ref="progress"
:class="['ui', 'small', 'vibrant', 'progress']"
>
<div class="buffer bar" />
<div class="position bar" />
</div>
</div>
<div class="progress">
<template v-if="!isLoadingAudio">
<a
href=""
:aria-label="labels.restart"
class="left floated timer discrete start"
@click.prevent="currentTime = 0"
>{{ currentTimeFormatted }}</a>
<span class="right floated timer total">{{ durationFormatted }}</span>
</template>
<template v-else>
<span class="left floated timer">00:00</span>
<span class="right floated timer">00:00</span>
</template>
</div>
</div>
<div class="player-controls tablet-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>
</template> </template>
</div> </div>
</div> </div>
<div class="ui ten wide column queue-column"> <div class="player-controls desktop-and-below">
<div class="ui basic clearing fixed-header segment"> <span
<h2 class="ui header"> role="button"
<div class="content"> :title="labels.previous"
<button :aria-label="labels.previous"
class="ui right floated basic button" class="control"
@click="$store.commit('ui/queueFocused', null)" :disabled="emptyQueue || null"
> @click.prevent.stop="previous"
<translate translate-context="*/Queue/*/Verb">
Close
</translate>
</button>
<button
class="ui right floated basic button danger"
@click="clear"
>
<translate translate-context="*/Queue/*/Verb">
Clear
</translate>
</button>
{{ labels.queue }}
<div class="sub header">
<div>
<translate
translate-context="Sidebar/Queue/Text"
:translate-params="{index: currentIndex + 1, length: tracks.length}"
>
Track %{ index } of %{ length }
</translate>
<template v-if="!$store.state.radios.running">
-
<span :title="labels.duration">
{{ timeLeft }}
</span>
</template>
</div>
</div>
</div>
</h2>
</div>
<table class="ui compact very basic fixed single line selectable unstackable table">
<draggable
v-model="tracks"
tag="tbody"
handle=".handle"
item-key="id"
@update="reorder"
>
<template #item="{ element: track, index }">
<tr
:key="track.id"
:class="['queue-item', {'active': index === currentIndex}]"
>
<td class="handle">
<i class="grip lines icon" />
</td>
<td
class="image-cell"
@click="play(index)"
>
<img
v-if="track.cover && track.cover.urls.original"
class="ui mini image"
alt=""
:src="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)"
>
<img
v-else-if="track.album && track.album.cover && track.album.cover.urls.original"
class="ui mini image"
alt=""
:src="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)"
>
<img
v-else
class="ui mini image"
alt=""
src="../assets/audio/default-cover.png"
>
</td>
<td
colspan="3"
@click="play(index)"
>
<button
class="title reset ellipsis"
:title="track.title"
:aria-label="labels.selectTrack"
>
<strong>{{ track.title }}</strong><br>
<span>
{{ track.artist.name }}
</span>
</button>
</td>
<td class="duration-cell">
<template v-if="track.uploads.length > 0">
{{ time.durationFormatted(track.uploads[0].duration) }}
</template>
</td>
<td class="controls">
<template v-if="$store.getters['favorites/isFavorite'](track.id)">
<i class="pink heart icon" />
</template>
<button
:aria-label="labels.remove"
:title="labels.remove"
:class="['ui', 'really', 'tiny', 'basic', 'circular', 'icon', 'button']"
@click.stop="removeTrack(index)"
>
<i class="x icon" />
</button>
</td>
</tr>
</template>
</draggable>
</table>
<div
v-if="$store.state.radios.running"
class="ui info message"
> >
<div class="content"> <i :class="['ui', 'backward step', {'disabled': emptyQueue}, 'icon']" />
<h3 class="header"> </span>
<i class="feed icon" /> <translate translate-context="Sidebar/Player/Title">
You have a radio playing <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>
</template>
</div>
<div>
<div class="ui basic clearing segment">
<h2 class="ui header">
<div class="content">
<button
class="ui right floated basic button"
@click="$store.commit('ui/queueFocused', null)"
>
<translate translate-context="*/Queue/*/Verb">
Close
</translate>
</button>
<button
class="ui right floated basic button danger"
@click="clear"
>
<translate translate-context="*/Queue/*/Verb">
Clear
</translate>
</button>
{{ labels.queue }}
<div class="sub header">
<div>
<translate
translate-context="Sidebar/Queue/Text"
:translate-params="{index: currentIndex + 1, length: tracks.length}"
>
Track %{ index } of %{ length }
</translate> </translate>
</h3> <template v-if="!$store.state.radios.running">
<p> -
<translate translate-context="Sidebar/Player/Paragraph"> <span :title="labels.duration">
New tracks will be appended here automatically. {{ timeLeft }}
</translate> </span>
</p> </template>
<button </div>
class="ui basic primary button"
@click="$store.dispatch('radios/stop')"
>
<translate translate-context="*/Player/Button.Label/Short, Verb">
Stop radio
</translate>
</button>
</div> </div>
</div> </div>
</h2>
</div>
<div>
<table class="ui compact very basic fixed single line selectable unstackable table">
<draggable
v-model="tracks"
handle=".handle"
item-key="id"
tag="tbody"
@update="reorder"
>
<template #item="{ element: track, index }">
<tr
:key="track.id"
:class="['queue-item', {'active': index === currentIndex}]"
>
<td class="handle">
<i class="grip lines icon" />
</td>
<td
class="image-cell"
@click="play(index)"
>
<img
v-if="track.cover && track.cover.urls.original"
class="ui mini image"
alt=""
:src="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)"
>
<img
v-else-if="track.album && track.album.cover && track.album.cover.urls.original"
class="ui mini image"
alt=""
:src="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)"
>
<img
v-else
class="ui mini image"
alt=""
src="../assets/audio/default-cover.png"
>
</td>
<td
colspan="3"
@click="play(index)"
>
<button
class="title reset ellipsis"
:title="track.title"
:aria-label="labels.selectTrack"
>
<strong>{{ track.title }}</strong><br>
<span>
{{ track.artist.name }}
</span>
</button>
</td>
<td class="duration-cell">
<template v-if="track.uploads.length > 0">
{{ time.durationFormatted(track.uploads[0].duration) }}
</template>
</td>
<td class="controls">
<template v-if="$store.getters['favorites/isFavorite'](track.id)">
<i class="pink heart icon" />
</template>
<button
:aria-label="labels.remove"
:title="labels.remove"
:class="['ui', 'really', 'tiny', 'basic', 'circular', 'icon', 'button']"
@click.stop="removeTrack(index)"
>
<i class="x icon" />
</button>
</td>
</tr>
</template>
</draggable>
</table>
<div
v-if="$store.state.radios.running"
class="ui info message"
>
<div class="content">
<h3 class="header">
<i class="feed icon" /> <translate translate-context="Sidebar/Player/Title">
You have a radio playing
</translate>
</h3>
<p>
<translate translate-context="Sidebar/Player/Paragraph">
New tracks will be appended here automatically.
</translate>
</p>
<button
class="ui basic primary button"
@click="$store.dispatch('radios/stop')"
>
<translate translate-context="*/Player/Button.Label/Short, Verb">
Stop radio
</translate>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,28 +1,26 @@
.queue.segment.player-focused #queue-grid #player {
@include media("<desktop") {
padding-bottom: $bottom-player-height + 2rem;
}
}
.queue-controls { .queue-controls {
@include media("<desktop") { @include media("<desktop") {
height: $bottom-player-height; height: $bottom-player-height;
} }
} }
.ui.fixed-header.segment { .ui.clearing.segment {
background-color: var(--site-background); background-color: var(--site-background);
box-shadow: var(--secondary-menu-box-shadow); box-shadow: var(--secondary-menu-box-shadow);
margin: 0 !important;
} }
.queue-enter-active, .queue-leave-active {
transition: all 0.2s ease-in-out; .queue-enter-active,
.current-track, .queue-column { .queue-leave-active {
opacity: 0; transition: all 0.2s ease;
} will-change: transform, opacity;
} }
.queue-enter, .queue-leave-to {
transform: translateY(100vh); .queue-enter-from,
.queue-leave-to {
opacity: 0; opacity: 0;
transform: translateY(5vh);
} }
.component-queue { .component-queue {
@ -33,11 +31,10 @@
} }
} }
&.main { &.main {
position: absolute; position: fixed;
min-height: 100vh; min-height: 100vh;
width: 100vw; width: 100vw;
z-index: 1000; z-index: 1000;
padding-bottom: 3em;
} }
&.main > .button { &.main > .button {
position: fixed; position: fixed;
@ -48,25 +45,13 @@
display: none; display: none;
} }
} }
.queue.segment:not(.player-focused) {
#player {
@include media("<desktop") {
height: 0;
display: none;
}
}
}
.queue.segment #player { .queue.segment #player {
padding: 0em; padding: 0em;
> * { > * {
padding: 0.5em; padding: 0.5em;
} }
} }
.player-focused .grid > .ui.queue-column {
@include media("<desktop") {
display: none;
}
}
.queue-column { .queue-column {
overflow-y: auto; overflow-y: auto;
} }
@ -116,7 +101,7 @@
@include media("<desktop") { @include media("<desktop") {
padding: 1em; padding: 1em;
} }
@include media(">desktop") { @include media(">=desktop") {
right: 1em; right: 1em;
left: 38%; left: 38%;
} }
@ -168,8 +153,7 @@
overflow: hidden; overflow: hidden;
} }
.progress-wrapper, .warning.message { .progress-wrapper, .warning.message {
max-width: 25em; width: 25em;
margin: 0 auto;
} }
.ui.progress .bar { .ui.progress .bar {
transition: none; transition: none;
@ -243,3 +227,65 @@
} }
} }
// Wvffle's styles
.component-queue {
#queue-grid {
display: grid;
grid-template-columns: 37.5% 62.5%;
@include media("<desktop") {
grid-template-columns: 1fr 0;
}
#player {
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
text-align: center;
.cover-container {
width: 50vh;
max-width: 90%;
.cover {
height: 0;
width: 100%;
padding-bottom: 100%;
position: relative;
img {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
}
}
}
.progress-wrapper {
font-size: 1.5rem;
width: 54vh;
max-width: 90%;
}
}
> div {
height: calc(100vh - 4rem);
margin: 0 !important;
&:nth-child(2) {
display: grid;
grid-template-rows: auto 1fr;
> :nth-child(2) {
overflow-y: auto;
padding-bottom: 2rem;
}
}
}
}
}

View File

@ -53,6 +53,11 @@ a {
display: none !important; display: none !important;
} }
} }
.desktop-and-below {
@include media(">=desktop") {
display: none !important;
}
}
.tablet-and-up { .tablet-and-up {
@include media("<tablet") { @include media("<tablet") {
display: none !important; display: none !important;