diff --git a/backend/data/server-stubs.json b/backend/data/server-stubs.json index 8d384c9..a099786 100644 --- a/backend/data/server-stubs.json +++ b/backend/data/server-stubs.json @@ -1 +1 @@ -{"playlists":[],"userid":"server-stubs","version":"1"} +{"playlists":[{"name":"Hello World","songs":[{"duration":236,"name":"CAN'T STOP THE FEELING! (Original Song From DreamWorks Animation's \"TROLLS\")","additional-info":"Test 3","artist":"Justin Timberlake","artwork":"https://is1-ssl.mzstatic.com/image/thumb/Music124/v4/52/d0/02/52d002d7-1f7f-8d91-4791-ed04c460ec93/886445894653.jpg/2000x2000bb.jpg","identifier":"blob:http://localhost:8081/fdb6fccd-30cb-458c-a9f6-c424d2f53c43","source":"local","additional-identifier":"16_Can't_Stop_The_Feeling-Justin_Timberlake.mp3__4407330"},{"duration":258,"name":"Muévelo","additional-info":"Test","artist":"Rey Ruiz","artwork":"https://is1-ssl.mzstatic.com/image/thumb/Music122/v4/58/15/0d/58150db1-0e74-dd19-f7eb-3f15e8a04b56/197187182459.jpg/2000x2000bb.jpg","identifier":"blob:http://localhost:8081/18dcea36-839d-4714-b7fc-6800f26e07b9","source":"local","additional-identifier":"18_Muevlo-Rey_Ruiz.mp3__4831574"}]}],"userid":"server-stubs","version":"1"} \ No newline at end of file diff --git a/web/src/components/main/PlayerComponent.vue b/web/src/components/main/PlayerComponent.vue index 2ffc248..0bcdd34 100644 --- a/web/src/components/main/PlayerComponent.vue +++ b/web/src/components/main/PlayerComponent.vue @@ -13,7 +13,7 @@
-
+
@@ -44,6 +44,15 @@ flex-direction: column; align-items: center; justify-content: center; + position: relative; + + >.current-song-wrapper { + margin-bottom: 1.5rem; + width: 100%; + height: 100%; + max-height: calc(100% - 15rem); + overflow: hidden; + } } } diff --git a/web/src/components/main/PlayerWrapper.vue b/web/src/components/main/PlayerWrapper.vue index 2ba74fd..a82ce29 100644 --- a/web/src/components/main/PlayerWrapper.vue +++ b/web/src/components/main/PlayerWrapper.vue @@ -32,6 +32,7 @@ background-color: var(--secondary-color); transition: bottom 1s ease; transition-delay: 0.25s; + overflow: hidden; &.hidden { transition-delay: 0s; diff --git a/web/src/components/main/SmallPlayerComponent.vue b/web/src/components/main/SmallPlayerComponent.vue index d7137e4..1d58526 100644 --- a/web/src/components/main/SmallPlayerComponent.vue +++ b/web/src/components/main/SmallPlayerComponent.vue @@ -1,4 +1,24 @@ diff --git a/web/src/components/player/CurrentSong.vue b/web/src/components/player/CurrentSong.vue index 09a091f..e367324 100644 --- a/web/src/components/player/CurrentSong.vue +++ b/web/src/components/player/CurrentSong.vue @@ -6,6 +6,9 @@ const song = defineModel( { 'required': false } ); + const props = defineProps<{ + 'showAdditionalInfo'?: boolean + }>(); @@ -31,14 +37,22 @@ diff --git a/web/src/scss/shared/main.scss b/web/src/scss/shared/main.scss new file mode 100644 index 0000000..1a7b4a2 --- /dev/null +++ b/web/src/scss/shared/main.scss @@ -0,0 +1,44 @@ +.shared-view { + width: 100%; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + flex-direction: row; + + .panel { + width: 45%; + margin-left: 2.5%; + margin-right: 2.5%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + + >.current-song-wrapper { + margin-bottom: 1.5rem; + width: 100%; + height: 100%; + max-height: calc(100% - 10rem); + overflow: hidden; + } + + >.time { + display: flex; + width: 85%; + margin-left: auto; + margin-right: auto; + + >p { + margin-top: 0px; + margin-bottom: 5px; + } + + >.duration { + margin-left: auto; + } + } + } +} diff --git a/web/src/ts/dtype/global.d.ts b/web/src/ts/dtype/global.d.ts index 9cab2e4..f3dd2cd 100644 --- a/web/src/ts/dtype/global.d.ts +++ b/web/src/ts/dtype/global.d.ts @@ -8,5 +8,8 @@ declare global { 'musicplayer:playpause': CustomEvent; 'musicplayer:playindex': CustomEvent; 'musicplayer:seek': CustomEvent; + 'musicplayer:update': CustomEvent; + 'musicplayer:reauth': CustomEvent; + 'musicplayer:autherror': CustomEvent; } } diff --git a/web/src/ts/messages/anti-tamper.ts b/web/src/ts/messages/anti-tamper.ts new file mode 100644 index 0000000..e69de29 diff --git a/web/src/ts/messages/index.ts b/web/src/ts/messages/index.ts index 3349758..deb196c 100644 --- a/web/src/ts/messages/index.ts +++ b/web/src/ts/messages/index.ts @@ -12,6 +12,9 @@ import { import { playbackPercentage } from '../player/status-tracking'; +import { + reauth +} from '../util/reauth'; import request from '../request'; const RETRY_CAP = 10; @@ -43,6 +46,8 @@ const createRoom = async ( name: string, antiTamper: boolean ): Promise useAntiTamper.value = antiTamper; room.value = name; + document.addEventListener( 'musicplayer:autherror', reauth ); + return await connect(); }; @@ -50,6 +55,11 @@ const closeRoom = async () => { // TODO: Trigger close on backend and disconnect. isConnected.value = false; localStorage.removeItem( 'room' ); + connection?.close(); + + try { + document.removeEventListener( 'musicplayer:autherror', reauth ); + } catch { /* empty */ } }; const connect = (): Promise => { @@ -102,9 +112,14 @@ let stateLock = false; const sendPlaylistData = () => { if ( isConnected.value && !playlistLock ) { playlistLock = true; - request.post( `/room/${ room.value }/update/playlist`, JSON.stringify( { - 'playlist': queue.value - } ) ); + + try { + request.post( `/room/${ room.value }/update/playlist`, JSON.stringify( { + 'playlist': queue.value + } ) ); + } catch ( e ) { + console.error( e ); + } setTimeout( () => { playlistLock = false; @@ -112,15 +127,20 @@ const sendPlaylistData = () => { } }; -const sendStateData = () => { +const sendStateData = async () => { if ( isConnected.value && !stateLock ) { stateLock = true; - request.post( `/room/${ room.value }/update/state`, JSON.stringify( { - 'playing': isPlaying.value, - 'index': queueIdx.value, - 'start': new Date().getTime() - 100, - 'offset': playbackPercentage.value * ( queue.value[ queueIdx.value ]?.duration ?? 0 ) - } ) ); + + try { + request.post( `/room/${ room.value }/update/state`, JSON.stringify( { + 'playing': isPlaying.value, + 'index': queueIdx.value, + 'start': new Date().getTime() - 100, + 'offset': playbackPercentage.value * ( queue.value[ queueIdx.value ]?.duration ?? 0 ) + } ) ); + } catch ( e ) { + console.error( e ); + } setTimeout( () => { stateLock = false; @@ -135,6 +155,7 @@ const useRoomWatchers = () => { document.addEventListener( 'musicplayer:playindex', sendStateData ); document.addEventListener( 'musicplayer:seek', sendStateData ); document.addEventListener( 'musicplayer:playpause', sendStateData ); + document.addEventListener( 'musicplayer:update', sendPlaylistData ); } ); onUnmounted( () => { @@ -149,6 +170,10 @@ const useRoomWatchers = () => { try { document.addEventListener( 'musicplayer:playpause', sendStateData ); } catch { /* empty */ } + + try { + document.addEventListener( 'musicplayer:update', sendPlaylistData ); + } catch { /* empty */ } } ); if ( room.value ) connect(); diff --git a/web/src/ts/request.ts b/web/src/ts/request.ts index 8ff1f6c..3237155 100644 --- a/web/src/ts/request.ts +++ b/web/src/ts/request.ts @@ -28,7 +28,8 @@ const wrapper = async ( url: string, opts: RequestInit ): Promise => { if ( res.ok ) { return res; } else if ( res.status === 403 || res.status === 401 ) { - // TODO: Handle these errors better (probably do something like in the old version, but better) + document.dispatchEvent( new CustomEvent( 'musicplayer:autherror' ) ); + throw new AuthError( 'ERR_USER_UNAUTHORIZED' ); } else if ( res.status === 402 ) { throw new UnownedError( 'ERR_USER_UNOWNED' ); diff --git a/web/src/ts/shared/index.ts b/web/src/ts/shared/index.ts index ec29d08..8daa20c 100644 --- a/web/src/ts/shared/index.ts +++ b/web/src/ts/shared/index.ts @@ -7,6 +7,7 @@ import { currentQueueIdx, isPlaying, playbackOffset, + playbackProgress, playbackTime, startTime } from './state'; @@ -44,7 +45,7 @@ const connect = (): Promise => { startTime.value = data.state.start; playbackTime.value = data.state.offset; playbackOffset.value = data.state.offset; - + playbackProgress.value = data.state.offset / ( data.playlist[ data.state.index ]?.duration ?? -1 ); resolve(); }; diff --git a/web/src/ts/shared/state.ts b/web/src/ts/shared/state.ts index 3cf4f6a..4b75483 100644 --- a/web/src/ts/shared/state.ts +++ b/web/src/ts/shared/state.ts @@ -19,3 +19,5 @@ export const showArtworks = ref( false ); export const playbackTime = ref( 0 ); export const playbackOffset = ref( 0 ); + +export const playbackProgress = ref( 0 ); diff --git a/web/src/ts/util/reauth.ts b/web/src/ts/util/reauth.ts new file mode 100644 index 0000000..38d4baf --- /dev/null +++ b/web/src/ts/util/reauth.ts @@ -0,0 +1,17 @@ +export const reauth = () => { + localStorage.setItem( 'close-tab', 'true' ); + + const listener = () => { + if ( localStorage.getItem( 'reauth-ok' ) === 'true' ) { + try { + window.removeEventListener( 'storage', listener ); + } catch { /* empty */ } + + localStorage.removeItem( 'reauth-ok' ); + + document.dispatchEvent( new CustomEvent( 'musicplayer:reauth' ) ); + } + }; + + window.addEventListener( 'storage', listener ); +}; diff --git a/web/src/views/SharedView.vue b/web/src/views/SharedView.vue index a9fbe83..7e4c32e 100644 --- a/web/src/views/SharedView.vue +++ b/web/src/views/SharedView.vue @@ -2,14 +2,14 @@ import { type ComputedRef, computed, - onMounted, - ref + onMounted } from 'vue'; import { currentQueue, currentQueueIdx, isPlaying, playbackOffset, + playbackProgress, playbackTime, startTime } from '@/ts/shared/state'; @@ -25,7 +25,6 @@ import shared from '@/ts/shared'; shared.connect(); - const playbackProgress = ref( 0 ); onMounted( () => { setInterval( () => { @@ -62,22 +61,26 @@ + + diff --git a/web/src/views/StartPage.vue b/web/src/views/StartPage.vue index 84bf638..fbbfca0 100644 --- a/web/src/views/StartPage.vue +++ b/web/src/views/StartPage.vue @@ -18,13 +18,24 @@ store.isAuth = await sdk.verify(); if ( store.isAuth ) - router.push( '/app' ); + isAuthorizedHandler(); isLoggingIn.value = false; // TODO: Logout button } ); + const isAuthorizedHandler = () => { + if ( localStorage.getItem( 'close-tab' ) === 'true' ) { + localStorage.setItem( 'login-ok', 'true' ); + localStorage.removeItem( 'close-tab' ); + + return window.close(); + } + + router.push( '/app' ); + }; + const login = () => { if ( isLoggingIn.value ) return;