This commit is contained in:
janis
2023-11-09 16:12:05 +01:00
parent b37c6c3635
commit 5037663fd5
11 changed files with 1266 additions and 29 deletions

61
backend/ui/index.html Normal file
View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=7">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Showcase - MusicPlayerV2</title>
<link rel="stylesheet" href="/showcase.css">
<script src="https://static.janishutz.com/libs/jquery/jquery.min.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
</head>
<body>
<div class="info">Designed and developed by Janis Hutz https://janishutz.com</div>
<div class="content" id="app">
<div v-if="hasLoaded" style="width: 100%">
<div class="current-song-wrapper">
<span class="material-symbols-outlined fancy-view-song-art" v-if="!playingSong.hasCoverArt">music_note</span>
<img v-else-if="playingSong.hasCoverArt && playingSong.coverArtOrigin === 'api'" :src="playingSong.coverArtURL" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
<img v-else :src="'/getSongCover?filename=' + playingSong.filename" class="fancy-view-song-art" id="current-image">
<div class="current-song">
<progress max="1000" id="progress" :value="progressBar"></progress>
<h1>{{ playingSong.title }}</h1>
<p class="dancing-style" v-if="playingSong.dancingStyle">{{ playingSong.dancingStyle }}</p>
<p>{{ playingSong.artist }}</p>
</div>
</div>
<div class="song-list-wrapper">
<div v-for="song in songQueue" class="song-list">
<span class="material-symbols-outlined song-image" v-if="!song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying )">music_note</span>
<img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying ) && song.coverArtOrigin === 'api'" :src="song.coverArtURL" class="song-image">
<img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying ) && song.coverArtOrigin !== 'api'" :src="'/getSongCover?filename=' + song.filename" class="song-image">
<div v-if="playingSong.filename === song.filename && isPlaying" class="playing-symbols">
<div class="playing-symbols-wrapper">
<div class="playing-bar" id="bar-1"></div>
<div class="playing-bar" id="bar-2"></div>
<div class="playing-bar" id="bar-3"></div>
</div>
</div>
<span class="material-symbols-outlined pause-icon" v-if="!isPlaying && playingSong.filename === song.filename">pause</span>
<div class="song-details-wrapper">
<h3>{{ song.title }}</h3>
<p>{{ song.artist }}</p>
</div>
<div class="time-until">
{{ getTimeUntil( song ) }}
</div>
</div>
<!-- <img :src="" alt=""> -->
</div>
</div>
<div v-else>
<h1>Loading...</h1>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script>
<script src="/showcase.js"></script>
</body>
</html>

201
backend/ui/showcase.css Normal file
View File

@@ -0,0 +1,201 @@
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24
}
body, html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
color: white;
background-color: rgb(29, 29, 29);
}
body {
font-family: sans-serif;
}
.content {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.playing-symbols {
position: absolute;
left: 10vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
width: 5vw;
height: 5vw;
background-color: rgba( 0, 0, 0, 0.6 );
}
.playing-symbols-wrapper {
width: 4vw;
height: 5vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.playing-bar {
height: 60%;
background-color: white;
width: 10%;
border-radius: 50px;
margin: auto;
}
#bar-1 {
animation: music-playing 0.9s infinite ease-in-out;
}
#bar-2 {
animation: music-playing 0.9s infinite ease-in-out;
animation-delay: 0.3s;
}
#bar-3 {
animation: music-playing 0.9s infinite ease-in-out;
animation-delay: 0.6s;
}
@keyframes music-playing {
0% {
transform: scaleY( 1 );
}
50% {
transform: scaleY( 0.5 );
}
100% {
transform: scaleY( 1 );
}
}
.song-list-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.song-list {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 80%;
margin: 2px;
padding: 1vh;
border: 1px white solid;
background-color: rgba( 0, 0, 0, 0.4 );
}
.song-details-wrapper {
margin: 0;
display: block;
margin-left: 10px;
margin-right: auto;
}
.song-list .song-image {
width: 5vw;
height: 5vw;
object-fit: cover;
object-position: center;
font-size: 5vw;
}
.pause-icon {
width: 5vw;
height: 5vw;
object-fit: cover;
object-position: center;
font-size: 5vw !important;
user-select: none;
}
.current-song-wrapper {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 55vh;
width: 100%;
margin-bottom: 0.5%;
margin-top: 0.25%;
}
.current-song {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 1vh;
padding: 1vh;
text-align: center;
background-color: rgba( 0, 0, 0, 0.4 );
}
.fancy-view-song-art {
height: 30vh;
width: 30vh;
object-fit: cover;
object-position: center;
margin-bottom: 10px;
font-size: 30vh !important;
}
#canvas {
display: none;
}
#app {
background-color: rgba( 0, 0, 0, 0 );
}
#progress {
background-color: rgba(45, 28, 145);
width: 30vw;
border: none;
border-radius: 50px;
}
.mode-selector-wrapper {
opacity: 0;
position: fixed;
right: 0.5%;
top: 0.5%;
padding: 0.5%;
}
.mode-selector-wrapper:hover {
opacity: 1;
}
.dancing-style {
font-size: 250%;
margin: 0;
font-weight: bolder;
}
.info {
position: fixed;
font-size: 12px;
transform: rotate(270deg);
left: -150px;
margin: 0;
padding: 0;
top: 50%;
}

126
backend/ui/showcase.js Normal file
View File

@@ -0,0 +1,126 @@
// eslint-disable-next-line no-undef
const { createApp } = Vue;
createApp( {
data() {
return {
hasLoaded: false,
songs: [],
playingSong: {},
isPlaying: false,
pos: 0,
queuePos: 0,
colourPalette: [],
startTime: 0,
offsetTime: 0,
progressBar: 0,
timeTracker: null,
};
},
computed: {
songQueue() {
let ret = [];
for ( let song in this.songs ) {
if ( parseInt( song ) >= this.queuePos ) {
ret.push( this.songs[ song ] );
}
}
return ret;
},
getTimeUntil() {
return ( song ) => {
let timeRemaining = 0;
for ( let i = this.queuePos; i < this.songs.length; i++ ) {
if ( this.songs[ i ] == song ) {
break;
}
timeRemaining += parseInt( this.songs[ i ].duration );
}
if ( this.isPlaying ) {
if ( timeRemaining === 0 ) {
return 'Currently playing';
} else {
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min';
}
} else {
if ( timeRemaining === 0 ) {
return 'Plays next';
} else {
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min after starting to play';
}
}
}
}
},
methods: {
startTimeTracker () {
this.startTime = new Date().getTime();
this.timeTracker = setInterval( () => {
this.pos = this.playingSong.startTime - new Date().getTime() / 1000;
this.progressBar = ( this.pos / this.playingSong.duration ) * 1000;
}, 75 );
},
stopTimeTracker () {
clearInterval( this.timeTracker );
this.oldPos = this.pos;
},
connect() {
let source = new EventSource( '/clientDisplayNotifier', { withCredentials: true } );
source.onmessage = ( e ) => {
let data;
try {
data = JSON.parse( e.data );
} catch ( err ) {
data = { 'type': e.data };
}
if ( data.type === 'basics' ) {
this.isPlaying = data.data.isPlaying ?? false;
this.playingSong = data.data.playingSong ?? {};
this.songs = data.data.songQueue ?? [];
this.pos = data.data.pos ?? 0;
this.oldPos = data.data.pos ?? 0;
this.startTime = new Date().getTime();
this.progressBar = this.pos / this.playingSong.duration * 1000;
this.queuePos = data.data.queuePos ?? 0;
} else if ( data.type === 'pos' ) {
this.pos = data.data;
this.oldPos = data.data;
this.startTime = new Date().getTime();
this.progressBar = data.data / this.playingSong.duration * 1000;
} else if ( data.type === 'isPlaying' ) {
this.isPlaying = data.data;
} else if ( data.type === 'songQueue' ) {
this.songs = data.data;
} else if ( data.type === 'playingSong' ) {
this.playingSong = data.data;
} else if ( data.type === 'queuePos' ) {
this.queuePos = data.data;
}
};
source.onopen = () => {
this.hasLoaded = true;
};
source.addEventListener( 'error', function( e ) {
if ( e.eventPhase == EventSource.CLOSED ) source.close();
if ( e.target.readyState == EventSource.CLOSED ) {
console.log( 'disconnected' );
}
}, false );
},
},
mounted() {
this.connect();
},
watch: {
isPlaying( value ) {
if ( value ) {
this.startTimeTracker();
} else {
this.stopTimeTracker();
}
}
}
} ).mount( '#app' );