Remove Madame Mode feature and associated styles, refactor video player server logic, and add MIT license and new styles for video player interface.

This commit is contained in:
Arthur
2025-09-14 19:55:44 +02:00
parent ce6722f824
commit 25e63efb75
13 changed files with 962 additions and 1485 deletions
-4
View File
@@ -1,4 +0,0 @@
PORT=3004
NODE_ENV=production
SOCKET_IO_PATH=/socket.io
STATIC_FILES_PATH=public
-20
View File
@@ -1,20 +0,0 @@
# Use the official Node.js LTS image
FROM node:18
# Set the working directory inside the container
WORKDIR /app
# Copy package.json and package-lock.json to the container
COPY package*.json ./
# Install dependencies
RUN npm install --production
# Copy the rest of the application code to the container
COPY . .
# Expose the port the app runs on
EXPOSE 3000
# Start the application
CMD ["npm", "start"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Arthur
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-18
View File
@@ -1,18 +0,0 @@
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3001:${PORT:-3000}" # Map the container port to the host port
restart: unless-stopped
environment:
- NODE_ENV=${NODE_ENV:-production}
- PORT=${PORT:-3000}
- SOCKET_IO_PATH=${SOCKET_IO_PATH:-/socket.io}
- STATIC_FILES_PATH=${STATIC_FILES_PATH:-public}
volumes:
- .:/app # Mount the current directory to the container
command: sh -c "npm install && npm start"
+420 -286
View File
File diff suppressed because it is too large Load Diff
+15 -9
View File
@@ -1,18 +1,24 @@
{
"name": "syncfilm",
"version": "1.0.0",
"main": "index.js",
"description": "SyncFilm est une application web qui permet à plusieurs utilisateurs de regarder des films ensemble en temps réel, avec une lecture parfaitement synchronisée.",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"repository": {
"type": "git",
"url": "git+https://github.com/arthur-pbty-labs/syncfilm.git"
},
"author": "arthur-pbty",
"license": "MIT",
"bugs": {
"url": "https://github.com/arthur-pbty-labs/syncfilm/issues"
},
"homepage": "https://github.com/arthur-pbty-labs/syncfilm#readme",
"dependencies": {
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^5.1.0",
"socket.io": "^4.8.1"
}
}
-10
View File
@@ -1,10 +0,0 @@
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const url = URL.createObjectURL(file);
video.src = url;
video.load();
}
});
+235 -63
View File
@@ -1,67 +1,239 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>SyncFilm</title>
<link rel="stylesheet" href="video.css" />
<link rel="icon" href="favicon.ico" />
</head>
<body>
<button id="madame-mode" class="madame-btn">
<span class="madame-text">Madame</span>
<span class="heart"></span>
</button>
<h1>SyncFilm</h1>
<input type="file" id="fileInput" accept="video/*" />
<div id="video-player">
<video id="video" src="" preload="metadata"></video>
<div id="loading" style="display: none">Chargement...</div>
<div id="controls">
<button id="rewind">
<svg viewBox="0 0 24 24">
<path d="M12 12L24 0v24zM0 12L12 0v24z" />
</svg>
</button>
<button id="play-pause">
<svg id="play-icon" viewBox="0 0 24 24">
<path d="M0 0v24l24-12z" />
</svg>
<svg id="pause-icon" viewBox="0 0 24 24">
<path d="M0 0h6v24H0zM18 0h6v24h-6z" />
</svg>
</button>
<button id="forward">
<svg viewBox="0 0 24 24">
<path d="M12 12L0 24V0zM24 12L12 24V0z" />
</svg>
</button>
<span id="current-time">0:00</span>
<input id="progress-bar" type="range" value="0" max="100" />
<span id="duration">0:00</span>
<button id="mute">
<svg id="sound-icon" viewBox="0 0 24 24">
<path d="M0 8v8h4l5 5V3L4 8H0z" />
</svg>
<svg id="mute-icon" viewBox="0 0 24 24">
<path
d="M0 8v8h4l5 5V3L4 8H0zM19 5l-1.5 1.5L16 8l-1.5-1.5L13 5l-1.5 1.5L10 8l-1.5-1.5L7 5l-1.5 1.5L4 8l-1.5-1.5L1 5l1.5-1.5L4 2l1.5 1.5L7 5l1.5-1.5L10 2l1.5 1.5L13 5l1.5-1.5L16 2l1.5 1.5L19 5z"
/>
</svg>
</button>
<input id="volume" type="range" value="100" max="100" />
<button id="fullscreen">
<svg viewBox="0 0 24 24">
<path
d="M0 0h10v2H2v8H0V0zm14 0h10v10h-2V2h-8V0zM0 14h2v8h8v2H0v-10zm14 0h8v8h2v10h-10v-2h8v-8h-8v-2z"
/>
</svg>
</button>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/video.js"></script>
<script src="/madameMode.js"></script>
<script src="chooseVideo.js"></script>
</body>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="video-player">
<input type="file" id="video-file" accept="video/*">
<video id="video"></video>
<div class="video-controls">
<button id="play-btn">Lecture</button>
<button id="pause-btn">Pause</button>
<button id="restart-btn">Recommencer</button>
<button id="forward-btn">+10s</button>
<button id="backward-btn">-10s</button>
<input type="range" id="volume-bar" min="0" max="1" step="0.01" value="1">
<button id="mute-btn">Mute</button>
</div>
<div class="video-time-bar">
<input type="range" id="seek-bar" min="0" value="0" step="0.01" disabled>
<span id="time-label">00:00 / 00:00</span>
</div>
</div>
<div id="ws-status">Connecté au WebSocket</div>
<div id="users">
<h2>Liste des personnes connectées</h2>
<ul></ul>
</div>
<script>
const socket = io();
let users = [];
let myFilename = null;
const video = document.getElementById('video');
const seekBar = document.getElementById('seek-bar');
const timeLabel = document.getElementById('time-label');
const volumeBar = document.getElementById('volume-bar');
const muteBtn = document.getElementById('mute-btn');
const wsStatus = document.getElementById('ws-status');
const videoPlayer = document.getElementById('video-player');
// Gestion de la liste des utilisateurs
socket.on('users', function(userList) {
users = userList;
const ul = document.querySelector('#users ul');
ul.innerHTML = '';
userList.forEach(function(user) {
const filename = user.filename ? user.filename : 'Aucun fichier sélectionné';
const li = document.createElement('li');
li.textContent = `ID: ${user.id} — Fichier: ${filename}`;
ul.appendChild(li);
if (user.id === socket.id) {
myFilename = user.filename || null;
}
});
updateSeekBarState();
pauseVideo();
});
// Sélection du fichier vidéo
document.getElementById('video-file').addEventListener('change', function(event) {
const file = event.target.files[0];
if (file) {
const url = URL.createObjectURL(file);
video.src = url;
video.load();
myFilename = file.name;
socket.emit('videoSelected', file.name);
} else {
myFilename = null;
socket.emit('videoSelected', null);
}
});
function allUsersHaveSameFile() {
if (!myFilename) return false;
return users.length > 0 && users.every(u => u.filename && u.filename === myFilename);
}
socket.on('connect', () => {
wsStatus.textContent = 'Connecté au WebSocket';
wsStatus.style.color = '#50fa7b';
if (video.src && video.src.startsWith('blob:') && myFilename) {
socket.emit('videoSelected', myFilename);
}
});
socket.on('disconnect', () => {
wsStatus.textContent = 'Déconnecté du WebSocket';
wsStatus.style.color = '#ff5555';
pauseVideo();
});
function pauseVideo() {
video.pause();
}
function formatTime(seconds) {
const min = Math.floor(seconds / 60);
const sec = Math.floor(seconds % 60);
return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
}
video.addEventListener('loadedmetadata', function() {
seekBar.max = video.duration;
seekBar.value = 0;
updateSeekBarState();
updateTimeLabel();
volumeBar.value = video.volume;
volumeBar.disabled = video.muted;
});
video.addEventListener('timeupdate', function() {
seekBar.value = video.currentTime;
updateTimeLabel();
});
seekBar.addEventListener('input', function() {
updateTimeLabel();
if (allUsersHaveSameFile()) {
socket.emit('videoCommand', { action: 'seek', time: parseFloat(seekBar.value) });
}
});
function updateTimeLabel() {
const current = formatTime(video.currentTime);
const total = formatTime(video.duration || 0);
timeLabel.textContent = `${current} / ${total}`;
}
function updateSeekBarState() {
seekBar.disabled = !allUsersHaveSameFile();
}
document.getElementById('play-btn').onclick = function() {
if (allUsersHaveSameFile()) {
socket.emit('videoCommand', { action: 'play', time: video.currentTime });
}
};
document.getElementById('pause-btn').onclick = function() {
if (allUsersHaveSameFile()) {
socket.emit('videoCommand', { action: 'pause', time: video.currentTime });
}
};
document.getElementById('restart-btn').onclick = function() {
if (allUsersHaveSameFile()) {
socket.emit('videoCommand', { action: 'restart' });
}
};
document.getElementById('forward-btn').onclick = function() {
if (allUsersHaveSameFile()) {
socket.emit('videoCommand', {
action: 'forward',
time: video.currentTime,
paused: video.paused
});
}
};
document.getElementById('backward-btn').onclick = function() {
if (allUsersHaveSameFile()) {
socket.emit('videoCommand', {
action: 'backward',
time: video.currentTime,
paused: video.paused
});
}
};
socket.on('videoCommand', function(cmd) {
if (!allUsersHaveSameFile()) return;
switch (cmd.action) {
case 'play':
video.currentTime = cmd.time;
video.play();
break;
case 'pause':
video.currentTime = cmd.time;
video.pause();
break;
case 'restart':
video.currentTime = 0;
video.play();
break;
case 'forward':
video.currentTime = cmd.time + 10;
if (cmd.paused) video.pause(); else video.play();
break;
case 'backward':
video.currentTime = Math.max(0, cmd.time - 10);
if (cmd.paused) video.pause(); else video.play();
break;
case 'seek':
video.currentTime = cmd.time;
break;
}
});
volumeBar.addEventListener('input', function() {
if (!video.muted) {
video.volume = parseFloat(volumeBar.value);
}
});
muteBtn.onclick = function() {
video.muted = !video.muted;
volumeBar.disabled = video.muted;
muteBtn.textContent = video.muted ? "Unmute" : "Mute";
};
video.addEventListener('volumechange', function() {
volumeBar.value = video.volume;
volumeBar.disabled = video.muted;
muteBtn.textContent = video.muted ? "Unmute" : "Mute";
});
// Gestion de l'affichage automatique des contrôles
let controlsTimeout;
function showControls() {
videoPlayer.classList.remove('hide-controls');
clearTimeout(controlsTimeout);
controlsTimeout = setTimeout(() => {
if (video.src) videoPlayer.classList.add('hide-controls');
}, 3000);
}
videoPlayer.addEventListener('mousemove', showControls);
document.addEventListener('mousemove', showControls);
videoPlayer.addEventListener('mouseleave', () => {
if (video.src) videoPlayer.classList.add('hide-controls');
});
video.addEventListener('loadeddata', showControls);
videoPlayer.addEventListener('focusin', showControls);
videoPlayer.addEventListener('click', showControls);
document.getElementById('video-file').addEventListener('change', showControls);
</script>
</body>
</html>
-259
View File
@@ -1,259 +0,0 @@
// Configuration du mode Madame
const config = {
maxHearts: 30,
heartColors: ["#ff69b4", "#ff1493", "#ff69b4"],
heartSizes: {
min: 15,
max: 30,
},
heartOpacity: {
min: 0.2,
max: 0.6,
},
animationDuration: {
min: 3,
max: 6,
},
explosionParticles: 8,
explosionDistance: 100,
};
// État du mode Madame
const state = {
isActive: localStorage.getItem("madameMode") === "true",
activeHearts: 0,
cursorHeart: null,
heartInterval: null,
};
// Création du cœur du curseur
function createCursorHeart() {
const heart = document.createElement("div");
heart.className = "cursor-heart";
heart.innerHTML = "❤";
heart.style.display = "none";
document.body.appendChild(heart);
return heart;
}
// Création d'un cœur avec des propriétés aléatoires
function createHeart(x, y) {
const heart = document.createElement("div");
heart.className = "floating-heart";
heart.innerHTML = "❤";
heart.style.position = "fixed";
heart.style.color =
config.heartColors[Math.floor(Math.random() * config.heartColors.length)];
heart.style.fontSize = `${
Math.random() * (config.heartSizes.max - config.heartSizes.min) +
config.heartSizes.min
}px`;
heart.style.opacity =
Math.random() * (config.heartOpacity.max - config.heartOpacity.min) +
config.heartOpacity.min;
heart.style.pointerEvents = "none";
if (x && y) {
heart.style.left = `${x}px`;
heart.style.top = `${y}px`;
} else {
heart.style.left = `${Math.random() * window.innerWidth}px`;
heart.style.top = `${Math.random() * window.innerHeight}px`;
}
const duration =
Math.random() *
(config.animationDuration.max - config.animationDuration.min) +
config.animationDuration.min;
const delay = Math.random() * 2;
heart.style.animation = `float ${duration}s infinite`;
heart.style.animationDelay = `${delay}s`;
return heart;
}
// Création des particules d'explosion
function createExplosionParticles(heart) {
for (let i = 0; i < config.explosionParticles; i++) {
const particle = document.createElement("div");
particle.innerHTML = "❤";
particle.style.position = "fixed";
particle.style.color =
config.heartColors[Math.floor(Math.random() * config.heartColors.length)];
particle.style.fontSize = "12px";
particle.style.opacity = "0.8";
particle.style.pointerEvents = "none";
particle.style.left = heart.style.left;
particle.style.top = heart.style.top;
const angle = (i / config.explosionParticles) * Math.PI * 2;
const x = Math.cos(angle) * config.explosionDistance;
const y = Math.sin(angle) * config.explosionDistance;
particle.style.animation = `explode 0.5s ease-out forwards`;
particle.style.setProperty("--x", `${x}px`);
particle.style.setProperty("--y", `${y}px`);
document.body.appendChild(particle);
setTimeout(() => particle.remove(), 500);
}
}
// Explosion d'un cœur
function explodeHeart(heart) {
createExplosionParticles(heart);
heart.remove();
}
// Création de cœurs flottants
function createFloatingHearts() {
const hearts = Array.from(document.querySelectorAll(".floating-heart"));
if (hearts.length >= config.maxHearts) {
// Calculer combien de cœurs sont en trop
const heartsToRemove = hearts.length - config.maxHearts;
// Mélanger le tableau des cœurs
for (let i = hearts.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[hearts[i], hearts[j]] = [hearts[j], hearts[i]];
}
// Supprimer les cœurs en trop
for (let i = 0; i < heartsToRemove; i++) {
explodeHeart(hearts[i]);
state.activeHearts--;
}
}
for (let i = 0; i < 5; i++) {
if (state.activeHearts >= config.maxHearts) break;
const heart = createHeart();
document.body.appendChild(heart);
state.activeHearts++;
heart.addEventListener("animationend", () => {
heart.remove();
state.activeHearts--;
if (state.isActive) {
createFloatingHearts();
}
});
}
}
// Création de cœurs au clic
function createClickHearts(e) {
if (!state.isActive) return;
// Cœur principal au point de clic
const mainHeart = createHeart(e.clientX, e.clientY);
document.body.appendChild(mainHeart);
state.activeHearts++;
// Cœurs supplémentaires dans un rayon
for (let i = 0; i < 5; i++) {
if (state.activeHearts >= config.maxHearts) break;
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * 100;
const x = e.clientX + Math.cos(angle) * radius;
const y = e.clientY + Math.sin(angle) * radius;
const heart = createHeart(x, y);
document.body.appendChild(heart);
state.activeHearts++;
}
// Gestion de la limite de cœurs
const hearts = Array.from(document.querySelectorAll(".floating-heart"));
if (hearts.length > config.maxHearts) {
const heartsToRemove = hearts.length - config.maxHearts;
// Mélanger le tableau des cœurs
for (let i = hearts.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[hearts[i], hearts[j]] = [hearts[j], hearts[i]];
}
// Supprimer les cœurs en trop
for (let i = 0; i < heartsToRemove; i++) {
explodeHeart(hearts[i]);
state.activeHearts--;
}
}
}
// Mise à jour de la position du cœur du curseur
function updateCursorHeart(e) {
if (state.isActive) {
state.cursorHeart.style.display = "block";
state.cursorHeart.style.left = `${e.clientX}px`;
state.cursorHeart.style.top = `${e.clientY}px`;
} else {
state.cursorHeart.style.display = "none";
}
}
// Activation/désactivation du mode Madame
function toggleMadameMode() {
state.isActive = !state.isActive;
document.body.classList.toggle("madame-mode", state.isActive);
localStorage.setItem("madameMode", state.isActive);
state.cursorHeart.style.display = state.isActive ? "block" : "none";
if (state.isActive) {
state.heartInterval = setInterval(createFloatingHearts, 2000);
createFloatingHearts();
} else {
clearInterval(state.heartInterval);
// Supprimer tous les cœurs existants avec effet d'explosion
const hearts = document.querySelectorAll(".floating-heart");
hearts.forEach(explodeHeart);
state.activeHearts = 0;
}
}
// Initialisation du mode Madame
function initMadameMode() {
state.cursorHeart = createCursorHeart();
if (state.isActive) {
document.body.classList.add("madame-mode");
state.cursorHeart.style.display = "block";
createFloatingHearts();
}
// Événements
document.addEventListener("mousemove", updateCursorHeart);
document.addEventListener("click", createClickHearts);
document
.getElementById("madame-mode")
.addEventListener("click", toggleMadameMode);
}
// Gestion du mode plein écran
function handleFullscreenChange() {
const isFullscreen =
document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement;
if (isFullscreen) {
document.getElementById("video-player").appendChild(state.cursorHeart);
} else {
document.body.appendChild(state.cursorHeart);
}
}
// Initialisation
document.addEventListener("DOMContentLoaded", initMadameMode);
// Événements de plein écran
document.addEventListener("fullscreenchange", handleFullscreenChange);
document.addEventListener("mozfullscreenchange", handleFullscreenChange);
document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
document.addEventListener("MSFullscreenChange", handleFullscreenChange);
+224
View File
@@ -0,0 +1,224 @@
html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
box-sizing: border-box;
background: #181a20;
color: #f3f3f3;
font-family: 'Segoe UI', Arial, sans-serif;
}
#video-player {
position: relative;
height: 100vh;
background: #23262f;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
#video-file {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 101;
background: #23262f;
color: #f3f3f3;
border: 1px solid #444;
border-radius: 5px;
padding: 7px 12px;
font-size: 1em;
transition: opacity 0.3s;
}
#video {
width: 100vw;
height: 100%;
object-fit: contain;
background: #111;
z-index: 1;
display: block;
}
/* Conteneur des contrôles vidéo */
.video-controls {
position: absolute;
left: 0;
right: 0;
bottom: 80px;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
z-index: 10;
}
.video-controls button {
background: #ffb86c;
color: #23262f;
border: none;
border-radius: 4px;
padding: 10px 0;
font-size: 1.1em;
cursor: pointer;
min-width: 70px;
max-width: 120px;
flex: 1 1 0;
box-shadow: 0 2px 8px #0002;
margin: 0 2px;
transition: background 0.2s;
}
.video-controls button:hover {
background: #ffa94d;
}
#mute-btn {
background: #6272a4;
color: #fff;
}
#mute-btn:hover {
background: #48507a;
}
.video-controls input[type="range"] {
accent-color: #ffb86c;
height: 4px;
max-width: 90px;
flex: 1 1 0;
margin: 0 6px;
}
#volume-bar {
width: 90px;
max-width: 90px;
vertical-align: middle;
}
.video-time-bar {
position: absolute;
left: 0;
right: 0;
bottom: 30px;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
z-index: 15;
pointer-events: none;
transition: opacity 0.3s;
}
#seek-bar {
width: 60vw;
max-width: 700px;
margin: 0;
pointer-events: auto;
accent-color: #ffb86c;
}
#time-label {
margin-left: 16px;
font-size: 1.15em;
color: #8be9fd;
background: rgba(35,38,47,0.85);
padding: 3px 14px;
border-radius: 4px;
pointer-events: auto;
transition: opacity 0.3s;
}
#ws-status {
background: #23262f;
color: #50fa7b;
font-weight: bold;
text-align: center;
padding: 10px 0;
font-size: 1.1em;
z-index: 100;
}
#users {
background: #23262f;
border-radius: 0;
box-shadow: none;
margin: 0;
padding: 18px 24px;
position: relative;
z-index: 50;
}
#users h2 {
color: #50fa7b;
margin-bottom: 12px;
font-size: 1.15em;
}
#users ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 18px;
justify-content: center;
}
#users li {
padding: 7px 0;
border-bottom: none;
font-size: 1em;
color: #f3f3f3;
background: #181a20;
border-radius: 6px;
padding: 7px 18px;
}
@media (max-width: 900px) {
#video-player {
height: 40vh;
}
.video-controls {
bottom: 60px;
}
.video-time-bar {
bottom: 15px;
}
#seek-bar {
width: 50vw;
max-width: 100vw;
}
#time-label {
font-size: 1em;
padding: 2px 8px;
margin-left: 8px;
}
#users ul {
gap: 8px;
}
}
/* Contrôles vidéo visibles par défaut */
.video-controls,
.video-time-bar,
#video-file {
opacity: 1;
pointer-events: auto;
transition: opacity 0.3s;
}
/* Masquage automatique des contrôles */
.hide-controls .video-controls,
.hide-controls .video-time-bar,
.hide-controls #video-file {
opacity: 0 !important;
pointer-events: none !important;
}
-434
View File
@@ -1,434 +0,0 @@
body {
margin: 0;
padding: 20px;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: #1a1a1a;
color: #ffffff;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}
h1 {
font-size: clamp(1.5rem, 5vw, 2.5rem);
margin-bottom: clamp(1rem, 3vw, 2rem);
color: #00ff88;
text-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
text-align: center;
}
#fileInput {
margin-bottom: clamp(1rem, 3vw, 2rem);
padding: clamp(0.5rem, 2vw, 1rem);
background: #2a2a2a;
border: 2px dashed #00ff88;
border-radius: 8px;
color: #ffffff;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
max-width: 400px;
}
#fileInput:hover {
background: #333333;
border-color: #00cc6a;
}
#video-player {
width: 100%;
max-width: 1200px;
margin: auto;
background: #000;
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
aspect-ratio: 16/9;
}
#video {
width: 100%;
height: 100%;
background: #000;
border-radius: 12px 12px 0 0;
object-fit: contain;
}
#controls {
width: 100%;
opacity: 0;
visibility: hidden;
background: rgba(0, 0, 0, 0.8);
position: absolute;
bottom: 0;
padding: clamp(10px, 2vw, 15px);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: clamp(5px, 1vw, 10px);
flex-wrap: wrap;
}
#controls button {
width: clamp(36px, 8vw, 44px);
height: clamp(36px, 8vw, 44px);
padding: 0;
border: none;
background: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: relative;
flex: 0 0 auto;
}
#controls button svg {
width: clamp(20px, 4vw, 24px);
height: clamp(20px, 4vw, 24px);
fill: #ffffff;
transition: fill 0.3s ease;
}
#controls button:hover svg {
fill: #00ff88;
}
#play-pause svg,
#mute svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: opacity 0.3s ease;
}
#play-icon,
#sound-icon {
opacity: 1;
}
#pause-icon,
#mute-icon {
opacity: 0;
}
#current-time,
#duration {
font-family: monospace;
font-size: clamp(0.8rem, 2vw, 0.9rem);
color: #ffffff;
opacity: 0.8;
white-space: nowrap;
flex: 0 0 auto;
}
#progress-bar {
flex: 1;
min-width: 100px;
height: 6px;
-webkit-appearance: none;
appearance: none; /* Added for compatibility */
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
outline: none;
}
#volume {
width: clamp(60px, 15vw, 100px);
height: 6px;
-webkit-appearance: none;
appearance: none; /* Added for compatibility */
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
outline: none;
flex: 0 0 auto;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: clamp(12px, 2vw, 16px);
height: clamp(12px, 2vw, 16px);
background: #00ff88;
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
background: #00cc6a;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
padding: clamp(0.5rem, 2vw, 1rem) clamp(1rem, 3vw, 2rem);
border-radius: 8px;
color: #00ff88;
font-weight: bold;
animation: pulse 1.5s infinite;
font-size: clamp(0.9rem, 2vw, 1rem);
}
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
#video-player.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
background: #000;
border-radius: 0;
margin: 0;
max-width: none;
}
#video-player.fullscreen #video {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 0;
}
#video-player.fullscreen #controls {
background: rgba(0, 0, 0, 0.9);
padding: 20px;
}
/* Styles pour le mode plein écran natif */
:fullscreen #video-player {
width: 100vw;
height: 100vh;
background: #000;
}
:-webkit-full-screen #video-player {
width: 100vw;
height: 100vh;
background: #000;
}
:-moz-full-screen #video-player {
width: 100vw;
height: 100vh;
background: #000;
}
:-ms-fullscreen #video-player {
width: 100vw;
height: 100vh;
background: #000;
}
#video-player:hover #controls {
opacity: 1;
visibility: visible;
}
@media (max-width: 768px) {
body {
padding: 10px;
}
#controls {
padding: 8px;
}
#progress-bar {
min-width: 80px;
}
#volume {
width: 60px;
}
}
@media (max-width: 480px) {
#controls {
gap: 5px;
}
#progress-bar {
min-width: 60px;
}
#volume {
width: 40px;
}
}
/* Styles pour le bouton Madame */
.madame-btn {
position: fixed;
top: 20px;
right: 20px;
padding: 10px 20px;
background: #ff69b4;
border: none;
border-radius: 25px;
color: white;
font-size: 1.2rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.3s ease;
z-index: 1000;
box-shadow: 0 0 15px rgba(255, 105, 180, 0.3);
}
.madame-btn:hover {
transform: scale(1.1);
background: #ff1493;
}
.heart {
font-size: 1.4rem;
animation: pulse 1.5s infinite;
}
/* Thème Madame */
body.madame-mode {
background: #fff0f5;
cursor: none;
}
body.madame-mode * {
cursor: none !important;
}
body.madame-mode h1 {
color: #ff69b4;
text-shadow: 0 0 10px rgba(255, 105, 180, 0.3);
}
body.madame-mode #fileInput {
border-color: #ff69b4;
background: #fff0f5;
}
body.madame-mode #fileInput:hover {
border-color: #ff1493;
background: #ffe4e1;
}
body.madame-mode #controls button svg {
fill: #ff69b4;
}
body.madame-mode #controls button:hover svg {
fill: #ff1493;
}
body.madame-mode input[type="range"] {
background: rgba(255, 105, 180, 0.2);
}
body.madame-mode input[type="range"]::-webkit-slider-thumb {
background: #ff69b4;
}
body.madame-mode input[type="range"]::-webkit-slider-thumb:hover {
background: #ff1493;
}
body.madame-mode #loading {
color: #ff69b4;
}
/* Animation des cœurs */
@keyframes float {
0% {
transform: translate(0, 0) rotate(0deg);
}
20% {
transform: translate(150px, -80px) rotate(72deg);
}
40% {
transform: translate(80px, -150px) rotate(144deg);
}
60% {
transform: translate(-80px, -120px) rotate(216deg);
}
80% {
transform: translate(-150px, -40px) rotate(288deg);
}
100% {
transform: translate(0, 0) rotate(360deg);
}
}
.madame-mode::before,
.madame-mode::after {
content: '❤';
position: fixed;
font-size: 20px;
color: #ff69b4;
opacity: 0.6;
pointer-events: none;
animation: float 6s infinite;
}
.madame-mode::before {
top: 10%;
left: 10%;
}
.madame-mode::after {
top: 80%;
right: 10%;
animation-delay: -3s;
}
/* Cœur qui suit la souris */
.cursor-heart {
position: fixed;
pointer-events: none;
font-size: 24px;
color: #ff0000;
z-index: 999999;
transform: translate(-50%, -50%);
text-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
will-change: transform;
backface-visibility: hidden;
transform-style: preserve-3d;
mix-blend-mode: difference;
}
#video-player.fullscreen .cursor-heart {
position: fixed;
z-index: 999999;
}
@keyframes explode {
0% {
transform: translate(0, 0) scale(1) rotate(0deg);
opacity: 0.8;
}
50% {
transform: translate(var(--x), var(--y)) scale(1.2) rotate(180deg);
opacity: 0.4;
}
100% {
transform: translate(calc(var(--x) * 1.5), calc(var(--y) * 1.5)) scale(0.3) rotate(360deg);
opacity: 0;
}
}
-314
View File
@@ -1,314 +0,0 @@
// Select DOM elements
const video = document.getElementById("video");
const playPauseBtn = document.getElementById("play-pause");
const progressBar = document.getElementById("progress-bar");
const currentTimeElem = document.getElementById("current-time");
const durationElem = document.getElementById("duration");
const muteBtn = document.getElementById("mute");
const volumeSlider = document.getElementById("volume");
const loadingElem = document.getElementById("loading");
const fullscreenBtn = document.getElementById("fullscreen");
const rewindBtn = document.getElementById("rewind");
const forwardBtn = document.getElementById("forward");
const playIcon = document.getElementById("play-icon");
const pauseIcon = document.getElementById("pause-icon");
const soundIcon = document.getElementById("sound-icon");
const muteIcon = document.getElementById("mute-icon");
const videoPlayer = document.getElementById("video-player");
const socket = io(); // Initialize socket.io connection
// Prompt for the user's name if not already stored
let userName = localStorage.getItem("userName");
if (!userName) {
userName = prompt("Enter your name:");
if (userName) {
localStorage.setItem("userName", userName);
socket.emit("setName", userName); // Send the name to the server
}
} else {
socket.emit("setName", userName); // Send the stored name to the server
}
// Handle synchronization actions from the server
socket.on("make", (data) => {
controls.style.display = "none";
loadingElem.style.display = "block"; // Show loading for all users
video.pause();
video.currentTime = data.time;
socket.emit("ok", data); // Acknowledge the action
});
// Handle synchronization completion
socket.on("allOk", (data) => {
loadingElem.style.display = "none"; // Hide loading for all users
controls.style.display = "flex";
if (data.action === "play") {
video.play(); // Resume playback
playIcon.style.opacity = "0";
pauseIcon.style.opacity = "1";
} else {
video.pause(); // Pause the video
playIcon.style.opacity = "1";
pauseIcon.style.opacity = "0";
}
});
// Display notifications (e.g., user connected/disconnected)
function showNotification(message, color) {
const notificationContainer = document.getElementById("notification-container");
if (!notificationContainer) {
const container = document.createElement("div");
container.id = "notification-container";
container.style.position = "fixed";
container.style.top = "20px";
container.style.right = "20px";
container.style.zIndex = "1000";
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.gap = "10px";
document.body.appendChild(container);
}
const notification = document.createElement("div");
notification.textContent = message;
notification.style.backgroundColor = color;
notification.style.color = "white";
notification.style.padding = "10px 20px";
notification.style.borderRadius = "5px";
notification.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.2)";
notification.style.animation = "fadeOut 3s forwards";
document.getElementById("notification-container").appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// Add fade-out animation for notifications
const style = document.createElement("style");
style.textContent = `
@keyframes fadeOut {
0% { opacity: 1; }
90% { opacity: 1; }
100% { opacity: 0; transform: translateY(-10px); }
}
`;
document.head.appendChild(style);
// Listen for notifications from the server
socket.on("notification", (data) => {
showNotification(data.message, data.color);
});
// Format time in minutes:seconds
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
seconds = Math.floor(seconds % 60);
return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
}
// Get the current video state (play/pause and time)
function getVideoData() {
return {
action: video.paused ? "pause" : "play",
time: video.currentTime,
};
}
// Rewind the video by 10 seconds
rewindBtn.addEventListener("click", () => {
video.currentTime = Math.max(0, video.currentTime - 10);
socket.emit("action", getVideoData());
});
// Fast forward the video by 10 seconds
forwardBtn.addEventListener("click", () => {
video.currentTime = Math.min(video.duration, video.currentTime + 10);
socket.emit("action", getVideoData());
});
// Toggle play/pause
playPauseBtn.addEventListener("click", () => {
if (video.paused) {
video.play();
socket.emit("action", getVideoData());
playIcon.style.opacity = "0";
pauseIcon.style.opacity = "1";
} else {
video.pause();
socket.emit("action", getVideoData());
playIcon.style.opacity = "1";
pauseIcon.style.opacity = "0";
}
});
// Toggle play/pause by clicking on the video
video.addEventListener("click", () => {
playPauseBtn.click();
});
// Add keyboard shortcut for play/pause
document.addEventListener("keydown", (event) => {
if (event.code === "Space") {
event.preventDefault(); // Prevent scrolling
playPauseBtn.click();
}
});
// Update progress bar and current time display
let isDragging = false;
video.addEventListener("timeupdate", () => {
if (!isDragging) {
progressBar.value = (video.currentTime / video.duration) * 100;
currentTimeElem.textContent = formatTime(video.currentTime);
}
});
// Update video time while dragging the progress bar
progressBar.addEventListener("input", () => {
isDragging = true;
currentTimeElem.textContent = formatTime(
(progressBar.value / 100) * video.duration
);
});
// Seek to a specific time when progress bar is released
progressBar.addEventListener("change", () => {
video.currentTime = (progressBar.value / 100) * video.duration;
isDragging = false;
socket.emit("action", getVideoData());
});
// Initialize video duration display
video.addEventListener("loadedmetadata", () => {
durationElem.textContent = formatTime(video.duration);
progressBar.value = 0;
currentTimeElem.textContent = "0:00";
});
// Reset progress bar when loading a new video
video.addEventListener("loadstart", () => {
progressBar.value = 0;
currentTimeElem.textContent = "0:00";
});
// Restore saved volume settings
const savedVolume = localStorage.getItem("videoVolume");
if (savedVolume !== null) {
video.volume = savedVolume;
volumeSlider.value = savedVolume * 100;
}
// Adjust volume and save the setting
volumeSlider.addEventListener("input", () => {
const newVolume = volumeSlider.value / 100;
video.volume = newVolume;
localStorage.setItem("videoVolume", newVolume);
});
// Toggle mute/unmute
muteBtn.addEventListener("click", () => {
video.muted = !video.muted;
if (video.muted) {
soundIcon.style.display = "none";
muteIcon.style.display = "block";
localStorage.setItem("videoMuted", "true");
} else {
soundIcon.style.display = "block";
muteIcon.style.display = "none";
localStorage.setItem("videoMuted", "false");
}
});
// Restore mute state on page load
const savedMuted = localStorage.getItem("videoMuted");
if (savedMuted === "true") {
video.muted = true;
soundIcon.style.display = "none";
muteIcon.style.display = "block";
}
// Show/hide controls on hover
let hideControlsTimeout;
function showControls() {
controls.style.opacity = "1";
controls.style.visibility = "visible";
if (hideControlsTimeout) {
clearTimeout(hideControlsTimeout);
}
hideControlsTimeout = setTimeout(hideControls, 3000);
}
function hideControls() {
controls.style.opacity = "0";
controls.style.visibility = "hidden";
}
videoPlayer.addEventListener("mousemove", showControls);
videoPlayer.addEventListener("mouseleave", hideControls);
// Handle fullscreen mode
let isFullscreen = false;
function enterFullscreen(element) {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
}
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
function handleFullscreenChange() {
isFullscreen =
document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement;
if (isFullscreen) {
videoPlayer.classList.add("fullscreen");
} else {
videoPlayer.classList.remove("fullscreen");
}
}
// Listen for fullscreen changes
document.addEventListener("fullscreenchange", handleFullscreenChange);
document.addEventListener("mozfullscreenchange", handleFullscreenChange);
document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
document.addEventListener("MSFullscreenChange", handleFullscreenChange);
// Exit fullscreen on Escape key
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && isFullscreen) {
exitFullscreen();
}
});
// Toggle fullscreen mode
fullscreenBtn.addEventListener("click", () => {
if (!isFullscreen) {
enterFullscreen(videoPlayer);
} else {
exitFullscreen();
}
});
+28 -49
View File
@@ -1,65 +1,44 @@
require("dotenv").config();
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const path = require("path");
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
path: process.env.SOCKET_IO_PATH || "/socket.io", // Custom socket.io path
const io = new Server(server);
const port = process.env.PORT || 3000;
app.use(express.static('public'));
app.get('/', (req, res) => {
res.sendFile(__dirname + '/public/index.html');
});
// Serve static files from the public directory
app.use(express.static(path.join(__dirname, process.env.STATIC_FILES_PATH || "public")));
const users = {};
let expectedResponses = 0; // Number of connected clients
let receivedResponses = 0; // Number of clients that acknowledged an action
let emitData = {}; // Data to synchronize across clients
io.on('connection', (socket) => {
console.log('Un utilisateur est connecté');
// Serve the main HTML file
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
});
users[socket.id] = { id: socket.id, filename: null };
io.emit('users', Object.values(users));
// Handle socket.io connections
io.on("connection", (socket) => {
console.log(`User connected: ${socket.id}`);
// Listen for the user's name
socket.on("setName", (name) => {
socket.data.name = name; // Store the user's name in the socket object
expectedResponses++;
io.emit("notification", { message: `${name} connected`, color: "green" });
socket.on('videoSelected', (filename) => {
users[socket.id].filename = filename || null;
io.emit('users', Object.values(users));
});
// Handle actions (e.g., play/pause, seek)
socket.on("action", (data) => {
io.emit("make", data); // Broadcast the action to all clients
emitData = data; // Save the action data
receivedResponses = 0; // Reset the acknowledgment counter
// Synchronisation des commandes vidéo
socket.on('videoCommand', (cmd) => {
io.emit('videoCommand', cmd);
});
// Handle acknowledgment from clients
socket.on("ok", (receivedData) => {
if (JSON.stringify(receivedData) === JSON.stringify(emitData)) {
receivedResponses++;
if (receivedResponses === expectedResponses) {
io.emit("allOk", emitData); // Notify all clients that everyone is synchronized
}
}
});
// Handle disconnections
socket.on("disconnect", () => {
const name = socket.data.name || "Unknown User";
console.log(`User disconnected: ${name}`);
expectedResponses--;
io.emit("notification", { message: `${name} disconnected`, color: "red" });
socket.on('disconnect', () => {
console.log('Un utilisateur s\'est déconnecté');
delete users[socket.id];
io.emit('users', Object.values(users));
});
});
// Start the server
server.listen(process.env.PORT || 3000, () => {
console.log(`Listening on *:${process.env.PORT || 3000}`);
server.listen(port, () => {
console.log(`Serveur en ligne sur http://localhost:${port}`);
});