added user permissions to rooms
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import crypto from 'crypto';
|
||||
import { closeTunnelIfUnused } from "./tunnel.manager";
|
||||
|
||||
interface RoomPermissions {
|
||||
canControl: boolean;
|
||||
canManageQueue: boolean;
|
||||
}
|
||||
|
||||
interface RoomUser {
|
||||
id: string;
|
||||
username: string;
|
||||
@@ -8,6 +13,8 @@ interface RoomUser {
|
||||
isHost: boolean;
|
||||
isGuest: boolean;
|
||||
userId?: number;
|
||||
permissions?: RoomPermissions;
|
||||
ipAddress?: string;
|
||||
}
|
||||
|
||||
interface SourceContext {
|
||||
@@ -55,8 +62,14 @@ interface RoomData {
|
||||
exposed: boolean;
|
||||
publicUrl?: string;
|
||||
queue: QueueItem[];
|
||||
bannedIPs: Set<string>;
|
||||
}
|
||||
|
||||
export const DEFAULT_GUEST_PERMISSIONS: RoomPermissions = {
|
||||
canControl: false,
|
||||
canManageQueue: false
|
||||
};
|
||||
|
||||
const rooms = new Map<string, RoomData>();
|
||||
|
||||
export function generateRoomId(): string {
|
||||
@@ -77,13 +90,81 @@ export function createRoom(name: string, host: RoomUser, password?: string, expo
|
||||
metadata: null,
|
||||
exposed,
|
||||
publicUrl,
|
||||
queue: []
|
||||
queue: [],
|
||||
bannedIPs: new Set() // NUEVO
|
||||
};
|
||||
|
||||
rooms.set(roomId, room);
|
||||
return room;
|
||||
}
|
||||
|
||||
export function updateUserPermissions(roomId: string, userId: string, permissions: Partial<RoomPermissions>): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
|
||||
const user: any = room.users.get(userId);
|
||||
if (!user) return false;
|
||||
|
||||
if (user.isHost) return false;
|
||||
|
||||
user.permissions = {
|
||||
...user.permissions,
|
||||
...permissions
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function banUserIP(roomId: string, ipAddress: string): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
|
||||
room.bannedIPs.add(ipAddress);
|
||||
|
||||
// Remover a todos los usuarios con esa IP
|
||||
Array.from(room.users.values()).forEach(user => {
|
||||
if (user.ipAddress === ipAddress) {
|
||||
removeUserFromRoom(roomId, user.id);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function unbanUserIP(roomId: string, ipAddress: string): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
|
||||
return room.bannedIPs.delete(ipAddress);
|
||||
}
|
||||
|
||||
export function isIPBanned(roomId: string, ipAddress: string): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
return room.bannedIPs.has(ipAddress);
|
||||
}
|
||||
|
||||
export function getBannedIPs(roomId: string): string[] {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return [];
|
||||
return Array.from(room.bannedIPs);
|
||||
}
|
||||
|
||||
export function hasPermission(roomId: string, userId: string, permission: keyof RoomPermissions): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
|
||||
const user = room.users.get(userId);
|
||||
if (!user) return false;
|
||||
|
||||
// El host siempre tiene todos los permisos
|
||||
if (user.isHost) return true;
|
||||
|
||||
// Si no tiene permisos definidos, usar defaults
|
||||
const userPerms = user.permissions || DEFAULT_GUEST_PERMISSIONS;
|
||||
return userPerms[permission] || false;
|
||||
}
|
||||
|
||||
export function getRoom(roomId: string): RoomData | null {
|
||||
return rooms.get(roomId) || null;
|
||||
}
|
||||
|
||||
@@ -13,14 +13,13 @@ interface WSClient {
|
||||
|
||||
const clients = new Map<string, WSClient>();
|
||||
|
||||
interface WSParams {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
interface WSQuery {
|
||||
token?: string;
|
||||
guestName?: string;
|
||||
password?: string;
|
||||
function getClientIP(req: any): string {
|
||||
return req.headers['x-forwarded-for']?.split(',')[0].trim() ||
|
||||
req.headers['x-real-ip'] ||
|
||||
req.connection?.remoteAddress ||
|
||||
req.socket?.remoteAddress ||
|
||||
'unknown';
|
||||
}
|
||||
|
||||
export function setupRoomWebSocket(fastify: FastifyInstance) {
|
||||
@@ -44,13 +43,14 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
||||
const guestName = req.query.guestName;
|
||||
const password = req.query.password;
|
||||
|
||||
const clientIP = getClientIP(req); // NUEVO
|
||||
|
||||
let userId: string;
|
||||
let username: string;
|
||||
let avatar: string | undefined;
|
||||
let isGuest = false;
|
||||
let realUserId: any;
|
||||
|
||||
// Verificar si la sala existe
|
||||
const room = roomService.getRoom(roomId);
|
||||
if (!room) {
|
||||
socket.send(JSON.stringify({
|
||||
@@ -61,6 +61,24 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (roomService.isIPBanned(roomId, clientIP)) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'error',
|
||||
message: 'You have been banned from this room'
|
||||
}));
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!room) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'error',
|
||||
message: 'Room not found'
|
||||
}));
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar contraseña si existe
|
||||
if (room.password) {
|
||||
if (!password || !roomService.verifyRoomPassword(roomId, password)) {
|
||||
@@ -134,14 +152,15 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
||||
isHost
|
||||
});
|
||||
|
||||
// Agregar usuario a la sala
|
||||
const userInRoom = {
|
||||
id: userId,
|
||||
username,
|
||||
avatar,
|
||||
isHost: isHost, // ← CORREGIDO: Usar la verificación correcta
|
||||
isHost: isHost,
|
||||
isGuest,
|
||||
userId: realUserId
|
||||
userId: realUserId,
|
||||
ipAddress: clientIP, // NUEVO
|
||||
permissions: isHost ? undefined : { ...roomService.DEFAULT_GUEST_PERMISSIONS }
|
||||
};
|
||||
|
||||
roomService.addUserToRoom(roomId, userInRoom);
|
||||
@@ -160,6 +179,7 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
||||
userId,
|
||||
username,
|
||||
isGuest,
|
||||
isHost, // NUEVO: Enviar explícitamente
|
||||
room: {
|
||||
id: room.id,
|
||||
name: room.name,
|
||||
@@ -168,7 +188,8 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
isHost: u.isHost,
|
||||
isGuest: u.isGuest
|
||||
isGuest: u.isGuest,
|
||||
permissions: u.permissions // NUEVO
|
||||
})),
|
||||
currentVideo: room.currentVideo,
|
||||
queue: room.queue || []
|
||||
@@ -212,6 +233,9 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
const room = roomService.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
const user = room.users.get(userId);
|
||||
if (!user) return;
|
||||
|
||||
console.log('Handling message:', data.type, 'from user:', userId, 'isHost:', room.host.id === userId);
|
||||
|
||||
switch (data.type) {
|
||||
@@ -226,8 +250,65 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
});
|
||||
break;
|
||||
|
||||
case 'update_permissions':
|
||||
if (!user.isHost) {
|
||||
console.warn('Non-host attempted to update permissions');
|
||||
return;
|
||||
}
|
||||
|
||||
const success = roomService.updateUserPermissions(
|
||||
roomId,
|
||||
data.targetUserId,
|
||||
data.permissions
|
||||
);
|
||||
|
||||
if (success) {
|
||||
const updatedRoom = roomService.getRoom(roomId);
|
||||
if (updatedRoom) {
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'permissions_updated',
|
||||
users: Array.from(updatedRoom.users.values()).map(u => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
isHost: u.isHost,
|
||||
isGuest: u.isGuest,
|
||||
permissions: u.permissions
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// NUEVO: Baneo de usuarios (solo host)
|
||||
case 'ban_user':
|
||||
if (!user.isHost) {
|
||||
console.warn('Non-host attempted to ban user');
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUser = room.users.get(data.targetUserId);
|
||||
if (targetUser && targetUser.ipAddress) {
|
||||
roomService.banUserIP(roomId, targetUser.ipAddress);
|
||||
|
||||
// Cerrar conexión del usuario baneado
|
||||
const targetClient = clients.get(data.targetUserId);
|
||||
if (targetClient && targetClient.socket) {
|
||||
targetClient.socket.send(JSON.stringify({
|
||||
type: 'banned',
|
||||
message: 'You have been banned from this room'
|
||||
}));
|
||||
targetClient.socket.close();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'queue_play_item':
|
||||
if (room.host.id !== userId) return;
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||
console.warn('User lacks permission for queue management');
|
||||
return;
|
||||
}
|
||||
|
||||
const itemToPlay = roomService.getAndRemoveQueueItem(roomId, data.itemUid);
|
||||
if (itemToPlay) {
|
||||
@@ -255,7 +336,9 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'queue_move':
|
||||
if (room.host.id !== userId) return;
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moved = roomService.moveQueueItem(roomId, data.itemUid, data.direction);
|
||||
if (moved) {
|
||||
@@ -295,7 +378,14 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'video_update':
|
||||
if (room.host.id !== userId) return;
|
||||
const canUpdateVideo = user.isHost ||
|
||||
roomService.hasPermission(roomId, userId, 'canControl') ||
|
||||
roomService.hasPermission(roomId, userId, 'canManageQueue');
|
||||
|
||||
if (!canUpdateVideo) {
|
||||
console.warn('User lacks permissions to update video');
|
||||
return;
|
||||
}
|
||||
|
||||
roomService.updateRoomVideo(roomId, data.video);
|
||||
roomService.updateRoomMetadata(roomId, data.metadata);
|
||||
@@ -308,13 +398,13 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'queue_add_batch':
|
||||
if (room.host.id !== userId) return;
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||
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,
|
||||
@@ -365,13 +455,11 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'play':
|
||||
// Solo el host puede controlar la reproducción
|
||||
if (room.host.id !== userId) {
|
||||
console.warn('Non-host attempted play:', userId);
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canControl')) {
|
||||
console.warn('User lacks control permissions');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Broadcasting play event to room:', roomId);
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'play',
|
||||
currentTime: data.currentTime,
|
||||
@@ -380,8 +468,8 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'pause':
|
||||
if (room.host.id !== userId) {
|
||||
console.warn('Non-host attempted pause:', userId);
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canControl')) {
|
||||
console.warn('User lacks control permissions for pause');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -394,8 +482,8 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'seek':
|
||||
if (room.host.id !== userId) {
|
||||
console.warn('Non-host attempted seek:', userId);
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canControl')) {
|
||||
console.warn('User lacks control permissions for seek');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -436,19 +524,19 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'queue_add':
|
||||
// Solo el host (o todos si quisieras) pueden añadir
|
||||
if (room.host.id !== userId) return;
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newItem = {
|
||||
uid: `q_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
|
||||
metadata: data.metadata,
|
||||
videoData: data.video, // El objeto que contiene url, type, subtitles
|
||||
videoData: data.video,
|
||||
addedBy: room.users.get(userId)?.username || 'Unknown'
|
||||
};
|
||||
|
||||
roomService.addQueueItem(roomId, newItem);
|
||||
|
||||
// Avisar a todos que la cola cambió
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'queue_update',
|
||||
queue: room.queue
|
||||
@@ -456,7 +544,10 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'queue_remove':
|
||||
if (room.host.id !== userId) return;
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||
return;
|
||||
}
|
||||
|
||||
roomService.removeQueueItem(roomId, data.itemUid);
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'queue_update',
|
||||
|
||||
@@ -402,8 +402,8 @@ const AnimePlayer = (function() {
|
||||
|
||||
// Control functions
|
||||
function togglePlayPause() {
|
||||
if (_roomMode && !_isRoomHost) {
|
||||
console.log('Guests cannot control playback');
|
||||
if (_roomMode && !_isRoomHost && !hasControlPermission()) {
|
||||
showPermissionToast('You need playback control permission');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -411,12 +411,14 @@ const AnimePlayer = (function() {
|
||||
|
||||
if (els.video.paused) {
|
||||
els.video.play().catch(() => {});
|
||||
if (_roomMode && _isRoomHost) {
|
||||
|
||||
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||
sendRoomEvent('play', { currentTime: els.video.currentTime });
|
||||
}
|
||||
} else {
|
||||
els.video.pause();
|
||||
if (_roomMode && _isRoomHost) {
|
||||
|
||||
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||
sendRoomEvent('pause', { currentTime: els.video.currentTime });
|
||||
}
|
||||
}
|
||||
@@ -460,7 +462,7 @@ const AnimePlayer = (function() {
|
||||
|
||||
function seekToPosition(e) {
|
||||
if (!els.video || !els.progressContainer) return;
|
||||
if (_roomMode && !_isRoomHost) return;
|
||||
if (_roomMode && !_isRoomHost && !hasControlPermission()) return;
|
||||
|
||||
const rect = els.progressContainer.getBoundingClientRect();
|
||||
const pos = (e.clientX - rect.left) / rect.width;
|
||||
@@ -468,8 +470,7 @@ const AnimePlayer = (function() {
|
||||
|
||||
els.video.currentTime = newTime;
|
||||
|
||||
// En room mode, enviar evento de seek
|
||||
if (_roomMode && _isRoomHost) {
|
||||
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||
sendRoomEvent('seek', { currentTime: newTime });
|
||||
}
|
||||
}
|
||||
@@ -484,29 +485,63 @@ const AnimePlayer = (function() {
|
||||
|
||||
function seekRelative(seconds) {
|
||||
if (!els.video) return;
|
||||
if (_roomMode && !_isRoomHost) return;
|
||||
if (_roomMode && !_isRoomHost && !hasControlPermission()) {
|
||||
showPermissionToast('You need playback control permission');
|
||||
return;
|
||||
}
|
||||
|
||||
const newTime = Math.max(0, Math.min(els.video.duration, els.video.currentTime + seconds));
|
||||
els.video.currentTime = newTime;
|
||||
|
||||
// En room mode, enviar evento de seek
|
||||
if (_roomMode && _isRoomHost) {
|
||||
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||
sendRoomEvent('seek', { currentTime: newTime });
|
||||
}
|
||||
}
|
||||
|
||||
function seekToPercent(percent) {
|
||||
if (!els.video) return;
|
||||
if (_roomMode && !_isRoomHost) return;
|
||||
|
||||
if (_roomMode && !_isRoomHost && !hasControlPermission()) {
|
||||
showPermissionToast('You need playback control permission');
|
||||
return;
|
||||
}
|
||||
const newTime = els.video.duration * percent;
|
||||
els.video.currentTime = newTime;
|
||||
|
||||
// En room mode, enviar evento de seek
|
||||
if (_roomMode && _isRoomHost) {
|
||||
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||
sendRoomEvent('seek', { currentTime: newTime });
|
||||
}
|
||||
}
|
||||
|
||||
function hasControlPermission() {
|
||||
return window.__userPermissions?.canControl || false;
|
||||
}
|
||||
|
||||
function showPermissionToast(message) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'permission-toast';
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(239, 68, 68, 0.95);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||
animation: fadeIn 0.3s ease;
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'fadeOut 0.3s ease';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
// Video event handlers
|
||||
function onPlay() {
|
||||
if (els.playPauseBtn) {
|
||||
@@ -589,7 +624,9 @@ const AnimePlayer = (function() {
|
||||
}
|
||||
|
||||
function sendRoomEvent(eventType, data = {}) {
|
||||
if (!_roomMode || !_isRoomHost || !_roomWebSocket) return;
|
||||
if (!_roomMode || !_roomWebSocket) return;
|
||||
if (!_isRoomHost && !hasControlPermission()) return;
|
||||
|
||||
if (_roomWebSocket.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
console.log('Sending room event:', eventType, data);
|
||||
|
||||
@@ -232,10 +232,15 @@ const RoomsApp = (function() {
|
||||
|
||||
if (elements.btnAddQueue) {
|
||||
elements.btnAddQueue.onclick = () => {
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!canManageQueue) {
|
||||
showSystemToast('You need queue management permission');
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -279,7 +284,9 @@ const RoomsApp = (function() {
|
||||
}
|
||||
if (elements.roomSdToggle) {
|
||||
elements.roomSdToggle.onclick = () => {
|
||||
if (!isHost) return;
|
||||
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
|
||||
if (!hasAccess) return;
|
||||
const currentState = elements.roomSdToggle.getAttribute('data-state');
|
||||
const newState = currentState === 'sub' ? 'dub' : 'sub';
|
||||
|
||||
@@ -338,7 +345,9 @@ const RoomsApp = (function() {
|
||||
// --- QUICK CONTROLS LOGIC (Header) ---
|
||||
|
||||
async function populateQuickControls() {
|
||||
if (!isHost || !selectedAnimeData) return;
|
||||
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
|
||||
if (!hasAccess || !selectedAnimeData) return;
|
||||
if (!extensionsReady) return;
|
||||
|
||||
elements.roomExtSelect.innerHTML = '';
|
||||
@@ -356,7 +365,8 @@ const RoomsApp = (function() {
|
||||
}
|
||||
|
||||
async function onQuickExtensionChange(e, silent = false) {
|
||||
if (!isHost) return;
|
||||
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!hasAccess) return;
|
||||
|
||||
const ext = elements.roomExtSelect.value;
|
||||
const settings = extensionsStore.settings[ext];
|
||||
@@ -433,7 +443,9 @@ const RoomsApp = (function() {
|
||||
}
|
||||
|
||||
function onQuickServerChange() {
|
||||
if (!isHost) return;
|
||||
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!hasAccess) return;
|
||||
|
||||
launchStream(false);
|
||||
}
|
||||
|
||||
@@ -488,6 +500,28 @@ const RoomsApp = (function() {
|
||||
if(elements.btnLaunch) elements.btnLaunch.disabled = false;
|
||||
if(elements.btnAddQueue) elements.btnAddQueue.disabled = false;
|
||||
|
||||
if (extensionsReady && elements.selExtension) {
|
||||
if (elements.selExtension.options.length <= 1) {
|
||||
elements.selExtension.innerHTML = '';
|
||||
|
||||
extensionsStore.list.forEach(ext => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ext;
|
||||
opt.textContent = ext[0].toUpperCase() + ext.slice(1);
|
||||
elements.selExtension.appendChild(opt);
|
||||
});
|
||||
|
||||
let defaultExt = selectedAnimeData.source || 'anilist';
|
||||
if (!extensionsStore.list.includes(defaultExt) && extensionsStore.list.length > 0) {
|
||||
defaultExt = extensionsStore.list[0];
|
||||
}
|
||||
|
||||
elements.selExtension.value = defaultExt;
|
||||
|
||||
handleModalExtensionChange();
|
||||
}
|
||||
}
|
||||
|
||||
setupConfigListeners();
|
||||
|
||||
elements.stepSearch.style.display = 'none';
|
||||
@@ -495,7 +529,7 @@ const RoomsApp = (function() {
|
||||
}
|
||||
|
||||
function setupConfigListeners() {
|
||||
elements.epInc.onclick = () => {
|
||||
elements.epInc.onclick = () => {
|
||||
elements.inpEpisode.value = parseInt(elements.inpEpisode.value || 0) + 1;
|
||||
};
|
||||
elements.epDec.onclick = () => {
|
||||
@@ -625,12 +659,24 @@ const RoomsApp = (function() {
|
||||
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;
|
||||
const ext = overrides.forceExtension ||
|
||||
(fromModal ? (configState.extension || elements.selExtension?.value) : null) ||
|
||||
activeContext.extension ||
|
||||
(elements.roomExtSelect ? elements.roomExtSelect.value : null);
|
||||
|
||||
const server = overrides.forceServer ||
|
||||
(fromModal ? (configState.server || elements.selServer?.value) : null) ||
|
||||
activeContext.server ||
|
||||
(elements.roomServerSelect ? elements.roomServerSelect.value : null);
|
||||
|
||||
const category = elements.roomSdToggle?.getAttribute('data-state') || activeContext.category || 'sub';
|
||||
|
||||
if (!ext || !server) {
|
||||
console.warn("Faltan datos (ext o server) para lanzar el stream");
|
||||
console.warn("Faltan datos (ext o server).", {ext, server});
|
||||
if (fromModal && elements.configError) {
|
||||
elements.configError.textContent = "Please select an extension and server.";
|
||||
elements.configError.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -850,7 +896,6 @@ const RoomsApp = (function() {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -918,7 +963,7 @@ const RoomsApp = (function() {
|
||||
episode: ep,
|
||||
title: currentAnimeDetails.title.userPreferred,
|
||||
server: document.getElementById('room-server-select').value || 'gogoanime',
|
||||
category: 'sub' // o lo que esté seleccionado
|
||||
category: 'sub'
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -1081,6 +1126,18 @@ const RoomsApp = (function() {
|
||||
currentUserId = data.userId;
|
||||
currentUsername = data.username;
|
||||
isGuest = data.isGuest;
|
||||
|
||||
if (data.room && data.room.users) {
|
||||
const currentUser = data.room.users.find(u => u.id === data.userId);
|
||||
if (currentUser) {
|
||||
window.__userPermissions = currentUser.permissions || {
|
||||
canControl: false,
|
||||
canManageQueue: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (data.room.queue) renderQueue(data.room.queue);
|
||||
updateRoomUI(data.room);
|
||||
|
||||
if (data.room.currentVideo) {
|
||||
@@ -1109,6 +1166,7 @@ const RoomsApp = (function() {
|
||||
break;
|
||||
|
||||
case 'users_update':
|
||||
saveRoomData({ users: data.users });
|
||||
renderUsersList(data.users);
|
||||
break;
|
||||
|
||||
@@ -1127,6 +1185,32 @@ const RoomsApp = (function() {
|
||||
updateUsersList();
|
||||
break;
|
||||
|
||||
case 'permissions_updated':
|
||||
saveRoomData({ users: data.users });
|
||||
|
||||
const updatedUser = data.users.find(u => u.id === currentUserId);
|
||||
if (updatedUser) {
|
||||
window.__userPermissions = updatedUser.permissions || {
|
||||
canControl: false,
|
||||
canManageQueue: false
|
||||
};
|
||||
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (elements.selectAnimeBtn) {
|
||||
elements.selectAnimeBtn.style.display = canManageQueue ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (currentQueue.length > 0) {
|
||||
renderQueue(currentQueue);
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionsModal && permissionsModal.classList.contains('show')) {
|
||||
updatePermissionsList();
|
||||
}
|
||||
|
||||
renderUsersList(data.users);
|
||||
break;
|
||||
case 'chat':
|
||||
addChatMessage(data);
|
||||
const isChatHidden = elements.roomLayout.classList.contains('chat-hidden');
|
||||
@@ -1210,33 +1294,34 @@ const RoomsApp = (function() {
|
||||
return;
|
||||
}
|
||||
|
||||
const canManageQueue = isHost || (window.__userPermissions && window.__userPermissions.canManageQueue);
|
||||
elements.queueList.innerHTML = queueItems.map((item, index) => {
|
||||
let actionsHtml = '';
|
||||
|
||||
if (isHost) {
|
||||
if (canManageQueue) {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === queueItems.length - 1;
|
||||
|
||||
actionsHtml = `
|
||||
<div class="q-actions">
|
||||
<button class="q-btn play" onclick="playQueueItem('${item.uid}')" title="Play Now">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
<div style="display:flex; gap:2px;">
|
||||
${!isFirst ? `
|
||||
<button class="q-btn" onclick="moveQueueItem('${item.uid}', 'up')" title="Move Up">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 15l-6-6-6 6"/></svg>
|
||||
</button>` : ''}
|
||||
${!isLast ? `
|
||||
<button class="q-btn" onclick="moveQueueItem('${item.uid}', 'down')" title="Move Down">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
|
||||
</button>` : ''}
|
||||
</div>
|
||||
<button class="q-btn remove" onclick="removeQueueItem('${item.uid}')" title="Remove">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
<div class="q-actions">
|
||||
<button class="q-btn play" onclick="playQueueItem('${item.uid}')" title="Play Now">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
<div style="display:flex; gap:2px;">
|
||||
${!isFirst ? `
|
||||
<button class="q-btn" onclick="moveQueueItem('${item.uid}', 'up')" title="Move Up">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 15l-6-6-6 6"/></svg>
|
||||
</button>` : ''}
|
||||
${!isLast ? `
|
||||
<button class="q-btn" onclick="moveQueueItem('${item.uid}', 'down')" title="Move Down">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
|
||||
</button>` : ''}
|
||||
</div>
|
||||
`;
|
||||
<button class="q-btn remove" onclick="removeQueueItem('${item.uid}')" title="Remove">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
@@ -1248,11 +1333,17 @@ const RoomsApp = (function() {
|
||||
</div>
|
||||
${actionsHtml}
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.playQueueItem = async function(uid) {
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!canManageQueue) {
|
||||
showSystemToast('You need queue management permission');
|
||||
return;
|
||||
}
|
||||
|
||||
const item = currentQueue.find(i => i.uid === uid);
|
||||
if (!item) return;
|
||||
|
||||
@@ -1305,12 +1396,25 @@ const RoomsApp = (function() {
|
||||
};
|
||||
|
||||
window.moveQueueItem = function(uid, direction) {
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!canManageQueue) {
|
||||
showSystemToast('You need queue management permission');
|
||||
return;
|
||||
}
|
||||
|
||||
if(ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'queue_move', itemUid: uid, direction }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
window.removeQueueItem = function(uid) {
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!canManageQueue) {
|
||||
showSystemToast('You need queue management permission');
|
||||
return;
|
||||
}
|
||||
|
||||
if(ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'queue_remove', itemUid: uid }));
|
||||
}
|
||||
@@ -1321,17 +1425,39 @@ const RoomsApp = (function() {
|
||||
elements.roomViewers.textContent = `${room.users.length}`;
|
||||
|
||||
const currentUser = room.users.find(u => u.id === currentUserId);
|
||||
|
||||
if (currentUser) {
|
||||
window.__userPermissions = currentUser.permissions || { canControl: false, canManageQueue: false };
|
||||
}
|
||||
isHost = currentUser?.isHost || false;
|
||||
|
||||
if (elements.selectAnimeBtn) elements.selectAnimeBtn.style.display = isHost ? 'flex' : 'none';
|
||||
if (elements.hostControls) elements.hostControls.style.display = isHost ? 'flex' : 'none';
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
|
||||
const canControlPlayer = isHost || (window.__userPermissions?.canControl || false);
|
||||
|
||||
if (elements.selectAnimeBtn) {
|
||||
elements.selectAnimeBtn.style.display = canManageQueue ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (elements.hostControls) {
|
||||
elements.hostControls.style.display = canManageQueue ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (window.AnimePlayer && typeof window.AnimePlayer.setRoomHost === 'function') {
|
||||
window.AnimePlayer.setRoomHost(isHost);
|
||||
window.AnimePlayer.setRoomHost(canControlPlayer);
|
||||
}
|
||||
|
||||
if (isHost) {
|
||||
initGlobalControls();
|
||||
initPermissionsUI();
|
||||
}
|
||||
else if (canManageQueue) {
|
||||
initGlobalControls();
|
||||
}
|
||||
|
||||
if (canManageQueue && room.metadata) {
|
||||
if(!selectedAnimeData) selectedAnimeData = { ...room.metadata, source: 'anilist' };
|
||||
populateQuickControls();
|
||||
}
|
||||
|
||||
const copyInviteBtn = document.getElementById('copy-invite-btn');
|
||||
@@ -1387,7 +1513,8 @@ const RoomsApp = (function() {
|
||||
}
|
||||
|
||||
async function initGlobalControls() {
|
||||
if (!isHost || !extensionsReady) return;
|
||||
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!hasAccess || !extensionsReady) return;
|
||||
|
||||
if (elements.roomExtSelect.options.length > 1) return;
|
||||
|
||||
@@ -1820,6 +1947,274 @@ const RoomsApp = (function() {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
let permissionsModal = null;
|
||||
|
||||
function initPermissionsUI() {
|
||||
if (!isHost) return;
|
||||
|
||||
if (!permissionsModal) {
|
||||
permissionsModal = document.createElement('div');
|
||||
permissionsModal.className = 'modal-overlay';
|
||||
permissionsModal.id = 'permissions-modal';
|
||||
permissionsModal.innerHTML = `
|
||||
<div class="modal-content permissions-content">
|
||||
<button class="modal-close" onclick="closePermissionsModal()">✕</button>
|
||||
<h2 class="modal-title">Manage Permissions</h2>
|
||||
|
||||
<div class="permissions-list" id="permissions-list">
|
||||
<!-- Se llenará dinámicamente -->
|
||||
</div>
|
||||
|
||||
<div class="banned-ips-section">
|
||||
<h3 class="section-title">Banned IPs</h3>
|
||||
<div id="banned-ips-list" class="banned-list">
|
||||
<p class="empty-state">No banned users</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(permissionsModal);
|
||||
}
|
||||
|
||||
const headerRight = document.querySelector('.header-right');
|
||||
if (headerRight && !document.getElementById('permissions-btn')) {
|
||||
const permBtn = document.createElement('button');
|
||||
permBtn.id = 'permissions-btn';
|
||||
permBtn.className = 'btn-icon-glass';
|
||||
permBtn.title = 'Manage Permissions';
|
||||
permBtn.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
`;
|
||||
permBtn.onclick = openPermissionsModal;
|
||||
|
||||
const searchBtn = document.getElementById('select-anime-btn');
|
||||
if (searchBtn) {
|
||||
headerRight.insertBefore(permBtn, searchBtn);
|
||||
} else {
|
||||
headerRight.appendChild(permBtn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openPermissionsModal() {
|
||||
if (!permissionsModal || !isHost) return;
|
||||
|
||||
updatePermissionsList();
|
||||
permissionsModal.classList.add('show');
|
||||
}
|
||||
|
||||
window.closePermissionsModal = function() {
|
||||
if (permissionsModal) {
|
||||
permissionsModal.classList.remove('show');
|
||||
}
|
||||
};
|
||||
|
||||
function updatePermissionsList() {
|
||||
const list = document.getElementById('permissions-list');
|
||||
if (!list) return;
|
||||
|
||||
const room = getCurrentRoomData();
|
||||
|
||||
if (!room || !room.users) {
|
||||
list.innerHTML = '<p class="empty-state">No active users</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = room.users
|
||||
.filter(u => !u.isHost)
|
||||
.map(user => createPermissionCard(user))
|
||||
.join('');
|
||||
}
|
||||
|
||||
function createPermissionCard(user) {
|
||||
const perms = user.permissions || { canControl: false, canManageQueue: false };
|
||||
|
||||
return `
|
||||
<div class="permission-card">
|
||||
<div class="user-info-section">
|
||||
<div class="user-avatar-small">
|
||||
${user.avatar ? `<img src="${user.avatar}">` : user.username[0].toUpperCase()}
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<span class="user-name-text">${escapeHtml(user.username)}</span>
|
||||
${user.isGuest ? '<span class="guest-badge">Guest</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permissions-toggles">
|
||||
<label class="perm-toggle" for="perm-ctrl-${user.id}">
|
||||
<input type="checkbox"
|
||||
id="perm-ctrl-${user.id}"
|
||||
data-user-id="${user.id}"
|
||||
${perms.canControl ? 'checked' : ''}
|
||||
onchange="togglePermission('${user.id}', 'canControl', this.checked)">
|
||||
<span class="perm-label">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||
</svg>
|
||||
Playback Control
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="perm-toggle" for="perm-queue-${user.id}">
|
||||
<input type="checkbox"
|
||||
id="perm-queue-${user.id}"
|
||||
data-user-id="${user.id}"
|
||||
${perms.canManageQueue ? 'checked' : ''}
|
||||
onchange="togglePermission('${user.id}', 'canManageQueue', this.checked)">
|
||||
<span class="perm-label">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="8" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="8" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="8" y1="18" x2="21" y2="18"></line>
|
||||
<line x1="3" y1="6" x2="3.01" y2="6"></line>
|
||||
<line x1="3" y1="12" x2="3.01" y2="12"></line>
|
||||
<line x1="3" y1="18" x2="3.01" y2="18"></line>
|
||||
</svg>
|
||||
Queue Management
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="ban-btn" onclick="banUser('${user.id}')" title="Ban User">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line>
|
||||
</svg>
|
||||
Ban
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
window.togglePermission = function(userId, permission, value) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
console.log(`Toggling permission ${permission} to ${value} for user ${userId}`);
|
||||
|
||||
const permissions = {};
|
||||
permissions[permission] = value;
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'update_permissions',
|
||||
targetUserId: userId,
|
||||
permissions
|
||||
}));
|
||||
|
||||
showSystemToast(`Permission ${value ? 'granted' : 'revoked'}`);
|
||||
};
|
||||
|
||||
window.banUser = function(userId) {
|
||||
const room = getCurrentRoomData();
|
||||
const user = room?.users?.find(u => u.id === userId);
|
||||
|
||||
if (!user) return;
|
||||
|
||||
const confirmed = confirm(`Ban ${user.username} from this room? They won't be able to rejoin.`);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ban_user',
|
||||
targetUserId: userId
|
||||
}));
|
||||
}
|
||||
|
||||
showSystemToast(`${user.username} has been banned`);
|
||||
setTimeout(() => updatePermissionsList(), 500);
|
||||
};
|
||||
|
||||
function getCurrentRoomData() {
|
||||
return window.__currentRoomData || null;
|
||||
}
|
||||
|
||||
function saveRoomData(roomData) {
|
||||
window.__currentRoomData = roomData;
|
||||
|
||||
if (roomData.users && currentUserId) {
|
||||
const currentUser = roomData.users.find(u => u.id === currentUserId);
|
||||
if (currentUser && currentUser.permissions) {
|
||||
window.__userPermissions = currentUser.permissions;
|
||||
|
||||
console.log('Permissions updated for current user:', window.__userPermissions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const originalHandleMessage = handleWebSocketMessage;
|
||||
handleWebSocketMessage = function(data) {
|
||||
switch(data.type) {
|
||||
case 'permissions_updated':
|
||||
saveRoomData({ users: data.users });
|
||||
|
||||
const updatedUser = data.users.find(u => u.id === currentUserId);
|
||||
if (updatedUser) {
|
||||
window.__userPermissions = updatedUser.permissions || {
|
||||
canControl: false,
|
||||
canManageQueue: false
|
||||
};
|
||||
|
||||
console.log('My permissions updated:', window.__userPermissions);
|
||||
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
|
||||
if (elements.selectAnimeBtn) {
|
||||
elements.selectAnimeBtn.style.display = canManageQueue ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (elements.hostControls) {
|
||||
elements.hostControls.style.display = canManageQueue ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (canManageQueue) {
|
||||
initGlobalControls();
|
||||
populateQuickControls();
|
||||
}
|
||||
|
||||
if (!canManageQueue && elements.animeSearchModal && elements.animeSearchModal.classList.contains('show')) {
|
||||
closeAnimeSearchModal();
|
||||
showSystemToast("Permissions updated: Access revoked");
|
||||
}
|
||||
|
||||
if (currentQueue && currentQueue.length > 0) {
|
||||
console.log('Forcing queue re-render due to permission change');
|
||||
renderQueue(currentQueue);
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionsModal && permissionsModal.classList.contains('show')) {
|
||||
updatePermissionsList();
|
||||
}
|
||||
|
||||
renderUsersList(data.users);
|
||||
break;
|
||||
|
||||
case 'banned':
|
||||
alert(data.message || 'You have been banned from this room');
|
||||
window.location.href = '/anime';
|
||||
break;
|
||||
|
||||
case 'users_update':
|
||||
saveRoomData({ users: data.users });
|
||||
break;
|
||||
|
||||
default:
|
||||
originalHandleMessage(data);
|
||||
}
|
||||
};
|
||||
|
||||
const originalUpdateRoomUI = updateRoomUI;
|
||||
updateRoomUI = function(room) {
|
||||
originalUpdateRoomUI(room);
|
||||
|
||||
if (isHost) {
|
||||
initPermissionsUI();
|
||||
}
|
||||
};
|
||||
|
||||
return { init };
|
||||
})();
|
||||
|
||||
|
||||
@@ -627,4 +627,227 @@ input[type=number] { -moz-appearance: textfield; }
|
||||
.config-sidebar { width: 100%; flex-direction: row; }
|
||||
.config-cover { width: 80px; }
|
||||
.ep-control { width: auto; flex: 1; }
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
PERMISSIONS & MODERATION
|
||||
========================================= */
|
||||
|
||||
.permissions-content {
|
||||
max-width: 700px;
|
||||
width: 90%;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.permissions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.permission-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.permission-card:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.user-info-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.user-avatar-small {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
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;
|
||||
}
|
||||
|
||||
.user-avatar-small img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.user-name-text {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.guest-badge {
|
||||
font-size: 0.7rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #aaa;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.permissions-toggles {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.perm-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.perm-toggle input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--brand-color);
|
||||
}
|
||||
|
||||
.perm-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.perm-toggle:hover .perm-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.perm-label svg {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ban-btn {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ban-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border-color: #ef4444;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.banned-ips-section {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-bottom: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.banned-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.banned-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.banned-ip {
|
||||
font-family: monospace;
|
||||
color: #ef4444;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.unban-btn {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(74, 222, 128, 0.3);
|
||||
color: #4ade80;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.unban-btn:hover {
|
||||
background: rgba(74, 222, 128, 0.1);
|
||||
border-color: #4ade80;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.permission-card {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.user-info-section {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.ban-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import crypto from 'crypto';
|
||||
import { closeTunnelIfUnused } from "./tunnel.manager";
|
||||
|
||||
interface RoomPermissions {
|
||||
canControl: boolean;
|
||||
canManageQueue: boolean;
|
||||
}
|
||||
|
||||
interface RoomUser {
|
||||
id: string;
|
||||
username: string;
|
||||
@@ -8,6 +13,8 @@ interface RoomUser {
|
||||
isHost: boolean;
|
||||
isGuest: boolean;
|
||||
userId?: number;
|
||||
permissions?: RoomPermissions;
|
||||
ipAddress?: string;
|
||||
}
|
||||
|
||||
interface SourceContext {
|
||||
@@ -55,8 +62,14 @@ interface RoomData {
|
||||
exposed: boolean;
|
||||
publicUrl?: string;
|
||||
queue: QueueItem[];
|
||||
bannedIPs: Set<string>;
|
||||
}
|
||||
|
||||
export const DEFAULT_GUEST_PERMISSIONS: RoomPermissions = {
|
||||
canControl: false,
|
||||
canManageQueue: false
|
||||
};
|
||||
|
||||
const rooms = new Map<string, RoomData>();
|
||||
|
||||
export function generateRoomId(): string {
|
||||
@@ -77,13 +90,81 @@ export function createRoom(name: string, host: RoomUser, password?: string, expo
|
||||
metadata: null,
|
||||
exposed,
|
||||
publicUrl,
|
||||
queue: []
|
||||
queue: [],
|
||||
bannedIPs: new Set() // NUEVO
|
||||
};
|
||||
|
||||
rooms.set(roomId, room);
|
||||
return room;
|
||||
}
|
||||
|
||||
export function updateUserPermissions(roomId: string, userId: string, permissions: Partial<RoomPermissions>): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
|
||||
const user: any = room.users.get(userId);
|
||||
if (!user) return false;
|
||||
|
||||
if (user.isHost) return false;
|
||||
|
||||
user.permissions = {
|
||||
...user.permissions,
|
||||
...permissions
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function banUserIP(roomId: string, ipAddress: string): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
|
||||
room.bannedIPs.add(ipAddress);
|
||||
|
||||
// Remover a todos los usuarios con esa IP
|
||||
Array.from(room.users.values()).forEach(user => {
|
||||
if (user.ipAddress === ipAddress) {
|
||||
removeUserFromRoom(roomId, user.id);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function unbanUserIP(roomId: string, ipAddress: string): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
|
||||
return room.bannedIPs.delete(ipAddress);
|
||||
}
|
||||
|
||||
export function isIPBanned(roomId: string, ipAddress: string): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
return room.bannedIPs.has(ipAddress);
|
||||
}
|
||||
|
||||
export function getBannedIPs(roomId: string): string[] {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return [];
|
||||
return Array.from(room.bannedIPs);
|
||||
}
|
||||
|
||||
export function hasPermission(roomId: string, userId: string, permission: keyof RoomPermissions): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
|
||||
const user = room.users.get(userId);
|
||||
if (!user) return false;
|
||||
|
||||
// El host siempre tiene todos los permisos
|
||||
if (user.isHost) return true;
|
||||
|
||||
// Si no tiene permisos definidos, usar defaults
|
||||
const userPerms = user.permissions || DEFAULT_GUEST_PERMISSIONS;
|
||||
return userPerms[permission] || false;
|
||||
}
|
||||
|
||||
export function getRoom(roomId: string): RoomData | null {
|
||||
return rooms.get(roomId) || null;
|
||||
}
|
||||
|
||||
@@ -13,14 +13,13 @@ interface WSClient {
|
||||
|
||||
const clients = new Map<string, WSClient>();
|
||||
|
||||
interface WSParams {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
interface WSQuery {
|
||||
token?: string;
|
||||
guestName?: string;
|
||||
password?: string;
|
||||
function getClientIP(req: any): string {
|
||||
return req.headers['x-forwarded-for']?.split(',')[0].trim() ||
|
||||
req.headers['x-real-ip'] ||
|
||||
req.connection?.remoteAddress ||
|
||||
req.socket?.remoteAddress ||
|
||||
'unknown';
|
||||
}
|
||||
|
||||
export function setupRoomWebSocket(fastify: FastifyInstance) {
|
||||
@@ -44,13 +43,14 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
||||
const guestName = req.query.guestName;
|
||||
const password = req.query.password;
|
||||
|
||||
const clientIP = getClientIP(req); // NUEVO
|
||||
|
||||
let userId: string;
|
||||
let username: string;
|
||||
let avatar: string | undefined;
|
||||
let isGuest = false;
|
||||
let realUserId: any;
|
||||
|
||||
// Verificar si la sala existe
|
||||
const room = roomService.getRoom(roomId);
|
||||
if (!room) {
|
||||
socket.send(JSON.stringify({
|
||||
@@ -61,6 +61,24 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (roomService.isIPBanned(roomId, clientIP)) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'error',
|
||||
message: 'You have been banned from this room'
|
||||
}));
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!room) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'error',
|
||||
message: 'Room not found'
|
||||
}));
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar contraseña si existe
|
||||
if (room.password) {
|
||||
if (!password || !roomService.verifyRoomPassword(roomId, password)) {
|
||||
@@ -134,14 +152,15 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
||||
isHost
|
||||
});
|
||||
|
||||
// Agregar usuario a la sala
|
||||
const userInRoom = {
|
||||
id: userId,
|
||||
username,
|
||||
avatar,
|
||||
isHost: isHost, // ← CORREGIDO: Usar la verificación correcta
|
||||
isHost: isHost,
|
||||
isGuest,
|
||||
userId: realUserId
|
||||
userId: realUserId,
|
||||
ipAddress: clientIP, // NUEVO
|
||||
permissions: isHost ? undefined : { ...roomService.DEFAULT_GUEST_PERMISSIONS }
|
||||
};
|
||||
|
||||
roomService.addUserToRoom(roomId, userInRoom);
|
||||
@@ -160,6 +179,7 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
||||
userId,
|
||||
username,
|
||||
isGuest,
|
||||
isHost, // NUEVO: Enviar explícitamente
|
||||
room: {
|
||||
id: room.id,
|
||||
name: room.name,
|
||||
@@ -168,7 +188,8 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
isHost: u.isHost,
|
||||
isGuest: u.isGuest
|
||||
isGuest: u.isGuest,
|
||||
permissions: u.permissions // NUEVO
|
||||
})),
|
||||
currentVideo: room.currentVideo,
|
||||
queue: room.queue || []
|
||||
@@ -212,6 +233,9 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
const room = roomService.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
const user = room.users.get(userId);
|
||||
if (!user) return;
|
||||
|
||||
console.log('Handling message:', data.type, 'from user:', userId, 'isHost:', room.host.id === userId);
|
||||
|
||||
switch (data.type) {
|
||||
@@ -226,8 +250,65 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
});
|
||||
break;
|
||||
|
||||
case 'update_permissions':
|
||||
if (!user.isHost) {
|
||||
console.warn('Non-host attempted to update permissions');
|
||||
return;
|
||||
}
|
||||
|
||||
const success = roomService.updateUserPermissions(
|
||||
roomId,
|
||||
data.targetUserId,
|
||||
data.permissions
|
||||
);
|
||||
|
||||
if (success) {
|
||||
const updatedRoom = roomService.getRoom(roomId);
|
||||
if (updatedRoom) {
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'permissions_updated',
|
||||
users: Array.from(updatedRoom.users.values()).map(u => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
isHost: u.isHost,
|
||||
isGuest: u.isGuest,
|
||||
permissions: u.permissions
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// NUEVO: Baneo de usuarios (solo host)
|
||||
case 'ban_user':
|
||||
if (!user.isHost) {
|
||||
console.warn('Non-host attempted to ban user');
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUser = room.users.get(data.targetUserId);
|
||||
if (targetUser && targetUser.ipAddress) {
|
||||
roomService.banUserIP(roomId, targetUser.ipAddress);
|
||||
|
||||
// Cerrar conexión del usuario baneado
|
||||
const targetClient = clients.get(data.targetUserId);
|
||||
if (targetClient && targetClient.socket) {
|
||||
targetClient.socket.send(JSON.stringify({
|
||||
type: 'banned',
|
||||
message: 'You have been banned from this room'
|
||||
}));
|
||||
targetClient.socket.close();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'queue_play_item':
|
||||
if (room.host.id !== userId) return;
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||
console.warn('User lacks permission for queue management');
|
||||
return;
|
||||
}
|
||||
|
||||
const itemToPlay = roomService.getAndRemoveQueueItem(roomId, data.itemUid);
|
||||
if (itemToPlay) {
|
||||
@@ -255,7 +336,9 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'queue_move':
|
||||
if (room.host.id !== userId) return;
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moved = roomService.moveQueueItem(roomId, data.itemUid, data.direction);
|
||||
if (moved) {
|
||||
@@ -295,7 +378,14 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'video_update':
|
||||
if (room.host.id !== userId) return;
|
||||
const canUpdateVideo = user.isHost ||
|
||||
roomService.hasPermission(roomId, userId, 'canControl') ||
|
||||
roomService.hasPermission(roomId, userId, 'canManageQueue');
|
||||
|
||||
if (!canUpdateVideo) {
|
||||
console.warn('User lacks permissions to update video');
|
||||
return;
|
||||
}
|
||||
|
||||
roomService.updateRoomVideo(roomId, data.video);
|
||||
roomService.updateRoomMetadata(roomId, data.metadata);
|
||||
@@ -308,13 +398,13 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'queue_add_batch':
|
||||
if (room.host.id !== userId) return;
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||
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,
|
||||
@@ -365,13 +455,11 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'play':
|
||||
// Solo el host puede controlar la reproducción
|
||||
if (room.host.id !== userId) {
|
||||
console.warn('Non-host attempted play:', userId);
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canControl')) {
|
||||
console.warn('User lacks control permissions');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Broadcasting play event to room:', roomId);
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'play',
|
||||
currentTime: data.currentTime,
|
||||
@@ -380,8 +468,8 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'pause':
|
||||
if (room.host.id !== userId) {
|
||||
console.warn('Non-host attempted pause:', userId);
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canControl')) {
|
||||
console.warn('User lacks control permissions for pause');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -394,8 +482,8 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'seek':
|
||||
if (room.host.id !== userId) {
|
||||
console.warn('Non-host attempted seek:', userId);
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canControl')) {
|
||||
console.warn('User lacks control permissions for seek');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -436,19 +524,19 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'queue_add':
|
||||
// Solo el host (o todos si quisieras) pueden añadir
|
||||
if (room.host.id !== userId) return;
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newItem = {
|
||||
uid: `q_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
|
||||
metadata: data.metadata,
|
||||
videoData: data.video, // El objeto que contiene url, type, subtitles
|
||||
videoData: data.video,
|
||||
addedBy: room.users.get(userId)?.username || 'Unknown'
|
||||
};
|
||||
|
||||
roomService.addQueueItem(roomId, newItem);
|
||||
|
||||
// Avisar a todos que la cola cambió
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'queue_update',
|
||||
queue: room.queue
|
||||
@@ -456,7 +544,10 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
||||
break;
|
||||
|
||||
case 'queue_remove':
|
||||
if (room.host.id !== userId) return;
|
||||
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||
return;
|
||||
}
|
||||
|
||||
roomService.removeQueueItem(roomId, data.itemUid);
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'queue_update',
|
||||
|
||||
@@ -402,8 +402,8 @@ const AnimePlayer = (function() {
|
||||
|
||||
// Control functions
|
||||
function togglePlayPause() {
|
||||
if (_roomMode && !_isRoomHost) {
|
||||
console.log('Guests cannot control playback');
|
||||
if (_roomMode && !_isRoomHost && !hasControlPermission()) {
|
||||
showPermissionToast('You need playback control permission');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -411,12 +411,14 @@ const AnimePlayer = (function() {
|
||||
|
||||
if (els.video.paused) {
|
||||
els.video.play().catch(() => {});
|
||||
if (_roomMode && _isRoomHost) {
|
||||
|
||||
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||
sendRoomEvent('play', { currentTime: els.video.currentTime });
|
||||
}
|
||||
} else {
|
||||
els.video.pause();
|
||||
if (_roomMode && _isRoomHost) {
|
||||
|
||||
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||
sendRoomEvent('pause', { currentTime: els.video.currentTime });
|
||||
}
|
||||
}
|
||||
@@ -460,7 +462,7 @@ const AnimePlayer = (function() {
|
||||
|
||||
function seekToPosition(e) {
|
||||
if (!els.video || !els.progressContainer) return;
|
||||
if (_roomMode && !_isRoomHost) return;
|
||||
if (_roomMode && !_isRoomHost && !hasControlPermission()) return;
|
||||
|
||||
const rect = els.progressContainer.getBoundingClientRect();
|
||||
const pos = (e.clientX - rect.left) / rect.width;
|
||||
@@ -468,8 +470,7 @@ const AnimePlayer = (function() {
|
||||
|
||||
els.video.currentTime = newTime;
|
||||
|
||||
// En room mode, enviar evento de seek
|
||||
if (_roomMode && _isRoomHost) {
|
||||
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||
sendRoomEvent('seek', { currentTime: newTime });
|
||||
}
|
||||
}
|
||||
@@ -484,29 +485,63 @@ const AnimePlayer = (function() {
|
||||
|
||||
function seekRelative(seconds) {
|
||||
if (!els.video) return;
|
||||
if (_roomMode && !_isRoomHost) return;
|
||||
if (_roomMode && !_isRoomHost && !hasControlPermission()) {
|
||||
showPermissionToast('You need playback control permission');
|
||||
return;
|
||||
}
|
||||
|
||||
const newTime = Math.max(0, Math.min(els.video.duration, els.video.currentTime + seconds));
|
||||
els.video.currentTime = newTime;
|
||||
|
||||
// En room mode, enviar evento de seek
|
||||
if (_roomMode && _isRoomHost) {
|
||||
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||
sendRoomEvent('seek', { currentTime: newTime });
|
||||
}
|
||||
}
|
||||
|
||||
function seekToPercent(percent) {
|
||||
if (!els.video) return;
|
||||
if (_roomMode && !_isRoomHost) return;
|
||||
|
||||
if (_roomMode && !_isRoomHost && !hasControlPermission()) {
|
||||
showPermissionToast('You need playback control permission');
|
||||
return;
|
||||
}
|
||||
const newTime = els.video.duration * percent;
|
||||
els.video.currentTime = newTime;
|
||||
|
||||
// En room mode, enviar evento de seek
|
||||
if (_roomMode && _isRoomHost) {
|
||||
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||
sendRoomEvent('seek', { currentTime: newTime });
|
||||
}
|
||||
}
|
||||
|
||||
function hasControlPermission() {
|
||||
return window.__userPermissions?.canControl || false;
|
||||
}
|
||||
|
||||
function showPermissionToast(message) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'permission-toast';
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(239, 68, 68, 0.95);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||
animation: fadeIn 0.3s ease;
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'fadeOut 0.3s ease';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
// Video event handlers
|
||||
function onPlay() {
|
||||
if (els.playPauseBtn) {
|
||||
@@ -589,7 +624,9 @@ const AnimePlayer = (function() {
|
||||
}
|
||||
|
||||
function sendRoomEvent(eventType, data = {}) {
|
||||
if (!_roomMode || !_isRoomHost || !_roomWebSocket) return;
|
||||
if (!_roomMode || !_roomWebSocket) return;
|
||||
if (!_isRoomHost && !hasControlPermission()) return;
|
||||
|
||||
if (_roomWebSocket.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
console.log('Sending room event:', eventType, data);
|
||||
|
||||
@@ -232,10 +232,15 @@ const RoomsApp = (function() {
|
||||
|
||||
if (elements.btnAddQueue) {
|
||||
elements.btnAddQueue.onclick = () => {
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!canManageQueue) {
|
||||
showSystemToast('You need queue management permission');
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -279,7 +284,9 @@ const RoomsApp = (function() {
|
||||
}
|
||||
if (elements.roomSdToggle) {
|
||||
elements.roomSdToggle.onclick = () => {
|
||||
if (!isHost) return;
|
||||
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
|
||||
if (!hasAccess) return;
|
||||
const currentState = elements.roomSdToggle.getAttribute('data-state');
|
||||
const newState = currentState === 'sub' ? 'dub' : 'sub';
|
||||
|
||||
@@ -338,7 +345,9 @@ const RoomsApp = (function() {
|
||||
// --- QUICK CONTROLS LOGIC (Header) ---
|
||||
|
||||
async function populateQuickControls() {
|
||||
if (!isHost || !selectedAnimeData) return;
|
||||
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
|
||||
if (!hasAccess || !selectedAnimeData) return;
|
||||
if (!extensionsReady) return;
|
||||
|
||||
elements.roomExtSelect.innerHTML = '';
|
||||
@@ -356,7 +365,8 @@ const RoomsApp = (function() {
|
||||
}
|
||||
|
||||
async function onQuickExtensionChange(e, silent = false) {
|
||||
if (!isHost) return;
|
||||
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!hasAccess) return;
|
||||
|
||||
const ext = elements.roomExtSelect.value;
|
||||
const settings = extensionsStore.settings[ext];
|
||||
@@ -433,7 +443,9 @@ const RoomsApp = (function() {
|
||||
}
|
||||
|
||||
function onQuickServerChange() {
|
||||
if (!isHost) return;
|
||||
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!hasAccess) return;
|
||||
|
||||
launchStream(false);
|
||||
}
|
||||
|
||||
@@ -488,6 +500,28 @@ const RoomsApp = (function() {
|
||||
if(elements.btnLaunch) elements.btnLaunch.disabled = false;
|
||||
if(elements.btnAddQueue) elements.btnAddQueue.disabled = false;
|
||||
|
||||
if (extensionsReady && elements.selExtension) {
|
||||
if (elements.selExtension.options.length <= 1) {
|
||||
elements.selExtension.innerHTML = '';
|
||||
|
||||
extensionsStore.list.forEach(ext => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ext;
|
||||
opt.textContent = ext[0].toUpperCase() + ext.slice(1);
|
||||
elements.selExtension.appendChild(opt);
|
||||
});
|
||||
|
||||
let defaultExt = selectedAnimeData.source || 'anilist';
|
||||
if (!extensionsStore.list.includes(defaultExt) && extensionsStore.list.length > 0) {
|
||||
defaultExt = extensionsStore.list[0];
|
||||
}
|
||||
|
||||
elements.selExtension.value = defaultExt;
|
||||
|
||||
handleModalExtensionChange();
|
||||
}
|
||||
}
|
||||
|
||||
setupConfigListeners();
|
||||
|
||||
elements.stepSearch.style.display = 'none';
|
||||
@@ -625,12 +659,24 @@ const RoomsApp = (function() {
|
||||
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;
|
||||
const ext = overrides.forceExtension ||
|
||||
(fromModal ? (configState.extension || elements.selExtension?.value) : null) ||
|
||||
activeContext.extension ||
|
||||
(elements.roomExtSelect ? elements.roomExtSelect.value : null);
|
||||
|
||||
const server = overrides.forceServer ||
|
||||
(fromModal ? (configState.server || elements.selServer?.value) : null) ||
|
||||
activeContext.server ||
|
||||
(elements.roomServerSelect ? elements.roomServerSelect.value : null);
|
||||
|
||||
const category = elements.roomSdToggle?.getAttribute('data-state') || activeContext.category || 'sub';
|
||||
|
||||
if (!ext || !server) {
|
||||
console.warn("Faltan datos (ext o server) para lanzar el stream");
|
||||
console.warn("Faltan datos (ext o server).", {ext, server});
|
||||
if (fromModal && elements.configError) {
|
||||
elements.configError.textContent = "Please select an extension and server.";
|
||||
elements.configError.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -850,7 +896,6 @@ const RoomsApp = (function() {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -918,7 +963,7 @@ const RoomsApp = (function() {
|
||||
episode: ep,
|
||||
title: currentAnimeDetails.title.userPreferred,
|
||||
server: document.getElementById('room-server-select').value || 'gogoanime',
|
||||
category: 'sub' // o lo que esté seleccionado
|
||||
category: 'sub'
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -1081,6 +1126,18 @@ const RoomsApp = (function() {
|
||||
currentUserId = data.userId;
|
||||
currentUsername = data.username;
|
||||
isGuest = data.isGuest;
|
||||
|
||||
if (data.room && data.room.users) {
|
||||
const currentUser = data.room.users.find(u => u.id === data.userId);
|
||||
if (currentUser) {
|
||||
window.__userPermissions = currentUser.permissions || {
|
||||
canControl: false,
|
||||
canManageQueue: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (data.room.queue) renderQueue(data.room.queue);
|
||||
updateRoomUI(data.room);
|
||||
|
||||
if (data.room.currentVideo) {
|
||||
@@ -1109,6 +1166,7 @@ const RoomsApp = (function() {
|
||||
break;
|
||||
|
||||
case 'users_update':
|
||||
saveRoomData({ users: data.users });
|
||||
renderUsersList(data.users);
|
||||
break;
|
||||
|
||||
@@ -1127,6 +1185,32 @@ const RoomsApp = (function() {
|
||||
updateUsersList();
|
||||
break;
|
||||
|
||||
case 'permissions_updated':
|
||||
saveRoomData({ users: data.users });
|
||||
|
||||
const updatedUser = data.users.find(u => u.id === currentUserId);
|
||||
if (updatedUser) {
|
||||
window.__userPermissions = updatedUser.permissions || {
|
||||
canControl: false,
|
||||
canManageQueue: false
|
||||
};
|
||||
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (elements.selectAnimeBtn) {
|
||||
elements.selectAnimeBtn.style.display = canManageQueue ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (currentQueue.length > 0) {
|
||||
renderQueue(currentQueue);
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionsModal && permissionsModal.classList.contains('show')) {
|
||||
updatePermissionsList();
|
||||
}
|
||||
|
||||
renderUsersList(data.users);
|
||||
break;
|
||||
case 'chat':
|
||||
addChatMessage(data);
|
||||
const isChatHidden = elements.roomLayout.classList.contains('chat-hidden');
|
||||
@@ -1210,33 +1294,34 @@ const RoomsApp = (function() {
|
||||
return;
|
||||
}
|
||||
|
||||
const canManageQueue = isHost || (window.__userPermissions && window.__userPermissions.canManageQueue);
|
||||
elements.queueList.innerHTML = queueItems.map((item, index) => {
|
||||
let actionsHtml = '';
|
||||
|
||||
if (isHost) {
|
||||
if (canManageQueue) {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === queueItems.length - 1;
|
||||
|
||||
actionsHtml = `
|
||||
<div class="q-actions">
|
||||
<button class="q-btn play" onclick="playQueueItem('${item.uid}')" title="Play Now">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
<div style="display:flex; gap:2px;">
|
||||
${!isFirst ? `
|
||||
<button class="q-btn" onclick="moveQueueItem('${item.uid}', 'up')" title="Move Up">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 15l-6-6-6 6"/></svg>
|
||||
</button>` : ''}
|
||||
${!isLast ? `
|
||||
<button class="q-btn" onclick="moveQueueItem('${item.uid}', 'down')" title="Move Down">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
|
||||
</button>` : ''}
|
||||
</div>
|
||||
<button class="q-btn remove" onclick="removeQueueItem('${item.uid}')" title="Remove">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
<div class="q-actions">
|
||||
<button class="q-btn play" onclick="playQueueItem('${item.uid}')" title="Play Now">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
<div style="display:flex; gap:2px;">
|
||||
${!isFirst ? `
|
||||
<button class="q-btn" onclick="moveQueueItem('${item.uid}', 'up')" title="Move Up">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 15l-6-6-6 6"/></svg>
|
||||
</button>` : ''}
|
||||
${!isLast ? `
|
||||
<button class="q-btn" onclick="moveQueueItem('${item.uid}', 'down')" title="Move Down">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
|
||||
</button>` : ''}
|
||||
</div>
|
||||
`;
|
||||
<button class="q-btn remove" onclick="removeQueueItem('${item.uid}')" title="Remove">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
@@ -1248,11 +1333,17 @@ const RoomsApp = (function() {
|
||||
</div>
|
||||
${actionsHtml}
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.playQueueItem = async function(uid) {
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!canManageQueue) {
|
||||
showSystemToast('You need queue management permission');
|
||||
return;
|
||||
}
|
||||
|
||||
const item = currentQueue.find(i => i.uid === uid);
|
||||
if (!item) return;
|
||||
|
||||
@@ -1305,12 +1396,25 @@ const RoomsApp = (function() {
|
||||
};
|
||||
|
||||
window.moveQueueItem = function(uid, direction) {
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!canManageQueue) {
|
||||
showSystemToast('You need queue management permission');
|
||||
return;
|
||||
}
|
||||
|
||||
if(ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'queue_move', itemUid: uid, direction }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
window.removeQueueItem = function(uid) {
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!canManageQueue) {
|
||||
showSystemToast('You need queue management permission');
|
||||
return;
|
||||
}
|
||||
|
||||
if(ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'queue_remove', itemUid: uid }));
|
||||
}
|
||||
@@ -1321,17 +1425,39 @@ const RoomsApp = (function() {
|
||||
elements.roomViewers.textContent = `${room.users.length}`;
|
||||
|
||||
const currentUser = room.users.find(u => u.id === currentUserId);
|
||||
|
||||
if (currentUser) {
|
||||
window.__userPermissions = currentUser.permissions || { canControl: false, canManageQueue: false };
|
||||
}
|
||||
isHost = currentUser?.isHost || false;
|
||||
|
||||
if (elements.selectAnimeBtn) elements.selectAnimeBtn.style.display = isHost ? 'flex' : 'none';
|
||||
if (elements.hostControls) elements.hostControls.style.display = isHost ? 'flex' : 'none';
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
|
||||
const canControlPlayer = isHost || (window.__userPermissions?.canControl || false);
|
||||
|
||||
if (elements.selectAnimeBtn) {
|
||||
elements.selectAnimeBtn.style.display = canManageQueue ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (elements.hostControls) {
|
||||
elements.hostControls.style.display = canManageQueue ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (window.AnimePlayer && typeof window.AnimePlayer.setRoomHost === 'function') {
|
||||
window.AnimePlayer.setRoomHost(isHost);
|
||||
window.AnimePlayer.setRoomHost(canControlPlayer);
|
||||
}
|
||||
|
||||
if (isHost) {
|
||||
initGlobalControls();
|
||||
initPermissionsUI();
|
||||
}
|
||||
else if (canManageQueue) {
|
||||
initGlobalControls();
|
||||
}
|
||||
|
||||
if (canManageQueue && room.metadata) {
|
||||
if(!selectedAnimeData) selectedAnimeData = { ...room.metadata, source: 'anilist' };
|
||||
populateQuickControls();
|
||||
}
|
||||
|
||||
const copyInviteBtn = document.getElementById('copy-invite-btn');
|
||||
@@ -1387,7 +1513,8 @@ const RoomsApp = (function() {
|
||||
}
|
||||
|
||||
async function initGlobalControls() {
|
||||
if (!isHost || !extensionsReady) return;
|
||||
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
if (!hasAccess || !extensionsReady) return;
|
||||
|
||||
if (elements.roomExtSelect.options.length > 1) return;
|
||||
|
||||
@@ -1820,6 +1947,274 @@ const RoomsApp = (function() {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
let permissionsModal = null;
|
||||
|
||||
function initPermissionsUI() {
|
||||
if (!isHost) return;
|
||||
|
||||
if (!permissionsModal) {
|
||||
permissionsModal = document.createElement('div');
|
||||
permissionsModal.className = 'modal-overlay';
|
||||
permissionsModal.id = 'permissions-modal';
|
||||
permissionsModal.innerHTML = `
|
||||
<div class="modal-content permissions-content">
|
||||
<button class="modal-close" onclick="closePermissionsModal()">✕</button>
|
||||
<h2 class="modal-title">Manage Permissions</h2>
|
||||
|
||||
<div class="permissions-list" id="permissions-list">
|
||||
<!-- Se llenará dinámicamente -->
|
||||
</div>
|
||||
|
||||
<div class="banned-ips-section">
|
||||
<h3 class="section-title">Banned IPs</h3>
|
||||
<div id="banned-ips-list" class="banned-list">
|
||||
<p class="empty-state">No banned users</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(permissionsModal);
|
||||
}
|
||||
|
||||
const headerRight = document.querySelector('.header-right');
|
||||
if (headerRight && !document.getElementById('permissions-btn')) {
|
||||
const permBtn = document.createElement('button');
|
||||
permBtn.id = 'permissions-btn';
|
||||
permBtn.className = 'btn-icon-glass';
|
||||
permBtn.title = 'Manage Permissions';
|
||||
permBtn.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
`;
|
||||
permBtn.onclick = openPermissionsModal;
|
||||
|
||||
const searchBtn = document.getElementById('select-anime-btn');
|
||||
if (searchBtn) {
|
||||
headerRight.insertBefore(permBtn, searchBtn);
|
||||
} else {
|
||||
headerRight.appendChild(permBtn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openPermissionsModal() {
|
||||
if (!permissionsModal || !isHost) return;
|
||||
|
||||
updatePermissionsList();
|
||||
permissionsModal.classList.add('show');
|
||||
}
|
||||
|
||||
window.closePermissionsModal = function() {
|
||||
if (permissionsModal) {
|
||||
permissionsModal.classList.remove('show');
|
||||
}
|
||||
};
|
||||
|
||||
function updatePermissionsList() {
|
||||
const list = document.getElementById('permissions-list');
|
||||
if (!list) return;
|
||||
|
||||
const room = getCurrentRoomData();
|
||||
|
||||
if (!room || !room.users) {
|
||||
list.innerHTML = '<p class="empty-state">No active users</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = room.users
|
||||
.filter(u => !u.isHost)
|
||||
.map(user => createPermissionCard(user))
|
||||
.join('');
|
||||
}
|
||||
|
||||
function createPermissionCard(user) {
|
||||
const perms = user.permissions || { canControl: false, canManageQueue: false };
|
||||
|
||||
return `
|
||||
<div class="permission-card">
|
||||
<div class="user-info-section">
|
||||
<div class="user-avatar-small">
|
||||
${user.avatar ? `<img src="${user.avatar}">` : user.username[0].toUpperCase()}
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<span class="user-name-text">${escapeHtml(user.username)}</span>
|
||||
${user.isGuest ? '<span class="guest-badge">Guest</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permissions-toggles">
|
||||
<label class="perm-toggle" for="perm-ctrl-${user.id}">
|
||||
<input type="checkbox"
|
||||
id="perm-ctrl-${user.id}"
|
||||
data-user-id="${user.id}"
|
||||
${perms.canControl ? 'checked' : ''}
|
||||
onchange="togglePermission('${user.id}', 'canControl', this.checked)">
|
||||
<span class="perm-label">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||
</svg>
|
||||
Playback Control
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="perm-toggle" for="perm-queue-${user.id}">
|
||||
<input type="checkbox"
|
||||
id="perm-queue-${user.id}"
|
||||
data-user-id="${user.id}"
|
||||
${perms.canManageQueue ? 'checked' : ''}
|
||||
onchange="togglePermission('${user.id}', 'canManageQueue', this.checked)">
|
||||
<span class="perm-label">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="8" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="8" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="8" y1="18" x2="21" y2="18"></line>
|
||||
<line x1="3" y1="6" x2="3.01" y2="6"></line>
|
||||
<line x1="3" y1="12" x2="3.01" y2="12"></line>
|
||||
<line x1="3" y1="18" x2="3.01" y2="18"></line>
|
||||
</svg>
|
||||
Queue Management
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="ban-btn" onclick="banUser('${user.id}')" title="Ban User">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line>
|
||||
</svg>
|
||||
Ban
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
window.togglePermission = function(userId, permission, value) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
console.log(`Toggling permission ${permission} to ${value} for user ${userId}`);
|
||||
|
||||
const permissions = {};
|
||||
permissions[permission] = value;
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'update_permissions',
|
||||
targetUserId: userId,
|
||||
permissions
|
||||
}));
|
||||
|
||||
showSystemToast(`Permission ${value ? 'granted' : 'revoked'}`);
|
||||
};
|
||||
|
||||
window.banUser = function(userId) {
|
||||
const room = getCurrentRoomData();
|
||||
const user = room?.users?.find(u => u.id === userId);
|
||||
|
||||
if (!user) return;
|
||||
|
||||
const confirmed = confirm(`Ban ${user.username} from this room? They won't be able to rejoin.`);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ban_user',
|
||||
targetUserId: userId
|
||||
}));
|
||||
}
|
||||
|
||||
showSystemToast(`${user.username} has been banned`);
|
||||
setTimeout(() => updatePermissionsList(), 500);
|
||||
};
|
||||
|
||||
function getCurrentRoomData() {
|
||||
return window.__currentRoomData || null;
|
||||
}
|
||||
|
||||
function saveRoomData(roomData) {
|
||||
window.__currentRoomData = roomData;
|
||||
|
||||
if (roomData.users && currentUserId) {
|
||||
const currentUser = roomData.users.find(u => u.id === currentUserId);
|
||||
if (currentUser && currentUser.permissions) {
|
||||
window.__userPermissions = currentUser.permissions;
|
||||
|
||||
console.log('Permissions updated for current user:', window.__userPermissions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const originalHandleMessage = handleWebSocketMessage;
|
||||
handleWebSocketMessage = function(data) {
|
||||
switch(data.type) {
|
||||
case 'permissions_updated':
|
||||
saveRoomData({ users: data.users });
|
||||
|
||||
const updatedUser = data.users.find(u => u.id === currentUserId);
|
||||
if (updatedUser) {
|
||||
window.__userPermissions = updatedUser.permissions || {
|
||||
canControl: false,
|
||||
canManageQueue: false
|
||||
};
|
||||
|
||||
console.log('My permissions updated:', window.__userPermissions);
|
||||
|
||||
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||
|
||||
if (elements.selectAnimeBtn) {
|
||||
elements.selectAnimeBtn.style.display = canManageQueue ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (elements.hostControls) {
|
||||
elements.hostControls.style.display = canManageQueue ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (canManageQueue) {
|
||||
initGlobalControls();
|
||||
populateQuickControls();
|
||||
}
|
||||
|
||||
if (!canManageQueue && elements.animeSearchModal && elements.animeSearchModal.classList.contains('show')) {
|
||||
closeAnimeSearchModal();
|
||||
showSystemToast("Permissions updated: Access revoked");
|
||||
}
|
||||
|
||||
if (currentQueue && currentQueue.length > 0) {
|
||||
console.log('Forcing queue re-render due to permission change');
|
||||
renderQueue(currentQueue);
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionsModal && permissionsModal.classList.contains('show')) {
|
||||
updatePermissionsList();
|
||||
}
|
||||
|
||||
renderUsersList(data.users);
|
||||
break;
|
||||
|
||||
case 'banned':
|
||||
alert(data.message || 'You have been banned from this room');
|
||||
window.location.href = '/anime';
|
||||
break;
|
||||
|
||||
case 'users_update':
|
||||
saveRoomData({ users: data.users });
|
||||
break;
|
||||
|
||||
default:
|
||||
originalHandleMessage(data);
|
||||
}
|
||||
};
|
||||
|
||||
const originalUpdateRoomUI = updateRoomUI;
|
||||
updateRoomUI = function(room) {
|
||||
originalUpdateRoomUI(room);
|
||||
|
||||
if (isHost) {
|
||||
initPermissionsUI();
|
||||
}
|
||||
};
|
||||
|
||||
return { init };
|
||||
})();
|
||||
|
||||
|
||||
@@ -627,4 +627,227 @@ input[type=number] { -moz-appearance: textfield; }
|
||||
.config-sidebar { width: 100%; flex-direction: row; }
|
||||
.config-cover { width: 80px; }
|
||||
.ep-control { width: auto; flex: 1; }
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
PERMISSIONS & MODERATION
|
||||
========================================= */
|
||||
|
||||
.permissions-content {
|
||||
max-width: 700px;
|
||||
width: 90%;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.permissions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.permission-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.permission-card:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.user-info-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.user-avatar-small {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
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;
|
||||
}
|
||||
|
||||
.user-avatar-small img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.user-name-text {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.guest-badge {
|
||||
font-size: 0.7rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #aaa;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.permissions-toggles {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.perm-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.perm-toggle input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--brand-color);
|
||||
}
|
||||
|
||||
.perm-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.perm-toggle:hover .perm-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.perm-label svg {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ban-btn {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ban-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border-color: #ef4444;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.banned-ips-section {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-bottom: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.banned-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.banned-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.banned-ip {
|
||||
font-family: monospace;
|
||||
color: #ef4444;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.unban-btn {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(74, 222, 128, 0.3);
|
||||
color: #4ade80;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.unban-btn:hover {
|
||||
background: rgba(74, 222, 128, 0.1);
|
||||
border-color: #4ade80;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.permission-card {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.user-info-section {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.ban-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user