This commit is contained in:
2025-11-20 16:55:16 +01:00
parent 1d714da494
commit 64d086dec4

View File

@@ -1,88 +1,147 @@
<template> <template>
<div> <div>
<h1>Queue</h1> <h1>Queue</h1>
<input type="file" multiple accept="audio/*" id="more-songs" class="small-buttons"> <input
<button @click="addNewSongs()" class="small-buttons" title="Load selected files"><span class="material-symbols-outlined">upload</span></button> id="more-songs"
<button @click="openSearch()" v-if="$props.isLoggedIntoAppleMusic" class="small-buttons" title="Search Apple Music for the song"><span class="material-symbols-outlined">search</span></button> type="file"
<button @click="clearPlaylist()" class="small-buttons" title="Clear the playlist"><span class="material-symbols-outlined">delete</span></button> multiple
<button title="Transmit additional information" class="small-buttons" @click="sendAdditionalInfo()"><span class="material-symbols-outlined">send</span></button> accept="audio/*"
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p> class="small-buttons"
<div class="playlist-box" id="pl-box"> >
<button class="small-buttons" title="Load selected files" @click="addNewSongs()">
<span class="material-symbols-outlined">upload</span>
</button>
<button
v-if="$props.isLoggedIntoAppleMusic"
class="small-buttons"
title="Search Apple Music for the song"
@click="openSearch()"
>
<span class="material-symbols-outlined">search</span>
</button>
<button class="small-buttons" title="Clear the playlist" @click="clearPlaylist()">
<span class="material-symbols-outlined">delete</span>
</button>
<button title="Transmit additional information" class="small-buttons" @click="sendAdditionalInfo()">
<span class="material-symbols-outlined">send</span>
</button>
<p v-if="!hasSelectedSongs">
Please select at least one song to proceed
</p>
<div id="pl-box" class="playlist-box">
<!-- TODO: Allow editing additionalInfo. Think also how to make it persist over reloads... Export to JSON and then best-guess add them? Very easy for Apple Music 'cause ID, but how for local songs? Maybe using retrieved ID from Apple Music? --> <!-- TODO: Allow editing additionalInfo. Think also how to make it persist over reloads... Export to JSON and then best-guess add them? Very easy for Apple Music 'cause ID, but how for local songs? Maybe using retrieved ID from Apple Music? -->
<!-- TODO: Handle long AppleMusic Playlists, as AppleMusic doesn't automatically load all songs of a playlist --> <!-- TODO: Handle long AppleMusic Playlists, as AppleMusic doesn't automatically load all songs of a playlist -->
<div class="song" v-for="song in computedPlaylist" v-bind:key="song.id" <div
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' ) v-for="song in computedPlaylist"
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )"> :key="song.id"
class="song"
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )"
>
<img :src="song.cover" alt="Song cover" class="song-cover"> <img :src="song.cover" alt="Song cover" class="song-cover">
<div v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && $props.isPlaying" class="playing-symbols"> <div v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && $props.isPlaying" class="playing-symbols">
<div class="playing-symbols-wrapper"> <div class="playing-symbols-wrapper">
<div class="playing-bar" id="bar-1"></div> <div id="bar-1" class="playing-bar"></div>
<div class="playing-bar" id="bar-2"></div> <div id="bar-2" class="playing-bar"></div>
<div class="playing-bar" id="bar-3"></div> <div id="bar-3" class="playing-bar"></div>
</div> </div>
</div> </div>
<span class="material-symbols-outlined play-icon" @click="control( 'play' )" v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' )">play_arrow</span> <span v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' )" class="material-symbols-outlined play-icon" @click="control( 'play' )">play_arrow</span>
<span class="material-symbols-outlined play-icon" @click="play( song.id )" v-else>play_arrow</span> <span v-else class="material-symbols-outlined play-icon" @click="play( song.id )">play_arrow</span>
<span class="material-symbols-outlined pause-icon" @click="control( 'pause' )">pause</span> <span class="material-symbols-outlined pause-icon" @click="control( 'pause' )">pause</span>
<span class="material-symbols-outlined move-icon" @click="moveSong( song.id, 'up' )" title="Move song up" v-if="canBeMoved( 'up', song.id )">arrow_upward</span> <span
<span class="material-symbols-outlined move-icon" @click="moveSong( song.id, 'down' )" title="Move song down" v-if="canBeMoved( 'down', song.id )">arrow_downward</span> v-if="canBeMoved( 'up', song.id )"
<h3 class="song-title">{{ song.title }}</h3> class="material-symbols-outlined move-icon"
title="Move song up"
@click="moveSong( song.id, 'up' )"
>arrow_upward</span>
<span
v-if="canBeMoved( 'down', song.id )"
class="material-symbols-outlined move-icon"
title="Move song down"
@click="moveSong( song.id, 'down' )"
>arrow_downward</span>
<h3 class="song-title">
{{ song.title }}
</h3>
<div> <div>
<input type="text" placeholder="Additional information for remote display" title="Additional information for remote display" v-model="song.additionalInfo" @focusin="kbControl( 'on' )" @focusout="kbControl( 'off' )"> <input
<p class="playing-in">{{ getTimeUntil( song ) }}</p> v-model="song.additionalInfo"
type="text"
placeholder="Additional information for remote display"
title="Additional information for remote display"
@focusin="kbControl( 'on' )"
@focusout="kbControl( 'off' )"
>
<p class="playing-in">
{{ getTimeUntil( song ) }}
</p>
</div> </div>
<button @click="deleteSong( song.id )" class="small-buttons" title="Remove this song from the queue" v-if="canBeMoved( 'down', song.id ) || canBeMoved( 'up', song.id )"><span class="material-symbols-outlined">delete</span></button> <button
v-if="canBeMoved( 'down', song.id ) || canBeMoved( 'up', song.id )"
class="small-buttons"
title="Remove this song from the queue"
@click="deleteSong( song.id )"
>
<span class="material-symbols-outlined">delete</span>
</button>
</div> </div>
</div> </div>
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }"></searchView> <searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// TODO: Add logout button // TODO: Add logout button
import type { AppleMusicSongData, ReadFile, Song } from '@/scripts/song'; import type {
import { computed, ref } from 'vue'; AppleMusicSongData, ReadFile, Song
} from '@/scripts/song';
import {
computed, ref
} from 'vue';
import searchView from './searchView.vue'; import searchView from './searchView.vue';
import { useUserStore } from '@/stores/userStore'; import {
useUserStore
} from '@/stores/userStore';
const userStore = useUserStore(); const userStore = useUserStore();
const search = ref( searchView ); const search = ref( searchView );
const props = defineProps( { const props = defineProps( {
'playlist': { 'playlist': {
default: [], 'default': [],
required: true, 'required': true,
type: Array<Song> 'type': Array<Song>
}, },
'currentlyPlaying': { 'currentlyPlaying': {
default: 0, 'default': 0,
required: true, 'required': true,
type: Number, 'type': Number,
}, },
'isPlaying': { 'isPlaying': {
default: true, 'default': true,
required: true, 'required': true,
type: Boolean, 'type': Boolean,
}, },
'pos': { 'pos': {
default: 0, 'default': 0,
required: false, 'required': false,
type: Number, 'type': Number,
}, },
'isLoggedIntoAppleMusic': { 'isLoggedIntoAppleMusic': {
default: false, 'default': false,
required: true, 'required': true,
type: Boolean, 'type': Boolean,
} }
} ); } );
const hasSelectedSongs = ref( true ); const hasSelectedSongs = ref( true );
const computedPlaylist = computed( () => { const computedPlaylist = computed( () => {
let pl: Song[] = []; let pl: Song[] = [];
// ( document.getElementById( 'pl-box' ) as HTMLDivElement ).scrollTo( { behavior: 'smooth', top: 0 } ); // ( document.getElementById( 'pl-box' ) as HTMLDivElement ).scrollTo( { behavior: 'smooth', top: 0 } );
for ( let i = props.currentlyPlaying; i < props.playlist.length; i++ ) { for ( let i = props.currentlyPlaying; i < props.playlist.length; i++ ) {
pl.push( props.playlist[ i ] ); pl.push( props.playlist[ i ] );
} }
return pl; return pl;
} ); } );
@@ -92,60 +151,66 @@
} else { } else {
userStore.setKeyboardUsageStatus( true ); userStore.setKeyboardUsageStatus( true );
} }
} };
const openSearch = () => { const openSearch = () => {
if ( search.value ) { if ( search.value ) {
search.value.controlSearch( 'show' ); search.value.controlSearch( 'show' );
} }
} };
const canBeMoved = computed( () => { const canBeMoved = computed( () => {
return ( direction: movementDirection, songID: string ): boolean => { return ( direction: movementDirection, songID: string ): boolean => {
let id = 0; let id = 0;
for ( let song in props.playlist ) { for ( let song in props.playlist ) {
if ( props.playlist[ song ].id === songID ) { if ( props.playlist[ song ].id === songID ) {
id = parseInt( song ); id = parseInt( song );
break; break;
} }
} }
if ( direction === 'up' ) { if ( direction === 'up' ) {
if ( props.currentlyPlaying + 1 === id || props.currentlyPlaying === id ) { if ( props.currentlyPlaying + 1 === id || props.currentlyPlaying === id ) {
return false; return false;
} }
return true; return true;
} else { } else {
if ( id === props.playlist.length - 1 || props.currentlyPlaying === id ) { if ( id === props.playlist.length - 1 || props.currentlyPlaying === id ) {
return false; return false;
} }
return true; return true;
} }
} };
} ) } );
const getTimeUntil = computed( () => { const getTimeUntil = computed( () => {
return ( song: Song ) => { return ( song: Song ) => {
let timeRemaining = 0; let timeRemaining = 0;
for ( let i = props.currentlyPlaying; i < Object.keys( props.playlist ).length; i++ ) { for ( let i = props.currentlyPlaying; i < Object.keys( props.playlist ).length; i++ ) {
if ( props.playlist[ i ] == song ) { if ( props.playlist[ i ] == song ) {
break; break;
} }
timeRemaining += props.playlist[ i ].duration; timeRemaining += props.playlist[ i ].duration;
} }
if ( props.isPlaying ) { if ( props.isPlaying ) {
if ( timeRemaining === 0 ) { if ( timeRemaining === 0 ) {
return 'Currently playing'; return 'Currently playing';
} else { } else {
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min'; return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min';
} }
} else { } else {
if ( timeRemaining === 0 ) { if ( timeRemaining === 0 ) {
return 'Plays next'; return 'Plays next';
} else { } else {
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play'; return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play';
} }
} }
} };
} ); } );
const deleteSong = ( songID: string ) => { const deleteSong = ( songID: string ) => {
@@ -154,69 +219,92 @@
emits( 'delete-song', parseInt( song ) ); emits( 'delete-song', parseInt( song ) );
} }
} }
} };
const clearPlaylist = () => { const clearPlaylist = () => {
emits( 'clear-playlist', '' ); emits( 'clear-playlist', '' );
} };
const control = ( action: string ) => { const control = ( action: string ) => {
emits( 'control', action ); emits( 'control', action );
} };
const play = ( song: string ) => { const play = ( song: string ) => {
emits( 'play-song', song ); emits( 'play-song', song );
} };
const addNewSongs = () => { const addNewSongs = () => {
const fileURLList: ReadFile[] = []; const fileURLList: ReadFile[] = [];
const allFiles = ( document.getElementById( 'more-songs' ) as HTMLInputElement ).files ?? []; const allFiles = ( document.getElementById( 'more-songs' ) as HTMLInputElement ).files ?? [];
if ( allFiles.length > 0 ) { if ( allFiles.length > 0 ) {
hasSelectedSongs.value = true; hasSelectedSongs.value = true;
for ( let file = 0; file < allFiles.length; file++ ) { for ( let file = 0; file < allFiles.length; file++ ) {
fileURLList.push( { 'url': URL.createObjectURL( allFiles[ file ] ), 'filename': allFiles[ file ].name } ); fileURLList.push( {
'url': URL.createObjectURL( allFiles[ file ] ),
'filename': allFiles[ file ].name
} );
} }
emits( 'add-new-songs', fileURLList ); emits( 'add-new-songs', fileURLList );
} else { } else {
hasSelectedSongs.value = false; hasSelectedSongs.value = false;
} }
} };
const addNewSongsAppleMusic = ( songData: AppleMusicSongData ) => { const addNewSongsAppleMusic = ( songData: AppleMusicSongData ) => {
const song: Song = { const song: Song = {
artist: songData.attributes.artistName, 'artist': songData.attributes.artistName,
cover: songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ), 'cover': songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
duration: songData.attributes.durationInMillis / 1000, 'duration': songData.attributes.durationInMillis / 1000,
id: songData.id, 'id': songData.id,
origin: 'apple-music', 'origin': 'apple-music',
title: songData.attributes.name 'title': songData.attributes.name
} };
emits( 'add-new-songs-apple-music', song ); emits( 'add-new-songs-apple-music', song );
} };
type movementDirection = 'up' | 'down'; type movementDirection = 'up' | 'down';
const moveSong = ( songID: string, direction: movementDirection ) => { const moveSong = ( songID: string, direction: movementDirection ) => {
let newSongPos = 0; let newSongPos = 0;
let hasFoundSongToMove = false; let hasFoundSongToMove = false;
for ( let el in props.playlist ) { for ( let el in props.playlist ) {
if ( props.playlist[ el ].id === songID ) { if ( props.playlist[ el ].id === songID ) {
const currPos = parseInt( el ); const currPos = parseInt( el );
newSongPos = currPos + ( direction === 'up' ? -1 : 1 ); newSongPos = currPos + ( direction === 'up' ? -1 : 1 );
hasFoundSongToMove = true; hasFoundSongToMove = true;
break; break;
} }
} }
if ( hasFoundSongToMove ) { if ( hasFoundSongToMove ) {
emits( 'playlist-reorder', { 'songID': songID, 'newPos': newSongPos } ); emits( 'playlist-reorder', {
'songID': songID,
'newPos': newSongPos
} );
} }
} };
const sendAdditionalInfo = () => { const sendAdditionalInfo = () => {
emits( 'send-additional-info' ); emits( 'send-additional-info' );
} };
const emits = defineEmits( [ 'play-song', 'control', 'playlist-reorder', 'add-new-songs', 'add-new-songs-apple-music', 'delete-song', 'clear-playlist', 'send-additional-info' ] ); const emits = defineEmits( [
'play-song',
'control',
'playlist-reorder',
'add-new-songs',
'add-new-songs-apple-music',
'delete-song',
'clear-playlist',
'send-additional-info'
] );
</script> </script>
<style scoped> <style scoped>
@@ -249,6 +337,10 @@
font-size: 6rem; font-size: 6rem;
} }
.song img.song-cover {
font-size: unset;
}
.song-title { .song-title {
margin-left: 10px; margin-left: 10px;
margin-right: auto; margin-right: auto;
@@ -372,4 +464,4 @@
.small-buttons:hover .material-symbols-outlined { .small-buttons:hover .material-symbols-outlined {
transform: scale(1.1); transform: scale(1.1);
} }
</style> </style>