mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 04:54:23 +00:00
start integrating websocket, player basically done
This commit is contained in:
@@ -141,7 +141,7 @@
|
||||
#themeSelector {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var( --primary-color );
|
||||
|
||||
@@ -45,7 +45,9 @@
|
||||
<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"
|
||||
@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>
|
||||
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
|
||||
<audio src="" id="local-audio" controls="false"></audio>
|
||||
@@ -63,6 +65,8 @@
|
||||
import type { ReadFile, Song, SongMove } from '@/scripts/song';
|
||||
import { parseBlob } from 'music-metadata-browser';
|
||||
import notificationsModule from './notificationsModule.vue';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import NotificationHandler from '@/scripts/notificationHandler';
|
||||
|
||||
const isPlaying = ref( false );
|
||||
const repeatMode = ref( '' );
|
||||
@@ -82,6 +86,7 @@
|
||||
const pos = ref( 0 );
|
||||
const duration = ref( 0 );
|
||||
const notifications = ref( notificationsModule );
|
||||
const notificationHandler = new NotificationHandler();
|
||||
|
||||
const emits = defineEmits( [ 'playerStateChange' ] );
|
||||
|
||||
@@ -90,6 +95,7 @@
|
||||
if ( isPlaying.value ) {
|
||||
player.control( 'play' );
|
||||
startProgressTracker();
|
||||
|
||||
} else {
|
||||
player.control( 'pause' );
|
||||
stopProgressTracker();
|
||||
@@ -364,7 +370,7 @@
|
||||
|
||||
const addNewSongs = async ( songs: ReadFile[] ) => {
|
||||
let n = notifications.value.createNotification( 'Analyzing new songs', 200, 'progress', 'normal' );
|
||||
playlist.value = player.getPlaylist();
|
||||
playlist.value = player.getQueue();
|
||||
for ( let element in songs ) {
|
||||
try {
|
||||
playlist.value.push( await fetchSongData( songs[ element ] ) );
|
||||
@@ -374,29 +380,48 @@
|
||||
notifications.value.updateNotification( n, `Analyzing new songs (${element}/${songs.length})` );
|
||||
}
|
||||
player.setPlaylist( playlist.value );
|
||||
player.prepare( 0 );
|
||||
isPlaying.value = true;
|
||||
setTimeout( () => {
|
||||
startProgressTracker();
|
||||
getDetails();
|
||||
}, 2000 );
|
||||
if ( !isPlaying.value ) {
|
||||
player.prepare( 0 );
|
||||
isPlaying.value = true;
|
||||
setTimeout( () => {
|
||||
startProgressTracker();
|
||||
getDetails();
|
||||
}, 2000 );
|
||||
}
|
||||
notifications.value.cancelNotification( n );
|
||||
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' );
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
document.addEventListener( 'keydown', ( e ) => {
|
||||
if ( e.key === ' ' ) {
|
||||
// TODO: fix
|
||||
e.preventDefault();
|
||||
playPause();
|
||||
} else if ( e.key === 'ArrowRight' ) {
|
||||
e.preventDefault();
|
||||
control( 'next' );
|
||||
} else if ( e.key === 'ArrowLeft' ) {
|
||||
e.preventDefault();
|
||||
control( 'previous' );
|
||||
if ( !userStore.isUsingKeyboard ) {
|
||||
if ( e.key === ' ' ) {
|
||||
e.preventDefault();
|
||||
playPause();
|
||||
} else if ( e.key === 'ArrowRight' ) {
|
||||
e.preventDefault();
|
||||
control( 'next' );
|
||||
} else if ( e.key === 'ArrowLeft' ) {
|
||||
e.preventDefault();
|
||||
control( 'previous' );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Playlist</h1>
|
||||
<input type="file" multiple accept="audio/*" id="more-songs">
|
||||
<button @click="addNewSongs()">Load local songs</button>
|
||||
<input type="file" multiple accept="audio/*" id="more-songs" class="small-buttons">
|
||||
<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>
|
||||
<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 -->
|
||||
<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' )
|
||||
@@ -27,13 +27,16 @@
|
||||
<p class="playing-in">{{ getTimeUntil( song ) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }"></searchView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 searchView from './searchView.vue';
|
||||
|
||||
const search = ref( searchView );
|
||||
const props = defineProps( {
|
||||
'playlist': {
|
||||
default: [],
|
||||
@@ -54,6 +57,11 @@
|
||||
default: 0,
|
||||
required: false,
|
||||
type: Number,
|
||||
},
|
||||
'isLoggedIntoAppleMusic': {
|
||||
default: false,
|
||||
required: true,
|
||||
type: Boolean,
|
||||
}
|
||||
} );
|
||||
const hasSelectedSongs = ref( true );
|
||||
@@ -67,6 +75,12 @@
|
||||
return pl;
|
||||
} );
|
||||
|
||||
const openSearch = () => {
|
||||
if ( search.value ) {
|
||||
search.value.controlSearch( 'show' );
|
||||
}
|
||||
}
|
||||
|
||||
const canBeMoved = computed( () => {
|
||||
return ( direction: movementDirection, songID: string ): boolean => {
|
||||
let id = 0;
|
||||
@@ -125,9 +139,8 @@
|
||||
}
|
||||
|
||||
const addNewSongs = () => {
|
||||
// TODO: Also allow loading Apple Music songs
|
||||
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 ) {
|
||||
hasSelectedSongs.value = true;
|
||||
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';
|
||||
const moveSong = ( songID: string, direction: movementDirection ) => {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
@@ -294,4 +319,22 @@
|
||||
cursor: pointer;
|
||||
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>
|
||||
@@ -7,8 +7,8 @@
|
||||
</div>
|
||||
<div v-else-if="!$props.isLoggedIn">
|
||||
<p>You are not logged into Apple Music.</p>
|
||||
<input class="fancy-button" type="file" multiple="true" accept="audio/*" id="pl-loader"><br>
|
||||
<button @click="loadPlaylistFromDisk()" class="fancy-button">Load custom playlist from disk</button>
|
||||
<input class="pl-loader-button" type="file" multiple="true" accept="audio/*" id="pl-loader"><br>
|
||||
<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>
|
||||
</div>
|
||||
<div class="playlist-wrapper">
|
||||
@@ -84,4 +84,8 @@
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pl-loader-button {
|
||||
background-color: white;
|
||||
}
|
||||
</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 ) {
|
||||
case "play":
|
||||
if ( this.isPreparedToPlay ) {
|
||||
this.control( 'pause' );
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.play();
|
||||
return false;
|
||||
@@ -285,6 +286,7 @@ class MusicKitJSWrapper {
|
||||
return false;
|
||||
}
|
||||
case "next":
|
||||
this.control( 'pause' );
|
||||
if ( this.queuePos < this.queue.length - 1 ) {
|
||||
this.queuePos += 1;
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
@@ -300,6 +302,7 @@ class MusicKitJSWrapper {
|
||||
return true;
|
||||
}
|
||||
case "previous":
|
||||
this.control( 'pause' );
|
||||
if ( this.queuePos > 0 ) {
|
||||
this.queuePos -= 1;
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
|
||||
@@ -15,7 +15,25 @@ class NotificationHandler {
|
||||
socket: Socket;
|
||||
|
||||
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 {
|
||||
this.socket.disconnect();
|
||||
}
|
||||
|
||||
joinRoom ( roomName: string ): void {
|
||||
// this.socket.
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface SongTransmitted {
|
||||
title: string;
|
||||
artist: string;
|
||||
duration: number;
|
||||
cover: string;
|
||||
additionalInfo?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface ReadFile {
|
||||
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 account from './account';
|
||||
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
|
||||
if ( typeof( __dirname ) === 'undefined' ) {
|
||||
@@ -22,6 +26,8 @@ const run = () => {
|
||||
origin: true
|
||||
} ) );
|
||||
|
||||
const httpServer = createServer( app );
|
||||
|
||||
// Load id.janishutz.com SDK and allow signing in
|
||||
sdk.routes( app, ( uid: string ) => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
@@ -42,11 +48,126 @@ const run = () => {
|
||||
} );
|
||||
}, 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' );
|
||||
} );
|
||||
|
||||
|
||||
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 ) => {
|
||||
// sign dev token
|
||||
@@ -73,7 +194,7 @@ const run = () => {
|
||||
|
||||
|
||||
const PORT = process.env.PORT || 8081;
|
||||
app.listen( PORT );
|
||||
httpServer.listen( PORT );
|
||||
}
|
||||
|
||||
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>
|
||||
.message-box {
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
z-index: -20;
|
||||
color: white;
|
||||
transition: all 0.5s;
|
||||
width: 95vw;
|
||||
|
||||
Reference in New Issue
Block a user