+
diff --git a/web/src/components/player/QueueViewer.vue b/web/src/components/player/QueueViewer.vue
index f115488..4fa10f9 100644
--- a/web/src/components/player/QueueViewer.vue
+++ b/web/src/components/player/QueueViewer.vue
@@ -63,11 +63,6 @@
Save
-
-
diff --git a/web/src/components/player/ShareManagement.vue b/web/src/components/player/ShareManagement.vue
new file mode 100644
index 0000000..a8da150
--- /dev/null
+++ b/web/src/components/player/ShareManagement.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+ Share
+
+
+ You can use a share to show what you are currently listening to (and the progress) on a page.
+
+
{{ errorMessage }}
+
+
+
+
+
+
+
Connected
+
+
+
+
+
diff --git a/web/src/components/shared/SharedQueue.vue b/web/src/components/shared/SharedQueue.vue
new file mode 100644
index 0000000..b534fcc
--- /dev/null
+++ b/web/src/components/shared/SharedQueue.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
![Song cover]()
+
+
+
+
{{ song.name }}
+
{{ song.artist }}
+
{{ song['additional-info'] }}
+
+
+
In {{ song.duration }}min
+
+
+
+
+
+
diff --git a/web/src/router/index.ts b/web/src/router/index.ts
index 2bdd70c..4b07e9c 100644
--- a/web/src/router/index.ts
+++ b/web/src/router/index.ts
@@ -27,6 +27,15 @@ const router = createRouter( {
'auth': true
}
},
+ {
+ 'path': '/share/:name',
+ 'name': 'share',
+ 'component': () => import( '@/views/SharedView.vue' ),
+ 'meta': {
+ 'title': 'Shared',
+ 'auth': false
+ }
+ },
{
'path': '/:pathMatch(.*)*',
'name': 'NotFound',
diff --git a/web/src/ts/messages/index.ts b/web/src/ts/messages/index.ts
index e69de29..61bbb35 100644
--- a/web/src/ts/messages/index.ts
+++ b/web/src/ts/messages/index.ts
@@ -0,0 +1,121 @@
+import {
+ duration,
+ playbackPercentage
+} from '../player/status-tracking';
+import {
+ isPlaying,
+ queue,
+ queueIdx
+} from '../player/state';
+import {
+ ref,
+ watch
+} from 'vue';
+import request from '../request';
+
+const RETRY_CAP = 10;
+
+export const room = ref( localStorage.getItem( 'room' ) ?? '' );
+
+export const isConnected = ref( false );
+
+export const useAntiTamper = ref( false );
+
+// const antiTamperClients = [];
+
+let connection: null | EventSource = null;
+let retries = 0;
+
+// TODO: Persist room name in local storage
+
+const createRoom = async ( name: string, antiTamper: boolean ): Promise
=> {
+ if ( !( /^[a-zA-Z0-9-]{3,20}$/ ).test( name ) ) return false;
+
+ try {
+ await request.get( '/room/create?room=' + name );
+ } catch ( err ) {
+ if ( err === 'ERR_409' ) {
+ return await connect();
+ }
+ }
+
+ localStorage.setItem( 'room', name );
+ useAntiTamper.value = antiTamper;
+ room.value = name;
+
+ return await connect();
+};
+
+const closeRoom = async () => {
+ // TODO: Trigger close on backend and disconnect.
+ isConnected.value = false;
+ localStorage.removeItem( 'room' );
+};
+
+const connect = (): Promise => {
+ return new Promise( ( resolve, reject ) => {
+ if ( !useAntiTamper.value ) {
+ isConnected.value = true;
+
+ return resolve( true );
+ }
+
+ connection = new EventSource( request.backendURL + `/room/${ room.value }/admin`, {
+ 'withCredentials': true
+ } );
+
+ connection.onopen = () => {
+ isConnected.value = true;
+ console.log( '[SSE] Connection established successfully' );
+ resolve( true );
+ };
+
+ connection.onmessage = msg => {
+ console.log( msg );
+ };
+
+ connection.onerror = () => {
+ connection?.close();
+ console.error( '[SSE] Reconnecting due to error' );
+
+ if ( !isConnected.value ) reject( 'ERR_CONNECT' );
+
+ if ( retries <= RETRY_CAP ) {
+ retries += 1;
+
+ setTimeout( () => {
+ createRoom( room.value, useAntiTamper.value );
+ }, 1000 * retries );
+ }
+ };
+ } );
+};
+
+const useRoomWatchers = () => {
+ watch( queue, () => {
+ if ( isConnected.value )
+ request.post( `/room/${ room.value }/update/playlist`, JSON.stringify( {
+ 'playlist': queue.value
+ } ) );
+ } );
+
+ watch( [
+ isPlaying,
+ queueIdx
+ ], () => {
+ 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();
+};
+
+export default {
+ createRoom,
+ useRoomWatchers,
+ closeRoom
+};
diff --git a/web/src/ts/player/index.ts b/web/src/ts/player/index.ts
index 3abcfdd..f419cdb 100644
--- a/web/src/ts/player/index.ts
+++ b/web/src/ts/player/index.ts
@@ -29,6 +29,7 @@ import type {
import {
load
} from './playlists/loader';
+import messages from '../messages';
import {
playIndex
} from './playlists';
@@ -148,6 +149,7 @@ const addSongFromSource = async ( source: string ): Promise => {
return true;
};
+messages.useRoomWatchers();
export default {
play,
diff --git a/web/src/ts/request.ts b/web/src/ts/request.ts
index 34fd3ac..8ff1f6c 100644
--- a/web/src/ts/request.ts
+++ b/web/src/ts/request.ts
@@ -9,6 +9,8 @@ const get = async ( url: string ): Promise => {
} );
};
+const backendURL = import.meta.env.VITE_BACKEND_URL;
+
const post = async ( url: string, payload: string, mime: string = 'application/json' ): Promise => {
return await wrapper( url, {
'credentials': 'include',
@@ -21,11 +23,12 @@ const post = async ( url: string, payload: string, mime: string = 'application/j
};
const wrapper = async ( url: string, opts: RequestInit ): Promise => {
- const res = await fetch( import.meta.env.VITE_BACKEND_URL + url, opts );
+ const res = await fetch( backendURL + url, opts );
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)
throw new AuthError( 'ERR_USER_UNAUTHORIZED' );
} else if ( res.status === 402 ) {
throw new UnownedError( 'ERR_USER_UNOWNED' );
@@ -36,5 +39,6 @@ const wrapper = async ( url: string, opts: RequestInit ): Promise => {
export default {
get,
- post
+ post,
+ backendURL
};
diff --git a/web/src/ts/shared/index.ts b/web/src/ts/shared/index.ts
new file mode 100644
index 0000000..96139fa
--- /dev/null
+++ b/web/src/ts/shared/index.ts
@@ -0,0 +1,47 @@
+import request from '../request';
+
+const RETRY_CAP = 10;
+
+let room = location.pathname;
+let connection: EventSource | null = null;
+let hasConnected = false;
+let retries = 0;
+
+// TODO: Persist settings in local storage
+
+const connect = (): Promise => {
+ return new Promise( ( resolve, reject ) => {
+ room = location.pathname.substring( location.pathname.lastIndexOf( '/' ) + 1 );
+ connection = new EventSource( request.backendURL + `/room/${ room }/connect` );
+
+ connection.onopen = () => {
+ hasConnected = true;
+ console.log( '[SSE] Connection established successfully' );
+ resolve();
+ };
+
+ connection.onmessage = msg => {
+ console.log( msg.data );
+ // TODO: On connect, retrieve data from poll endpoint
+ };
+
+ connection.onerror = () => {
+ connection?.close();
+
+ if ( !hasConnected ) return reject( 'ERR_CONNECT' );
+
+ console.error( '[SSE] Connection failed, reconnecting' );
+
+ if ( retries <= RETRY_CAP ) {
+ retries += 1;
+ setTimeout( () => {
+ connect();
+ }, 1000 * retries );
+ }
+ };
+ } );
+};
+
+export default {
+ connect
+};
diff --git a/web/src/views/SharedView.vue b/web/src/views/SharedView.vue
index e69de29..07d5e57 100644
--- a/web/src/views/SharedView.vue
+++ b/web/src/views/SharedView.vue
@@ -0,0 +1,20 @@
+
+
+
+
+