added public watch parties with cloudflared
This commit is contained in:
@@ -24,6 +24,13 @@ class CreateRoomModal {
|
||||
<label>Password (Optional)</label>
|
||||
<input type="password" class="cr-input" name="password" placeholder="Leave empty for public" maxlength="50" />
|
||||
</div>
|
||||
|
||||
<div class="cr-form-group cr-checkbox-group">
|
||||
<label class="cr-checkbox">
|
||||
<input type="checkbox" name="expose" />
|
||||
<span>Generate public link (via tunnel)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="cr-actions">
|
||||
<button type="button" class="cr-btn-cancel" id="cr-cancel">Cancel</button>
|
||||
@@ -87,6 +94,7 @@ class CreateRoomModal {
|
||||
btn.textContent = 'Creating...';
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const expose = formData.get('expose') === 'on';
|
||||
const name = formData.get('name').trim();
|
||||
const password = formData.get('password').trim();
|
||||
const token = localStorage.getItem('token');
|
||||
@@ -100,7 +108,8 @@ class CreateRoomModal {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
password: password || undefined
|
||||
password: password || undefined,
|
||||
expose
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ const RoomsApp = (function() {
|
||||
let configState = {
|
||||
extension: null,
|
||||
server: null,
|
||||
category: 'sub', // 'sub' o 'dub'
|
||||
category: 'sub',
|
||||
episode: 1
|
||||
};
|
||||
|
||||
let extensionsStore = {
|
||||
list: [],
|
||||
settings: {} // { anilist: {...}, gogo: {...} }
|
||||
settings: {}
|
||||
};
|
||||
|
||||
const elements = {
|
||||
@@ -27,7 +27,6 @@ const RoomsApp = (function() {
|
||||
roomView: document.getElementById('room-view'),
|
||||
roomName: document.getElementById('room-name'),
|
||||
roomViewers: document.getElementById('room-viewers'),
|
||||
leaveRoomBtn: document.getElementById('leave-room-btn'),
|
||||
selectAnimeBtn: document.getElementById('select-anime-btn'),
|
||||
toggleChatBtn: document.getElementById('toggle-chat-btn'),
|
||||
|
||||
@@ -59,7 +58,7 @@ const RoomsApp = (function() {
|
||||
chatMessages: document.getElementById('chat-messages'),
|
||||
chatForm: document.getElementById('chat-form'),
|
||||
chatInput: document.getElementById('chat-input'),
|
||||
roomLayout: document.getElementById('room-layout'), // Corregido: referencia al layout
|
||||
roomLayout: document.getElementById('room-layout'),
|
||||
|
||||
// Modals
|
||||
joinRoomModal: document.getElementById('join-room-modal'),
|
||||
@@ -113,7 +112,6 @@ const RoomsApp = (function() {
|
||||
setupEventListeners();
|
||||
await preloadExtensions();
|
||||
|
||||
// --- NUEVO: Obtener info de la sala primero ---
|
||||
try {
|
||||
const res = await fetch(`/api/rooms/${currentRoomId}`);
|
||||
if (!res.ok) throw new Error('Room not found');
|
||||
@@ -122,41 +120,39 @@ const RoomsApp = (function() {
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Room not found or deleted");
|
||||
window.location.href = '/anime';
|
||||
}
|
||||
}
|
||||
|
||||
// --- NUEVO: Función para manejar la entrada lógica ---
|
||||
function handleInitialEntry(roomInfo) {
|
||||
const token = localStorage.getItem('token');
|
||||
const passwordGroup = document.getElementById('password-group');
|
||||
|
||||
// Configurar UI del Modal con datos del Host
|
||||
const hostInfoDiv = document.getElementById('join-host-info');
|
||||
const hostAvatar = document.getElementById('join-host-avatar');
|
||||
const hostText = document.getElementById('join-host-text');
|
||||
|
||||
if (hostInfoDiv && roomInfo.host) {
|
||||
hostInfoDiv.style.display = 'flex';
|
||||
// Usar avatar del host o un placeholder
|
||||
hostAvatar.src = roomInfo.host.avatar || '/public/assets/placeholder.png';
|
||||
hostText.innerHTML = `<span>${escapeHtml(roomInfo.host.username)}</span> invited you to watch`;
|
||||
}
|
||||
|
||||
// Configurar si pide contraseña
|
||||
if (passwordGroup) {
|
||||
// Si la sala tiene pass, mostramos el campo
|
||||
passwordGroup.style.display = roomInfo.hasPassword ? 'block' : 'none';
|
||||
// Marcar en un atributo dataset si es requerida para validación
|
||||
passwordGroup.dataset.required = roomInfo.hasPassword ? 'true' : 'false';
|
||||
}
|
||||
|
||||
window.__roomPublicUrl = roomInfo.publicUrl || null;
|
||||
window.__roomExposed = roomInfo.exposed || false;
|
||||
|
||||
console.log('Room info loaded:', {
|
||||
exposed: window.__roomExposed,
|
||||
publicUrl: window.__roomPublicUrl
|
||||
});
|
||||
|
||||
if (token) {
|
||||
// Si tiene token, intentamos conectar directamente.
|
||||
// Si hay pass y no somos el host/dueño, el socket fallará y pedirá pass luego.
|
||||
connectToRoom(currentRoomId);
|
||||
} else {
|
||||
// Es Guest: Mostrar modal directamente
|
||||
console.log('Guest user, showing modal...');
|
||||
if (elements.joinRoomModal) {
|
||||
elements.joinRoomModal.classList.add('show');
|
||||
@@ -186,39 +182,32 @@ const RoomsApp = (function() {
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
// Join Room Form
|
||||
const cancelJoinBtn = document.getElementById('cancel-join-btn');
|
||||
if (cancelJoinBtn) cancelJoinBtn.onclick = leaveRoom;
|
||||
if (elements.joinRoomForm) elements.joinRoomForm.onsubmit = submitJoinForm;
|
||||
|
||||
// Header Controls
|
||||
if (elements.selectAnimeBtn) elements.selectAnimeBtn.onclick = openAnimeSearchModal;
|
||||
if (elements.toggleChatBtn) elements.toggleChatBtn.onclick = toggleChat;
|
||||
if (elements.leaveRoomBtn) elements.leaveRoomBtn.onclick = leaveRoom;
|
||||
|
||||
// Host Quick Controls Listeners
|
||||
if (elements.roomExtSelect) elements.roomExtSelect.onchange = (e) => onQuickExtensionChange(e, false);
|
||||
if (elements.roomServerSelect) elements.roomServerSelect.onchange = onQuickServerChange;
|
||||
|
||||
// Sub/Dub Toggle Logic (Header)
|
||||
if (elements.roomSdToggle) {
|
||||
elements.roomSdToggle.onclick = () => {
|
||||
if (!isHost) return;
|
||||
const currentState = elements.roomSdToggle.getAttribute('data-state');
|
||||
const newState = currentState === 'sub' ? 'dub' : 'sub';
|
||||
|
||||
// Update UI visually immediately
|
||||
elements.roomSdToggle.setAttribute('data-state', newState);
|
||||
elements.roomSdToggle.querySelectorAll('.sd-option').forEach(opt => {
|
||||
opt.classList.toggle('active', opt.dataset.val === newState);
|
||||
});
|
||||
|
||||
// Trigger Stream Reload
|
||||
onQuickServerChange();
|
||||
};
|
||||
}
|
||||
|
||||
// Anime Search Modal
|
||||
const closeSearchBtn = document.getElementById('close-search-modal');
|
||||
const animeSearchBtn = document.getElementById('anime-search-btn');
|
||||
|
||||
@@ -234,7 +223,6 @@ const RoomsApp = (function() {
|
||||
};
|
||||
}
|
||||
|
||||
// Config Step (Modal)
|
||||
if (elements.backToSearchBtn) {
|
||||
elements.backToSearchBtn.onclick = () => {
|
||||
elements.stepConfig.style.display = 'none';
|
||||
@@ -244,13 +232,10 @@ const RoomsApp = (function() {
|
||||
if (elements.selExtension) elements.selExtension.onchange = handleModalExtensionChange;
|
||||
if (elements.btnLaunch) elements.btnLaunch.onclick = () => launchStream(true); // true = from modal
|
||||
|
||||
// Room & User List
|
||||
if (elements.toggleUsersBtn) elements.toggleUsersBtn.onclick = toggleUsersList;
|
||||
|
||||
// Chat
|
||||
if (elements.chatForm) elements.chatForm.onsubmit = sendMessage;
|
||||
|
||||
// Anime results delegation
|
||||
if (elements.animeResults) {
|
||||
elements.animeResults.addEventListener('click', handleAnimeResultClick);
|
||||
}
|
||||
@@ -271,11 +256,8 @@ const RoomsApp = (function() {
|
||||
elements.roomExtSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
// 🔥 FORZAR valor real
|
||||
const extToUse = selectedAnimeData.source || extensionsStore.list[0];
|
||||
elements.roomExtSelect.value = extToUse;
|
||||
elements.roomExtSelect.value = selectedAnimeData.source || extensionsStore.list[0];
|
||||
|
||||
// 🔥 FORZAR carga de servers
|
||||
await onQuickExtensionChange(null, true);
|
||||
}
|
||||
|
||||
@@ -299,7 +281,6 @@ const RoomsApp = (function() {
|
||||
elements.roomServerSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
// 🔥 FORZAR server seleccionado
|
||||
elements.roomServerSelect.value = servers[0];
|
||||
|
||||
handleDubUI(settings, 'header');
|
||||
@@ -348,8 +329,6 @@ const RoomsApp = (function() {
|
||||
launchStream(false);
|
||||
}
|
||||
|
||||
// --- MODAL LOGIC ---
|
||||
|
||||
function handleAnimeResultClick(e) {
|
||||
const itemLink = e.target.closest('.search-item, .anime-result-item, a[href*="/anime/"]');
|
||||
|
||||
@@ -363,7 +342,7 @@ const RoomsApp = (function() {
|
||||
const imgEl = itemLink.querySelector('.search-poster, img');
|
||||
|
||||
title = titleEl ? titleEl.textContent : (itemLink.textContent.trim() || 'Unknown');
|
||||
img = imgEl ? (imgEl.src || imgEl.dataset.src || '/public/assets/placeholder.png') : '/public/assets/placeholder.png';
|
||||
img = imgEl ? (imgEl.src || imgEl.dataset.src || '/public/assets/placeholder.svg') : '/public/assets/placeholder.svg';
|
||||
|
||||
const href = itemLink.getAttribute('href') || '';
|
||||
const hrefParts = href.split('/').filter(p => p);
|
||||
@@ -386,36 +365,28 @@ const RoomsApp = (function() {
|
||||
if (!selectedAnimeData) return;
|
||||
if (!extensionsReady) return;
|
||||
|
||||
// 1. Resetear UI básica
|
||||
elements.configTitle.textContent = selectedAnimeData.title;
|
||||
elements.configCover.src = selectedAnimeData.image;
|
||||
if(ui.configError) ui.configError.style.display = 'none';
|
||||
|
||||
// 2. Resetear Estado interno
|
||||
configState.episode = 1;
|
||||
configState.server = null;
|
||||
configState.category = 'sub'; // Reset a sub por defecto
|
||||
configState.extension = null; // Reset extensión
|
||||
configState.category = 'sub';
|
||||
configState.extension = null;
|
||||
|
||||
// 3. Resetear controles visuales
|
||||
if(ui.epInput) ui.epInput.value = 1;
|
||||
if(ui.launchBtn) ui.launchBtn.disabled = true;
|
||||
updateSDUI(); // Función visual para el toggle sub/dub
|
||||
updateSDUI();
|
||||
|
||||
// 4. Configurar listeners de botones +/- y toggle
|
||||
setupConfigListeners();
|
||||
|
||||
// 5. Renderizar los botones de extensiones
|
||||
renderExtensionChips();
|
||||
|
||||
// Mostrar pantalla
|
||||
elements.stepSearch.style.display = 'none';
|
||||
elements.stepConfig.style.display = 'block';
|
||||
}
|
||||
|
||||
// Configura los botones + / - y el toggle Sub/Dub
|
||||
function setupConfigListeners() {
|
||||
// Episode Stepper
|
||||
if(ui.epInc) ui.epInc.onclick = () => {
|
||||
ui.epInput.value = parseInt(ui.epInput.value || 0) + 1;
|
||||
configState.episode = ui.epInput.value;
|
||||
@@ -426,14 +397,12 @@ const RoomsApp = (function() {
|
||||
};
|
||||
if(ui.epInput) ui.epInput.onchange = (e) => configState.episode = e.target.value;
|
||||
|
||||
// Sub/Dub Toggle
|
||||
if(ui.sdToggle) {
|
||||
ui.sdToggle.querySelectorAll('.cat-opt').forEach(opt => {
|
||||
opt.onclick = () => {
|
||||
if(opt.classList.contains('disabled')) return;
|
||||
configState.category = opt.dataset.val;
|
||||
updateSDUI();
|
||||
// Al cambiar categoría, recargar servidores (quizás cambien los disponibles)
|
||||
if(configState.extension) loadServersForExtension(configState.extension);
|
||||
};
|
||||
});
|
||||
@@ -447,7 +416,6 @@ const RoomsApp = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
// Dibuja los botones de Extensiones
|
||||
function renderExtensionChips() {
|
||||
ui.extContainer.innerHTML = '';
|
||||
|
||||
@@ -461,30 +429,25 @@ const RoomsApp = (function() {
|
||||
chip.className = 'chip';
|
||||
chip.textContent = ext.charAt(0).toUpperCase() + ext.slice(1);
|
||||
|
||||
// Auto-seleccionar si ya estaba en el estado (o default a anilist)
|
||||
if (!configState.extension && ext === 'anilist') configState.extension = 'anilist';
|
||||
if (ext === configState.extension) chip.classList.add('active');
|
||||
|
||||
chip.onclick = () => {
|
||||
// Actualizar visual
|
||||
document.querySelectorAll('#ext-chips-container .chip').forEach(c => c.classList.remove('active'));
|
||||
chip.classList.add('active');
|
||||
|
||||
// Actualizar lógica
|
||||
configState.extension = ext;
|
||||
configState.server = null; // Reset servidor al cambiar extensión
|
||||
ui.launchBtn.disabled = true; // Deshabilitar Play hasta elegir server
|
||||
configState.server = null;
|
||||
ui.launchBtn.disabled = true;
|
||||
|
||||
loadServersForExtension(ext);
|
||||
};
|
||||
ui.extContainer.appendChild(chip);
|
||||
});
|
||||
|
||||
// Cargar servidores de la extensión inicial
|
||||
if(configState.extension) loadServersForExtension(configState.extension);
|
||||
}
|
||||
|
||||
// Carga los servidores de la API (Asíncrono)
|
||||
async function loadServersForExtension(extName) {
|
||||
if (!extensionsReady) return;
|
||||
ui.serverContainer.innerHTML = '<div class="grid-loader"><div class="spinner" style="width:20px;height:20px;"></div> Loading servers...</div>';
|
||||
@@ -500,7 +463,6 @@ const RoomsApp = (function() {
|
||||
|
||||
renderServerChips(servers);
|
||||
|
||||
// Manejar si la extensión soporta Dub
|
||||
const dubBtn = ui.sdToggle.querySelector('[data-val="dub"]');
|
||||
if (dubBtn) {
|
||||
if (!settings.supportsDub) {
|
||||
@@ -520,7 +482,6 @@ const RoomsApp = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja los botones de Servidores
|
||||
function renderServerChips(servers) {
|
||||
ui.serverContainer.innerHTML = '';
|
||||
|
||||
@@ -534,7 +495,6 @@ const RoomsApp = (function() {
|
||||
chip.classList.add('active');
|
||||
|
||||
configState.server = srv;
|
||||
// AHORA sí habilitamos el botón de Play
|
||||
ui.launchBtn.disabled = false;
|
||||
};
|
||||
|
||||
@@ -597,19 +557,16 @@ const RoomsApp = (function() {
|
||||
episode = configState.episode;
|
||||
category = configState.category;
|
||||
} else {
|
||||
// LÓGICA DEL HEADER (Corregida)
|
||||
ext = elements.roomExtSelect.value;
|
||||
server = elements.roomServerSelect.value;
|
||||
|
||||
// Intentar leer episodio del texto
|
||||
let epText = elements.npEpisode.textContent.replace('Ep ', '').trim();
|
||||
|
||||
// Fallback robusto: Si dice "--" o está vacío, usar los datos guardados o 1
|
||||
if (!epText || epText === '--' || isNaN(epText)) {
|
||||
if (selectedAnimeData.episode) {
|
||||
epText = selectedAnimeData.episode;
|
||||
} else {
|
||||
epText = "1"; // Default absoluto
|
||||
epText = "1";
|
||||
}
|
||||
}
|
||||
episode = epText;
|
||||
@@ -617,7 +574,6 @@ const RoomsApp = (function() {
|
||||
category = elements.roomSdToggle.getAttribute('data-state');
|
||||
}
|
||||
|
||||
// Validación
|
||||
if (!ext || !server || !episode) {
|
||||
console.error("Missing params:", { ext, server, episode });
|
||||
if(fromModal) {
|
||||
@@ -629,7 +585,6 @@ const RoomsApp = (function() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Feedback UI
|
||||
if(fromModal) {
|
||||
elements.btnLaunch.disabled = true;
|
||||
elements.btnLaunch.innerHTML = '<div class="spinner" style="width:20px;height:20px;"></div> Fetching...';
|
||||
@@ -645,7 +600,6 @@ const RoomsApp = (function() {
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Lógica de fuentes (igual que antes)
|
||||
const source = data.videoSources?.find(s => s.type === 'm3u8') || data.videoSources?.[0];
|
||||
if (!source) throw new Error('No video source found');
|
||||
|
||||
@@ -673,7 +627,7 @@ const RoomsApp = (function() {
|
||||
},
|
||||
metadata: {
|
||||
title: selectedAnimeData.title,
|
||||
episode: episode, // Usar el episodio corregido
|
||||
episode: episode,
|
||||
image: selectedAnimeData.image,
|
||||
id: selectedAnimeData.id
|
||||
}
|
||||
@@ -682,32 +636,21 @@ const RoomsApp = (function() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(videoPayload));
|
||||
|
||||
// Carga local
|
||||
loadVideo(videoPayload.video);
|
||||
updateHeaderInfo(videoPayload.metadata);
|
||||
|
||||
// Si venimos del Modal, sincronizamos los controles rápidos del header
|
||||
if(fromModal) {
|
||||
closeAnimeSearchModal();
|
||||
|
||||
// --- CORRECCIÓN INICIO ---
|
||||
|
||||
// 1. Actualizamos el source en la data global para que coincida con lo que acabas de elegir
|
||||
selectedAnimeData.source = ext;
|
||||
|
||||
// 2. Forzamos el repoblado del dropdown del header (ahora que tenemos anime y extensión)
|
||||
await populateQuickControls();
|
||||
|
||||
// --- CORRECCIÓN FIN ---
|
||||
|
||||
// Sincronizar UI del header
|
||||
if (extensionsStore.list.includes(ext)) {
|
||||
elements.roomExtSelect.value = ext;
|
||||
// Forzamos carga silenciosa para llenar los servidores en el select del header
|
||||
await onQuickExtensionChange(null, true);
|
||||
elements.roomServerSelect.value = server;
|
||||
|
||||
// Sincronizar toggle Dub/Sub
|
||||
elements.roomSdToggle.setAttribute('data-state', category);
|
||||
elements.roomSdToggle.querySelectorAll('.sd-option').forEach(o =>
|
||||
o.classList.toggle('active', o.dataset.val === category)
|
||||
@@ -737,48 +680,165 @@ const RoomsApp = (function() {
|
||||
|
||||
function connectToRoom(roomId, guestName, password) {
|
||||
const token = localStorage.getItem('token');
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/room/${roomId}`;
|
||||
|
||||
const isTunnel = window.location.hostname.includes('trycloudflare.com');
|
||||
|
||||
let wsUrl;
|
||||
|
||||
if (isTunnel) {
|
||||
wsUrl = `wss://${window.location.host}/ws/room/${roomId}`;
|
||||
console.log('[Tunnel Mode] Using secure WebSocket:', wsUrl);
|
||||
} else {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
wsUrl = `${protocol}//${window.location.host}/ws/room/${roomId}`;
|
||||
console.log('[Local Mode] Using WebSocket:', wsUrl);
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (token) params.append('token', token);
|
||||
if (guestName) params.append('guestName', guestName);
|
||||
if (password) params.append('password', password);
|
||||
|
||||
if (ws) ws.close();
|
||||
if (ws) {
|
||||
console.log('Closing existing WebSocket...');
|
||||
ws.close();
|
||||
}
|
||||
|
||||
console.log('Connecting to:', `${wsUrl}?${params.toString()}`);
|
||||
ws = new WebSocket(`${wsUrl}?${params.toString()}`);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket Connected');
|
||||
console.log('✓ WebSocket Connected');
|
||||
if (window.AnimePlayer && typeof window.AnimePlayer.setWebSocket === 'function') {
|
||||
window.AnimePlayer.setWebSocket(ws);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
console.log('Requesting sync from host...');
|
||||
ws.send(JSON.stringify({ type: 'request_sync' }));
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
ws.onmessage = (e) => handleWebSocketMessage(JSON.parse(e.data));
|
||||
ws.onerror = (e) => console.error('WebSocket error:', e);
|
||||
ws.onclose = () => {
|
||||
console.log('Disconnected');
|
||||
|
||||
ws.onerror = (e) => {
|
||||
console.error('✗ WebSocket error:', e);
|
||||
showConnectionError('WebSocket connection failed. Check your connection.');
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('WebSocket Disconnected:', event.code, event.reason);
|
||||
|
||||
if (window.AnimePlayer && typeof window.AnimePlayer.setWebSocket === 'function') {
|
||||
window.AnimePlayer.setWebSocket(null);
|
||||
}
|
||||
|
||||
if (event.code !== 1000 && event.code !== 1001) {
|
||||
console.log('Attempting reconnection in 3 seconds...');
|
||||
showReconnectingToast();
|
||||
setTimeout(() => {
|
||||
if (currentRoomId) {
|
||||
console.log('Reconnecting to room...');
|
||||
connectToRoom(currentRoomId, guestName, password);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function showReconnectingToast() {
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'reconnecting-toast';
|
||||
toast.className = 'connection-error-toast';
|
||||
toast.innerHTML = `
|
||||
<div style="display:flex; align-items:center; gap:10px;">
|
||||
<div class="spinner" style="width:16px; height:16px; border-width:2px;"></div>
|
||||
<span>Reconnecting...</span>
|
||||
</div>
|
||||
`;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(251, 191, 36, 0.95);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
`;
|
||||
|
||||
const existing = document.getElementById('reconnecting-toast');
|
||||
if (existing) existing.remove();
|
||||
|
||||
document.body.appendChild(toast);
|
||||
}
|
||||
|
||||
function showConnectionError(message) {
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'connection-error-toast';
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.style.cssText = `
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(239, 68, 68, 0.95);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
`;
|
||||
|
||||
document.body.appendChild(errorDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
errorDiv.style.animation = 'fadeOut 0.3s ease-out';
|
||||
setTimeout(() => errorDiv.remove(), 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function handleWebSocketMessage(data) {
|
||||
switch (data.type) {
|
||||
case 'error':
|
||||
handleConnectionError(data.message);
|
||||
break;
|
||||
|
||||
case 'init':
|
||||
const reconnectToast = document.getElementById('reconnecting-toast');
|
||||
if (reconnectToast) reconnectToast.remove();
|
||||
|
||||
elements.joinRoomModal.classList.remove('show');
|
||||
currentUserId = data.userId;
|
||||
currentUsername = data.username;
|
||||
isGuest = data.isGuest;
|
||||
updateRoomUI(data.room);
|
||||
if (data.room.currentVideo && data.room.metadata) {
|
||||
updateHeaderInfo(data.room.metadata);
|
||||
|
||||
if (data.room.currentVideo) {
|
||||
loadVideo(data.room.currentVideo);
|
||||
|
||||
if (data.room.metadata) {
|
||||
updateHeaderInfo(data.room.metadata);
|
||||
}
|
||||
|
||||
if (!isHost) {
|
||||
console.log('Video detected on join, requesting sync...');
|
||||
setTimeout(() => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'request_sync' }));
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -791,29 +851,27 @@ const RoomsApp = (function() {
|
||||
updateUsersList();
|
||||
|
||||
if (isHost) {
|
||||
sendSync();
|
||||
console.log('New user joined, sending sync...');
|
||||
setTimeout(() => sendSync(), 500);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'user_left':
|
||||
addSystemMessage(`${data.user.username} left`);
|
||||
updateUsersList();
|
||||
break;
|
||||
|
||||
case 'chat':
|
||||
addChatMessage(data); // Siempre añadir al historial del chat lateral
|
||||
|
||||
// Comprobar si el chat está oculto
|
||||
addChatMessage(data);
|
||||
const isChatHidden = elements.roomLayout.classList.contains('chat-hidden');
|
||||
|
||||
if (isChatHidden) {
|
||||
// 1. Mostrar Toast sobre el video
|
||||
showChatToast(data);
|
||||
|
||||
// 2. Poner punto rojo en el botón
|
||||
if (elements.toggleChatBtn) {
|
||||
elements.toggleChatBtn.classList.add('has-unread');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'video_update':
|
||||
loadVideo(data.video);
|
||||
if (data.metadata) {
|
||||
@@ -823,25 +881,53 @@ const RoomsApp = (function() {
|
||||
};
|
||||
updateHeaderInfo(data.metadata);
|
||||
}
|
||||
|
||||
if (!isHost) {
|
||||
setTimeout(() => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
console.log('New video loaded, requesting sync...');
|
||||
ws.send(JSON.stringify({ type: 'request_sync' }));
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sync':
|
||||
console.log('Received sync:', data.currentTime, data.isPlaying ? 'playing' : 'paused');
|
||||
syncVideo(data.currentTime, data.isPlaying);
|
||||
updatePlayPauseButton(data.isPlaying);
|
||||
break;
|
||||
|
||||
case 'play':
|
||||
const vP = getVideoElement();
|
||||
if(vP) { vP.currentTime = data.currentTime; vP.play().catch(console.error); updatePlayPauseButton(true); }
|
||||
if(vP) {
|
||||
vP.currentTime = data.currentTime;
|
||||
vP.play().catch(console.error);
|
||||
updatePlayPauseButton(true);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'pause':
|
||||
const vPa = getVideoElement();
|
||||
if(vPa) { vPa.currentTime = data.currentTime; vPa.pause(); updatePlayPauseButton(false); }
|
||||
if(vPa) {
|
||||
vPa.currentTime = data.currentTime;
|
||||
vPa.pause();
|
||||
updatePlayPauseButton(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'seek':
|
||||
const vS = getVideoElement();
|
||||
if(vS) { vS.currentTime = data.currentTime; }
|
||||
if(vS) {
|
||||
vS.currentTime = data.currentTime;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sync_requested':
|
||||
if (isHost) sendSync();
|
||||
if (isHost) {
|
||||
console.log('Sync requested, sending current state...');
|
||||
sendSync();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -853,7 +939,6 @@ const RoomsApp = (function() {
|
||||
const currentUser = room.users.find(u => u.id === currentUserId);
|
||||
isHost = currentUser?.isHost || false;
|
||||
|
||||
// Mostrar controles solo si es Host
|
||||
if (elements.selectAnimeBtn) elements.selectAnimeBtn.style.display = isHost ? 'flex' : 'none';
|
||||
if (elements.hostControls) elements.hostControls.style.display = isHost ? 'flex' : 'none';
|
||||
|
||||
@@ -861,7 +946,49 @@ const RoomsApp = (function() {
|
||||
window.AnimePlayer.setRoomHost(isHost);
|
||||
}
|
||||
|
||||
// Si somos host y tenemos metadatos, poblar los controles del header
|
||||
const copyInviteBtn = document.getElementById('copy-invite-btn');
|
||||
|
||||
if (copyInviteBtn) {
|
||||
let inviteUrl = null;
|
||||
|
||||
if (window.__roomExposed && window.__roomPublicUrl) {
|
||||
inviteUrl = window.__roomPublicUrl;
|
||||
} else {
|
||||
inviteUrl = `${window.location.origin}/room?id=${room.id}`;
|
||||
}
|
||||
|
||||
console.log('Copy button configured with URL:', inviteUrl);
|
||||
|
||||
copyInviteBtn.style.display = 'inline-flex';
|
||||
copyInviteBtn.title = window.__roomExposed
|
||||
? 'Copy public invite link (works outside your network)'
|
||||
: 'Copy local invite link (only works on your network)';
|
||||
|
||||
copyInviteBtn.onclick = async () => {
|
||||
try {
|
||||
console.log('Copying to clipboard:', inviteUrl);
|
||||
await navigator.clipboard.writeText(inviteUrl);
|
||||
|
||||
const originalHTML = copyInviteBtn.innerHTML;
|
||||
copyInviteBtn.innerHTML = `
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
`;
|
||||
copyInviteBtn.style.color = '#4ade80';
|
||||
|
||||
setTimeout(() => {
|
||||
copyInviteBtn.innerHTML = originalHTML;
|
||||
copyInviteBtn.style.color = '';
|
||||
}, 2000);
|
||||
|
||||
showCopyToast(window.__roomExposed ? 'Public link copied!' : 'Local link copied!');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (isHost && room.metadata) {
|
||||
if(!selectedAnimeData) selectedAnimeData = { ...room.metadata, source: 'anilist' };
|
||||
populateQuickControls();
|
||||
@@ -871,18 +998,44 @@ const RoomsApp = (function() {
|
||||
if (room.currentVideo) loadVideo(room.currentVideo);
|
||||
}
|
||||
|
||||
function showCopyToast(message) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'copy-toast';
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(74, 222, 128, 0.95);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
animation: slideUp 0.3s ease-out;
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'fadeOut 0.3s ease-out';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function updateHeaderInfo(meta) {
|
||||
if (!meta) return;
|
||||
if (elements.npTitle) elements.npTitle.textContent = meta.title;
|
||||
if (elements.npEpisode) elements.npEpisode.textContent = `Episode ${meta.episode}`;
|
||||
if (elements.npInfo) elements.npInfo.style.opacity = '1';
|
||||
|
||||
// Save data locally so we can use quick controls
|
||||
if(!selectedAnimeData) selectedAnimeData = { ...meta, source: 'anilist' };
|
||||
else {
|
||||
selectedAnimeData.id = meta.id;
|
||||
selectedAnimeData.title = meta.title;
|
||||
// Episode is tracked visually in header
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,7 +1054,6 @@ const RoomsApp = (function() {
|
||||
if(!token) document.getElementById('guest-name-input').focus();
|
||||
} else {
|
||||
alert(message);
|
||||
window.location.href = '/anime';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -911,13 +1063,11 @@ const RoomsApp = (function() {
|
||||
const password = document.getElementById('join-password-input').value.trim();
|
||||
const passwordGroup = document.getElementById('password-group');
|
||||
|
||||
// Validar Nombre para Guest
|
||||
if (!guestName && !localStorage.getItem('token')) {
|
||||
alert("Please enter a name");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar Password si es requerida y está visible
|
||||
if (passwordGroup.style.display !== 'none' && !password) {
|
||||
alert("This room requires a password");
|
||||
return;
|
||||
@@ -926,17 +1076,13 @@ const RoomsApp = (function() {
|
||||
connectToRoom(currentRoomId, guestName, password);
|
||||
}
|
||||
|
||||
// room.js - Reemplazar toggleChat
|
||||
|
||||
function toggleChat() {
|
||||
if (elements.roomLayout) {
|
||||
elements.roomLayout.classList.toggle('chat-hidden');
|
||||
|
||||
// Si acabamos de ABRIR el chat (ya no tiene la clase chat-hidden)
|
||||
if (!elements.roomLayout.classList.contains('chat-hidden')) {
|
||||
// Quitar notificación roja
|
||||
elements.toggleChatBtn.classList.remove('has-unread');
|
||||
// Opcional: Limpiar los toasts flotantes para que no estorben
|
||||
if(elements.toastContainer) elements.toastContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
@@ -947,12 +1093,10 @@ const RoomsApp = (function() {
|
||||
function showChatToast(data) {
|
||||
if (!elements.toastContainer) return;
|
||||
|
||||
// Crear elemento
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'video-toast';
|
||||
|
||||
// Avatar (usar el mismo fallback que el chat)
|
||||
const avatarSrc = data.avatar || '/public/assets/placeholder.png'; // Asegúrate de tener un placeholder o lógica de iniciales
|
||||
const avatarSrc = data.avatar || '/public/assets/placeholder.png';
|
||||
|
||||
toast.innerHTML = `
|
||||
<img src="${avatarSrc}" class="toast-avatar" onerror="this.style.display='none'">
|
||||
@@ -962,17 +1106,14 @@ const RoomsApp = (function() {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Añadir al contenedor
|
||||
elements.toastContainer.appendChild(toast);
|
||||
|
||||
// Eliminar del DOM después de que termine la animación (5s total: 0.3s in + 4.2s wait + 0.5s out)
|
||||
setTimeout(() => {
|
||||
if (toast.parentElement) {
|
||||
toast.remove();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// Limitar número de toasts (máximo 3 al mismo tiempo para no tapar todo el video)
|
||||
while (elements.toastContainer.children.length > 3) {
|
||||
elements.toastContainer.removeChild(elements.toastContainer.firstChild);
|
||||
}
|
||||
@@ -998,14 +1139,12 @@ const RoomsApp = (function() {
|
||||
}
|
||||
|
||||
function addSystemMessage(text) {
|
||||
// 1. Agregar al chat (siempre)
|
||||
const div = document.createElement('div');
|
||||
div.className = 'chat-message system';
|
||||
div.innerHTML = `<div class="message-content">${escapeHtml(text)}</div>`;
|
||||
elements.chatMessages.appendChild(div);
|
||||
elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight;
|
||||
|
||||
// 2. Si el chat está oculto, mostrar notificación flotante
|
||||
if (elements.roomLayout && elements.roomLayout.classList.contains('chat-hidden')) {
|
||||
showSystemToast(text);
|
||||
}
|
||||
@@ -1015,12 +1154,11 @@ const RoomsApp = (function() {
|
||||
if (!elements.toastContainer) return;
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'video-toast system-toast'; // Clase especial para diferenciarlo
|
||||
toast.className = 'video-toast system-toast';
|
||||
toast.innerHTML = `<span class="toast-msg">${escapeHtml(text)}</span>`;
|
||||
|
||||
elements.toastContainer.appendChild(toast);
|
||||
|
||||
// Eliminar a los 4 segundos
|
||||
setTimeout(() => toast.remove(), 4000);
|
||||
}
|
||||
|
||||
@@ -1029,13 +1167,10 @@ const RoomsApp = (function() {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'chat-message';
|
||||
|
||||
// LÓGICA DE AVATAR ACTUALIZADA
|
||||
let avatarHtml;
|
||||
if (data.avatar) {
|
||||
// Si tiene imagen, usamos img tag
|
||||
avatarHtml = `<img src="${data.avatar}" alt="${data.username}" style="width:100%; height:100%; object-fit:cover; border-radius:50%;">`;
|
||||
} else {
|
||||
// Fallback a inicial
|
||||
avatarHtml = data.username[0].toUpperCase();
|
||||
}
|
||||
|
||||
@@ -1093,7 +1228,6 @@ const RoomsApp = (function() {
|
||||
return document.getElementById('player');
|
||||
}
|
||||
|
||||
// Fallback simple video loader
|
||||
function loadVideoBasic(url, type, videoData) {
|
||||
elements.loading.style.display = 'none';
|
||||
if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; }
|
||||
@@ -1113,13 +1247,39 @@ const RoomsApp = (function() {
|
||||
|
||||
function syncVideo(currentTime, isPlaying) {
|
||||
const video = getVideoElement();
|
||||
if (!video) return;
|
||||
if (!video) {
|
||||
console.warn('Cannot sync: video element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const timeDiff = Math.abs(video.currentTime - currentTime);
|
||||
if (timeDiff > 1) video.currentTime = currentTime;
|
||||
|
||||
console.log('Syncing video:', {
|
||||
targetTime: currentTime,
|
||||
currentTime: video.currentTime,
|
||||
diff: timeDiff.toFixed(2) + 's',
|
||||
targetState: isPlaying ? 'playing' : 'paused',
|
||||
currentState: video.paused ? 'paused' : 'playing'
|
||||
});
|
||||
|
||||
if (timeDiff > 0.5) {
|
||||
console.log('Time diff exceeds threshold, seeking to:', currentTime);
|
||||
video.currentTime = currentTime;
|
||||
}
|
||||
|
||||
if (isPlaying && video.paused) {
|
||||
video.play().then(() => updatePlayPauseButton(true)).catch(console.error);
|
||||
console.log('Starting playback...');
|
||||
video.play()
|
||||
.then(() => {
|
||||
console.log('✓ Playback started');
|
||||
updatePlayPauseButton(true);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('✗ Playback failed:', err);
|
||||
showPlaybackBlockedToast();
|
||||
});
|
||||
} else if (!isPlaying && !video.paused) {
|
||||
console.log('Pausing playback...');
|
||||
video.pause();
|
||||
updatePlayPauseButton(false);
|
||||
}
|
||||
@@ -1127,8 +1287,62 @@ const RoomsApp = (function() {
|
||||
|
||||
function sendSync() {
|
||||
const video = getVideoElement();
|
||||
if (!video || !ws) return;
|
||||
ws.send(JSON.stringify({ type: 'sync', currentTime: video.currentTime, isPlaying: !video.paused }));
|
||||
if (!video) {
|
||||
console.warn('Cannot send sync: video element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
console.warn('Cannot send sync: WebSocket not connected');
|
||||
return;
|
||||
}
|
||||
|
||||
const syncData = {
|
||||
type: 'sync',
|
||||
currentTime: video.currentTime,
|
||||
isPlaying: !video.paused
|
||||
};
|
||||
|
||||
console.log('Sending sync:', syncData);
|
||||
ws.send(JSON.stringify(syncData));
|
||||
}
|
||||
|
||||
function showPlaybackBlockedToast() {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'playback-blocked-toast';
|
||||
toast.innerHTML = `
|
||||
<div style="display:flex; flex-direction:column; gap:8px; align-items:center;">
|
||||
<span>⚠️ Autoplay blocked by browser</span>
|
||||
<button onclick="this.parentElement.parentElement.remove(); getVideoElement()?.play();"
|
||||
style="background:white; color:#1a1a2e; border:none; padding:6px 12px; border-radius:6px; cursor:pointer; font-weight:600;">
|
||||
Click to Play
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(251, 191, 36, 0.95);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||
animation: scaleIn 0.3s ease-out;
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
if (toast.parentElement) {
|
||||
toast.style.animation = 'fadeOut 0.3s ease-out';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
function updatePlayPauseButton(isPlaying) {
|
||||
@@ -1158,7 +1372,6 @@ const RoomsApp = (function() {
|
||||
function leaveRoom() {
|
||||
if (ws) ws.close();
|
||||
if (hlsInstance) hlsInstance.destroy();
|
||||
window.location.href = '/anime';
|
||||
}
|
||||
|
||||
function openAnimeSearchModal() {
|
||||
|
||||
Reference in New Issue
Block a user