added user permissions to rooms
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { closeTunnelIfUnused } from "./tunnel.manager";
|
import { closeTunnelIfUnused } from "./tunnel.manager";
|
||||||
|
|
||||||
|
interface RoomPermissions {
|
||||||
|
canControl: boolean;
|
||||||
|
canManageQueue: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface RoomUser {
|
interface RoomUser {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -8,6 +13,8 @@ interface RoomUser {
|
|||||||
isHost: boolean;
|
isHost: boolean;
|
||||||
isGuest: boolean;
|
isGuest: boolean;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
permissions?: RoomPermissions;
|
||||||
|
ipAddress?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SourceContext {
|
interface SourceContext {
|
||||||
@@ -55,8 +62,14 @@ interface RoomData {
|
|||||||
exposed: boolean;
|
exposed: boolean;
|
||||||
publicUrl?: string;
|
publicUrl?: string;
|
||||||
queue: QueueItem[];
|
queue: QueueItem[];
|
||||||
|
bannedIPs: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_GUEST_PERMISSIONS: RoomPermissions = {
|
||||||
|
canControl: false,
|
||||||
|
canManageQueue: false
|
||||||
|
};
|
||||||
|
|
||||||
const rooms = new Map<string, RoomData>();
|
const rooms = new Map<string, RoomData>();
|
||||||
|
|
||||||
export function generateRoomId(): string {
|
export function generateRoomId(): string {
|
||||||
@@ -77,13 +90,81 @@ export function createRoom(name: string, host: RoomUser, password?: string, expo
|
|||||||
metadata: null,
|
metadata: null,
|
||||||
exposed,
|
exposed,
|
||||||
publicUrl,
|
publicUrl,
|
||||||
queue: []
|
queue: [],
|
||||||
|
bannedIPs: new Set() // NUEVO
|
||||||
};
|
};
|
||||||
|
|
||||||
rooms.set(roomId, room);
|
rooms.set(roomId, room);
|
||||||
return 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 {
|
export function getRoom(roomId: string): RoomData | null {
|
||||||
return rooms.get(roomId) || null;
|
return rooms.get(roomId) || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,13 @@ interface WSClient {
|
|||||||
|
|
||||||
const clients = new Map<string, WSClient>();
|
const clients = new Map<string, WSClient>();
|
||||||
|
|
||||||
interface WSParams {
|
|
||||||
roomId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WSQuery {
|
function getClientIP(req: any): string {
|
||||||
token?: string;
|
return req.headers['x-forwarded-for']?.split(',')[0].trim() ||
|
||||||
guestName?: string;
|
req.headers['x-real-ip'] ||
|
||||||
password?: string;
|
req.connection?.remoteAddress ||
|
||||||
|
req.socket?.remoteAddress ||
|
||||||
|
'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupRoomWebSocket(fastify: FastifyInstance) {
|
export function setupRoomWebSocket(fastify: FastifyInstance) {
|
||||||
@@ -44,13 +43,14 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
|||||||
const guestName = req.query.guestName;
|
const guestName = req.query.guestName;
|
||||||
const password = req.query.password;
|
const password = req.query.password;
|
||||||
|
|
||||||
|
const clientIP = getClientIP(req); // NUEVO
|
||||||
|
|
||||||
let userId: string;
|
let userId: string;
|
||||||
let username: string;
|
let username: string;
|
||||||
let avatar: string | undefined;
|
let avatar: string | undefined;
|
||||||
let isGuest = false;
|
let isGuest = false;
|
||||||
let realUserId: any;
|
let realUserId: any;
|
||||||
|
|
||||||
// Verificar si la sala existe
|
|
||||||
const room = roomService.getRoom(roomId);
|
const room = roomService.getRoom(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
socket.send(JSON.stringify({
|
socket.send(JSON.stringify({
|
||||||
@@ -61,6 +61,24 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
|||||||
return;
|
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
|
// Verificar contraseña si existe
|
||||||
if (room.password) {
|
if (room.password) {
|
||||||
if (!password || !roomService.verifyRoomPassword(roomId, password)) {
|
if (!password || !roomService.verifyRoomPassword(roomId, password)) {
|
||||||
@@ -134,14 +152,15 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
|||||||
isHost
|
isHost
|
||||||
});
|
});
|
||||||
|
|
||||||
// Agregar usuario a la sala
|
|
||||||
const userInRoom = {
|
const userInRoom = {
|
||||||
id: userId,
|
id: userId,
|
||||||
username,
|
username,
|
||||||
avatar,
|
avatar,
|
||||||
isHost: isHost, // ← CORREGIDO: Usar la verificación correcta
|
isHost: isHost,
|
||||||
isGuest,
|
isGuest,
|
||||||
userId: realUserId
|
userId: realUserId,
|
||||||
|
ipAddress: clientIP, // NUEVO
|
||||||
|
permissions: isHost ? undefined : { ...roomService.DEFAULT_GUEST_PERMISSIONS }
|
||||||
};
|
};
|
||||||
|
|
||||||
roomService.addUserToRoom(roomId, userInRoom);
|
roomService.addUserToRoom(roomId, userInRoom);
|
||||||
@@ -160,6 +179,7 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
|||||||
userId,
|
userId,
|
||||||
username,
|
username,
|
||||||
isGuest,
|
isGuest,
|
||||||
|
isHost, // NUEVO: Enviar explícitamente
|
||||||
room: {
|
room: {
|
||||||
id: room.id,
|
id: room.id,
|
||||||
name: room.name,
|
name: room.name,
|
||||||
@@ -168,7 +188,8 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
|||||||
username: u.username,
|
username: u.username,
|
||||||
avatar: u.avatar,
|
avatar: u.avatar,
|
||||||
isHost: u.isHost,
|
isHost: u.isHost,
|
||||||
isGuest: u.isGuest
|
isGuest: u.isGuest,
|
||||||
|
permissions: u.permissions // NUEVO
|
||||||
})),
|
})),
|
||||||
currentVideo: room.currentVideo,
|
currentVideo: room.currentVideo,
|
||||||
queue: room.queue || []
|
queue: room.queue || []
|
||||||
@@ -212,6 +233,9 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
const room = roomService.getRoom(roomId);
|
const room = roomService.getRoom(roomId);
|
||||||
if (!room) return;
|
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);
|
console.log('Handling message:', data.type, 'from user:', userId, 'isHost:', room.host.id === userId);
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
@@ -226,8 +250,65 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
});
|
});
|
||||||
break;
|
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':
|
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);
|
const itemToPlay = roomService.getAndRemoveQueueItem(roomId, data.itemUid);
|
||||||
if (itemToPlay) {
|
if (itemToPlay) {
|
||||||
@@ -255,7 +336,9 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'queue_move':
|
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);
|
const moved = roomService.moveQueueItem(roomId, data.itemUid, data.direction);
|
||||||
if (moved) {
|
if (moved) {
|
||||||
@@ -295,7 +378,14 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'video_update':
|
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.updateRoomVideo(roomId, data.video);
|
||||||
roomService.updateRoomMetadata(roomId, data.metadata);
|
roomService.updateRoomMetadata(roomId, data.metadata);
|
||||||
@@ -308,13 +398,13 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'queue_add_batch':
|
case 'queue_add_batch':
|
||||||
if (room.host.id !== userId) return;
|
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(data.items)) {
|
if (Array.isArray(data.items)) {
|
||||||
// Añadimos el índice (i) al forEach
|
|
||||||
data.items.forEach((item: any, i: number) => {
|
data.items.forEach((item: any, i: number) => {
|
||||||
const newItem = {
|
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)}`,
|
uid: `q_${Date.now()}_${i}_${Math.random().toString(36).substr(2, 5)}`,
|
||||||
metadata: item.metadata,
|
metadata: item.metadata,
|
||||||
videoData: item.video,
|
videoData: item.video,
|
||||||
@@ -365,13 +455,11 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'play':
|
case 'play':
|
||||||
// Solo el host puede controlar la reproducción
|
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canControl')) {
|
||||||
if (room.host.id !== userId) {
|
console.warn('User lacks control permissions');
|
||||||
console.warn('Non-host attempted play:', userId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Broadcasting play event to room:', roomId);
|
|
||||||
broadcastToRoom(roomId, {
|
broadcastToRoom(roomId, {
|
||||||
type: 'play',
|
type: 'play',
|
||||||
currentTime: data.currentTime,
|
currentTime: data.currentTime,
|
||||||
@@ -380,8 +468,8 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'pause':
|
case 'pause':
|
||||||
if (room.host.id !== userId) {
|
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canControl')) {
|
||||||
console.warn('Non-host attempted pause:', userId);
|
console.warn('User lacks control permissions for pause');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,8 +482,8 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'seek':
|
case 'seek':
|
||||||
if (room.host.id !== userId) {
|
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canControl')) {
|
||||||
console.warn('Non-host attempted seek:', userId);
|
console.warn('User lacks control permissions for seek');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,19 +524,19 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'queue_add':
|
case 'queue_add':
|
||||||
// Solo el host (o todos si quisieras) pueden añadir
|
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||||
if (room.host.id !== userId) return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newItem = {
|
const newItem = {
|
||||||
uid: `q_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
|
uid: `q_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
|
||||||
metadata: data.metadata,
|
metadata: data.metadata,
|
||||||
videoData: data.video, // El objeto que contiene url, type, subtitles
|
videoData: data.video,
|
||||||
addedBy: room.users.get(userId)?.username || 'Unknown'
|
addedBy: room.users.get(userId)?.username || 'Unknown'
|
||||||
};
|
};
|
||||||
|
|
||||||
roomService.addQueueItem(roomId, newItem);
|
roomService.addQueueItem(roomId, newItem);
|
||||||
|
|
||||||
// Avisar a todos que la cola cambió
|
|
||||||
broadcastToRoom(roomId, {
|
broadcastToRoom(roomId, {
|
||||||
type: 'queue_update',
|
type: 'queue_update',
|
||||||
queue: room.queue
|
queue: room.queue
|
||||||
@@ -456,7 +544,10 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'queue_remove':
|
case 'queue_remove':
|
||||||
if (room.host.id !== userId) return;
|
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
roomService.removeQueueItem(roomId, data.itemUid);
|
roomService.removeQueueItem(roomId, data.itemUid);
|
||||||
broadcastToRoom(roomId, {
|
broadcastToRoom(roomId, {
|
||||||
type: 'queue_update',
|
type: 'queue_update',
|
||||||
|
|||||||
@@ -402,8 +402,8 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
// Control functions
|
// Control functions
|
||||||
function togglePlayPause() {
|
function togglePlayPause() {
|
||||||
if (_roomMode && !_isRoomHost) {
|
if (_roomMode && !_isRoomHost && !hasControlPermission()) {
|
||||||
console.log('Guests cannot control playback');
|
showPermissionToast('You need playback control permission');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,12 +411,14 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
if (els.video.paused) {
|
if (els.video.paused) {
|
||||||
els.video.play().catch(() => {});
|
els.video.play().catch(() => {});
|
||||||
if (_roomMode && _isRoomHost) {
|
|
||||||
|
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||||
sendRoomEvent('play', { currentTime: els.video.currentTime });
|
sendRoomEvent('play', { currentTime: els.video.currentTime });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
els.video.pause();
|
els.video.pause();
|
||||||
if (_roomMode && _isRoomHost) {
|
|
||||||
|
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||||
sendRoomEvent('pause', { currentTime: els.video.currentTime });
|
sendRoomEvent('pause', { currentTime: els.video.currentTime });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,7 +462,7 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
function seekToPosition(e) {
|
function seekToPosition(e) {
|
||||||
if (!els.video || !els.progressContainer) return;
|
if (!els.video || !els.progressContainer) return;
|
||||||
if (_roomMode && !_isRoomHost) return;
|
if (_roomMode && !_isRoomHost && !hasControlPermission()) return;
|
||||||
|
|
||||||
const rect = els.progressContainer.getBoundingClientRect();
|
const rect = els.progressContainer.getBoundingClientRect();
|
||||||
const pos = (e.clientX - rect.left) / rect.width;
|
const pos = (e.clientX - rect.left) / rect.width;
|
||||||
@@ -468,8 +470,7 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
els.video.currentTime = newTime;
|
els.video.currentTime = newTime;
|
||||||
|
|
||||||
// En room mode, enviar evento de seek
|
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||||
if (_roomMode && _isRoomHost) {
|
|
||||||
sendRoomEvent('seek', { currentTime: newTime });
|
sendRoomEvent('seek', { currentTime: newTime });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -484,29 +485,63 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
function seekRelative(seconds) {
|
function seekRelative(seconds) {
|
||||||
if (!els.video) return;
|
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));
|
const newTime = Math.max(0, Math.min(els.video.duration, els.video.currentTime + seconds));
|
||||||
els.video.currentTime = newTime;
|
els.video.currentTime = newTime;
|
||||||
|
|
||||||
// En room mode, enviar evento de seek
|
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||||
if (_roomMode && _isRoomHost) {
|
|
||||||
sendRoomEvent('seek', { currentTime: newTime });
|
sendRoomEvent('seek', { currentTime: newTime });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function seekToPercent(percent) {
|
function seekToPercent(percent) {
|
||||||
if (!els.video) return;
|
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;
|
const newTime = els.video.duration * percent;
|
||||||
els.video.currentTime = newTime;
|
els.video.currentTime = newTime;
|
||||||
|
|
||||||
// En room mode, enviar evento de seek
|
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||||
if (_roomMode && _isRoomHost) {
|
|
||||||
sendRoomEvent('seek', { currentTime: newTime });
|
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
|
// Video event handlers
|
||||||
function onPlay() {
|
function onPlay() {
|
||||||
if (els.playPauseBtn) {
|
if (els.playPauseBtn) {
|
||||||
@@ -589,7 +624,9 @@ const AnimePlayer = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendRoomEvent(eventType, data = {}) {
|
function sendRoomEvent(eventType, data = {}) {
|
||||||
if (!_roomMode || !_isRoomHost || !_roomWebSocket) return;
|
if (!_roomMode || !_roomWebSocket) return;
|
||||||
|
if (!_isRoomHost && !hasControlPermission()) return;
|
||||||
|
|
||||||
if (_roomWebSocket.readyState !== WebSocket.OPEN) return;
|
if (_roomWebSocket.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
console.log('Sending room event:', eventType, data);
|
console.log('Sending room event:', eventType, data);
|
||||||
|
|||||||
@@ -232,10 +232,15 @@ const RoomsApp = (function() {
|
|||||||
|
|
||||||
if (elements.btnAddQueue) {
|
if (elements.btnAddQueue) {
|
||||||
elements.btnAddQueue.onclick = () => {
|
elements.btnAddQueue.onclick = () => {
|
||||||
|
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||||
|
if (!canManageQueue) {
|
||||||
|
showSystemToast('You need queue management permission');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedEpisodes.size === 0) return;
|
if (selectedEpisodes.size === 0) return;
|
||||||
|
|
||||||
const sortedEps = Array.from(selectedEpisodes).sort((a, b) => a - b);
|
const sortedEps = Array.from(selectedEpisodes).sort((a, b) => a - b);
|
||||||
|
|
||||||
const currentProvider = elements.roomExtSelect ? elements.roomExtSelect.value : 'gogoanime';
|
const currentProvider = elements.roomExtSelect ? elements.roomExtSelect.value : 'gogoanime';
|
||||||
|
|
||||||
const originalText = elements.btnAddQueue.innerHTML;
|
const originalText = elements.btnAddQueue.innerHTML;
|
||||||
@@ -279,7 +284,9 @@ const RoomsApp = (function() {
|
|||||||
}
|
}
|
||||||
if (elements.roomSdToggle) {
|
if (elements.roomSdToggle) {
|
||||||
elements.roomSdToggle.onclick = () => {
|
elements.roomSdToggle.onclick = () => {
|
||||||
if (!isHost) return;
|
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||||
|
|
||||||
|
if (!hasAccess) return;
|
||||||
const currentState = elements.roomSdToggle.getAttribute('data-state');
|
const currentState = elements.roomSdToggle.getAttribute('data-state');
|
||||||
const newState = currentState === 'sub' ? 'dub' : 'sub';
|
const newState = currentState === 'sub' ? 'dub' : 'sub';
|
||||||
|
|
||||||
@@ -338,7 +345,9 @@ const RoomsApp = (function() {
|
|||||||
// --- QUICK CONTROLS LOGIC (Header) ---
|
// --- QUICK CONTROLS LOGIC (Header) ---
|
||||||
|
|
||||||
async function populateQuickControls() {
|
async function populateQuickControls() {
|
||||||
if (!isHost || !selectedAnimeData) return;
|
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||||
|
|
||||||
|
if (!hasAccess || !selectedAnimeData) return;
|
||||||
if (!extensionsReady) return;
|
if (!extensionsReady) return;
|
||||||
|
|
||||||
elements.roomExtSelect.innerHTML = '';
|
elements.roomExtSelect.innerHTML = '';
|
||||||
@@ -356,7 +365,8 @@ const RoomsApp = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onQuickExtensionChange(e, silent = false) {
|
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 ext = elements.roomExtSelect.value;
|
||||||
const settings = extensionsStore.settings[ext];
|
const settings = extensionsStore.settings[ext];
|
||||||
@@ -433,7 +443,9 @@ const RoomsApp = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onQuickServerChange() {
|
function onQuickServerChange() {
|
||||||
if (!isHost) return;
|
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||||
|
if (!hasAccess) return;
|
||||||
|
|
||||||
launchStream(false);
|
launchStream(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,6 +500,28 @@ const RoomsApp = (function() {
|
|||||||
if(elements.btnLaunch) elements.btnLaunch.disabled = false;
|
if(elements.btnLaunch) elements.btnLaunch.disabled = false;
|
||||||
if(elements.btnAddQueue) elements.btnAddQueue.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();
|
setupConfigListeners();
|
||||||
|
|
||||||
elements.stepSearch.style.display = 'none';
|
elements.stepSearch.style.display = 'none';
|
||||||
@@ -495,7 +529,7 @@ const RoomsApp = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupConfigListeners() {
|
function setupConfigListeners() {
|
||||||
elements.epInc.onclick = () => {
|
elements.epInc.onclick = () => {
|
||||||
elements.inpEpisode.value = parseInt(elements.inpEpisode.value || 0) + 1;
|
elements.inpEpisode.value = parseInt(elements.inpEpisode.value || 0) + 1;
|
||||||
};
|
};
|
||||||
elements.epDec.onclick = () => {
|
elements.epDec.onclick = () => {
|
||||||
@@ -625,12 +659,24 @@ const RoomsApp = (function() {
|
|||||||
let episodeToPlay = activeContext.episode;
|
let episodeToPlay = activeContext.episode;
|
||||||
if (fromModal && elements.inpEpisode) episodeToPlay = elements.inpEpisode.value;
|
if (fromModal && elements.inpEpisode) episodeToPlay = elements.inpEpisode.value;
|
||||||
|
|
||||||
const ext = overrides.forceExtension || elements.roomExtSelect.value || activeContext.extension;
|
const ext = overrides.forceExtension ||
|
||||||
const server = overrides.forceServer || elements.roomServerSelect.value || activeContext.server;
|
(fromModal ? (configState.extension || elements.selExtension?.value) : null) ||
|
||||||
const category = elements.roomSdToggle.getAttribute('data-state') || activeContext.category;
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,7 +896,6 @@ const RoomsApp = (function() {
|
|||||||
|
|
||||||
if (context.extension && elements.roomExtSelect.value !== context.extension) {
|
if (context.extension && elements.roomExtSelect.value !== 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);
|
await onQuickExtensionChange(null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,7 +963,7 @@ const RoomsApp = (function() {
|
|||||||
episode: ep,
|
episode: ep,
|
||||||
title: currentAnimeDetails.title.userPreferred,
|
title: currentAnimeDetails.title.userPreferred,
|
||||||
server: document.getElementById('room-server-select').value || 'gogoanime',
|
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;
|
currentUserId = data.userId;
|
||||||
currentUsername = data.username;
|
currentUsername = data.username;
|
||||||
isGuest = data.isGuest;
|
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);
|
updateRoomUI(data.room);
|
||||||
|
|
||||||
if (data.room.currentVideo) {
|
if (data.room.currentVideo) {
|
||||||
@@ -1109,6 +1166,7 @@ const RoomsApp = (function() {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'users_update':
|
case 'users_update':
|
||||||
|
saveRoomData({ users: data.users });
|
||||||
renderUsersList(data.users);
|
renderUsersList(data.users);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -1127,6 +1185,32 @@ const RoomsApp = (function() {
|
|||||||
updateUsersList();
|
updateUsersList();
|
||||||
break;
|
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':
|
case 'chat':
|
||||||
addChatMessage(data);
|
addChatMessage(data);
|
||||||
const isChatHidden = elements.roomLayout.classList.contains('chat-hidden');
|
const isChatHidden = elements.roomLayout.classList.contains('chat-hidden');
|
||||||
@@ -1210,33 +1294,34 @@ const RoomsApp = (function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canManageQueue = isHost || (window.__userPermissions && window.__userPermissions.canManageQueue);
|
||||||
elements.queueList.innerHTML = queueItems.map((item, index) => {
|
elements.queueList.innerHTML = queueItems.map((item, index) => {
|
||||||
let actionsHtml = '';
|
let actionsHtml = '';
|
||||||
|
|
||||||
if (isHost) {
|
if (canManageQueue) {
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
const isLast = index === queueItems.length - 1;
|
const isLast = index === queueItems.length - 1;
|
||||||
|
|
||||||
actionsHtml = `
|
actionsHtml = `
|
||||||
<div class="q-actions">
|
<div class="q-actions">
|
||||||
<button class="q-btn play" onclick="playQueueItem('${item.uid}')" title="Play Now">
|
<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>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div style="display:flex; gap:2px;">
|
<div style="display:flex; gap:2px;">
|
||||||
${!isFirst ? `
|
${!isFirst ? `
|
||||||
<button class="q-btn" onclick="moveQueueItem('${item.uid}', 'up')" title="Move Up">
|
<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>
|
<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>` : ''}
|
</button>` : ''}
|
||||||
${!isLast ? `
|
${!isLast ? `
|
||||||
<button class="q-btn" onclick="moveQueueItem('${item.uid}', 'down')" title="Move Down">
|
<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>
|
<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>` : ''}
|
</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>
|
</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 `
|
return `
|
||||||
@@ -1248,11 +1333,17 @@ const RoomsApp = (function() {
|
|||||||
</div>
|
</div>
|
||||||
${actionsHtml}
|
${actionsHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.playQueueItem = async function(uid) {
|
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);
|
const item = currentQueue.find(i => i.uid === uid);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
@@ -1305,12 +1396,25 @@ const RoomsApp = (function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.moveQueueItem = function(uid, direction) {
|
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) {
|
if(ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: 'queue_move', itemUid: uid, direction }));
|
ws.send(JSON.stringify({ type: 'queue_move', itemUid: uid, direction }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
window.removeQueueItem = function(uid) {
|
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) {
|
if(ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: 'queue_remove', itemUid: uid }));
|
ws.send(JSON.stringify({ type: 'queue_remove', itemUid: uid }));
|
||||||
}
|
}
|
||||||
@@ -1321,17 +1425,39 @@ const RoomsApp = (function() {
|
|||||||
elements.roomViewers.textContent = `${room.users.length}`;
|
elements.roomViewers.textContent = `${room.users.length}`;
|
||||||
|
|
||||||
const currentUser = room.users.find(u => u.id === currentUserId);
|
const currentUser = room.users.find(u => u.id === currentUserId);
|
||||||
|
|
||||||
|
if (currentUser) {
|
||||||
|
window.__userPermissions = currentUser.permissions || { canControl: false, canManageQueue: false };
|
||||||
|
}
|
||||||
isHost = currentUser?.isHost || false;
|
isHost = currentUser?.isHost || false;
|
||||||
|
|
||||||
if (elements.selectAnimeBtn) elements.selectAnimeBtn.style.display = isHost ? 'flex' : 'none';
|
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||||
if (elements.hostControls) elements.hostControls.style.display = isHost ? 'flex' : 'none';
|
|
||||||
|
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') {
|
if (window.AnimePlayer && typeof window.AnimePlayer.setRoomHost === 'function') {
|
||||||
window.AnimePlayer.setRoomHost(isHost);
|
window.AnimePlayer.setRoomHost(canControlPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHost) {
|
if (isHost) {
|
||||||
initGlobalControls();
|
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');
|
const copyInviteBtn = document.getElementById('copy-invite-btn');
|
||||||
@@ -1387,7 +1513,8 @@ const RoomsApp = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function initGlobalControls() {
|
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;
|
if (elements.roomExtSelect.options.length > 1) return;
|
||||||
|
|
||||||
@@ -1820,6 +1947,274 @@ const RoomsApp = (function() {
|
|||||||
return div.innerHTML;
|
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 };
|
return { init };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -628,3 +628,226 @@ input[type=number] { -moz-appearance: textfield; }
|
|||||||
.config-cover { width: 80px; }
|
.config-cover { width: 80px; }
|
||||||
.ep-control { width: auto; flex: 1; }
|
.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 crypto from 'crypto';
|
||||||
import { closeTunnelIfUnused } from "./tunnel.manager";
|
import { closeTunnelIfUnused } from "./tunnel.manager";
|
||||||
|
|
||||||
|
interface RoomPermissions {
|
||||||
|
canControl: boolean;
|
||||||
|
canManageQueue: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface RoomUser {
|
interface RoomUser {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -8,6 +13,8 @@ interface RoomUser {
|
|||||||
isHost: boolean;
|
isHost: boolean;
|
||||||
isGuest: boolean;
|
isGuest: boolean;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
permissions?: RoomPermissions;
|
||||||
|
ipAddress?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SourceContext {
|
interface SourceContext {
|
||||||
@@ -55,8 +62,14 @@ interface RoomData {
|
|||||||
exposed: boolean;
|
exposed: boolean;
|
||||||
publicUrl?: string;
|
publicUrl?: string;
|
||||||
queue: QueueItem[];
|
queue: QueueItem[];
|
||||||
|
bannedIPs: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_GUEST_PERMISSIONS: RoomPermissions = {
|
||||||
|
canControl: false,
|
||||||
|
canManageQueue: false
|
||||||
|
};
|
||||||
|
|
||||||
const rooms = new Map<string, RoomData>();
|
const rooms = new Map<string, RoomData>();
|
||||||
|
|
||||||
export function generateRoomId(): string {
|
export function generateRoomId(): string {
|
||||||
@@ -77,13 +90,81 @@ export function createRoom(name: string, host: RoomUser, password?: string, expo
|
|||||||
metadata: null,
|
metadata: null,
|
||||||
exposed,
|
exposed,
|
||||||
publicUrl,
|
publicUrl,
|
||||||
queue: []
|
queue: [],
|
||||||
|
bannedIPs: new Set() // NUEVO
|
||||||
};
|
};
|
||||||
|
|
||||||
rooms.set(roomId, room);
|
rooms.set(roomId, room);
|
||||||
return 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 {
|
export function getRoom(roomId: string): RoomData | null {
|
||||||
return rooms.get(roomId) || null;
|
return rooms.get(roomId) || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,13 @@ interface WSClient {
|
|||||||
|
|
||||||
const clients = new Map<string, WSClient>();
|
const clients = new Map<string, WSClient>();
|
||||||
|
|
||||||
interface WSParams {
|
|
||||||
roomId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WSQuery {
|
function getClientIP(req: any): string {
|
||||||
token?: string;
|
return req.headers['x-forwarded-for']?.split(',')[0].trim() ||
|
||||||
guestName?: string;
|
req.headers['x-real-ip'] ||
|
||||||
password?: string;
|
req.connection?.remoteAddress ||
|
||||||
|
req.socket?.remoteAddress ||
|
||||||
|
'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupRoomWebSocket(fastify: FastifyInstance) {
|
export function setupRoomWebSocket(fastify: FastifyInstance) {
|
||||||
@@ -44,13 +43,14 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
|||||||
const guestName = req.query.guestName;
|
const guestName = req.query.guestName;
|
||||||
const password = req.query.password;
|
const password = req.query.password;
|
||||||
|
|
||||||
|
const clientIP = getClientIP(req); // NUEVO
|
||||||
|
|
||||||
let userId: string;
|
let userId: string;
|
||||||
let username: string;
|
let username: string;
|
||||||
let avatar: string | undefined;
|
let avatar: string | undefined;
|
||||||
let isGuest = false;
|
let isGuest = false;
|
||||||
let realUserId: any;
|
let realUserId: any;
|
||||||
|
|
||||||
// Verificar si la sala existe
|
|
||||||
const room = roomService.getRoom(roomId);
|
const room = roomService.getRoom(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
socket.send(JSON.stringify({
|
socket.send(JSON.stringify({
|
||||||
@@ -61,6 +61,24 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
|||||||
return;
|
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
|
// Verificar contraseña si existe
|
||||||
if (room.password) {
|
if (room.password) {
|
||||||
if (!password || !roomService.verifyRoomPassword(roomId, password)) {
|
if (!password || !roomService.verifyRoomPassword(roomId, password)) {
|
||||||
@@ -134,14 +152,15 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
|||||||
isHost
|
isHost
|
||||||
});
|
});
|
||||||
|
|
||||||
// Agregar usuario a la sala
|
|
||||||
const userInRoom = {
|
const userInRoom = {
|
||||||
id: userId,
|
id: userId,
|
||||||
username,
|
username,
|
||||||
avatar,
|
avatar,
|
||||||
isHost: isHost, // ← CORREGIDO: Usar la verificación correcta
|
isHost: isHost,
|
||||||
isGuest,
|
isGuest,
|
||||||
userId: realUserId
|
userId: realUserId,
|
||||||
|
ipAddress: clientIP, // NUEVO
|
||||||
|
permissions: isHost ? undefined : { ...roomService.DEFAULT_GUEST_PERMISSIONS }
|
||||||
};
|
};
|
||||||
|
|
||||||
roomService.addUserToRoom(roomId, userInRoom);
|
roomService.addUserToRoom(roomId, userInRoom);
|
||||||
@@ -160,6 +179,7 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
|||||||
userId,
|
userId,
|
||||||
username,
|
username,
|
||||||
isGuest,
|
isGuest,
|
||||||
|
isHost, // NUEVO: Enviar explícitamente
|
||||||
room: {
|
room: {
|
||||||
id: room.id,
|
id: room.id,
|
||||||
name: room.name,
|
name: room.name,
|
||||||
@@ -168,7 +188,8 @@ async function handleWebSocketConnection(connection: any, req: any) {
|
|||||||
username: u.username,
|
username: u.username,
|
||||||
avatar: u.avatar,
|
avatar: u.avatar,
|
||||||
isHost: u.isHost,
|
isHost: u.isHost,
|
||||||
isGuest: u.isGuest
|
isGuest: u.isGuest,
|
||||||
|
permissions: u.permissions // NUEVO
|
||||||
})),
|
})),
|
||||||
currentVideo: room.currentVideo,
|
currentVideo: room.currentVideo,
|
||||||
queue: room.queue || []
|
queue: room.queue || []
|
||||||
@@ -212,6 +233,9 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
const room = roomService.getRoom(roomId);
|
const room = roomService.getRoom(roomId);
|
||||||
if (!room) return;
|
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);
|
console.log('Handling message:', data.type, 'from user:', userId, 'isHost:', room.host.id === userId);
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
@@ -226,8 +250,65 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
});
|
});
|
||||||
break;
|
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':
|
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);
|
const itemToPlay = roomService.getAndRemoveQueueItem(roomId, data.itemUid);
|
||||||
if (itemToPlay) {
|
if (itemToPlay) {
|
||||||
@@ -255,7 +336,9 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'queue_move':
|
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);
|
const moved = roomService.moveQueueItem(roomId, data.itemUid, data.direction);
|
||||||
if (moved) {
|
if (moved) {
|
||||||
@@ -295,7 +378,14 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'video_update':
|
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.updateRoomVideo(roomId, data.video);
|
||||||
roomService.updateRoomMetadata(roomId, data.metadata);
|
roomService.updateRoomMetadata(roomId, data.metadata);
|
||||||
@@ -308,13 +398,13 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'queue_add_batch':
|
case 'queue_add_batch':
|
||||||
if (room.host.id !== userId) return;
|
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(data.items)) {
|
if (Array.isArray(data.items)) {
|
||||||
// Añadimos el índice (i) al forEach
|
|
||||||
data.items.forEach((item: any, i: number) => {
|
data.items.forEach((item: any, i: number) => {
|
||||||
const newItem = {
|
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)}`,
|
uid: `q_${Date.now()}_${i}_${Math.random().toString(36).substr(2, 5)}`,
|
||||||
metadata: item.metadata,
|
metadata: item.metadata,
|
||||||
videoData: item.video,
|
videoData: item.video,
|
||||||
@@ -365,13 +455,11 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'play':
|
case 'play':
|
||||||
// Solo el host puede controlar la reproducción
|
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canControl')) {
|
||||||
if (room.host.id !== userId) {
|
console.warn('User lacks control permissions');
|
||||||
console.warn('Non-host attempted play:', userId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Broadcasting play event to room:', roomId);
|
|
||||||
broadcastToRoom(roomId, {
|
broadcastToRoom(roomId, {
|
||||||
type: 'play',
|
type: 'play',
|
||||||
currentTime: data.currentTime,
|
currentTime: data.currentTime,
|
||||||
@@ -380,8 +468,8 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'pause':
|
case 'pause':
|
||||||
if (room.host.id !== userId) {
|
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canControl')) {
|
||||||
console.warn('Non-host attempted pause:', userId);
|
console.warn('User lacks control permissions for pause');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,8 +482,8 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'seek':
|
case 'seek':
|
||||||
if (room.host.id !== userId) {
|
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canControl')) {
|
||||||
console.warn('Non-host attempted seek:', userId);
|
console.warn('User lacks control permissions for seek');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,19 +524,19 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'queue_add':
|
case 'queue_add':
|
||||||
// Solo el host (o todos si quisieras) pueden añadir
|
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||||
if (room.host.id !== userId) return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newItem = {
|
const newItem = {
|
||||||
uid: `q_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
|
uid: `q_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
|
||||||
metadata: data.metadata,
|
metadata: data.metadata,
|
||||||
videoData: data.video, // El objeto que contiene url, type, subtitles
|
videoData: data.video,
|
||||||
addedBy: room.users.get(userId)?.username || 'Unknown'
|
addedBy: room.users.get(userId)?.username || 'Unknown'
|
||||||
};
|
};
|
||||||
|
|
||||||
roomService.addQueueItem(roomId, newItem);
|
roomService.addQueueItem(roomId, newItem);
|
||||||
|
|
||||||
// Avisar a todos que la cola cambió
|
|
||||||
broadcastToRoom(roomId, {
|
broadcastToRoom(roomId, {
|
||||||
type: 'queue_update',
|
type: 'queue_update',
|
||||||
queue: room.queue
|
queue: room.queue
|
||||||
@@ -456,7 +544,10 @@ function handleMessage(roomId: string, userId: string, data: any) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'queue_remove':
|
case 'queue_remove':
|
||||||
if (room.host.id !== userId) return;
|
if (!user.isHost && !roomService.hasPermission(roomId, userId, 'canManageQueue')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
roomService.removeQueueItem(roomId, data.itemUid);
|
roomService.removeQueueItem(roomId, data.itemUid);
|
||||||
broadcastToRoom(roomId, {
|
broadcastToRoom(roomId, {
|
||||||
type: 'queue_update',
|
type: 'queue_update',
|
||||||
|
|||||||
@@ -402,8 +402,8 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
// Control functions
|
// Control functions
|
||||||
function togglePlayPause() {
|
function togglePlayPause() {
|
||||||
if (_roomMode && !_isRoomHost) {
|
if (_roomMode && !_isRoomHost && !hasControlPermission()) {
|
||||||
console.log('Guests cannot control playback');
|
showPermissionToast('You need playback control permission');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,12 +411,14 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
if (els.video.paused) {
|
if (els.video.paused) {
|
||||||
els.video.play().catch(() => {});
|
els.video.play().catch(() => {});
|
||||||
if (_roomMode && _isRoomHost) {
|
|
||||||
|
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||||
sendRoomEvent('play', { currentTime: els.video.currentTime });
|
sendRoomEvent('play', { currentTime: els.video.currentTime });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
els.video.pause();
|
els.video.pause();
|
||||||
if (_roomMode && _isRoomHost) {
|
|
||||||
|
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||||
sendRoomEvent('pause', { currentTime: els.video.currentTime });
|
sendRoomEvent('pause', { currentTime: els.video.currentTime });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,7 +462,7 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
function seekToPosition(e) {
|
function seekToPosition(e) {
|
||||||
if (!els.video || !els.progressContainer) return;
|
if (!els.video || !els.progressContainer) return;
|
||||||
if (_roomMode && !_isRoomHost) return;
|
if (_roomMode && !_isRoomHost && !hasControlPermission()) return;
|
||||||
|
|
||||||
const rect = els.progressContainer.getBoundingClientRect();
|
const rect = els.progressContainer.getBoundingClientRect();
|
||||||
const pos = (e.clientX - rect.left) / rect.width;
|
const pos = (e.clientX - rect.left) / rect.width;
|
||||||
@@ -468,8 +470,7 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
els.video.currentTime = newTime;
|
els.video.currentTime = newTime;
|
||||||
|
|
||||||
// En room mode, enviar evento de seek
|
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||||
if (_roomMode && _isRoomHost) {
|
|
||||||
sendRoomEvent('seek', { currentTime: newTime });
|
sendRoomEvent('seek', { currentTime: newTime });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -484,29 +485,63 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
function seekRelative(seconds) {
|
function seekRelative(seconds) {
|
||||||
if (!els.video) return;
|
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));
|
const newTime = Math.max(0, Math.min(els.video.duration, els.video.currentTime + seconds));
|
||||||
els.video.currentTime = newTime;
|
els.video.currentTime = newTime;
|
||||||
|
|
||||||
// En room mode, enviar evento de seek
|
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||||
if (_roomMode && _isRoomHost) {
|
|
||||||
sendRoomEvent('seek', { currentTime: newTime });
|
sendRoomEvent('seek', { currentTime: newTime });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function seekToPercent(percent) {
|
function seekToPercent(percent) {
|
||||||
if (!els.video) return;
|
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;
|
const newTime = els.video.duration * percent;
|
||||||
els.video.currentTime = newTime;
|
els.video.currentTime = newTime;
|
||||||
|
|
||||||
// En room mode, enviar evento de seek
|
if (_roomMode && (_isRoomHost || hasControlPermission())) {
|
||||||
if (_roomMode && _isRoomHost) {
|
|
||||||
sendRoomEvent('seek', { currentTime: newTime });
|
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
|
// Video event handlers
|
||||||
function onPlay() {
|
function onPlay() {
|
||||||
if (els.playPauseBtn) {
|
if (els.playPauseBtn) {
|
||||||
@@ -589,7 +624,9 @@ const AnimePlayer = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendRoomEvent(eventType, data = {}) {
|
function sendRoomEvent(eventType, data = {}) {
|
||||||
if (!_roomMode || !_isRoomHost || !_roomWebSocket) return;
|
if (!_roomMode || !_roomWebSocket) return;
|
||||||
|
if (!_isRoomHost && !hasControlPermission()) return;
|
||||||
|
|
||||||
if (_roomWebSocket.readyState !== WebSocket.OPEN) return;
|
if (_roomWebSocket.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
console.log('Sending room event:', eventType, data);
|
console.log('Sending room event:', eventType, data);
|
||||||
|
|||||||
@@ -232,10 +232,15 @@ const RoomsApp = (function() {
|
|||||||
|
|
||||||
if (elements.btnAddQueue) {
|
if (elements.btnAddQueue) {
|
||||||
elements.btnAddQueue.onclick = () => {
|
elements.btnAddQueue.onclick = () => {
|
||||||
|
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||||
|
if (!canManageQueue) {
|
||||||
|
showSystemToast('You need queue management permission');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedEpisodes.size === 0) return;
|
if (selectedEpisodes.size === 0) return;
|
||||||
|
|
||||||
const sortedEps = Array.from(selectedEpisodes).sort((a, b) => a - b);
|
const sortedEps = Array.from(selectedEpisodes).sort((a, b) => a - b);
|
||||||
|
|
||||||
const currentProvider = elements.roomExtSelect ? elements.roomExtSelect.value : 'gogoanime';
|
const currentProvider = elements.roomExtSelect ? elements.roomExtSelect.value : 'gogoanime';
|
||||||
|
|
||||||
const originalText = elements.btnAddQueue.innerHTML;
|
const originalText = elements.btnAddQueue.innerHTML;
|
||||||
@@ -279,7 +284,9 @@ const RoomsApp = (function() {
|
|||||||
}
|
}
|
||||||
if (elements.roomSdToggle) {
|
if (elements.roomSdToggle) {
|
||||||
elements.roomSdToggle.onclick = () => {
|
elements.roomSdToggle.onclick = () => {
|
||||||
if (!isHost) return;
|
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||||
|
|
||||||
|
if (!hasAccess) return;
|
||||||
const currentState = elements.roomSdToggle.getAttribute('data-state');
|
const currentState = elements.roomSdToggle.getAttribute('data-state');
|
||||||
const newState = currentState === 'sub' ? 'dub' : 'sub';
|
const newState = currentState === 'sub' ? 'dub' : 'sub';
|
||||||
|
|
||||||
@@ -338,7 +345,9 @@ const RoomsApp = (function() {
|
|||||||
// --- QUICK CONTROLS LOGIC (Header) ---
|
// --- QUICK CONTROLS LOGIC (Header) ---
|
||||||
|
|
||||||
async function populateQuickControls() {
|
async function populateQuickControls() {
|
||||||
if (!isHost || !selectedAnimeData) return;
|
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||||
|
|
||||||
|
if (!hasAccess || !selectedAnimeData) return;
|
||||||
if (!extensionsReady) return;
|
if (!extensionsReady) return;
|
||||||
|
|
||||||
elements.roomExtSelect.innerHTML = '';
|
elements.roomExtSelect.innerHTML = '';
|
||||||
@@ -356,7 +365,8 @@ const RoomsApp = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onQuickExtensionChange(e, silent = false) {
|
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 ext = elements.roomExtSelect.value;
|
||||||
const settings = extensionsStore.settings[ext];
|
const settings = extensionsStore.settings[ext];
|
||||||
@@ -433,7 +443,9 @@ const RoomsApp = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onQuickServerChange() {
|
function onQuickServerChange() {
|
||||||
if (!isHost) return;
|
const hasAccess = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||||
|
if (!hasAccess) return;
|
||||||
|
|
||||||
launchStream(false);
|
launchStream(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,6 +500,28 @@ const RoomsApp = (function() {
|
|||||||
if(elements.btnLaunch) elements.btnLaunch.disabled = false;
|
if(elements.btnLaunch) elements.btnLaunch.disabled = false;
|
||||||
if(elements.btnAddQueue) elements.btnAddQueue.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();
|
setupConfigListeners();
|
||||||
|
|
||||||
elements.stepSearch.style.display = 'none';
|
elements.stepSearch.style.display = 'none';
|
||||||
@@ -625,12 +659,24 @@ const RoomsApp = (function() {
|
|||||||
let episodeToPlay = activeContext.episode;
|
let episodeToPlay = activeContext.episode;
|
||||||
if (fromModal && elements.inpEpisode) episodeToPlay = elements.inpEpisode.value;
|
if (fromModal && elements.inpEpisode) episodeToPlay = elements.inpEpisode.value;
|
||||||
|
|
||||||
const ext = overrides.forceExtension || elements.roomExtSelect.value || activeContext.extension;
|
const ext = overrides.forceExtension ||
|
||||||
const server = overrides.forceServer || elements.roomServerSelect.value || activeContext.server;
|
(fromModal ? (configState.extension || elements.selExtension?.value) : null) ||
|
||||||
const category = elements.roomSdToggle.getAttribute('data-state') || activeContext.category;
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,7 +896,6 @@ const RoomsApp = (function() {
|
|||||||
|
|
||||||
if (context.extension && elements.roomExtSelect.value !== context.extension) {
|
if (context.extension && elements.roomExtSelect.value !== 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);
|
await onQuickExtensionChange(null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,7 +963,7 @@ const RoomsApp = (function() {
|
|||||||
episode: ep,
|
episode: ep,
|
||||||
title: currentAnimeDetails.title.userPreferred,
|
title: currentAnimeDetails.title.userPreferred,
|
||||||
server: document.getElementById('room-server-select').value || 'gogoanime',
|
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;
|
currentUserId = data.userId;
|
||||||
currentUsername = data.username;
|
currentUsername = data.username;
|
||||||
isGuest = data.isGuest;
|
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);
|
updateRoomUI(data.room);
|
||||||
|
|
||||||
if (data.room.currentVideo) {
|
if (data.room.currentVideo) {
|
||||||
@@ -1109,6 +1166,7 @@ const RoomsApp = (function() {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'users_update':
|
case 'users_update':
|
||||||
|
saveRoomData({ users: data.users });
|
||||||
renderUsersList(data.users);
|
renderUsersList(data.users);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -1127,6 +1185,32 @@ const RoomsApp = (function() {
|
|||||||
updateUsersList();
|
updateUsersList();
|
||||||
break;
|
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':
|
case 'chat':
|
||||||
addChatMessage(data);
|
addChatMessage(data);
|
||||||
const isChatHidden = elements.roomLayout.classList.contains('chat-hidden');
|
const isChatHidden = elements.roomLayout.classList.contains('chat-hidden');
|
||||||
@@ -1210,33 +1294,34 @@ const RoomsApp = (function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canManageQueue = isHost || (window.__userPermissions && window.__userPermissions.canManageQueue);
|
||||||
elements.queueList.innerHTML = queueItems.map((item, index) => {
|
elements.queueList.innerHTML = queueItems.map((item, index) => {
|
||||||
let actionsHtml = '';
|
let actionsHtml = '';
|
||||||
|
|
||||||
if (isHost) {
|
if (canManageQueue) {
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
const isLast = index === queueItems.length - 1;
|
const isLast = index === queueItems.length - 1;
|
||||||
|
|
||||||
actionsHtml = `
|
actionsHtml = `
|
||||||
<div class="q-actions">
|
<div class="q-actions">
|
||||||
<button class="q-btn play" onclick="playQueueItem('${item.uid}')" title="Play Now">
|
<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>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div style="display:flex; gap:2px;">
|
<div style="display:flex; gap:2px;">
|
||||||
${!isFirst ? `
|
${!isFirst ? `
|
||||||
<button class="q-btn" onclick="moveQueueItem('${item.uid}', 'up')" title="Move Up">
|
<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>
|
<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>` : ''}
|
</button>` : ''}
|
||||||
${!isLast ? `
|
${!isLast ? `
|
||||||
<button class="q-btn" onclick="moveQueueItem('${item.uid}', 'down')" title="Move Down">
|
<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>
|
<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>` : ''}
|
</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>
|
</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 `
|
return `
|
||||||
@@ -1248,11 +1333,17 @@ const RoomsApp = (function() {
|
|||||||
</div>
|
</div>
|
||||||
${actionsHtml}
|
${actionsHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.playQueueItem = async function(uid) {
|
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);
|
const item = currentQueue.find(i => i.uid === uid);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
@@ -1305,12 +1396,25 @@ const RoomsApp = (function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.moveQueueItem = function(uid, direction) {
|
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) {
|
if(ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: 'queue_move', itemUid: uid, direction }));
|
ws.send(JSON.stringify({ type: 'queue_move', itemUid: uid, direction }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
window.removeQueueItem = function(uid) {
|
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) {
|
if(ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: 'queue_remove', itemUid: uid }));
|
ws.send(JSON.stringify({ type: 'queue_remove', itemUid: uid }));
|
||||||
}
|
}
|
||||||
@@ -1321,17 +1425,39 @@ const RoomsApp = (function() {
|
|||||||
elements.roomViewers.textContent = `${room.users.length}`;
|
elements.roomViewers.textContent = `${room.users.length}`;
|
||||||
|
|
||||||
const currentUser = room.users.find(u => u.id === currentUserId);
|
const currentUser = room.users.find(u => u.id === currentUserId);
|
||||||
|
|
||||||
|
if (currentUser) {
|
||||||
|
window.__userPermissions = currentUser.permissions || { canControl: false, canManageQueue: false };
|
||||||
|
}
|
||||||
isHost = currentUser?.isHost || false;
|
isHost = currentUser?.isHost || false;
|
||||||
|
|
||||||
if (elements.selectAnimeBtn) elements.selectAnimeBtn.style.display = isHost ? 'flex' : 'none';
|
const canManageQueue = isHost || (window.__userPermissions?.canManageQueue || false);
|
||||||
if (elements.hostControls) elements.hostControls.style.display = isHost ? 'flex' : 'none';
|
|
||||||
|
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') {
|
if (window.AnimePlayer && typeof window.AnimePlayer.setRoomHost === 'function') {
|
||||||
window.AnimePlayer.setRoomHost(isHost);
|
window.AnimePlayer.setRoomHost(canControlPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHost) {
|
if (isHost) {
|
||||||
initGlobalControls();
|
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');
|
const copyInviteBtn = document.getElementById('copy-invite-btn');
|
||||||
@@ -1387,7 +1513,8 @@ const RoomsApp = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function initGlobalControls() {
|
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;
|
if (elements.roomExtSelect.options.length > 1) return;
|
||||||
|
|
||||||
@@ -1820,6 +1947,274 @@ const RoomsApp = (function() {
|
|||||||
return div.innerHTML;
|
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 };
|
return { init };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -628,3 +628,226 @@ input[type=number] { -moz-appearance: textfield; }
|
|||||||
.config-cover { width: 80px; }
|
.config-cover { width: 80px; }
|
||||||
.ep-control { width: auto; flex: 1; }
|
.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