added watchparties
This commit is contained in:
141
desktop/src/api/rooms/rooms.controller.ts
Normal file
141
desktop/src/api/rooms/rooms.controller.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import * as roomService from './rooms.service';
|
||||
import { getUserById } from '../user/user.service';
|
||||
|
||||
interface CreateRoomBody {
|
||||
name: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
interface JoinRoomBody {
|
||||
password?: string;
|
||||
guestName?: string;
|
||||
}
|
||||
|
||||
export async function createRoom(req: any, reply: FastifyReply) {
|
||||
try {
|
||||
const { name, password } = req.body as CreateRoomBody;
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return reply.code(401).send({ error: "Authentication required to create room" });
|
||||
}
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
return reply.code(400).send({ error: "Room name is required" });
|
||||
}
|
||||
|
||||
const user = await getUserById(userId);
|
||||
if (!user) {
|
||||
return reply.code(404).send({ error: "User not found" });
|
||||
}
|
||||
|
||||
const host = {
|
||||
id: `user_${userId}`,
|
||||
username: user.username,
|
||||
avatar: user.profile_picture_url || undefined,
|
||||
isHost: true,
|
||||
isGuest: false,
|
||||
userId
|
||||
};
|
||||
|
||||
const room = roomService.createRoom(name, host, password);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
room: {
|
||||
id: room.id,
|
||||
name: room.name,
|
||||
hasPassword: !!room.password,
|
||||
userCount: room.users.size
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Create Room Error:", err);
|
||||
return reply.code(500).send({ error: "Failed to create room" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRooms(req: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const rooms = roomService.getAllRooms();
|
||||
|
||||
const roomList = rooms.map((room) => ({
|
||||
id: room.id,
|
||||
name: room.name,
|
||||
host: room.host.username,
|
||||
userCount: room.users.size,
|
||||
hasPassword: !!room.password,
|
||||
currentlyWatching: room.currentVideo ? {
|
||||
animeId: room.currentVideo.animeId,
|
||||
episode: room.currentVideo.episode
|
||||
} : null
|
||||
}));
|
||||
|
||||
return reply.send({ rooms: roomList });
|
||||
} catch (err) {
|
||||
console.error("Get Rooms Error:", err);
|
||||
return reply.code(500).send({ error: "Failed to retrieve rooms" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRoom(req: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { id } = req.params as { id: string };
|
||||
const room = roomService.getRoom(id);
|
||||
|
||||
if (!room) {
|
||||
return reply.code(404).send({ error: "Room not found" });
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
room: {
|
||||
id: room.id,
|
||||
name: room.name,
|
||||
host: {
|
||||
username: room.host.username,
|
||||
avatar: room.host.avatar
|
||||
},
|
||||
users: Array.from(room.users.values()).map(u => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
isHost: u.isHost,
|
||||
isGuest: u.isGuest
|
||||
})),
|
||||
hasPassword: !!room.password,
|
||||
currentVideo: room.currentVideo
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Get Room Error:", err);
|
||||
return reply.code(500).send({ error: "Failed to retrieve room" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRoom(req: any, reply: FastifyReply) {
|
||||
try {
|
||||
const { id } = req.params as { id: string };
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return reply.code(401).send({ error: "Authentication required" });
|
||||
}
|
||||
|
||||
const room = roomService.getRoom(id);
|
||||
if (!room) {
|
||||
return reply.code(404).send({ error: "Room not found" });
|
||||
}
|
||||
|
||||
if (room.host.userId !== userId) {
|
||||
return reply.code(403).send({ error: "Only the host can delete the room" });
|
||||
}
|
||||
|
||||
roomService.deleteRoom(id);
|
||||
|
||||
return reply.send({ success: true });
|
||||
} catch (err) {
|
||||
console.error("Delete Room Error:", err);
|
||||
return reply.code(500).send({ error: "Failed to delete room" });
|
||||
}
|
||||
}
|
||||
11
desktop/src/api/rooms/rooms.routes.ts
Normal file
11
desktop/src/api/rooms/rooms.routes.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import * as controller from './rooms.controller';
|
||||
|
||||
async function roomRoutes(fastify: FastifyInstance) {
|
||||
fastify.post('/rooms', controller.createRoom);
|
||||
fastify.get('/rooms', controller.getRooms);
|
||||
fastify.get('/rooms/:id', controller.getRoom);
|
||||
fastify.delete('/rooms/:id', controller.deleteRoom);
|
||||
}
|
||||
|
||||
export default roomRoutes;
|
||||
130
desktop/src/api/rooms/rooms.service.ts
Normal file
130
desktop/src/api/rooms/rooms.service.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
interface RoomUser {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar?: string;
|
||||
isHost: boolean;
|
||||
isGuest: boolean;
|
||||
userId?: number; // ID real del usuario si está logueado
|
||||
}
|
||||
|
||||
interface RoomMetadata {
|
||||
id: string;
|
||||
title: string;
|
||||
episode: number;
|
||||
image?: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
interface RoomData {
|
||||
id: string;
|
||||
name: string;
|
||||
host: RoomUser;
|
||||
users: Map<string, RoomUser>;
|
||||
createdAt: number;
|
||||
currentVideo: {
|
||||
animeId?: number;
|
||||
episode?: number;
|
||||
source?: string;
|
||||
videoData?: any;
|
||||
currentTime: number;
|
||||
isPlaying: boolean;
|
||||
} | null;
|
||||
password?: string;
|
||||
metadata?: RoomMetadata | null;
|
||||
}
|
||||
|
||||
const rooms = new Map<string, RoomData>();
|
||||
|
||||
export function generateRoomId(): string {
|
||||
return crypto.randomBytes(8).toString('hex');
|
||||
}
|
||||
|
||||
export function createRoom(name: string, host: RoomUser, password?: string): RoomData {
|
||||
const roomId = generateRoomId();
|
||||
|
||||
const room: RoomData = {
|
||||
id: roomId,
|
||||
name,
|
||||
host,
|
||||
users: new Map([[host.id, host]]),
|
||||
createdAt: Date.now(),
|
||||
currentVideo: null,
|
||||
password: password || undefined,
|
||||
metadata: null,
|
||||
};
|
||||
|
||||
rooms.set(roomId, room);
|
||||
return room;
|
||||
}
|
||||
|
||||
export function getRoom(roomId: string): RoomData | null {
|
||||
return rooms.get(roomId) || null;
|
||||
}
|
||||
|
||||
export function getAllRooms(): RoomData[] {
|
||||
return Array.from(rooms.values()).map(room => ({
|
||||
...room,
|
||||
users: room.users
|
||||
}));
|
||||
}
|
||||
|
||||
export function addUserToRoom(roomId: string, user: RoomUser): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
|
||||
room.users.set(user.id, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function removeUserFromRoom(roomId: string, userId: string): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
|
||||
room.users.delete(userId);
|
||||
|
||||
// Si no quedan usuarios, eliminar la sala
|
||||
if (room.users.size === 0) {
|
||||
rooms.delete(roomId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si era el host, asignar nuevo host
|
||||
if (room.host.id === userId && room.users.size > 0) {
|
||||
const newHost = Array.from(room.users.values())[0];
|
||||
newHost.isHost = true;
|
||||
room.host = newHost;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function updateRoomVideo(roomId: string, videoData: any): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
|
||||
room.currentVideo = videoData;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function deleteRoom(roomId: string): boolean {
|
||||
return rooms.delete(roomId);
|
||||
}
|
||||
|
||||
export function verifyRoomPassword(roomId: string, password?: string): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
if (!room.password) return true;
|
||||
if (!password) return false;
|
||||
|
||||
return room.password === password;
|
||||
}
|
||||
|
||||
export function updateRoomMetadata(roomId: string, metadata: any): boolean {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return false;
|
||||
|
||||
room.metadata = metadata;
|
||||
return true;
|
||||
}
|
||||
397
desktop/src/api/rooms/rooms.websocket.ts
Normal file
397
desktop/src/api/rooms/rooms.websocket.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
import { FastifyInstance, FastifyRequest } from 'fastify';
|
||||
import * as roomService from './rooms.service';
|
||||
import { getUserById } from '../user/user.service';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
interface WSClient {
|
||||
socket: any;
|
||||
userId: string;
|
||||
username: string;
|
||||
roomId: string;
|
||||
isGuest: boolean;
|
||||
}
|
||||
|
||||
const clients = new Map<string, WSClient>();
|
||||
|
||||
interface WSParams {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
interface WSQuery {
|
||||
token?: string;
|
||||
guestName?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export function setupRoomWebSocket(fastify: FastifyInstance) {
|
||||
// @ts-ignore
|
||||
fastify.get('/ws/room/:roomId', { websocket: true }, (connection: any, req: any) => {
|
||||
handleWebSocketConnection(connection, req).catch(err => {
|
||||
console.error('WebSocket error:', err);
|
||||
try {
|
||||
connection.socket.close();
|
||||
} catch (e) {
|
||||
// Socket already closed
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleWebSocketConnection(connection: any, req: any) {
|
||||
const socket = connection.socket || connection;
|
||||
const roomId = req.params.roomId;
|
||||
const token = req.query.token;
|
||||
const guestName = req.query.guestName;
|
||||
const password = req.query.password;
|
||||
|
||||
let userId: string;
|
||||
let username: string;
|
||||
let avatar: string | undefined;
|
||||
let isGuest = false;
|
||||
let realUserId: any;
|
||||
|
||||
// Verificar si la sala existe
|
||||
const room = roomService.getRoom(roomId);
|
||||
if (!room) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'error',
|
||||
message: 'Room not found'
|
||||
}));
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar contraseña si existe
|
||||
if (room.password) {
|
||||
if (!password || !roomService.verifyRoomPassword(roomId, password)) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'error',
|
||||
message: 'Invalid password'
|
||||
}));
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Autenticar usuario o crear invitado
|
||||
if (token) {
|
||||
try {
|
||||
const decoded: any = jwt.verify(token, process.env.JWT_SECRET!);
|
||||
realUserId = decoded.id;
|
||||
const user = await getUserById(realUserId);
|
||||
|
||||
if (user) {
|
||||
userId = `user_${realUserId}`;
|
||||
username = user.username;
|
||||
avatar = user.profile_picture_url || undefined;
|
||||
isGuest = false;
|
||||
} else {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
} catch (err) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'error',
|
||||
message: 'Invalid token'
|
||||
}));
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
} else if (guestName && guestName.trim()) {
|
||||
const nameToCheck = guestName.trim();
|
||||
|
||||
const isNameTaken = Array.from(room.users.values()).some(
|
||||
u => u.username.toLowerCase() === nameToCheck.toLowerCase()
|
||||
);
|
||||
|
||||
if (isNameTaken) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'error',
|
||||
message: 'Username is already taken'
|
||||
}));
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
userId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
username = nameToCheck;
|
||||
isGuest = true;
|
||||
} else {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'error',
|
||||
message: 'Authentication required'
|
||||
}));
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const isHost = room.host.userId === realUserId || room.host.id === userId;
|
||||
|
||||
console.log('WebSocket Connection:', {
|
||||
userId,
|
||||
realUserId,
|
||||
roomHostId: room.host.id,
|
||||
roomHostUserId: room.host.userId,
|
||||
isHost
|
||||
});
|
||||
|
||||
// Agregar usuario a la sala
|
||||
const userInRoom = {
|
||||
id: userId,
|
||||
username,
|
||||
avatar,
|
||||
isHost: isHost, // ← CORREGIDO: Usar la verificación correcta
|
||||
isGuest,
|
||||
userId: realUserId
|
||||
};
|
||||
|
||||
roomService.addUserToRoom(roomId, userInRoom);
|
||||
|
||||
// Registrar cliente
|
||||
clients.set(userId, {
|
||||
socket: socket,
|
||||
userId,
|
||||
username,
|
||||
roomId,
|
||||
isGuest
|
||||
});
|
||||
|
||||
// Enviar estado inicial
|
||||
socket.send(JSON.stringify({
|
||||
type: 'init',
|
||||
userId,
|
||||
username,
|
||||
isGuest,
|
||||
room: {
|
||||
id: room.id,
|
||||
name: room.name,
|
||||
users: Array.from(room.users.values()).map(u => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
isHost: u.isHost,
|
||||
isGuest: u.isGuest
|
||||
})),
|
||||
currentVideo: room.currentVideo
|
||||
}
|
||||
}));
|
||||
|
||||
// Notificar a otros usuarios
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'user_joined',
|
||||
user: {
|
||||
id: userId,
|
||||
username,
|
||||
avatar,
|
||||
isGuest
|
||||
}
|
||||
}, userId);
|
||||
|
||||
// Manejar mensajes
|
||||
socket.on('message', (message: Buffer) => {
|
||||
try {
|
||||
const data = JSON.parse(message.toString());
|
||||
handleMessage(roomId, userId, data);
|
||||
} catch (err) {
|
||||
console.error('WebSocket message error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Manejar desconexión
|
||||
socket.on('close', () => {
|
||||
clients.delete(userId);
|
||||
roomService.removeUserFromRoom(roomId, userId);
|
||||
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'user_left',
|
||||
user: { userId, username }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleMessage(roomId: string, userId: string, data: any) {
|
||||
const room = roomService.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
console.log('Handling message:', data.type, 'from user:', userId, 'isHost:', room.host.id === userId);
|
||||
|
||||
switch (data.type) {
|
||||
case 'chat':
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'chat',
|
||||
userId,
|
||||
username: room.users.get(userId)?.username || 'Unknown',
|
||||
avatar: room.users.get(userId)?.avatar,
|
||||
message: data.message,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
break;
|
||||
|
||||
case 'video_update':
|
||||
if (room.host.id !== userId) return;
|
||||
|
||||
roomService.updateRoomVideo(roomId, data.video);
|
||||
roomService.updateRoomMetadata(roomId, data.metadata);
|
||||
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'video_update',
|
||||
video: data.video,
|
||||
metadata: data.metadata // ✅ CLAVE
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
|
||||
case 'sync':
|
||||
// Solo el host puede hacer sync
|
||||
if (room.host.id !== userId) return;
|
||||
|
||||
if (room.currentVideo) {
|
||||
room.currentVideo.currentTime = data.currentTime;
|
||||
room.currentVideo.isPlaying = data.isPlaying;
|
||||
}
|
||||
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'sync',
|
||||
currentTime: data.currentTime,
|
||||
isPlaying: data.isPlaying
|
||||
}, userId);
|
||||
break;
|
||||
|
||||
case 'request_users':
|
||||
const currentRoom = roomService.getRoom(roomId);
|
||||
if (currentRoom) {
|
||||
const client = clients.get(userId);
|
||||
if (client && client.socket && client.socket.readyState === 1) {
|
||||
client.socket.send(JSON.stringify({
|
||||
type: 'users_update', // Nuevo tipo de respuesta
|
||||
users: Array.from(currentRoom.users.values()).map(u => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
isHost: u.isHost,
|
||||
isGuest: u.isGuest
|
||||
}))
|
||||
}));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'play':
|
||||
// Solo el host puede controlar la reproducción
|
||||
if (room.host.id !== userId) {
|
||||
console.warn('Non-host attempted play:', userId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Broadcasting play event to room:', roomId);
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'play',
|
||||
currentTime: data.currentTime,
|
||||
timestamp: Date.now()
|
||||
}, userId); // IMPORTANTE: excludeUserId para no enviar al host
|
||||
break;
|
||||
|
||||
case 'pause':
|
||||
// Solo el host puede controlar la reproducción
|
||||
if (room.host.id !== userId) {
|
||||
console.warn('Non-host attempted pause:', userId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Broadcasting pause event to room:', roomId);
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'pause',
|
||||
currentTime: data.currentTime,
|
||||
timestamp: Date.now()
|
||||
}, userId); // IMPORTANTE: excludeUserId para no enviar al host
|
||||
break;
|
||||
|
||||
case 'seek':
|
||||
// Solo el host puede controlar la reproducción
|
||||
if (room.host.id !== userId) {
|
||||
console.warn('Non-host attempted seek:', userId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Broadcasting seek event to room:', roomId);
|
||||
broadcastToRoom(roomId, {
|
||||
type: 'seek',
|
||||
currentTime: data.currentTime,
|
||||
timestamp: Date.now()
|
||||
}, userId); // IMPORTANTE: excludeUserId para no enviar al host
|
||||
break;
|
||||
|
||||
case 'request_sync':
|
||||
// Cualquier usuario puede pedir sync
|
||||
const host = clients.get(room.host.id);
|
||||
if (host && host.socket && host.socket.readyState === 1) {
|
||||
host.socket.send(JSON.stringify({
|
||||
type: 'sync_requested',
|
||||
requestedBy: userId
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'request_state':
|
||||
// Enviar estado actual de la sala al usuario que lo solicita
|
||||
const client = clients.get(userId);
|
||||
if (client && client.socket && client.socket.readyState === 1) {
|
||||
const updatedRoom = roomService.getRoom(roomId);
|
||||
if (updatedRoom) {
|
||||
client.socket.send(JSON.stringify({
|
||||
type: 'init',
|
||||
userId,
|
||||
username: client.username,
|
||||
isGuest: client.isGuest,
|
||||
room: {
|
||||
id: updatedRoom.id,
|
||||
name: updatedRoom.name,
|
||||
users: Array.from(updatedRoom.users.values()).map(u => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
isHost: u.isHost,
|
||||
isGuest: u.isGuest
|
||||
})),
|
||||
currentVideo: room.currentVideo,
|
||||
metadata: room.metadata
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('Unknown message type:', data.type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastToRoom(roomId: string, message: any, excludeUserId?: string) {
|
||||
const room = roomService.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
const messageStr = JSON.stringify(message);
|
||||
|
||||
console.log('Broadcasting to room:', roomId, 'message type:', message.type, 'excluding:', excludeUserId);
|
||||
|
||||
let sentCount = 0;
|
||||
room.users.forEach((user) => {
|
||||
if (user.id !== excludeUserId) {
|
||||
const client = clients.get(user.id);
|
||||
if (client && client.socket && client.socket.readyState === 1) {
|
||||
try {
|
||||
client.socket.send(messageStr);
|
||||
sentCount++;
|
||||
console.log('Sent to user:', user.id, user.username);
|
||||
} catch (err) {
|
||||
console.error('Error sending message to user:', user.id, err);
|
||||
}
|
||||
} else {
|
||||
console.warn('User socket not ready:', user.id, 'readyState:', client?.socket?.readyState);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Broadcast complete: sent to ${sentCount} users`);
|
||||
}
|
||||
@@ -16,6 +16,9 @@ const AnimePlayer = (function() {
|
||||
let _totalEpisodes = 0;
|
||||
let _manualExtensionId = null;
|
||||
let _activeSubtitleIndex = -1;
|
||||
let _roomMode = false;
|
||||
let _isRoomHost = false;
|
||||
let _roomWebSocket = null;
|
||||
|
||||
let hlsInstance = null;
|
||||
let subtitleRenderer = null;
|
||||
@@ -61,23 +64,30 @@ const AnimePlayer = (function() {
|
||||
subtitlesCanvas: null
|
||||
};
|
||||
|
||||
function init(animeId, initialSource, isLocal, animeData) {
|
||||
function init(animeId, initialSource, isLocal, animeData, roomMode = false) {
|
||||
_roomMode = roomMode;
|
||||
_animeId = animeId;
|
||||
_entrySource = initialSource || 'anilist';
|
||||
_isLocal = isLocal;
|
||||
_malId = animeData.idMal || null;
|
||||
_totalEpisodes = animeData.episodes || 1000;
|
||||
|
||||
if (animeData.title) {
|
||||
_animeTitle = animeData.title.romaji || animeData.title.english || animeData.title.native || animeData.title || "Anime";
|
||||
_animeTitle = animeData.title.romaji || animeData.title.english || "Anime";
|
||||
}
|
||||
|
||||
_skipIntervals = [];
|
||||
_localEntryId = null;
|
||||
|
||||
initElements();
|
||||
setupEventListeners();
|
||||
loadExtensionsList();
|
||||
|
||||
// In Room Mode, we show the player immediately and hide extra controls
|
||||
if (_roomMode) {
|
||||
if(els.playerWrapper) {
|
||||
els.playerWrapper.style.display = 'block';
|
||||
els.playerWrapper.classList.add('room-mode');
|
||||
}
|
||||
// Hide extension list loading in room mode
|
||||
} else {
|
||||
loadExtensionsList();
|
||||
}
|
||||
}
|
||||
|
||||
function initElements() {
|
||||
@@ -134,8 +144,10 @@ const AnimePlayer = (function() {
|
||||
|
||||
function setupEventListeners() {
|
||||
// Close player
|
||||
const closeBtn = document.getElementById('close-player-btn');
|
||||
if(closeBtn) closeBtn.addEventListener('click', closePlayer);
|
||||
if(!_roomMode) {
|
||||
const closeBtn = document.getElementById('close-player-btn');
|
||||
if(closeBtn) closeBtn.addEventListener('click', closePlayer);
|
||||
}
|
||||
|
||||
// Episode navigation
|
||||
if(els.prevBtn) els.prevBtn.onclick = () => playEpisode(_currentEpisode - 1);
|
||||
@@ -183,18 +195,35 @@ const AnimePlayer = (function() {
|
||||
setupKeyboardShortcuts();
|
||||
}
|
||||
|
||||
function setupCustomControls() {
|
||||
// Play/Pause
|
||||
if(els.playPauseBtn) {
|
||||
els.playPauseBtn.onclick = togglePlayPause;
|
||||
}
|
||||
if(els.video) {
|
||||
// Remove old listeners to be safe (though usually new element)
|
||||
els.video.onclick = togglePlayPause;
|
||||
els.video.ondblclick = toggleFullscreen;
|
||||
function loadVideoFromRoom(videoData) {
|
||||
console.log('AnimePlayer.loadVideoFromRoom called with:', videoData);
|
||||
|
||||
if (!videoData || !videoData.url) {
|
||||
console.error('Invalid video data provided to loadVideoFromRoom');
|
||||
return;
|
||||
}
|
||||
|
||||
// Volume
|
||||
_currentSubtitles = videoData.subtitles || [];
|
||||
|
||||
if (els.loader) els.loader.style.display = 'none';
|
||||
|
||||
initVideoPlayer(videoData.url, videoData.type || 'm3u8', videoData.subtitles || []);
|
||||
}
|
||||
|
||||
function setupCustomControls() {
|
||||
// ELIMINADO: if (_roomMode && !_isRoomHost) return;
|
||||
// Ahora permitimos que el código fluya para habilitar volumen y ajustes a todos
|
||||
|
||||
// 1. Play/Pause (SOLO HOST)
|
||||
if(els.playPauseBtn) {
|
||||
els.playPauseBtn.onclick = togglePlayPause; // La validación de permiso se hará dentro de togglePlayPause
|
||||
}
|
||||
if(els.video) {
|
||||
els.video.onclick = togglePlayPause; // Click en video para pausar
|
||||
els.video.ondblclick = toggleFullscreen; // Doble click siempre permitido
|
||||
}
|
||||
|
||||
// 2. Volume (TODOS)
|
||||
if(els.volumeBtn) {
|
||||
els.volumeBtn.onclick = toggleMute;
|
||||
}
|
||||
@@ -204,7 +233,7 @@ const AnimePlayer = (function() {
|
||||
};
|
||||
}
|
||||
|
||||
// Settings
|
||||
// 3. Settings (TODOS - Aquí están los subtítulos y audio)
|
||||
if(els.settingsBtn) {
|
||||
els.settingsBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -219,7 +248,7 @@ const AnimePlayer = (function() {
|
||||
};
|
||||
}
|
||||
|
||||
// Close settings when clicking outside
|
||||
// Close settings when clicking outside (TODOS)
|
||||
document.onclick = (e) => {
|
||||
if (settingsPanelActive && els.settingsPanel &&
|
||||
!els.settingsPanel.contains(e.target) &&
|
||||
@@ -229,19 +258,19 @@ const AnimePlayer = (function() {
|
||||
}
|
||||
};
|
||||
|
||||
// Fullscreen
|
||||
// 4. Fullscreen (TODOS)
|
||||
if(els.fullscreenBtn) {
|
||||
els.fullscreenBtn.onclick = toggleFullscreen;
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
// 5. Progress bar (SOLO HOST para buscar, TODOS para ver)
|
||||
if(els.progressContainer) {
|
||||
// El listener se añade, pero seekToPosition bloqueará a los invitados
|
||||
els.progressContainer.onclick = seekToPosition;
|
||||
}
|
||||
|
||||
// Video events
|
||||
// 6. Video events (TODOS - Necesarios para actualizar la UI localmente)
|
||||
if(els.video) {
|
||||
// Remove previous listeners first if sticking to same element, but we replace element usually
|
||||
els.video.onplay = onPlay;
|
||||
els.video.onpause = onPause;
|
||||
els.video.ontimeupdate = onTimeUpdate;
|
||||
@@ -283,9 +312,27 @@ const AnimePlayer = (function() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!els.playerWrapper || els.playerWrapper.style.display === 'none') return;
|
||||
|
||||
// Ignore if typing in input
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
|
||||
// En room mode, solo el host puede usar shortcuts de control
|
||||
if (_roomMode && !_isRoomHost) {
|
||||
// Permitir fullscreen y volumen para todos
|
||||
if (e.key.toLowerCase() === 'f') {
|
||||
e.preventDefault();
|
||||
toggleFullscreen();
|
||||
} else if (e.key.toLowerCase() === 'm') {
|
||||
e.preventDefault();
|
||||
toggleMute();
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
adjustVolume(0.1);
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
adjustVolume(-0.1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch(e.key.toLowerCase()) {
|
||||
case ' ':
|
||||
case 'k':
|
||||
@@ -352,11 +399,23 @@ const AnimePlayer = (function() {
|
||||
|
||||
// Control functions
|
||||
function togglePlayPause() {
|
||||
if (_roomMode && !_isRoomHost) {
|
||||
console.log('Guests cannot control playback');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!els.video) return;
|
||||
|
||||
if (els.video.paused) {
|
||||
els.video.play().catch(() => {});
|
||||
if (_roomMode && _isRoomHost) {
|
||||
sendRoomEvent('play', { currentTime: els.video.currentTime });
|
||||
}
|
||||
} else {
|
||||
els.video.pause();
|
||||
if (_roomMode && _isRoomHost) {
|
||||
sendRoomEvent('pause', { currentTime: els.video.currentTime });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,9 +457,18 @@ const AnimePlayer = (function() {
|
||||
|
||||
function seekToPosition(e) {
|
||||
if (!els.video || !els.progressContainer) return;
|
||||
if (_roomMode && !_isRoomHost) return;
|
||||
|
||||
const rect = els.progressContainer.getBoundingClientRect();
|
||||
const pos = (e.clientX - rect.left) / rect.width;
|
||||
els.video.currentTime = pos * els.video.duration;
|
||||
const newTime = pos * els.video.duration;
|
||||
|
||||
els.video.currentTime = newTime;
|
||||
|
||||
// En room mode, enviar evento de seek
|
||||
if (_roomMode && _isRoomHost) {
|
||||
sendRoomEvent('seek', { currentTime: newTime });
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgressHandle(e) {
|
||||
@@ -410,14 +478,30 @@ const AnimePlayer = (function() {
|
||||
els.progressHandle.style.left = `${pos * 100}%`;
|
||||
}
|
||||
|
||||
|
||||
function seekRelative(seconds) {
|
||||
if (!els.video) return;
|
||||
els.video.currentTime = Math.max(0, Math.min(els.video.duration, els.video.currentTime + seconds));
|
||||
}
|
||||
if (_roomMode && !_isRoomHost) return;
|
||||
|
||||
const newTime = Math.max(0, Math.min(els.video.duration, els.video.currentTime + seconds));
|
||||
els.video.currentTime = newTime;
|
||||
|
||||
// En room mode, enviar evento de seek
|
||||
if (_roomMode && _isRoomHost) {
|
||||
sendRoomEvent('seek', { currentTime: newTime });
|
||||
}
|
||||
}
|
||||
function seekToPercent(percent) {
|
||||
if (!els.video) return;
|
||||
els.video.currentTime = els.video.duration * percent;
|
||||
if (_roomMode && !_isRoomHost) return;
|
||||
|
||||
const newTime = els.video.duration * percent;
|
||||
els.video.currentTime = newTime;
|
||||
|
||||
// En room mode, enviar evento de seek
|
||||
if (_roomMode && _isRoomHost) {
|
||||
sendRoomEvent('seek', { currentTime: newTime });
|
||||
}
|
||||
}
|
||||
|
||||
// Video event handlers
|
||||
@@ -476,7 +560,7 @@ const AnimePlayer = (function() {
|
||||
}
|
||||
|
||||
function onEnded() {
|
||||
if (_currentEpisode < _totalEpisodes) {
|
||||
if (!_roomMode && _currentEpisode < _totalEpisodes) {
|
||||
playEpisode(_currentEpisode + 1);
|
||||
}
|
||||
}
|
||||
@@ -501,6 +585,22 @@ const AnimePlayer = (function() {
|
||||
els.volumeBtn.innerHTML = icon;
|
||||
}
|
||||
|
||||
function sendRoomEvent(eventType, data = {}) {
|
||||
if (!_roomMode || !_isRoomHost || !_roomWebSocket) return;
|
||||
if (_roomWebSocket.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
console.log('Sending room event:', eventType, data);
|
||||
_roomWebSocket.send(JSON.stringify({
|
||||
type: eventType,
|
||||
...data
|
||||
}));
|
||||
}
|
||||
|
||||
function setWebSocket(ws) {
|
||||
console.log('Setting WebSocket reference in AnimePlayer');
|
||||
_roomWebSocket = ws;
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (!isFinite(seconds) || isNaN(seconds)) return '0:00';
|
||||
const h = Math.floor(seconds / 3600);
|
||||
@@ -563,7 +663,7 @@ const AnimePlayer = (function() {
|
||||
}
|
||||
|
||||
// 4. Playback Speed
|
||||
if (els.video) {
|
||||
if (els.video && (!_roomMode || _isRoomHost)) {
|
||||
const label = els.video.playbackRate === 1 ? 'Normal' : `${els.video.playbackRate}x`;
|
||||
html += createMenuItem('speed', 'Playback Speed', label, Icons.speed);
|
||||
}
|
||||
@@ -812,7 +912,7 @@ const AnimePlayer = (function() {
|
||||
}
|
||||
|
||||
// Update progress for AniList
|
||||
if (!_progressUpdated && els.video.duration) {
|
||||
if (!_roomMode && !_progressUpdated && els.video.duration) {
|
||||
const percentage = els.video.currentTime / els.video.duration;
|
||||
if (percentage >= 0.8) {
|
||||
updateProgress();
|
||||
@@ -1183,8 +1283,9 @@ const AnimePlayer = (function() {
|
||||
}
|
||||
|
||||
function initVideoPlayer(url, type, subtitles = []) {
|
||||
// 1. CLEANUP FIRST: Destroy subtitle renderer while elements still exist
|
||||
// This prevents "removeChild" errors because the DOM is still intact
|
||||
console.log('initVideoPlayer called:', { url, type, subtitles });
|
||||
|
||||
// 1. CLEANUP FIRST
|
||||
if (subtitleRenderer) {
|
||||
try {
|
||||
subtitleRenderer.dispose();
|
||||
@@ -1194,16 +1295,18 @@ const AnimePlayer = (function() {
|
||||
subtitleRenderer = null;
|
||||
}
|
||||
|
||||
// 2. Destroy HLS instance
|
||||
if (hlsInstance) {
|
||||
hlsInstance.destroy();
|
||||
hlsInstance = null;
|
||||
}
|
||||
|
||||
const container = document.querySelector('.video-frame');
|
||||
if (!container) return;
|
||||
if (!container) {
|
||||
console.error('Video frame container not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Remove OLD Elements
|
||||
// 2. Remove OLD Elements
|
||||
const oldVideo = container.querySelector('video');
|
||||
const oldCanvas = container.querySelector('#subtitles-canvas');
|
||||
|
||||
@@ -1216,65 +1319,65 @@ const AnimePlayer = (function() {
|
||||
oldCanvas.remove();
|
||||
}
|
||||
|
||||
// 4. Create NEW Elements
|
||||
// 3. Create NEW Elements - CANVAS FIRST, then VIDEO
|
||||
const newCanvas = document.createElement('canvas');
|
||||
newCanvas.id = 'subtitles-canvas';
|
||||
|
||||
const newVideo = document.createElement('video');
|
||||
newVideo.id = 'player';
|
||||
newVideo.crossOrigin = 'anonymous';
|
||||
newVideo.playsInline = true;
|
||||
|
||||
const newCanvas = document.createElement('canvas');
|
||||
newCanvas.id = 'subtitles-canvas';
|
||||
container.appendChild(newCanvas)
|
||||
container.appendChild(newCanvas);
|
||||
container.appendChild(newVideo);
|
||||
|
||||
els.video = newVideo;
|
||||
els.subtitlesCanvas = newCanvas;
|
||||
|
||||
console.log('Video and canvas elements created:', { video: els.video, canvas: els.subtitlesCanvas });
|
||||
|
||||
// Re-setup controls with new video element
|
||||
setupCustomControls();
|
||||
|
||||
// 5. Initialize Player (HLS or Native)
|
||||
// Hide loader
|
||||
if (els.loader) els.loader.style.display = 'none';
|
||||
|
||||
// 4. Initialize Player
|
||||
if (Hls.isSupported() && type === 'm3u8') {
|
||||
console.log('Initializing HLS player');
|
||||
|
||||
hlsInstance = new Hls({
|
||||
enableWorker: true,
|
||||
lowLatencyMode: false,
|
||||
backBufferLength: 90
|
||||
backBufferLength: 90,
|
||||
debug: false
|
||||
});
|
||||
|
||||
hlsInstance.on(Hls.Events.ERROR, (event, data) => {
|
||||
console.error('HLS Error:', data);
|
||||
if (data.fatal) {
|
||||
if (els.loader) {
|
||||
els.loader.style.display = 'flex';
|
||||
if (els.loaderText) els.loaderText.textContent = 'Stream error: ' + (data.details || 'Unknown');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
hlsInstance.attachMedia(els.video);
|
||||
|
||||
hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
console.log('HLS media attached, loading source:', url);
|
||||
hlsInstance.loadSource(url);
|
||||
});
|
||||
|
||||
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
console.log('HLS manifest parsed, attaching subtitles');
|
||||
attachSubtitles(subtitles);
|
||||
buildSettingsPanel();
|
||||
if (els.downloadBtn) els.downloadBtn.style.display = 'flex';
|
||||
els.video.play().catch(() => {});
|
||||
if (els.loader) els.loader.style.display = 'none';
|
||||
});
|
||||
|
||||
hlsInstance.on(Hls.Events.LEVEL_SWITCHED, () => buildSettingsPanel());
|
||||
hlsInstance.on(Hls.Events.AUDIO_TRACK_SWITCHED, () => buildSettingsPanel());
|
||||
if (els.downloadBtn && !_roomMode) els.downloadBtn.style.display = 'flex';
|
||||
|
||||
} else {
|
||||
els.video.src = url;
|
||||
attachSubtitles(subtitles);
|
||||
buildSettingsPanel();
|
||||
els.video.play().catch(() => {});
|
||||
if(els.loader) els.loader.style.display = 'none';
|
||||
if (els.downloadBtn) els.downloadBtn.style.display = 'flex';
|
||||
}
|
||||
|
||||
// 6. Init Subtitles with explicit delay
|
||||
// We use setTimeout instead of requestAnimationFrame to let the Layout Engine catch up
|
||||
if (type === 'm3u8') {
|
||||
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
attachSubtitles(subtitles);
|
||||
buildSettingsPanel();
|
||||
if (els.downloadBtn) els.downloadBtn.style.display = 'flex';
|
||||
|
||||
// IMPORTANTE: Esperar a loadedmetadata antes de init subtitles
|
||||
// --- FIX: Inicializar el renderizador de subtítulos para HLS ---
|
||||
if (els.video.readyState >= 1) {
|
||||
initSubtitleRenderer();
|
||||
} else {
|
||||
@@ -1282,23 +1385,34 @@ const AnimePlayer = (function() {
|
||||
initSubtitleRenderer();
|
||||
}, { once: true });
|
||||
}
|
||||
// -------------------------------------------------------------
|
||||
|
||||
els.video.play().catch(() => {});
|
||||
if (els.loader) els.loader.style.display = 'none';
|
||||
console.log('Attempting to play video');
|
||||
els.video.play().catch(err => {
|
||||
console.error('Play error:', err);
|
||||
});
|
||||
});
|
||||
|
||||
hlsInstance.on(Hls.Events.LEVEL_SWITCHED, () => buildSettingsPanel());
|
||||
hlsInstance.on(Hls.Events.AUDIO_TRACK_SWITCHED, () => buildSettingsPanel());
|
||||
|
||||
} else {
|
||||
console.log('Using native video player');
|
||||
els.video.src = url;
|
||||
attachSubtitles(subtitles);
|
||||
buildSettingsPanel();
|
||||
|
||||
// Para video directo, esperar metadata
|
||||
els.video.addEventListener('loadedmetadata', () => {
|
||||
console.log('Video metadata loaded');
|
||||
initSubtitleRenderer();
|
||||
}, { once: true });
|
||||
|
||||
els.video.play().catch(() => {});
|
||||
if(els.loader) els.loader.style.display = 'none';
|
||||
if (els.downloadBtn) els.downloadBtn.style.display = 'flex';
|
||||
console.log('Attempting to play video');
|
||||
els.video.play().catch(err => {
|
||||
console.error('Play error:', err);
|
||||
});
|
||||
|
||||
if (els.downloadBtn && !_roomMode) els.downloadBtn.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1762,12 +1876,19 @@ const AnimePlayer = (function() {
|
||||
|
||||
// RPC
|
||||
function sendRPC({ startTimestamp, endTimestamp, paused = false } = {}) {
|
||||
let stateText = `Episode ${_currentEpisode}`;
|
||||
let detailsText = _animeTitle;
|
||||
|
||||
if (_roomMode) {
|
||||
stateText = `Watch Party - Ep ${_currentEpisode}`;
|
||||
}
|
||||
|
||||
fetch("/api/rpc", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
details: _animeTitle,
|
||||
state: `Episode ${_currentEpisode}`,
|
||||
details: detailsText,
|
||||
state: stateText,
|
||||
mode: "watching",
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
@@ -1800,9 +1921,29 @@ const AnimePlayer = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function setRoomHost(isHost) {
|
||||
console.log('Setting player host status:', isHost);
|
||||
_isRoomHost = isHost;
|
||||
|
||||
// Re-ejecutar la configuración de controles con el nuevo permiso
|
||||
setupCustomControls();
|
||||
|
||||
// Forzar actualización visual si es necesario
|
||||
if (els.playerWrapper) {
|
||||
if (isHost) els.playerWrapper.classList.add('is-host');
|
||||
else els.playerWrapper.classList.remove('is-host');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
playEpisode,
|
||||
getCurrentEpisode: () => _currentEpisode
|
||||
getCurrentEpisode: () => _currentEpisode,
|
||||
loadVideoFromRoom,
|
||||
getVideoElement: () => els.video,
|
||||
setRoomHost,
|
||||
setWebSocket
|
||||
};
|
||||
})();
|
||||
})();
|
||||
|
||||
window.AnimePlayer = AnimePlayer;
|
||||
@@ -115,4 +115,16 @@ function setupDropdown() {
|
||||
})
|
||||
}
|
||||
|
||||
loadMeUI()
|
||||
loadMeUI()
|
||||
const createRoomModal = new CreateRoomModal();
|
||||
|
||||
const createBtn = document.getElementById('nav-create-party');
|
||||
if (createBtn) {
|
||||
createBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const dropdown = document.getElementById('nav-dropdown');
|
||||
if(dropdown) dropdown.classList.remove('active');
|
||||
|
||||
createRoomModal.open();
|
||||
});
|
||||
}
|
||||
128
desktop/src/scripts/room-modal.js
Normal file
128
desktop/src/scripts/room-modal.js
Normal file
@@ -0,0 +1,128 @@
|
||||
class CreateRoomModal {
|
||||
constructor() {
|
||||
this.modalId = 'cr-modal-overlay';
|
||||
this.isRendered = false;
|
||||
this.render(); // Crear el HTML en el DOM al instanciar
|
||||
}
|
||||
|
||||
render() {
|
||||
if (document.getElementById(this.modalId)) return;
|
||||
|
||||
const modalHtml = `
|
||||
<div class="cr-modal-overlay" id="${this.modalId}">
|
||||
<div class="cr-modal-content">
|
||||
<button class="cr-modal-close" id="cr-close">✕</button>
|
||||
<h2 class="cr-modal-title">Create Watch Party</h2>
|
||||
|
||||
<form id="cr-form">
|
||||
<div class="cr-form-group">
|
||||
<label>Room Name</label>
|
||||
<input type="text" class="cr-input" name="name" placeholder="My Awesome Room" required maxlength="50" />
|
||||
</div>
|
||||
|
||||
<div class="cr-form-group">
|
||||
<label>Password (Optional)</label>
|
||||
<input type="password" class="cr-input" name="password" placeholder="Leave empty for public" maxlength="50" />
|
||||
</div>
|
||||
|
||||
<div class="cr-actions">
|
||||
<button type="button" class="cr-btn-cancel" id="cr-cancel">Cancel</button>
|
||||
<button type="submit" class="cr-btn-confirm">Create Room</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
this.bindEvents();
|
||||
this.isRendered = true;
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
const modal = document.getElementById(this.modalId);
|
||||
const closeBtn = document.getElementById('cr-close');
|
||||
const cancelBtn = document.getElementById('cr-cancel');
|
||||
const form = document.getElementById('cr-form');
|
||||
|
||||
const close = () => this.close();
|
||||
|
||||
closeBtn.onclick = close;
|
||||
cancelBtn.onclick = close;
|
||||
|
||||
// Cerrar si clicamos fuera del contenido
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) close();
|
||||
};
|
||||
|
||||
form.onsubmit = (e) => this.handleSubmit(e);
|
||||
}
|
||||
|
||||
open() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
// Aquí puedes disparar tu modal de login o redirigir
|
||||
alert('You must be logged in to create a room');
|
||||
window.location.href = '/login'; // Opcional
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById(this.modalId);
|
||||
modal.classList.add('show');
|
||||
document.querySelector('#cr-form input[name="name"]').focus();
|
||||
}
|
||||
|
||||
close() {
|
||||
const modal = document.getElementById(this.modalId);
|
||||
modal.classList.remove('show');
|
||||
document.getElementById('cr-form').reset();
|
||||
}
|
||||
|
||||
async handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const btn = e.target.querySelector('button[type="submit"]');
|
||||
const originalText = btn.textContent;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Creating...';
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const name = formData.get('name').trim();
|
||||
const password = formData.get('password').trim();
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/rooms', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
password: password || undefined
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Failed to create room');
|
||||
|
||||
this.close();
|
||||
|
||||
// REDIRECCIÓN:
|
||||
// Si estamos en la página de rooms, recargamos o dejamos que el socket actualice.
|
||||
// Si estamos en otra página, vamos a la sala creada.
|
||||
// Asumo que tu ruta de sala es /room (o query params).
|
||||
// Ajusta esta línea según tu router:
|
||||
window.location.href = `/room?id=${data.room.id}`;
|
||||
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = originalText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.CreateRoomModal = CreateRoomModal;
|
||||
1196
desktop/src/scripts/room.js
Normal file
1196
desktop/src/scripts/room.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -130,6 +130,12 @@ async function viewsRoutes(fastify: FastifyInstance) {
|
||||
reply.type('text/html').send(html);
|
||||
});
|
||||
|
||||
fastify.get('/room', (req: FastifyRequest, reply: FastifyReply) => {
|
||||
const htmlPath = path.join(__dirname, '..', '..', 'views', 'room.html');
|
||||
const html = fs.readFileSync(htmlPath, 'utf-8');
|
||||
reply.type('text/html').send(html);
|
||||
});
|
||||
|
||||
fastify.setNotFoundHandler((req, reply) => {
|
||||
const htmlPath = path.join(__dirname, '..', '..', 'views', '404.html');
|
||||
const html = fs.readFileSync(htmlPath, 'utf-8');
|
||||
|
||||
Reference in New Issue
Block a user