feat: basic rendering of queue on share screen

This commit is contained in:
2026-09-07 20:24:23 +02:00
parent 054fda2fb9
commit 6d9853f2c1
8 changed files with 158 additions and 34 deletions
+2 -2
View File
@@ -152,9 +152,9 @@ const sendUpdate = ( room: string, kind: 'state' | 'playlist' ) => {
if ( !roomObject ) return false; if ( !roomObject ) return false;
roomObject.clients.forEach( client => client.response.write( `data: ${ JSON.stringify( { roomObject.clients.forEach( client => client.response.write( `data: json:${ JSON.stringify( {
'type': kind, 'type': kind,
'data': JSON.stringify( roomObject[kind] ) 'data': roomObject[kind]
} ) }\n\n` ) ); } ) }\n\n` ) );
return true; return true;
+1 -1
View File
@@ -32,7 +32,7 @@ const routes = ( app: express.Application, foss: boolean, config: Config ) => {
response.send( response.send(
JSON.stringify( { JSON.stringify( {
'state': room?.state, 'state': room?.state,
'playlist': lastRequest < room!.playlist!.lastUpdate ? room!.playlist : undefined 'playlist': ( lastRequest < room!.playlist!.lastUpdate || isNaN( lastRequest ) ) ? room!.playlist.playlist : undefined
} ) } )
); );
} ); } );
+19 -9
View File
@@ -3,17 +3,27 @@
computed, computed,
ref ref
} from 'vue'; } from 'vue';
import type { import {
Song currentQueue,
} from '@/ts/dtype/playlist'; currentQueueIdx
} from '@/ts/shared/state';
const props = defineProps<{ const songs = computed( () => currentQueue.value?.slice( currentQueueIdx.value + 1 ) ?? [] );
'songs': Song[],
'idx': number
}>();
const songs = computed( () => props.songs?.slice( props.idx + 1 ) ?? [] );
// TODO: Move this out of this file // TODO: Move this out of this file
const showArtworks = ref( false ); const showArtworks = ref( false );
const timeToPlay = computed( () => {
return ( idx: number ) => {
let total = 0;
for ( let i = 0; i < idx; i++ ) {
total += songs.value[ i ]!.duration;
}
total += currentQueue.value[ currentQueueIdx.value ]?.duration ?? 0;
return Math.round( total / 60 );
};
} );
</script> </script>
<template> <template>
@@ -34,7 +44,7 @@
<p>{{ song['additional-info'] }}</p> <p>{{ song['additional-info'] }}</p>
</div> </div>
<div class="song-actions"> <div class="song-actions">
<p>In {{ song.duration }}min</p> <p>In {{ timeToPlay( index ) }}min</p>
</div> </div>
</div> </div>
</div> </div>
+39 -16
View File
@@ -1,7 +1,3 @@
import {
duration,
playbackPercentage
} from '../player/status-tracking';
import { import {
isPlaying, isPlaying,
queue, queue,
@@ -11,6 +7,9 @@ import {
ref, ref,
watch watch
} from 'vue'; } from 'vue';
import {
playbackPercentage
} from '../player/status-tracking';
import request from '../request'; import request from '../request';
const RETRY_CAP = 10; const RETRY_CAP = 10;
@@ -56,6 +55,8 @@ const connect = (): Promise<boolean> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
if ( !useAntiTamper.value ) { if ( !useAntiTamper.value ) {
isConnected.value = true; isConnected.value = true;
sendPlaylistData();
sendStateData();
return resolve( true ); return resolve( true );
} }
@@ -67,6 +68,8 @@ const connect = (): Promise<boolean> => {
connection.onopen = () => { connection.onopen = () => {
isConnected.value = true; isConnected.value = true;
console.log( '[SSE] Connection established successfully' ); console.log( '[SSE] Connection established successfully' );
sendPlaylistData();
sendStateData();
resolve( true ); resolve( true );
}; };
@@ -91,25 +94,45 @@ const connect = (): Promise<boolean> => {
} ); } );
}; };
const useRoomWatchers = () => { // FIXME: This is not a sensible solution, but easy for now (i.e. solve properly)
watch( queue, () => { let playlistLock = false;
if ( isConnected.value ) let stateLock = false;
const sendPlaylistData = () => {
if ( isConnected.value && !playlistLock ) {
playlistLock = true;
request.post( `/room/${ room.value }/update/playlist`, JSON.stringify( { request.post( `/room/${ room.value }/update/playlist`, JSON.stringify( {
'playlist': queue.value 'playlist': queue.value
} ) ); } ) );
} );
setTimeout( () => {
playlistLock = false;
}, 500 );
}
};
const sendStateData = () => {
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() - ( playbackPercentage.value * ( queue.value[ queueIdx.value ]?.duration ?? 0 ) ) - 100
} ) );
setTimeout( () => {
stateLock = false;
}, 500 );
}
};
const useRoomWatchers = () => {
watch( queue, sendPlaylistData );
watch( [ watch( [
isPlaying, isPlaying,
queueIdx queueIdx
], () => { ], sendStateData );
if ( isConnected.value )
request.post( `/room/${ room.value }/update/state`, JSON.stringify( {
'playing': isPlaying.value,
'index': queueIdx.value,
'start': new Date().getTime() - ( playbackPercentage.value * duration.value ) - 100
} ) );
} );
if ( room.value ) connect(); if ( room.value ) connect();
}; };
+33 -3
View File
@@ -1,3 +1,16 @@
import {
type StateUpdate,
messageHandler
} from './messageHandler';
import {
currentQueue,
currentQueueIdx,
isPlaying,
startTime
} from './state';
import type {
Song
} from '../dtype/playlist';
import request from '../request'; import request from '../request';
const RETRY_CAP = 10; const RETRY_CAP = 10;
@@ -8,21 +21,38 @@ let hasConnected = false;
let retries = 0; let retries = 0;
// TODO: Persist settings in local storage // TODO: Persist settings in local storage
// TODO: Polling instead of sse
const connect = (): Promise<void> => { const connect = (): Promise<void> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
room = location.pathname.substring( location.pathname.lastIndexOf( '/' ) + 1 ); room = location.pathname.substring( location.pathname.lastIndexOf( '/' ) + 1 );
connection = new EventSource( request.backendURL + `/room/${ room }/connect` ); connection = new EventSource( request.backendURL + `/room/${ room }/connect` );
connection.onopen = () => { connection.onopen = async () => {
hasConnected = true; hasConnected = true;
console.log( '[SSE] Connection established successfully' ); console.log( '[SSE] Connection established successfully' );
const data = await ( await request.get( `/room/${ room }/poll` ) ).json() as {
'playlist': Song[],
'state': StateUpdate
};
currentQueue.value = data.playlist;
currentQueueIdx.value = data.state.index;
isPlaying.value = data.state.playing;
startTime.value = data.state.start;
resolve(); resolve();
}; };
connection.onmessage = msg => { connection.onmessage = msg => {
console.log( msg.data ); if ( msg.data === 'close' ) {
// TODO: On connect, retrieve data from poll endpoint connection?.close();
// TODO: Show popup informing user that share was closed
return;
}
messageHandler( msg.data );
}; };
connection.onerror = () => { connection.onerror = () => {
+46
View File
@@ -0,0 +1,46 @@
import {
currentQueue,
currentQueueIdx,
isPlaying,
startTime
} from './state';
import type {
Song
} from '../dtype/playlist';
interface ReceivedJSONMessage {
'type': 'state' | 'playlist';
'data': unknown;
}
export interface StateUpdate {
'playing': boolean;
'index': number;
'start': number;
}
export const messageHandler = ( msg: string ) => {
if ( msg.startsWith( 'json:' ) ) {
try {
const data = JSON.parse( msg.substring( 5 ) ) as ReceivedJSONMessage;
if ( data.type === 'playlist' ) {
currentQueue.value = ( data.data as {
'playlist': Song[]
} ?? {
'playlist': []
} ).playlist;
} else if ( data.type === 'state' ) {
const state = data.data as StateUpdate;
currentQueueIdx.value = state.index;
isPlaying.value = state.playing;
startTime.value = state.start;
} else {
console.log( '[SSE] Received unknown data', data.type );
}
} catch ( err ) {
console.error( 'JSON DECODE failed with error', err );
}
}
};
+15
View File
@@ -0,0 +1,15 @@
import {
type Ref,
ref
} from 'vue';
import type {
Song
} from '../dtype/playlist';
export const currentQueue: Ref<Song[]> = ref( [] );
export const isPlaying = ref( false );
export const currentQueueIdx = ref( -1 );
export const startTime = ref( new Date().getTime() );
+1 -1
View File
@@ -13,7 +13,7 @@
<CurrentSong /> <CurrentSong />
</div> </div>
<div> <div>
<SharedQueue :songs="[]" :idx="-1" /> <SharedQueue />
</div> </div>
</div> </div>
</div> </div>