+
+ list.innerHTML = this.requests
+ .map((r) => {
+ try {
+ const api = Player.instance.api;
+ const artists = getTrackArtists(r.track);
+ const coverUrl = api.getCoverUrl(r.track.artwork || r.track.cover || r.track.album?.cover);
+ return `
`;
- } catch (e) { return ''; }
- }).join('');
+ } catch (e) {
+ return '';
+ }
+ })
+ .join('');
if (this.isHost) {
const f_id = authManager.user ? authManager.user.$id : 'guest';
- list.querySelectorAll('.add-request-btn').forEach(btn => btn.addEventListener('click', async (e) => {
- const reqId = e.currentTarget.dataset.reqId;
- const req = this.requests.find(r => r.id === reqId);
- if (req) {
- Player.instance.addToQueue(req.track);
- showNotification(`Added "${req.track.title}" to queue`);
- await pb.collection('party_requests').delete(req.id, { f_id });
- }
- }));
+ list.querySelectorAll('.add-request-btn').forEach((btn) =>
+ btn.addEventListener('click', async (e) => {
+ const reqId = e.currentTarget.dataset.reqId;
+ const req = this.requests.find((r) => r.id === reqId);
+ if (req) {
+ Player.instance.addToQueue(req.track);
+ showNotification(`Added "${req.track.title}" to queue`);
+ await pb.collection('party_requests').delete(req.id, { f_id });
+ }
+ })
+ );
}
}
@@ -387,15 +466,17 @@ export class ListeningPartyManager {
if (!container) return;
const div = document.createElement('div');
div.className = 'chat-msg';
-
+
const urlRegex = /(https?:\/\/[^\s]+)/g;
let content = escapeHtml(msg.content);
-
+
content = content.replace(urlRegex, (url) => {
if (url.match(/\.(jpeg|jpg|gif|png|webp|svg)(\?.*)?$/i)) {
return `
`;
}
- const ytMatch = url.match(/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/i);
+ const ytMatch = url.match(
+ /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/i
+ );
if (ytMatch) {
return `
`;
}
@@ -407,7 +488,7 @@ export class ListeningPartyManager {
}
return `
@@ -421,10 +502,15 @@ export class ListeningPartyManager {
async sendChatMessage() {
const input = document.getElementById('party-chat-input');
if (!input || !input.value.trim()) return;
- const content = input.value.trim(); input.value = '';
+ const content = input.value.trim();
+ input.value = '';
const profile = await this.getMemberProfile();
const f_id = authManager.user ? authManager.user.$id : 'guest';
- try { await pb.collection('party_messages').create({ party: this.currentParty.id, sender_name: profile.name, content }, { f_id }); } catch (e) {}
+ try {
+ await pb
+ .collection('party_messages')
+ .create({ party: this.currentParty.id, sender_name: profile.name, content }, { f_id });
+ } catch (e) {}
}
async requestSong(track) {
@@ -433,13 +519,18 @@ export class ListeningPartyManager {
const f_id = authManager.user ? authManager.user.$id : 'guest';
try {
const minifiedTrack = syncManager._minifyItem('track', track);
- await pb.collection('party_requests').create({
- party: this.currentParty.id,
- track: minifiedTrack,
- requested_by: profile.name
- }, { f_id });
+ await pb.collection('party_requests').create(
+ {
+ party: this.currentParty.id,
+ track: minifiedTrack,
+ requested_by: profile.name,
+ },
+ { f_id }
+ );
showNotification(`Requested "${track.title}"`);
- } catch (e) { console.error('Request error:', e); }
+ } catch (e) {
+ console.error('Request error:', e);
+ }
}
async syncWithHost(party) {
@@ -448,18 +539,23 @@ export class ListeningPartyManager {
try {
const player = Player.instance;
const el = player.activeElement;
- if (!party.current_track) { if (player.currentTrack) el.pause(); return; }
-
+ if (!party.current_track) {
+ if (player.currentTrack) el.pause();
+ return;
+ }
+
const currentId = String(player.currentTrack?.id || '');
const targetId = String(party.current_track.id || '');
if (currentId !== targetId) {
const cleanedTrack = { ...party.current_track };
- delete cleanedTrack.audioUrl; delete cleanedTrack.streamUrl; delete cleanedTrack.remoteUrl;
+ delete cleanedTrack.audioUrl;
+ delete cleanedTrack.streamUrl;
+ delete cleanedTrack.remoteUrl;
player.setQueue([cleanedTrack], 0);
await player.playTrackFromQueue(party.playback_time);
if (!party.is_playing) el.pause();
- return;
+ return;
}
if (party.is_playing) {
@@ -473,17 +569,57 @@ export class ListeningPartyManager {
if (!el.paused) el.pause();
if (Math.abs(el.currentTime - party.playback_time) > 0.5) el.currentTime = party.playback_time;
}
- } catch (e) { console.error('Sync error:', e); } finally { this.isInternalSync = false; }
+ } catch (e) {
+ console.error('Sync error:', e);
+ } finally {
+ this.isInternalSync = false;
+ }
}
lockControls() {
- const selectors = ['.play-pause-btn', '#next-btn', '#prev-btn', '#shuffle-btn', '#repeat-btn', '#progress-bar', '#fs-play-pause-btn', '#fs-next-btn', '#fs-prev-btn', '#fs-shuffle-btn', '#fs-repeat-btn', '#fs-progress-bar'];
- selectors.forEach(s => document.querySelectorAll(s).forEach(el => { el.style.opacity = '0.5'; el.style.pointerEvents = 'none'; }));
+ const selectors = [
+ '.play-pause-btn',
+ '#next-btn',
+ '#prev-btn',
+ '#shuffle-btn',
+ '#repeat-btn',
+ '#progress-bar',
+ '#fs-play-pause-btn',
+ '#fs-next-btn',
+ '#fs-prev-btn',
+ '#fs-shuffle-btn',
+ '#fs-repeat-btn',
+ '#fs-progress-bar',
+ ];
+ selectors.forEach((s) =>
+ document.querySelectorAll(s).forEach((el) => {
+ el.style.opacity = '0.5';
+ el.style.pointerEvents = 'none';
+ })
+ );
}
unlockControls() {
- const selectors = ['.play-pause-btn', '#next-btn', '#prev-btn', '#shuffle-btn', '#repeat-btn', '#progress-bar', '#fs-play-pause-btn', '#fs-next-btn', '#fs-prev-btn', '#fs-shuffle-btn', '#fs-repeat-btn', '#fs-progress-bar'];
- selectors.forEach(s => document.querySelectorAll(s).forEach(el => { el.style.opacity = '1'; el.style.pointerEvents = 'auto'; }));
+ const selectors = [
+ '.play-pause-btn',
+ '#next-btn',
+ '#prev-btn',
+ '#shuffle-btn',
+ '#repeat-btn',
+ '#progress-bar',
+ '#fs-play-pause-btn',
+ '#fs-next-btn',
+ '#fs-prev-btn',
+ '#fs-shuffle-btn',
+ '#fs-repeat-btn',
+ '#fs-progress-bar',
+ ];
+ selectors.forEach((s) =>
+ document.querySelectorAll(s).forEach((el) => {
+ el.style.opacity = '1';
+ el.style.pointerEvents = 'auto';
+ })
+ );
}
setupHostPlayerSync() {
@@ -493,16 +629,20 @@ export class ListeningPartyManager {
const el = player.activeElement;
const sharedTrack = player.currentTrack ? syncManager._minifyItem('track', player.currentTrack) : null;
try {
- await pb.collection('parties').update(this.currentParty.id, {
- current_track: sharedTrack,
- is_playing: !el.paused,
- playback_time: el.currentTime,
- playback_timestamp: Date.now(),
- queue: player.queue?.map(t => syncManager._minifyItem('track', t)) || []
- }, { f_id: authManager.user?.$id });
+ await pb.collection('parties').update(
+ this.currentParty.id,
+ {
+ current_track: sharedTrack,
+ is_playing: !el.paused,
+ playback_time: el.currentTime,
+ playback_timestamp: Date.now(),
+ queue: player.queue?.map((t) => syncManager._minifyItem('track', t)) || [],
+ },
+ { f_id: authManager.user?.$id }
+ );
} catch (e) {}
};
- ['play', 'pause', 'seeked'].forEach(ev => {
+ ['play', 'pause', 'seeked'].forEach((ev) => {
player.audio.addEventListener(ev, updateParty);
if (player.video) player.video.addEventListener(ev, updateParty);
});
@@ -520,7 +660,12 @@ export class ListeningPartyManager {
const originalPlayTrackFromQueue = player.playTrackFromQueue.bind(player);
player.playTrackFromQueue = async (...args) => {
if (this.currentParty && !this.isHost && !this.isInternalSync) {
- const leave = await Modal.confirm('Leave Party?', 'Playing a song will cause you to leave the listening party. Are you sure?', 'Leave and Play', 'danger');
+ const leave = await Modal.confirm(
+ 'Leave Party?',
+ 'Playing a song will cause you to leave the listening party. Are you sure?',
+ 'Leave and Play',
+ 'danger'
+ );
if (!leave) return;
this.leaveParty();
}
@@ -531,39 +676,60 @@ export class ListeningPartyManager {
startHeartbeat() {
this.heartbeatInterval = setInterval(async () => {
if (!this.memberId) return;
- try { await pb.collection('party_members').update(this.memberId, { last_seen: Date.now() }, { f_id: authManager.user?.$id || 'guest' }); } catch (e) {}
+ try {
+ await pb
+ .collection('party_members')
+ .update(this.memberId, { last_seen: Date.now() }, { f_id: authManager.user?.$id || 'guest' });
+ } catch (e) {}
}, 30000);
}
async leaveParty(shouldCleanup = true) {
const f_id = authManager.user?.$id || 'guest';
if (this.isHost && shouldCleanup) {
- const end = await Modal.confirm('End Party?', 'Leaving will end the party for everyone. Are you sure?', 'End Party', 'danger');
+ const end = await Modal.confirm(
+ 'End Party?',
+ 'Leaving will end the party for everyone. Are you sure?',
+ 'End Party',
+ 'danger'
+ );
if (!end) return;
try {
const cleanup = async (coll) => {
- const items = await pb.collection(coll).getFullList({ filter: `party = "${this.currentParty.id}"`, f_id });
+ const items = await pb
+ .collection(coll)
+ .getFullList({ filter: `party = "${this.currentParty.id}"`, f_id });
for (const i of items) await pb.collection(coll).delete(i.id, { f_id });
};
- await cleanup('party_members'); await cleanup('party_messages'); await cleanup('party_requests');
+ await cleanup('party_members');
+ await cleanup('party_messages');
+ await cleanup('party_requests');
await pb.collection('parties').delete(this.currentParty.id, { f_id });
} catch (e) {}
} else if (this.memberId) {
- try { await pb.collection('party_members').delete(this.memberId, { f_id }); } catch (e) {}
+ try {
+ await pb.collection('party_members').delete(this.memberId, { f_id });
+ } catch (e) {}
}
this.restorePlayerMethods();
this.unlockControls();
- this.unsubscribeFunctions.forEach(unsub => unsub());
+ this.unsubscribeFunctions.forEach((unsub) => unsub());
this.unsubscribeFunctions = [];
- clearInterval(this.syncInterval); clearInterval(this.heartbeatInterval);
- this.currentParty = null; this.isHost = false; this.memberId = null;
+ clearInterval(this.syncInterval);
+ clearInterval(this.heartbeatInterval);
+ this.currentParty = null;
+ this.isHost = false;
+ this.memberId = null;
document.getElementById('party-indicator')?.remove();
navigate('/parties');
}
restorePlayerMethods() {
const player = Player.instance;
- if (this.originalSafePlay) { player.safePlay = this.originalSafePlay; this.originalSafePlay = null; }
+ if (this.originalSafePlay) {
+ player.safePlay = this.originalSafePlay;
+ this.originalSafePlay = null;
+ }
}
copyInviteLink() {
@@ -580,7 +746,7 @@ export class ListeningPartyManager {
document.body.appendChild(indicator);
indicator.onclick = () => navigate(`/party/${this.currentParty.id}`);
}
-
+
indicator.innerHTML = `
Listening Party
diff --git a/styles.css b/styles.css
index 71ecf74..8399b70 100644
--- a/styles.css
+++ b/styles.css
@@ -9180,8 +9180,14 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
}
@keyframes fadeIn {
- from { opacity: 0; transform: translateY(5px); }
- to { opacity: 1; transform: translateY(0); }
+ from {
+ opacity: 0;
+ transform: translateY(5px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
.party-status-badge {