refactor, style(front): Player queue

This commit is contained in:
ArneBo 2025-02-05 19:19:24 +01:00
parent 406e852bea
commit 8766bac14f
6 changed files with 143 additions and 105 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -195,8 +195,14 @@
> * {
padding: 0.5em;
}
}
.close-control {
background-color: transparent;
& i {
color: var(--vibrant-color);
}
}
}
}
}

View File

@ -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);
}
}
}
}