From 5cf034200e79c2e14245537b54ffb16ed47666e8 Mon Sep 17 00:00:00 2001 From: lenafx Date: Mon, 5 Jan 2026 00:00:12 +0100 Subject: [PATCH] better UX and UI on the room page --- desktop/src/api/rooms/rooms.service.ts | 16 +- desktop/src/api/rooms/rooms.websocket.ts | 26 +- desktop/src/scripts/anime/player.js | 15 +- desktop/src/scripts/room-modal.js | 2 +- desktop/src/scripts/room.js | 717 ++++++++---- desktop/views/css/room.css | 1270 +++++++--------------- desktop/views/room.html | 120 +- docker/src/api/rooms/rooms.service.ts | 16 +- docker/src/api/rooms/rooms.websocket.ts | 26 +- docker/src/scripts/anime/player.js | 15 +- docker/src/scripts/room.js | 717 ++++++++---- docker/views/css/room.css | 1270 +++++++--------------- docker/views/room.html | 115 +- 13 files changed, 1984 insertions(+), 2341 deletions(-) diff --git a/desktop/src/api/rooms/rooms.service.ts b/desktop/src/api/rooms/rooms.service.ts index 193095b..4d6cf95 100644 --- a/desktop/src/api/rooms/rooms.service.ts +++ b/desktop/src/api/rooms/rooms.service.ts @@ -10,6 +10,15 @@ interface RoomUser { userId?: number; } +interface SourceContext { + animeId: string; + episode: string | number; + source: string; + extension: string; + server: string; + category: string; +} + export interface QueueItem { uid: string; metadata: RoomMetadata; @@ -23,6 +32,7 @@ interface RoomMetadata { episode: number; image?: string; source?: string; + malId?: number; } interface RoomData { @@ -38,6 +48,7 @@ interface RoomData { videoData?: any; currentTime: number; isPlaying: boolean; + context?: SourceContext; } | null; password?: string; metadata?: RoomMetadata | null; @@ -168,7 +179,10 @@ export function updateRoomVideo(roomId: string, videoData: any): boolean { const room = rooms.get(roomId); if (!room) return false; - room.currentVideo = videoData; + room.currentVideo = { + ...room.currentVideo, + ...videoData + }; return true; } diff --git a/desktop/src/api/rooms/rooms.websocket.ts b/desktop/src/api/rooms/rooms.websocket.ts index 379b16c..f50bb63 100644 --- a/desktop/src/api/rooms/rooms.websocket.ts +++ b/desktop/src/api/rooms/rooms.websocket.ts @@ -303,14 +303,34 @@ function handleMessage(roomId: string, userId: string, data: any) { broadcastToRoom(roomId, { type: 'video_update', video: data.video, - metadata: data.metadata // ✅ CLAVE + metadata: data.metadata }); - break; + case 'queue_add_batch': + if (room.host.id !== userId) return; + + if (Array.isArray(data.items)) { + // Añadimos el índice (i) al forEach + data.items.forEach((item: any, i: number) => { + const newItem = { + // Añadimos el índice '_${i}' al UID para garantizar unicidad en milisegundos + uid: `q_${Date.now()}_${i}_${Math.random().toString(36).substr(2, 5)}`, + metadata: item.metadata, + videoData: item.video, + addedBy: room.users.get(userId)?.username || 'Unknown' + }; + roomService.addQueueItem(roomId, newItem); + }); + + broadcastToRoom(roomId, { + type: 'queue_update', + queue: room.queue + }); + } + break; case 'sync': - // Solo el host puede hacer sync if (room.host.id !== userId) return; if (room.currentVideo) { diff --git a/desktop/src/scripts/anime/player.js b/desktop/src/scripts/anime/player.js index c672fb4..a694675 100644 --- a/desktop/src/scripts/anime/player.js +++ b/desktop/src/scripts/anime/player.js @@ -78,13 +78,11 @@ const AnimePlayer = (function() { initElements(); setupEventListeners(); - // In Room Mode, we show the player immediately and hide extra controls if (_roomMode) { if(els.playerWrapper) { els.playerWrapper.style.display = 'block'; els.playerWrapper.classList.add('room-mode'); } - // Hide extension list loading in room mode } else { loadExtensionsList(); } @@ -128,10 +126,8 @@ const AnimePlayer = (function() { els.progressBuffer = document.querySelector('.progress-buffer'); els.progressHandle = document.querySelector('.progress-handle'); - // Subtitles canvas els.subtitlesCanvas = document.getElementById('subtitles-canvas'); - // Create skip button if not exists if (!document.getElementById('skip-overlay-btn')) { const btn = document.createElement('button'); btn.id = 'skip-overlay-btn'; @@ -143,13 +139,11 @@ const AnimePlayer = (function() { } function setupEventListeners() { - // Close player if(!_roomMode) { const closeBtn = document.getElementById('close-player-btn'); if(closeBtn) closeBtn.addEventListener('click', closePlayer); } - // Episode navigation if(els.prevBtn) els.prevBtn.onclick = () => playEpisode(_currentEpisode - 1); if(els.nextBtn) els.nextBtn.onclick = () => playEpisode(_currentEpisode + 1); @@ -203,6 +197,15 @@ const AnimePlayer = (function() { return; } + if (videoData.malId) _malId = videoData.malId; + if (videoData.episode) _currentEpisode = parseInt(videoData.episode); + + _skipIntervals = []; + if (els.progressContainer) { + els.progressContainer.querySelectorAll('.skip-range, .skip-cut').forEach(e => e.remove()); + } + if (_skipBtn) _skipBtn.classList.remove('visible'); + _currentSubtitles = videoData.subtitles || []; if (els.loader) els.loader.style.display = 'none'; diff --git a/desktop/src/scripts/room-modal.js b/desktop/src/scripts/room-modal.js index 3a22f86..eb73461 100644 --- a/desktop/src/scripts/room-modal.js +++ b/desktop/src/scripts/room-modal.js @@ -118,7 +118,7 @@ class CreateRoomModal { this.close(); - window.open(`/room?id=${data.room.id}`, '_blank', 'noopener,noreferrer'); + window.location.href = `/room?id=${data.room.id}` } catch (err) { alert(err.message); } finally { diff --git a/desktop/src/scripts/room.js b/desktop/src/scripts/room.js index 3aa60a7..062441b 100644 --- a/desktop/src/scripts/room.js +++ b/desktop/src/scripts/room.js @@ -5,10 +5,16 @@ const RoomsApp = (function() { let currentUsername = null; let isHost = false; let isGuest = false; - let hlsInstance = null; let selectedAnimeData = null; let currentLoadedUrl = null; let extensionsReady = false; + let currentAnimeDetails = null; + let selectedEpisodes = new Set([1]); + let lastSelectedEp = 1; + let modalCurrentPage = 1; + let modalTotalEpisodes = 0; + const MODAL_EPS_PER_PAGE = 50; + let currentQueue = []; let configState = { extension: null, @@ -17,6 +23,13 @@ const RoomsApp = (function() { episode: 1 }; + let activeContext = { + extension: null, + server: null, + category: 'sub', + episode: 1 + }; + let extensionsStore = { list: [], settings: {} @@ -29,6 +42,7 @@ const RoomsApp = (function() { roomViewers: document.getElementById('room-viewers'), selectAnimeBtn: document.getElementById('select-anime-btn'), toggleChatBtn: document.getElementById('toggle-chat-btn'), + copyInviteBtn: document.getElementById('copy-invite-btn'), // NUEVO // Host Quick Controls (Header) hostControls: document.getElementById('host-controls'), @@ -60,46 +74,59 @@ const RoomsApp = (function() { chatInput: document.getElementById('chat-input'), roomLayout: document.getElementById('room-layout'), - // Modals + // Join Room Modal Elements joinRoomModal: document.getElementById('join-room-modal'), joinRoomForm: document.getElementById('join-room-form'), + guestNameInput: document.getElementById('guest-name-input'), + joinPasswordInput: document.getElementById('join-password-input'), + passwordGroup: document.getElementById('password-group'), + cancelJoinBtn: document.getElementById('cancel-join-btn'), + joinHostInfo: document.getElementById('join-host-info'), + joinHostAvatar: document.getElementById('join-host-avatar'), + joinHostText: document.getElementById('join-host-text'), + + // Anime Search Modal animeSearchModal: document.getElementById('anime-search-modal'), animeSearchInput: document.getElementById('anime-search-input'), animeResults: document.getElementById('anime-results'), + closeSearchBtn: document.getElementById('close-search-modal'), + animeSearchBtn: document.getElementById('anime-search-btn'), - // Config Elements (Modal) + // Config Elements (Stream Configuration Modal) stepSearch: document.getElementById('step-search'), stepConfig: document.getElementById('step-config'), backToSearchBtn: document.getElementById('back-to-search'), configTitle: document.getElementById('selected-anime-title'), configCover: document.getElementById('config-cover'), + + // Selects del Modal (Legacy/Manual) selExtension: document.getElementById('sel-extension'), selServer: document.getElementById('sel-server'), selCategory: document.getElementById('sel-category'), + + // Controles de Configuración + extContainer: document.getElementById('ext-chips-container'), + serverContainer: document.getElementById('server-chips-container'), + modalSdToggle: document.getElementById('modal-sd-toggle'), + + // Episode Controls inpEpisode: document.getElementById('inp-episode'), + epInc: document.getElementById('ep-inc'), + epDec: document.getElementById('ep-dec'), + + // Launch Actions btnLaunch: document.getElementById('btn-launch-stream'), configError: document.getElementById('config-error'), + btnAddQueue: document.getElementById('btn-add-queue'), + // Toasts & Tabs toastContainer: document.getElementById('video-toast-container'), - tabChatBtn: document.getElementById('tab-chat-btn'), tabQueueBtn: document.getElementById('tab-queue-btn'), tabContentChat: document.getElementById('tab-content-chat'), tabContentQueue: document.getElementById('tab-content-queue'), queueList: document.getElementById('queue-list'), queueCount: document.getElementById('queue-count'), - btnAddQueue: document.getElementById('btn-add-queue'), - }; - - const ui = { - extContainer: document.getElementById('ext-chips-container'), - serverContainer: document.getElementById('server-chips-container'), - sdToggle: document.getElementById('modal-sd-toggle'), - epInput: document.getElementById('inp-episode'), - epInc: document.getElementById('ep-inc'), - epDec: document.getElementById('ep-dec'), - launchBtn: document.getElementById('btn-launch-stream'), - configError: document.getElementById('config-error') }; async function init() { @@ -113,7 +140,6 @@ const RoomsApp = (function() { currentRoomId = urlRoomId; if (window.AnimePlayer) { - console.log('Initializing AnimePlayer in room mode'); window.AnimePlayer.init(null, 'anilist', false, {}, true); } @@ -196,16 +222,61 @@ const RoomsApp = (function() { if (elements.selectAnimeBtn) elements.selectAnimeBtn.onclick = openAnimeSearchModal; if (elements.toggleChatBtn) elements.toggleChatBtn.onclick = toggleChat; - if (elements.leaveRoomBtn) elements.leaveRoomBtn.onclick = leaveRoom; - if (elements.roomExtSelect) elements.roomExtSelect.onchange = (e) => onQuickExtensionChange(e, false); - if (elements.roomServerSelect) elements.roomServerSelect.onchange = onQuickServerChange; + if (elements.roomExtSelect) elements.roomExtSelect.onchange = (e) => { + onQuickExtensionChange(e, false).then(() => {}); + }; if (elements.roomServerSelect) elements.roomServerSelect.onchange = onQuickServerChange; elements.tabChatBtn.onclick = () => switchTab('chat'); elements.tabQueueBtn.onclick = () => switchTab('queue'); - if(elements.btnAddQueue) elements.btnAddQueue.onclick = () => launchStream(true, true); + if (elements.btnAddQueue) { + elements.btnAddQueue.onclick = () => { + if (selectedEpisodes.size === 0) return; + const sortedEps = Array.from(selectedEpisodes).sort((a, b) => a - b); + + const currentProvider = elements.roomExtSelect ? elements.roomExtSelect.value : 'gogoanime'; + + const originalText = elements.btnAddQueue.innerHTML; + elements.btnAddQueue.disabled = true; + elements.btnAddQueue.textContent = "Adding..."; + + const itemsBatch = sortedEps.map(ep => ({ + metadata: { + id: currentAnimeDetails.id, + title: currentAnimeDetails.title.userPreferred || currentAnimeDetails.title.english, + episode: ep, + image: currentAnimeDetails.coverImage?.large, + source: 'anilist', + malId: currentAnimeDetails.idMal + }, + video: { + animeId: currentAnimeDetails.id, + episode: ep, + title: currentAnimeDetails.title.userPreferred, + extension: currentProvider, + server: elements.roomServerSelect?.value || 'gogoanime', + category: elements.roomSdToggle?.getAttribute('data-state') || 'sub' + } + })); + + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'queue_add_batch', + items: itemsBatch + })); + closeAnimeSearchModal(); + } else { + console.error("WebSocket not connected"); + } + + setTimeout(() => { + elements.btnAddQueue.disabled = false; + elements.btnAddQueue.innerHTML = originalText; + }, 500); + }; + } if (elements.roomSdToggle) { elements.roomSdToggle.onclick = () => { if (!isHost) return; @@ -243,7 +314,7 @@ const RoomsApp = (function() { }; } if (elements.selExtension) elements.selExtension.onchange = handleModalExtensionChange; - if (elements.btnLaunch) elements.btnLaunch.onclick = () => launchStream(true); // true = from modal + if (elements.btnLaunch) elements.btnLaunch.onclick = () => launchStream(true); if (elements.toggleUsersBtn) elements.toggleUsersBtn.onclick = toggleUsersList; @@ -284,15 +355,18 @@ const RoomsApp = (function() { await onQuickExtensionChange(null, true); } - function onQuickExtensionChange(e, silent = false) { + async function onQuickExtensionChange(e, silent = false) { if (!isHost) return; const ext = elements.roomExtSelect.value; const settings = extensionsStore.settings[ext]; - if (!settings) return; + + if (!settings) { + console.error("Settings not loaded for extension:", ext); + return; + } elements.roomServerSelect.innerHTML = ''; - const servers = settings.episodeServers?.length ? settings.episodeServers : ['Default']; @@ -304,11 +378,24 @@ const RoomsApp = (function() { elements.roomServerSelect.appendChild(opt); }); - elements.roomServerSelect.value = servers[0]; + const defaultServer = servers[0]; + elements.roomServerSelect.value = defaultServer; - handleDubUI(settings, 'header'); + if (typeof handleDubUI === 'function') { + handleDubUI(settings, 'header'); + } - if (!silent) onQuickServerChange(); + if (typeof activeContext !== 'undefined') { + activeContext.extension = ext; + activeContext.server = defaultServer; + } + + if (!silent) { + await launchStream(false, false, { + forceExtension: ext, + forceServer: defaultServer + }); + } } function handleDubUI(settings, context = 'header') { @@ -333,7 +420,7 @@ const RoomsApp = (function() { } if (context === 'modal') { - const dubBtn = ui.sdToggle?.querySelector('[data-val="dub"]'); + const dubBtn = elements.modalSdToggle?.querySelector('[data-val="dub"]'); if (!dubBtn) return; dubBtn.classList.toggle('disabled', !supportsDub); @@ -347,8 +434,6 @@ const RoomsApp = (function() { function onQuickServerChange() { if (!isHost) return; - if (!elements.roomExtSelect.value) return; - if (!elements.roomServerSelect.value) return; launchStream(false); } @@ -379,50 +464,47 @@ const RoomsApp = (function() { image: img, source: 'anilist' }; + const animeResultObj = { + id: id, + title: title, + cover: img + }; showConfigStep(); + onAnimeSelected(animeResultObj); } } function showConfigStep() { if (!selectedAnimeData) return; - if (!extensionsReady) return; elements.configTitle.textContent = selectedAnimeData.title; elements.configCover.src = selectedAnimeData.image; - if(ui.configError) ui.configError.style.display = 'none'; + if(elements.configError) elements.configError.style.display = 'none'; configState.episode = 1; - configState.server = null; - configState.category = 'sub'; - configState.extension = null; + if(elements.inpEpisode) elements.inpEpisode.value = 1; - if(ui.epInput) ui.epInput.value = 1; - if(ui.launchBtn) ui.launchBtn.disabled = true; - if(elements.btnAddQueue) elements.btnAddQueue.disabled = true; - updateSDUI(); + if(elements.btnLaunch) elements.btnLaunch.disabled = false; + if(elements.btnAddQueue) elements.btnAddQueue.disabled = false; setupConfigListeners(); - renderExtensionChips(); - elements.stepSearch.style.display = 'none'; elements.stepConfig.style.display = 'block'; } function setupConfigListeners() { - if(ui.epInc) ui.epInc.onclick = () => { - ui.epInput.value = parseInt(ui.epInput.value || 0) + 1; - configState.episode = ui.epInput.value; + elements.epInc.onclick = () => { + elements.inpEpisode.value = parseInt(elements.inpEpisode.value || 0) + 1; }; - if(ui.epDec) ui.epDec.onclick = () => { - if(ui.epInput.value > 1) ui.epInput.value = parseInt(ui.epInput.value) - 1; - configState.episode = ui.epInput.value; + elements.epDec.onclick = () => { + if(elements.inpEpisode.value > 1) elements.inpEpisode.value = parseInt(elements.inpEpisode.value) - 1; }; - if(ui.epInput) ui.epInput.onchange = (e) => configState.episode = e.target.value; + if(elements.inpEpisode) elements.inpEpisode.onchange = (e) => configState.episode = e.target.value; - if(ui.sdToggle) { - ui.sdToggle.querySelectorAll('.cat-opt').forEach(opt => { + if(elements.modalSdToggle) { + elements.modalSdToggle.querySelectorAll('.cat-opt').forEach(opt => { opt.onclick = () => { if(opt.classList.contains('disabled')) return; configState.category = opt.dataset.val; @@ -434,49 +516,16 @@ const RoomsApp = (function() { } function updateSDUI() { - if(!ui.sdToggle) return; - ui.sdToggle.querySelectorAll('.cat-opt').forEach(opt => { + if(!elements.modalSdToggle) return; + elements.modalSdToggle.querySelectorAll('.cat-opt').forEach(opt => { opt.classList.toggle('active', opt.dataset.val === configState.category); }); } - function renderExtensionChips() { - ui.extContainer.innerHTML = ''; - - if (!extensionsStore.list || extensionsStore.list.length === 0) { - ui.extContainer.innerHTML = 'No extensions'; - return; - } - - extensionsStore.list.forEach(ext => { - const chip = document.createElement('div'); - chip.className = 'chip'; - chip.textContent = ext.charAt(0).toUpperCase() + ext.slice(1); - - if (!configState.extension && ext === 'anilist') configState.extension = 'anilist'; - if (ext === configState.extension) chip.classList.add('active'); - - chip.onclick = () => { - document.querySelectorAll('#ext-chips-container .chip').forEach(c => c.classList.remove('active')); - chip.classList.add('active'); - - configState.extension = ext; - configState.server = null; - ui.launchBtn.disabled = true; - if(elements.btnAddQueue) elements.btnAddQueue.disabled = true; - - loadServersForExtension(ext); - }; - ui.extContainer.appendChild(chip); - }); - - if(configState.extension) loadServersForExtension(configState.extension); - } - async function loadServersForExtension(extName) { if (!extensionsReady) return; - ui.serverContainer.innerHTML = '
Loading servers...
'; - ui.launchBtn.disabled = true; + elements.serverContainer.innerHTML = '
Loading servers...
'; + elements.btnLaunch.disabled = true; if(elements.btnAddQueue) elements.btnAddQueue.disabled = true; try { @@ -489,12 +538,12 @@ const RoomsApp = (function() { renderServerChips(servers); - const dubBtn = ui.sdToggle.querySelector('[data-val="dub"]'); + const dubBtn = elements.modalSdToggle.querySelector('[data-val="dub"]'); if (dubBtn) { if (!settings.supportsDub) { dubBtn.classList.add('disabled'); if(configState.category === 'dub') { - configState.category = 'sub'; // Forzar sub si no hay dub + configState.category = 'sub'; updateSDUI(); } } else { @@ -504,12 +553,12 @@ const RoomsApp = (function() { } catch (e) { console.error("Error loading settings", e); - ui.serverContainer.innerHTML = '
Error loading servers
'; + elements.serverContainer.innerHTML = '
Error loading servers
'; } } function renderServerChips(servers) { - ui.serverContainer.innerHTML = ''; + elements.serverContainer.innerHTML = ''; servers.forEach(srv => { const chip = document.createElement('div'); @@ -521,11 +570,11 @@ const RoomsApp = (function() { chip.classList.add('active'); configState.server = srv; - ui.launchBtn.disabled = false; + elements.btnLaunch.disabled = false; if(elements.btnAddQueue) elements.btnAddQueue.disabled = false; }; - ui.serverContainer.appendChild(chip); + elements.serverContainer.appendChild(chip); }); } @@ -570,67 +619,51 @@ const RoomsApp = (function() { // --- STREAM LAUNCHER (Unified) --- - async function launchStream(fromModal = false, isQueueAction = false) { - if (!selectedAnimeData) { - console.warn("No anime selected data found"); + async function launchStream(fromModal = false, isQueueAction = false, overrides = {}) { + if (!selectedAnimeData) return; + + let episodeToPlay = activeContext.episode; + if (fromModal && elements.inpEpisode) episodeToPlay = elements.inpEpisode.value; + + const ext = overrides.forceExtension || elements.roomExtSelect.value || activeContext.extension; + const server = overrides.forceServer || elements.roomServerSelect.value || activeContext.server; + const category = elements.roomSdToggle.getAttribute('data-state') || activeContext.category; + + if (!ext || !server) { + console.warn("Faltan datos (ext o server) para lanzar el stream"); return; } - let ext, server, episode, category; + activeContext = { extension: ext, server, category, episode: episodeToPlay }; + selectedAnimeData.episode = parseInt(episodeToPlay); - if (fromModal) { - ext = configState.extension; - server = configState.server; - episode = configState.episode; - category = configState.category; - if(isQueueAction) { - elements.btnAddQueue.disabled = true; - elements.btnAddQueue.textContent = 'Adding...'; - } - } else { - ext = elements.roomExtSelect.value; - server = elements.roomServerSelect.value; - - let epText = elements.npEpisode.textContent.replace('Ep ', '').trim(); - - if (!epText || epText === '--' || isNaN(epText)) { - if (selectedAnimeData.episode) { - epText = selectedAnimeData.episode; - } else { - epText = "1"; - } - } - episode = epText; - - category = elements.roomSdToggle.getAttribute('data-state'); - } - - if (!ext || !server || !episode) { - console.error("Missing params:", { ext, server, episode }); - if(fromModal) { - elements.configError.textContent = "Please select an extension, server, and episode."; - elements.configError.style.display = 'block'; - } else { - alert("Please select a valid server/extension."); - } - return; + if (fromModal && isQueueAction) { + elements.btnAddQueue.disabled = true; + elements.btnAddQueue.textContent = 'Adding...'; + } else if (!isQueueAction) { + if(typeof showSystemToast === 'function') showSystemToast(`Loading ${ext}...`); } try { - const apiUrl = `/api/watch/stream?animeId=${selectedAnimeData.id}&episode=${episode}&server=${encodeURIComponent(server)}&category=${category}&ext=${ext}&source=${selectedAnimeData.source}`; + const apiUrl = `/api/watch/stream?animeId=${selectedAnimeData.id}&episode=${episodeToPlay}&server=${encodeURIComponent(server)}&category=${category}&ext=${ext}&source=anilist`; console.log('Fetching stream:', apiUrl); const res = await fetch(apiUrl); - if (!res.ok) throw new Error(`Fetch failed: ${res.status}`); + if (!res.ok) throw new Error(`Error ${res.status}: Failed to fetch stream`); const data = await res.json(); + if (data.error) throw new Error(data.error); + const source = data.videoSources?.find(s => s.type === 'm3u8') || data.videoSources?.[0]; - if (!source) throw new Error('No video source found'); + + if (!source) { + throw new Error(`No sources found on ${ext} for this episode.`); + } let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`; const headers = data.headers || {}; - if (headers['Referer'] && headers['Referer'] !== "null") proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`; + if (headers['Referer']) proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`; const subtitles = (source.subtitles || []).map(sub => ({ label: sub.language, @@ -638,17 +671,30 @@ const RoomsApp = (function() { src: `/api/proxy?url=${encodeURIComponent(sub.url)}` })); + const contextPayload = { + animeId: selectedAnimeData.id, + episode: episodeToPlay, + source: selectedAnimeData.source, + extension: ext, + server: server, + category: category + }; + const videoPayload = { - videoData: { url: proxyUrl, type: source.type || 'm3u8', headers: headers }, - subtitles: subtitles + videoData: { url: proxyUrl, type: source.type || 'm3u8', headers }, + subtitles: subtitles, + context: contextPayload, + malId: selectedAnimeData.malId, + episode: episodeToPlay }; const metaPayload = { title: selectedAnimeData.title, - episode: episode, + episode: episodeToPlay, image: selectedAnimeData.image, id: selectedAnimeData.id, - source: ext + source: ext, + malId: selectedAnimeData.malId }; if (ws && ws.readyState === WebSocket.OPEN) { @@ -658,59 +704,239 @@ const RoomsApp = (function() { video: videoPayload, metadata: metaPayload })); - - showSystemToast("Added to queue!"); - if(fromModal) closeAnimeSearchModal(); - + if(typeof showSystemToast === 'function') showSystemToast("Added to queue!"); } else { ws.send(JSON.stringify({ type: 'video_update', video: { ...videoPayload, currentTime: 0, isPlaying: true }, metadata: metaPayload })); - } - loadVideo(videoPayload.video); - updateHeaderInfo(videoPayload.metadata); - if(fromModal) { - closeAnimeSearchModal(); + loadVideo({ ...videoPayload, currentTime: 0, isPlaying: true }); + updateHeaderInfo(metaPayload); - selectedAnimeData.source = ext; - - await populateQuickControls(); - - if (extensionsStore.list.includes(ext)) { - elements.roomExtSelect.value = ext; - await onQuickExtensionChange(null, true); - elements.roomServerSelect.value = server; - - elements.roomSdToggle.setAttribute('data-state', category); - elements.roomSdToggle.querySelectorAll('.sd-option').forEach(o => - o.classList.toggle('active', o.dataset.val === category) - ); - } + if(typeof syncControlsUI === 'function') syncControlsUI(contextPayload); } } + if (fromModal) closeAnimeSearchModal(); + } catch (err) { - console.error('Stream launch error:', err); - const msg = "Error loading stream. Try another server."; - if(fromModal) { - elements.configError.textContent = msg; - elements.configError.style.display = 'block'; + console.error("Stream Launch Error:", err); + + const errorMsg = err.message || "Error loading stream."; + + if (fromModal) { + if(elements.configError) { + elements.configError.textContent = errorMsg; + elements.configError.style.display = 'block'; + } } else { - alert(msg); + if(typeof showSystemToast === 'function') { + showSystemToast(`Error: ${errorMsg}`); + } else { + alert(errorMsg); + } } } finally { - if(fromModal && isQueueAction) { + if (fromModal && isQueueAction) { elements.btnAddQueue.disabled = false; elements.btnAddQueue.textContent = '+ Add to Queue'; } } } - // --- CONNECTION & WS --- + async function onAnimeSelected(animeResult) { + const gridContainer = document.getElementById('episodes-grid'); + gridContainer.innerHTML = '
Loading info...
'; + document.getElementById('step-search').style.display = 'none'; + document.getElementById('step-config').style.display = 'block'; + + document.getElementById('config-cover').src = animeResult.cover; + document.getElementById('selected-anime-title').textContent = animeResult.title; + + selectedEpisodes.clear(); + selectedEpisodes.add(1); + updateCountBtn(); + + try { + const response = await fetch(`/api/anime/${animeResult.id}?source=anilist`); + if (!response.ok) throw new Error("Failed to load anime details"); + + const data = await response.json(); + currentAnimeDetails = data; + + if (selectedAnimeData) { + selectedAnimeData.malId = data.idMal; + } + + modalTotalEpisodes = data.episodes || 12; + modalCurrentPage = 1; + + renderModalEpisodes(); + setupModalPaginationControls(); + + } catch (error) { + console.error("Error fetching details", error); + gridContainer.innerHTML = ''; + document.querySelector('.manual-ep-input').style.display = 'block'; + document.getElementById('modal-pagination').style.display = 'none'; + } + } + + function renderModalEpisodes() { + const grid = document.getElementById('episodes-grid'); + grid.innerHTML = ''; + + const start = (modalCurrentPage - 1) * MODAL_EPS_PER_PAGE + 1; + const end = Math.min(start + MODAL_EPS_PER_PAGE - 1, modalTotalEpisodes); + + const infoText = document.getElementById('modal-page-info'); + if (infoText) infoText.textContent = `${start} - ${end} of ${modalTotalEpisodes}`; + + for (let i = start; i <= end; i++) { + const btn = document.createElement('div'); + const isSelected = selectedEpisodes.has(i); + btn.className = `ep-grid-btn ${isSelected ? 'selected' : ''}`; + btn.textContent = i; + btn.dataset.ep = i; + + btn.onclick = (e) => toggleEpisode(i, btn, e.shiftKey); + + grid.appendChild(btn); + } + + document.getElementById('modal-prev-page').disabled = modalCurrentPage === 1; + document.getElementById('modal-next-page').disabled = end >= modalTotalEpisodes; + + document.getElementById('modal-pagination').style.display = modalTotalEpisodes > MODAL_EPS_PER_PAGE ? 'flex' : 'none'; + document.querySelector('.manual-ep-input').style.display = 'none'; + } + + function setupModalPaginationControls() { + document.getElementById('modal-prev-page').onclick = () => { + if (modalCurrentPage > 1) { + modalCurrentPage--; + renderModalEpisodes(); + } + }; + + document.getElementById('modal-next-page').onclick = () => { + const maxPage = Math.ceil(modalTotalEpisodes / MODAL_EPS_PER_PAGE); + if (modalCurrentPage < maxPage) { + modalCurrentPage++; + renderModalEpisodes(); + } + }; + + document.getElementById('select-all-page').onclick = () => { + const start = (modalCurrentPage - 1) * MODAL_EPS_PER_PAGE + 1; + const end = Math.min(start + MODAL_EPS_PER_PAGE - 1, modalTotalEpisodes); + for(let i=start; i<=end; i++) selectedEpisodes.add(i); + renderModalEpisodes(); + updateCountBtn(); + }; + + document.getElementById('select-none-eps').onclick = () => { + selectedEpisodes.clear(); + renderModalEpisodes(); + updateCountBtn(); + }; + } + + async function syncControlsUI(context) { + if (!context || !isHost) return; + + if (context.extension && elements.roomExtSelect.value !== context.extension) { + elements.roomExtSelect.value = context.extension; + // Importante: Cargar los servidores de esa extensión sin disparar otro play + await onQuickExtensionChange(null, true); + } + + if (context.server) { + const options = Array.from(elements.roomServerSelect.options).map(o => o.value); + if (options.includes(context.server)) { + elements.roomServerSelect.value = context.server; + } + } + + if (context.category) { + elements.roomSdToggle.setAttribute('data-state', context.category); + elements.roomSdToggle.querySelectorAll('.sd-option').forEach(opt => { + opt.classList.toggle('active', opt.dataset.val === context.category); + }); + } + + activeContext = { ...activeContext, ...context }; + } + + function toggleEpisode(epNum, btnElement, isShift) { + if (isShift && lastSelectedEp) { + const start = Math.min(lastSelectedEp, epNum); + const end = Math.max(lastSelectedEp, epNum); + + for (let i = start; i <= end; i++) { + selectedEpisodes.add(i); + const el = document.querySelector(`.ep-grid-btn[data-ep="${i}"]`); + if (el) el.classList.add('selected'); + } + } else { + if (selectedEpisodes.has(epNum)) { + selectedEpisodes.delete(epNum); + btnElement.classList.remove('selected'); + } else { + selectedEpisodes.add(epNum); + btnElement.classList.add('selected'); + } + } + + lastSelectedEp = epNum; + updateCountBtn(); + } + + function updateCountBtn() { + document.getElementById('sel-count').textContent = selectedEpisodes.size; + } + + document.getElementById('btn-add-queue').addEventListener('click', () => { + if (selectedEpisodes.size === 0) return; + + const sortedEps = Array.from(selectedEpisodes).sort((a, b) => a - b); + + const itemsBatch = sortedEps.map(ep => ({ + metadata: { + id: currentAnimeDetails.id, + title: currentAnimeDetails.title.userPreferred || currentAnimeDetails.title.english, + episode: ep, + image: currentAnimeDetails.coverImage?.large, + source: 'anilist', + malId: currentAnimeDetails.idMal + }, + video: { + animeId: currentAnimeDetails.id, + episode: ep, + title: currentAnimeDetails.title.userPreferred, + server: document.getElementById('room-server-select').value || 'gogoanime', + category: 'sub' // o lo que esté seleccionado + } + })); + + if (itemsBatch.length === 1) { + socket.send(JSON.stringify({ + type: 'queue_add', + metadata: itemsBatch[0].metadata, + video: itemsBatch[0].video + })); + } else { + socket.send(JSON.stringify({ + type: 'queue_add_batch', + items: itemsBatch + })); + } + + closeModal(); + }); function connectToRoom(roomId, guestName, password) { const token = localStorage.getItem('token'); @@ -863,6 +1089,9 @@ const RoomsApp = (function() { if (data.room.metadata) { updateHeaderInfo(data.room.metadata); } + if (data.room.currentVideo.context) { + syncControlsUI(data.room.currentVideo.context); + } if (!isHost) { console.log('Video detected on join, requesting sync...'); @@ -911,22 +1140,23 @@ const RoomsApp = (function() { case 'video_update': loadVideo(data.video); + if (data.metadata) { - selectedAnimeData = { - ...selectedAnimeData, - ...data.metadata - }; + selectedAnimeData = { ...selectedAnimeData, ...data.metadata }; updateHeaderInfo(data.metadata); } + if (data.video.context) { + syncControlsUI(data.video.context); + } + if (!isHost) { setTimeout(() => { if (ws && ws.readyState === WebSocket.OPEN) { console.log('New video loaded, requesting sync...'); ws.send(JSON.stringify({ type: 'request_sync' })); } - }, 1500); - } + }, 1500); } break; case 'sync': @@ -970,6 +1200,7 @@ const RoomsApp = (function() { } function renderQueue(queueItems) { + currentQueue = queueItems; if (!elements.queueList) return; elements.queueCount.textContent = queueItems.length; @@ -1021,10 +1252,55 @@ const RoomsApp = (function() { }).join(''); } - // Funciones globales (window) para los botones onclick - window.playQueueItem = function(uid) { - if(ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'queue_play_item', itemUid: uid })); + window.playQueueItem = async function(uid) { + const item = currentQueue.find(i => i.uid === uid); + if (!item) return; + + if(typeof showSystemToast === 'function') showSystemToast("Loading from queue..."); + + if (item.videoData && item.videoData.animeId) { + const v = item.videoData; + + selectedAnimeData = { + id: v.animeId, + title: v.title, + image: item.metadata.image, + source: 'anilist', + episode: parseInt(v.episode), + malId: item.metadata.malId + }; + + activeContext.episode = parseInt(v.episode); + activeContext.category = v.category || 'sub'; + + if (elements.roomSdToggle) { + elements.roomSdToggle.setAttribute('data-state', activeContext.category); + elements.roomSdToggle.querySelectorAll('.sd-option').forEach(opt => { + opt.classList.toggle('active', opt.dataset.val === activeContext.category); + }); + } + + try { + const providerExtension = v.extension || elements.roomExtSelect?.value || 'gogoanime'; + + console.log(`Playing from queue with provider: ${providerExtension}`); + + await launchStream(false, false, { + forceExtension: providerExtension, + forceServer: v.server + }); + + removeQueueItem(uid); + + } catch(e) { + console.error("Error playing queue item:", e); + if(typeof showSystemToast === 'function') showSystemToast("Error: " + e.message); + } + + } else { + if(ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'queue_play_item', itemUid: uid })); + } } }; @@ -1054,6 +1330,10 @@ const RoomsApp = (function() { window.AnimePlayer.setRoomHost(isHost); } + if (isHost) { + initGlobalControls(); + } + const copyInviteBtn = document.getElementById('copy-invite-btn'); if (copyInviteBtn) { @@ -1106,6 +1386,30 @@ const RoomsApp = (function() { if (room.currentVideo) loadVideo(room.currentVideo); } + async function initGlobalControls() { + if (!isHost || !extensionsReady) return; + + if (elements.roomExtSelect.options.length > 1) return; + + elements.roomExtSelect.innerHTML = ''; + + extensionsStore.list.forEach(ext => { + const opt = document.createElement('option'); + opt.value = ext; + opt.textContent = ext[0].toUpperCase() + ext.slice(1); + elements.roomExtSelect.appendChild(opt); + }); + + const defaultExt = 'anilist'; + if (extensionsStore.list.includes(defaultExt)) { + elements.roomExtSelect.value = defaultExt; + } else { + elements.roomExtSelect.value = extensionsStore.list[0]; + } + + await onQuickExtensionChange(null, true); + } + function showCopyToast(message) { const toast = document.createElement('div'); toast.className = 'copy-toast'; @@ -1140,10 +1444,13 @@ const RoomsApp = (function() { if (elements.npEpisode) elements.npEpisode.textContent = `Episode ${meta.episode}`; if (elements.npInfo) elements.npInfo.style.opacity = '1'; - if(!selectedAnimeData) selectedAnimeData = { ...meta, source: 'anilist' }; - else { + if(!selectedAnimeData) { + selectedAnimeData = { ...meta, source: 'anilist' }; + } else { selectedAnimeData.id = meta.id; selectedAnimeData.title = meta.title; + selectedAnimeData.image = meta.image || selectedAnimeData.image; + selectedAnimeData.episode = parseInt(meta.episode); } } @@ -1316,8 +1623,13 @@ const RoomsApp = (function() { if (window.AnimePlayer && typeof window.AnimePlayer.loadVideoFromRoom === 'function') { if(elements.loading) elements.loading.style.display = 'none'; - window.AnimePlayer.loadVideoFromRoom({ url: url, type: type, subtitles: videoData.subtitles }); - + window.AnimePlayer.loadVideoFromRoom({ + url: url, + type: type, + subtitles: videoData.subtitles, + malId: videoData.malId, + episode: videoData.episode + }); setTimeout(() => { const newVideoElement = document.getElementById('player'); if (newVideoElement) { @@ -1326,8 +1638,6 @@ const RoomsApp = (function() { updatePlayPauseButton(!newVideoElement.paused); } }, 500); - } else { - loadVideoBasic(url, type, videoData); } } @@ -1336,23 +1646,6 @@ const RoomsApp = (function() { return document.getElementById('player'); } - function loadVideoBasic(url, type, videoData) { - elements.loading.style.display = 'none'; - if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; } - - if (Hls.isSupported() && type === 'm3u8') { - hlsInstance = new Hls(); - hlsInstance.loadSource(url); - hlsInstance.attachMedia(elements.video); - hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { - if (videoData.isPlaying) elements.video.play().catch(e=>console.error(e)); - }); - } else { - elements.video.src = url; - if (videoData.isPlaying) elements.video.play(); - } - } - function syncVideo(currentTime, isPlaying) { const video = getVideoElement(); if (!video) { @@ -1473,14 +1766,20 @@ const RoomsApp = (function() { if (isHost) { console.log('Video ended. Checking queue...'); setTimeout(() => { - if(ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'play_next' })); - } + playNextInQueue(); }, 1000); } }); } + async function playNextInQueue() { + if (currentQueue.length === 0) return; + const nextItem = currentQueue[0]; + if (nextItem) { + await window.playQueueItem(nextItem.uid); + } + } + function formatTime(s) { if(isNaN(s)) return "0:00"; const m = Math.floor(s/60), sec = Math.floor(s%60); diff --git a/desktop/views/css/room.css b/desktop/views/css/room.css index 2a2c2fc..457a0f6 100644 --- a/desktop/views/css/room.css +++ b/desktop/views/css/room.css @@ -1,3 +1,6 @@ +/* ========================================= + 1. VARIABLES & RESET + ========================================= */ :root { --brand-color: #8b5cf6; --brand-gradient: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%); @@ -23,6 +26,9 @@ background: rgba(255, 255, 255, 0.4); } +/* ========================================= + 2. UTILITIES & ANIMATIONS + ========================================= */ .spinner { width: 24px; height: 24px; @@ -33,7 +39,22 @@ } @keyframes spin { to { transform: rotate(360deg); } } +@keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } +@keyframes slideUp { from { transform: translateX(-50%) translateY(20px); opacity: 0; } to { transform: translateX(-50%) translateY(0); opacity: 1; } } +@keyframes slideDown { from { transform: translateX(-50%) translateY(-20px); opacity: 0; } to { transform: translateX(-50%) translateY(0); opacity: 1; } } +@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } +@keyframes scaleIn { from { transform: translate(-50%, -50%) scale(0.8); opacity: 0; } to { transform: translate(-50%, -50%) scale(1); opacity: 1; } } +@keyframes toastSlideIn { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); } } +@keyframes toastFadeOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-10px); visibility: hidden; } } +@keyframes pulse { + 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } + 70% { transform: scale(1); box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); } + 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } +} +/* ========================================= + 3. BUTTONS & INPUTS + ========================================= */ .btn-icon-glass, .btn-icon-small, .modal-close { appearance: none; background: rgba(255, 255, 255, 0.05); @@ -46,7 +67,6 @@ align-items: center; justify-content: center; } - .btn-icon-glass { width: 36px; height: 36px; backdrop-filter: blur(8px); } .btn-icon-small { padding: 6px; } .modal-close { width: 32px; height: 32px; position: absolute; top: 20px; right: 20px; background: rgba(255, 255, 255, 0.1); } @@ -60,22 +80,15 @@ .modal-close:hover { background: rgba(255, 255, 255, 0.3); transform: rotate(90deg); } .btn-glass-primary { - display: flex; - align-items: center; - gap: 8px; + display: flex; align-items: center; gap: 8px; background: rgba(139, 92, 246, 0.25); border: 1px solid rgba(139, 92, 246, 0.4); color: white; - padding: 0 16px; - height: 36px; - border-radius: 10px; - font-weight: 600; - font-size: 0.9rem; - cursor: pointer; - backdrop-filter: blur(8px); + padding: 0 16px; height: 36px; + border-radius: 10px; font-weight: 600; font-size: 0.9rem; + cursor: pointer; backdrop-filter: blur(8px); transition: all 0.2s; } - .btn-glass-primary:hover { background: rgba(139, 92, 246, 0.4); box-shadow: 0 0 15px rgba(139, 92, 246, 0.2); @@ -84,23 +97,13 @@ .btn-confirm, .btn-primary { background: var(--brand-color); - border: none; - color: white; - padding: 10px 24px; - border-radius: 10px; - font-weight: 700; - cursor: pointer; + border: none; color: white; + padding: 10px 24px; border-radius: 10px; + font-weight: 700; cursor: pointer; transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; -} - -.btn-confirm:hover, .btn-primary:hover { - background: #7c3aed; - transform: translateY(-2px); + display: flex; align-items: center; justify-content: center; gap: 8px; } +.btn-confirm:hover, .btn-primary:hover { background: #7c3aed; transform: translateY(-2px); } .btn-confirm:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .btn-confirm.full-width { width: 100%; } @@ -108,469 +111,359 @@ background: transparent; border: 1px solid rgba(255, 255, 255, 0.2); color: rgba(255, 255, 255, 0.8); - padding: 10px 20px; - border-radius: 8px; - cursor: pointer; - font-weight: 600; + padding: 10px 20px; border-radius: 8px; + cursor: pointer; font-weight: 600; transition: all 0.2s; } .btn-cancel:hover { background: rgba(255, 255, 255, 0.1); color: white; } +.btn-quick-action { + display: flex; align-items: center; gap: 6px; + height: 34px; padding: 0 12px; + background: rgba(139, 92, 246, 0.15); + border: 1px solid rgba(139, 92, 246, 0.3); + border-radius: 8px; color: #d8b4fe; + font-size: 0.85rem; font-weight: 600; + cursor: pointer; transition: all 0.2s; +} +.btn-quick-action:hover { + background: var(--brand-color); border-color: var(--brand-color); + color: white; transform: translateY(-1px); +} +#copy-invite-btn { transition: all 0.3s ease; } +#copy-invite-btn:hover { transform: scale(1.05); } +#copy-invite-btn:active { transform: scale(0.95); } + +/* Inputs */ input[type="text"], input[type="password"], input[type="number"], .form-input { width: 100%; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); - color: white; - padding: 12px 16px; - border-radius: 10px; - font-size: 1rem; - outline: none; - transition: border-color 0.2s; + color: white; padding: 12px 16px; + border-radius: 10px; font-size: 1rem; + outline: none; transition: border-color 0.2s; } +input:focus, .form-input:focus { border-color: var(--brand-color); background: rgba(255, 255, 255, 0.08); } +input[type=number]::-webkit-inner-spin-button, input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } +input[type=number] { -moz-appearance: textfield; } -input:focus, .form-input:focus { - border-color: var(--brand-color); - background: rgba(255, 255, 255, 0.08); +/* Custom Select Wrappers */ +.quick-select-wrapper { + position: relative; display: flex; align-items: center; + height: 34px; background: rgba(255, 255, 255, 0.03); + border-radius: 8px; transition: all 0.2s; + padding: 0 8px; min-width: 110px; } +.quick-select-wrapper:hover { background: rgba(255, 255, 255, 0.08); } +.quick-select-wrapper svg { pointer-events: none; color: var(--text-muted); margin-right: 6px; flex-shrink: 0; } +.select-arrow { font-size: 0.6rem; color: rgba(255, 255, 255, 0.4); pointer-events: none; margin-left: 4px; } +.quick-select, .glass-select-sm { + appearance: none; -webkit-appearance: none; + background: transparent; border: none; + color: #fff; width: 100%; height: 100%; + cursor: pointer; outline: none; position: relative; z-index: 2; +} .glass-select-sm { - appearance: none; - -webkit-appearance: none; background-color: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); - color: #eee; - padding: 0 32px 0 12px; - height: 32px; - line-height: 30px; - border-radius: 8px; - font-size: 0.85rem; - font-weight: 600; - cursor: pointer; - outline: none; - transition: all 0.2s; - max-width: 140px; - white-space: nowrap; - text-overflow: ellipsis; - - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='rgba(255,255,255,0.6)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 8px center; - background-size: 14px; + padding: 0 32px 0 12px; height: 32px; line-height: 30px; + border-radius: 8px; font-size: 0.85rem; font-weight: 600; + max-width: 140px; white-space: nowrap; text-overflow: ellipsis; + background-image: url("data:image/svg+xml,..."); /* SVG truncated for brevity */ + background-repeat: no-repeat; background-position: right 8px center; background-size: 14px; } +.quick-select option, .glass-select-sm option { background: #1a1a1a; color: #eee; } -.glass-select-sm:hover { - background-color: rgba(255, 255, 255, 0.1); - border-color: rgba(255,255,255,0.3); - color: #fff; -} -.glass-select-sm option { background: #1a1a1a; color: #e0e0e0; } - -.chips-grid { display: flex; flex-wrap: wrap; gap: 10px; } -.chip { - padding: 8px 16px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 8px; - color: #ccc; - font-size: 0.9rem; - cursor: pointer; - transition: all 0.2s; - user-select: none; -} -.chip:hover { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.3); transform: translateY(-2px); } -.chip.active { - background: var(--brand-color); - border-color: var(--brand-color); - color: white; - box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); -} -.chip.disabled { opacity: 0.5; pointer-events: none; filter: grayscale(1); } - +/* ========================================= + 4. LAYOUT + ========================================= */ .room-layout { display: grid; grid-template-columns: 1fr 380px; - height: 100vh; - overflow: hidden; + height: 100vh; overflow: hidden; transition: grid-template-columns 0.3s cubic-bezier(0.4, 0, 0.2, 1); } +.room-layout.chat-hidden { grid-template-columns: 1fr 0px !important; } -.room-layout.chat-hidden { - grid-template-columns: 1fr 0px !important; -} - +/* Video Area */ .video-area { - display: flex; - flex-direction: column; - background: #000; - flex: 1; - min-height: 0; - overflow: hidden; - position: relative; -} - -@media (max-width: 1200px) { - .room-layout { - grid-template-columns: 1fr; - grid-template-rows: 1fr auto; - } - .room-layout.chat-hidden { - grid-template-rows: 1fr 0px; - } - .chat-sidebar { - height: 350px; - border-left: none; - border-top: 1px solid var(--glass-border); - } + display: flex; flex-direction: column; + background: #000; flex: 1; min-height: 0; + overflow: hidden; position: relative; } +/* ========================================= + 5. HEADER + ========================================= */ .room-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 24px; - background: rgba(10, 10, 10, 0.85); - backdrop-filter: blur(20px); + display: flex; justify-content: space-between; align-items: center; + padding: 0 24px; height: 64px; + background: rgba(10, 10, 10, 0.85); backdrop-filter: blur(20px); border-bottom: 1px solid var(--glass-border); - height: 64px; - z-index: 20; - flex-shrink: 0; + z-index: 20; flex-shrink: 0; } -.header-left, .header-right { - display: flex; - align-items: center; - gap: 16px; - flex: 1; -} +.header-left, .header-right { display: flex; align-items: center; gap: 16px; flex: 1; } .header-right { justify-content: flex-end; } +.header-center { flex: 2; display: flex; justify-content: center; z-index: 50; } .room-info { display: flex; flex-direction: column; justify-content: center; line-height: 1.2; } -#room-name { - margin: 0; - font-size: 1rem; - font-weight: 700; - color: white; - letter-spacing: -0.01em; -} +#room-name { margin: 0; font-size: 1rem; font-weight: 700; color: white; letter-spacing: -0.01em; } .np-fade { display: flex; align-items: center; opacity: 0.7; font-size: 0.8rem; gap: 6px; margin-top: 2px; } .np-title { color: #ccc; font-weight: 500; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .np-sep { color: #555; } -.np-badge { - background: rgba(255, 255, 255, 0.1); - color: #fff; - padding: 1px 6px; - border-radius: 4px; - font-size: 0.7rem; - font-weight: 700; -} - -.header-center { flex: 2; display: flex; justify-content: center; z-index: 50; } +.np-badge { background: rgba(255, 255, 255, 0.1); color: #fff; padding: 1px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 700; } +/* Header Controls */ .quick-controls-group { - display: flex; - align-items: center; - gap: 8px; - background: rgba(20, 20, 20, 0.6); - backdrop-filter: blur(12px); - padding: 4px 8px; - border-radius: 10px; - border: 1px solid var(--glass-border); - box-shadow: 0 4px 12px rgba(0,0,0,0.2); + display: flex; align-items: center; gap: 4px; + background: rgba(15, 15, 15, 0.85); + backdrop-filter: blur(16px); + padding: 4px; height: 44px; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); transition: all 0.2s ease; - height: 42px; } -.quick-controls-group:hover { border-color: rgba(255,255,255,0.15); background: rgba(30, 30, 30, 0.7); } +.quick-controls-group:hover { border-color: rgba(255, 255, 255, 0.2); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } +.control-divider { width: 1px; height: 20px; background: rgba(255, 255, 255, 0.1); margin: 0 4px; } + +/* Sub/Dub Toggle */ .sd-toggle.small { - height: 32px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255,255,255,0.1); - border-radius: 8px; + height: 34px; min-width: 86px; + background: transparent; border: none; + display: flex; position: relative; cursor: pointer; box-sizing: border-box; padding: 2px; - display: flex; - position: relative; - cursor: pointer; - min-width: 80px; - box-sizing: border-box; } .sd-toggle.small .sd-bg { - position: absolute; - top: 2px; left: 2px; + position: absolute; top: 2px; left: 2px; width: calc(50% - 2px); height: calc(100% - 4px); - background: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 6px; transition: transform 0.2s cubic-bezier(0.4, 0.0, 0.2, 1); } .sd-toggle[data-state="dub"] .sd-bg { transform: translateX(100%); } .sd-toggle.small .sd-option { - flex: 1; - z-index: 2; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.75rem; - font-weight: 700; + flex: 1; z-index: 2; + display: flex; align-items: center; justify-content: center; + font-size: 0.75rem; font-weight: 700; color: rgba(255,255,255,0.4); - transition: color 0.2s; - user-select: none; + transition: color 0.2s; user-select: none; } .sd-toggle.small .sd-option.active { color: #fff; } .viewers-pill { - display: flex; - align-items: center; - gap: 6px; - background: rgba(0,0,0,0.3); - padding: 0 10px; - border-radius: 20px; - border: 1px solid rgba(255,255,255,0.05); - font-size: 0.8rem; - color: #aaa; - height: 32px; + display: flex; align-items: center; gap: 6px; + background: rgba(0,0,0,0.3); padding: 0 10px; + border-radius: 20px; border: 1px solid rgba(255,255,255,0.05); + font-size: 0.8rem; color: #aaa; height: 32px; } +#toggle-chat-btn { position: relative; } +#toggle-chat-btn.has-unread::after { + content: ''; position: absolute; top: 2px; right: 2px; + width: 10px; height: 10px; background-color: #ef4444; + border: 2px solid #1a1a1a; border-radius: 50%; + animation: pulse 2s infinite; +} + +/* ========================================= + 6. PLAYER + ========================================= */ .player-wrapper { - display: flex !important; - flex-direction: column; - flex: 1; - width: 100%; - height: auto !important; - min-height: 0; - position: relative !important; - z-index: 1 !important; - background: transparent !important; - overflow: hidden; + display: flex !important; flex-direction: column; flex: 1; + width: 100%; height: auto !important; min-height: 0; + position: relative !important; z-index: 1 !important; + background: transparent !important; overflow: hidden; } - -.player-container { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - position: relative; -} - +.player-container { width: 100%; height: 100%; display: flex; flex-direction: column; position: relative; } .video-frame { - flex: 1; - min-height: 0; - position: relative; - background: #000; - display: flex; - align-items: center; - justify-content: center; -} - -#player { - width: 100%; - height: 100%; - max-height: 100%; + flex: 1; min-height: 0; position: relative; + background: #000; display: flex; align-items: center; justify-content: center; } +#player { width: 100%; height: 100%; max-height: 100%; } +#subtitles-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; } .custom-controls { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - z-index: 60; + position: absolute; bottom: 0; left: 0; width: 100%; z-index: 60; background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0) 100%); padding: 20px 10px 10px; } +.controls-left { display: flex; align-items: center; gap: 10px; } +.time-display { white-space: nowrap; font-variant-numeric: tabular-nums; font-size: 0.9rem; color: #ddd; min-width: fit-content; } -.controls-left { - display: flex; - align-items: center; - gap: 10px; -} - -.time-display { - white-space: nowrap; - font-variant-numeric: tabular-nums; - font-size: 0.9rem; - color: #ddd; - min-width: fit-content; -} - -#subtitles-canvas { - position: absolute; - top: 0; left: 0; - width: 100%; height: 100%; - pointer-events: none; - z-index: 10; -} - -#download-btn, -#manual-match-btn, -#server-select, -#extension-select, -#sd-toggle, -.side-nav-btn { - display: none !important; -} +/* Hidden Legacy Buttons */ +#download-btn, #manual-match-btn, #server-select, #extension-select, #sd-toggle, .side-nav-btn { display: none !important; } +/* Settings Panel */ .settings-panel { - position: absolute; - bottom: 70px; - right: 20px; - z-index: 1000; - max-height: 60vh; - overflow-y: auto; + position: absolute; bottom: 70px; right: 20px; z-index: 1000; + max-height: 60vh; overflow-y: auto; background: rgba(15, 15, 15, 0.95); - border: 1px solid var(--glass-border); - border-radius: 8px; + border: 1px solid var(--glass-border); border-radius: 8px; } +/* Video Toasts */ +.video-toast-container { + position: absolute; bottom: 100px; left: 20px; z-index: 80; + display: flex; flex-direction: column; gap: 10px; + pointer-events: none; max-width: 400px; +} +.video-toast { + display: flex; align-items: center; gap: 10px; + background: rgba(15, 15, 15, 0.85); backdrop-filter: blur(8px); + padding: 8px 12px; border-radius: 8px; + border-left: 3px solid var(--brand-color); + color: white; font-size: 0.9rem; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + animation: toastSlideIn 0.3s ease forwards, toastFadeOut 0.5s ease 4.5s forwards; + pointer-events: auto; opacity: 0; +} +.toast-avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; flex-shrink: 0; } +.toast-content { display: flex; flex-direction: column; line-height: 1.2; } +.toast-user { font-weight: 700; font-size: 0.8rem; color: #a78bfa; } +.toast-msg { color: #eee; margin-top: 2px; } +.video-toast.system-toast { + border-left-color: #9ca3af; background: rgba(20, 20, 20, 0.7); + justify-content: center; padding: 6px 12px; min-height: auto; +} +.video-toast.system-toast .toast-msg { font-size: 0.85rem; font-style: italic; color: rgba(255, 255, 255, 0.8); margin: 0; } + +/* ========================================= + 7. CHAT SIDEBAR + ========================================= */ .chat-sidebar { - display: flex; - flex-direction: column; + display: flex; flex-direction: column; height: 100%; background: rgba(15, 15, 15, 0.95); - border-left: 1px solid var(--glass-border); - height: 100%; - overflow: hidden; -} -.room-layout.chat-hidden .chat-sidebar { - opacity: 0; - pointer-events: none; - border-left: none; + border-left: 1px solid var(--glass-border); overflow: hidden; } +.room-layout.chat-hidden .chat-sidebar { opacity: 0; pointer-events: none; border-left: none; } .chat-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 20px; - border-bottom: 1px solid var(--glass-border); + display: flex; justify-content: space-between; align-items: center; + padding: 16px 20px; border-bottom: 1px solid var(--glass-border); } .chat-header h3 { margin: 0; font-size: 1.1rem; font-weight: 700; color: white; } +.sidebar-tabs { + display: flex; width: 100%; flex: 0 0 auto; height: 50px; + border-bottom: 1px solid var(--glass-border); background: rgba(0,0,0,0.2); +} +.tab-btn { + flex: 1; height: 100%; padding: 0; + display: flex; align-items: center; justify-content: center; + background: transparent; border: none; border-bottom: 2px solid transparent; + color: var(--text-muted); font-weight: 600; cursor: pointer; + transition: all 0.2s; +} +.tab-btn:hover { background: rgba(255,255,255,0.05); color: white; } +.tab-btn.active { border-bottom-color: var(--brand-color); color: white; background: rgba(255,255,255,0.02); } + +.tab-content { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; } + +/* Users List */ .users-list { - padding: 12px; - border-bottom: 1px solid var(--glass-border); - max-height: 200px; - overflow-y: auto; - background: rgba(0,0,0,0.2); -} -.user-item { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 12px; - border-radius: 8px; - margin-bottom: 4px; + padding: 12px; border-bottom: 1px solid var(--glass-border); + max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.2); } +.user-item { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 8px; margin-bottom: 4px; } .user-item:hover { background: rgba(255, 255, 255, 0.05); } .user-avatar { - width: 32px; height: 32px; - border-radius: 50%; + width: 32px; height: 32px; border-radius: 50%; background: var(--brand-gradient); - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - color: white; - font-size: 0.8rem; + display: flex; align-items: center; justify-content: center; + font-weight: 700; color: white; font-size: 0.8rem; } .user-avatar img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; } .user-name { flex: 1; font-size: 0.9rem; color: white; } -.user-badge { - font-size: 0.7rem; - background: var(--brand-color); - color: white; - padding: 2px 8px; - border-radius: 4px; - font-weight: 600; -} +.user-badge { font-size: 0.7rem; background: var(--brand-color); color: white; padding: 2px 8px; border-radius: 4px; font-weight: 600; } +/* Chat Messages */ .chat-messages { - flex: 1; - overflow-y: auto; - padding: 16px; - display: flex; - flex-direction: column; - gap: 12px; - min-height: 0; + flex: 1; overflow-y: auto; padding: 16px; + display: flex; flex-direction: column; gap: 12px; min-height: 0; } .chat-message { display: flex; gap: 10px; } .chat-message.system { justify-content: center; margin: 8px 0; } .chat-message.system .message-content { - background: rgba(255, 255, 255, 0.05); - color: var(--text-muted); - font-size: 0.8rem; - text-align: center; - padding: 4px 12px; - border-radius: 12px; + background: rgba(255, 255, 255, 0.05); color: var(--text-muted); + font-size: 0.8rem; text-align: center; padding: 4px 12px; border-radius: 12px; } .message-avatar { - width: 36px; height: 36px; - border-radius: 50%; + width: 36px; height: 36px; border-radius: 50%; background: var(--brand-gradient); - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - color: white; - font-size: 0.9rem; - flex-shrink: 0; + display: flex; align-items: center; justify-content: center; + font-weight: 700; color: white; font-size: 0.9rem; flex-shrink: 0; } .message-body { flex: 1; min-width: 0; } .message-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; } .message-username { font-weight: 600; color: white; font-size: 0.9rem; } .message-time { font-size: 0.75rem; color: rgba(255, 255, 255, 0.4); } -.message-content { - color: rgba(255, 255, 255, 0.9); - font-size: 0.95rem; - line-height: 1.4; - word-wrap: break-word; -} +.message-content { color: rgba(255, 255, 255, 0.9); font-size: 0.95rem; line-height: 1.4; word-wrap: break-word; } -.chat-input { - display: flex; - gap: 8px; - padding: 16px; - border-top: 1px solid var(--glass-border); -} +.chat-input { display: flex; gap: 8px; padding: 16px; border-top: 1px solid var(--glass-border); } .chat-input input { - flex: 1; - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--glass-border); - color: white; - padding: 12px 16px; - border-radius: 10px; - outline: none; + flex: 1; background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); color: white; + padding: 12px 16px; border-radius: 10px; outline: none; } .chat-input button { - background: var(--brand-color); - border: none; - color: white; - padding: 0 16px; - border-radius: 10px; - cursor: pointer; - transition: background 0.2s; + background: var(--brand-color); border: none; color: white; + padding: 0 16px; border-radius: 10px; cursor: pointer; transition: background 0.2s; } .chat-input button:hover { background: #7c3aed; } +/* ========================================= + 8. QUEUE TAB + ========================================= */ +.queue-list { + flex: 1; overflow-y: auto; padding: 10px; + display: flex; flex-direction: column; gap: 10px; +} +.queue-item { + display: flex; gap: 10px; padding: 10px; + background: rgba(255,255,255,0.05); + border-radius: 8px; border: 1px solid transparent; + position: relative; align-items: center; transition: transform 0.2s; +} +.queue-item:hover { background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.1); } +.q-img { width: 50px; height: 70px; object-fit: cover; border-radius: 4px; flex-shrink: 0; } +.q-info { flex: 1; display: flex; flex-direction: column; justify-content: center; overflow: hidden; } +.q-title { font-weight: 700; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.q-meta { font-size: 0.8rem; color: #aaa; } +.q-actions { display: flex; flex-direction: column; gap: 4px; opacity: 0.7; transition: opacity 0.2s; } +.queue-item:hover .q-actions { opacity: 1; } + +.q-btn { + background: rgba(255,255,255,0.1); border: none; color: white; + width: 28px; height: 28px; border-radius: 6px; cursor: pointer; + display: flex; align-items: center; justify-content: center; transition: all 0.2s; +} +.q-btn:hover { background: rgba(255,255,255,0.2); transform: scale(1.1); } +.q-btn.play:hover { background: var(--brand-color); } +.q-btn.remove:hover { background: #ff6b6b; } +.badge { background: var(--brand-color); padding: 1px 6px; border-radius: 10px; font-size: 0.7rem; margin-left: 6px; } +.queue-empty { text-align: center; color: #666; margin-top: 40px; font-style: italic; } + +/* ========================================= + 9. MODALS + ========================================= */ .modal-overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.8); - backdrop-filter: blur(8px); - display: none; - align-items: center; - justify-content: center; - z-index: 10000; - opacity: 0; - transition: opacity 0.3s ease; + background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(8px); + display: none; align-items: center; justify-content: center; + z-index: 10000; opacity: 0; transition: opacity 0.3s ease; } .modal-overlay.show { display: flex; opacity: 1; } .modal-content { background: rgba(20, 20, 20, 0.95); - border: 1px solid var(--glass-border); - border-radius: 16px; - padding: 32px; - max-width: 500px; - width: 90%; - position: relative; - transform: scale(0.9); - transition: transform 0.3s ease; + border: 1px solid var(--glass-border); border-radius: 16px; + padding: 32px; max-width: 500px; width: 90%; + position: relative; transform: scale(0.9); transition: transform 0.3s ease; } .modal-overlay.show .modal-content { transform: scale(1); } .modal-title { margin: 0 0 24px 0; font-size: 1.5rem; font-weight: 800; color: white; } @@ -581,35 +474,36 @@ input:focus, .form-input:focus { .form-group label { display: block; margin-bottom: 8px; font-weight: 600; color: white; } .form-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px; } +/* Join Info */ +.join-host-info { display: flex; flex-direction: column; align-items: center; margin-bottom: 24px; animation: fadeIn 0.5s ease; } +.join-avatar-container { + width: 80px; height: 80px; border-radius: 50%; padding: 3px; + background: var(--brand-gradient); margin-bottom: 12px; + box-shadow: 0 4px 15px rgba(139, 92, 246, 0.4); +} +.join-avatar-container img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; border: 3px solid #1a1a1a; background: #2a2a2a; } +.join-text { font-size: 1.1rem; color: white; font-weight: 500; text-align: center; margin: 0; } +.join-text span { font-weight: 700; color: #a78bfa; } + +/* Search Content */ .anime-search-content { max-width: 800px; max-height: 85vh; display: flex; flex-direction: column; } .search-bar { display: flex; gap: 12px; margin-bottom: 20px; } .search-bar button { - background: var(--brand-color); - border: none; color: white; padding: 12px 24px; - border-radius: 10px; font-weight: 700; cursor: pointer; + background: var(--brand-color); border: none; color: white; + padding: 12px 24px; border-radius: 10px; font-weight: 700; cursor: pointer; } .anime-results { - flex: 1; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 12px; - min-height: 0; - max-height: 60vh; - padding-right: 8px; + flex: 1; overflow-y: auto; display: flex; flex-direction: column; + gap: 12px; min-height: 0; max-height: 60vh; padding-right: 8px; } - .anime-result-item, .search-item { display: flex; gap: 16px; padding: 12px; background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 12px; - cursor: pointer; text-decoration: none; color: inherit; - transition: all 0.2s; + border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; + cursor: pointer; text-decoration: none; color: inherit; transition: all 0.2s; } .anime-result-item:hover { - background: rgba(255, 255, 255, 0.08); - border-color: var(--brand-color); + background: rgba(255, 255, 255, 0.08); border-color: var(--brand-color); transform: translateX(4px); } .search-poster { width: 60px; height: 85px; border-radius: 8px; object-fit: cover; flex-shrink: 0; } @@ -617,6 +511,7 @@ input:focus, .form-input:focus { .search-title { font-weight: 700; color: white; margin-bottom: 4px; font-size: 1rem; } .search-meta { font-size: 0.85rem; color: var(--text-muted); } +/* Config Layout */ .config-layout { display: flex; gap: 24px; margin-top: 20px; } .config-sidebar { width: 140px; flex-shrink: 0; display: flex; flex-direction: column; align-items: center; gap: 12px; } .config-cover { @@ -631,88 +526,100 @@ input:focus, .form-input:focus { margin-bottom: 8px; font-weight: 700; } -input[type=number]::-webkit-inner-spin-button, -input[type=number]::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; +/* Chips & Toggles in Modal */ +.chips-grid { display: flex; flex-wrap: wrap; gap: 10px; } +.chip { + padding: 8px 16px; background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; + color: #ccc; font-size: 0.9rem; cursor: pointer; + transition: all 0.2s; user-select: none; } -input[type=number] { - -moz-appearance: textfield; -} - -.ep-control { - display: flex; - align-items: center; - justify-content: space-between; - background: rgba(0, 0, 0, 0.4); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 12px; - padding: 4px; - width: 100%; - height: 48px; - margin-top: 8px; - box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); -} - -.ep-btn { - width: 40px; - height: 38px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255,255,255,0.05); - color: white; - border-radius: 8px; - cursor: pointer; - font-size: 1.2rem; - transition: all 0.2s ease; -} - -.ep-btn:hover { - background: var(--brand-color); - border-color: var(--brand-color); - transform: translateY(-1px); -} - -.ep-btn:active { - transform: translateY(1px); -} - -.ep-input { - flex: 1; - min-width: 0; - background: transparent; - border: none; - color: white; - text-align: center; - font-size: 1.2rem; - font-weight: 800; - outline: none; - font-family: monospace; -} - -.ep-input:focus { - background: transparent; - border: none; -} - -.cat-toggle { - display: flex; background: rgba(0,0,0,0.3); - padding: 4px; border-radius: 10px; width: fit-content; +.chip:hover { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.3); transform: translateY(-2px); } +.chip.active { + background: var(--brand-color); border-color: var(--brand-color); + color: white; box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); } +.cat-toggle { display: flex; background: rgba(0,0,0,0.3); padding: 4px; border-radius: 10px; width: fit-content; } .cat-opt { - padding: 6px 16px; border-radius: 8px; - font-size: 0.85rem; color: #888; + padding: 6px 16px; border-radius: 8px; font-size: 0.85rem; color: #888; cursor: pointer; transition: all 0.2s; font-weight: 600; } .cat-opt.active { background: rgba(255,255,255,0.15); color: white; } .cat-opt.disabled { opacity: 0.3; pointer-events: none; text-decoration: line-through; } -.grid-loader { - width: 100%; padding: 20px; text-align: center; - color: var(--text-muted); font-size: 0.9rem; +/* Episodes Grid */ +.episodes-grid-container { + display: grid; grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)); + gap: 8px; max-height: 280px; overflow-y: auto; padding: 10px; + background: rgba(0, 0, 0, 0.2); border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.05); +} +.episodes-grid-container::-webkit-scrollbar { width: 4px; } +.episodes-grid-container::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); } + +.ep-grid-btn { + background: rgba(255,255,255,0.03); border: 1px solid rgba(255, 255, 255, 0.1); + color: #ccc; padding: 8px 0; border-radius: 6px; text-align: center; + font-size: 0.9rem; font-weight: 600; cursor: pointer; + transition: all 0.2s; user-select: none; +} +.ep-grid-btn:hover { background: white; color: black; transform: translateY(-2px); } +.ep-grid-btn.selected, .ep-grid-btn.active { + background: var(--brand-color); color: white; + border-color: var(--brand-color); box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); +} + +.btn-text-tiny { + background: transparent; border: none; color: var(--brand-color); + font-size: 0.75rem; font-weight: 700; cursor: pointer; padding: 2px 6px; +} +.btn-text-tiny:hover { text-decoration: underline; } + +/* Manual Control */ +.ep-control { + display: flex; align-items: center; justify-content: space-between; + background: rgba(0, 0, 0, 0.4); border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 12px; padding: 4px; width: 100%; height: 48px; + margin-top: 8px; box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); +} +.ep-btn { + width: 40px; height: 38px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; + background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255,255,255,0.05); + color: white; border-radius: 8px; cursor: pointer; font-size: 1.2rem; transition: all 0.2s ease; +} +.ep-btn:hover { background: var(--brand-color); border-color: var(--brand-color); transform: translateY(-1px); } +.ep-btn:active { transform: translateY(1px); } +.ep-input { + flex: 1; min-width: 0; background: transparent; border: none; + color: white; text-align: center; font-size: 1.2rem; font-weight: 800; + outline: none; font-family: monospace; +} +.grid-loader { width: 100%; padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.9rem; } + +/* Modal Pagination */ +.modal-pagination { + display: flex; justify-content: center; align-items: center; gap: 15px; + margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.05); +} +.modal-page-btn { + background: transparent; border: 1px solid rgba(255, 255, 255, 0.2); + color: white; width: 30px; height: 30px; border-radius: 6px; + display: flex; align-items: center; justify-content: center; + cursor: pointer; transition: all 0.2s; +} +.modal-page-btn:hover:not(:disabled) { background: white; color: black; } +.modal-page-btn:disabled { opacity: 0.3; cursor: not-allowed; } +.modal-page-info { font-size: 0.85rem; color: #888; font-weight: 700; } + +/* ========================================= + 10. MEDIA QUERIES (Combined) + ========================================= */ +@media (max-width: 1200px) { + .room-layout { grid-template-columns: 1fr; grid-template-rows: 1fr auto; } + .room-layout.chat-hidden { grid-template-rows: 1fr 0px; } + .chat-sidebar { + height: 350px; border-left: none; border-top: 1px solid var(--glass-border); + } } @media (max-width: 600px) { @@ -720,437 +627,4 @@ input[type=number] { .config-sidebar { width: 100%; flex-direction: row; } .config-cover { width: 80px; } .ep-control { width: auto; flex: 1; } -} - -.container { max-width: 1400px; margin: 0 auto; padding: 80px 40px 40px; } -.header h1 { - font-size: 2.5rem; font-weight: 800; - background: var(--brand-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} -.rooms-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 24px; -} -.room-card { - background: var(--glass-bg); - border: 1px solid var(--glass-border); - border-radius: 16px; padding: 24px; - backdrop-filter: var(--glass-blur); - cursor: pointer; transition: all 0.3s; -} -.room-card:hover { - transform: translateY(-4px); - border-color: var(--brand-color); - box-shadow: 0 8px 32px rgba(139, 92, 246, 0.3); -} -.room-card-title { font-size: 1.25rem; font-weight: 700; color: white; margin-bottom: 4px; } -.room-card-host { font-size: 0.85rem; color: var(--text-muted); } -.room-card-footer { - display: flex; justify-content: space-between; - align-items: center; padding-top: 16px; - border-top: 1px solid rgba(255, 255, 255, 0.1); margin-top: 16px; -} - -.join-host-info { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 24px; - animation: fadeIn 0.5s ease; -} - -.join-avatar-container { - width: 80px; - height: 80px; - border-radius: 50%; - padding: 3px; - background: var(--brand-gradient); - margin-bottom: 12px; - box-shadow: 0 4px 15px rgba(139, 92, 246, 0.4); -} - -.join-avatar-container img { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; - border: 3px solid #1a1a1a; - background: #2a2a2a; -} - -.join-text { - font-size: 1.1rem; - color: white; - font-weight: 500; - text-align: center; - margin: 0; -} - -.join-text span { - font-weight: 700; - color: #a78bfa; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(-10px); } - to { opacity: 1; transform: translateY(0); } -} - -.video-toast-container { - position: absolute; - bottom: 100px; - left: 20px; - z-index: 80; - display: flex; - flex-direction: column; - gap: 10px; - pointer-events: none; - max-width: 400px; -} - -.video-toast { - display: flex; - align-items: center; - gap: 10px; - background: rgba(15, 15, 15, 0.85); - backdrop-filter: blur(8px); - padding: 8px 12px; - border-radius: 8px; - border-left: 3px solid var(--brand-color); - color: white; - font-size: 0.9rem; - box-shadow: 0 4px 12px rgba(0,0,0,0.3); - - animation: toastSlideIn 0.3s ease forwards, toastFadeOut 0.5s ease 4.5s forwards; - pointer-events: auto; - opacity: 0; -} - -.toast-avatar { - width: 28px; - height: 28px; - border-radius: 50%; - object-fit: cover; - flex-shrink: 0; -} - -.toast-content { - display: flex; - flex-direction: column; - line-height: 1.2; -} - -.toast-user { - font-weight: 700; - font-size: 0.8rem; - color: #a78bfa; -} - -.toast-msg { - color: #eee; - margin-top: 2px; -} - -#toggle-chat-btn { - position: relative; -} - -#toggle-chat-btn.has-unread::after { - content: ''; - position: absolute; - top: 2px; - right: 2px; - width: 10px; - height: 10px; - background-color: #ef4444; - border: 2px solid #1a1a1a; - border-radius: 50%; - animation: pulse 2s infinite; -} - -@keyframes toastSlideIn { - from { opacity: 0; transform: translateX(-20px); } - to { opacity: 1; transform: translateX(0); } -} - -@keyframes toastFadeOut { - from { opacity: 1; transform: translateY(0); } - to { opacity: 0; transform: translateY(-10px); visibility: hidden; } -} - -@keyframes pulse { - 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } - 70% { transform: scale(1); box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); } - 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } -} - -.video-toast.system-toast { - border-left-color: #9ca3af; - background: rgba(20, 20, 20, 0.7); - justify-content: center; - padding: 6px 12px; - min-height: auto; -} - -.video-toast.system-toast .toast-msg { - font-size: 0.85rem; - font-style: italic; - color: rgba(255, 255, 255, 0.8); - margin: 0; -} - -@keyframes slideUp { - from { - transform: translateX(-50%) translateY(20px); - opacity: 0; - } - to { - transform: translateX(-50%) translateY(0); - opacity: 1; - } -} - -@keyframes fadeOut { - from { - opacity: 1; - } - to { - opacity: 0; - } -} - -@keyframes slideUp { - from { - transform: translateX(-50%) translateY(20px); - opacity: 0; - } - to { - transform: translateX(-50%) translateY(0); - opacity: 1; - } -} - -@keyframes slideDown { - from { - transform: translateX(-50%) translateY(-20px); - opacity: 0; - } - to { - transform: translateX(-50%) translateY(0); - opacity: 1; - } -} - -@keyframes fadeOut { - from { - opacity: 1; - } - to { - opacity: 0; - } -} - -#copy-invite-btn { - transition: all 0.3s ease; -} - -#copy-invite-btn:hover { - transform: scale(1.05); -} - -#copy-invite-btn:active { - transform: scale(0.95); -} - -@keyframes scaleIn { - from { - transform: translate(-50%, -50%) scale(0.8); - opacity: 0; - } - to { - transform: translate(-50%, -50%) scale(1); - opacity: 1; - } -} - -.sidebar-tabs { - display: flex; - border-bottom: 1px solid var(--glass-border); - background: rgba(0,0,0,0.2); -} - -.tab-btn { - flex: 1; - background: transparent; - border: none; - color: var(--text-muted); - padding: 14px; - font-weight: 600; - cursor: pointer; - border-bottom: 2px solid transparent; - transition: all 0.2s; -} - -.tab-btn:hover { color: white; background: rgba(255,255,255,0.05); } -.tab-btn.active { color: white; border-bottom-color: var(--brand-color); background: rgba(255,255,255,0.02); } - -.tab-content { - display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; -} - -.queue-list { - padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; -} - -.queue-item { - display: flex; gap: 10px; padding: 10px; - background: rgba(255,255,255,0.05); border-radius: 8px; - border: 1px solid transparent; position: relative; -} - -.queue-item:hover { background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.1); } - -.q-img { width: 50px; height: 70px; object-fit: cover; border-radius: 4px; } -.q-info { flex: 1; display: flex; flex-direction: column; justify-content: center; overflow: hidden; } -.q-title { font-weight: 700; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.q-meta { font-size: 0.8rem; color: #aaa; } -.q-remove { - position: absolute; top: 5px; right: 5px; - background: rgba(0,0,0,0.5); border: none; color: #ff6b6b; - width: 24px; height: 24px; border-radius: 4px; cursor: pointer; - display: flex; align-items: center; justify-content: center; -} -.q-remove:hover { background: #ff6b6b; color: white; } - -.badge { background: var(--brand-color); padding: 1px 6px; border-radius: 10px; font-size: 0.7rem; margin-left: 6px; } -.queue-empty { text-align: center; color: #666; margin-top: 40px; font-style: italic; } - -.chat-sidebar-wrapper { - display: flex; - flex-direction: column; - height: 100vh; - background: rgba(15, 15, 15, 0.95); - border-left: 1px solid var(--glass-border); - overflow: hidden; -} - -@media (max-width: 1200px) { - .chat-sidebar-wrapper { - height: 400px; - border-left: none; - border-top: 1px solid var(--glass-border); - } -} - -.queue-item { - display: flex; - gap: 10px; - padding: 10px; - background: rgba(255,255,255,0.05); - border-radius: 8px; - border: 1px solid transparent; - position: relative; - align-items: center; - transition: transform 0.2s; -} - -.queue-item:hover { - background: rgba(255,255,255,0.08); - border-color: rgba(255,255,255,0.1); -} - -.q-img { width: 50px; height: 70px; object-fit: cover; border-radius: 4px; flex-shrink: 0; } -.q-info { flex: 1; display: flex; flex-direction: column; justify-content: center; overflow: hidden; } - -.q-actions { - display: flex; - flex-direction: column; - gap: 4px; - opacity: 0.7; - transition: opacity 0.2s; -} -.queue-item:hover .q-actions { opacity: 1; } - -.q-btn { - background: rgba(255,255,255,0.1); - border: none; - color: white; - width: 28px; - height: 28px; - border-radius: 6px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s; -} - -.q-btn:hover { background: rgba(255,255,255,0.2); transform: scale(1.1); } -.q-btn.play:hover { background: var(--brand-color); } -.q-btn.remove:hover { background: #ff6b6b; } - -.chat-sidebar { - display: flex; - flex-direction: column; - height: 100%; - background: rgba(15, 15, 15, 0.95); - border-left: 1px solid var(--glass-border); - overflow: hidden; -} - -.sidebar-tabs { - display: flex; - width: 100%; - flex: 0 0 auto; - height: 50px; - border-bottom: 1px solid var(--glass-border); - background: rgba(0,0,0,0.2); -} - -.tab-btn { - flex: 1; - height: 100%; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - background: transparent; - border: none; - color: var(--text-muted); - font-weight: 600; - cursor: pointer; - border-bottom: 2px solid transparent; - transition: all 0.2s; -} - -.tab-btn:hover { background: rgba(255,255,255,0.05); color: white; } -.tab-btn.active { border-bottom-color: var(--brand-color); color: white; background: rgba(255,255,255,0.02); } - -.tab-content { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - overflow: hidden; -} - -.queue-list { - flex: 1; - overflow-y: auto; - padding: 10px; - display: flex; - flex-direction: column; - gap: 10px; -} - -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 16px; - display: flex; - flex-direction: column; - gap: 12px; } \ No newline at end of file diff --git a/desktop/views/room.html b/desktop/views/room.html index d72de7b..9e9473e 100644 --- a/desktop/views/room.html +++ b/desktop/views/room.html @@ -13,7 +13,8 @@ - + + @@ -23,7 +24,7 @@ WaifuBoard
- +
@@ -51,23 +52,32 @@
Dub
- +
- - @@ -80,7 +90,6 @@ - +
Cover - -
-
Episode
-
- - - -
-
-
-
-
-
Source
-
+
+ +
+ +
+
Select Episodes
+
+ +
-
-
Audio
- -
-
Select Server
-
-
Select a source first
+ + +
+ + +
- - - -
- - - -
diff --git a/docker/src/api/rooms/rooms.service.ts b/docker/src/api/rooms/rooms.service.ts index 193095b..4d6cf95 100644 --- a/docker/src/api/rooms/rooms.service.ts +++ b/docker/src/api/rooms/rooms.service.ts @@ -10,6 +10,15 @@ interface RoomUser { userId?: number; } +interface SourceContext { + animeId: string; + episode: string | number; + source: string; + extension: string; + server: string; + category: string; +} + export interface QueueItem { uid: string; metadata: RoomMetadata; @@ -23,6 +32,7 @@ interface RoomMetadata { episode: number; image?: string; source?: string; + malId?: number; } interface RoomData { @@ -38,6 +48,7 @@ interface RoomData { videoData?: any; currentTime: number; isPlaying: boolean; + context?: SourceContext; } | null; password?: string; metadata?: RoomMetadata | null; @@ -168,7 +179,10 @@ export function updateRoomVideo(roomId: string, videoData: any): boolean { const room = rooms.get(roomId); if (!room) return false; - room.currentVideo = videoData; + room.currentVideo = { + ...room.currentVideo, + ...videoData + }; return true; } diff --git a/docker/src/api/rooms/rooms.websocket.ts b/docker/src/api/rooms/rooms.websocket.ts index 379b16c..f50bb63 100644 --- a/docker/src/api/rooms/rooms.websocket.ts +++ b/docker/src/api/rooms/rooms.websocket.ts @@ -303,14 +303,34 @@ function handleMessage(roomId: string, userId: string, data: any) { broadcastToRoom(roomId, { type: 'video_update', video: data.video, - metadata: data.metadata // ✅ CLAVE + metadata: data.metadata }); - break; + case 'queue_add_batch': + if (room.host.id !== userId) return; + + if (Array.isArray(data.items)) { + // Añadimos el índice (i) al forEach + data.items.forEach((item: any, i: number) => { + const newItem = { + // Añadimos el índice '_${i}' al UID para garantizar unicidad en milisegundos + uid: `q_${Date.now()}_${i}_${Math.random().toString(36).substr(2, 5)}`, + metadata: item.metadata, + videoData: item.video, + addedBy: room.users.get(userId)?.username || 'Unknown' + }; + roomService.addQueueItem(roomId, newItem); + }); + + broadcastToRoom(roomId, { + type: 'queue_update', + queue: room.queue + }); + } + break; case 'sync': - // Solo el host puede hacer sync if (room.host.id !== userId) return; if (room.currentVideo) { diff --git a/docker/src/scripts/anime/player.js b/docker/src/scripts/anime/player.js index c672fb4..a694675 100644 --- a/docker/src/scripts/anime/player.js +++ b/docker/src/scripts/anime/player.js @@ -78,13 +78,11 @@ const AnimePlayer = (function() { initElements(); setupEventListeners(); - // In Room Mode, we show the player immediately and hide extra controls if (_roomMode) { if(els.playerWrapper) { els.playerWrapper.style.display = 'block'; els.playerWrapper.classList.add('room-mode'); } - // Hide extension list loading in room mode } else { loadExtensionsList(); } @@ -128,10 +126,8 @@ const AnimePlayer = (function() { els.progressBuffer = document.querySelector('.progress-buffer'); els.progressHandle = document.querySelector('.progress-handle'); - // Subtitles canvas els.subtitlesCanvas = document.getElementById('subtitles-canvas'); - // Create skip button if not exists if (!document.getElementById('skip-overlay-btn')) { const btn = document.createElement('button'); btn.id = 'skip-overlay-btn'; @@ -143,13 +139,11 @@ const AnimePlayer = (function() { } function setupEventListeners() { - // Close player if(!_roomMode) { const closeBtn = document.getElementById('close-player-btn'); if(closeBtn) closeBtn.addEventListener('click', closePlayer); } - // Episode navigation if(els.prevBtn) els.prevBtn.onclick = () => playEpisode(_currentEpisode - 1); if(els.nextBtn) els.nextBtn.onclick = () => playEpisode(_currentEpisode + 1); @@ -203,6 +197,15 @@ const AnimePlayer = (function() { return; } + if (videoData.malId) _malId = videoData.malId; + if (videoData.episode) _currentEpisode = parseInt(videoData.episode); + + _skipIntervals = []; + if (els.progressContainer) { + els.progressContainer.querySelectorAll('.skip-range, .skip-cut').forEach(e => e.remove()); + } + if (_skipBtn) _skipBtn.classList.remove('visible'); + _currentSubtitles = videoData.subtitles || []; if (els.loader) els.loader.style.display = 'none'; diff --git a/docker/src/scripts/room.js b/docker/src/scripts/room.js index 3aa60a7..062441b 100644 --- a/docker/src/scripts/room.js +++ b/docker/src/scripts/room.js @@ -5,10 +5,16 @@ const RoomsApp = (function() { let currentUsername = null; let isHost = false; let isGuest = false; - let hlsInstance = null; let selectedAnimeData = null; let currentLoadedUrl = null; let extensionsReady = false; + let currentAnimeDetails = null; + let selectedEpisodes = new Set([1]); + let lastSelectedEp = 1; + let modalCurrentPage = 1; + let modalTotalEpisodes = 0; + const MODAL_EPS_PER_PAGE = 50; + let currentQueue = []; let configState = { extension: null, @@ -17,6 +23,13 @@ const RoomsApp = (function() { episode: 1 }; + let activeContext = { + extension: null, + server: null, + category: 'sub', + episode: 1 + }; + let extensionsStore = { list: [], settings: {} @@ -29,6 +42,7 @@ const RoomsApp = (function() { roomViewers: document.getElementById('room-viewers'), selectAnimeBtn: document.getElementById('select-anime-btn'), toggleChatBtn: document.getElementById('toggle-chat-btn'), + copyInviteBtn: document.getElementById('copy-invite-btn'), // NUEVO // Host Quick Controls (Header) hostControls: document.getElementById('host-controls'), @@ -60,46 +74,59 @@ const RoomsApp = (function() { chatInput: document.getElementById('chat-input'), roomLayout: document.getElementById('room-layout'), - // Modals + // Join Room Modal Elements joinRoomModal: document.getElementById('join-room-modal'), joinRoomForm: document.getElementById('join-room-form'), + guestNameInput: document.getElementById('guest-name-input'), + joinPasswordInput: document.getElementById('join-password-input'), + passwordGroup: document.getElementById('password-group'), + cancelJoinBtn: document.getElementById('cancel-join-btn'), + joinHostInfo: document.getElementById('join-host-info'), + joinHostAvatar: document.getElementById('join-host-avatar'), + joinHostText: document.getElementById('join-host-text'), + + // Anime Search Modal animeSearchModal: document.getElementById('anime-search-modal'), animeSearchInput: document.getElementById('anime-search-input'), animeResults: document.getElementById('anime-results'), + closeSearchBtn: document.getElementById('close-search-modal'), + animeSearchBtn: document.getElementById('anime-search-btn'), - // Config Elements (Modal) + // Config Elements (Stream Configuration Modal) stepSearch: document.getElementById('step-search'), stepConfig: document.getElementById('step-config'), backToSearchBtn: document.getElementById('back-to-search'), configTitle: document.getElementById('selected-anime-title'), configCover: document.getElementById('config-cover'), + + // Selects del Modal (Legacy/Manual) selExtension: document.getElementById('sel-extension'), selServer: document.getElementById('sel-server'), selCategory: document.getElementById('sel-category'), + + // Controles de Configuración + extContainer: document.getElementById('ext-chips-container'), + serverContainer: document.getElementById('server-chips-container'), + modalSdToggle: document.getElementById('modal-sd-toggle'), + + // Episode Controls inpEpisode: document.getElementById('inp-episode'), + epInc: document.getElementById('ep-inc'), + epDec: document.getElementById('ep-dec'), + + // Launch Actions btnLaunch: document.getElementById('btn-launch-stream'), configError: document.getElementById('config-error'), + btnAddQueue: document.getElementById('btn-add-queue'), + // Toasts & Tabs toastContainer: document.getElementById('video-toast-container'), - tabChatBtn: document.getElementById('tab-chat-btn'), tabQueueBtn: document.getElementById('tab-queue-btn'), tabContentChat: document.getElementById('tab-content-chat'), tabContentQueue: document.getElementById('tab-content-queue'), queueList: document.getElementById('queue-list'), queueCount: document.getElementById('queue-count'), - btnAddQueue: document.getElementById('btn-add-queue'), - }; - - const ui = { - extContainer: document.getElementById('ext-chips-container'), - serverContainer: document.getElementById('server-chips-container'), - sdToggle: document.getElementById('modal-sd-toggle'), - epInput: document.getElementById('inp-episode'), - epInc: document.getElementById('ep-inc'), - epDec: document.getElementById('ep-dec'), - launchBtn: document.getElementById('btn-launch-stream'), - configError: document.getElementById('config-error') }; async function init() { @@ -113,7 +140,6 @@ const RoomsApp = (function() { currentRoomId = urlRoomId; if (window.AnimePlayer) { - console.log('Initializing AnimePlayer in room mode'); window.AnimePlayer.init(null, 'anilist', false, {}, true); } @@ -196,16 +222,61 @@ const RoomsApp = (function() { if (elements.selectAnimeBtn) elements.selectAnimeBtn.onclick = openAnimeSearchModal; if (elements.toggleChatBtn) elements.toggleChatBtn.onclick = toggleChat; - if (elements.leaveRoomBtn) elements.leaveRoomBtn.onclick = leaveRoom; - if (elements.roomExtSelect) elements.roomExtSelect.onchange = (e) => onQuickExtensionChange(e, false); - if (elements.roomServerSelect) elements.roomServerSelect.onchange = onQuickServerChange; + if (elements.roomExtSelect) elements.roomExtSelect.onchange = (e) => { + onQuickExtensionChange(e, false).then(() => {}); + }; if (elements.roomServerSelect) elements.roomServerSelect.onchange = onQuickServerChange; elements.tabChatBtn.onclick = () => switchTab('chat'); elements.tabQueueBtn.onclick = () => switchTab('queue'); - if(elements.btnAddQueue) elements.btnAddQueue.onclick = () => launchStream(true, true); + if (elements.btnAddQueue) { + elements.btnAddQueue.onclick = () => { + if (selectedEpisodes.size === 0) return; + const sortedEps = Array.from(selectedEpisodes).sort((a, b) => a - b); + + const currentProvider = elements.roomExtSelect ? elements.roomExtSelect.value : 'gogoanime'; + + const originalText = elements.btnAddQueue.innerHTML; + elements.btnAddQueue.disabled = true; + elements.btnAddQueue.textContent = "Adding..."; + + const itemsBatch = sortedEps.map(ep => ({ + metadata: { + id: currentAnimeDetails.id, + title: currentAnimeDetails.title.userPreferred || currentAnimeDetails.title.english, + episode: ep, + image: currentAnimeDetails.coverImage?.large, + source: 'anilist', + malId: currentAnimeDetails.idMal + }, + video: { + animeId: currentAnimeDetails.id, + episode: ep, + title: currentAnimeDetails.title.userPreferred, + extension: currentProvider, + server: elements.roomServerSelect?.value || 'gogoanime', + category: elements.roomSdToggle?.getAttribute('data-state') || 'sub' + } + })); + + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'queue_add_batch', + items: itemsBatch + })); + closeAnimeSearchModal(); + } else { + console.error("WebSocket not connected"); + } + + setTimeout(() => { + elements.btnAddQueue.disabled = false; + elements.btnAddQueue.innerHTML = originalText; + }, 500); + }; + } if (elements.roomSdToggle) { elements.roomSdToggle.onclick = () => { if (!isHost) return; @@ -243,7 +314,7 @@ const RoomsApp = (function() { }; } if (elements.selExtension) elements.selExtension.onchange = handleModalExtensionChange; - if (elements.btnLaunch) elements.btnLaunch.onclick = () => launchStream(true); // true = from modal + if (elements.btnLaunch) elements.btnLaunch.onclick = () => launchStream(true); if (elements.toggleUsersBtn) elements.toggleUsersBtn.onclick = toggleUsersList; @@ -284,15 +355,18 @@ const RoomsApp = (function() { await onQuickExtensionChange(null, true); } - function onQuickExtensionChange(e, silent = false) { + async function onQuickExtensionChange(e, silent = false) { if (!isHost) return; const ext = elements.roomExtSelect.value; const settings = extensionsStore.settings[ext]; - if (!settings) return; + + if (!settings) { + console.error("Settings not loaded for extension:", ext); + return; + } elements.roomServerSelect.innerHTML = ''; - const servers = settings.episodeServers?.length ? settings.episodeServers : ['Default']; @@ -304,11 +378,24 @@ const RoomsApp = (function() { elements.roomServerSelect.appendChild(opt); }); - elements.roomServerSelect.value = servers[0]; + const defaultServer = servers[0]; + elements.roomServerSelect.value = defaultServer; - handleDubUI(settings, 'header'); + if (typeof handleDubUI === 'function') { + handleDubUI(settings, 'header'); + } - if (!silent) onQuickServerChange(); + if (typeof activeContext !== 'undefined') { + activeContext.extension = ext; + activeContext.server = defaultServer; + } + + if (!silent) { + await launchStream(false, false, { + forceExtension: ext, + forceServer: defaultServer + }); + } } function handleDubUI(settings, context = 'header') { @@ -333,7 +420,7 @@ const RoomsApp = (function() { } if (context === 'modal') { - const dubBtn = ui.sdToggle?.querySelector('[data-val="dub"]'); + const dubBtn = elements.modalSdToggle?.querySelector('[data-val="dub"]'); if (!dubBtn) return; dubBtn.classList.toggle('disabled', !supportsDub); @@ -347,8 +434,6 @@ const RoomsApp = (function() { function onQuickServerChange() { if (!isHost) return; - if (!elements.roomExtSelect.value) return; - if (!elements.roomServerSelect.value) return; launchStream(false); } @@ -379,50 +464,47 @@ const RoomsApp = (function() { image: img, source: 'anilist' }; + const animeResultObj = { + id: id, + title: title, + cover: img + }; showConfigStep(); + onAnimeSelected(animeResultObj); } } function showConfigStep() { if (!selectedAnimeData) return; - if (!extensionsReady) return; elements.configTitle.textContent = selectedAnimeData.title; elements.configCover.src = selectedAnimeData.image; - if(ui.configError) ui.configError.style.display = 'none'; + if(elements.configError) elements.configError.style.display = 'none'; configState.episode = 1; - configState.server = null; - configState.category = 'sub'; - configState.extension = null; + if(elements.inpEpisode) elements.inpEpisode.value = 1; - if(ui.epInput) ui.epInput.value = 1; - if(ui.launchBtn) ui.launchBtn.disabled = true; - if(elements.btnAddQueue) elements.btnAddQueue.disabled = true; - updateSDUI(); + if(elements.btnLaunch) elements.btnLaunch.disabled = false; + if(elements.btnAddQueue) elements.btnAddQueue.disabled = false; setupConfigListeners(); - renderExtensionChips(); - elements.stepSearch.style.display = 'none'; elements.stepConfig.style.display = 'block'; } function setupConfigListeners() { - if(ui.epInc) ui.epInc.onclick = () => { - ui.epInput.value = parseInt(ui.epInput.value || 0) + 1; - configState.episode = ui.epInput.value; + elements.epInc.onclick = () => { + elements.inpEpisode.value = parseInt(elements.inpEpisode.value || 0) + 1; }; - if(ui.epDec) ui.epDec.onclick = () => { - if(ui.epInput.value > 1) ui.epInput.value = parseInt(ui.epInput.value) - 1; - configState.episode = ui.epInput.value; + elements.epDec.onclick = () => { + if(elements.inpEpisode.value > 1) elements.inpEpisode.value = parseInt(elements.inpEpisode.value) - 1; }; - if(ui.epInput) ui.epInput.onchange = (e) => configState.episode = e.target.value; + if(elements.inpEpisode) elements.inpEpisode.onchange = (e) => configState.episode = e.target.value; - if(ui.sdToggle) { - ui.sdToggle.querySelectorAll('.cat-opt').forEach(opt => { + if(elements.modalSdToggle) { + elements.modalSdToggle.querySelectorAll('.cat-opt').forEach(opt => { opt.onclick = () => { if(opt.classList.contains('disabled')) return; configState.category = opt.dataset.val; @@ -434,49 +516,16 @@ const RoomsApp = (function() { } function updateSDUI() { - if(!ui.sdToggle) return; - ui.sdToggle.querySelectorAll('.cat-opt').forEach(opt => { + if(!elements.modalSdToggle) return; + elements.modalSdToggle.querySelectorAll('.cat-opt').forEach(opt => { opt.classList.toggle('active', opt.dataset.val === configState.category); }); } - function renderExtensionChips() { - ui.extContainer.innerHTML = ''; - - if (!extensionsStore.list || extensionsStore.list.length === 0) { - ui.extContainer.innerHTML = 'No extensions'; - return; - } - - extensionsStore.list.forEach(ext => { - const chip = document.createElement('div'); - chip.className = 'chip'; - chip.textContent = ext.charAt(0).toUpperCase() + ext.slice(1); - - if (!configState.extension && ext === 'anilist') configState.extension = 'anilist'; - if (ext === configState.extension) chip.classList.add('active'); - - chip.onclick = () => { - document.querySelectorAll('#ext-chips-container .chip').forEach(c => c.classList.remove('active')); - chip.classList.add('active'); - - configState.extension = ext; - configState.server = null; - ui.launchBtn.disabled = true; - if(elements.btnAddQueue) elements.btnAddQueue.disabled = true; - - loadServersForExtension(ext); - }; - ui.extContainer.appendChild(chip); - }); - - if(configState.extension) loadServersForExtension(configState.extension); - } - async function loadServersForExtension(extName) { if (!extensionsReady) return; - ui.serverContainer.innerHTML = '
Loading servers...
'; - ui.launchBtn.disabled = true; + elements.serverContainer.innerHTML = '
Loading servers...
'; + elements.btnLaunch.disabled = true; if(elements.btnAddQueue) elements.btnAddQueue.disabled = true; try { @@ -489,12 +538,12 @@ const RoomsApp = (function() { renderServerChips(servers); - const dubBtn = ui.sdToggle.querySelector('[data-val="dub"]'); + const dubBtn = elements.modalSdToggle.querySelector('[data-val="dub"]'); if (dubBtn) { if (!settings.supportsDub) { dubBtn.classList.add('disabled'); if(configState.category === 'dub') { - configState.category = 'sub'; // Forzar sub si no hay dub + configState.category = 'sub'; updateSDUI(); } } else { @@ -504,12 +553,12 @@ const RoomsApp = (function() { } catch (e) { console.error("Error loading settings", e); - ui.serverContainer.innerHTML = '
Error loading servers
'; + elements.serverContainer.innerHTML = '
Error loading servers
'; } } function renderServerChips(servers) { - ui.serverContainer.innerHTML = ''; + elements.serverContainer.innerHTML = ''; servers.forEach(srv => { const chip = document.createElement('div'); @@ -521,11 +570,11 @@ const RoomsApp = (function() { chip.classList.add('active'); configState.server = srv; - ui.launchBtn.disabled = false; + elements.btnLaunch.disabled = false; if(elements.btnAddQueue) elements.btnAddQueue.disabled = false; }; - ui.serverContainer.appendChild(chip); + elements.serverContainer.appendChild(chip); }); } @@ -570,67 +619,51 @@ const RoomsApp = (function() { // --- STREAM LAUNCHER (Unified) --- - async function launchStream(fromModal = false, isQueueAction = false) { - if (!selectedAnimeData) { - console.warn("No anime selected data found"); + async function launchStream(fromModal = false, isQueueAction = false, overrides = {}) { + if (!selectedAnimeData) return; + + let episodeToPlay = activeContext.episode; + if (fromModal && elements.inpEpisode) episodeToPlay = elements.inpEpisode.value; + + const ext = overrides.forceExtension || elements.roomExtSelect.value || activeContext.extension; + const server = overrides.forceServer || elements.roomServerSelect.value || activeContext.server; + const category = elements.roomSdToggle.getAttribute('data-state') || activeContext.category; + + if (!ext || !server) { + console.warn("Faltan datos (ext o server) para lanzar el stream"); return; } - let ext, server, episode, category; + activeContext = { extension: ext, server, category, episode: episodeToPlay }; + selectedAnimeData.episode = parseInt(episodeToPlay); - if (fromModal) { - ext = configState.extension; - server = configState.server; - episode = configState.episode; - category = configState.category; - if(isQueueAction) { - elements.btnAddQueue.disabled = true; - elements.btnAddQueue.textContent = 'Adding...'; - } - } else { - ext = elements.roomExtSelect.value; - server = elements.roomServerSelect.value; - - let epText = elements.npEpisode.textContent.replace('Ep ', '').trim(); - - if (!epText || epText === '--' || isNaN(epText)) { - if (selectedAnimeData.episode) { - epText = selectedAnimeData.episode; - } else { - epText = "1"; - } - } - episode = epText; - - category = elements.roomSdToggle.getAttribute('data-state'); - } - - if (!ext || !server || !episode) { - console.error("Missing params:", { ext, server, episode }); - if(fromModal) { - elements.configError.textContent = "Please select an extension, server, and episode."; - elements.configError.style.display = 'block'; - } else { - alert("Please select a valid server/extension."); - } - return; + if (fromModal && isQueueAction) { + elements.btnAddQueue.disabled = true; + elements.btnAddQueue.textContent = 'Adding...'; + } else if (!isQueueAction) { + if(typeof showSystemToast === 'function') showSystemToast(`Loading ${ext}...`); } try { - const apiUrl = `/api/watch/stream?animeId=${selectedAnimeData.id}&episode=${episode}&server=${encodeURIComponent(server)}&category=${category}&ext=${ext}&source=${selectedAnimeData.source}`; + const apiUrl = `/api/watch/stream?animeId=${selectedAnimeData.id}&episode=${episodeToPlay}&server=${encodeURIComponent(server)}&category=${category}&ext=${ext}&source=anilist`; console.log('Fetching stream:', apiUrl); const res = await fetch(apiUrl); - if (!res.ok) throw new Error(`Fetch failed: ${res.status}`); + if (!res.ok) throw new Error(`Error ${res.status}: Failed to fetch stream`); const data = await res.json(); + if (data.error) throw new Error(data.error); + const source = data.videoSources?.find(s => s.type === 'm3u8') || data.videoSources?.[0]; - if (!source) throw new Error('No video source found'); + + if (!source) { + throw new Error(`No sources found on ${ext} for this episode.`); + } let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`; const headers = data.headers || {}; - if (headers['Referer'] && headers['Referer'] !== "null") proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`; + if (headers['Referer']) proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`; const subtitles = (source.subtitles || []).map(sub => ({ label: sub.language, @@ -638,17 +671,30 @@ const RoomsApp = (function() { src: `/api/proxy?url=${encodeURIComponent(sub.url)}` })); + const contextPayload = { + animeId: selectedAnimeData.id, + episode: episodeToPlay, + source: selectedAnimeData.source, + extension: ext, + server: server, + category: category + }; + const videoPayload = { - videoData: { url: proxyUrl, type: source.type || 'm3u8', headers: headers }, - subtitles: subtitles + videoData: { url: proxyUrl, type: source.type || 'm3u8', headers }, + subtitles: subtitles, + context: contextPayload, + malId: selectedAnimeData.malId, + episode: episodeToPlay }; const metaPayload = { title: selectedAnimeData.title, - episode: episode, + episode: episodeToPlay, image: selectedAnimeData.image, id: selectedAnimeData.id, - source: ext + source: ext, + malId: selectedAnimeData.malId }; if (ws && ws.readyState === WebSocket.OPEN) { @@ -658,59 +704,239 @@ const RoomsApp = (function() { video: videoPayload, metadata: metaPayload })); - - showSystemToast("Added to queue!"); - if(fromModal) closeAnimeSearchModal(); - + if(typeof showSystemToast === 'function') showSystemToast("Added to queue!"); } else { ws.send(JSON.stringify({ type: 'video_update', video: { ...videoPayload, currentTime: 0, isPlaying: true }, metadata: metaPayload })); - } - loadVideo(videoPayload.video); - updateHeaderInfo(videoPayload.metadata); - if(fromModal) { - closeAnimeSearchModal(); + loadVideo({ ...videoPayload, currentTime: 0, isPlaying: true }); + updateHeaderInfo(metaPayload); - selectedAnimeData.source = ext; - - await populateQuickControls(); - - if (extensionsStore.list.includes(ext)) { - elements.roomExtSelect.value = ext; - await onQuickExtensionChange(null, true); - elements.roomServerSelect.value = server; - - elements.roomSdToggle.setAttribute('data-state', category); - elements.roomSdToggle.querySelectorAll('.sd-option').forEach(o => - o.classList.toggle('active', o.dataset.val === category) - ); - } + if(typeof syncControlsUI === 'function') syncControlsUI(contextPayload); } } + if (fromModal) closeAnimeSearchModal(); + } catch (err) { - console.error('Stream launch error:', err); - const msg = "Error loading stream. Try another server."; - if(fromModal) { - elements.configError.textContent = msg; - elements.configError.style.display = 'block'; + console.error("Stream Launch Error:", err); + + const errorMsg = err.message || "Error loading stream."; + + if (fromModal) { + if(elements.configError) { + elements.configError.textContent = errorMsg; + elements.configError.style.display = 'block'; + } } else { - alert(msg); + if(typeof showSystemToast === 'function') { + showSystemToast(`Error: ${errorMsg}`); + } else { + alert(errorMsg); + } } } finally { - if(fromModal && isQueueAction) { + if (fromModal && isQueueAction) { elements.btnAddQueue.disabled = false; elements.btnAddQueue.textContent = '+ Add to Queue'; } } } - // --- CONNECTION & WS --- + async function onAnimeSelected(animeResult) { + const gridContainer = document.getElementById('episodes-grid'); + gridContainer.innerHTML = '
Loading info...
'; + document.getElementById('step-search').style.display = 'none'; + document.getElementById('step-config').style.display = 'block'; + + document.getElementById('config-cover').src = animeResult.cover; + document.getElementById('selected-anime-title').textContent = animeResult.title; + + selectedEpisodes.clear(); + selectedEpisodes.add(1); + updateCountBtn(); + + try { + const response = await fetch(`/api/anime/${animeResult.id}?source=anilist`); + if (!response.ok) throw new Error("Failed to load anime details"); + + const data = await response.json(); + currentAnimeDetails = data; + + if (selectedAnimeData) { + selectedAnimeData.malId = data.idMal; + } + + modalTotalEpisodes = data.episodes || 12; + modalCurrentPage = 1; + + renderModalEpisodes(); + setupModalPaginationControls(); + + } catch (error) { + console.error("Error fetching details", error); + gridContainer.innerHTML = ''; + document.querySelector('.manual-ep-input').style.display = 'block'; + document.getElementById('modal-pagination').style.display = 'none'; + } + } + + function renderModalEpisodes() { + const grid = document.getElementById('episodes-grid'); + grid.innerHTML = ''; + + const start = (modalCurrentPage - 1) * MODAL_EPS_PER_PAGE + 1; + const end = Math.min(start + MODAL_EPS_PER_PAGE - 1, modalTotalEpisodes); + + const infoText = document.getElementById('modal-page-info'); + if (infoText) infoText.textContent = `${start} - ${end} of ${modalTotalEpisodes}`; + + for (let i = start; i <= end; i++) { + const btn = document.createElement('div'); + const isSelected = selectedEpisodes.has(i); + btn.className = `ep-grid-btn ${isSelected ? 'selected' : ''}`; + btn.textContent = i; + btn.dataset.ep = i; + + btn.onclick = (e) => toggleEpisode(i, btn, e.shiftKey); + + grid.appendChild(btn); + } + + document.getElementById('modal-prev-page').disabled = modalCurrentPage === 1; + document.getElementById('modal-next-page').disabled = end >= modalTotalEpisodes; + + document.getElementById('modal-pagination').style.display = modalTotalEpisodes > MODAL_EPS_PER_PAGE ? 'flex' : 'none'; + document.querySelector('.manual-ep-input').style.display = 'none'; + } + + function setupModalPaginationControls() { + document.getElementById('modal-prev-page').onclick = () => { + if (modalCurrentPage > 1) { + modalCurrentPage--; + renderModalEpisodes(); + } + }; + + document.getElementById('modal-next-page').onclick = () => { + const maxPage = Math.ceil(modalTotalEpisodes / MODAL_EPS_PER_PAGE); + if (modalCurrentPage < maxPage) { + modalCurrentPage++; + renderModalEpisodes(); + } + }; + + document.getElementById('select-all-page').onclick = () => { + const start = (modalCurrentPage - 1) * MODAL_EPS_PER_PAGE + 1; + const end = Math.min(start + MODAL_EPS_PER_PAGE - 1, modalTotalEpisodes); + for(let i=start; i<=end; i++) selectedEpisodes.add(i); + renderModalEpisodes(); + updateCountBtn(); + }; + + document.getElementById('select-none-eps').onclick = () => { + selectedEpisodes.clear(); + renderModalEpisodes(); + updateCountBtn(); + }; + } + + async function syncControlsUI(context) { + if (!context || !isHost) return; + + if (context.extension && elements.roomExtSelect.value !== context.extension) { + elements.roomExtSelect.value = context.extension; + // Importante: Cargar los servidores de esa extensión sin disparar otro play + await onQuickExtensionChange(null, true); + } + + if (context.server) { + const options = Array.from(elements.roomServerSelect.options).map(o => o.value); + if (options.includes(context.server)) { + elements.roomServerSelect.value = context.server; + } + } + + if (context.category) { + elements.roomSdToggle.setAttribute('data-state', context.category); + elements.roomSdToggle.querySelectorAll('.sd-option').forEach(opt => { + opt.classList.toggle('active', opt.dataset.val === context.category); + }); + } + + activeContext = { ...activeContext, ...context }; + } + + function toggleEpisode(epNum, btnElement, isShift) { + if (isShift && lastSelectedEp) { + const start = Math.min(lastSelectedEp, epNum); + const end = Math.max(lastSelectedEp, epNum); + + for (let i = start; i <= end; i++) { + selectedEpisodes.add(i); + const el = document.querySelector(`.ep-grid-btn[data-ep="${i}"]`); + if (el) el.classList.add('selected'); + } + } else { + if (selectedEpisodes.has(epNum)) { + selectedEpisodes.delete(epNum); + btnElement.classList.remove('selected'); + } else { + selectedEpisodes.add(epNum); + btnElement.classList.add('selected'); + } + } + + lastSelectedEp = epNum; + updateCountBtn(); + } + + function updateCountBtn() { + document.getElementById('sel-count').textContent = selectedEpisodes.size; + } + + document.getElementById('btn-add-queue').addEventListener('click', () => { + if (selectedEpisodes.size === 0) return; + + const sortedEps = Array.from(selectedEpisodes).sort((a, b) => a - b); + + const itemsBatch = sortedEps.map(ep => ({ + metadata: { + id: currentAnimeDetails.id, + title: currentAnimeDetails.title.userPreferred || currentAnimeDetails.title.english, + episode: ep, + image: currentAnimeDetails.coverImage?.large, + source: 'anilist', + malId: currentAnimeDetails.idMal + }, + video: { + animeId: currentAnimeDetails.id, + episode: ep, + title: currentAnimeDetails.title.userPreferred, + server: document.getElementById('room-server-select').value || 'gogoanime', + category: 'sub' // o lo que esté seleccionado + } + })); + + if (itemsBatch.length === 1) { + socket.send(JSON.stringify({ + type: 'queue_add', + metadata: itemsBatch[0].metadata, + video: itemsBatch[0].video + })); + } else { + socket.send(JSON.stringify({ + type: 'queue_add_batch', + items: itemsBatch + })); + } + + closeModal(); + }); function connectToRoom(roomId, guestName, password) { const token = localStorage.getItem('token'); @@ -863,6 +1089,9 @@ const RoomsApp = (function() { if (data.room.metadata) { updateHeaderInfo(data.room.metadata); } + if (data.room.currentVideo.context) { + syncControlsUI(data.room.currentVideo.context); + } if (!isHost) { console.log('Video detected on join, requesting sync...'); @@ -911,22 +1140,23 @@ const RoomsApp = (function() { case 'video_update': loadVideo(data.video); + if (data.metadata) { - selectedAnimeData = { - ...selectedAnimeData, - ...data.metadata - }; + selectedAnimeData = { ...selectedAnimeData, ...data.metadata }; updateHeaderInfo(data.metadata); } + if (data.video.context) { + syncControlsUI(data.video.context); + } + if (!isHost) { setTimeout(() => { if (ws && ws.readyState === WebSocket.OPEN) { console.log('New video loaded, requesting sync...'); ws.send(JSON.stringify({ type: 'request_sync' })); } - }, 1500); - } + }, 1500); } break; case 'sync': @@ -970,6 +1200,7 @@ const RoomsApp = (function() { } function renderQueue(queueItems) { + currentQueue = queueItems; if (!elements.queueList) return; elements.queueCount.textContent = queueItems.length; @@ -1021,10 +1252,55 @@ const RoomsApp = (function() { }).join(''); } - // Funciones globales (window) para los botones onclick - window.playQueueItem = function(uid) { - if(ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'queue_play_item', itemUid: uid })); + window.playQueueItem = async function(uid) { + const item = currentQueue.find(i => i.uid === uid); + if (!item) return; + + if(typeof showSystemToast === 'function') showSystemToast("Loading from queue..."); + + if (item.videoData && item.videoData.animeId) { + const v = item.videoData; + + selectedAnimeData = { + id: v.animeId, + title: v.title, + image: item.metadata.image, + source: 'anilist', + episode: parseInt(v.episode), + malId: item.metadata.malId + }; + + activeContext.episode = parseInt(v.episode); + activeContext.category = v.category || 'sub'; + + if (elements.roomSdToggle) { + elements.roomSdToggle.setAttribute('data-state', activeContext.category); + elements.roomSdToggle.querySelectorAll('.sd-option').forEach(opt => { + opt.classList.toggle('active', opt.dataset.val === activeContext.category); + }); + } + + try { + const providerExtension = v.extension || elements.roomExtSelect?.value || 'gogoanime'; + + console.log(`Playing from queue with provider: ${providerExtension}`); + + await launchStream(false, false, { + forceExtension: providerExtension, + forceServer: v.server + }); + + removeQueueItem(uid); + + } catch(e) { + console.error("Error playing queue item:", e); + if(typeof showSystemToast === 'function') showSystemToast("Error: " + e.message); + } + + } else { + if(ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'queue_play_item', itemUid: uid })); + } } }; @@ -1054,6 +1330,10 @@ const RoomsApp = (function() { window.AnimePlayer.setRoomHost(isHost); } + if (isHost) { + initGlobalControls(); + } + const copyInviteBtn = document.getElementById('copy-invite-btn'); if (copyInviteBtn) { @@ -1106,6 +1386,30 @@ const RoomsApp = (function() { if (room.currentVideo) loadVideo(room.currentVideo); } + async function initGlobalControls() { + if (!isHost || !extensionsReady) return; + + if (elements.roomExtSelect.options.length > 1) return; + + elements.roomExtSelect.innerHTML = ''; + + extensionsStore.list.forEach(ext => { + const opt = document.createElement('option'); + opt.value = ext; + opt.textContent = ext[0].toUpperCase() + ext.slice(1); + elements.roomExtSelect.appendChild(opt); + }); + + const defaultExt = 'anilist'; + if (extensionsStore.list.includes(defaultExt)) { + elements.roomExtSelect.value = defaultExt; + } else { + elements.roomExtSelect.value = extensionsStore.list[0]; + } + + await onQuickExtensionChange(null, true); + } + function showCopyToast(message) { const toast = document.createElement('div'); toast.className = 'copy-toast'; @@ -1140,10 +1444,13 @@ const RoomsApp = (function() { if (elements.npEpisode) elements.npEpisode.textContent = `Episode ${meta.episode}`; if (elements.npInfo) elements.npInfo.style.opacity = '1'; - if(!selectedAnimeData) selectedAnimeData = { ...meta, source: 'anilist' }; - else { + if(!selectedAnimeData) { + selectedAnimeData = { ...meta, source: 'anilist' }; + } else { selectedAnimeData.id = meta.id; selectedAnimeData.title = meta.title; + selectedAnimeData.image = meta.image || selectedAnimeData.image; + selectedAnimeData.episode = parseInt(meta.episode); } } @@ -1316,8 +1623,13 @@ const RoomsApp = (function() { if (window.AnimePlayer && typeof window.AnimePlayer.loadVideoFromRoom === 'function') { if(elements.loading) elements.loading.style.display = 'none'; - window.AnimePlayer.loadVideoFromRoom({ url: url, type: type, subtitles: videoData.subtitles }); - + window.AnimePlayer.loadVideoFromRoom({ + url: url, + type: type, + subtitles: videoData.subtitles, + malId: videoData.malId, + episode: videoData.episode + }); setTimeout(() => { const newVideoElement = document.getElementById('player'); if (newVideoElement) { @@ -1326,8 +1638,6 @@ const RoomsApp = (function() { updatePlayPauseButton(!newVideoElement.paused); } }, 500); - } else { - loadVideoBasic(url, type, videoData); } } @@ -1336,23 +1646,6 @@ const RoomsApp = (function() { return document.getElementById('player'); } - function loadVideoBasic(url, type, videoData) { - elements.loading.style.display = 'none'; - if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; } - - if (Hls.isSupported() && type === 'm3u8') { - hlsInstance = new Hls(); - hlsInstance.loadSource(url); - hlsInstance.attachMedia(elements.video); - hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { - if (videoData.isPlaying) elements.video.play().catch(e=>console.error(e)); - }); - } else { - elements.video.src = url; - if (videoData.isPlaying) elements.video.play(); - } - } - function syncVideo(currentTime, isPlaying) { const video = getVideoElement(); if (!video) { @@ -1473,14 +1766,20 @@ const RoomsApp = (function() { if (isHost) { console.log('Video ended. Checking queue...'); setTimeout(() => { - if(ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'play_next' })); - } + playNextInQueue(); }, 1000); } }); } + async function playNextInQueue() { + if (currentQueue.length === 0) return; + const nextItem = currentQueue[0]; + if (nextItem) { + await window.playQueueItem(nextItem.uid); + } + } + function formatTime(s) { if(isNaN(s)) return "0:00"; const m = Math.floor(s/60), sec = Math.floor(s%60); diff --git a/docker/views/css/room.css b/docker/views/css/room.css index 2a2c2fc..457a0f6 100644 --- a/docker/views/css/room.css +++ b/docker/views/css/room.css @@ -1,3 +1,6 @@ +/* ========================================= + 1. VARIABLES & RESET + ========================================= */ :root { --brand-color: #8b5cf6; --brand-gradient: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%); @@ -23,6 +26,9 @@ background: rgba(255, 255, 255, 0.4); } +/* ========================================= + 2. UTILITIES & ANIMATIONS + ========================================= */ .spinner { width: 24px; height: 24px; @@ -33,7 +39,22 @@ } @keyframes spin { to { transform: rotate(360deg); } } +@keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } +@keyframes slideUp { from { transform: translateX(-50%) translateY(20px); opacity: 0; } to { transform: translateX(-50%) translateY(0); opacity: 1; } } +@keyframes slideDown { from { transform: translateX(-50%) translateY(-20px); opacity: 0; } to { transform: translateX(-50%) translateY(0); opacity: 1; } } +@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } +@keyframes scaleIn { from { transform: translate(-50%, -50%) scale(0.8); opacity: 0; } to { transform: translate(-50%, -50%) scale(1); opacity: 1; } } +@keyframes toastSlideIn { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); } } +@keyframes toastFadeOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-10px); visibility: hidden; } } +@keyframes pulse { + 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } + 70% { transform: scale(1); box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); } + 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } +} +/* ========================================= + 3. BUTTONS & INPUTS + ========================================= */ .btn-icon-glass, .btn-icon-small, .modal-close { appearance: none; background: rgba(255, 255, 255, 0.05); @@ -46,7 +67,6 @@ align-items: center; justify-content: center; } - .btn-icon-glass { width: 36px; height: 36px; backdrop-filter: blur(8px); } .btn-icon-small { padding: 6px; } .modal-close { width: 32px; height: 32px; position: absolute; top: 20px; right: 20px; background: rgba(255, 255, 255, 0.1); } @@ -60,22 +80,15 @@ .modal-close:hover { background: rgba(255, 255, 255, 0.3); transform: rotate(90deg); } .btn-glass-primary { - display: flex; - align-items: center; - gap: 8px; + display: flex; align-items: center; gap: 8px; background: rgba(139, 92, 246, 0.25); border: 1px solid rgba(139, 92, 246, 0.4); color: white; - padding: 0 16px; - height: 36px; - border-radius: 10px; - font-weight: 600; - font-size: 0.9rem; - cursor: pointer; - backdrop-filter: blur(8px); + padding: 0 16px; height: 36px; + border-radius: 10px; font-weight: 600; font-size: 0.9rem; + cursor: pointer; backdrop-filter: blur(8px); transition: all 0.2s; } - .btn-glass-primary:hover { background: rgba(139, 92, 246, 0.4); box-shadow: 0 0 15px rgba(139, 92, 246, 0.2); @@ -84,23 +97,13 @@ .btn-confirm, .btn-primary { background: var(--brand-color); - border: none; - color: white; - padding: 10px 24px; - border-radius: 10px; - font-weight: 700; - cursor: pointer; + border: none; color: white; + padding: 10px 24px; border-radius: 10px; + font-weight: 700; cursor: pointer; transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; -} - -.btn-confirm:hover, .btn-primary:hover { - background: #7c3aed; - transform: translateY(-2px); + display: flex; align-items: center; justify-content: center; gap: 8px; } +.btn-confirm:hover, .btn-primary:hover { background: #7c3aed; transform: translateY(-2px); } .btn-confirm:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .btn-confirm.full-width { width: 100%; } @@ -108,469 +111,359 @@ background: transparent; border: 1px solid rgba(255, 255, 255, 0.2); color: rgba(255, 255, 255, 0.8); - padding: 10px 20px; - border-radius: 8px; - cursor: pointer; - font-weight: 600; + padding: 10px 20px; border-radius: 8px; + cursor: pointer; font-weight: 600; transition: all 0.2s; } .btn-cancel:hover { background: rgba(255, 255, 255, 0.1); color: white; } +.btn-quick-action { + display: flex; align-items: center; gap: 6px; + height: 34px; padding: 0 12px; + background: rgba(139, 92, 246, 0.15); + border: 1px solid rgba(139, 92, 246, 0.3); + border-radius: 8px; color: #d8b4fe; + font-size: 0.85rem; font-weight: 600; + cursor: pointer; transition: all 0.2s; +} +.btn-quick-action:hover { + background: var(--brand-color); border-color: var(--brand-color); + color: white; transform: translateY(-1px); +} +#copy-invite-btn { transition: all 0.3s ease; } +#copy-invite-btn:hover { transform: scale(1.05); } +#copy-invite-btn:active { transform: scale(0.95); } + +/* Inputs */ input[type="text"], input[type="password"], input[type="number"], .form-input { width: 100%; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); - color: white; - padding: 12px 16px; - border-radius: 10px; - font-size: 1rem; - outline: none; - transition: border-color 0.2s; + color: white; padding: 12px 16px; + border-radius: 10px; font-size: 1rem; + outline: none; transition: border-color 0.2s; } +input:focus, .form-input:focus { border-color: var(--brand-color); background: rgba(255, 255, 255, 0.08); } +input[type=number]::-webkit-inner-spin-button, input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } +input[type=number] { -moz-appearance: textfield; } -input:focus, .form-input:focus { - border-color: var(--brand-color); - background: rgba(255, 255, 255, 0.08); +/* Custom Select Wrappers */ +.quick-select-wrapper { + position: relative; display: flex; align-items: center; + height: 34px; background: rgba(255, 255, 255, 0.03); + border-radius: 8px; transition: all 0.2s; + padding: 0 8px; min-width: 110px; } +.quick-select-wrapper:hover { background: rgba(255, 255, 255, 0.08); } +.quick-select-wrapper svg { pointer-events: none; color: var(--text-muted); margin-right: 6px; flex-shrink: 0; } +.select-arrow { font-size: 0.6rem; color: rgba(255, 255, 255, 0.4); pointer-events: none; margin-left: 4px; } +.quick-select, .glass-select-sm { + appearance: none; -webkit-appearance: none; + background: transparent; border: none; + color: #fff; width: 100%; height: 100%; + cursor: pointer; outline: none; position: relative; z-index: 2; +} .glass-select-sm { - appearance: none; - -webkit-appearance: none; background-color: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); - color: #eee; - padding: 0 32px 0 12px; - height: 32px; - line-height: 30px; - border-radius: 8px; - font-size: 0.85rem; - font-weight: 600; - cursor: pointer; - outline: none; - transition: all 0.2s; - max-width: 140px; - white-space: nowrap; - text-overflow: ellipsis; - - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='rgba(255,255,255,0.6)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 8px center; - background-size: 14px; + padding: 0 32px 0 12px; height: 32px; line-height: 30px; + border-radius: 8px; font-size: 0.85rem; font-weight: 600; + max-width: 140px; white-space: nowrap; text-overflow: ellipsis; + background-image: url("data:image/svg+xml,..."); /* SVG truncated for brevity */ + background-repeat: no-repeat; background-position: right 8px center; background-size: 14px; } +.quick-select option, .glass-select-sm option { background: #1a1a1a; color: #eee; } -.glass-select-sm:hover { - background-color: rgba(255, 255, 255, 0.1); - border-color: rgba(255,255,255,0.3); - color: #fff; -} -.glass-select-sm option { background: #1a1a1a; color: #e0e0e0; } - -.chips-grid { display: flex; flex-wrap: wrap; gap: 10px; } -.chip { - padding: 8px 16px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 8px; - color: #ccc; - font-size: 0.9rem; - cursor: pointer; - transition: all 0.2s; - user-select: none; -} -.chip:hover { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.3); transform: translateY(-2px); } -.chip.active { - background: var(--brand-color); - border-color: var(--brand-color); - color: white; - box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); -} -.chip.disabled { opacity: 0.5; pointer-events: none; filter: grayscale(1); } - +/* ========================================= + 4. LAYOUT + ========================================= */ .room-layout { display: grid; grid-template-columns: 1fr 380px; - height: 100vh; - overflow: hidden; + height: 100vh; overflow: hidden; transition: grid-template-columns 0.3s cubic-bezier(0.4, 0, 0.2, 1); } +.room-layout.chat-hidden { grid-template-columns: 1fr 0px !important; } -.room-layout.chat-hidden { - grid-template-columns: 1fr 0px !important; -} - +/* Video Area */ .video-area { - display: flex; - flex-direction: column; - background: #000; - flex: 1; - min-height: 0; - overflow: hidden; - position: relative; -} - -@media (max-width: 1200px) { - .room-layout { - grid-template-columns: 1fr; - grid-template-rows: 1fr auto; - } - .room-layout.chat-hidden { - grid-template-rows: 1fr 0px; - } - .chat-sidebar { - height: 350px; - border-left: none; - border-top: 1px solid var(--glass-border); - } + display: flex; flex-direction: column; + background: #000; flex: 1; min-height: 0; + overflow: hidden; position: relative; } +/* ========================================= + 5. HEADER + ========================================= */ .room-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 24px; - background: rgba(10, 10, 10, 0.85); - backdrop-filter: blur(20px); + display: flex; justify-content: space-between; align-items: center; + padding: 0 24px; height: 64px; + background: rgba(10, 10, 10, 0.85); backdrop-filter: blur(20px); border-bottom: 1px solid var(--glass-border); - height: 64px; - z-index: 20; - flex-shrink: 0; + z-index: 20; flex-shrink: 0; } -.header-left, .header-right { - display: flex; - align-items: center; - gap: 16px; - flex: 1; -} +.header-left, .header-right { display: flex; align-items: center; gap: 16px; flex: 1; } .header-right { justify-content: flex-end; } +.header-center { flex: 2; display: flex; justify-content: center; z-index: 50; } .room-info { display: flex; flex-direction: column; justify-content: center; line-height: 1.2; } -#room-name { - margin: 0; - font-size: 1rem; - font-weight: 700; - color: white; - letter-spacing: -0.01em; -} +#room-name { margin: 0; font-size: 1rem; font-weight: 700; color: white; letter-spacing: -0.01em; } .np-fade { display: flex; align-items: center; opacity: 0.7; font-size: 0.8rem; gap: 6px; margin-top: 2px; } .np-title { color: #ccc; font-weight: 500; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .np-sep { color: #555; } -.np-badge { - background: rgba(255, 255, 255, 0.1); - color: #fff; - padding: 1px 6px; - border-radius: 4px; - font-size: 0.7rem; - font-weight: 700; -} - -.header-center { flex: 2; display: flex; justify-content: center; z-index: 50; } +.np-badge { background: rgba(255, 255, 255, 0.1); color: #fff; padding: 1px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 700; } +/* Header Controls */ .quick-controls-group { - display: flex; - align-items: center; - gap: 8px; - background: rgba(20, 20, 20, 0.6); - backdrop-filter: blur(12px); - padding: 4px 8px; - border-radius: 10px; - border: 1px solid var(--glass-border); - box-shadow: 0 4px 12px rgba(0,0,0,0.2); + display: flex; align-items: center; gap: 4px; + background: rgba(15, 15, 15, 0.85); + backdrop-filter: blur(16px); + padding: 4px; height: 44px; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); transition: all 0.2s ease; - height: 42px; } -.quick-controls-group:hover { border-color: rgba(255,255,255,0.15); background: rgba(30, 30, 30, 0.7); } +.quick-controls-group:hover { border-color: rgba(255, 255, 255, 0.2); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } +.control-divider { width: 1px; height: 20px; background: rgba(255, 255, 255, 0.1); margin: 0 4px; } + +/* Sub/Dub Toggle */ .sd-toggle.small { - height: 32px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255,255,255,0.1); - border-radius: 8px; + height: 34px; min-width: 86px; + background: transparent; border: none; + display: flex; position: relative; cursor: pointer; box-sizing: border-box; padding: 2px; - display: flex; - position: relative; - cursor: pointer; - min-width: 80px; - box-sizing: border-box; } .sd-toggle.small .sd-bg { - position: absolute; - top: 2px; left: 2px; + position: absolute; top: 2px; left: 2px; width: calc(50% - 2px); height: calc(100% - 4px); - background: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 6px; transition: transform 0.2s cubic-bezier(0.4, 0.0, 0.2, 1); } .sd-toggle[data-state="dub"] .sd-bg { transform: translateX(100%); } .sd-toggle.small .sd-option { - flex: 1; - z-index: 2; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.75rem; - font-weight: 700; + flex: 1; z-index: 2; + display: flex; align-items: center; justify-content: center; + font-size: 0.75rem; font-weight: 700; color: rgba(255,255,255,0.4); - transition: color 0.2s; - user-select: none; + transition: color 0.2s; user-select: none; } .sd-toggle.small .sd-option.active { color: #fff; } .viewers-pill { - display: flex; - align-items: center; - gap: 6px; - background: rgba(0,0,0,0.3); - padding: 0 10px; - border-radius: 20px; - border: 1px solid rgba(255,255,255,0.05); - font-size: 0.8rem; - color: #aaa; - height: 32px; + display: flex; align-items: center; gap: 6px; + background: rgba(0,0,0,0.3); padding: 0 10px; + border-radius: 20px; border: 1px solid rgba(255,255,255,0.05); + font-size: 0.8rem; color: #aaa; height: 32px; } +#toggle-chat-btn { position: relative; } +#toggle-chat-btn.has-unread::after { + content: ''; position: absolute; top: 2px; right: 2px; + width: 10px; height: 10px; background-color: #ef4444; + border: 2px solid #1a1a1a; border-radius: 50%; + animation: pulse 2s infinite; +} + +/* ========================================= + 6. PLAYER + ========================================= */ .player-wrapper { - display: flex !important; - flex-direction: column; - flex: 1; - width: 100%; - height: auto !important; - min-height: 0; - position: relative !important; - z-index: 1 !important; - background: transparent !important; - overflow: hidden; + display: flex !important; flex-direction: column; flex: 1; + width: 100%; height: auto !important; min-height: 0; + position: relative !important; z-index: 1 !important; + background: transparent !important; overflow: hidden; } - -.player-container { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - position: relative; -} - +.player-container { width: 100%; height: 100%; display: flex; flex-direction: column; position: relative; } .video-frame { - flex: 1; - min-height: 0; - position: relative; - background: #000; - display: flex; - align-items: center; - justify-content: center; -} - -#player { - width: 100%; - height: 100%; - max-height: 100%; + flex: 1; min-height: 0; position: relative; + background: #000; display: flex; align-items: center; justify-content: center; } +#player { width: 100%; height: 100%; max-height: 100%; } +#subtitles-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; } .custom-controls { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - z-index: 60; + position: absolute; bottom: 0; left: 0; width: 100%; z-index: 60; background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0) 100%); padding: 20px 10px 10px; } +.controls-left { display: flex; align-items: center; gap: 10px; } +.time-display { white-space: nowrap; font-variant-numeric: tabular-nums; font-size: 0.9rem; color: #ddd; min-width: fit-content; } -.controls-left { - display: flex; - align-items: center; - gap: 10px; -} - -.time-display { - white-space: nowrap; - font-variant-numeric: tabular-nums; - font-size: 0.9rem; - color: #ddd; - min-width: fit-content; -} - -#subtitles-canvas { - position: absolute; - top: 0; left: 0; - width: 100%; height: 100%; - pointer-events: none; - z-index: 10; -} - -#download-btn, -#manual-match-btn, -#server-select, -#extension-select, -#sd-toggle, -.side-nav-btn { - display: none !important; -} +/* Hidden Legacy Buttons */ +#download-btn, #manual-match-btn, #server-select, #extension-select, #sd-toggle, .side-nav-btn { display: none !important; } +/* Settings Panel */ .settings-panel { - position: absolute; - bottom: 70px; - right: 20px; - z-index: 1000; - max-height: 60vh; - overflow-y: auto; + position: absolute; bottom: 70px; right: 20px; z-index: 1000; + max-height: 60vh; overflow-y: auto; background: rgba(15, 15, 15, 0.95); - border: 1px solid var(--glass-border); - border-radius: 8px; + border: 1px solid var(--glass-border); border-radius: 8px; } +/* Video Toasts */ +.video-toast-container { + position: absolute; bottom: 100px; left: 20px; z-index: 80; + display: flex; flex-direction: column; gap: 10px; + pointer-events: none; max-width: 400px; +} +.video-toast { + display: flex; align-items: center; gap: 10px; + background: rgba(15, 15, 15, 0.85); backdrop-filter: blur(8px); + padding: 8px 12px; border-radius: 8px; + border-left: 3px solid var(--brand-color); + color: white; font-size: 0.9rem; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + animation: toastSlideIn 0.3s ease forwards, toastFadeOut 0.5s ease 4.5s forwards; + pointer-events: auto; opacity: 0; +} +.toast-avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; flex-shrink: 0; } +.toast-content { display: flex; flex-direction: column; line-height: 1.2; } +.toast-user { font-weight: 700; font-size: 0.8rem; color: #a78bfa; } +.toast-msg { color: #eee; margin-top: 2px; } +.video-toast.system-toast { + border-left-color: #9ca3af; background: rgba(20, 20, 20, 0.7); + justify-content: center; padding: 6px 12px; min-height: auto; +} +.video-toast.system-toast .toast-msg { font-size: 0.85rem; font-style: italic; color: rgba(255, 255, 255, 0.8); margin: 0; } + +/* ========================================= + 7. CHAT SIDEBAR + ========================================= */ .chat-sidebar { - display: flex; - flex-direction: column; + display: flex; flex-direction: column; height: 100%; background: rgba(15, 15, 15, 0.95); - border-left: 1px solid var(--glass-border); - height: 100%; - overflow: hidden; -} -.room-layout.chat-hidden .chat-sidebar { - opacity: 0; - pointer-events: none; - border-left: none; + border-left: 1px solid var(--glass-border); overflow: hidden; } +.room-layout.chat-hidden .chat-sidebar { opacity: 0; pointer-events: none; border-left: none; } .chat-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 20px; - border-bottom: 1px solid var(--glass-border); + display: flex; justify-content: space-between; align-items: center; + padding: 16px 20px; border-bottom: 1px solid var(--glass-border); } .chat-header h3 { margin: 0; font-size: 1.1rem; font-weight: 700; color: white; } +.sidebar-tabs { + display: flex; width: 100%; flex: 0 0 auto; height: 50px; + border-bottom: 1px solid var(--glass-border); background: rgba(0,0,0,0.2); +} +.tab-btn { + flex: 1; height: 100%; padding: 0; + display: flex; align-items: center; justify-content: center; + background: transparent; border: none; border-bottom: 2px solid transparent; + color: var(--text-muted); font-weight: 600; cursor: pointer; + transition: all 0.2s; +} +.tab-btn:hover { background: rgba(255,255,255,0.05); color: white; } +.tab-btn.active { border-bottom-color: var(--brand-color); color: white; background: rgba(255,255,255,0.02); } + +.tab-content { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; } + +/* Users List */ .users-list { - padding: 12px; - border-bottom: 1px solid var(--glass-border); - max-height: 200px; - overflow-y: auto; - background: rgba(0,0,0,0.2); -} -.user-item { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 12px; - border-radius: 8px; - margin-bottom: 4px; + padding: 12px; border-bottom: 1px solid var(--glass-border); + max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.2); } +.user-item { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 8px; margin-bottom: 4px; } .user-item:hover { background: rgba(255, 255, 255, 0.05); } .user-avatar { - width: 32px; height: 32px; - border-radius: 50%; + width: 32px; height: 32px; border-radius: 50%; background: var(--brand-gradient); - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - color: white; - font-size: 0.8rem; + display: flex; align-items: center; justify-content: center; + font-weight: 700; color: white; font-size: 0.8rem; } .user-avatar img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; } .user-name { flex: 1; font-size: 0.9rem; color: white; } -.user-badge { - font-size: 0.7rem; - background: var(--brand-color); - color: white; - padding: 2px 8px; - border-radius: 4px; - font-weight: 600; -} +.user-badge { font-size: 0.7rem; background: var(--brand-color); color: white; padding: 2px 8px; border-radius: 4px; font-weight: 600; } +/* Chat Messages */ .chat-messages { - flex: 1; - overflow-y: auto; - padding: 16px; - display: flex; - flex-direction: column; - gap: 12px; - min-height: 0; + flex: 1; overflow-y: auto; padding: 16px; + display: flex; flex-direction: column; gap: 12px; min-height: 0; } .chat-message { display: flex; gap: 10px; } .chat-message.system { justify-content: center; margin: 8px 0; } .chat-message.system .message-content { - background: rgba(255, 255, 255, 0.05); - color: var(--text-muted); - font-size: 0.8rem; - text-align: center; - padding: 4px 12px; - border-radius: 12px; + background: rgba(255, 255, 255, 0.05); color: var(--text-muted); + font-size: 0.8rem; text-align: center; padding: 4px 12px; border-radius: 12px; } .message-avatar { - width: 36px; height: 36px; - border-radius: 50%; + width: 36px; height: 36px; border-radius: 50%; background: var(--brand-gradient); - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - color: white; - font-size: 0.9rem; - flex-shrink: 0; + display: flex; align-items: center; justify-content: center; + font-weight: 700; color: white; font-size: 0.9rem; flex-shrink: 0; } .message-body { flex: 1; min-width: 0; } .message-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; } .message-username { font-weight: 600; color: white; font-size: 0.9rem; } .message-time { font-size: 0.75rem; color: rgba(255, 255, 255, 0.4); } -.message-content { - color: rgba(255, 255, 255, 0.9); - font-size: 0.95rem; - line-height: 1.4; - word-wrap: break-word; -} +.message-content { color: rgba(255, 255, 255, 0.9); font-size: 0.95rem; line-height: 1.4; word-wrap: break-word; } -.chat-input { - display: flex; - gap: 8px; - padding: 16px; - border-top: 1px solid var(--glass-border); -} +.chat-input { display: flex; gap: 8px; padding: 16px; border-top: 1px solid var(--glass-border); } .chat-input input { - flex: 1; - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--glass-border); - color: white; - padding: 12px 16px; - border-radius: 10px; - outline: none; + flex: 1; background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); color: white; + padding: 12px 16px; border-radius: 10px; outline: none; } .chat-input button { - background: var(--brand-color); - border: none; - color: white; - padding: 0 16px; - border-radius: 10px; - cursor: pointer; - transition: background 0.2s; + background: var(--brand-color); border: none; color: white; + padding: 0 16px; border-radius: 10px; cursor: pointer; transition: background 0.2s; } .chat-input button:hover { background: #7c3aed; } +/* ========================================= + 8. QUEUE TAB + ========================================= */ +.queue-list { + flex: 1; overflow-y: auto; padding: 10px; + display: flex; flex-direction: column; gap: 10px; +} +.queue-item { + display: flex; gap: 10px; padding: 10px; + background: rgba(255,255,255,0.05); + border-radius: 8px; border: 1px solid transparent; + position: relative; align-items: center; transition: transform 0.2s; +} +.queue-item:hover { background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.1); } +.q-img { width: 50px; height: 70px; object-fit: cover; border-radius: 4px; flex-shrink: 0; } +.q-info { flex: 1; display: flex; flex-direction: column; justify-content: center; overflow: hidden; } +.q-title { font-weight: 700; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.q-meta { font-size: 0.8rem; color: #aaa; } +.q-actions { display: flex; flex-direction: column; gap: 4px; opacity: 0.7; transition: opacity 0.2s; } +.queue-item:hover .q-actions { opacity: 1; } + +.q-btn { + background: rgba(255,255,255,0.1); border: none; color: white; + width: 28px; height: 28px; border-radius: 6px; cursor: pointer; + display: flex; align-items: center; justify-content: center; transition: all 0.2s; +} +.q-btn:hover { background: rgba(255,255,255,0.2); transform: scale(1.1); } +.q-btn.play:hover { background: var(--brand-color); } +.q-btn.remove:hover { background: #ff6b6b; } +.badge { background: var(--brand-color); padding: 1px 6px; border-radius: 10px; font-size: 0.7rem; margin-left: 6px; } +.queue-empty { text-align: center; color: #666; margin-top: 40px; font-style: italic; } + +/* ========================================= + 9. MODALS + ========================================= */ .modal-overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.8); - backdrop-filter: blur(8px); - display: none; - align-items: center; - justify-content: center; - z-index: 10000; - opacity: 0; - transition: opacity 0.3s ease; + background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(8px); + display: none; align-items: center; justify-content: center; + z-index: 10000; opacity: 0; transition: opacity 0.3s ease; } .modal-overlay.show { display: flex; opacity: 1; } .modal-content { background: rgba(20, 20, 20, 0.95); - border: 1px solid var(--glass-border); - border-radius: 16px; - padding: 32px; - max-width: 500px; - width: 90%; - position: relative; - transform: scale(0.9); - transition: transform 0.3s ease; + border: 1px solid var(--glass-border); border-radius: 16px; + padding: 32px; max-width: 500px; width: 90%; + position: relative; transform: scale(0.9); transition: transform 0.3s ease; } .modal-overlay.show .modal-content { transform: scale(1); } .modal-title { margin: 0 0 24px 0; font-size: 1.5rem; font-weight: 800; color: white; } @@ -581,35 +474,36 @@ input:focus, .form-input:focus { .form-group label { display: block; margin-bottom: 8px; font-weight: 600; color: white; } .form-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px; } +/* Join Info */ +.join-host-info { display: flex; flex-direction: column; align-items: center; margin-bottom: 24px; animation: fadeIn 0.5s ease; } +.join-avatar-container { + width: 80px; height: 80px; border-radius: 50%; padding: 3px; + background: var(--brand-gradient); margin-bottom: 12px; + box-shadow: 0 4px 15px rgba(139, 92, 246, 0.4); +} +.join-avatar-container img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; border: 3px solid #1a1a1a; background: #2a2a2a; } +.join-text { font-size: 1.1rem; color: white; font-weight: 500; text-align: center; margin: 0; } +.join-text span { font-weight: 700; color: #a78bfa; } + +/* Search Content */ .anime-search-content { max-width: 800px; max-height: 85vh; display: flex; flex-direction: column; } .search-bar { display: flex; gap: 12px; margin-bottom: 20px; } .search-bar button { - background: var(--brand-color); - border: none; color: white; padding: 12px 24px; - border-radius: 10px; font-weight: 700; cursor: pointer; + background: var(--brand-color); border: none; color: white; + padding: 12px 24px; border-radius: 10px; font-weight: 700; cursor: pointer; } .anime-results { - flex: 1; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 12px; - min-height: 0; - max-height: 60vh; - padding-right: 8px; + flex: 1; overflow-y: auto; display: flex; flex-direction: column; + gap: 12px; min-height: 0; max-height: 60vh; padding-right: 8px; } - .anime-result-item, .search-item { display: flex; gap: 16px; padding: 12px; background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 12px; - cursor: pointer; text-decoration: none; color: inherit; - transition: all 0.2s; + border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; + cursor: pointer; text-decoration: none; color: inherit; transition: all 0.2s; } .anime-result-item:hover { - background: rgba(255, 255, 255, 0.08); - border-color: var(--brand-color); + background: rgba(255, 255, 255, 0.08); border-color: var(--brand-color); transform: translateX(4px); } .search-poster { width: 60px; height: 85px; border-radius: 8px; object-fit: cover; flex-shrink: 0; } @@ -617,6 +511,7 @@ input:focus, .form-input:focus { .search-title { font-weight: 700; color: white; margin-bottom: 4px; font-size: 1rem; } .search-meta { font-size: 0.85rem; color: var(--text-muted); } +/* Config Layout */ .config-layout { display: flex; gap: 24px; margin-top: 20px; } .config-sidebar { width: 140px; flex-shrink: 0; display: flex; flex-direction: column; align-items: center; gap: 12px; } .config-cover { @@ -631,88 +526,100 @@ input:focus, .form-input:focus { margin-bottom: 8px; font-weight: 700; } -input[type=number]::-webkit-inner-spin-button, -input[type=number]::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; +/* Chips & Toggles in Modal */ +.chips-grid { display: flex; flex-wrap: wrap; gap: 10px; } +.chip { + padding: 8px 16px; background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; + color: #ccc; font-size: 0.9rem; cursor: pointer; + transition: all 0.2s; user-select: none; } -input[type=number] { - -moz-appearance: textfield; -} - -.ep-control { - display: flex; - align-items: center; - justify-content: space-between; - background: rgba(0, 0, 0, 0.4); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 12px; - padding: 4px; - width: 100%; - height: 48px; - margin-top: 8px; - box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); -} - -.ep-btn { - width: 40px; - height: 38px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255,255,255,0.05); - color: white; - border-radius: 8px; - cursor: pointer; - font-size: 1.2rem; - transition: all 0.2s ease; -} - -.ep-btn:hover { - background: var(--brand-color); - border-color: var(--brand-color); - transform: translateY(-1px); -} - -.ep-btn:active { - transform: translateY(1px); -} - -.ep-input { - flex: 1; - min-width: 0; - background: transparent; - border: none; - color: white; - text-align: center; - font-size: 1.2rem; - font-weight: 800; - outline: none; - font-family: monospace; -} - -.ep-input:focus { - background: transparent; - border: none; -} - -.cat-toggle { - display: flex; background: rgba(0,0,0,0.3); - padding: 4px; border-radius: 10px; width: fit-content; +.chip:hover { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.3); transform: translateY(-2px); } +.chip.active { + background: var(--brand-color); border-color: var(--brand-color); + color: white; box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); } +.cat-toggle { display: flex; background: rgba(0,0,0,0.3); padding: 4px; border-radius: 10px; width: fit-content; } .cat-opt { - padding: 6px 16px; border-radius: 8px; - font-size: 0.85rem; color: #888; + padding: 6px 16px; border-radius: 8px; font-size: 0.85rem; color: #888; cursor: pointer; transition: all 0.2s; font-weight: 600; } .cat-opt.active { background: rgba(255,255,255,0.15); color: white; } .cat-opt.disabled { opacity: 0.3; pointer-events: none; text-decoration: line-through; } -.grid-loader { - width: 100%; padding: 20px; text-align: center; - color: var(--text-muted); font-size: 0.9rem; +/* Episodes Grid */ +.episodes-grid-container { + display: grid; grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)); + gap: 8px; max-height: 280px; overflow-y: auto; padding: 10px; + background: rgba(0, 0, 0, 0.2); border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.05); +} +.episodes-grid-container::-webkit-scrollbar { width: 4px; } +.episodes-grid-container::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); } + +.ep-grid-btn { + background: rgba(255,255,255,0.03); border: 1px solid rgba(255, 255, 255, 0.1); + color: #ccc; padding: 8px 0; border-radius: 6px; text-align: center; + font-size: 0.9rem; font-weight: 600; cursor: pointer; + transition: all 0.2s; user-select: none; +} +.ep-grid-btn:hover { background: white; color: black; transform: translateY(-2px); } +.ep-grid-btn.selected, .ep-grid-btn.active { + background: var(--brand-color); color: white; + border-color: var(--brand-color); box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); +} + +.btn-text-tiny { + background: transparent; border: none; color: var(--brand-color); + font-size: 0.75rem; font-weight: 700; cursor: pointer; padding: 2px 6px; +} +.btn-text-tiny:hover { text-decoration: underline; } + +/* Manual Control */ +.ep-control { + display: flex; align-items: center; justify-content: space-between; + background: rgba(0, 0, 0, 0.4); border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 12px; padding: 4px; width: 100%; height: 48px; + margin-top: 8px; box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); +} +.ep-btn { + width: 40px; height: 38px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; + background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255,255,255,0.05); + color: white; border-radius: 8px; cursor: pointer; font-size: 1.2rem; transition: all 0.2s ease; +} +.ep-btn:hover { background: var(--brand-color); border-color: var(--brand-color); transform: translateY(-1px); } +.ep-btn:active { transform: translateY(1px); } +.ep-input { + flex: 1; min-width: 0; background: transparent; border: none; + color: white; text-align: center; font-size: 1.2rem; font-weight: 800; + outline: none; font-family: monospace; +} +.grid-loader { width: 100%; padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.9rem; } + +/* Modal Pagination */ +.modal-pagination { + display: flex; justify-content: center; align-items: center; gap: 15px; + margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.05); +} +.modal-page-btn { + background: transparent; border: 1px solid rgba(255, 255, 255, 0.2); + color: white; width: 30px; height: 30px; border-radius: 6px; + display: flex; align-items: center; justify-content: center; + cursor: pointer; transition: all 0.2s; +} +.modal-page-btn:hover:not(:disabled) { background: white; color: black; } +.modal-page-btn:disabled { opacity: 0.3; cursor: not-allowed; } +.modal-page-info { font-size: 0.85rem; color: #888; font-weight: 700; } + +/* ========================================= + 10. MEDIA QUERIES (Combined) + ========================================= */ +@media (max-width: 1200px) { + .room-layout { grid-template-columns: 1fr; grid-template-rows: 1fr auto; } + .room-layout.chat-hidden { grid-template-rows: 1fr 0px; } + .chat-sidebar { + height: 350px; border-left: none; border-top: 1px solid var(--glass-border); + } } @media (max-width: 600px) { @@ -720,437 +627,4 @@ input[type=number] { .config-sidebar { width: 100%; flex-direction: row; } .config-cover { width: 80px; } .ep-control { width: auto; flex: 1; } -} - -.container { max-width: 1400px; margin: 0 auto; padding: 80px 40px 40px; } -.header h1 { - font-size: 2.5rem; font-weight: 800; - background: var(--brand-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} -.rooms-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 24px; -} -.room-card { - background: var(--glass-bg); - border: 1px solid var(--glass-border); - border-radius: 16px; padding: 24px; - backdrop-filter: var(--glass-blur); - cursor: pointer; transition: all 0.3s; -} -.room-card:hover { - transform: translateY(-4px); - border-color: var(--brand-color); - box-shadow: 0 8px 32px rgba(139, 92, 246, 0.3); -} -.room-card-title { font-size: 1.25rem; font-weight: 700; color: white; margin-bottom: 4px; } -.room-card-host { font-size: 0.85rem; color: var(--text-muted); } -.room-card-footer { - display: flex; justify-content: space-between; - align-items: center; padding-top: 16px; - border-top: 1px solid rgba(255, 255, 255, 0.1); margin-top: 16px; -} - -.join-host-info { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 24px; - animation: fadeIn 0.5s ease; -} - -.join-avatar-container { - width: 80px; - height: 80px; - border-radius: 50%; - padding: 3px; - background: var(--brand-gradient); - margin-bottom: 12px; - box-shadow: 0 4px 15px rgba(139, 92, 246, 0.4); -} - -.join-avatar-container img { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; - border: 3px solid #1a1a1a; - background: #2a2a2a; -} - -.join-text { - font-size: 1.1rem; - color: white; - font-weight: 500; - text-align: center; - margin: 0; -} - -.join-text span { - font-weight: 700; - color: #a78bfa; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(-10px); } - to { opacity: 1; transform: translateY(0); } -} - -.video-toast-container { - position: absolute; - bottom: 100px; - left: 20px; - z-index: 80; - display: flex; - flex-direction: column; - gap: 10px; - pointer-events: none; - max-width: 400px; -} - -.video-toast { - display: flex; - align-items: center; - gap: 10px; - background: rgba(15, 15, 15, 0.85); - backdrop-filter: blur(8px); - padding: 8px 12px; - border-radius: 8px; - border-left: 3px solid var(--brand-color); - color: white; - font-size: 0.9rem; - box-shadow: 0 4px 12px rgba(0,0,0,0.3); - - animation: toastSlideIn 0.3s ease forwards, toastFadeOut 0.5s ease 4.5s forwards; - pointer-events: auto; - opacity: 0; -} - -.toast-avatar { - width: 28px; - height: 28px; - border-radius: 50%; - object-fit: cover; - flex-shrink: 0; -} - -.toast-content { - display: flex; - flex-direction: column; - line-height: 1.2; -} - -.toast-user { - font-weight: 700; - font-size: 0.8rem; - color: #a78bfa; -} - -.toast-msg { - color: #eee; - margin-top: 2px; -} - -#toggle-chat-btn { - position: relative; -} - -#toggle-chat-btn.has-unread::after { - content: ''; - position: absolute; - top: 2px; - right: 2px; - width: 10px; - height: 10px; - background-color: #ef4444; - border: 2px solid #1a1a1a; - border-radius: 50%; - animation: pulse 2s infinite; -} - -@keyframes toastSlideIn { - from { opacity: 0; transform: translateX(-20px); } - to { opacity: 1; transform: translateX(0); } -} - -@keyframes toastFadeOut { - from { opacity: 1; transform: translateY(0); } - to { opacity: 0; transform: translateY(-10px); visibility: hidden; } -} - -@keyframes pulse { - 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } - 70% { transform: scale(1); box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); } - 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } -} - -.video-toast.system-toast { - border-left-color: #9ca3af; - background: rgba(20, 20, 20, 0.7); - justify-content: center; - padding: 6px 12px; - min-height: auto; -} - -.video-toast.system-toast .toast-msg { - font-size: 0.85rem; - font-style: italic; - color: rgba(255, 255, 255, 0.8); - margin: 0; -} - -@keyframes slideUp { - from { - transform: translateX(-50%) translateY(20px); - opacity: 0; - } - to { - transform: translateX(-50%) translateY(0); - opacity: 1; - } -} - -@keyframes fadeOut { - from { - opacity: 1; - } - to { - opacity: 0; - } -} - -@keyframes slideUp { - from { - transform: translateX(-50%) translateY(20px); - opacity: 0; - } - to { - transform: translateX(-50%) translateY(0); - opacity: 1; - } -} - -@keyframes slideDown { - from { - transform: translateX(-50%) translateY(-20px); - opacity: 0; - } - to { - transform: translateX(-50%) translateY(0); - opacity: 1; - } -} - -@keyframes fadeOut { - from { - opacity: 1; - } - to { - opacity: 0; - } -} - -#copy-invite-btn { - transition: all 0.3s ease; -} - -#copy-invite-btn:hover { - transform: scale(1.05); -} - -#copy-invite-btn:active { - transform: scale(0.95); -} - -@keyframes scaleIn { - from { - transform: translate(-50%, -50%) scale(0.8); - opacity: 0; - } - to { - transform: translate(-50%, -50%) scale(1); - opacity: 1; - } -} - -.sidebar-tabs { - display: flex; - border-bottom: 1px solid var(--glass-border); - background: rgba(0,0,0,0.2); -} - -.tab-btn { - flex: 1; - background: transparent; - border: none; - color: var(--text-muted); - padding: 14px; - font-weight: 600; - cursor: pointer; - border-bottom: 2px solid transparent; - transition: all 0.2s; -} - -.tab-btn:hover { color: white; background: rgba(255,255,255,0.05); } -.tab-btn.active { color: white; border-bottom-color: var(--brand-color); background: rgba(255,255,255,0.02); } - -.tab-content { - display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; -} - -.queue-list { - padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; -} - -.queue-item { - display: flex; gap: 10px; padding: 10px; - background: rgba(255,255,255,0.05); border-radius: 8px; - border: 1px solid transparent; position: relative; -} - -.queue-item:hover { background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.1); } - -.q-img { width: 50px; height: 70px; object-fit: cover; border-radius: 4px; } -.q-info { flex: 1; display: flex; flex-direction: column; justify-content: center; overflow: hidden; } -.q-title { font-weight: 700; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.q-meta { font-size: 0.8rem; color: #aaa; } -.q-remove { - position: absolute; top: 5px; right: 5px; - background: rgba(0,0,0,0.5); border: none; color: #ff6b6b; - width: 24px; height: 24px; border-radius: 4px; cursor: pointer; - display: flex; align-items: center; justify-content: center; -} -.q-remove:hover { background: #ff6b6b; color: white; } - -.badge { background: var(--brand-color); padding: 1px 6px; border-radius: 10px; font-size: 0.7rem; margin-left: 6px; } -.queue-empty { text-align: center; color: #666; margin-top: 40px; font-style: italic; } - -.chat-sidebar-wrapper { - display: flex; - flex-direction: column; - height: 100vh; - background: rgba(15, 15, 15, 0.95); - border-left: 1px solid var(--glass-border); - overflow: hidden; -} - -@media (max-width: 1200px) { - .chat-sidebar-wrapper { - height: 400px; - border-left: none; - border-top: 1px solid var(--glass-border); - } -} - -.queue-item { - display: flex; - gap: 10px; - padding: 10px; - background: rgba(255,255,255,0.05); - border-radius: 8px; - border: 1px solid transparent; - position: relative; - align-items: center; - transition: transform 0.2s; -} - -.queue-item:hover { - background: rgba(255,255,255,0.08); - border-color: rgba(255,255,255,0.1); -} - -.q-img { width: 50px; height: 70px; object-fit: cover; border-radius: 4px; flex-shrink: 0; } -.q-info { flex: 1; display: flex; flex-direction: column; justify-content: center; overflow: hidden; } - -.q-actions { - display: flex; - flex-direction: column; - gap: 4px; - opacity: 0.7; - transition: opacity 0.2s; -} -.queue-item:hover .q-actions { opacity: 1; } - -.q-btn { - background: rgba(255,255,255,0.1); - border: none; - color: white; - width: 28px; - height: 28px; - border-radius: 6px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s; -} - -.q-btn:hover { background: rgba(255,255,255,0.2); transform: scale(1.1); } -.q-btn.play:hover { background: var(--brand-color); } -.q-btn.remove:hover { background: #ff6b6b; } - -.chat-sidebar { - display: flex; - flex-direction: column; - height: 100%; - background: rgba(15, 15, 15, 0.95); - border-left: 1px solid var(--glass-border); - overflow: hidden; -} - -.sidebar-tabs { - display: flex; - width: 100%; - flex: 0 0 auto; - height: 50px; - border-bottom: 1px solid var(--glass-border); - background: rgba(0,0,0,0.2); -} - -.tab-btn { - flex: 1; - height: 100%; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - background: transparent; - border: none; - color: var(--text-muted); - font-weight: 600; - cursor: pointer; - border-bottom: 2px solid transparent; - transition: all 0.2s; -} - -.tab-btn:hover { background: rgba(255,255,255,0.05); color: white; } -.tab-btn.active { border-bottom-color: var(--brand-color); color: white; background: rgba(255,255,255,0.02); } - -.tab-content { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - overflow: hidden; -} - -.queue-list { - flex: 1; - overflow-y: auto; - padding: 10px; - display: flex; - flex-direction: column; - gap: 10px; -} - -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 16px; - display: flex; - flex-direction: column; - gap: 12px; } \ No newline at end of file diff --git a/docker/views/room.html b/docker/views/room.html index ed4f349..4e6b280 100644 --- a/docker/views/room.html +++ b/docker/views/room.html @@ -39,23 +39,32 @@
Dub
- +
- -
@@ -68,7 +77,6 @@ - +
Cover - -
-
Episode
-
- - - -
-
-
-
-
-
Source
-
+
+ +
+ +
+
Select Episodes
+
+ +
-
-
Audio
- -
-
Select Server
-
-
Select a source first
+ + +
+ + +
- - - -
- - - -