mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 13:04:23 +00:00
start integrating websocket, player basically done
This commit is contained in:
@@ -141,7 +141,7 @@
|
|||||||
#themeSelector {
|
#themeSelector {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
left: 10px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var( --primary-color );
|
color: var( --primary-color );
|
||||||
|
|||||||
@@ -45,7 +45,9 @@
|
|||||||
<span class="material-symbols-outlined close-fullscreen" @click="controlUI( 'hide' )">close</span>
|
<span class="material-symbols-outlined close-fullscreen" @click="controlUI( 'hide' )">close</span>
|
||||||
<playlistView :playlist="playlist" class="pl-wrapper" :currently-playing="currentlyPlayingSongIndex" :is-playing="isPlaying" :pos="pos"
|
<playlistView :playlist="playlist" class="pl-wrapper" :currently-playing="currentlyPlayingSongIndex" :is-playing="isPlaying" :pos="pos"
|
||||||
@control="( action ) => { control( action ) }" @play-song="( song ) => { playSong( song ) }"
|
@control="( action ) => { control( action ) }" @play-song="( song ) => { playSong( song ) }"
|
||||||
@add-new-songs="( songs ) => addNewSongs( songs )" @playlist-reorder="( move ) => moveSong( move )"></playlistView>
|
@add-new-songs="( songs ) => addNewSongs( songs )" @playlist-reorder="( move ) => moveSong( move )"
|
||||||
|
:is-logged-into-apple-music="player.isLoggedIn"
|
||||||
|
@add-new-songs-apple-music="( song ) => addNewSongFromObject( song )"></playlistView>
|
||||||
</div>
|
</div>
|
||||||
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
|
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
|
||||||
<audio src="" id="local-audio" controls="false"></audio>
|
<audio src="" id="local-audio" controls="false"></audio>
|
||||||
@@ -63,6 +65,8 @@
|
|||||||
import type { ReadFile, Song, SongMove } from '@/scripts/song';
|
import type { ReadFile, Song, SongMove } from '@/scripts/song';
|
||||||
import { parseBlob } from 'music-metadata-browser';
|
import { parseBlob } from 'music-metadata-browser';
|
||||||
import notificationsModule from './notificationsModule.vue';
|
import notificationsModule from './notificationsModule.vue';
|
||||||
|
import { useUserStore } from '@/stores/userStore';
|
||||||
|
import NotificationHandler from '@/scripts/notificationHandler';
|
||||||
|
|
||||||
const isPlaying = ref( false );
|
const isPlaying = ref( false );
|
||||||
const repeatMode = ref( '' );
|
const repeatMode = ref( '' );
|
||||||
@@ -82,6 +86,7 @@
|
|||||||
const pos = ref( 0 );
|
const pos = ref( 0 );
|
||||||
const duration = ref( 0 );
|
const duration = ref( 0 );
|
||||||
const notifications = ref( notificationsModule );
|
const notifications = ref( notificationsModule );
|
||||||
|
const notificationHandler = new NotificationHandler();
|
||||||
|
|
||||||
const emits = defineEmits( [ 'playerStateChange' ] );
|
const emits = defineEmits( [ 'playerStateChange' ] );
|
||||||
|
|
||||||
@@ -90,6 +95,7 @@
|
|||||||
if ( isPlaying.value ) {
|
if ( isPlaying.value ) {
|
||||||
player.control( 'play' );
|
player.control( 'play' );
|
||||||
startProgressTracker();
|
startProgressTracker();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
player.control( 'pause' );
|
player.control( 'pause' );
|
||||||
stopProgressTracker();
|
stopProgressTracker();
|
||||||
@@ -364,7 +370,7 @@
|
|||||||
|
|
||||||
const addNewSongs = async ( songs: ReadFile[] ) => {
|
const addNewSongs = async ( songs: ReadFile[] ) => {
|
||||||
let n = notifications.value.createNotification( 'Analyzing new songs', 200, 'progress', 'normal' );
|
let n = notifications.value.createNotification( 'Analyzing new songs', 200, 'progress', 'normal' );
|
||||||
playlist.value = player.getPlaylist();
|
playlist.value = player.getQueue();
|
||||||
for ( let element in songs ) {
|
for ( let element in songs ) {
|
||||||
try {
|
try {
|
||||||
playlist.value.push( await fetchSongData( songs[ element ] ) );
|
playlist.value.push( await fetchSongData( songs[ element ] ) );
|
||||||
@@ -374,21 +380,39 @@
|
|||||||
notifications.value.updateNotification( n, `Analyzing new songs (${element}/${songs.length})` );
|
notifications.value.updateNotification( n, `Analyzing new songs (${element}/${songs.length})` );
|
||||||
}
|
}
|
||||||
player.setPlaylist( playlist.value );
|
player.setPlaylist( playlist.value );
|
||||||
|
if ( !isPlaying.value ) {
|
||||||
player.prepare( 0 );
|
player.prepare( 0 );
|
||||||
isPlaying.value = true;
|
isPlaying.value = true;
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
startProgressTracker();
|
startProgressTracker();
|
||||||
getDetails();
|
getDetails();
|
||||||
}, 2000 );
|
}, 2000 );
|
||||||
|
}
|
||||||
notifications.value.cancelNotification( n );
|
notifications.value.cancelNotification( n );
|
||||||
notifications.value.createNotification( 'New songs added', 10, 'ok', 'normal' );
|
notifications.value.createNotification( 'New songs added', 10, 'ok', 'normal' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addNewSongFromObject = ( song: Song ) => {
|
||||||
|
playlist.value = player.getQueue();
|
||||||
|
playlist.value.push( song );
|
||||||
|
player.setPlaylist( playlist.value );
|
||||||
|
if ( !isPlaying.value ) {
|
||||||
|
player.prepare( 0 );
|
||||||
|
isPlaying.value = true;
|
||||||
|
setTimeout( () => {
|
||||||
|
startProgressTracker();
|
||||||
|
getDetails();
|
||||||
|
}, 2000 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' );
|
emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' );
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
document.addEventListener( 'keydown', ( e ) => {
|
document.addEventListener( 'keydown', ( e ) => {
|
||||||
|
if ( !userStore.isUsingKeyboard ) {
|
||||||
if ( e.key === ' ' ) {
|
if ( e.key === ' ' ) {
|
||||||
// TODO: fix
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
playPause();
|
playPause();
|
||||||
} else if ( e.key === 'ArrowRight' ) {
|
} else if ( e.key === 'ArrowRight' ) {
|
||||||
@@ -398,6 +422,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
control( 'previous' );
|
control( 'previous' );
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
defineExpose( {
|
defineExpose( {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>Playlist</h1>
|
<h1>Playlist</h1>
|
||||||
<input type="file" multiple accept="audio/*" id="more-songs">
|
<input type="file" multiple accept="audio/*" id="more-songs" class="small-buttons">
|
||||||
<button @click="addNewSongs()">Load local songs</button>
|
<button @click="addNewSongs()" class="small-buttons" title="Load selected files"><span class="material-symbols-outlined">upload</span></button>
|
||||||
|
<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>
|
||||||
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p>
|
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p>
|
||||||
<div class="playlist-box" id="pl-box">
|
<div class="playlist-box" id="pl-box">
|
||||||
<!-- TODO: Allow sorting -->
|
|
||||||
<!-- TODO: Allow adding more songs with search on Apple Music or loading from local disk -->
|
<!-- TODO: Allow adding more songs with search on Apple Music or loading from local disk -->
|
||||||
<div class="song" v-for="song in computedPlaylist" v-bind:key="song.id"
|
<div class="song" v-for="song in computedPlaylist" v-bind:key="song.id"
|
||||||
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
|
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
|
||||||
@@ -27,13 +27,16 @@
|
|||||||
<p class="playing-in">{{ getTimeUntil( song ) }}</p>
|
<p class="playing-in">{{ getTimeUntil( song ) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }"></searchView>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ReadFile, Song } from '@/scripts/song';
|
import type { AppleMusicSongData, ReadFile, Song } from '@/scripts/song';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import searchView from './searchView.vue';
|
||||||
|
|
||||||
|
const search = ref( searchView );
|
||||||
const props = defineProps( {
|
const props = defineProps( {
|
||||||
'playlist': {
|
'playlist': {
|
||||||
default: [],
|
default: [],
|
||||||
@@ -54,6 +57,11 @@
|
|||||||
default: 0,
|
default: 0,
|
||||||
required: false,
|
required: false,
|
||||||
type: Number,
|
type: Number,
|
||||||
|
},
|
||||||
|
'isLoggedIntoAppleMusic': {
|
||||||
|
default: false,
|
||||||
|
required: true,
|
||||||
|
type: Boolean,
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
const hasSelectedSongs = ref( true );
|
const hasSelectedSongs = ref( true );
|
||||||
@@ -67,6 +75,12 @@
|
|||||||
return pl;
|
return pl;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
const openSearch = () => {
|
||||||
|
if ( search.value ) {
|
||||||
|
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;
|
||||||
@@ -125,9 +139,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addNewSongs = () => {
|
const addNewSongs = () => {
|
||||||
// TODO: Also allow loading Apple Music songs
|
|
||||||
const fileURLList: ReadFile[] = [];
|
const fileURLList: ReadFile[] = [];
|
||||||
const allFiles = ( document.getElementById( 'pl-loader' ) 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++ ) {
|
||||||
@@ -139,6 +152,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addNewSongsAppleMusic = ( songData: AppleMusicSongData ) => {
|
||||||
|
const song: Song = {
|
||||||
|
artist: songData.attributes.artistName,
|
||||||
|
cover: songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
|
||||||
|
duration: songData.attributes.durationInMillis / 1000,
|
||||||
|
id: songData.id,
|
||||||
|
origin: 'apple-music',
|
||||||
|
title: songData.attributes.name
|
||||||
|
}
|
||||||
|
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;
|
||||||
@@ -156,7 +181,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emits = defineEmits( [ 'play-song', 'control', 'playlist-reorder', 'add-new-songs' ] );
|
const emits = defineEmits( [ 'play-song', 'control', 'playlist-reorder', 'add-new-songs', 'add-new-songs-apple-music' ] );
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -294,4 +319,22 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.small-buttons {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-buttons .material-symbols-outlined {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var( --primary-color );
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-buttons:hover .material-symbols-outlined {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="!$props.isLoggedIn">
|
<div v-else-if="!$props.isLoggedIn">
|
||||||
<p>You are not logged into Apple Music.</p>
|
<p>You are not logged into Apple Music.</p>
|
||||||
<input class="fancy-button" type="file" multiple="true" accept="audio/*" id="pl-loader"><br>
|
<input class="pl-loader-button" type="file" multiple="true" accept="audio/*" id="pl-loader"><br>
|
||||||
<button @click="loadPlaylistFromDisk()" class="fancy-button">Load custom playlist from disk</button>
|
<button @click="loadPlaylistFromDisk()" class="pl-loader-button">Load custom playlist from disk</button>
|
||||||
<p v-if="!hasSelectedSongs">Please select at least one song to proceed!</p>
|
<p v-if="!hasSelectedSongs">Please select at least one song to proceed!</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="playlist-wrapper">
|
<div class="playlist-wrapper">
|
||||||
@@ -84,4 +84,8 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pl-loader-button {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
265
MusicPlayerV2-GUI/src/components/searchView.vue
Normal file
265
MusicPlayerV2-GUI/src/components/searchView.vue
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div id="search-bar" :class="showsSearch ? 'search-shown' : ''">
|
||||||
|
<div id="search-box-wrapper">
|
||||||
|
<input type="text" v-model="searchText" id="search-box" placeholder="Type to search..." @keyup="e => { keyHandler( e ) }">
|
||||||
|
<div class="symbol-wrapper" id="search-symbol-wrapper">
|
||||||
|
<span class="material-symbols-outlined search-symbol" @click="search()">search</span>
|
||||||
|
</div>
|
||||||
|
<div :class="'search-result-wrapper' + ( searchText.length > 0 ? ' show-search-results' : '' )">
|
||||||
|
<div v-for="result in searchResults" v-bind:key="result.id"
|
||||||
|
:class="'search-result' + ( selectedProduct === result.id ? ' prod-selected' : '' )"
|
||||||
|
@mouseenter="removeSelection()" @click="select( result )">
|
||||||
|
<div :style="'background-image: url(' + result.attributes.artwork.url.replace( '{w}', '500' ).replace( '{h}', '500' ) + ');'" class="search-product-image"></div>
|
||||||
|
<div class="search-product-name"><p><b>{{ result.attributes.name }}</b> <i>by {{ result.attributes.artistName }}</i></p></div>
|
||||||
|
</div>
|
||||||
|
<div v-if="searchResults.length === 0 && searchText.length < 3">
|
||||||
|
<p>No results to show</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="searchText.length < 3">
|
||||||
|
<p>Enter at least three characters to start searching</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="symbol-wrapper">
|
||||||
|
<span class="material-symbols-outlined search-symbol" @click="controlSearch( 'hide' )" id="close-icon">close</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MusicKitJSWrapper from '@/scripts/music-player';
|
||||||
|
import type { AppleMusicSongData } from '@/scripts/song';
|
||||||
|
import { useUserStore } from '@/stores/userStore';
|
||||||
|
import { ref, type Ref } from 'vue';
|
||||||
|
|
||||||
|
const showsSearch = ref( false );
|
||||||
|
const searchText = ref( '' );
|
||||||
|
const selectedProduct = ref( '' );
|
||||||
|
let selectedProductIndex = -1;
|
||||||
|
const player = new MusicKitJSWrapper();
|
||||||
|
|
||||||
|
const updateSearchResults = () => {
|
||||||
|
if ( searchText.value.length > 2 ) {
|
||||||
|
player.findSongOnAppleMusic( searchText.value ).then( data => {
|
||||||
|
searchResults.value = data.data.results.songs.data ?? [];
|
||||||
|
selectedProductIndex = -1;
|
||||||
|
selectedProduct.value = '';
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchResults: Ref<AppleMusicSongData[]> = ref( [] );
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const controlSearch = ( action: string ) => {
|
||||||
|
if ( action === 'show' ) {
|
||||||
|
userStore.setKeyboardUsageStatus( true );
|
||||||
|
showsSearch.value = true;
|
||||||
|
setTimeout( () => {
|
||||||
|
const searchBox = document.getElementById( 'search-box' ) as HTMLInputElement;
|
||||||
|
searchBox.focus();
|
||||||
|
}, 500 );
|
||||||
|
} else if ( action === 'hide' ) {
|
||||||
|
userStore.setKeyboardUsageStatus( false );
|
||||||
|
showsSearch.value = false;
|
||||||
|
}
|
||||||
|
searchText.value = '';
|
||||||
|
removeSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeSelection = () => {
|
||||||
|
selectedProduct.value = '';
|
||||||
|
selectedProductIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyHandler = ( e: KeyboardEvent ) => {
|
||||||
|
if ( e.key === 'Escape' ) {
|
||||||
|
controlSearch( 'hide' );
|
||||||
|
} else if ( e.key === 'Enter' ) {
|
||||||
|
e.preventDefault();
|
||||||
|
if ( selectedProductIndex >= 0 ) {
|
||||||
|
select( searchResults.value[ selectedProductIndex ] );
|
||||||
|
controlSearch( 'hide' );
|
||||||
|
} else {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
} else if ( e.key === 'ArrowDown' ) {
|
||||||
|
e.preventDefault();
|
||||||
|
if ( selectedProductIndex < searchResults.value.length - 1 ) {
|
||||||
|
selectedProductIndex += 1;
|
||||||
|
selectedProduct.value = searchResults.value[ selectedProductIndex ].id;
|
||||||
|
}
|
||||||
|
} else if ( e.key === 'ArrowUp' ) {
|
||||||
|
e.preventDefault();
|
||||||
|
if ( selectedProductIndex > 0 ) {
|
||||||
|
selectedProductIndex -= 1;
|
||||||
|
selectedProduct.value = searchResults.value[ selectedProductIndex ].id;
|
||||||
|
} else {
|
||||||
|
removeSelection();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateSearchResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = ( song: AppleMusicSongData ) => {
|
||||||
|
emits( 'selected-song', song );
|
||||||
|
controlSearch( 'hide' );
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = () => {
|
||||||
|
emits( 'selected-song', searchResults.value[ 0 ] );
|
||||||
|
controlSearch( 'hide' );
|
||||||
|
}
|
||||||
|
|
||||||
|
const emits = defineEmits( [ 'selected-song' ] );
|
||||||
|
|
||||||
|
|
||||||
|
defineExpose( {
|
||||||
|
controlSearch
|
||||||
|
} );
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#search-bar {
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
top: -15vh;
|
||||||
|
background-color: var( --accent-background );
|
||||||
|
height: 15vh;
|
||||||
|
width: 100vw;
|
||||||
|
left: 0;
|
||||||
|
transition: all 1s;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-bar.search-shown {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-box {
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 150%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-box-wrapper {
|
||||||
|
width: 60vw;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 1000px) {
|
||||||
|
#search-box-wrapper {
|
||||||
|
width: 45vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 1500px) {
|
||||||
|
#search-box-wrapper {
|
||||||
|
width: 30vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-symbol-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.symbol-wrapper {
|
||||||
|
display: flex;
|
||||||
|
height: 3rem;
|
||||||
|
width: 3rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-symbol {
|
||||||
|
color: black;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 200%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-symbol:hover {
|
||||||
|
font-size: 250%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#close-icon {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: var( --primary-color );
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-result-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 2px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
left: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: white;
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
transform-origin: top;
|
||||||
|
transform: scaleY( 0 );
|
||||||
|
transition: all 0.5s;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-search-results {
|
||||||
|
transform: scaleY( 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result {
|
||||||
|
padding: 3px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 98%;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.5s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result:hover, .prod-selected {
|
||||||
|
text-decoration: underline black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-product-image {
|
||||||
|
height: 4rem;
|
||||||
|
width: 4rem;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
margin-right: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-product-name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: calc( 100% - 4rem - 20px );
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -228,6 +228,7 @@ class MusicKitJSWrapper {
|
|||||||
switch ( action ) {
|
switch ( action ) {
|
||||||
case "play":
|
case "play":
|
||||||
if ( this.isPreparedToPlay ) {
|
if ( this.isPreparedToPlay ) {
|
||||||
|
this.control( 'pause' );
|
||||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
this.musicKit.play();
|
this.musicKit.play();
|
||||||
return false;
|
return false;
|
||||||
@@ -285,6 +286,7 @@ class MusicKitJSWrapper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
case "next":
|
case "next":
|
||||||
|
this.control( 'pause' );
|
||||||
if ( this.queuePos < this.queue.length - 1 ) {
|
if ( this.queuePos < this.queue.length - 1 ) {
|
||||||
this.queuePos += 1;
|
this.queuePos += 1;
|
||||||
this.prepare( this.queue[ this.queuePos ] );
|
this.prepare( this.queue[ this.queuePos ] );
|
||||||
@@ -300,6 +302,7 @@ class MusicKitJSWrapper {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case "previous":
|
case "previous":
|
||||||
|
this.control( 'pause' );
|
||||||
if ( this.queuePos > 0 ) {
|
if ( this.queuePos > 0 ) {
|
||||||
this.queuePos -= 1;
|
this.queuePos -= 1;
|
||||||
this.prepare( this.queue[ this.queuePos ] );
|
this.prepare( this.queue[ this.queuePos ] );
|
||||||
|
|||||||
@@ -15,7 +15,25 @@ class NotificationHandler {
|
|||||||
socket: Socket;
|
socket: Socket;
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
this.socket = io();
|
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
||||||
|
autoConnect: false,
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a room token and connect to
|
||||||
|
* @param {string} roomName
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
connect ( roomName: string ): Promise<string> {
|
||||||
|
fetch( localStorage.getItem( 'url' ) + '/createRoomToken', { credentials: 'include' } ).then( res => {
|
||||||
|
if ( res.status === 200 ) {
|
||||||
|
res.json().then( json => {
|
||||||
|
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,10 +53,6 @@ class NotificationHandler {
|
|||||||
disconnect (): void {
|
disconnect (): void {
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
joinRoom ( roomName: string ): void {
|
|
||||||
// this.socket.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotificationHandler;
|
export default NotificationHandler;
|
||||||
8
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
8
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
@@ -42,6 +42,14 @@ export interface Song {
|
|||||||
additionalInfo?: string;
|
additionalInfo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SongTransmitted {
|
||||||
|
title: string;
|
||||||
|
artist: string;
|
||||||
|
duration: number;
|
||||||
|
cover: string;
|
||||||
|
additionalInfo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ReadFile {
|
export interface ReadFile {
|
||||||
url: string;
|
url: string;
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { ref, computed } from 'vue'
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
|
|
||||||
export const useCounterStore = defineStore('counter', () => {
|
|
||||||
const count = ref(0)
|
|
||||||
const doubleCount = computed(() => count.value * 2)
|
|
||||||
function increment() {
|
|
||||||
count.value++
|
|
||||||
}
|
|
||||||
|
|
||||||
return { count, doubleCount, increment }
|
|
||||||
})
|
|
||||||
32
MusicPlayerV2-GUI/src/stores/userStore.ts
Normal file
32
MusicPlayerV2-GUI/src/stores/userStore.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* LanguageSchoolHossegorBookingSystem - userStore.js
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 10/27/2023, Licensed under a proprietary License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export const useUserStore = defineStore( 'user', {
|
||||||
|
state: () => ( { 'isUserAuth': false, 'isAdminAuth': false, 'isUsingKeyboard': false, 'username': '' } ),
|
||||||
|
getters: {
|
||||||
|
getUserAuthenticated: ( state ) => state.isUserAuth,
|
||||||
|
getAdminAuthenticated: ( state ) => state.isAdminAuth,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setUserAuth ( auth: boolean ) {
|
||||||
|
this.isUserAuth = auth;
|
||||||
|
},
|
||||||
|
setAdminAuth ( auth: boolean ) {
|
||||||
|
this.isAdminAuth = auth;
|
||||||
|
},
|
||||||
|
setUsername ( username: string ) {
|
||||||
|
this.username = username;
|
||||||
|
},
|
||||||
|
setKeyboardUsageStatus ( status: boolean ) {
|
||||||
|
this.isUsingKeyboard = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
@@ -5,6 +5,10 @@ import jwt from 'jsonwebtoken';
|
|||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import account from './account';
|
import account from './account';
|
||||||
import sdk from 'oauth-janishutz-client-server';
|
import sdk from 'oauth-janishutz-client-server';
|
||||||
|
import { createServer } from 'node:http';
|
||||||
|
import { Server } from 'socket.io';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import type { Room, Song } from './definitions';
|
||||||
|
|
||||||
declare let __dirname: string | undefined
|
declare let __dirname: string | undefined
|
||||||
if ( typeof( __dirname ) === 'undefined' ) {
|
if ( typeof( __dirname ) === 'undefined' ) {
|
||||||
@@ -22,6 +26,8 @@ const run = () => {
|
|||||||
origin: true
|
origin: true
|
||||||
} ) );
|
} ) );
|
||||||
|
|
||||||
|
const httpServer = createServer( app );
|
||||||
|
|
||||||
// Load id.janishutz.com SDK and allow signing in
|
// Load id.janishutz.com SDK and allow signing in
|
||||||
sdk.routes( app, ( uid: string ) => {
|
sdk.routes( app, ( uid: string ) => {
|
||||||
return new Promise( ( resolve, reject ) => {
|
return new Promise( ( resolve, reject ) => {
|
||||||
@@ -42,12 +48,127 @@ const run = () => {
|
|||||||
} );
|
} );
|
||||||
}, sdkConfig );
|
}, sdkConfig );
|
||||||
|
|
||||||
|
// Websocket for events
|
||||||
|
interface SocketData {
|
||||||
|
[key: string]: Room;
|
||||||
|
}
|
||||||
|
const socketData: SocketData = {};
|
||||||
|
const io = new Server( httpServer, {
|
||||||
|
cors: {
|
||||||
|
origin: true,
|
||||||
|
credentials: true,
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
app.get( '/', ( request, response ) => {
|
io.on( 'connection', ( socket ) => {
|
||||||
|
socket.on( 'create-room', ( room: { name: string, token: string }, cb: ( res: { status: boolean, msg: string } ) => void ) => {
|
||||||
|
if ( room.token === socketData[ room.name ].roomToken ) {
|
||||||
|
socket.join( room.name );
|
||||||
|
cb( {
|
||||||
|
status: true,
|
||||||
|
msg: 'ADDED_TO_ROOM'
|
||||||
|
} )
|
||||||
|
} else {
|
||||||
|
cb( {
|
||||||
|
status: false,
|
||||||
|
msg: 'ERR_TOKEN_INVALID'
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
socket.on( 'join-room', ( room: string, cb: ( res: { status: boolean, msg: string, data?: { playbackStatus: boolean, playbackStart: number, playlist: Song[], playlistIndex: number } } ) => void ) => {
|
||||||
|
if ( socketData[ room ] ) {
|
||||||
|
socket.join( room );
|
||||||
|
cb( {
|
||||||
|
data: {
|
||||||
|
playbackStart: socketData[ room ].playbackStart,
|
||||||
|
playbackStatus: socketData[ room ].playbackStatus,
|
||||||
|
playlist: socketData[ room ].playlist,
|
||||||
|
playlistIndex: socketData[ room ].playlistIndex,
|
||||||
|
},
|
||||||
|
msg: 'STATUS_OK',
|
||||||
|
status: true,
|
||||||
|
} )
|
||||||
|
} else {
|
||||||
|
cb( {
|
||||||
|
msg: 'ERR_NO_ROOM_WITH_THIS_ID',
|
||||||
|
status: false,
|
||||||
|
} )
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
socket.on( 'tampering', ( data: { msg: string, roomName: string } ) => {
|
||||||
|
if ( data.roomName ) {
|
||||||
|
socket.to( data.roomName ).emit( 'tampering-msg', data.msg );
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
|
||||||
|
socket.on( 'playlist', ( data: { roomName: string, roomToken: string, data: Song[] } ) => {
|
||||||
|
if ( socketData[ data.roomName ] ) {
|
||||||
|
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||||
|
socketData[ data.roomName ].playlist = data.data;
|
||||||
|
io.to( data.roomName ).emit( 'playlist', data.data );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
socket.on( 'playback', ( data: { roomName: string, roomToken: string, data: boolean } ) => {
|
||||||
|
if ( socketData[ data.roomName ] ) {
|
||||||
|
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||||
|
socketData[ data.roomName ].playbackStatus = data.data;
|
||||||
|
io.to( data.roomName ).emit( 'playback', data.data );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
socket.on( 'playlist-index', ( data: { roomName: string, roomToken: string, data: number } ) => {
|
||||||
|
if ( socketData[ data.roomName ] ) {
|
||||||
|
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||||
|
socketData[ data.roomName ].playlistIndex = data.data;
|
||||||
|
io.to( data.roomName ).emit( 'playlist-index', data.data );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
socket.on( 'playback-start', ( data: { roomName: string, roomToken: string, data: number } ) => {
|
||||||
|
if ( socketData[ data.roomName ] ) {
|
||||||
|
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||||
|
socketData[ data.roomName ].playbackStart = data.data;
|
||||||
|
io.to( data.roomName ).emit( 'playback-start', data.data );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
app.get( '/', ( request: express.Request, response: express.Response ) => {
|
||||||
response.send( 'Please visit <a href="https://music.janishutz.com">https://music.janishutz.com</a> to use this service' );
|
response.send( 'Please visit <a href="https://music.janishutz.com">https://music.janishutz.com</a> to use this service' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
app.get( '/createRoomToken', ( request: express.Request, response: express.Response ) => {
|
||||||
|
if ( sdk.checkAuth( request ) ) {
|
||||||
|
const roomName = String( request.query.roomName ) ?? '';
|
||||||
|
if ( !socketData[ roomName ] ) {
|
||||||
|
const roomToken = crypto.randomUUID();
|
||||||
|
socketData[ roomName ] = {
|
||||||
|
playbackStart: 0,
|
||||||
|
playbackStatus: false,
|
||||||
|
playlist: [],
|
||||||
|
playlistIndex: 0,
|
||||||
|
roomName: roomName,
|
||||||
|
roomToken: roomToken,
|
||||||
|
};
|
||||||
|
response.send( roomToken );
|
||||||
|
} else {
|
||||||
|
response.status( 409 ).send( 'ERR_CONFLICT' );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.status( 403 ).send( 'ERR_FORBIDDEN' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
app.get( '/getAppleMusicDevToken', ( req, res ) => {
|
app.get( '/getAppleMusicDevToken', ( req, res ) => {
|
||||||
// sign dev token
|
// sign dev token
|
||||||
const privateKey = fs.readFileSync( path.join( __dirname + '/config/apple_private_key.p8' ) ).toString();
|
const privateKey = fs.readFileSync( path.join( __dirname + '/config/apple_private_key.p8' ) ).toString();
|
||||||
@@ -73,7 +194,7 @@ const run = () => {
|
|||||||
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 8081;
|
const PORT = process.env.PORT || 8081;
|
||||||
app.listen( PORT );
|
httpServer.listen( PORT );
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
16
backend/src/definitions.d.ts
vendored
Normal file
16
backend/src/definitions.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export interface Room {
|
||||||
|
playbackStatus: boolean;
|
||||||
|
playbackStart: number;
|
||||||
|
playlist: Song[];
|
||||||
|
playlistIndex: number;
|
||||||
|
roomName: string;
|
||||||
|
roomToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Song {
|
||||||
|
title: string;
|
||||||
|
artist: string;
|
||||||
|
duration: number;
|
||||||
|
cover: string;
|
||||||
|
additionalInfo?: string;
|
||||||
|
}
|
||||||
@@ -131,7 +131,7 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.message-box {
|
.message-box {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: -1;
|
z-index: -20;
|
||||||
color: white;
|
color: white;
|
||||||
transition: all 0.5s;
|
transition: all 0.5s;
|
||||||
width: 95vw;
|
width: 95vw;
|
||||||
|
|||||||
Reference in New Issue
Block a user