diff --git a/frontend/src/appleMusicRoutes.js b/frontend/src/appleMusicRoutes.js index 9e8980a..dd19e99 100644 --- a/frontend/src/appleMusicRoutes.js +++ b/frontend/src/appleMusicRoutes.js @@ -17,4 +17,16 @@ module.exports = ( app ) => { app.get( '/apple-music/helpers/:file', ( req, res ) => { res.sendFile( path.join( __dirname + '/client/appleMusic/' + req.params.file ) ); } ); + + app.get( '/icon-font.css', ( req, res ) => { + res.sendFile( path.join( __dirname + '/client/icon-font.css' ) ); + } ); + + app.get( '/iconFont.woff2', ( req, res ) => { + res.sendFile( path.join( __dirname + '/client/iconFont.woff2' ) ); + } ); + + app.get( '/logo.png', ( req, res ) => { + res.sendFile( path.join( __dirname + '/client/logo.png' ) ); + } ); } \ No newline at end of file diff --git a/frontend/src/client/appleMusic/index.html b/frontend/src/client/appleMusic/index.html index 5059789..6e016fe 100644 --- a/frontend/src/client/appleMusic/index.html +++ b/frontend/src/client/appleMusic/index.html @@ -5,6 +5,9 @@ MusicPlayerV2 + + +
@@ -12,12 +15,93 @@

Apple Music

-
- +
+
+
+

{{ playlist.title }}

+
+
+
+
+ +
+
+
+ skip_previous + replay_10 + play_arrow + pause + play_disabled + forward_10 + skip_next + shuffle + shuffle_on + repeat + repeat_one_on + repeat_on +
+ info +
+

IP to connect to:


+

{{ localIP }}:8081

+
+
+
+
+ +
+
+ music_note + +
+ music_note +
+

{{ playingSong.title ?? 'No song selected' }}

+

{{ playingSong.artist }}

+
+
+
+
+
{{ playbackPosBeautified }}
+
{{ durationBeautified }}
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ play_arrow + pause +

{{ song.title }}

+
+
+
+
- \ No newline at end of file diff --git a/frontend/src/client/appleMusic/index.js b/frontend/src/client/appleMusic/index.js index 6cbf3b5..80e1ff1 100644 --- a/frontend/src/client/appleMusic/index.js +++ b/frontend/src/client/appleMusic/index.js @@ -3,14 +3,41 @@ const app = Vue.createApp( { return { musicKit: null, isLoggedIn: false, + config: { + 'devToken': '', + 'userToken': '' + }, + playlists: {}, + hasSelectedPlaylist: false, + songQueue: {}, + queuePos: 0, + pos: 0, + playingSong: {}, + isPlaying: false, + isShuffleEnabled: false, + repeatMode: 'off', + audioLoaded: false, + // TODO: Set audio loaded to true + + // slider + offset: 0, + isDragging: false, + sliderPos: 0, + originalPos: 0, + sliderProgress: 0, + position: 0, + active: false, } }, methods: { logInto() { if ( !this.musicKit.isAuthorized ) { + this.musicKit.authorize().then( () => { + this.isLoggedIn = true; + } ); + } else { this.musicKit.authorize().then( () => { this.musicKit.play(); - this.isLoggedIn(); } ); } }, @@ -26,13 +53,112 @@ const app = Vue.createApp( { build: '2' } } ); + this.config.devToken = token; this.musicKit = MusicKit.getInstance(); if ( this.musicKit.isAuthorized ) { this.isLoggedIn = true; + this.config.userToken = this.musicKit.musicUserToken; } + this.apiGetRequest( 'https://api.music.apple.com/v1/me/library/playlists', this.playlistHandler ); } ); } } ); + }, + playlistHandler ( data ) { + if ( data.status === 'ok' ) { + const d = data.data.data; + this.playlist = {}; + for ( let el in d ) { + this.playlists[ d[ el ].id ] = { + title: d[ el ].attributes.name, + id: d[ el ].id, + playParams: d[ el ].attributes.playParams, + } + } + } + }, + apiGetRequest( url, callback ) { + if ( this.config.devToken != '' && this.config.userToken != '' ) { + fetch( url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${ this.config.devToken }`, + 'Music-User-Token': this.config.userToken + } + } ).then( res => { + if ( res.status === 200 ) { + res.json().then( json => { + try { + callback( { 'status': 'ok', 'data': json } ); + } catch( err ) {} + } ); + } else { + try { + callback( { 'status': 'error', 'error': res.status } ); + } catch( err ) {} + } + } ); + } else return false; + }, + selectPlaylist( id ) { + this.musicKit.api.library.playlist( id ).then( playlist => { + const tracks = playlist.relationships.tracks.data.map( tracks => tracks.id ); + + this.musicKit.setQueue( { songs: tracks } ).then( () => { + try { + this.musicKit.play(); + this.hasSelectedPlaylist = true; + } catch( err ) { + this.hasSelectedPlaylist = false; + console.error( err ); + alert( 'We were unable to play. Please ensure that DRM (yeah sorry it is Apple Music, we cannot do anything about that) is enabled and working' ); + } + } ).catch( err => { + console.error( 'ERROR whilst settings Queue', err ); + } ) + } ); + }, + handleDrag( e ) { + if ( this.isDragging ) { + if ( 0 < this.originalPos + e.screenX - this.offset && this.originalPos + e.screenX - this.offset < document.getElementById( 'progress-slider-' + this.name ).clientWidth - 5 ) { + this.sliderPos = e.screenX - this.offset; + this.calcProgressPos(); + } + } + }, + startMove( e ) { + this.offset = e.screenX; + this.isDragging = true; + document.getElementById( 'drag-support' ).classList.add( 'drag-support-active' ); + }, + stopMove() { + this.originalPos += parseInt( this.sliderPos ); + this.isDragging = false; + this.offset = 0; + this.sliderPos = 0; + document.getElementById( 'drag-support' ).classList.remove( 'drag-support-active' ); + this.calcPlaybackPos(); + }, + setPos ( e ) { + if ( this.active ) { + this.originalPos = e.offsetX; + this.calcProgressPos(); + this.calcPlaybackPos(); + } + }, + calcProgressPos() { + this.sliderProgress = Math.ceil( ( this.originalPos + parseInt( this.sliderPos ) ) / ( document.getElementById( 'progress-slider-' + this.name ).clientWidth - 5 ) * 1000 ); + }, + calcPlaybackPos() { + this.pos = Math.round( ( this.originalPos + parseInt( this.sliderPos ) ) / ( document.getElementById( 'progress-slider-' + this.name ).clientWidth - 5 ) * this.duration ); + } + }, + watch: { + position() { + if ( !this.isDragging ) { + this.sliderProgress = Math.ceil( this.position / this.duration * 1000 + 2 ); + this.originalPos = Math.ceil( this.position / this.duration * ( document.getElementById( 'progress-slider-' + this.name ).scrollWidth - 5 ) ); + } } }, created() { diff --git a/frontend/src/client/appleMusic/playerStyle.css b/frontend/src/client/appleMusic/playerStyle.css new file mode 100644 index 0000000..8ac667a --- /dev/null +++ b/frontend/src/client/appleMusic/playerStyle.css @@ -0,0 +1,108 @@ +.song-info { + background-color: #8e9ced; + height: 13vh; + width: 50%; + margin-left: auto; + margin-right: auto; + position: relative; +} + +.image { + width: 7vh; + height: 7vh; + object-fit: cover; + object-position: center; + font-size: 7vh; + margin-left: 1vh; + margin-top: 1vh; +} + +.name { + margin-left: auto; + margin-right: auto; +} + +.song-info-wrapper { + display: flex; + flex-direction: row; +} + +.song-info-wrapper h3 { + margin: 0; + margin-bottom: 0.5vh; + margin-top: 1vh; +} + +.controls { + margin-left: 5%; + display: flex; + justify-content: center; + align-items: center; +} + +.control-icon { + cursor: pointer; + font-size: 3vh; + user-select: none; +} + +.play-pause { + font-size: 5vh; +} + +.inactive { + color: gray; + cursor: default; +} + +.player { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.playback-pos-info { + display: flex; + flex-direction: row; + width: 98%; + margin-left: 1%; + position: absolute; + bottom: 17px; +} + +#showIP { + background-color: rgb(63, 63, 63); + display: none; + position: absolute; + min-height: 16vh; + padding: 2vh; + min-width: 20vw; + z-index: 10; + justify-content: center; + align-items: center; + flex-direction: column; + font-size: 70%; + border-radius: 5px 10px 10px 10px; +} + +#showIP h4, #showIP p { + margin: 0; +} + +#settings:hover #showIP { + display: flex; +} + +#showIP::before { + content: " "; + position: absolute; + bottom: 100%; /* At the bottom of the tooltip */ + left: 0; + margin-left: 3px; + border-width: 10px; + border-style: solid; + border-color: transparent transparent rgb(63, 63, 63) transparent; +} \ No newline at end of file diff --git a/frontend/src/client/appleMusic/style.css b/frontend/src/client/appleMusic/style.css index 64abd8c..3c8c1ca 100644 --- a/frontend/src/client/appleMusic/style.css +++ b/frontend/src/client/appleMusic/style.css @@ -12,5 +12,262 @@ body, html { height: 100%; margin: 0; padding: 0; + background-color: rgb(49, 49, 49); font-family: sans-serif; + color: white; +} + +/* Main style */ + +.home { + width: 100%; + height: 100%; +} + +.pool-wrapper { + height: 84vh; + margin-top: 16vh; +} + +.top-bar { + top: 0; + margin-left: auto; + margin-right: auto; + position: fixed; + z-index: 8; + width: 99%; + height: 15vh; + display: flex; + align-items: center; + flex-direction: row; + border: white 2px solid; + background-color: rgb(49, 49, 49); +} + +.player-wrapper { + width: 70vw; + margin-right: auto; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.logo { + height: 13vh; + margin-left: 3%; + margin-right: auto; +} + + +/* Media Pool */ + +.playing-symbols { + position: absolute; + left: 10%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: row; + margin: 0; + 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 ); + } +} + +.loading-spinner { + animation: spin 2s infinite linear; +} + +@keyframes spin { + from { + transform: rotate( 0deg ); + } + to { + transform: rotate( 720deg ); + } +} + +.media-pool { + width: 100%; + height: 100%; + display: flex; + align-items: center; + flex-direction: column; +} + +.no-songs { + height: 50vh; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.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; +} + +.song-list h3 { + 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; +} + +.play-icon, .pause-icon { + display: none; + width: 5vw; + height: 5vw; + object-fit: cover; + object-position: center; + font-size: 5vw; + cursor: pointer; + user-select: none; +} + +.playing:hover .pause-icon { + display: block; +} + +.playing:hover .playing-symbols { + display: none; +} + +.song-list:hover .song-image { + display: none; +} + +.not-playing:hover .play-icon { + display: block; +} + +.active-song .pause-icon { + display: block; +} + +.active-song .song-image, .active-song:hover .pause-icon { + display: none; +} + +/* Slider */ + +.progress-slider { + width: 100%; + margin: 0; + position: absolute; + left: 0; + bottom: 0; + height: 5px; + cursor: pointer; + background-color: #baf4c9; +} + +.progress-slider::-webkit-progress-value { + background-color: #baf4c9; +} + +#slider-knob { + height: 20px; + width: 10px; + display: flex; + justify-content: flex-start; + align-items: flex-end; + position: absolute; + bottom: 0; + left: 0; + z-index: 2; + cursor: grab; +} + +#slider-knob-style { + background-color: #baf4c9; + height: 15px; + width: 5px; +} + +#drag-support { + display: none; + opacity: 0; + height: 100vh; + width: 100vw; + position: fixed; + top: 0; + left: 0; + z-index: 10; + cursor: grabbing; +} + +.drag-support-active { + display: block !important; +} + +.slider-inactive { + cursor: default !important; } \ No newline at end of file diff --git a/frontend/src/client/icon-font.css b/frontend/src/client/icon-font.css new file mode 100644 index 0000000..c221497 --- /dev/null +++ b/frontend/src/client/icon-font.css @@ -0,0 +1,24 @@ +/* fallback */ +@font-face { + font-family: 'Material Symbols Outlined'; + font-style: normal; + font-weight: 100 700; + src: url(/iconFont.woff2) format('woff2'); + } + + .material-symbols-outlined { + font-family: 'Material Symbols Outlined'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -moz-font-feature-settings: 'liga'; + -moz-osx-font-smoothing: grayscale; + } + \ No newline at end of file diff --git a/frontend/src/client/iconFont.woff2 b/frontend/src/client/iconFont.woff2 new file mode 100644 index 0000000..3ac032b Binary files /dev/null and b/frontend/src/client/iconFont.woff2 differ diff --git a/frontend/src/client/logo.png b/frontend/src/client/logo.png new file mode 100644 index 0000000..52cdfa9 Binary files /dev/null and b/frontend/src/client/logo.png differ