refactor, style(front): Player queue
This commit is contained in:
parent
406e852bea
commit
8766bac14f
|
@ -9,15 +9,13 @@ import { color } from '~/composables/color';
|
|||
|
||||
import { generateTrackCreditStringFromQueue } from '~/utils/utils'
|
||||
|
||||
// TODO: Check if these modals are still needed:
|
||||
// import ChannelUploadModal from '~/components/channels/UploadModal.vue'
|
||||
// import PlaylistModal from '~/components/playlists/PlaylistModal.vue'
|
||||
// import FilterModal from '~/components/moderation/FilterModal.vue'
|
||||
// import ReportModal from '~/components/moderation/ReportModal.vue'
|
||||
// import ServiceMessages from '~/components/ServiceMessages.vue'
|
||||
// TODO: Modernise and add player/queue:
|
||||
import PlaylistModal from '~/components/playlists/PlaylistModal.vue'
|
||||
import FilterModal from '~/components/moderation/FilterModal.vue'
|
||||
import ReportModal from '~/components/moderation/ReportModal.vue'
|
||||
import ServiceMessages from '~/components/ServiceMessages.vue'
|
||||
import AudioPlayer from '~/components/audio/Player.vue'
|
||||
// import Queue from '~/components/Queue.vue'
|
||||
import Queue from '~/components/Queue.vue'
|
||||
import Sidebar from '~/ui/components/Sidebar.vue'
|
||||
import ShortcutsModal from '~/ui/modals/Shortcuts.vue'
|
||||
import LanguagesModal from '~/ui/modals/Languages.vue'
|
||||
|
@ -83,8 +81,15 @@ store.dispatch('auth/fetchUser')
|
|||
<Sidebar />
|
||||
<RouterView v-bind="color({}, ['default', 'solid'])()" />
|
||||
<AudioPlayer />
|
||||
<transition name="queue">
|
||||
<Queue v-show="store.state.ui.queueFocused" />
|
||||
</transition>
|
||||
<LanguagesModal />
|
||||
<ShortcutsModal />
|
||||
<!-- TODO: Make modals available -->
|
||||
<!-- <PlaylistModal /> -->
|
||||
<!-- <FilterModal /> -->
|
||||
<!-- <ReportModal /> -->
|
||||
<UploadModal />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -22,6 +22,8 @@ import PlayerControls from '~/components/audio/PlayerControls.vue'
|
|||
import VirtualList from '~/components/vui/list/VirtualList.vue'
|
||||
import QueueItem from '~/components/QueueItem.vue'
|
||||
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
|
||||
const MilkDrop = defineAsyncComponent(() => import('~/components/audio/visualizer/MilkDrop.vue'))
|
||||
|
||||
const {
|
||||
|
@ -174,7 +176,7 @@ if (!isWebGLSupported) {
|
|||
|
||||
<template>
|
||||
<section
|
||||
class="main with-background component-queue"
|
||||
class="main opaque component-queue"
|
||||
:aria-label="labels.queue"
|
||||
>
|
||||
<div
|
||||
|
@ -214,46 +216,39 @@ if (!isWebGLSupported) {
|
|||
class="cover-buttons"
|
||||
>
|
||||
<tooltip :content="!isWebGLSupported && t('components.Queue.message.webglUnsupported')">
|
||||
<button
|
||||
<Button
|
||||
v-if="coverType === CoverType.COVER_ART"
|
||||
class="ui secondary button"
|
||||
:aria-label="labels.showVisualizer"
|
||||
:title="labels.showVisualizer"
|
||||
:disabled="!isWebGLSupported"
|
||||
icon="bi-display"
|
||||
@click="coverType = CoverType.MILK_DROP"
|
||||
>
|
||||
<i class="icon signal" />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<Button
|
||||
v-else-if="coverType === CoverType.MILK_DROP"
|
||||
class="ui secondary button"
|
||||
:aria-label="labels.showCoverArt"
|
||||
:title="labels.showCoverArt"
|
||||
:disabled="!isWebGLSupported"
|
||||
icon="bi-image-fill"
|
||||
@click="coverType = CoverType.COVER_ART"
|
||||
>
|
||||
<i class="icon image outline" />
|
||||
</button>
|
||||
/>
|
||||
</tooltip>
|
||||
|
||||
<button
|
||||
<Button
|
||||
v-if="!fullscreen"
|
||||
class="ui secondary button"
|
||||
:aria-label="labels.fullscreen"
|
||||
:title="labels.fullscreen"
|
||||
icon="bi-arrows-fullscreen"
|
||||
@click="enter"
|
||||
>
|
||||
<i class="icon expand" />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<Button
|
||||
secondary
|
||||
v-else
|
||||
class="ui secondary button"
|
||||
:aria-label="labels.exitFullscreen"
|
||||
:title="labels.exitFullscreen"
|
||||
icon="bi-fullscreen-exit"
|
||||
@click="exit"
|
||||
>
|
||||
<i class="icon compress" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="queue">
|
||||
|
@ -345,20 +340,21 @@ if (!isWebGLSupported) {
|
|||
<track-favorite-icon
|
||||
v-if="store.state.auth.authenticated"
|
||||
:track="currentTrack"
|
||||
ghost
|
||||
/>
|
||||
<track-playlist-icon
|
||||
v-if="store.state.auth.authenticated"
|
||||
:track="currentTrack"
|
||||
ghost
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
v-if="store.state.auth.authenticated"
|
||||
:class="['ui', 'really', 'basic', 'circular', 'icon', 'button']"
|
||||
ghost
|
||||
icon="bi-eye-slash"
|
||||
:aria-label="labels.addArtistContentFilter"
|
||||
:title="labels.addArtistContentFilter"
|
||||
@click="hideArtist"
|
||||
>
|
||||
<i :class="['eye slash outline', 'basic', 'icon']" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
<div class="progress-wrapper">
|
||||
<div class="progress-area">
|
||||
|
@ -399,16 +395,21 @@ if (!isWebGLSupported) {
|
|||
<div class="ui basic clearing segment">
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<button
|
||||
v-t="'components.Queue.button.close'"
|
||||
class="ui right floated basic button"
|
||||
<Button
|
||||
ghost
|
||||
icon="bi-chevron-down"
|
||||
style="float: right; margin-right: 24px;"
|
||||
@click="store.commit('ui/queueFocused', null)"
|
||||
/>
|
||||
<button
|
||||
v-t="'components.Queue.button.clear'"
|
||||
class="ui right floated basic button danger"
|
||||
<Button
|
||||
red
|
||||
outline
|
||||
icon="bi-trash-fill"
|
||||
style="float: right; margin-right: 16px;"
|
||||
@click="clear"
|
||||
/>
|
||||
>
|
||||
{{ t('components.Queue.button.clear') }}
|
||||
</Button>
|
||||
{{ labels.queue }}
|
||||
<div class="sub header">
|
||||
<div>
|
||||
|
@ -421,7 +422,7 @@ if (!isWebGLSupported) {
|
|||
</template>
|
||||
</i18n-t>
|
||||
<span class="middle pipe symbol" />
|
||||
<span v-t="'components.Queue.meta.end'" />
|
||||
<span style="margin-right: 8px;" v-t="'components.Queue.meta.end'" />
|
||||
<span :title="labels.duration">
|
||||
{{ endsIn }}
|
||||
</span>
|
||||
|
@ -464,18 +465,19 @@ if (!isWebGLSupported) {
|
|||
>
|
||||
<div class="content">
|
||||
<h3 class="header">
|
||||
<i class="feed icon" />
|
||||
<i class="bi bi-boombox-fill" />
|
||||
{{ t('components.Queue.header.radio') }}
|
||||
</h3>
|
||||
<p>
|
||||
{{ t('components.Queue.message.radio') }}
|
||||
</p>
|
||||
<button
|
||||
class="ui basic primary button"
|
||||
<Button
|
||||
primary
|
||||
icon="bi-stop-fill"
|
||||
@click="store.dispatch('radios/stop')"
|
||||
>
|
||||
{{ t('components.Queue.button.stopRadio') }}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -5,6 +5,8 @@ import time from '~/utils/time'
|
|||
import { generateTrackCreditStringFromQueue } from '~/utils/utils'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
interface Events {
|
||||
|
@ -27,7 +29,7 @@ defineProps<Props>()
|
|||
tabindex="0"
|
||||
>
|
||||
<div class="handle">
|
||||
<i class="grip lines icon" />
|
||||
<i class="bi bi-list" />
|
||||
</div>
|
||||
<div
|
||||
class="image-cell"
|
||||
|
@ -40,7 +42,7 @@ defineProps<Props>()
|
|||
>
|
||||
</div>
|
||||
<div @click="$emit('play', index)">
|
||||
<button
|
||||
<div
|
||||
class="title reset ellipsis"
|
||||
:title="source.title"
|
||||
:aria-label="source.labels.selectTrack"
|
||||
|
@ -49,7 +51,7 @@ defineProps<Props>()
|
|||
<span>
|
||||
{{ generateTrackCreditStringFromQueue(source) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="duration-cell">
|
||||
<template v-if="source.sources.length > 0">
|
||||
|
@ -57,26 +59,28 @@ defineProps<Props>()
|
|||
</template>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button
|
||||
<Button
|
||||
v-if="store.state.auth.authenticated"
|
||||
:aria-label="source.labels.favorite"
|
||||
:title="source.labels.favorite"
|
||||
class="ui really basic circular icon button"
|
||||
:icon="store.getters['favorites/isFavorite'](source.id) ? 'bi-heart-fill' : 'bi-heart'"
|
||||
round
|
||||
ghost
|
||||
square-small
|
||||
style="align-self: center;"
|
||||
:class="store.getters['favorites/isFavorite'](source.id) ? 'pink' : ''"
|
||||
@click.stop="store.dispatch('favorites/toggle', source.id)"
|
||||
>
|
||||
<i
|
||||
:class="store.getters['favorites/isFavorite'](source.id) ? 'pink' : ''"
|
||||
class="heart icon"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<Button
|
||||
:aria-label="source.labels.remove"
|
||||
:title="source.labels.remove"
|
||||
class="ui really tiny basic circular icon button"
|
||||
icon="bi-x"
|
||||
round
|
||||
ghost
|
||||
square-small
|
||||
style="align-self: center;"
|
||||
@click.stop="$emit('remove', index)"
|
||||
>
|
||||
<i class="x icon" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -48,12 +48,21 @@ const store = useStore()
|
|||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
const toggleMobilePlayer = () => {
|
||||
store.commit('ui/queueFocused', ['queue', 'player'].includes(store.state.ui.queueFocused as string) ? null : 'player')
|
||||
/** Toggle between null and player */
|
||||
const togglePlayer = () => {
|
||||
store.commit('ui/queueFocused',
|
||||
store.state.ui.queueFocused === 'queue' ? null
|
||||
: store.state.ui.queueFocused === 'player' ? null
|
||||
: 'player'
|
||||
)
|
||||
}
|
||||
|
||||
const switchTab = () => {
|
||||
store.commit('ui/queueFocused', store.state.ui.queueFocused === 'player' ? 'queue' : 'player')
|
||||
}
|
||||
|
||||
// Key binds
|
||||
onKeyboardShortcut('e', toggleMobilePlayer)
|
||||
onKeyboardShortcut('e', togglePlayer)
|
||||
onKeyboardShortcut('p', () => { isPlaying.value = !isPlaying.value })
|
||||
onKeyboardShortcut('s', shuffle)
|
||||
onKeyboardShortcut('q', clear)
|
||||
|
@ -87,10 +96,6 @@ const labels = computed(() => ({
|
|||
addArtistContentFilter: t('components.audio.Player.label.addArtistContentFilter')
|
||||
}))
|
||||
|
||||
const switchTab = () => {
|
||||
store.commit('ui/queueFocused', store.state.ui.queueFocused === 'player' ? 'queue' : 'player')
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -138,7 +143,7 @@ const hideArtist = () => {
|
|||
/>
|
||||
<div
|
||||
class="ui inverted segment fixed-controls"
|
||||
@click.prevent.stop="toggleMobilePlayer"
|
||||
@click.prevent.stop="togglePlayer"
|
||||
>
|
||||
<div
|
||||
ref="progressBar"
|
||||
|
@ -300,14 +305,16 @@ const hideArtist = () => {
|
|||
@click.prevent.stop="shuffle()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- TODO: Remove fake responsive elements -->
|
||||
<div class="group">
|
||||
<div class="fake-dropdown">
|
||||
<Button
|
||||
class="position circular control button desktop-and-up"
|
||||
aria-expanded="true"
|
||||
ghost
|
||||
round
|
||||
icon="bi-music-note-list"
|
||||
@click.stop="toggleMobilePlayer"
|
||||
@click.stop="togglePlayer"
|
||||
>
|
||||
<i18n-t keypath="components.audio.Player.meta.position">
|
||||
<template #index>
|
||||
|
@ -320,7 +327,6 @@ const hideArtist = () => {
|
|||
</Button>
|
||||
<Button
|
||||
class="position circular control button desktop-and-below"
|
||||
@click.stop="switchTab"
|
||||
icon="bi-music-note-list"
|
||||
>
|
||||
<i18n-t keypath="components.audio.Player.meta.position">
|
||||
|
@ -334,37 +340,19 @@ const hideArtist = () => {
|
|||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="store.state.ui.queueFocused"
|
||||
ghost
|
||||
class="close-control desktop-and-up"
|
||||
icon="bi-caret-down-fill"
|
||||
@click.stop="toggleMobilePlayer"
|
||||
>
|
||||
</Button>
|
||||
:class="['desktop-and-up', { 'close-control': store.state.ui.queueFocused }]"
|
||||
:icon="store.state.ui.queueFocused ? 'bi-chevron-down' : 'bi-chevron-up'"
|
||||
:aria-pressed="store.state.ui.queueFocused ? true : undefined"
|
||||
@click.stop="togglePlayer"
|
||||
/>
|
||||
<Button
|
||||
v-else
|
||||
ghost
|
||||
class="desktop-and-up"
|
||||
icon="bi-caret-up-fill"
|
||||
@click.stop="toggleMobilePlayer"
|
||||
>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="store.state.ui.queueFocused === 'player'"
|
||||
ghost
|
||||
class="close-control desktop-and-below"
|
||||
icon="bi-caret-up-fill"
|
||||
:class="['desktop-and-below', { 'close-control': store.state.ui.queueFocused === 'player' }]"
|
||||
:icon="store.state.ui.queueFocused === 'queue' ? 'bi-chevron-down' : 'bi-chevron-up'"
|
||||
:aria-pressed="store.state.ui.queueFocused ? true : undefined"
|
||||
@click.stop="switchTab"
|
||||
>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="store.state.ui.queueFocused === 'queue'"
|
||||
ghost
|
||||
class="desktop-and-below"
|
||||
icon="bi-caret-down-fill"
|
||||
@click.stop="switchTab"
|
||||
>
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
class="close-control desktop-and-below"
|
||||
|
|
|
@ -195,8 +195,14 @@
|
|||
> * {
|
||||
padding: 0.5em;
|
||||
}
|
||||
}
|
||||
.close-control {
|
||||
background-color: transparent;
|
||||
|
||||
& i {
|
||||
color: var(--vibrant-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
.opaque {
|
||||
@include dark-theme {
|
||||
background-color: rgba(#292525, 0.8);
|
||||
}
|
||||
@include light-theme {
|
||||
background-color: rgba(#ffffff, 0.8);
|
||||
}
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.queue-controls {
|
||||
@include media("<desktop") {
|
||||
height: $bottom-player-height;
|
||||
|
@ -5,7 +16,6 @@
|
|||
}
|
||||
|
||||
.ui.clearing.segment {
|
||||
background-color: var(--site-background);
|
||||
box-shadow: var(--secondary-menu-box-shadow);
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
@ -26,7 +36,6 @@
|
|||
|
||||
.queue-controls {
|
||||
@include media("<desktop") {
|
||||
background: var(--site-background);
|
||||
}
|
||||
}
|
||||
&.main {
|
||||
|
@ -51,6 +60,11 @@
|
|||
padding: 0.5em;
|
||||
}
|
||||
}
|
||||
#queue .sub.header {
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.queue-column {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
@ -73,6 +87,10 @@
|
|||
.image-cell {
|
||||
width: 4em;
|
||||
}
|
||||
.mini.image {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
.queue.segment {
|
||||
@include media("<desktop") {
|
||||
padding: 0;
|
||||
|
@ -468,10 +486,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
// NOTE: Taken from semantic ui
|
||||
/* button.pink > i {
|
||||
color: var(--fw-red-800);
|
||||
} */
|
||||
|
||||
&.active {
|
||||
background: #E0E0E0;
|
||||
color: #000000de;
|
||||
background: var(--player-color);
|
||||
color: var(--player-background);
|
||||
|
||||
.button > i {
|
||||
color: var(--player-background);
|
||||
}
|
||||
|
||||
/* .button.pink > i {
|
||||
color: var(--fw-red-800);
|
||||
} */
|
||||
|
||||
&:hover .button > i {
|
||||
color: var(--player-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue