style: auto-fix linting issues

This commit is contained in:
SamidyFR 2026-04-01 21:02:31 +00:00 committed by github-actions[bot]
parent 2a6c763176
commit b00cb086f4
3 changed files with 360 additions and 142 deletions

View file

@ -1832,16 +1832,24 @@
<div style="text-align: center; padding: 4rem 2rem; max-width: 600px; margin: 0 auto">
<h2 class="section-title">Listening Parties</h2>
<p style="color: var(--muted-foreground); margin-bottom: 2rem">
Listen to music together with your friends in real-time.
Host controls the music, everyone enjoys it.
Listen to music together with your friends in real-time. Host controls the music, everyone
enjoys it.
</p>
<div id="parties-auth-required" style="display: none">
<p style="margin-bottom: 1rem">You need an account to host a listening party.</p>
<button id="parties-login-btn" class="btn-primary">Sign In / Sign Up</button>
</div>
<div id="parties-host-controls" style="display: none">
<input type="text" id="party-name-input" class="template-input" placeholder="Party Name" style="margin-bottom: 1rem">
<button id="create-party-btn" class="btn-primary" style="width: 100%">Create Listening Party</button>
<input
type="text"
id="party-name-input"
class="template-input"
placeholder="Party Name"
style="margin-bottom: 1rem"
/>
<button id="create-party-btn" class="btn-primary" style="width: 100%">
Create Listening Party
</button>
</div>
</div>
</div>
@ -1860,37 +1868,75 @@
</div>
</header>
<div class="party-content-layout" style="display: grid; grid-template-columns: 1fr 350px; gap: 2rem; margin-top: 2rem">
<div
class="party-content-layout"
style="display: grid; grid-template-columns: 1fr 350px; gap: 2rem; margin-top: 2rem"
>
<div class="party-main-section">
<section class="content-section">
<h2 class="section-title">Currently Playing</h2>
<div id="party-current-track-display">
</div>
<div id="party-current-track-display"></div>
</section>
<section class="content-section">
<h2 class="section-title">Participants (<span id="party-member-count">0</span>)</h2>
<div id="party-members-list" class="card-grid compact">
</div>
<div id="party-members-list" class="card-grid compact"></div>
</section>
<section class="content-section" id="party-requests-section">
<h2 class="section-title">Song Requests</h2>
<div id="party-requests-list" class="track-list">
</div>
<div id="party-requests-list" class="track-list"></div>
</section>
</div>
<div class="party-sidebar-section">
<div class="party-chat-container" style="display: flex; flex-direction: column; height: 600px; background: var(--background-secondary); border-radius: var(--radius); border: 1px solid var(--border)">
<div class="chat-header" style="padding: 1rem; border-bottom: 1px solid var(--border); font-weight: 600">
<div
class="party-chat-container"
style="
display: flex;
flex-direction: column;
height: 600px;
background: var(--background-secondary);
border-radius: var(--radius);
border: 1px solid var(--border);
"
>
<div
class="chat-header"
style="padding: 1rem; border-bottom: 1px solid var(--border); font-weight: 600"
>
Chat
</div>
<div id="party-chat-messages" style="flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem">
</div>
<div class="chat-input-container" style="padding: 1rem; border-top: 1px solid var(--border); display: flex; gap: 0.5rem">
<input type="text" id="party-chat-input" class="template-input" placeholder="Say something..." style="flex: 1">
<button id="party-chat-send-btn" class="btn-primary" style="padding: 0 1rem">Send</button>
<div
id="party-chat-messages"
style="
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
"
></div>
<div
class="chat-input-container"
style="
padding: 1rem;
border-top: 1px solid var(--border);
display: flex;
gap: 0.5rem;
"
>
<input
type="text"
id="party-chat-input"
class="template-input"
placeholder="Say something..."
style="flex: 1"
/>
<button id="party-chat-send-btn" class="btn-primary" style="padding: 0 1rem">
Send
</button>
</div>
</div>
</div>

View file

@ -19,20 +19,24 @@ class Modal {
<h3 style="margin-bottom: 1rem; font-size: 1.5rem;">${title}</h3>
<div class="modal-body" style="margin-bottom: 2rem; color: var(--muted-foreground); line-height: 1.5;">${content}</div>
<div class="modal-actions" style="display: flex; flex-direction: column; gap: 0.75rem;">
${actions.map((a, i) => `
${actions
.map(
(a, i) => `
<button class="btn-${a.type || 'secondary'} modal-action-btn" data-index="${i}" style="width: 100%; padding: 0.8rem; font-weight: 600;">${a.label}</button>
`).join('')}
`
)
.join('')}
</div>
</div>
`;
document.body.appendChild(modal);
const cleanup = (val) => {
modal.remove();
resolve(val);
};
modal.querySelectorAll('.modal-action-btn').forEach(btn => {
modal.querySelectorAll('.modal-action-btn').forEach((btn) => {
btn.onclick = () => {
const action = actions[btn.dataset.index];
if (action.callback) {
@ -52,7 +56,7 @@ class Modal {
return this.show({
title,
content: message,
actions: [{ label: 'OK', type: 'primary' }]
actions: [{ label: 'OK', type: 'primary' }],
});
}
@ -62,8 +66,8 @@ class Modal {
content: message,
actions: [
{ label: confirmLabel, type: type },
{ label: 'Cancel', type: 'secondary', callback: () => false }
]
{ label: 'Cancel', type: 'secondary', callback: () => false },
],
});
}
}
@ -82,7 +86,7 @@ export class ListeningPartyManager {
this.isJoining = false;
this.isInternalSync = false;
this.originalSafePlay = null;
this.setupEventListeners();
}
@ -119,25 +123,27 @@ export class ListeningPartyManager {
is_playing: player.currentTrack ? !player.activeElement.paused : false,
playback_time: player.activeElement.currentTime || 0,
playback_timestamp: Date.now(),
queue: player.queue?.map(t => syncManager._minifyItem('track', t)) || []
queue: player.queue?.map((t) => syncManager._minifyItem('track', t)) || [],
};
if (currentTrack) partyData.current_track = currentTrack;
try {
const party = await pb.collection('parties').create(partyData, { f_id: user.$id });
navigate(`/party/${party.id}`);
} catch (e) { console.error('Create error:', e); }
} catch (e) {
console.error('Create error:', e);
}
}
async joinParty(partyId) {
if (this.currentParty?.id === partyId || this.isJoining) return;
this.isJoining = true;
try {
const user = authManager.user;
const f_id = user ? user.$id : 'guest';
const party = await pb.collection('parties').getOne(partyId, { expand: 'host', f_id });
const confirmed = await this.showJoinModal(user);
if (!confirmed) {
this.isJoining = false;
@ -148,14 +154,20 @@ export class ListeningPartyManager {
this.currentParty = party;
const pbUser = user ? await syncManager._getUserRecord(user.$id) : null;
this.isHost = pbUser && pbUser.id === party.host;
const profile = confirmed.profile || await this.getMemberProfile(pbUser);
const memberData = { party: partyId, name: profile.name, avatar_url: profile.avatar_url, is_host: !!this.isHost, last_seen: Date.now() };
const profile = confirmed.profile || (await this.getMemberProfile(pbUser));
const memberData = {
party: partyId,
name: profile.name,
avatar_url: profile.avatar_url,
is_host: !!this.isHost,
last_seen: Date.now(),
};
if (pbUser?.id) memberData.user = pbUser.id;
const member = await pb.collection('party_members').create(memberData, { f_id });
this.memberId = member.id;
this.setupSubscriptions(partyId);
this.startHeartbeat();
this.renderPartyUI();
@ -169,12 +181,12 @@ export class ListeningPartyManager {
this.syncWithHost(party);
}
}
} catch (error) {
console.error('Join error:', error);
} catch (error) {
console.error('Join error:', error);
Modal.alert('Error', 'Failed to join the party. It may have ended.');
navigate('/parties');
} finally {
this.isJoining = false;
navigate('/parties');
} finally {
this.isJoining = false;
}
}
@ -198,18 +210,21 @@ export class ListeningPartyManager {
<input type="text" id="guest-name-input" class="template-input" value="${defaultName}" placeholder="Your nickname" style="width: 100%; text-align: center;">
`,
actions: [
{
label: 'Join Party',
{
label: 'Join Party',
type: 'primary',
callback: (modal) => {
const name = modal.querySelector('#guest-name-input').value.trim() || 'Guest';
const profile = { name, avatar_url: `https://api.dicebear.com/9.x/identicon/svg?seed=${name}` };
const profile = {
name,
avatar_url: `https://api.dicebear.com/9.x/identicon/svg?seed=${name}`,
};
localStorage.setItem('party_guest_profile', JSON.stringify(profile));
return { profile };
}
},
},
{ label: 'Cancel', type: 'secondary', callback: () => false }
]
{ label: 'Cancel', type: 'secondary', callback: () => false },
],
}).then(resolve);
});
}
@ -227,57 +242,96 @@ export class ListeningPartyManager {
async getMemberProfile(pbUser = null) {
const user = authManager.user;
if (user) {
const name = pbUser?.display_name || pbUser?.username || user.displayName || user.email?.split('@')[0] || 'Member';
const avatar = pbUser?.avatar_url || user.photoURL || `https://api.dicebear.com/9.x/identicon/svg?seed=${name}`;
const name =
pbUser?.display_name || pbUser?.username || user.displayName || user.email?.split('@')[0] || 'Member';
const avatar =
pbUser?.avatar_url || user.photoURL || `https://api.dicebear.com/9.x/identicon/svg?seed=${name}`;
return { name, avatar_url: avatar };
}
const cached = localStorage.getItem('party_guest_profile');
return cached ? JSON.parse(cached) : { name: 'Guest', avatar_url: 'https://api.dicebear.com/9.x/identicon/svg?seed=Guest' };
return cached
? JSON.parse(cached)
: { name: 'Guest', avatar_url: 'https://api.dicebear.com/9.x/identicon/svg?seed=Guest' };
}
setupSubscriptions(partyId) {
this.unsubscribeFunctions.forEach(unsub => unsub());
this.unsubscribeFunctions.forEach((unsub) => unsub());
this.unsubscribeFunctions = [];
const f_id = authManager.user ? authManager.user.$id : 'guest';
pb.collection('parties').subscribe(partyId, (e) => {
if (e.action === 'update') {
this.currentParty = e.record;
if (!this.isHost) this.syncWithHost(e.record);
this.updatePartyHeader();
} else if (e.action === 'delete') {
Modal.alert('Party Ended', 'The host has ended the listening party.');
this.leaveParty(false);
}
}, { f_id }).then(unsub => this.unsubscribeFunctions.push(unsub));
pb.collection('parties')
.subscribe(
partyId,
(e) => {
if (e.action === 'update') {
this.currentParty = e.record;
if (!this.isHost) this.syncWithHost(e.record);
this.updatePartyHeader();
} else if (e.action === 'delete') {
Modal.alert('Party Ended', 'The host has ended the listening party.');
this.leaveParty(false);
}
},
{ f_id }
)
.then((unsub) => this.unsubscribeFunctions.push(unsub));
pb.collection('party_members').subscribe('*', (e) => {
if (e.record.party === partyId) this.loadMembers();
}, { f_id }).then(unsub => this.unsubscribeFunctions.push(unsub));
pb.collection('party_members')
.subscribe(
'*',
(e) => {
if (e.record.party === partyId) this.loadMembers();
},
{ f_id }
)
.then((unsub) => this.unsubscribeFunctions.push(unsub));
pb.collection('party_messages').subscribe('*', (e) => {
if (e.record.party === partyId && e.action === 'create') this.addChatMessage(e.record);
}, { f_id }).then(unsub => this.unsubscribeFunctions.push(unsub));
pb.collection('party_messages')
.subscribe(
'*',
(e) => {
if (e.record.party === partyId && e.action === 'create') this.addChatMessage(e.record);
},
{ f_id }
)
.then((unsub) => this.unsubscribeFunctions.push(unsub));
pb.collection('party_requests').subscribe('*', (e) => {
if (e.record.party === partyId) this.loadRequests();
}, { f_id }).then(unsub => this.unsubscribeFunctions.push(unsub));
pb.collection('party_requests')
.subscribe(
'*',
(e) => {
if (e.record.party === partyId) this.loadRequests();
},
{ f_id }
)
.then((unsub) => this.unsubscribeFunctions.push(unsub));
}
async loadInitialData(partyId) { this.loadMembers(); this.loadMessages(); this.loadRequests(); }
async loadInitialData(partyId) {
this.loadMembers();
this.loadMessages();
this.loadRequests();
}
async loadMembers() {
const f_id = authManager.user ? authManager.user.$id : 'guest';
this.members = await pb.collection('party_members').getFullList({ filter: `party = "${this.currentParty.id}"`, sort: '-is_host,name', f_id });
this.members = await pb
.collection('party_members')
.getFullList({ filter: `party = "${this.currentParty.id}"`, sort: '-is_host,name', f_id });
this.renderMembers();
}
async loadMessages() {
const f_id = authManager.user ? authManager.user.$id : 'guest';
const res = await pb.collection('party_messages').getList(1, 50, { filter: `party = "${this.currentParty.id}"`, sort: '-created', f_id });
const res = await pb
.collection('party_messages')
.getList(1, 50, { filter: `party = "${this.currentParty.id}"`, sort: '-created', f_id });
this.messages = res.items.reverse();
const container = document.getElementById('party-chat-messages');
if (container) { container.innerHTML = ''; this.messages.forEach(m => this.addChatMessage(m)); }
if (container) {
container.innerHTML = '';
this.messages.forEach((m) => this.addChatMessage(m));
}
}
async loadRequests() {
@ -286,26 +340,36 @@ export class ListeningPartyManager {
this.requests = await pb.collection('party_requests').getFullList({
filter: `party = "${this.currentParty.id}"`,
sort: 'created',
f_id: f_id
f_id: f_id,
});
this.renderRequests();
} catch (e) { console.error('Failed to load requests:', e); }
} catch (e) {
console.error('Failed to load requests:', e);
}
}
renderPartyUI() {
this.updatePartyHeader(); this.renderMembers(); this.renderRequests(); this.showPartyIndicator();
if (this.isHost) { this.unlockControls(); this.setupHostPlayerSync(); }
else { this.lockControls(); this.setupGuestPlayerInterferenceCheck(); }
this.updatePartyHeader();
this.renderMembers();
this.renderRequests();
this.showPartyIndicator();
if (this.isHost) {
this.unlockControls();
this.setupHostPlayerSync();
} else {
this.lockControls();
this.setupGuestPlayerInterferenceCheck();
}
}
updatePartyHeader() {
const titleEl = document.getElementById('party-title');
const countEl = document.getElementById('party-member-count');
const metaEl = document.getElementById('party-meta');
if (titleEl) titleEl.textContent = this.currentParty.name;
if (countEl) countEl.textContent = this.members.length;
if (metaEl) {
const host = this.currentParty.expand?.host;
const hostName = host?.display_name || host?.username || 'Unknown';
@ -325,11 +389,15 @@ export class ListeningPartyManager {
<div class="track-title" style="font-size: 1.8rem; font-weight: 700; margin-bottom: 0.5rem">${track.title}</div>
<div class="track-artist" style="font-size: 1.2rem; color: var(--muted-foreground)">${getTrackArtists(track)}</div>
</div>
${!this.currentParty.is_playing ? `
${
!this.currentParty.is_playing
? `
<div style="display: flex; align-items: center; gap: 0.5rem; color: var(--primary); font-weight: 600; text-transform: uppercase; letter-spacing: 1px; font-size: 0.9rem">
${SVG_PAUSE(24)} Paused
</div>
` : ''}
`
: ''
}
</div>
`;
} else {
@ -341,7 +409,12 @@ export class ListeningPartyManager {
renderMembers() {
const list = document.getElementById('party-members-list');
if (!list) return;
list.innerHTML = this.members.map(m => `<div class="member-item" style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; background: var(--background-secondary); border-radius: var(--radius); border: 1px solid var(--border)"><img src="${m.avatar_url}" style="width: 40px; height: 40px; border-radius: 50%; background: var(--background-modifier-accent)"><div style="flex: 1; overflow: hidden"><div style="font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">${m.name}</div>${m.is_host ? '<div style="color: var(--primary); font-size: 0.7rem; font-weight: bold; text-transform: uppercase;">Host</div>' : '<div style="color: var(--muted-foreground); font-size: 0.7rem">Listening</div>'}</div></div>`).join('');
list.innerHTML = this.members
.map(
(m) =>
`<div class="member-item" style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; background: var(--background-secondary); border-radius: var(--radius); border: 1px solid var(--border)"><img src="${m.avatar_url}" style="width: 40px; height: 40px; border-radius: 50%; background: var(--background-modifier-accent)"><div style="flex: 1; overflow: hidden"><div style="font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">${m.name}</div>${m.is_host ? '<div style="color: var(--primary); font-size: 0.7rem; font-weight: bold; text-transform: uppercase;">Host</div>' : '<div style="color: var(--muted-foreground); font-size: 0.7rem">Listening</div>'}</div></div>`
)
.join('');
}
renderRequests() {
@ -351,13 +424,14 @@ export class ListeningPartyManager {
list.innerHTML = `<div style="padding: 2rem; text-align: center; color: var(--muted-foreground); font-size: 0.9rem">No requests yet. Right-click a song to request!</div>`;
return;
}
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 `<div class="track-item" style="display: flex; align-items: center; gap: 1rem; padding: 0.75rem; border-bottom: 1px solid var(--border)">
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 `<div class="track-item" style="display: flex; align-items: center; gap: 1rem; padding: 0.75rem; border-bottom: 1px solid var(--border)">
<img src="${coverUrl}" style="width: 48px; height: 48px; border-radius: 4px; object-fit: cover; flex-shrink: 0;">
<div class="track-info" style="flex: 1; min-width: 0;">
<div class="track-title" style="font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${r.track.title || 'Unknown Title'}</div>
@ -365,20 +439,25 @@ export class ListeningPartyManager {
</div>
${this.isHost ? `<button class="btn-primary btn-sm add-request-btn" data-req-id="${r.id}" style="padding: 0.4rem 1rem; font-size: 0.8rem; flex-shrink: 0; white-space: nowrap;">Add to Queue</button>` : ''}
</div>`;
} 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 `<a href="${url}" target="_blank" class="chat-link">${url}</a><img src="${url}" style="max-width: 100%; border-radius: 8px; margin-top: 8px; display: block; cursor: pointer" onclick="window.open('${url}')">`;
}
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 `<a href="${url}" target="_blank" class="chat-link">${url}</a><iframe style="width: 100%; aspect-ratio: 16/9; border-radius: 8px; margin-top: 8px; border: none" src="https://www.youtube.com/embed/${ytMatch[1]}" allowfullscreen></iframe>`;
}
@ -407,7 +488,7 @@ export class ListeningPartyManager {
}
return `<a href="${url}" target="_blank" class="chat-link" style="color: var(--primary); text-decoration: underline;">${url}</a>`;
});
div.innerHTML = `
<div style="font-weight: 600; font-size: 0.75rem; color: var(--primary); margin-bottom: 2px">${escapeHtml(msg.sender_name)}</div>
<div style="background: var(--background-modifier-accent); padding: 0.6rem 0.8rem; border-radius: 0.75rem; display: inline-block; max-width: 100%; word-break: break-word; font-size: 0.9rem; line-height: 1.4">
@ -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 = `
<div class="party-indicator-content">
<span class="party-indicator-label">Listening Party</span>

View file

@ -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 {