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", "name": "syncfilm",
"version": "1.0.0", "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": { "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": [], "repository": {
"author": "", "type": "git",
"license": "ISC", "url": "git+https://github.com/arthur-pbty-labs/syncfilm.git"
"description": "", },
"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": { "dependencies": {
"dotenv": "^16.4.7", "express": "^5.1.0",
"express": "^4.21.2",
"socket.io": "^4.8.1" "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();
}
});
+239 -67
View File
@@ -1,67 +1,239 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="fr">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>SyncFilm</title>
<title>SyncFilm</title> <script src="/socket.io/socket.io.js"></script>
<link rel="stylesheet" href="video.css" /> <link rel="stylesheet" href="style.css">
<link rel="icon" href="favicon.ico" /> </head>
</head> <body>
<body> <div id="video-player">
<button id="madame-mode" class="madame-btn"> <input type="file" id="video-file" accept="video/*">
<span class="madame-text">Madame</span> <video id="video"></video>
<span class="heart"></span> <div class="video-controls">
</button> <button id="play-btn">Lecture</button>
<h1>SyncFilm</h1> <button id="pause-btn">Pause</button>
<input type="file" id="fileInput" accept="video/*" /> <button id="restart-btn">Recommencer</button>
<div id="video-player"> <button id="forward-btn">+10s</button>
<video id="video" src="" preload="metadata"></video> <button id="backward-btn">-10s</button>
<div id="loading" style="display: none">Chargement...</div> <input type="range" id="volume-bar" min="0" max="1" step="0.01" value="1">
<div id="controls"> <button id="mute-btn">Mute</button>
<button id="rewind"> </div>
<svg viewBox="0 0 24 24"> <div class="video-time-bar">
<path d="M12 12L24 0v24zM0 12L12 0v24z" /> <input type="range" id="seek-bar" min="0" value="0" step="0.01" disabled>
</svg> <span id="time-label">00:00 / 00:00</span>
</button> </div>
<button id="play-pause"> </div>
<svg id="play-icon" viewBox="0 0 24 24">
<path d="M0 0v24l24-12z" /> <div id="ws-status">Connecté au WebSocket</div>
</svg> <div id="users">
<svg id="pause-icon" viewBox="0 0 24 24"> <h2>Liste des personnes connectées</h2>
<path d="M0 0h6v24H0zM18 0h6v24h-6z" /> <ul></ul>
</svg> </div>
</button>
<button id="forward"> <script>
<svg viewBox="0 0 24 24"> const socket = io();
<path d="M12 12L0 24V0zM24 12L12 24V0z" /> let users = [];
</svg> let myFilename = null;
</button>
<span id="current-time">0:00</span> const video = document.getElementById('video');
<input id="progress-bar" type="range" value="0" max="100" /> const seekBar = document.getElementById('seek-bar');
<span id="duration">0:00</span> const timeLabel = document.getElementById('time-label');
<button id="mute"> const volumeBar = document.getElementById('volume-bar');
<svg id="sound-icon" viewBox="0 0 24 24"> const muteBtn = document.getElementById('mute-btn');
<path d="M0 8v8h4l5 5V3L4 8H0z" /> const wsStatus = document.getElementById('ws-status');
</svg> const videoPlayer = document.getElementById('video-player');
<svg id="mute-icon" viewBox="0 0 24 24">
<path // Gestion de la liste des utilisateurs
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" socket.on('users', function(userList) {
/> users = userList;
</svg> const ul = document.querySelector('#users ul');
</button> ul.innerHTML = '';
<input id="volume" type="range" value="100" max="100" /> userList.forEach(function(user) {
<button id="fullscreen"> const filename = user.filename ? user.filename : 'Aucun fichier sélectionné';
<svg viewBox="0 0 24 24"> const li = document.createElement('li');
<path li.textContent = `ID: ${user.id} — Fichier: ${filename}`;
d="M0 0h10v2H2v8H0V0zm14 0h10v10h-2V2h-8V0zM0 14h2v8h8v2H0v-10zm14 0h8v8h2v10h-10v-2h8v-8h-8v-2z" ul.appendChild(li);
/> if (user.id === socket.id) {
</svg> myFilename = user.filename || null;
</button> }
</div> });
</div> updateSeekBarState();
<script src="/socket.io/socket.io.js"></script> pauseVideo();
<script src="/video.js"></script> });
<script src="/madameMode.js"></script>
<script src="chooseVideo.js"></script> // Sélection du fichier vidéo
</body> document.getElementById('video-file').addEventListener('change', function(event) {
</html> 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();
}
});
+43 -64
View File
@@ -1,65 +1,44 @@
require("dotenv").config(); const express = require('express');
const express = require("express"); const http = require('http');
const http = require("http"); const { Server } = require('socket.io');
const { Server } = require("socket.io");
const path = require("path"); const app = express();
const server = http.createServer(app);
const app = express(); const io = new Server(server);
const server = http.createServer(app);
const io = new Server(server, { const port = process.env.PORT || 3000;
path: process.env.SOCKET_IO_PATH || "/socket.io", // Custom socket.io path
}); app.use(express.static('public'));
// Serve static files from the public directory app.get('/', (req, res) => {
app.use(express.static(path.join(__dirname, process.env.STATIC_FILES_PATH || "public"))); res.sendFile(__dirname + '/public/index.html');
});
let expectedResponses = 0; // Number of connected clients
let receivedResponses = 0; // Number of clients that acknowledged an action const users = {};
let emitData = {}; // Data to synchronize across clients
io.on('connection', (socket) => {
// Serve the main HTML file console.log('Un utilisateur est connecté');
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 socket.on('videoSelected', (filename) => {
io.on("connection", (socket) => { users[socket.id].filename = filename || null;
console.log(`User connected: ${socket.id}`); io.emit('users', Object.values(users));
});
// Listen for the user's name
socket.on("setName", (name) => { // Synchronisation des commandes vidéo
socket.data.name = name; // Store the user's name in the socket object socket.on('videoCommand', (cmd) => {
expectedResponses++; io.emit('videoCommand', cmd);
io.emit("notification", { message: `${name} connected`, color: "green" }); });
});
socket.on('disconnect', () => {
// Handle actions (e.g., play/pause, seek) console.log('Un utilisateur s\'est déconnecté');
socket.on("action", (data) => { delete users[socket.id];
io.emit("make", data); // Broadcast the action to all clients io.emit('users', Object.values(users));
emitData = data; // Save the action data });
receivedResponses = 0; // Reset the acknowledgment counter });
});
server.listen(port, () => {
// Handle acknowledgment from clients console.log(`Serveur en ligne sur http://localhost:${port}`);
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" });
});
});
// Start the server
server.listen(process.env.PORT || 3000, () => {
console.log(`Listening on *:${process.env.PORT || 3000}`);
}); });