better UX and UI on the room page

This commit is contained in:
2026-01-05 00:00:12 +01:00
parent 6e51bf8568
commit 5cf034200e
13 changed files with 1984 additions and 2341 deletions

View File

@@ -10,6 +10,15 @@ interface RoomUser {
userId?: number; userId?: number;
} }
interface SourceContext {
animeId: string;
episode: string | number;
source: string;
extension: string;
server: string;
category: string;
}
export interface QueueItem { export interface QueueItem {
uid: string; uid: string;
metadata: RoomMetadata; metadata: RoomMetadata;
@@ -23,6 +32,7 @@ interface RoomMetadata {
episode: number; episode: number;
image?: string; image?: string;
source?: string; source?: string;
malId?: number;
} }
interface RoomData { interface RoomData {
@@ -38,6 +48,7 @@ interface RoomData {
videoData?: any; videoData?: any;
currentTime: number; currentTime: number;
isPlaying: boolean; isPlaying: boolean;
context?: SourceContext;
} | null; } | null;
password?: string; password?: string;
metadata?: RoomMetadata | null; metadata?: RoomMetadata | null;
@@ -168,7 +179,10 @@ export function updateRoomVideo(roomId: string, videoData: any): boolean {
const room = rooms.get(roomId); const room = rooms.get(roomId);
if (!room) return false; if (!room) return false;
room.currentVideo = videoData; room.currentVideo = {
...room.currentVideo,
...videoData
};
return true; return true;
} }

View File

@@ -303,14 +303,34 @@ function handleMessage(roomId: string, userId: string, data: any) {
broadcastToRoom(roomId, { broadcastToRoom(roomId, {
type: 'video_update', type: 'video_update',
video: data.video, video: data.video,
metadata: data.metadata // ✅ CLAVE metadata: data.metadata
}); });
break; 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': case 'sync':
// Solo el host puede hacer sync
if (room.host.id !== userId) return; if (room.host.id !== userId) return;
if (room.currentVideo) { if (room.currentVideo) {

View File

@@ -78,13 +78,11 @@ const AnimePlayer = (function() {
initElements(); initElements();
setupEventListeners(); setupEventListeners();
// In Room Mode, we show the player immediately and hide extra controls
if (_roomMode) { if (_roomMode) {
if(els.playerWrapper) { if(els.playerWrapper) {
els.playerWrapper.style.display = 'block'; els.playerWrapper.style.display = 'block';
els.playerWrapper.classList.add('room-mode'); els.playerWrapper.classList.add('room-mode');
} }
// Hide extension list loading in room mode
} else { } else {
loadExtensionsList(); loadExtensionsList();
} }
@@ -128,10 +126,8 @@ const AnimePlayer = (function() {
els.progressBuffer = document.querySelector('.progress-buffer'); els.progressBuffer = document.querySelector('.progress-buffer');
els.progressHandle = document.querySelector('.progress-handle'); els.progressHandle = document.querySelector('.progress-handle');
// Subtitles canvas
els.subtitlesCanvas = document.getElementById('subtitles-canvas'); els.subtitlesCanvas = document.getElementById('subtitles-canvas');
// Create skip button if not exists
if (!document.getElementById('skip-overlay-btn')) { if (!document.getElementById('skip-overlay-btn')) {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.id = 'skip-overlay-btn'; btn.id = 'skip-overlay-btn';
@@ -143,13 +139,11 @@ const AnimePlayer = (function() {
} }
function setupEventListeners() { function setupEventListeners() {
// Close player
if(!_roomMode) { if(!_roomMode) {
const closeBtn = document.getElementById('close-player-btn'); const closeBtn = document.getElementById('close-player-btn');
if(closeBtn) closeBtn.addEventListener('click', closePlayer); if(closeBtn) closeBtn.addEventListener('click', closePlayer);
} }
// Episode navigation
if(els.prevBtn) els.prevBtn.onclick = () => playEpisode(_currentEpisode - 1); if(els.prevBtn) els.prevBtn.onclick = () => playEpisode(_currentEpisode - 1);
if(els.nextBtn) els.nextBtn.onclick = () => playEpisode(_currentEpisode + 1); if(els.nextBtn) els.nextBtn.onclick = () => playEpisode(_currentEpisode + 1);
@@ -203,6 +197,15 @@ const AnimePlayer = (function() {
return; 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 || []; _currentSubtitles = videoData.subtitles || [];
if (els.loader) els.loader.style.display = 'none'; if (els.loader) els.loader.style.display = 'none';

View File

@@ -118,7 +118,7 @@ class CreateRoomModal {
this.close(); this.close();
window.open(`/room?id=${data.room.id}`, '_blank', 'noopener,noreferrer'); window.location.href = `/room?id=${data.room.id}`
} catch (err) { } catch (err) {
alert(err.message); alert(err.message);
} finally { } finally {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,8 @@
<link rel="stylesheet" href="/views/css/globals.css" /> <link rel="stylesheet" href="/views/css/globals.css" />
<link rel="stylesheet" href="/views/css/anime/player.css" /> <link rel="stylesheet" href="/views/css/anime/player.css" />
<link rel="stylesheet" href="/views/css/room.css" /> <link rel="stylesheet" href="/views/css/room.css" />
<link rel="stylesheet" href="/views/css/components/titlebar.css"> <link rel="stylesheet" href="/views/css/components/create-room.css"/>
<link rel="stylesheet" href="/views/css/components/titlebar.css"/>
<script src="/src/scripts/titlebar.js"></script> <script src="/src/scripts/titlebar.js"></script>
</head> </head>
<body> <body>
@@ -23,7 +24,7 @@
<span class="app-title">WaifuBoard</span> <span class="app-title">WaifuBoard</span>
</div> </div>
<div class="title-right"> <div class="title-right">
<button class="min"></button> <button class="min"></button>
<button class="max">🗖</button> <button class="max">🗖</button>
<button class="close"></button> <button class="close"></button>
</div> </div>
@@ -51,23 +52,32 @@
<div class="sd-option" data-val="dub">Dub</div> <div class="sd-option" data-val="dub">Dub</div>
</div> </div>
<select id="room-ext-select" class="glass-select-sm" title="Extension"> <div class="control-divider"></div>
<option value="" disabled selected>Ext</option>
</select>
<select id="room-server-select" class="glass-select-sm" title="Server"> <div class="quick-select-wrapper">
<option value="" disabled selected>Server</option> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></svg>
</select> <select id="room-ext-select" class="quick-select" title="Extension">
<button <option value="" disabled selected>Extension</option>
id="copy-invite-btn" </select>
class="btn-icon-glass" <div class="select-arrow"></div>
title="Copy invite link" </div>
style="display:none;"
> <div class="quick-select-wrapper">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>
<select id="room-server-select" class="quick-select" title="Server">
<option value="" disabled selected>Server</option>
</select>
<div class="select-arrow"></div>
</div>
<div class="control-divider"></div>
<button id="copy-invite-btn" class="btn-quick-action" title="Copy invite link" style="display:none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 0 0 7.07 0l1.41-1.41a5 5 0 0 0-7.07-7.07L10 5"/> <path d="M10 13a5 5 0 0 0 7.07 0l1.41-1.41a5 5 0 0 0-7.07-7.07L10 5"/>
<path d="M14 11a5 5 0 0 0-7.07 0L5.5 12.41a5 5 0 0 0 7.07 7.07L14 19"/> <path d="M14 11a5 5 0 0 0-7.07 0L5.5 12.41a5 5 0 0 0 7.07 7.07L14 19"/>
</svg> </svg>
<span>Invite</span>
</button> </button>
</div> </div>
</div> </div>
@@ -80,7 +90,6 @@
<button id="select-anime-btn" class="btn-glass-primary" style="display: none;"> <button id="select-anime-btn" class="btn-glass-primary" style="display: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg>
<span>Change Anime</span>
</button> </button>
<button id="toggle-chat-btn" class="btn-icon-glass" title="Toggle Chat"> <button id="toggle-chat-btn" class="btn-icon-glass" title="Toggle Chat">
@@ -219,59 +228,62 @@
<button class="btn-icon-small" id="back-to-search" title="Back"> <button class="btn-icon-small" id="back-to-search" title="Back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
</button> </button>
<h2 class="modal-title" id="selected-anime-title">Configure Stream</h2> <h2 class="modal-title" id="selected-anime-title">Configure Episode</h2>
</div> </div>
<div class="config-layout"> <div class="config-layout">
<div class="config-sidebar"> <div class="config-sidebar">
<img id="config-cover" class="config-cover" src="" alt="Cover"> <img id="config-cover" class="config-cover" src="" alt="Cover">
<div style="width: 100%">
<div class="cfg-section-title">Episode</div>
<div class="ep-control">
<button class="ep-btn" id="ep-dec"></button>
<input type="number" id="inp-episode" class="ep-input" value="1" min="1">
<button class="ep-btn" id="ep-inc">+</button>
</div>
</div>
</div> </div>
<div class="config-main"> <div class="config-main" style="justify-content: center;">
<div style="display:flex; justify-content:space-between; align-items:end; gap:10px; flex-wrap:wrap;">
<div> <div style="display: flex; flex-direction: column; height: 100%;">
<div class="cfg-section-title">Source</div>
<div class="chips-grid" id="ext-chips-container"> <div class="cfg-header" style="margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
<div class="cfg-section-title">Select Episodes</div>
<div class="cfg-actions">
<button id="select-all-page" class="btn-text-tiny">Select Page</button>
<button id="select-none-eps" class="btn-text-tiny">Clear</button>
</div> </div>
</div> </div>
<div> <div id="episodes-grid" class="episodes-grid-container">
<div class="cfg-section-title">Audio</div> <div class="grid-loader">Loading...</div>
<div class="cat-toggle" id="modal-sd-toggle"> </div>
<div class="cat-opt active" data-val="sub">Subtitles</div>
<div class="cat-opt" data-val="dub">Dubbed</div> <div id="modal-pagination" class="modal-pagination" style="display: none;">
<button id="modal-prev-page" class="modal-page-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
</button>
<span id="modal-page-info" class="modal-page-info">1 - 50</span>
<button id="modal-next-page" class="modal-page-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
</button>
</div>
<div class="manual-ep-input" style="margin-top: 10px; display: none;">
<label style="font-size: 0.8rem; color: #aaa;">Manual Entry:</label>
<div class="ep-control">
<button class="ep-btn" id="ep-dec"></button>
<input type="number" id="inp-episode" class="ep-input" value="1" min="1">
<button class="ep-btn" id="ep-inc">+</button>
</div> </div>
</div> </div>
</div>
<div> <div id="config-error" style="color:#ff6b6b; font-size:0.9rem; display:none; background:rgba(255,0,0,0.1); padding:10px; border-radius:8px; margin-top: 10px;"></div>
<div class="cfg-section-title">Select Server</div>
<div class="chips-grid" id="server-chips-container"> <div class="form-actions" style="margin-top:auto; display:flex; gap:10px; padding-top: 20px;">
<div class="grid-loader">Select a source first</div> <button id="btn-add-queue" class="btn-cancel" style="flex:1; justify-content: center; border-color: var(--brand-color); color: white;">
+ Add Selected (<span id="sel-count">0</span>)
</button>
<button id="btn-launch-stream" class="btn-confirm" style="flex:1;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
Play First
</button>
</div> </div>
</div> </div>
<div id="config-error" style="color:#ff6b6b; font-size:0.9rem; display:none; background:rgba(255,0,0,0.1); padding:10px; border-radius:8px;"></div>
<div class="form-actions" style="margin-top:auto; display:flex; gap:10px;">
<button id="btn-add-queue" class="btn-cancel" style="flex:1; justify-content: center; border-color: var(--brand-color); color: white;" disabled>
+ Add to Queue
</button>
<button id="btn-launch-stream" class="btn-confirm" style="flex:1;" disabled>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
Play Now
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -10,6 +10,15 @@ interface RoomUser {
userId?: number; userId?: number;
} }
interface SourceContext {
animeId: string;
episode: string | number;
source: string;
extension: string;
server: string;
category: string;
}
export interface QueueItem { export interface QueueItem {
uid: string; uid: string;
metadata: RoomMetadata; metadata: RoomMetadata;
@@ -23,6 +32,7 @@ interface RoomMetadata {
episode: number; episode: number;
image?: string; image?: string;
source?: string; source?: string;
malId?: number;
} }
interface RoomData { interface RoomData {
@@ -38,6 +48,7 @@ interface RoomData {
videoData?: any; videoData?: any;
currentTime: number; currentTime: number;
isPlaying: boolean; isPlaying: boolean;
context?: SourceContext;
} | null; } | null;
password?: string; password?: string;
metadata?: RoomMetadata | null; metadata?: RoomMetadata | null;
@@ -168,7 +179,10 @@ export function updateRoomVideo(roomId: string, videoData: any): boolean {
const room = rooms.get(roomId); const room = rooms.get(roomId);
if (!room) return false; if (!room) return false;
room.currentVideo = videoData; room.currentVideo = {
...room.currentVideo,
...videoData
};
return true; return true;
} }

View File

@@ -303,14 +303,34 @@ function handleMessage(roomId: string, userId: string, data: any) {
broadcastToRoom(roomId, { broadcastToRoom(roomId, {
type: 'video_update', type: 'video_update',
video: data.video, video: data.video,
metadata: data.metadata // ✅ CLAVE metadata: data.metadata
}); });
break; 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': case 'sync':
// Solo el host puede hacer sync
if (room.host.id !== userId) return; if (room.host.id !== userId) return;
if (room.currentVideo) { if (room.currentVideo) {

View File

@@ -78,13 +78,11 @@ const AnimePlayer = (function() {
initElements(); initElements();
setupEventListeners(); setupEventListeners();
// In Room Mode, we show the player immediately and hide extra controls
if (_roomMode) { if (_roomMode) {
if(els.playerWrapper) { if(els.playerWrapper) {
els.playerWrapper.style.display = 'block'; els.playerWrapper.style.display = 'block';
els.playerWrapper.classList.add('room-mode'); els.playerWrapper.classList.add('room-mode');
} }
// Hide extension list loading in room mode
} else { } else {
loadExtensionsList(); loadExtensionsList();
} }
@@ -128,10 +126,8 @@ const AnimePlayer = (function() {
els.progressBuffer = document.querySelector('.progress-buffer'); els.progressBuffer = document.querySelector('.progress-buffer');
els.progressHandle = document.querySelector('.progress-handle'); els.progressHandle = document.querySelector('.progress-handle');
// Subtitles canvas
els.subtitlesCanvas = document.getElementById('subtitles-canvas'); els.subtitlesCanvas = document.getElementById('subtitles-canvas');
// Create skip button if not exists
if (!document.getElementById('skip-overlay-btn')) { if (!document.getElementById('skip-overlay-btn')) {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.id = 'skip-overlay-btn'; btn.id = 'skip-overlay-btn';
@@ -143,13 +139,11 @@ const AnimePlayer = (function() {
} }
function setupEventListeners() { function setupEventListeners() {
// Close player
if(!_roomMode) { if(!_roomMode) {
const closeBtn = document.getElementById('close-player-btn'); const closeBtn = document.getElementById('close-player-btn');
if(closeBtn) closeBtn.addEventListener('click', closePlayer); if(closeBtn) closeBtn.addEventListener('click', closePlayer);
} }
// Episode navigation
if(els.prevBtn) els.prevBtn.onclick = () => playEpisode(_currentEpisode - 1); if(els.prevBtn) els.prevBtn.onclick = () => playEpisode(_currentEpisode - 1);
if(els.nextBtn) els.nextBtn.onclick = () => playEpisode(_currentEpisode + 1); if(els.nextBtn) els.nextBtn.onclick = () => playEpisode(_currentEpisode + 1);
@@ -203,6 +197,15 @@ const AnimePlayer = (function() {
return; 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 || []; _currentSubtitles = videoData.subtitles || [];
if (els.loader) els.loader.style.display = 'none'; if (els.loader) els.loader.style.display = 'none';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -39,23 +39,32 @@
<div class="sd-option" data-val="dub">Dub</div> <div class="sd-option" data-val="dub">Dub</div>
</div> </div>
<select id="room-ext-select" class="glass-select-sm" title="Extension"> <div class="control-divider"></div>
<option value="" disabled selected>Ext</option>
</select>
<select id="room-server-select" class="glass-select-sm" title="Server"> <div class="quick-select-wrapper">
<option value="" disabled selected>Server</option> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></svg>
</select> <select id="room-ext-select" class="quick-select" title="Extension">
<button <option value="" disabled selected>Extension</option>
id="copy-invite-btn" </select>
class="btn-icon-glass" <div class="select-arrow"></div>
title="Copy invite link" </div>
style="display:none;"
> <div class="quick-select-wrapper">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>
<select id="room-server-select" class="quick-select" title="Server">
<option value="" disabled selected>Server</option>
</select>
<div class="select-arrow"></div>
</div>
<div class="control-divider"></div>
<button id="copy-invite-btn" class="btn-quick-action" title="Copy invite link" style="display:none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 0 0 7.07 0l1.41-1.41a5 5 0 0 0-7.07-7.07L10 5"/> <path d="M10 13a5 5 0 0 0 7.07 0l1.41-1.41a5 5 0 0 0-7.07-7.07L10 5"/>
<path d="M14 11a5 5 0 0 0-7.07 0L5.5 12.41a5 5 0 0 0 7.07 7.07L14 19"/> <path d="M14 11a5 5 0 0 0-7.07 0L5.5 12.41a5 5 0 0 0 7.07 7.07L14 19"/>
</svg> </svg>
<span>Invite</span>
</button> </button>
</div> </div>
</div> </div>
@@ -68,7 +77,6 @@
<button id="select-anime-btn" class="btn-glass-primary" style="display: none;"> <button id="select-anime-btn" class="btn-glass-primary" style="display: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg>
<span>Change Anime</span>
</button> </button>
<button id="toggle-chat-btn" class="btn-icon-glass" title="Toggle Chat"> <button id="toggle-chat-btn" class="btn-icon-glass" title="Toggle Chat">
@@ -207,59 +215,62 @@
<button class="btn-icon-small" id="back-to-search" title="Back"> <button class="btn-icon-small" id="back-to-search" title="Back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
</button> </button>
<h2 class="modal-title" id="selected-anime-title">Configure Stream</h2> <h2 class="modal-title" id="selected-anime-title">Configure Episode</h2>
</div> </div>
<div class="config-layout"> <div class="config-layout">
<div class="config-sidebar"> <div class="config-sidebar">
<img id="config-cover" class="config-cover" src="" alt="Cover"> <img id="config-cover" class="config-cover" src="" alt="Cover">
<div style="width: 100%">
<div class="cfg-section-title">Episode</div>
<div class="ep-control">
<button class="ep-btn" id="ep-dec"></button>
<input type="number" id="inp-episode" class="ep-input" value="1" min="1">
<button class="ep-btn" id="ep-inc">+</button>
</div>
</div>
</div> </div>
<div class="config-main"> <div class="config-main" style="justify-content: center;">
<div style="display:flex; justify-content:space-between; align-items:end; gap:10px; flex-wrap:wrap;">
<div> <div style="display: flex; flex-direction: column; height: 100%;">
<div class="cfg-section-title">Source</div>
<div class="chips-grid" id="ext-chips-container"> <div class="cfg-header" style="margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
<div class="cfg-section-title">Select Episodes</div>
<div class="cfg-actions">
<button id="select-all-page" class="btn-text-tiny">Select Page</button>
<button id="select-none-eps" class="btn-text-tiny">Clear</button>
</div> </div>
</div> </div>
<div> <div id="episodes-grid" class="episodes-grid-container">
<div class="cfg-section-title">Audio</div> <div class="grid-loader">Loading...</div>
<div class="cat-toggle" id="modal-sd-toggle"> </div>
<div class="cat-opt active" data-val="sub">Subtitles</div>
<div class="cat-opt" data-val="dub">Dubbed</div> <div id="modal-pagination" class="modal-pagination" style="display: none;">
<button id="modal-prev-page" class="modal-page-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
</button>
<span id="modal-page-info" class="modal-page-info">1 - 50</span>
<button id="modal-next-page" class="modal-page-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
</button>
</div>
<div class="manual-ep-input" style="margin-top: 10px; display: none;">
<label style="font-size: 0.8rem; color: #aaa;">Manual Entry:</label>
<div class="ep-control">
<button class="ep-btn" id="ep-dec"></button>
<input type="number" id="inp-episode" class="ep-input" value="1" min="1">
<button class="ep-btn" id="ep-inc">+</button>
</div> </div>
</div> </div>
</div>
<div> <div id="config-error" style="color:#ff6b6b; font-size:0.9rem; display:none; background:rgba(255,0,0,0.1); padding:10px; border-radius:8px; margin-top: 10px;"></div>
<div class="cfg-section-title">Select Server</div>
<div class="chips-grid" id="server-chips-container"> <div class="form-actions" style="margin-top:auto; display:flex; gap:10px; padding-top: 20px;">
<div class="grid-loader">Select a source first</div> <button id="btn-add-queue" class="btn-cancel" style="flex:1; justify-content: center; border-color: var(--brand-color); color: white;">
+ Add Selected (<span id="sel-count">0</span>)
</button>
<button id="btn-launch-stream" class="btn-confirm" style="flex:1;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
Play First
</button>
</div> </div>
</div> </div>
<div id="config-error" style="color:#ff6b6b; font-size:0.9rem; display:none; background:rgba(255,0,0,0.1); padding:10px; border-radius:8px;"></div>
<div class="form-actions" style="margin-top:auto; display:flex; gap:10px;">
<button id="btn-add-queue" class="btn-cancel" style="flex:1; justify-content: center; border-color: var(--brand-color); color: white;" disabled>
+ Add to Queue
</button>
<button id="btn-launch-stream" class="btn-confirm" style="flex:1;" disabled>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
Play Now
</button>
</div>
</div> </div>
</div> </div>
</div> </div>