Spaces:
Running
Running
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>YouTube Lounge Viewer</title> | |
| <!-- Importation de la police Inter --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet"> | |
| <!-- Importation de FontAwesome pour les icônes --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| /* --- VARIABLES CSS & THEME --- */ | |
| :root { | |
| --bg-body: #0f0f0f; | |
| --bg-sidebar: #1e1e1e; | |
| --bg-card: #2a2a2a; | |
| --bg-input: #333333; | |
| --text-primary: #ffffff; | |
| --text-secondary: #aaaaaa; | |
| --accent-color: #ff0000; /* YouTube Red */ | |
| --accent-hover: #cc0000; | |
| --border-color: #3f3f3f; | |
| --shadow: 0 4px 15px rgba(0,0,0,0.5); | |
| --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); | |
| --header-height: 70px; | |
| } | |
| /* --- RESET & BASE --- */ | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| outline: none; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-body); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; /* Empêche le scroll global, on scroll dans les zones */ | |
| } | |
| a { text-decoration: none; color: inherit; } | |
| ul { list-style: none; } | |
| button { cursor: pointer; border: none; font-family: inherit; } | |
| /* --- HEADER --- */ | |
| header { | |
| height: var(--header-height); | |
| background-color: rgba(30, 30, 30, 0.95); | |
| backdrop-filter: blur(10px); | |
| border-bottom: 1px solid var(--border-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 2rem; | |
| z-index: 100; | |
| } | |
| .logo { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .logo i { color: var(--accent-color); font-size: 1.8rem; } | |
| .header-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 1.5rem; | |
| } | |
| .anycoder-link { | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| transition: var(--transition); | |
| background: rgba(255, 255, 255, 0.05); | |
| padding: 5px 10px; | |
| border-radius: 4px; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--text-primary); | |
| background: rgba(255, 255, 255, 0.1); | |
| } | |
| /* --- MAIN LAYOUT --- */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 1fr 350px; /* Player area | Sidebar */ | |
| height: calc(100vh - var(--header-height)); | |
| overflow: hidden; | |
| } | |
| /* --- PLAYER SECTION --- */ | |
| .player-container { | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 2rem; | |
| background-color: #000; | |
| position: relative; | |
| } | |
| .video-wrapper { | |
| width: 100%; | |
| max-width: 1200px; | |
| aspect-ratio: 16 / 9; | |
| background: #111; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| box-shadow: var(--shadow); | |
| position: relative; | |
| } | |
| iframe { | |
| width: 100%; | |
| height: 100%; | |
| border: none; | |
| } | |
| .video-info { | |
| width: 100%; | |
| max-width: 1200px; | |
| margin-top: 1.5rem; | |
| } | |
| .video-title { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| margin-bottom: 0.5rem; | |
| line-height: 1.3; | |
| } | |
| .video-meta { | |
| color: var(--text-secondary); | |
| font-size: 0.9rem; | |
| display: flex; | |
| gap: 1rem; | |
| } | |
| /* --- SIDEBAR / PLAYLIST --- */ | |
| .sidebar { | |
| background-color: var(--bg-sidebar); | |
| border-left: 1px solid var(--border-color); | |
| display: flex; | |
| flex-direction: column; | |
| height: 100%; | |
| } | |
| .sidebar-header { | |
| padding: 1.5rem; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .sidebar-title { | |
| font-size: 1.2rem; | |
| font-weight: 600; | |
| margin-bottom: 1rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| /* Formulaire d'ajout */ | |
| .add-video-form { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .input-group { | |
| position: relative; | |
| } | |
| .input-group i { | |
| position: absolute; | |
| left: 12px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-secondary); | |
| } | |
| input[type="text"], input[type="url"] { | |
| width: 100%; | |
| background-color: var(--bg-input); | |
| border: 1px solid transparent; | |
| color: var(--text-primary); | |
| padding: 10px 10px 10px 35px; | |
| border-radius: 6px; | |
| font-size: 0.9rem; | |
| transition: var(--transition); | |
| } | |
| input:focus { | |
| border-color: var(--accent-color); | |
| background-color: #3a3a3a; | |
| } | |
| .btn-add { | |
| background-color: var(--accent-color); | |
| color: white; | |
| padding: 10px; | |
| border-radius: 6px; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| transition: var(--transition); | |
| } | |
| .btn-add:hover { | |
| background-color: var(--accent-hover); | |
| transform: translateY(-1px); | |
| } | |
| /* Liste des vidéos */ | |
| .playlist { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 1rem; | |
| } | |
| /* Scrollbar custom */ | |
| .playlist::-webkit-scrollbar { width: 6px; } | |
| .playlist::-webkit-scrollbar-track { background: var(--bg-sidebar); } | |
| .playlist::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; } | |
| .playlist::-webkit-scrollbar-thumb:hover { background: #555; } | |
| .playlist-item { | |
| display: flex; | |
| gap: 10px; | |
| padding: 10px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| margin-bottom: 8px; | |
| border: 1px solid transparent; | |
| } | |
| .playlist-item:hover { | |
| background-color: var(--bg-card); | |
| } | |
| .playlist-item.active { | |
| background-color: rgba(255, 0, 0, 0.1); | |
| border-color: rgba(255, 0, 0, 0.3); | |
| } | |
| .playlist-thumb { | |
| width: 100px; | |
| height: 56px; /* 16:9 approx */ | |
| background-color: #000; | |
| border-radius: 4px; | |
| overflow: hidden; | |
| flex-shrink: 0; | |
| position: relative; | |
| } | |
| .playlist-thumb img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .playlist-info { | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| overflow: hidden; | |
| } | |
| .playlist-title { | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| margin-bottom: 4px; | |
| } | |
| .playlist-actions { | |
| margin-top: auto; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .btn-delete { | |
| color: var(--text-secondary); | |
| font-size: 0.8rem; | |
| padding: 4px; | |
| transition: color 0.2s; | |
| } | |
| .btn-delete:hover { color: var(--accent-color); } | |
| .empty-state { | |
| text-align: center; | |
| color: var(--text-secondary); | |
| margin-top: 2rem; | |
| font-size: 0.9rem; | |
| } | |
| /* --- CINEMA MODE --- */ | |
| body.cinema-mode header, | |
| body.cinema-mode .sidebar { | |
| display: none; | |
| } | |
| body.cinema-mode main { | |
| grid-template-columns: 1fr; | |
| height: 100vh; | |
| } | |
| body.cinema-mode .player-container { | |
| height: 100vh; | |
| padding: 0; | |
| } | |
| body.cinema-mode .video-wrapper { | |
| max-width: 100%; | |
| border-radius: 0; | |
| height: 100%; | |
| } | |
| .btn-cinema { | |
| background: transparent; | |
| color: var(--text-primary); | |
| font-size: 1.2rem; | |
| padding: 8px; | |
| border-radius: 50%; | |
| transition: var(--transition); | |
| } | |
| .btn-cinema:hover { | |
| background-color: rgba(255,255,255,0.1); | |
| } | |
| /* --- TOAST NOTIFICATIONS --- */ | |
| #toast-container { | |
| position: fixed; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| z-index: 1000; | |
| } | |
| .toast { | |
| background-color: #333; | |
| color: #fff; | |
| padding: 12px 24px; | |
| border-radius: 50px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| font-size: 0.9rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| animation: slideUp 0.3s ease-out forwards; | |
| min-width: 250px; | |
| justify-content: center; | |
| } | |
| .toast.success { border-left: 4px solid #4caf50; } | |
| .toast.error { border-left: 4px solid #f44336; } | |
| @keyframes slideUp { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* --- RESPONSIVE --- */ | |
| @media (max-width: 900px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| overflow-y: auto; | |
| height: auto; | |
| } | |
| .player-container { | |
| padding: 1rem; | |
| min-height: 50vh; | |
| } | |
| .sidebar { | |
| border-left: none; | |
| border-top: 1px solid var(--border-color); | |
| height: auto; | |
| } | |
| .playlist { | |
| max-height: 500px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header> | |
| <div class="logo"> | |
| <i class="fa-brands fa-youtube"></i> | |
| <span>YT Lounge</span> | |
| </div> | |
| <div class="header-controls"> | |
| <button class="btn-cinema" id="cinema-toggle" title="Mode Cinéma"> | |
| <i class="fa-solid fa-expand"></i> | |
| </button> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder | |
| </a> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main> | |
| <!-- Player Section --> | |
| <section class="player-container"> | |
| <div class="video-wrapper"> | |
| <!-- Iframe YouTube par défaut --> | |
| <iframe id="yt-player" | |
| src="https://www.youtube.com/embed/jfKfPfyJRdk?enablejsapi=1" | |
| title="YouTube video player" | |
| allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" | |
| allowfullscreen> | |
| </iframe> | |
| </div> | |
| <div class="video-info"> | |
| <h1 class="video-title" id="current-title">Lofi Hip Hop Radio - Beats to Relax/Study to</h1> | |
| <div class="video-meta"> | |
| <span><i class="fa-regular fa-clock"></i> Lecture en cours</span> | |
| <span><i class="fa-solid fa-tv"></i> YouTube Embed</span> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Sidebar / Playlist --> | |
| <aside class="sidebar"> | |
| <div class="sidebar-header"> | |
| <div class="sidebar-title"> | |
| <span>Mes Vidéos</span> | |
| <span style="font-size: 0.8rem; font-weight: 400; color: var(--text-secondary);" id="playlist-count">0</span> | |
| </div> | |
| <form class="add-video-form" id="add-form"> | |
| <div class="input-group"> | |
| <i class="fa-solid fa-link"></i> | |
| <input type="url" id="video-url" placeholder="Lien YouTube (ex: youtube.com/watch?v=...)" required> | |
| </div> | |
| <!-- Optionnel : Titre personnalisé, sinon on utilisera l'ID généré --> | |
| <button type="submit" class="btn-add"> | |
| <i class="fa-solid fa-plus"></i> Ajouter | |
| </button> | |
| </form> | |
| </div> | |
| <ul class="playlist" id="playlist"> | |
| <!-- Les éléments seront injectés ici par JS --> | |
| </ul> | |
| </aside> | |
| </main> | |
| <!-- Toast Container --> | |
| <div id="toast-container"></div> | |
| <script> | |
| /** | |
| * APPLICATION YOUTUBE LOUNGE | |
| * Logique : Vanilla JS | |
| * Stockage : LocalStorage | |
| */ | |
| // --- État de l'application --- | |
| let playlist = []; | |
| const STORAGE_KEY = 'yt_lounge_playlist_v1'; | |
| // Éléments DOM | |
| const playlistEl = document.getElementById('playlist'); | |
| const playerEl = document.getElementById('yt-player'); | |
| const currentTitleEl = document.getElementById('current-title'); | |
| const addForm = document.getElementById('add-form'); | |
| const urlInput = document.getElementById('video-url'); | |
| const playlistCountEl = document.getElementById('playlist-count'); | |
| const cinemaToggle = document.getElementById('cinema-toggle'); | |
| // --- Initialisation --- | |
| document.addEventListener('DOMContentLoaded', () => { | |
| loadPlaylist(); | |
| // Si la playlist est vide au premier lancement, ajouter une vidéo par défaut | |
| if (playlist.length === 0) { | |
| addVideoToState('https://www.youtube.com/watch?v=jfKfPfyJRdk', 'Lofi Hip Hop Radio - Beats to Relax/Study to'); | |
| } else { | |
| // Charger la première vidéo | |
| loadVideo(playlist[0].id); | |
| } | |
| renderPlaylist(); | |
| }); | |
| // --- Fonctions Principales --- | |
| // 1. Charger la playlist depuis le LocalStorage | |
| function loadPlaylist() { | |
| const stored = localStorage.getItem(STORAGE_KEY); | |
| if (stored) { | |
| try { | |
| playlist = JSON.parse(stored); | |
| } catch (e) { | |
| console.error("Erreur de lecture du storage", e); | |
| playlist = []; | |
| } | |
| } | |
| } | |
| // 2. Sauvegarder dans le LocalStorage | |
| function savePlaylist() { | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(playlist)); | |
| updateCount(); | |
| } | |
| // 3. Extraire l'ID YouTube depuis une URL | |
| function extractYouTubeID(url) { | |
| const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; | |
| const match = url.match(regExp); | |
| return (match && match[2].length === 11) ? match[2] : null; | |
| } | |
| // 4. Ajouter une vidéo | |
| function addVideoToState(url, customTitle = null) { | |
| const videoId = extractYouTubeID(url); | |
| if (!videoId) { | |
| showToast("Lien YouTube invalide", "error"); | |
| return false; | |
| } | |
| // Vérifier doublon | |
| const exists = playlist.find(v => v.id === videoId); | |
| if (exists) { | |
| showToast("Cette vidéo est déjà dans la liste", "error"); | |
| return false; | |
| } | |
| const title = customTitle || `Vidéo YouTube (${videoId})`; | |
| const newVideo = { | |
| id: videoId, | |
| title: title, | |
| thumbnail: `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`, | |
| addedAt: Date.now() | |
| }; | |
| playlist.unshift(newVideo); // Ajouter au début | |
| savePlaylist(); | |
| renderPlaylist(); | |
| showToast("Vidéo ajoutée avec succès", "success"); | |
| return true; | |
| } | |
| // 5. Charger une vidéo dans le lecteur | |
| function loadVideo(id) { | |
| // Mise à jour de l'iframe | |
| playerEl.src = `https://www.youtube.com/embed/${id}?autoplay=1&rel=0`; | |
| // Mise à jour de l'UI active | |
| document.querySelectorAll('.playlist-item').forEach(item => { | |
| item.classList.remove('active'); | |
| if(item.dataset.id === id) { | |
| item.classList.add('active'); | |
| } | |
| }); | |
| // Mise à jour du titre | |
| const vidObj = playlist.find(v => v.id === id); | |
| if (vidObj) { | |
| currentTitleEl.textContent = vidObj.title; | |
| } | |
| } | |
| // 6. Supprimer une vidéo | |
| function deleteVideo(id, event) { | |
| event.stopPropagation(); // Empêcher le clic de jouer la vidéo | |
| if(confirm("Voulez-vous vraiment supprimer cette vidéo de la liste ?")) { | |
| playlist = playlist.filter(v => v.id !== id); | |
| savePlaylist(); | |
| renderPlaylist(); | |
| showToast("Vidéo supprimée", "success"); | |
| // Si on supprime la vidéo en cours, on peut remettre la première ou arrêter | |
| // Ici on laisse la iframe telle quelle pour ne pas couper brutalement | |
| } | |
| } | |
| // 7. Rendu de la liste | |
| function renderPlaylist() { | |
| playlistEl.innerHTML = ''; | |
| if (playlist.length === 0) { | |
| playlistEl.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fa-solid fa-film" style="font-size: 2rem; margin-bottom: 10px; display:block;"></i> | |
| Votre liste est vide.<br>Ajoutez un lien YouTube ci-dessus. | |
| </div> | |
| `; | |
| updateCount(); | |
| return; | |
| } | |
| playlist.forEach(video => { | |
| const li = document.createElement('li'); | |
| li.className = 'playlist-item'; | |
| li.dataset.id = video.id; | |
| li.onclick = () => loadVideo(video.id); | |
| li.innerHTML = ` | |
| <div class="playlist-thumb"> | |
| <img src="${video.thumbnail}" alt="Thumbnail" loading="lazy"> | |
| </div> | |
| <div class="playlist-info"> | |
| <div class="playlist-title" title="${video.title}">${video.title}</div> | |
| <div class="playlist-actions"> | |
| <span style="font-size: 0.7rem; color: #666;">ID: ${video.id}</span> | |
| <button class="btn-delete" onclick="deleteVideo('${video.id}', event)" title="Supprimer"> | |
| <i class="fa-solid fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| playlistEl.appendChild(li); | |
| }); | |
| updateCount(); | |
| } | |
| function updateCount() { | |
| playlistCountEl.textContent = playlist.length; | |
| } | |
| // --- Gestion des événements --- | |
| addForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| const url = urlInput.value.trim(); | |
| if (!url) return; | |
| const success = addVideoToState(url); | |
| if (success) { | |
| urlInput.value = ''; | |
| } | |
| }); | |
| // Mode Cinéma | |
| cinemaToggle.addEventListener('click', () => { | |
| document.body.classList.toggle('cinema-mode'); | |
| const isCinema = document.body.classList.contains('cinema-mode'); | |
| const icon = cinemaToggle.querySelector('i'); | |
| if (isCinema) { | |
| icon.classList.remove('fa-expand'); | |
| icon.classList.add('fa-compress'); | |
| showToast("Mode Cinéma activé. Appuyez sur Esc pour quitter.", "success"); | |
| } else { | |
| icon.classList.remove('fa-compress'); | |
| icon.classList.add('fa-expand'); | |
| } | |
| }); | |
| // Quitter le mode cinéma avec Echap | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape' && document.body.classList.contains('cinema-mode')) { | |
| document.body.classList.remove('cinema-mode'); | |
| cinemaToggle.querySelector('i').classList.remove('fa-compress'); | |
| cinemaToggle.querySelector('i').classList.add('fa-expand'); | |
| } | |
| }); | |
| // --- Système de Toast (Notifications) --- | |
| function showToast(message, type = 'success') { | |
| const container = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| const icon = type === 'success' ? '<i class="fa-solid fa-check-circle"></i>' : '<i class="fa-solid fa-exclamation-circle"></i>'; | |
| toast.innerHTML = `${icon} <span>${message}</span>`; | |
| container.appendChild(toast); | |
| // Auto remove | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| toast.style.transform = 'translateY(20px)'; | |
| toast.addEventListener('transitionend', () => { | |
| toast.remove(); | |
| }); | |
| }, 3000); | |
| } | |
| </script> | |
| </body> | |
| </html> |