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

View file

@ -19,9 +19,13 @@ class Modal {
<h3 style="margin-bottom: 1rem; font-size: 1.5rem;">${title}</h3> <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-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;"> <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> <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>
</div> </div>
`; `;
@ -32,7 +36,7 @@ class Modal {
resolve(val); resolve(val);
}; };
modal.querySelectorAll('.modal-action-btn').forEach(btn => { modal.querySelectorAll('.modal-action-btn').forEach((btn) => {
btn.onclick = () => { btn.onclick = () => {
const action = actions[btn.dataset.index]; const action = actions[btn.dataset.index];
if (action.callback) { if (action.callback) {
@ -52,7 +56,7 @@ class Modal {
return this.show({ return this.show({
title, title,
content: message, content: message,
actions: [{ label: 'OK', type: 'primary' }] actions: [{ label: 'OK', type: 'primary' }],
}); });
} }
@ -62,8 +66,8 @@ class Modal {
content: message, content: message,
actions: [ actions: [
{ label: confirmLabel, type: type }, { label: confirmLabel, type: type },
{ label: 'Cancel', type: 'secondary', callback: () => false } { label: 'Cancel', type: 'secondary', callback: () => false },
] ],
}); });
} }
} }
@ -119,14 +123,16 @@ export class ListeningPartyManager {
is_playing: player.currentTrack ? !player.activeElement.paused : false, is_playing: player.currentTrack ? !player.activeElement.paused : false,
playback_time: player.activeElement.currentTime || 0, playback_time: player.activeElement.currentTime || 0,
playback_timestamp: Date.now(), 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; if (currentTrack) partyData.current_track = currentTrack;
try { try {
const party = await pb.collection('parties').create(partyData, { f_id: user.$id }); const party = await pb.collection('parties').create(partyData, { f_id: user.$id });
navigate(`/party/${party.id}`); navigate(`/party/${party.id}`);
} catch (e) { console.error('Create error:', e); } } catch (e) {
console.error('Create error:', e);
}
} }
async joinParty(partyId) { async joinParty(partyId) {
@ -149,8 +155,14 @@ export class ListeningPartyManager {
const pbUser = user ? await syncManager._getUserRecord(user.$id) : null; const pbUser = user ? await syncManager._getUserRecord(user.$id) : null;
this.isHost = pbUser && pbUser.id === party.host; this.isHost = pbUser && pbUser.id === party.host;
const profile = confirmed.profile || await this.getMemberProfile(pbUser); 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 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; if (pbUser?.id) memberData.user = pbUser.id;
const member = await pb.collection('party_members').create(memberData, { f_id }); const member = await pb.collection('party_members').create(memberData, { f_id });
@ -203,13 +215,16 @@ export class ListeningPartyManager {
type: 'primary', type: 'primary',
callback: (modal) => { callback: (modal) => {
const name = modal.querySelector('#guest-name-input').value.trim() || 'Guest'; 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)); localStorage.setItem('party_guest_profile', JSON.stringify(profile));
return { profile }; return { profile };
} },
}, },
{ label: 'Cancel', type: 'secondary', callback: () => false } { label: 'Cancel', type: 'secondary', callback: () => false },
] ],
}).then(resolve); }).then(resolve);
}); });
} }
@ -227,57 +242,96 @@ export class ListeningPartyManager {
async getMemberProfile(pbUser = null) { async getMemberProfile(pbUser = null) {
const user = authManager.user; const user = authManager.user;
if (user) { if (user) {
const name = pbUser?.display_name || pbUser?.username || user.displayName || user.email?.split('@')[0] || 'Member'; const name =
const avatar = pbUser?.avatar_url || user.photoURL || `https://api.dicebear.com/9.x/identicon/svg?seed=${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 }; return { name, avatar_url: avatar };
} }
const cached = localStorage.getItem('party_guest_profile'); 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) { setupSubscriptions(partyId) {
this.unsubscribeFunctions.forEach(unsub => unsub()); this.unsubscribeFunctions.forEach((unsub) => unsub());
this.unsubscribeFunctions = []; this.unsubscribeFunctions = [];
const f_id = authManager.user ? authManager.user.$id : 'guest'; const f_id = authManager.user ? authManager.user.$id : 'guest';
pb.collection('parties').subscribe(partyId, (e) => { pb.collection('parties')
if (e.action === 'update') { .subscribe(
this.currentParty = e.record; partyId,
if (!this.isHost) this.syncWithHost(e.record); (e) => {
this.updatePartyHeader(); if (e.action === 'update') {
} else if (e.action === 'delete') { this.currentParty = e.record;
Modal.alert('Party Ended', 'The host has ended the listening party.'); if (!this.isHost) this.syncWithHost(e.record);
this.leaveParty(false); this.updatePartyHeader();
} } else if (e.action === 'delete') {
}, { f_id }).then(unsub => this.unsubscribeFunctions.push(unsub)); 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) => { pb.collection('party_members')
if (e.record.party === partyId) this.loadMembers(); .subscribe(
}, { f_id }).then(unsub => this.unsubscribeFunctions.push(unsub)); '*',
(e) => {
if (e.record.party === partyId) this.loadMembers();
},
{ f_id }
)
.then((unsub) => this.unsubscribeFunctions.push(unsub));
pb.collection('party_messages').subscribe('*', (e) => { pb.collection('party_messages')
if (e.record.party === partyId && e.action === 'create') this.addChatMessage(e.record); .subscribe(
}, { f_id }).then(unsub => this.unsubscribeFunctions.push(unsub)); '*',
(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) => { pb.collection('party_requests')
if (e.record.party === partyId) this.loadRequests(); .subscribe(
}, { f_id }).then(unsub => this.unsubscribeFunctions.push(unsub)); '*',
(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() { async loadMembers() {
const f_id = authManager.user ? authManager.user.$id : 'guest'; 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(); this.renderMembers();
} }
async loadMessages() { async loadMessages() {
const f_id = authManager.user ? authManager.user.$id : 'guest'; 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(); this.messages = res.items.reverse();
const container = document.getElementById('party-chat-messages'); 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() { async loadRequests() {
@ -286,16 +340,26 @@ export class ListeningPartyManager {
this.requests = await pb.collection('party_requests').getFullList({ this.requests = await pb.collection('party_requests').getFullList({
filter: `party = "${this.currentParty.id}"`, filter: `party = "${this.currentParty.id}"`,
sort: 'created', sort: 'created',
f_id: f_id f_id: f_id,
}); });
this.renderRequests(); this.renderRequests();
} catch (e) { console.error('Failed to load requests:', e); } } catch (e) {
console.error('Failed to load requests:', e);
}
} }
renderPartyUI() { renderPartyUI() {
this.updatePartyHeader(); this.renderMembers(); this.renderRequests(); this.showPartyIndicator(); this.updatePartyHeader();
if (this.isHost) { this.unlockControls(); this.setupHostPlayerSync(); } this.renderMembers();
else { this.lockControls(); this.setupGuestPlayerInterferenceCheck(); } this.renderRequests();
this.showPartyIndicator();
if (this.isHost) {
this.unlockControls();
this.setupHostPlayerSync();
} else {
this.lockControls();
this.setupGuestPlayerInterferenceCheck();
}
} }
updatePartyHeader() { updatePartyHeader() {
@ -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-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 class="track-artist" style="font-size: 1.2rem; color: var(--muted-foreground)">${getTrackArtists(track)}</div>
</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"> <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 ${SVG_PAUSE(24)} Paused
</div> </div>
` : ''} `
: ''
}
</div> </div>
`; `;
} else { } else {
@ -341,7 +409,12 @@ export class ListeningPartyManager {
renderMembers() { renderMembers() {
const list = document.getElementById('party-members-list'); const list = document.getElementById('party-members-list');
if (!list) return; 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() { renderRequests() {
@ -352,12 +425,13 @@ export class ListeningPartyManager {
return; return;
} }
list.innerHTML = this.requests.map(r => { list.innerHTML = this.requests
try { .map((r) => {
const api = Player.instance.api; try {
const artists = getTrackArtists(r.track); const api = Player.instance.api;
const coverUrl = api.getCoverUrl(r.track.artwork || r.track.cover || r.track.album?.cover); const artists = getTrackArtists(r.track);
return `<div class="track-item" style="display: flex; align-items: center; gap: 1rem; padding: 0.75rem; border-bottom: 1px solid var(--border)"> 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;"> <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-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> <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> </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>` : ''} ${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>`; </div>`;
} catch (e) { return ''; } } catch (e) {
}).join(''); return '';
}
})
.join('');
if (this.isHost) { if (this.isHost) {
const f_id = authManager.user ? authManager.user.$id : 'guest'; const f_id = authManager.user ? authManager.user.$id : 'guest';
list.querySelectorAll('.add-request-btn').forEach(btn => btn.addEventListener('click', async (e) => { list.querySelectorAll('.add-request-btn').forEach((btn) =>
const reqId = e.currentTarget.dataset.reqId; btn.addEventListener('click', async (e) => {
const req = this.requests.find(r => r.id === reqId); const reqId = e.currentTarget.dataset.reqId;
if (req) { const req = this.requests.find((r) => r.id === reqId);
Player.instance.addToQueue(req.track); if (req) {
showNotification(`Added "${req.track.title}" to queue`); Player.instance.addToQueue(req.track);
await pb.collection('party_requests').delete(req.id, { f_id }); showNotification(`Added "${req.track.title}" to queue`);
} await pb.collection('party_requests').delete(req.id, { f_id });
})); }
})
);
} }
} }
@ -395,7 +474,9 @@ export class ListeningPartyManager {
if (url.match(/\.(jpeg|jpg|gif|png|webp|svg)(\?.*)?$/i)) { 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}')">`; 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) { 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>`; 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>`;
} }
@ -421,10 +502,15 @@ export class ListeningPartyManager {
async sendChatMessage() { async sendChatMessage() {
const input = document.getElementById('party-chat-input'); const input = document.getElementById('party-chat-input');
if (!input || !input.value.trim()) return; 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 profile = await this.getMemberProfile();
const f_id = authManager.user ? authManager.user.$id : 'guest'; 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) { async requestSong(track) {
@ -433,13 +519,18 @@ export class ListeningPartyManager {
const f_id = authManager.user ? authManager.user.$id : 'guest'; const f_id = authManager.user ? authManager.user.$id : 'guest';
try { try {
const minifiedTrack = syncManager._minifyItem('track', track); const minifiedTrack = syncManager._minifyItem('track', track);
await pb.collection('party_requests').create({ await pb.collection('party_requests').create(
party: this.currentParty.id, {
track: minifiedTrack, party: this.currentParty.id,
requested_by: profile.name track: minifiedTrack,
}, { f_id }); requested_by: profile.name,
},
{ f_id }
);
showNotification(`Requested "${track.title}"`); showNotification(`Requested "${track.title}"`);
} catch (e) { console.error('Request error:', e); } } catch (e) {
console.error('Request error:', e);
}
} }
async syncWithHost(party) { async syncWithHost(party) {
@ -448,14 +539,19 @@ export class ListeningPartyManager {
try { try {
const player = Player.instance; const player = Player.instance;
const el = player.activeElement; 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 currentId = String(player.currentTrack?.id || '');
const targetId = String(party.current_track.id || ''); const targetId = String(party.current_track.id || '');
if (currentId !== targetId) { if (currentId !== targetId) {
const cleanedTrack = { ...party.current_track }; 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); player.setQueue([cleanedTrack], 0);
await player.playTrackFromQueue(party.playback_time); await player.playTrackFromQueue(party.playback_time);
if (!party.is_playing) el.pause(); if (!party.is_playing) el.pause();
@ -473,17 +569,57 @@ export class ListeningPartyManager {
if (!el.paused) el.pause(); if (!el.paused) el.pause();
if (Math.abs(el.currentTime - party.playback_time) > 0.5) el.currentTime = party.playback_time; 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() { 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']; const selectors = [
selectors.forEach(s => document.querySelectorAll(s).forEach(el => { el.style.opacity = '0.5'; el.style.pointerEvents = 'none'; })); '.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() { 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']; const selectors = [
selectors.forEach(s => document.querySelectorAll(s).forEach(el => { el.style.opacity = '1'; el.style.pointerEvents = 'auto'; })); '.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() { setupHostPlayerSync() {
@ -493,16 +629,20 @@ export class ListeningPartyManager {
const el = player.activeElement; const el = player.activeElement;
const sharedTrack = player.currentTrack ? syncManager._minifyItem('track', player.currentTrack) : null; const sharedTrack = player.currentTrack ? syncManager._minifyItem('track', player.currentTrack) : null;
try { try {
await pb.collection('parties').update(this.currentParty.id, { await pb.collection('parties').update(
current_track: sharedTrack, this.currentParty.id,
is_playing: !el.paused, {
playback_time: el.currentTime, current_track: sharedTrack,
playback_timestamp: Date.now(), is_playing: !el.paused,
queue: player.queue?.map(t => syncManager._minifyItem('track', t)) || [] playback_time: el.currentTime,
}, { f_id: authManager.user?.$id }); playback_timestamp: Date.now(),
queue: player.queue?.map((t) => syncManager._minifyItem('track', t)) || [],
},
{ f_id: authManager.user?.$id }
);
} catch (e) {} } catch (e) {}
}; };
['play', 'pause', 'seeked'].forEach(ev => { ['play', 'pause', 'seeked'].forEach((ev) => {
player.audio.addEventListener(ev, updateParty); player.audio.addEventListener(ev, updateParty);
if (player.video) player.video.addEventListener(ev, updateParty); if (player.video) player.video.addEventListener(ev, updateParty);
}); });
@ -520,7 +660,12 @@ export class ListeningPartyManager {
const originalPlayTrackFromQueue = player.playTrackFromQueue.bind(player); const originalPlayTrackFromQueue = player.playTrackFromQueue.bind(player);
player.playTrackFromQueue = async (...args) => { player.playTrackFromQueue = async (...args) => {
if (this.currentParty && !this.isHost && !this.isInternalSync) { 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; if (!leave) return;
this.leaveParty(); this.leaveParty();
} }
@ -531,39 +676,60 @@ export class ListeningPartyManager {
startHeartbeat() { startHeartbeat() {
this.heartbeatInterval = setInterval(async () => { this.heartbeatInterval = setInterval(async () => {
if (!this.memberId) return; 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); }, 30000);
} }
async leaveParty(shouldCleanup = true) { async leaveParty(shouldCleanup = true) {
const f_id = authManager.user?.$id || 'guest'; const f_id = authManager.user?.$id || 'guest';
if (this.isHost && shouldCleanup) { 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; if (!end) return;
try { try {
const cleanup = async (coll) => { 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 }); 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 }); await pb.collection('parties').delete(this.currentParty.id, { f_id });
} catch (e) {} } catch (e) {}
} else if (this.memberId) { } 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.restorePlayerMethods();
this.unlockControls(); this.unlockControls();
this.unsubscribeFunctions.forEach(unsub => unsub()); this.unsubscribeFunctions.forEach((unsub) => unsub());
this.unsubscribeFunctions = []; this.unsubscribeFunctions = [];
clearInterval(this.syncInterval); clearInterval(this.heartbeatInterval); clearInterval(this.syncInterval);
this.currentParty = null; this.isHost = false; this.memberId = null; clearInterval(this.heartbeatInterval);
this.currentParty = null;
this.isHost = false;
this.memberId = null;
document.getElementById('party-indicator')?.remove(); document.getElementById('party-indicator')?.remove();
navigate('/parties'); navigate('/parties');
} }
restorePlayerMethods() { restorePlayerMethods() {
const player = Player.instance; 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() { copyInviteLink() {

View file

@ -9180,8 +9180,14 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); } from {
to { opacity: 1; transform: translateY(0); } opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.party-status-badge { .party-status-badge {