mirror of
https://github.com/janishutz/MusicPlayer.git
synced 2026-09-10 14:25:24 +02:00
feat: improve sync
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
import {
|
||||
beautifyTime
|
||||
} from '@/ts/util/time';
|
||||
import messages from '@/ts/messages';
|
||||
import player from '@/ts/player';
|
||||
|
||||
const playbackPercentage = player.playbackPercentage;
|
||||
@@ -42,6 +43,8 @@
|
||||
const duration = computed( () => {
|
||||
return beautifyTime( player.duration.value );
|
||||
} );
|
||||
|
||||
messages.useRoomWatchers();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -12,8 +12,13 @@
|
||||
const offset = ref( -1 );
|
||||
const isMoving = ref( false );
|
||||
const bar = useTemplateRef( 'bar' );
|
||||
const props = defineProps<{
|
||||
'disallowMove'?: boolean
|
||||
}>();
|
||||
|
||||
const start = ( ev: MouseEvent ) => {
|
||||
if ( props.disallowMove ) return;
|
||||
|
||||
offset.value = bar.value!.getBoundingClientRect().x;
|
||||
isMoving.value = true;
|
||||
moveVal.value = ( ev.x - offset.value ) / bar.value!.clientWidth;
|
||||
@@ -47,7 +52,7 @@
|
||||
<div :style="`width: ${ isMoving ? moveVal * 100 : val * 100 }%;`"></div>
|
||||
</div>
|
||||
<div
|
||||
:class="['click-target', offset >= 0 ? 'active' : undefined]"
|
||||
:class="['click-target', offset >= 0 ? 'active' : undefined, props.disallowMove ? 'disallowed' : undefined]"
|
||||
@mousedown="start"
|
||||
@mousemove="move"
|
||||
@mouseup="end"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
addPlaylist,
|
||||
removePlaylist
|
||||
removePlaylist,
|
||||
selectPlaylist
|
||||
} from '@/ts/userPlaylists';
|
||||
import {
|
||||
editingPlaylists,
|
||||
playlistIdx as playlistIdx,
|
||||
playlists
|
||||
} from '@/ts/userPlaylists/state';
|
||||
import {
|
||||
@@ -20,7 +20,6 @@
|
||||
import {
|
||||
UnownedError
|
||||
} from '@/ts/request';
|
||||
import player from '@/ts/player';
|
||||
import router from '@/router';
|
||||
|
||||
const checkingStatus = ref( true );
|
||||
@@ -65,12 +64,6 @@
|
||||
const togglePlaylistEditing = ( idx: number ) => {
|
||||
editingPlaylists.value[ idx ] = !editingPlaylists.value[ idx ];
|
||||
};
|
||||
|
||||
const selectPlaylist = ( idx: number ) => {
|
||||
playlistIdx.value = idx;
|
||||
player.clearQueue();
|
||||
player.loadPlaylist( playlists.value[ idx ]!.songs );
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
ref
|
||||
} from 'vue';
|
||||
import {
|
||||
currentQueue,
|
||||
currentQueueIdx
|
||||
currentQueueIdx,
|
||||
playbackTime,
|
||||
showArtworks
|
||||
} from '@/ts/shared/state';
|
||||
import {
|
||||
computed
|
||||
} from 'vue';
|
||||
|
||||
const songs = computed( () => currentQueue.value?.slice( currentQueueIdx.value + 1 ) ?? [] );
|
||||
// TODO: Move this out of this file
|
||||
const showArtworks = ref( false );
|
||||
const timeToPlay = computed( () => {
|
||||
return ( idx: number ) => {
|
||||
let total = 0;
|
||||
@@ -20,6 +19,7 @@
|
||||
}
|
||||
|
||||
total += currentQueue.value[ currentQueueIdx.value ]?.duration ?? 0;
|
||||
total -= playbackTime.value;
|
||||
|
||||
return Math.round( total / 60 );
|
||||
};
|
||||
@@ -27,24 +27,26 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="queue-container">
|
||||
<div v-for="(song, index) in songs" :key="index" class="song-list-element">
|
||||
<div class="song-cover-wrapper">
|
||||
<img
|
||||
v-if="song.artwork && showArtworks"
|
||||
:src="song.artwork"
|
||||
alt="Song cover"
|
||||
class="song-cover"
|
||||
>
|
||||
<i v-else class="fa-solid fa-music song-cover"></i>
|
||||
</div>
|
||||
<div class="song-details">
|
||||
<h3>{{ song.name }}</h3>
|
||||
<p>{{ song.artist }}</p>
|
||||
<p>{{ song['additional-info'] }}</p>
|
||||
</div>
|
||||
<div class="song-actions">
|
||||
<p>In {{ timeToPlay( index ) }}min</p>
|
||||
<div class="queue-viewer">
|
||||
<div class="queue-container">
|
||||
<div v-for="(song, index) in songs" :key="index" class="song-list-element">
|
||||
<div class="song-cover-wrapper">
|
||||
<img
|
||||
v-if="song.artwork && showArtworks"
|
||||
:src="song.artwork"
|
||||
alt="Song cover"
|
||||
class="song-cover"
|
||||
>
|
||||
<i v-else class="fa-solid fa-music song-cover"></i>
|
||||
</div>
|
||||
<div class="song-details">
|
||||
<h3>{{ song.name }}</h3>
|
||||
<p>{{ song.artist }}</p>
|
||||
<p>{{ song['additional-info'] }}</p>
|
||||
</div>
|
||||
<div class="song-actions">
|
||||
<p>In {{ timeToPlay( index ) }}min</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,5 +33,9 @@
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
&.disallowed {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -4,4 +4,9 @@ import type {
|
||||
|
||||
declare global {
|
||||
var MusicKit: MusicKitObject;
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'musicplayer:playpause': CustomEvent<void>;
|
||||
'musicplayer:playindex': CustomEvent<void>;
|
||||
'musicplayer:seek': CustomEvent<void>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
queueIdx
|
||||
} from '../player/state';
|
||||
import {
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch
|
||||
} from 'vue';
|
||||
@@ -116,7 +118,8 @@ const sendStateData = () => {
|
||||
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
|
||||
'start': new Date().getTime() - 100,
|
||||
'offset': playbackPercentage.value * ( queue.value[ queueIdx.value ]?.duration ?? 0 )
|
||||
} ) );
|
||||
|
||||
setTimeout( () => {
|
||||
@@ -128,10 +131,25 @@ const sendStateData = () => {
|
||||
const useRoomWatchers = () => {
|
||||
watch( queue, sendPlaylistData );
|
||||
|
||||
watch( [
|
||||
isPlaying,
|
||||
queueIdx
|
||||
], sendStateData );
|
||||
onMounted( () => {
|
||||
document.addEventListener( 'musicplayer:playindex', sendStateData );
|
||||
document.addEventListener( 'musicplayer:seek', sendStateData );
|
||||
document.addEventListener( 'musicplayer:playpause', sendStateData );
|
||||
} );
|
||||
|
||||
onUnmounted( () => {
|
||||
try {
|
||||
document.addEventListener( 'musicplayer:play', sendStateData );
|
||||
} catch { /* empty */ }
|
||||
|
||||
try {
|
||||
document.addEventListener( 'musicplayer:seek', sendStateData );
|
||||
} catch { /* empty */ }
|
||||
|
||||
try {
|
||||
document.addEventListener( 'musicplayer:playpause', sendStateData );
|
||||
} catch { /* empty */ }
|
||||
} );
|
||||
|
||||
if ( room.value ) connect();
|
||||
};
|
||||
|
||||
@@ -29,7 +29,6 @@ import type {
|
||||
import {
|
||||
load
|
||||
} from './playlists/loader';
|
||||
import messages from '../messages';
|
||||
import {
|
||||
playIndex
|
||||
} from './playlists';
|
||||
@@ -52,6 +51,7 @@ const play = () => {
|
||||
|
||||
sources[currentSource.value]?.play();
|
||||
startTracking();
|
||||
document.dispatchEvent( new CustomEvent( 'musicplayer:playpause' ) );
|
||||
};
|
||||
|
||||
const pause = () => {
|
||||
@@ -61,6 +61,7 @@ const pause = () => {
|
||||
|
||||
sources[currentSource.value]?.pause();
|
||||
stopTracking();
|
||||
document.dispatchEvent( new CustomEvent( 'musicplayer:playpause' ) );
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -71,6 +72,7 @@ const seekTo = ( pos: number ) => {
|
||||
if ( currentSource.value === '' ) return;
|
||||
|
||||
sources[currentSource.value]?.seekTo( pos );
|
||||
document.dispatchEvent( new CustomEvent( 'musicplayer:seek' ) );
|
||||
};
|
||||
|
||||
const skip10 = () => {
|
||||
@@ -153,8 +155,6 @@ const addSongFromSource = async ( source: string, cb?: ( songs: Song[] ) => void
|
||||
return true;
|
||||
};
|
||||
|
||||
messages.useRoomWatchers();
|
||||
|
||||
export default {
|
||||
play,
|
||||
pause,
|
||||
|
||||
@@ -16,8 +16,8 @@ import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
import {
|
||||
playlistIdx
|
||||
} from '@/ts/userPlaylists/state';
|
||||
setPlaylistIdx
|
||||
} from '@/ts/userPlaylists';
|
||||
|
||||
export const addSongList = ( songs: Song[] ) => {
|
||||
rawQueue.value = rawQueue.value.concat( songs );
|
||||
@@ -25,7 +25,7 @@ export const addSongList = ( songs: Song[] ) => {
|
||||
};
|
||||
|
||||
export const clearQueue = () => {
|
||||
playlistIdx.value = -1;
|
||||
setPlaylistIdx( 0 );
|
||||
queue.value = [];
|
||||
rawQueue.value = [];
|
||||
sources[currentSource.value]?.stop();
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
* Play a song at the given index of the queue. Wraps to 0 and end if index below 0 or above end
|
||||
* @param idx - The index in the queue to play at
|
||||
*/
|
||||
export const playIndex = async ( idx: number ) => {
|
||||
export const playIndex = ( idx: number ) => {
|
||||
if ( idx >= queue.value.length ) {
|
||||
idx = repeat.value === 'all' ? 0 : -1;
|
||||
} else if ( idx < 0 ) {
|
||||
@@ -42,4 +42,7 @@ export const playIndex = async ( idx: number ) => {
|
||||
startTracking();
|
||||
|
||||
isPlaying.value = true;
|
||||
setTimeout( () => {
|
||||
document.dispatchEvent( new CustomEvent( 'musicplayer:playindex', {} ) );
|
||||
}, 500 );
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
currentQueue,
|
||||
currentQueueIdx,
|
||||
isPlaying,
|
||||
playbackOffset,
|
||||
playbackTime,
|
||||
startTime
|
||||
} from './state';
|
||||
import type {
|
||||
@@ -40,6 +42,8 @@ const connect = (): Promise<void> => {
|
||||
currentQueueIdx.value = data.state.index;
|
||||
isPlaying.value = data.state.playing;
|
||||
startTime.value = data.state.start;
|
||||
playbackTime.value = data.state.offset;
|
||||
playbackOffset.value = data.state.offset;
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ import {
|
||||
currentQueue,
|
||||
currentQueueIdx,
|
||||
isPlaying,
|
||||
playbackOffset,
|
||||
playbackTime,
|
||||
startTime
|
||||
} from './state';
|
||||
import type {
|
||||
@@ -17,6 +19,7 @@ export interface StateUpdate {
|
||||
'playing': boolean;
|
||||
'index': number;
|
||||
'start': number;
|
||||
'offset': number;
|
||||
}
|
||||
|
||||
export const messageHandler = ( msg: string ) => {
|
||||
@@ -36,6 +39,8 @@ export const messageHandler = ( msg: string ) => {
|
||||
currentQueueIdx.value = state.index;
|
||||
isPlaying.value = state.playing;
|
||||
startTime.value = state.start;
|
||||
playbackTime.value = state.offset;
|
||||
playbackOffset.value = state.offset;
|
||||
} else {
|
||||
console.log( '[SSE] Received unknown data', data.type );
|
||||
}
|
||||
|
||||
@@ -13,3 +13,9 @@ export const isPlaying = ref( false );
|
||||
export const currentQueueIdx = ref( -1 );
|
||||
|
||||
export const startTime = ref( new Date().getTime() );
|
||||
|
||||
export const showArtworks = ref( false );
|
||||
|
||||
export const playbackTime = ref( 0 );
|
||||
|
||||
export const playbackOffset = ref( 0 );
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {
|
||||
editingPlaylists,
|
||||
playlistIdx,
|
||||
playlists
|
||||
} from './state';
|
||||
import player from '../player';
|
||||
|
||||
|
||||
export const addPlaylist = ( name: string ) => {
|
||||
playlists.value.push( {
|
||||
@@ -17,3 +20,13 @@ export const removePlaylist = ( idx: number ) => {
|
||||
editingPlaylists.value.splice( idx, 1 );
|
||||
}
|
||||
};
|
||||
|
||||
export const selectPlaylist = ( idx: number ) => {
|
||||
playlistIdx.value = idx;
|
||||
player.clearQueue();
|
||||
player.loadPlaylist( playlists.value[ idx ]!.songs );
|
||||
};
|
||||
|
||||
export const setPlaylistIdx = ( idx: number ) => {
|
||||
playlistIdx.value = idx;
|
||||
};
|
||||
|
||||
@@ -16,15 +16,17 @@ import {
|
||||
useNotification
|
||||
} from '@kyvg/vue3-notification';
|
||||
|
||||
export const savePlaylist = async () => {
|
||||
export const savePlaylist = () => {
|
||||
rawQueue.value = queue.value;
|
||||
shuffle.value = false;
|
||||
|
||||
if ( playlistIdx.value ) {
|
||||
if ( playlistIdx.value < 0 ) {
|
||||
let name: null | string = '';
|
||||
|
||||
while ( !name || name.length === 0 ) {
|
||||
name = prompt( 'You are trying to save a playlist that has not previously been created. Please enter a name for it' );
|
||||
|
||||
if ( !name ) return;
|
||||
}
|
||||
|
||||
addPlaylist( name );
|
||||
|
||||
@@ -1,21 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type ComputedRef,
|
||||
computed
|
||||
computed,
|
||||
onMounted,
|
||||
ref
|
||||
} from 'vue';
|
||||
import {
|
||||
currentQueue,
|
||||
currentQueueIdx
|
||||
currentQueueIdx,
|
||||
isPlaying,
|
||||
playbackOffset,
|
||||
playbackTime,
|
||||
startTime
|
||||
} from '@/ts/shared/state';
|
||||
import CurrentSong from '@/components/player/CurrentSong.vue';
|
||||
import ProgressBar from '@/components/player/ProgressBar.vue';
|
||||
import SharedQueue from '@/components/shared/SharedQueue.vue';
|
||||
import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
import {
|
||||
beautifyTime
|
||||
} from '@/ts/util/time';
|
||||
import shared from '@/ts/shared';
|
||||
|
||||
shared.connect();
|
||||
const playbackProgress = ref( 0 );
|
||||
|
||||
onMounted( () => {
|
||||
setInterval( () => {
|
||||
if ( !isPlaying.value ) return;
|
||||
|
||||
playbackTime.value = ( ( new Date().getTime() - startTime.value ) / 1000 ) + playbackOffset.value;
|
||||
playbackProgress.value = playbackTime.value / song.value.duration;
|
||||
|
||||
if ( playbackTime.value > song.value.duration ) {
|
||||
playbackOffset.value -= song.value.duration;
|
||||
|
||||
if ( currentQueueIdx.value < currentQueue.value.length - 1 )
|
||||
currentQueueIdx.value++;
|
||||
else
|
||||
isPlaying.value = false;
|
||||
}
|
||||
}, 250 );
|
||||
} );
|
||||
const song: ComputedRef<Song> = computed( () => {
|
||||
if ( currentQueueIdx.value >= 0 )
|
||||
return currentQueue.value[currentQueueIdx.value]!;
|
||||
@@ -37,6 +65,15 @@
|
||||
<div>
|
||||
<div>
|
||||
<CurrentSong v-model="song" />
|
||||
<div class="time">
|
||||
<p class="current">
|
||||
{{ beautifyTime( playbackTime ) }}
|
||||
</p>
|
||||
<p class="duration">
|
||||
{{ beautifyTime( song.duration ) }}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar v-model="playbackProgress" :disallow-move="true" />
|
||||
</div>
|
||||
<div>
|
||||
<SharedQueue />
|
||||
|
||||
Reference in New Issue
Block a user