refactor(front): Player

This commit is contained in:
ArneBo 2025-02-05 02:34:21 +01:00
parent fd83ebb287
commit 614cfeafc0
6 changed files with 148 additions and 178 deletions

View File

@ -15,6 +15,7 @@ import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue' import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
import PlayerControls from './PlayerControls.vue' import PlayerControls from './PlayerControls.vue'
import VolumeControl from './VolumeControl.vue' import VolumeControl from './VolumeControl.vue'
import Button from '~/components/ui/Button.vue'
const { const {
LoopingMode, LoopingMode,
@ -160,10 +161,11 @@ const hideArtist = () => {
class="ui tiny image" class="ui tiny image"
@click.stop.prevent="router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})" @click.stop.prevent="router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})"
> >
<!-- TODO: Use smaller covers -->
<img <img
ref="cover" ref="cover"
alt="" alt=""
:src="store.getters['instance/absoluteUrl'](currentTrack.coverUrl)" v-lazy="store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
> >
</div> </div>
<div <div
@ -172,7 +174,7 @@ const hideArtist = () => {
> >
<strong> <strong>
<router-link <router-link
class="small header discrete link track" class="header discrete link track"
:to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}" :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}"
@click.stop.prevent="" @click.stop.prevent=""
> >
@ -186,7 +188,7 @@ const hideArtist = () => {
:key="ac.artist.id" :key="ac.artist.id"
> >
<router-link <router-link
class="discrete link" class="small discrete link"
:to="{name: 'library.artists.detail', params: {id: ac.artist.id }}" :to="{name: 'library.artists.detail', params: {id: ac.artist.id }}"
@click.stop.prevent="" @click.stop.prevent=""
> >
@ -198,7 +200,7 @@ const hideArtist = () => {
<template v-if="currentTrack.albumId !== -1"> <template v-if="currentTrack.albumId !== -1">
<span class="middle slash symbol" /> <span class="middle slash symbol" />
<router-link <router-link
class="discrete link" class="small discrete link"
:to="{name: 'library.albums.detail', params: {id: currentTrack.albumId }}" :to="{name: 'library.albums.detail', params: {id: currentTrack.albumId }}"
@click.stop.prevent="" @click.stop.prevent=""
> >
@ -210,10 +212,11 @@ const hideArtist = () => {
</div> </div>
<div class="controls track-controls queue-not-focused desktop-and-below"> <div class="controls track-controls queue-not-focused desktop-and-below">
<div class="ui tiny image"> <div class="ui tiny image">
<!-- TODO: Use smaller covers -->
<img <img
ref="cover" ref="cover"
alt="" alt=""
:src="store.getters['instance/absoluteUrl'](currentTrack.coverUrl)" v-lazy="store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
> >
</div> </div>
<div class="middle aligned content ellipsis"> <div class="middle aligned content ellipsis">
@ -240,21 +243,22 @@ const hideArtist = () => {
class="controls desktop-and-up fluid align-right" class="controls desktop-and-up fluid align-right"
> >
<track-favorite-icon <track-favorite-icon
class="control white" ghost
:track="currentTrack" :track="currentTrack"
/> />
<track-playlist-icon <track-playlist-icon
class="control white" ghost
:track="currentTrack" :track="currentTrack"
/> />
<button <!-- <Button
:class="['ui', 'really', 'basic', 'circular', 'icon', 'button', 'control']" round
ghost
icon="bi-eye-slash"
:aria-label="labels.addArtistContentFilter" :aria-label="labels.addArtistContentFilter"
:title="labels.addArtistContentFilter" :title="labels.addArtistContentFilter"
@click="hideArtist" @click="hideArtist"
> >
<i :class="['eye slash outline', 'basic', 'icon']" /> </Button> -->
</button>
</div> </div>
<player-controls class="controls queue-not-focused" /> <player-controls class="controls queue-not-focused" />
<div class="controls progress-controls queue-not-focused tablet-and-up small align-left"> <div class="controls progress-controls queue-not-focused tablet-and-up small align-left">
@ -274,49 +278,37 @@ const hideArtist = () => {
<div class="controls queue-controls when-queue-focused align-right"> <div class="controls queue-controls when-queue-focused align-right">
<div class="group"> <div class="group">
<volume-control class="expandable" /> <volume-control class="expandable" />
<button <Button
class="circular control button"
:class="{ looping: looping !== LoopingMode.None }" :class="{ looping: looping !== LoopingMode.None }"
:title="loopingTitle" :title="loopingTitle"
ghost
round
:aria-label="loopingTitle" :aria-label="loopingTitle"
:disabled="!currentTrack" :disabled="!currentTrack"
:icon="looping === LoopingMode.LoopTrack ? 'bi-repeat-1' : looping === LoopingMode.LoopQueue ? 'bi-repeat' : 'bi-arrow-clockwise'"
@click.prevent.stop="toggleLooping" @click.prevent.stop="toggleLooping"
>
<i class="repeat icon">
<span
v-if="looping !== LoopingMode.None"
class="ui circular tiny vibrant label"
>
<span
v-if="looping === LoopingMode.LoopTrack"
class="symbol single"
/> />
<span
v-else-if="looping === LoopingMode.LoopQueue"
class="infinity symbol"
/>
</span>
</i>
</button>
<button <Button
class="circular control button" round
ghost
:class="{ shuffling: isShuffled }"
:disabled="queue.length === 0" :disabled="queue.length === 0"
:title="labels.shuffle" :title="labels.shuffle"
:aria-label="labels.shuffle" :aria-label="labels.shuffle"
icon="bi-shuffle"
@click.prevent.stop="shuffle()" @click.prevent.stop="shuffle()"
> />
<i :class="['ui', 'random', { disabled: queue.length === 0, shuffling: isShuffled }, 'icon']" />
</button>
</div> </div>
<div class="group"> <div class="group">
<div class="fake-dropdown"> <div class="fake-dropdown">
<button <Button
class="position circular control button desktop-and-up" class="position circular control button desktop-and-up"
aria-expanded="true" aria-expanded="true"
ghost
icon="bi-music-note-list"
@click.stop="toggleMobilePlayer" @click.stop="toggleMobilePlayer"
> >
<i class="stream icon" />
<i18n-t keypath="components.audio.Player.meta.position"> <i18n-t keypath="components.audio.Player.meta.position">
<template #index> <template #index>
{{ currentIndex + 1 }} {{ currentIndex + 1 }}
@ -325,12 +317,12 @@ const hideArtist = () => {
{{ queue.length }} {{ queue.length }}
</template> </template>
</i18n-t> </i18n-t>
</button> </Button>
<button <Button
class="position circular control button desktop-and-below" class="position circular control button desktop-and-below"
@click.stop="switchTab" @click.stop="switchTab"
icon="bi-music-note-list"
> >
<i class="stream icon" />
<i18n-t keypath="components.audio.Player.meta.position"> <i18n-t keypath="components.audio.Player.meta.position">
<template #index> <template #index>
{{ currentIndex + 1 }} {{ currentIndex + 1 }}
@ -339,46 +331,54 @@ const hideArtist = () => {
{{ queue.length }} {{ queue.length }}
</template> </template>
</i18n-t> </i18n-t>
</button> </Button>
<button <Button
v-if="store.state.ui.queueFocused" v-if="store.state.ui.queueFocused"
class="circular control button close-control desktop-and-up" ghost
class="close-control desktop-and-up"
icon="bi-caret-down-fill"
@click.stop="toggleMobilePlayer" @click.stop="toggleMobilePlayer"
> >
<i class="large down angle icon" /> </Button>
</button> <Button
<button
v-else v-else
class="circular control button desktop-and-up" ghost
class="desktop-and-up"
icon="bi-caret-up-fill"
@click.stop="toggleMobilePlayer" @click.stop="toggleMobilePlayer"
> >
<i class="large up angle icon" /> </Button>
</button> <Button
<button
v-if="store.state.ui.queueFocused === 'player'" v-if="store.state.ui.queueFocused === 'player'"
class="circular control button close-control desktop-and-below" ghost
class="close-control desktop-and-below"
icon="bi-caret-up-fill"
@click.stop="switchTab" @click.stop="switchTab"
> >
<i class="large up angle icon" /> </Button>
</button> <Button
<button
v-if="store.state.ui.queueFocused === 'queue'" v-if="store.state.ui.queueFocused === 'queue'"
class="circular control button desktop-and-below" ghost
class="desktop-and-below"
icon="bi-caret-down-fill"
@click.stop="switchTab" @click.stop="switchTab"
> >
<i class="large down angle icon" /> </Button>
</button>
</div> </div>
<button <Button
class="circular control button close-control desktop-and-below" class="close-control desktop-and-below"
icon="bi-x"
@click.stop="store.commit('ui/queueFocused', null)" @click.stop="store.commit('ui/queueFocused', null)"
> >
<i class="x icon" /> </Button>
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</template> </template>
<style lang="scss" scoped>
</style>

View File

@ -5,6 +5,8 @@ import { computed } from 'vue'
import { usePlayer } from '~/composables/audio/player' import { usePlayer } from '~/composables/audio/player'
import { useQueue } from '~/composables/audio/queue' import { useQueue } from '~/composables/audio/queue'
import Button from '~/components/ui/Button.vue'
const { playPrevious, hasNext, playNext, currentTrack } = useQueue() const { playPrevious, hasNext, playNext, currentTrack } = useQueue()
const { isPlaying } = usePlayer() const { isPlaying } = usePlayer()
@ -19,40 +21,36 @@ const labels = computed(() => ({
<template> <template>
<div class="player-controls"> <div class="player-controls">
<button <Button
:title="labels.previous" :title="labels.previous"
:aria-label="labels.previous" :aria-label="labels.previous"
class="circular button control tablet-and-up" round
ghost
alignSelf="center"
class="control tablet-and-up"
icon="bi-skip-backward-fill"
@click.prevent.stop="playPrevious()" @click.prevent.stop="playPrevious()"
> />
<i :class="['ui', 'large', 'backward step', 'icon']" /> <Button
</button> :title="isPlaying ? labels.pause : labels.play"
<button round
v-if="!isPlaying" ghost
:title="labels.play" alignSelf="center"
:aria-label="labels.play" :aria-label="isPlaying ? labels.pause : labels.play"
class="circular button control" :class="['control', isPlaying ? 'pause' : 'play', 'large']"
@click.prevent.stop="isPlaying = true" :icon="isPlaying ? 'bi-pause-fill' : 'bi-play-fill'"
> @click.prevent.stop="isPlaying = !isPlaying"
<i :class="['ui', 'big', 'play', {'disabled': !currentTrack}, 'icon']" /> />
</button> <Button
<button
v-else
:title="labels.pause"
:aria-label="labels.pause"
class="circular button control"
@click.prevent.stop="isPlaying = false"
>
<i :class="['ui', 'big', 'pause', {'disabled': !currentTrack}, 'icon']" />
</button>
<button
:title="labels.next" :title="labels.next"
:aria-label="labels.next" :aria-label="labels.next"
round
ghost
alignSelf="center"
:disabled="!hasNext" :disabled="!hasNext"
class="circular button control" class="control"
icon="bi-skip-forward-fill"
@click.prevent.stop="playNext()" @click.prevent.stop="playNext()"
> />
<i :class="['ui', 'large', {'disabled': !hasNext}, 'forward step', 'icon']" />
</button>
</div> </div>
</template> </template>

View File

@ -4,6 +4,8 @@ import { useTimeoutFn } from '@vueuse/core'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Button from '~/components/ui/Button.vue'
const { volume, mute } = usePlayer() const { volume, mute } = usePlayer()
const expanded = ref(false) const expanded = ref(false)
@ -32,8 +34,9 @@ const scroll = (event: WheelEvent) => {
</script> </script>
<template> <template>
<button <Button
class="circular control button" round
ghost
:class="['component-volume-control', {'expanded': expanded}]" :class="['component-volume-control', {'expanded': expanded}]"
@click.prevent.stop="" @click.prevent.stop=""
@mouseover="handleOver" @mouseover="handleOver"
@ -47,7 +50,7 @@ const scroll = (event: WheelEvent) => {
:aria-label="labels.unmute" :aria-label="labels.unmute"
@click.prevent.stop="mute" @click.prevent.stop="mute"
> >
<i class="volume off icon" /> <i class="bi bi-volume-mute-fill" />
</span> </span>
<span <span
v-else-if="volume < 0.5" v-else-if="volume < 0.5"
@ -56,7 +59,7 @@ const scroll = (event: WheelEvent) => {
:aria-label="labels.mute" :aria-label="labels.mute"
@click.prevent.stop="mute" @click.prevent.stop="mute"
> >
<i class="volume down icon" /> <i class="bi bi-volume-down-fill" />
</span> </span>
<span <span
v-else v-else
@ -65,7 +68,7 @@ const scroll = (event: WheelEvent) => {
:aria-label="labels.mute" :aria-label="labels.mute"
@click.prevent.stop="mute" @click.prevent.stop="mute"
> >
<i class="volume up icon" /> <i class="bi bi-volume-up-fill" />
</span> </span>
<div class="popup"> <div class="popup">
<label <label
@ -81,5 +84,5 @@ const scroll = (event: WheelEvent) => {
max="1" max="1"
> >
</div> </div>
</button> </Button>
</template> </template>

View File

@ -102,9 +102,6 @@ $dropdown-item-selected-background: var(--dropdown-item-hover-background) !defau
$segment-color: var(--text-color) !default; $segment-color: var(--text-color) !default;
$segment-background: var(--site-background) !default; $segment-background: var(--site-background) !default;
$player-color: rgba(255, 255, 255, 0.9) !default;
$player-background: #1B1C1D !default;
$table-background: transparent !default; $table-background: transparent !default;
$table-border: var(--divider) !default; $table-border: var(--divider) !default;

View File

@ -107,6 +107,9 @@
--disabled-background-color:var(--fw-beige-100); --disabled-background-color:var(--fw-beige-100);
--disabled-border-color:var(--fw-beige-100); --disabled-border-color:var(--fw-beige-100);
--player-color: var(--fw-gray-700);
--player-background: var(--fw-blue-010);
&.raised { &.raised {
--background-color:var(--fw-beige-300); --background-color:var(--fw-beige-300);
--border-color:color-mix(in oklab, var(--fw-beige-400) 90%, currentcolor); --border-color:color-mix(in oklab, var(--fw-beige-400) 90%, currentcolor);
@ -295,6 +298,9 @@
} }
} }
// Dark theme // Dark theme
:is(body.theme-dark, html.dark>body:not(.theme-light)), .force-dark-theme.force-dark-theme.force-dark-theme { :is(body.theme-dark, html.dark>body:not(.theme-light)), .force-dark-theme.force-dark-theme.force-dark-theme {
@ -323,6 +329,9 @@
--disabled-background-color:var(--fw-gray-950); --disabled-background-color:var(--fw-gray-950);
--disabled-border-color:var(--fw-gray-950); --disabled-border-color:var(--fw-gray-950);
--player-color: var(--fw-gray-300);
--player-background: var(--fw-gray-950);
&.raised{ &.raised{
--background-color:var(--fw-gray-950); --background-color:var(--fw-gray-950);
--border-color:var(--fw-gray-600); --border-color:var(--fw-gray-600);

View File

@ -6,9 +6,8 @@
.ui.top.attached.progress { .ui.top.attached.progress {
top: 0; top: 0;
height: 1rem; height: 3px;
z-index: 1; z-index: 1;
padding-bottom: 0.8rem;
border-radius: 0; border-radius: 0;
.bar { .bar {
@ -18,7 +17,7 @@
} }
.ui.bottom-player > .segment.fixed-controls { .ui.bottom-player > .segment.fixed-controls {
color: var(--player-color); color: var(--color);
background: var(--player-background); background: var(--player-background);
width: 100%; width: 100%;
border-radius: 0; border-radius: 0;
@ -106,18 +105,44 @@
justify-content: flex-start; justify-content: flex-start;
flex-grow: 1; flex-grow: 1;
.image { .image {
padding: 0.5em; margin: 0.5em;
width: auto; width: auto;
margin-right: 0.5em; margin-right: 1em;
> img { > img {
max-height: 3.7em; max-height: 40px;
max-width: 4.7em; max-width: 40px;
} }
} }
} }
.controls { .controls {
min-width: 8em; min-width: 8em;
font-size: 1.1em;
a {
text-decoration: none;
font-size: 16px;
}
.meta {
line-height: 1.5em;
}
.button {
border: none;
cursor: pointer;
&:hover {
background-color: transparent;
> i {
color: var(--color);
transform: scale(1.2);
}
}
}
#volume-slider {
accent-color: var(--vibrant-color);
}
@include media(">desktop") { @include media(">desktop") {
&:not(.fluid) { &:not(.fluid) {
width: 20%; width: 20%;
@ -129,7 +154,11 @@
width: 10%; width: 10%;
} }
&.player-controls { &.player-controls {
width: 15%; gap: 8px;
& i {
font-size: 1.8em;
}
} }
} }
&.small, .small { &.small, .small {
@ -137,11 +166,12 @@
font-size: 0.9em; font-size: 0.9em;
} }
} }
.icon { i {
font-size: 1.1em; font-size: 1.3em;
color: var(--player-color);
} }
.icon.large { .large i {
font-size: 1.4em; font-size: 3.2em;
} }
&:not(.track-controls) { &:not(.track-controls) {
@include media(">desktop") { @include media(">desktop") {
@ -159,11 +189,6 @@
padding: 0.5em; padding: 0.5em;
} }
} }
&.player-controls {
.icon {
margin: 0;
}
}
} }
} }
@ -171,74 +196,12 @@
.component-player { .component-player {
.controls { .controls {
display: flex; display: flex;
justify-content: space-between;
}
.controls .icon.big {
cursor: pointer;
font-size: 2em !important;
}
.controls .icon {
cursor: pointer;
vertical-align: middle;
}
.timer {
font-size: 1.2em;
}
.looping {
i {
position: relative;
}
.ui.circular.label {
font-family: sans-serif;
position: absolute;
font-size: 0.5em !important;
bottom: -0.7rem;
right: -0.7rem;
padding: 2px 0 !important;
width: 15px !important;
height: 15px !important;
min-width: 15px !important;
min-height: 15px !important;
@include media(">desktop") {
font-size: 0.6em !important;
}
}
} }
.shuffling { .shuffling {
color: var(--vibrant-color); color: var(--vibrant-color);
} }
.control.circular.button {
padding: 0;
border: none;
background-color: transparent;
color: inherit;
}
.fake-dropdown { .fake-dropdown {
border: 1px solid gray;
border-radius: 3px;
display: flex; display: flex;
align-items: center;
justify-content: space-between;
min-width: 8em;
z-index: 2; z-index: 2;
> .control.button {
padding: 0.5em;
}
.position.control {
flex-grow: 1;
i.stream.icon {
position: relative;
top: -2px;
left: -2px;
}
}
.angle.icon {
margin-right: 0;
}
} }
} }