[Music UI] Many improvements to the interface.

This commit is contained in:
Fabio Manganiello 2024-01-08 02:44:21 +01:00
parent 80c2f0d8dd
commit 3282588c6e
Signed by: blacklight
GPG key ID: D90FBA7F76362774
7 changed files with 246 additions and 43 deletions

View file

@ -1,9 +1,18 @@
<template> <template>
<div class="extension fade-in" :class="{hidden: !expanded}"> <div class="extension fade-in" :class="{hidden: !expanded}">
<div class="row"> <div class="image-container" @click.prevent="onImageClick" v-if="status?.state !== 'stop'">
<div class="col-3"> <div class="remote-image-container" v-if="track?.image">
<img class="image" :src="track.image" :alt="track.title">
</div> </div>
<div class="col-6 buttons">
<div class="icon-container" v-else>
<i class="icon fas fa-compact-disc"
:class="{playing: status?.state === 'play'}" />
</div>
</div>
<div class="row buttons-container">
<div class="buttons">
<div class="buttons"> <div class="buttons">
<button @click="$emit('previous')" title="Play previous track" v-if="buttons_.previous"> <button @click="$emit('previous')" title="Play previous track" v-if="buttons_.previous">
<i class="icon fa fa-step-backward"></i> <i class="icon fa fa-step-backward"></i>
@ -16,8 +25,6 @@
</button> </button>
</div> </div>
</div> </div>
<div class="col-3">
</div>
</div> </div>
<div class="row"> <div class="row">
@ -185,7 +192,12 @@ export default {
methods: { methods: {
getTime() { getTime() {
return (new Date()).getTime() / 1000 return (new Date()).getTime() / 1000
} },
onImageClick() {
if (this.track?.artist && this.track?.album)
this.$emit('search', {artist: this.track.artist, album: this.track.album})
},
}, },
mounted() { mounted() {
@ -248,15 +260,98 @@ button {
flex: 1; flex: 1;
} }
:deep(.progress-bar-container, .volume-slider-container) {
font-size: 1.25em;
}
:deep(.volume-slider-container) {
margin: 1em 0;
}
.row { .row {
display: flex; display: flex;
} }
.buttons-container {
width: calc(100% + 1em);
margin-left: -0.5em;
font-size: 2em;
justify-content: center;
box-shadow: $border-shadow-bottom;
button {
text-align: center;
&:hover {
color: $default-hover-fg;
}
i {
margin: auto;
}
}
}
.buttons { .buttons {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin: 0; margin: 0;
} }
.image-container {
width: 100%;
display: flex;
justify-content: center;
cursor: pointer;
.remote-image-container {
height: 30vh;
.image {
height: 100%;
}
}
.icon-container {
padding: 0.05em;
display: flex;
align-items: center;
justify-content: center;
font-size: 15em;
opacity: 0.5;
border: $default-border-2;
box-shadow: $border-shadow-bottom;
&:hover {
color: $default-hover-fg;
opacity: 1;
}
}
.icon {
&.playing {
animation-duration: 3s;
animation-name: rotate;
animation-iteration-count: infinite;
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
transform: rotate(359deg);
opacity: 1;
}
}
}
} }
.controls { .controls {
@ -277,8 +372,9 @@ button {
align-items: center; align-items: center;
margin-left: 0; margin-left: 0;
@include until($tablet) { @include until($desktop) {
align-items: center; flex-direction: column;
text-align: center;
} }
a { a {

View file

@ -46,9 +46,10 @@
:devices="devices" :devices="devices"
:selected-device="selectedDevice" :selected-device="selectedDevice"
:active-device="activeDevice" :active-device="activeDevice"
v-else-if="selectedView === 'playlists'"
:edited-playlist="editedPlaylist" :edited-playlist="editedPlaylist"
:tracks="editedPlaylistTracks" :tracks="editedPlaylistTracks"
:show-nav-button="!navVisible"
v-else-if="selectedView === 'playlists'"
@play="$emit('play-playlist', $event)" @play="$emit('play-playlist', $event)"
@load="$emit('load-playlist', $event)" @load="$emit('load-playlist', $event)"
@remove="$emit('remove-playlist', $event)" @remove="$emit('remove-playlist', $event)"
@ -60,20 +61,44 @@
@add-to-playlist="openAddToPlaylist" @add-to-playlist="openAddToPlaylist"
@track-move="$emit('playlist-track-move', $event)" @track-move="$emit('playlist-track-move', $event)"
@search="search" @search="search"
@toggle-nav="navVisible = !navVisible"
@refresh-status="refreshStatus" @refresh-status="refreshStatus"
@select-device="selectDevice" /> @select-device="selectDevice" />
<Search :loading="loading" v-else-if="selectedView === 'search'" :devices="devices" <Search :loading="loading"
:selected-device="selectedDevice" :active-device="activeDevice" @search="search" :results="searchResults"
:results="searchResults" @clear="$emit('search-clear')" @info="$emit('info', $event)" :devices="devices"
@play="$emit('play', $event)" @load="$emit('add-to-tracklist', $event)" :selected-device="selectedDevice"
@add-to-playlist="openAddToPlaylist" @refresh-status="refreshStatus" @select-device="selectDevice" /> :active-device="activeDevice"
:show-nav-button="!navVisible"
v-else-if="selectedView === 'search'"
@search="search"
@clear="$emit('search-clear')"
@info="$emit('info', $event)"
@play="$emit('play', $event)"
@load="$emit('add-to-tracklist', $event)"
@add-to-playlist="openAddToPlaylist"
@refresh-status="refreshStatus"
@toggle-nav="navVisible = !navVisible"
@select-device="selectDevice" />
<Library :loading="loading" v-else-if="selectedView === 'library'" :devices="devices" <Library :loading="loading"
:selected-device="selectedDevice" :active-device="activeDevice" @search="search" :results="libraryResults"
:results="libraryResults" :path="path" @clear="$emit('search-clear')" @info="$emit('info', $event)" :path="path"
@play="$emit('play', $event)" @load="$emit('add-to-tracklist', $event)" :devices="devices"
@add-to-playlist="openAddToPlaylist" @cd="$emit('cd', $event)" @refresh-status="refreshStatus" :selected-device="selectedDevice"
:active-device="activeDevice"
:show-nav-button="!navVisible"
v-else-if="selectedView === 'library'"
@search="search"
@clear="$emit('search-clear')"
@info="$emit('info', $event)"
@play="$emit('play', $event)"
@load="$emit('add-to-tracklist', $event)"
@add-to-playlist="openAddToPlaylist"
@cd="$emit('cd', $event)"
@toggle-nav="navVisible = !navVisible"
@refresh-status="refreshStatus"
@select-device="selectDevice" /> @select-device="selectDevice" />
</div> </div>
</main> </main>

View file

@ -3,9 +3,15 @@
<Loading v-if="loading" /> <Loading v-if="loading" />
<MusicHeader> <MusicHeader>
<label class="search-box"> <label class="col-10 search-box">
<input type="search" placeholder="Filter" v-model="filter"> <input type="search" placeholder="Filter" v-model="filter">
</label> </label>
<div class="col-2 buttons">
<button class="mobile" title="Menu" @click="$emit('toggle-nav')" v-if="showNavButton">
<i class="fas fa-bars" />
</button>
</div>
</MusicHeader> </MusicHeader>
<div class="results"> <div class="results">
@ -67,7 +73,18 @@ export default {
name: "Library", name: "Library",
components: {Dropdown, DropdownItem, MusicHeader, Loading}, components: {Dropdown, DropdownItem, MusicHeader, Loading},
mixins: [MediaUtils], mixins: [MediaUtils],
emits: ['search', 'play', 'load', 'add-to-playlist', 'info', 'cd', 'refresh-status', 'select-device'], emits: [
'add-to-playlist',
'cd',
'info',
'load',
'play',
'refresh-status',
'search',
'select-device',
'toggle-nav',
],
props: { props: {
loading: { loading: {
type: Boolean, type: Boolean,
@ -93,6 +110,11 @@ export default {
activeDevice: { activeDevice: {
type: String, type: String,
}, },
showNavButton: {
type: Boolean,
default: false,
},
}, },
data() { data() {

View file

@ -171,6 +171,7 @@ export default {
targetPos: null, targetPos: null,
centerPos: 0, centerPos: 0,
mounted: false, mounted: false,
scrollTimeout: null,
} }
}, },
@ -296,10 +297,24 @@ export default {
const bodyHeight = parseFloat(getComputedStyle(this.$refs.body).height) const bodyHeight = parseFloat(getComputedStyle(this.$refs.body).height)
const scrollHeight = this.$refs.body.scrollHeight const scrollHeight = this.$refs.body.scrollHeight
if (offset < 5) if (offset < 5) {
this.centerPos = Math.max(0, parseInt(this.centerPos - (this.maxVisibleTracks / 1.5))) if (this.scrollTimeout)
else if (offset === scrollHeight - bodyHeight) return
this.centerPos = Math.min(this.tracks.length - 1, parseInt(this.centerPos + (this.maxVisibleTracks / 1.5)))
this.scrollTimeout = setTimeout(() => {
this.centerPos = Math.max(0, parseInt(this.centerPos - (this.maxVisibleTracks / 1.5)))
this.$refs.body.scrollTop = 6
this.scrollTimeout = null
}, 250)
} else if (offset >= (scrollHeight - bodyHeight - 5)) {
if (this.scrollTimeout)
return
this.scrollTimeout = setTimeout(() => {
this.centerPos = Math.min(this.tracks.length - 1, parseInt(this.centerPos + (this.maxVisibleTracks / 1.5)))
this.scrollTimeout = null
}, 250)
}
}, },
playlistSave() { playlistSave() {

View file

@ -21,12 +21,13 @@
icon-class="fa fa-volume-up" @click="$emit('select-device', id)" /> icon-class="fa fa-volume-up" @click="$emit('select-device', id)" />
</Dropdown> </Dropdown>
<button title="Refresh status" @click="$emit('refresh-status')" v-if="devices != null"> <Dropdown title="Actions" icon-class="fa fa-ellipsis-h">
<i class="fa fa-sync"></i> <DropdownItem text="Add track" icon-class="fa fa-plus" @click="addTrack" />
</button> <DropdownItem text="Refresh status" icon-class="fa fa-sync" @click="$emit('refresh-status')" v-if="devices != null" />
</Dropdown>
<button class="add-btn" title="Add track" @click="addTrack"> <button class="mobile" title="Menu" @click="$emit('toggle-nav')" v-if="showNavButton">
<i class="fas fa-plus" /> <i class="fas fa-bars" />
</button> </button>
</div> </div>
</MusicHeader> </MusicHeader>
@ -76,13 +77,13 @@
<div class="playlists fade-in" v-else> <div class="playlists fade-in" v-else>
<div class="header-container"> <div class="header-container">
<MusicHeader ref="header"> <MusicHeader ref="header">
<div class="col-8 filter"> <div class="col-7 filter">
<label> <label>
<input type="search" placeholder="Filter" v-model="filter"> <input type="search" placeholder="Filter" v-model="filter">
</label> </label>
</div> </div>
<div class="col-4 buttons"> <div class="col-5 buttons">
<Dropdown title="Players" icon-class="fa fa-volume-up" v-if="Object.keys(devices || {}).length"> <Dropdown title="Players" icon-class="fa fa-volume-up" v-if="Object.keys(devices || {}).length">
<DropdownItem v-for="(device, id) in devices" :key="id" v-text="device.name" <DropdownItem v-for="(device, id) in devices" :key="id" v-text="device.name"
:item-class="{active: activeDevice === id, selected: selectedDevice === id}" :item-class="{active: activeDevice === id, selected: selectedDevice === id}"
@ -92,6 +93,10 @@
<button title="Refresh status" @click="$emit('refresh-status')" v-if="devices != null"> <button title="Refresh status" @click="$emit('refresh-status')" v-if="devices != null">
<i class="fa fa-sync"></i> <i class="fa fa-sync"></i>
</button> </button>
<button class="mobile" title="Menu" @click="$emit('toggle-nav')" v-if="showNavButton">
<i class="fas fa-bars" />
</button>
</div> </div>
</MusicHeader> </MusicHeader>
</div> </div>
@ -168,6 +173,11 @@ export default {
activeDevice: { activeDevice: {
type: String, type: String,
}, },
showNavButton: {
type: Boolean,
default: false,
},
}, },
data() { data() {
@ -341,6 +351,7 @@ export default {
.header { .header {
.buttons { .buttons {
display: flex;
align-items: flex-end; align-items: flex-end;
justify-content: flex-end; justify-content: flex-end;
} }
@ -362,10 +373,6 @@ export default {
padding-left: .25em; padding-left: .25em;
} }
.add-btn {
float: right;
}
.search-box { .search-box {
input { input {
width: 65%; width: 65%;

View file

@ -1,5 +1,9 @@
<template> <template>
<div class="search fade-in" :class="{'form-collapsed': formCollapsed}"> <div class="search fade-in" :class="{'form-collapsed': formCollapsed}">
<button class="nav-toggler mobile floating" title="Menu" @click="$emit('toggle-nav')" v-if="showNavButton && !formCollapsed">
<i class="fas fa-bars" />
</button>
<div class="form-container" v-if="!formCollapsed" @submit.prevent="$emit('search', filteredQuery)"> <div class="form-container" v-if="!formCollapsed" @submit.prevent="$emit('search', filteredQuery)">
<form class="search-form"> <form class="search-form">
<div class="row"> <div class="row">
@ -41,14 +45,17 @@
</div> </div>
<MusicHeader v-else> <MusicHeader v-else>
<label class="search-box"> <label class="col-10 search-box">
<button class="back-btn" title="Back" @click="clear">
<i class="fas fa-arrow-left" />
</button>
<input type="search" placeholder="Filter" v-model="filter"> <input type="search" placeholder="Filter" v-model="filter">
</label> </label>
<span class="buttons"> <span class="col-2 buttons">
<button @click="clear"> <button class="mobile" title="Menu" @click="$emit('toggle-nav')" v-if="showNavButton">
<i class="icon fa fa-times" /> <i class="fas fa-bars" />
<span class="btn-title">Clear</span>
</button> </button>
</span> </span>
</MusicHeader> </MusicHeader>
@ -93,7 +100,18 @@ export default {
name: "Search", name: "Search",
components: {Dropdown, DropdownItem, FormFooter, MusicHeader}, components: {Dropdown, DropdownItem, FormFooter, MusicHeader},
mixins: [MediaUtils], mixins: [MediaUtils],
emits: ['search', 'clear', 'play', 'load', 'add-to-playlist', 'info', 'refresh-status', 'select-device'], emits: [
'add-to-playlist',
'clear',
'info',
'load',
'play',
'refresh-status',
'search',
'select-device',
'toggle-nav',
],
props: { props: {
loading: { loading: {
type: Boolean, type: Boolean,
@ -115,6 +133,11 @@ export default {
activeDevice: { activeDevice: {
type: String, type: String,
}, },
showNavButton: {
type: Boolean,
default: false,
},
}, },
data() { data() {
@ -210,6 +233,14 @@ export default {
height: calc(100% - #{$media-ctrl-panel-height}); height: calc(100% - #{$media-ctrl-panel-height});
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative;
.nav-toggler.floating {
position: absolute;
top: 0;
right: 0;
z-index: 1;
}
&:not(.form-collapsed) { &:not(.form-collapsed) {
justify-content: center; justify-content: center;
@ -276,6 +307,13 @@ export default {
.search-box { .search-box {
width: 70%; width: 70%;
display: flex;
align-items: center;
.back-btn {
margin-left: 0;
padding-left: 0;
}
input[type=search] { input[type=search] {
width: 100%; width: 100%;

View file

@ -147,7 +147,7 @@ $slider-thumb-disabled-bg: rgba(0,215,80,0.3) !default;
$slider-thumb-shadow: #475c40 !default; $slider-thumb-shadow: #475c40 !default;
$slider-thumb-border: #83997896 !default; $slider-thumb-border: #83997896 !default;
$slider-hover-on-hover-bg: #d2d2d2 !default; $slider-hover-on-hover-bg: #d2d2d2 !default;
$slider-progress-bg: rgba(0,215,80,0.5) !default; $slider-progress-bg: rgb(0,215,80) !default;
//// Input element //// Input element
$input-icon-fg: #888; $input-icon-fg: #888;