parent
9234720710
commit
53d9015e17
|
@ -97,14 +97,14 @@ if (store.state.auth.authenticated) {
|
|||
<set-instance-modal v-model:show="showSetInstanceModal" />
|
||||
<service-messages />
|
||||
<transition name="queue">
|
||||
<queue v-if="store.state.ui.queueFocused" />
|
||||
<queue v-show="store.state.ui.queueFocused" />
|
||||
</transition>
|
||||
|
||||
<router-view v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<keep-alive :max="1">
|
||||
<Suspense v-if="!store.state.ui.queueFocused">
|
||||
<component :is="Component" />
|
||||
<Suspense>
|
||||
<component :is="Component" v-show="!store.state.ui.queueFocused" />
|
||||
<template #fallback>
|
||||
<!-- TODO (wvffle): Add loader -->
|
||||
Loading...
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
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 time from '~/utils/time'
|
||||
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { whenever, useTimeoutFn, useWindowScroll, useWindowSize } from '@vueuse/core'
|
||||
import { whenever, watchDebounced } from '@vueuse/core'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import useQueue from '~/composables/audio/useQueue'
|
||||
import usePlayer from '~/composables/audio/usePlayer'
|
||||
|
@ -28,33 +28,18 @@ activate()
|
|||
const store = useStore()
|
||||
const currentIndex = computed(() => store.state.queue.currentIndex)
|
||||
|
||||
const { y: pageYOffset } = useWindowScroll()
|
||||
const { height: windowHeight } = useWindowSize()
|
||||
const scrollToCurrent = async () => {
|
||||
await nextTick()
|
||||
|
||||
const item = queueModal.value?.querySelector('.queue-item.active')
|
||||
const { top } = item?.getBoundingClientRect() ?? { top: 0 }
|
||||
|
||||
window.scrollTo({
|
||||
top: top + pageYOffset.value - windowHeight.value / 2,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
item?.scrollIntoView({ behavior: store.state.ui.queueFocused ? 'smooth' : 'auto' })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
// NOTE: delay is to let transition work
|
||||
useTimeoutFn(scrollToCurrent, 400)
|
||||
})
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
|
||||
const {
|
||||
playing,
|
||||
loading: isLoadingAudio,
|
||||
errored,
|
||||
focused: playerFocused,
|
||||
duration,
|
||||
durationFormatted,
|
||||
currentTimeFormatted,
|
||||
|
@ -66,7 +51,6 @@ const {
|
|||
} = usePlayer()
|
||||
|
||||
const {
|
||||
focused: queueFocused,
|
||||
currentTrack,
|
||||
hasNext,
|
||||
isEmpty: emptyQueue,
|
||||
|
@ -94,7 +78,7 @@ const labels = computed(() => ({
|
|||
selectTrack: $pgettext('*/*/*', 'Select track')
|
||||
}))
|
||||
|
||||
whenever(queueFocused, scrollToCurrent, { immediate: true })
|
||||
watchDebounced(() => store.state.ui.queueFocused, scrollToCurrent, { debounce: 400 })
|
||||
whenever(currentTrack, scrollToCurrent, { immediate: true })
|
||||
|
||||
whenever(
|
||||
|
@ -124,339 +108,335 @@ const play = (index: number) => {
|
|||
class="main with-background component-queue"
|
||||
:aria-label="labels.queue"
|
||||
>
|
||||
<div :class="['ui vertical stripe queue segment', playerFocused ? 'player-focused' : '']">
|
||||
<div class="ui fluid container">
|
||||
<div
|
||||
id="queue-grid"
|
||||
class="ui stackable grid"
|
||||
>
|
||||
<div class="ui six wide column current-track">
|
||||
<div
|
||||
id="player"
|
||||
class="ui basic segment"
|
||||
<div id="queue-grid">
|
||||
<div
|
||||
id="player"
|
||||
class="ui basic segment"
|
||||
>
|
||||
<template v-if="currentTrack">
|
||||
<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"
|
||||
>
|
||||
</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">
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<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
|
||||
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 tablet-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})"
|
||||
>
|
||||
<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>
|
||||
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="ui ten wide column queue-column">
|
||||
<div class="ui basic clearing fixed-header 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>
|
||||
<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="player-controls desktop-and-below">
|
||||
<span
|
||||
role="button"
|
||||
:title="labels.previous"
|
||||
:aria-label="labels.previous"
|
||||
class="control"
|
||||
:disabled="emptyQueue || null"
|
||||
@click.prevent.stop="previous"
|
||||
>
|
||||
<div class="content">
|
||||
<h3 class="header">
|
||||
<i class="feed icon" /> <translate translate-context="Sidebar/Player/Title">
|
||||
You have a radio playing
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
<template v-if="!$store.state.radios.running">
|
||||
-
|
||||
<span :title="labels.duration">
|
||||
{{ timeLeft }}
|
||||
</span>
|
||||
</template>
|
||||
</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>
|
||||
|
|
|
@ -1,28 +1,26 @@
|
|||
|
||||
.queue.segment.player-focused #queue-grid #player {
|
||||
@include media("<desktop") {
|
||||
padding-bottom: $bottom-player-height + 2rem;
|
||||
}
|
||||
}
|
||||
.queue-controls {
|
||||
|
||||
@include media("<desktop") {
|
||||
height: $bottom-player-height;
|
||||
}
|
||||
}
|
||||
.ui.fixed-header.segment {
|
||||
.ui.clearing.segment {
|
||||
background-color: var(--site-background);
|
||||
box-shadow: var(--secondary-menu-box-shadow);
|
||||
margin: 0 !important;
|
||||
}
|
||||
.queue-enter-active, .queue-leave-active {
|
||||
transition: all 0.2s ease-in-out;
|
||||
.current-track, .queue-column {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.queue-enter-active,
|
||||
.queue-leave-active {
|
||||
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;
|
||||
transform: translateY(5vh);
|
||||
}
|
||||
|
||||
.component-queue {
|
||||
|
@ -33,11 +31,10 @@
|
|||
}
|
||||
}
|
||||
&.main {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
z-index: 1000;
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
&.main > .button {
|
||||
position: fixed;
|
||||
|
@ -48,25 +45,13 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
.queue.segment:not(.player-focused) {
|
||||
#player {
|
||||
@include media("<desktop") {
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.queue.segment #player {
|
||||
padding: 0em;
|
||||
> * {
|
||||
padding: 0.5em;
|
||||
}
|
||||
}
|
||||
.player-focused .grid > .ui.queue-column {
|
||||
@include media("<desktop") {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.queue-column {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
@ -116,7 +101,7 @@
|
|||
@include media("<desktop") {
|
||||
padding: 1em;
|
||||
}
|
||||
@include media(">desktop") {
|
||||
@include media(">=desktop") {
|
||||
right: 1em;
|
||||
left: 38%;
|
||||
}
|
||||
|
@ -168,8 +153,7 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
.progress-wrapper, .warning.message {
|
||||
max-width: 25em;
|
||||
margin: 0 auto;
|
||||
width: 25em;
|
||||
}
|
||||
.ui.progress .bar {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -53,6 +53,11 @@ a {
|
|||
display: none !important;
|
||||
}
|
||||
}
|
||||
.desktop-and-below {
|
||||
@include media(">=desktop") {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
.tablet-and-up {
|
||||
@include media("<tablet") {
|
||||
display: none !important;
|
||||
|
|
Loading…
Reference in New Issue