1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 6 <meta name="theme-color" content="#030712"> 7 <link rel="manifest" href="manifest.json"> 8 <link rel="apple-touch-icon" href="icon-192x192.png"> 9 10 <title>Aura Media Player Pro</title> 11
11<script src="https://cdn.tailwindcss.com"></script>
11 12
12<script src="https://unpkg.com/@phosphor-icons/web"></script>
12 13 14 <style> 15 /* Custom Scrollbar for Playlist */ 16 ::-webkit-scrollbar { width: 6px; } 17 ::-webkit-scrollbar-track { background: transparent; } 18 ::-webkit-scrollbar-thumb { background: #374151; border-radius: 4px; } 19 ::-webkit-scrollbar-thumb:hover { background: #4b5563; } 20 21 /* Custom Range Slider Styling */ 22 input[type=range] { 23 -webkit-appearance: none; 24 width: 100%; 25 background: transparent; 26 } 27 input[type=range]:focus { outline: none; } 28 input[type=range]::-webkit-slider-thumb { 29 -webkit-appearance: none; 30 height: 12px; 31 width: 12px; 32 border-radius: 50%; 33 background: #fff; 34 cursor: pointer; 35 margin-top: -4px; 36 box-shadow: 0 0 4px rgba(0,0,0,0.5); 37 transition: transform 0.1s; 38 } 39 input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.3); } 40 input[type=range]::-webkit-slider-runnable-track { 41 width: 100%; 42 height: 4px; 43 cursor: pointer; 44 background: var(--track-color, #4b5563); 45 border-radius: 2px; 46 } 47 48 /* Pulsing animation for audio playback */ 49 @keyframes pulse-ring { 50 0% { transform: scale(0.85); box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.6); } 51 70% { transform: scale(1); box-shadow: 0 0 0 20px rgba(99, 102, 241, 0); } 52 100% { transform: scale(0.85); box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); } 53 } 54 .playing-audio .audio-ring { 55 animation: pulse-ring 2s infinite cubic-bezier(0.4, 0, 0.2, 1); 56 } 57 58 .hidden-force { display: none !important; } 59 60 /* Smooth transitions for view switching */ 61 .view-transition { transition: opacity 0.3s ease, transform 0.3s ease; } 62 </style> 63</head> 64<body class="bg-gray-950 text-white h-[100dvh] w-full flex flex-col font-sans overflow-hidden select-none"> 65 66 <header class="h-14 sm:h-16 bg-gray-900 border-b border-gray-800 flex items-center justify-between px-3 sm:px-6 shrink-0 z-20 shadow-md"> 67 <div class="flex items-center gap-2 text-indigo-500 w-auto sm:w-1/4 shrink-0"> 68 <i class="ph-fill ph-play-circle text-2xl sm:text-3xl"></i> 69 <h1 class="text-lg sm:text-xl font-bold tracking-wide text-white hidden sm:block">Aura Pro</h1> 70 </div> 71 72 <div class="flex-1 flex justify-center shrink-0 mx-2"> 73 <div class="flex bg-gray-950 p-1 rounded-full border border-gray-800 shadow-inner"> 74 <button id="tab-audio" class="flex items-center gap-1.5 sm:gap-2 px-3 sm:px-6 py-1 sm:py-1.5 rounded-full bg-indigo-600 text-white text-xs sm:text-sm font-medium transition-all shadow-md"> 75 <i class="ph-fill ph-music-notes text-base sm:text-lg"></i> 76 <span class="hidden sm:inline">Music</span> 77 </button> 78 <button id="tab-video" class="flex items-center gap-1.5 sm:gap-2 px-3 sm:px-6 py-1 sm:py-1.5 rounded-full text-gray-400 hover:text-gray-200 text-xs sm:text-sm font-medium transition-all"> 79 <i class="ph-fill ph-film-strip text-base sm:text-lg"></i> 80 <span class="hidden sm:inline">Video</span> 81 </button> 82 <button id="tab-credits" class="flex items-center gap-1.5 sm:gap-2 px-3 sm:px-6 py-1 sm:py-1.5 rounded-full text-gray-400 hover:text-gray-200 text-xs sm:text-sm font-medium transition-all bg-transparent"> 83 <i class="ph-fill ph-info text-base sm:text-lg"></i> 84 <span class="hidden sm:inline">Credits</span> 85 </button> 86 </div> 87 </div> 88 89 <div class="w-auto sm:w-1/4 flex justify-end shrink-0"> 90 <input type="file" id="file-input" multiple accept="audio/*,video/*" class="hidden"> 91 <button id="upload-btn" class="flex items-center gap-2 bg-gray-800 hover:bg-gray-700 border border-gray-700 text-white px-3 py-1.5 sm:py-2 rounded-full text-xs sm:text-sm font-medium transition-colors"> 92 <i class="ph ph-plus text-base sm:text-lg"></i>
93 <span class="hidden md:inline">Add Media</span> 94 </button> 95 </div> 96 </header> 97 98 <main class="flex-1 flex flex-col md:flex-row overflow-hidden relative min-h-0"> 99 100 <section id="main-visual-area" class="h-[35vh] md:h-auto md:flex-1 bg-black relative overflow-hidden flex items-center justify-center shrink-0 z-10 transition-all duration-300"> 101 102 <video id="media-player" class="absolute inset-0 w-full h-full object-contain z-10 hidden-force cursor-pointer" playsinline></video> 103 104 <div id="view-audio" class="absolute inset-0 flex flex-col items-center justify-center bg-gradient-to-b from-gray-800 to-gray-950 z-20 view-transition p-4 overflow-hidden"> 105 <div class="audio-ring shrink-0 flex items-center justify-center aspect-square rounded-full bg-gradient-to-tr from-indigo-900 to-purple-900 p-6 md:p-10 mb-3 md:mb-6 shadow-2xl transition-all duration-300 border border-indigo-500/30"> 106 <i class="ph-fill ph-music-notes text-5xl md:text-7xl text-indigo-300 drop-shadow-lg leading-none m-0"></i> 107 </div> 108 <h2 id="audio-title" class="text-xl md:text-3xl font-bold text-white text-center px-4 w-full truncate tracking-tight shrink-0">Music Mode</h2> 109 <p id="audio-subtitle" class="text-sm md:text-base text-indigo-300 mt-1 md:mt-2 font-medium tracking-wide text-center w-full truncate shrink-0">Select a track to play</p> 110 </div> 111 112 <div id="view-video-empty" class="absolute inset-0 flex flex-col items-center justify-center bg-gray-950 z-20 hidden-force view-transition p-4"> 113 <div class="p-6 md:p-8 mb-3 md:mb-4 rounded-3xl bg-gray-900/50 border border-gray-800"> 114 <i class="ph-fill ph-film-slate text-5xl md:text-7xl text-gray-600"></i> 115 </div> 116 <h2 class="text-xl md:text-2xl font-semibold text-gray-300 text-center px-4">Video Mode</h2> 117 <p class="text-sm md:text-base text-gray-500 mt-2 text-center">Select a video from your playlist</p> 118 </div> 119 120 <div id="view-credits" class="absolute inset-0 bg-gray-950 z-30 hidden-force view-transition overflow-y-auto"> 121 <div class="min-h-full flex flex-col p-4 sm:p-6"> 122 <div class="max-w-2xl w-full mx-auto bg-gray-900 border border-gray-800 rounded-2xl p-6 sm:p-8 shadow-2xl mt-auto mb-auto shrink-0"> 123 <h2 class="text-2xl sm:text-3xl font-bold text-white mb-6 text-center">Credits</h2> 124 <div class="space-y-4 text-gray-300 text-sm sm:text-base"> 125 <div class="flex flex-col sm:flex-row sm:items-center border-b border-gray-800 pb-4"> 126 <span class="text-gray-500 w-32 font-medium mb-1 sm:mb-0">License:</span> 127 <span class="font-semibold text-white">AGPLv3</span> 128 </div> 129 <div class="flex flex-col sm:flex-row sm:items-center border-b border-gray-800 pb-4"> 130 <span class="text-gray-500 w-32 font-medium mb-1 sm:mb-0">Developer:</span> 131 <span class="font-semibold text-indigo-400">Lcdo. César Cordero RodrÃguez, Gemini AI.</span> 132 </div> 133 <div class="flex flex-col sm:flex-row sm:items-center border-b border-gray-800 pb-4"> 134 <span class="text-gray-500 w-32 font-medium mb-1 sm:mb-0">Maintainer:</span> 135 <span class="font-semibold text-indigo-400">Lcdo. César Cordero RodrÃguez, Gemini AI.</span> 136 </div> 137 </div> 138 139 <div class="mt-6 sm:mt-8 bg-indigo-900/20 border border-indigo-500/30 rounded-xl p-4 sm:p-6 text-center"> 140 <p class="text-base sm:text-lg text-indigo-100 font-medium mb-4 sm:mb-6">I did this software with an AI. Do you want a Software for you?</p> 141 <div class="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4"> 142 <a href="https://t.me/rockcesar" target="_blank" class="w-full sm:w-auto flex items-center justify-center gap-2 bg-indigo-600 hover:bg-indigo-500 text-white px-5 py-2.5 rounded-full transition-colors font-medium text-sm sm:text-base shadow-lg"> 143 <i class="ph-fill ph-telegram-logo text-xl"></i> Contact me 144 </a> 145 <a href="https://rockcesar.github.io" target="_blank" class="w-full sm:w-auto flex items-center justify-center gap-2 bg-gray-800 hover:bg-gray-700 text-white border border-gray-700 px-5 py-2.5 rounded-full transition-colors font-medium text-sm sm:text-base shadow-lg"> 146 <i class="ph-fill ph-user-circle text-xl"></i> My profile 147 </a> 148 <a href="https://github.com/rockcesar/rockcesar.github.io/tree/main/media" target="_blank" class="w-full sm:w-auto flex items-center justify-center gap-2 bg-gray-800 hover:bg-gray-700 text-white border border-gray-700 px-5 py-2.5 rounded-full transition-colors font-medium text-sm sm:text-base shadow-lg"> 149 <i class="ph-fill ph-user-circle text-xl"></i> Source code 150 </a> 151 </div> 152 </div> 153 </div> 154 </div> 155 </div> 156 157 </section> 158 159 <aside id="playlist-aside" class="flex-1 md:w-80 lg:w-96 bg-gray-900 border-t md:border-t-0 md:border-l border-gray-800 flex flex-col shrink-0 min-h-0 z-30 shadow-2xl"> 160 <div class="p-3 sm:p-4 border-b border-gray-800 flex items-center justify-between bg-gray-900/95 backdrop-blur shrink-0"> 161 <h3 id="playlist-header-title" class="text-base sm:text-lg font-semibold flex items-center gap-2 text-gray-100"> 162 <i class="ph-fill ph-playlist"></i> Music Playlist 163 </h3> 164 <div class="flex items-center gap-2"> 165 <button id="btn-clear-playlist" class="text-gray-400 hover:text-red-400 transition-colors p-1.5 rounded-full hover:bg-gray-800 hidden-for
165ce" title="Clear Playlist"> 166 <i class="ph-bold ph-trash text-sm sm:text-base"></i> 167 </button> 168 <span id="playlist-count" class="text-[10px] sm:text-xs font-bold bg-indigo-500/20 text-indigo-400 border border-indigo-500/30 px-2 sm:px-2.5 py-1 rounded-full">0 files</span> 169 </div> 170 </div> 171 172 <div class="px-3 py-2 border-b border-gray-800 bg-gray-900/50 shrink-0"> 173 <div class="relative"> 174 <i class="ph ph-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i> 175 <input type="text" id="input-search" placeholder="Search playlist..." class="w-full bg-gray-950 text-sm text-gray-200 border border-gray-800 rounded-full pl-9 pr-4 py-1.5 focus:outline-none focus:border-indigo-500 transition-colors"> 176 </div> 177 </div> 178 179 <div id="playlist" class="flex-1 overflow-y-auto p-2 space-y-1 relative min-h-0"> 180 <div id="empty-state" class="absolute inset-0 flex flex-col items-center justify-center text-gray-500 pointer-events-none p-6 text-center"> 181 <i class="ph-fill ph-folder-open text-3xl sm:text-4xl mb-3 opacity-40"></i> 182 <p id="empty-state-text" class="text-xs sm:text-sm text-gray-400">No music uploaded.</p> 183 <button class="mt-4 px-4 py-1.5 rounded-full border border-gray-600 text-xs pointer-events-auto hover:bg-gray-800 hover:text-white transition-colors" onclick="document.getElementById('file-input').click()"> 184 Upload Files 185 </button> 186 </div> 187 </div> 188 </aside> 189 190 </main> 191 192 <footer class="h-auto sm:h-24 bg-gray-900 border-t border-gray-800 flex flex-col sm:flex-row items-center px-2 sm:px-6 shrink-0 z-40 py-2 sm:py-0 relative"> 193 194 <div class="w-full flex items-center gap-2 sm:hidden px-2 mb-2"> 195 <span id="time-current-mobile" class="text-[10px] text-gray-400 font-mono w-8 text-right">0:00</span> 196 <input type="range" id="progress-bar-mobile" value="
1960" min="0" max="100" class="flex-1 h-1"> 197 <span id="time-total-mobile" class="text-[10px] text-gray-400 font-mono w-8">0:00</span> 198 </div> 199 200 <div class="w-full sm:w-1/3 flex items-center gap-3 mb-2 sm:mb-0 justify-center sm:justify-start px-2 sm:px-0"> 201 <div id="bottom-thumbnail" class="w-10 h-10 sm:w-12 sm:h-12 bg-gray-800 rounded-lg flex items-center justify-center shrink-0 overflow-hidden relative shadow-md border border-gray-700"> 202 <i id="mini-icon" class="ph-fill ph-speaker-hifi text-xl sm:text-2xl text-gray-500"></i> 203 <video id="mini-video-preview" class="w-full h-full object-cover hidden-force" muted playsinline></video> 204 </div> 205 <div class="flex flex-col text-left flex-1 min-w-0"> 206 <span id="current-title" class="text-xs sm:text-sm font-bold truncate w-full text-white">No active media</span> 207 <span id="current-type" class="text-[10px] sm:text-xs text-gray-400 truncate w-full font-medium">Ready to play</span> 208 </div> 209 </div> 210 211 <div class="w-full sm:w-1/3 flex flex-col items-center justify-center gap-2 pb-1 sm:pb-0"> 212 <div class="flex items-center gap-4 sm:gap-6"> 213 <button id="btn-shuffle" class="text-gray-400 hover:text-white transition-colors" title="Shuffle"> 214 <i class="ph-bold ph-shuffle text-base sm:text-xl"></i> 215 </button> 216 <button id="btn-prev" class="text-gray-300 hover:text-white transition-colors" title="Previous"> 217 <i class="ph-fill ph-skip-back text-xl sm:text-3xl"></i> 218 </button> 219 <button id="btn-play" class="w-10 h-10 sm:w-14 sm:h-14 bg-white text-black rounded-full flex items-center justify-center hover:scale-105 active:scale-95 transition-all shadow-lg hover:shadow-white/20" title="Play/Pause"> 220 <i id="icon-play" class="ph-fill ph-play text-xl sm:text-3xl"></i> 221 </button> 222 <button id="btn-next" class="text-gray-300 hover:text-white transition-colors" title="Next"> 223 <i class="ph-fill ph-skip-forward text-xl sm:text-3xl"></i> 224 </button> 225 <button id="btn-repeat" class="text-gray-400 hover:text-white transition-colors" title="Repeat"> 226 <i class="ph-bold ph-repeat text-base sm:text-xl"></i> 227 </button> 228 </div> 229 230 <div class="hidden sm:flex w-full max-w-md items-center gap-3 mt-1"> 231 <span id="time-current" class="text-xs text-gray-400 font-mono w-10 text-right">0:00</span>
232 <input type="range" id="progress-bar" value="0" min="0" max="100" class="flex-1 group-hover:bg-indigo-500"> 233 <span id="time-total" class="text-xs text-gray-400 font-mono w-10">0:00</span> 234 </div> 235 </div> 236 237 <div class="hidden sm:flex w-1/3 items-center justify-end gap-3 pr-2"> 238 <button id="btn-mute" class="text-gray-400 hover:text-white transition-colors"> 239 <i id="icon-volume" class="ph-fill ph-speaker-high text-xl"></i> 240 </button> 241 <input type="range" id="volume-bar" value="100" min="0" max="100" class="w-24"> 242 <div class="h-6 w-px bg-gray-700 mx-2"></div> 243 <button id="btn-fullscreen" class="text-gray-500 transition-colors pointer-events-none" title="Fullscreen Video"> 244 <i class="ph-bold ph-corners-out text-xl"></i> 245 </button> 246 </div> 247 </footer> 248 249
249<script> 250 document.addEventListener('DOMContentLoaded', () => { 251 // --- Application State --- 252 const state = { 253 currentTab: 'audio', // 'audio' or 'video' 254 playlists: { 255 audio: [], 256 video: [] 257 }, 258 searchQuery: '', // State for the new search feature 259 currentTrack: null, // Object: { id, name, type, url, file } 260 currentIndex: { 261 audio: -1, 262 video: -1 263 }, 264 isPlaying: false, 265 isShuffle: false, 266 repeatMode: 0 // 0: none, 1: all, 2: one 267 }; 268 269 // --- DOM Elements --- 270 // Main Player 271 const player = document.getElementById('media-player'); 272 273 // File Management 274 const fileInput = document.getElementById('file-input'); 275 const uploadBtn = document.getElementById('upload-btn'); 276 const btnClearPlaylist = document.getElementById('btn-clear-playlist'); 277 278 // Tabs 279 const tabAudio = document.getElementById('tab-audio'); 280 const tabVideo = document.getElementById('tab-video'); 281 const tabCredits = document.getElementById('tab-credits'); 282 283 // Visual Views 284 const mainVisualArea = document.getElementById('main-visual-area'); 285 const viewAudio = document.getElementById('view-audio'); 286 const viewVideoEmpty = document.getElementById('view-video-empty'); 287 const viewCredits = document.getElementById('view-credits'); 288 const audioTitle = document.getElementById('audio-title'); 289 const audioSubtitle = document.getElementById('audio-subtitle'); 290 291 // Playlist Area 292 const playlistAside = document.getElementById('playlist-aside'); 293 const playlistContainer = document.getElementById('playlist'); 294 const emptyState = document.getElementById('empty-state'); 295 const emptyStateText = document.getElementById('empty-state-text'); 296 const playlistCount = document.getElementById('playlist-count'); 297 const playlistHeaderTitle = document.getElementById('playlist-header-title'); 298 const inputSearch = document.getElementById('input-search'); // Search DOM 299 300 // Bottom Controls 301 const btnPlay = document.getElementById('btn-play'); 302 const iconPlay = document.getElementById('icon-play'); 303 const btnPrev = document.getElementById('btn-prev'); 304 const btnNext = document.getElementById('btn-next'); 305 const btnShuffle = document.getElementById('btn-shuffle'); 306 const btnRepeat = document.getElementById('btn-repeat'); 307 const btnMute = document.getElementById('btn-mute'); 308 const iconVolume = document.getElementById('icon-volume'); 309 const btnFullscreen = document.getElementById('btn-fullscreen'); 310 311 // Progress & Meta 312 const currentTitle = document.getElementById('current-title'); 313 const currentType = document.getElementById('current-type'); 314 const miniIcon = document.getElementById('mini-icon'); 315 const miniVideoPreview = document.getElementById('mini-video-preview'); 316 const bottomThumbnail = document.getElementById('bottom-thumbnail'); 317 318 const progressBar = document.getElementById('progress-bar'); 319 const progressBarMobile = document.getElementById('progress-bar-mobile'); 320 const volumeBar = document.getElementById('volume-bar'); 321 const timeCurrent = document.getElementById('time-current'); 322 const timeTotal = document.getElementById('time-total'); 323 const timeCurrentMobile = document.getElementById('time-current-mobile'); 324 const timeTotalMobile = document.getElementById('time-total-mobile'); 325 326 // --- Initialization --- 327 player.volume = 1;
328 updateRangeBackground(volumeBar); 329 updateMainVisuals(); 330 331 // --- Event Listeners --- 332 333 // Search Logic 334 inputSearch.addEventListener('input', (e) => { 335 state.searchQuery = e.target.value.toLowerCase(); 336 renderPlaylist(); 337 }); 338 339 // Upload Logic 340 uploadBtn.addEventListener('click', () => fileInput.click()); 341 fileInput.addEventListener('change', (e) => { 342 const files = Array.from(e.target.files); 343 if (files.length === 0) return; 344 345 let addedAudio = 0, addedVideo = 0; 346 347 files.forEach(file => { 348 const isVideo = file.type.startsWith('video/'); 349 const type = isVideo ? 'video' : 'audio'; 350 351 state.playlists[type].push({ 352 id: Math.random().toString(36).substr(2, 9), 353 name: file.name.replace(/\.[^/.]+$/, ""), 354 type: type, 355 mimeType: file.type, 356 url: URL.createObjectURL(file), 357 file: file 358 }); 359 360 if(isVideo) addedVideo++; else addedAudio++; 361 }); 362 363 // Auto switch tab if we uploaded files for the non-active tab and none for active 364 if (state.currentTab === 'audio' && addedAudio === 0 && addedVideo > 0) { 365 switchTab('video'); 366 } else if (state.currentTab === 'video' && addedVideo === 0 && addedAudio > 0) { 367 switchTab('audio'); 368 } else { 369 renderPlaylist(); // Render active tab's list 370 } 371 372 updatePlaylistCountUI(); 373 fileInput.value = ''; // Reset 374 }); 375 376 // Clear Playlist Logic 377 btnClearPlaylist.addEventListener('click', () => { 378 const type = state.currentTab; 379 if (state.playlists[type].length === 0) return; 380 381 // Revoke URLs to free memory 382 state.playlists[type].forEach(track => URL.revokeObjectURL(track.url)); 383 384 // Clear the array 385 state.playlists[type] = []; 386 387 // If the current track is from this playlist, stop playback completely 388 if (state.currentTrack && state.currentTrack.type === type) { 389 player.pause(); 390 player.src = ''; 391 state.currentTrack = null; 392 state.currentIndex[type] = -1; 393 394 // Reset bottom UI 395 currentTitle.textContent = 'No active media'; 396 currentType.textContent = 'Ready to play'; 397 miniIcon.classList.remove('hidden-force'); 398 miniIcon.className = 'ph-fill ph-speaker-hifi text-xl sm:text-2xl text-gray-500';
399 miniVideoPreview.classList.add('hidden-force'); 400 timeCurrent.textContent = '0:00'; 401 timeTotal.textContent = '0:00'; 402 progressBar.value = 0; 403 updateRangeBackground(progressBar); 404 btnFullscreen.classList.add('pointer-events-none', 'text-gray-500'); 405 } else { 406 // Just reset the index for the cleared tab 407 state.currentIndex[type] = -1; 408 } 409 410 updatePlaylistCountUI(); 411 renderPlaylist(); 412 updateMainVisuals(); 413 }); 414 415 // Tab Switching Logic 416 tabAudio.addEventListener('click', () => switchTab('audio')); 417 tabVideo.addEventListener('click', () => switchTab('video')); 418 tabCredits.addEventListener('click', () => switchTab('credits')); 419 420 // Playback Controls 421 btnPlay.addEventListener('click', togglePlay); 422 btnNext.addEventListener('click', playNext); 423 btnPrev.addEventListener('click', playPrev); 424 425 btnShuffle.addEventListener('click', () => { 426 state.isShuffle = !state.isShuffle; 427 btnShuffle.classList.toggle('text-indigo-400', state.isShuffle); 428 btnShuffle.classList.toggle('text-gray-400', !state.isShuffle); 429 }); 430 431 btnRepeat.addEventListener('click', () => { 432 state.repeatMode = (state.repeatMode + 1) % 3; 433 const icon = btnRepeat.querySelector('i'); 434 435 if (state.repeatMode === 0) { 436 btnRepeat.className = 'text-gray-400 hover:text-white transition-colors'; 437 icon.className = 'ph-bold ph-repeat text-base sm:text-xl'; 438 } else if (state.repeatMode === 1) { 439 btnRepeat.className = 'text-indigo-400 transition-colors'; 440 icon.className = 'ph-bold ph-repeat text-base sm:text-xl'; 441 } else { 442 btnRepeat.className = 'text-indigo-400 transition-colors'; 443 icon.className = 'ph-bold ph-repeat-once text-base sm:text-xl'; 444 } 445 }); 446 447 // Volume Controls 448 btnMute.addEventListener('click', () => { 449 player.muted = !player.muted; 450 volumeBar.value = player.muted ? 0 : (player.volume * 100 || 50); 451 if(!player.muted && player.volume === 0) player.volume = 0.5; 452 updateVolumeIcon(); 453 updateRangeBackground(volumeBar); 454 }); 455 456 volumeBar.addEventListener('input', (e) => { 457 player.volume = e.target.value / 100; 458 player.muted = false; 459 updateVolumeIcon(); 460 updateRangeBackground(volumeBar); 461 }); 462 463 // Progress Controls 464 [progressBar, progressBarMobile].forEach(bar => { 465 bar.addEventListener('input', (e) => { 466 if(!player.duration) return; 467 player.currentTime = (e.target.value / 100) * player.duration; 468 updateRangeBackground(bar); 469 }); 470 }); 471 472 // Player Events 473 player.addEventListener('timeupdate', () => { 474 if (!player.duration) return; 475 const percentage = (player.currentTime / player.duration) * 100; 476 progressBar.value = percentage; 477 progressBarMobile.value = percentage; 478 updateRangeBackground(progressBar); 479 updateRangeBackground(progressBarMobile); 480 481 const currTimeStr = formatTime(player.currentTime); 482 timeCurrent.textContent = currTimeStr; 483 timeCurrentMobile.textContent = currTimeStr; 484 }); 485 486 player.addEventListener('loadedmetadata', () => { 487 const totalTimeStr = formatTime(player.duration); 488 timeTotal.textContent = totalTimeStr; 489 timeTotalMobile.textContent = totalTimeStr; 490 }); 491 492 player.addEventListener('ended', handleTrackEnded); 493 494 player.addEventListener('play', () => { 495 state.isPlaying = true; 496 iconPlay.className = 'ph-fill ph-pause text-xl sm:text-3xl'; 497 updateMainVisuals(); 498 renderPlaylist(); 499 }); 500 501 player.addEventListener('pause', () => { 502 state.isPlaying = false;
503 iconPlay.className = 'ph-fill ph-play text-xl sm:text-3xl'; 504 updateMainVisuals(); 505 renderPlaylist(); 506 }); 507 508 // Fullscreen Logic 509 const toggleFullscreen = () => { 510 if (state.currentTrack && state.currentTrack.type === 'video') { 511 if (document.fullscreenElement) { 512 if (document.exitFullscreen) document.exitFullscreen(); 513 } else { 514 if (player.requestFullscreen) player.requestFullscreen(); 515 else if (player.webkitRequestFullscreen) player.webkitRequestFullscreen(); 516 } 517 } 518 }; 519 520 btnFullscreen.addEventListener('click', toggleFullscreen); 521 player.addEventListener('dblclick', toggleFullscreen); 522 523 // --- Core Functions --- 524 525 function switchTab(tab) { 526 state.currentTab = tab; 527 528 // Reset search when switching tabs 529 state.searchQuery = ''; 530 if(inputSearch) inputSearch.value = ''; 531 532 // Update Header Tab UI 533 const activeClasses = ['bg-indigo-600', 'text-white', 'shadow-md']; 534 const inactiveClasses = ['text-gray-400', 'hover:text-gray-200', 'bg-transparent']; 535 536 [tabAudio, tabVideo, tabCredits].forEach(t => { 537 t.classList.remove(...activeClasses); 538 t.classList.add(...inactiveClasses); 539 }); 540 541 if (tab === 'audio') { 542 tabAudio.classList.remove(...inactiveClasses); 543 tabAudio.classList.add(...activeClasses); 544 playlistAside.classList.remove('hidden-force'); 545 mainVisualArea.classList.remove('flex-1'); 546 mainVisualArea.classList.add('h-[35vh]'); 547 548 playlistHeaderTitle.innerHTML = '<i class="ph-fill ph-playlist"></i> Music Playlist'; 549 emptyStateText.textContent = 'No music uploaded.'; 550 } else if (tab === 'video') { 551 tabVideo.classList.remove(...inactiveClasses); 552 tabVideo.classList.add(...activeClasses); 553 playlistAside.classList.remove('hidden-force'); 554 mainVisualArea.classList.remove('flex-1'); 555 mainVisualArea.classList.add('h-[35vh]'); 556 557 playlistHeaderTitle.innerHTML = '<i class="ph-fill ph-film-strip"></i> Video Playlist'; 558 emptyStateText.textContent = 'No videos uploaded.'; 559 } else if (tab === 'credits') { 560 tabCredits.classList.remove(...inactiveClasses); 561 tabCredits.classList.add(...activeClasses);
562 playlistAside.classList.add('hidden-force'); 563 mainVisualArea.classList.remove('h-[35vh]'); 564 mainVisualArea.classList.add('flex-1'); 565 } 566 567 if (tab !== 'credits') { 568 updatePlaylistCountUI(); 569 renderPlaylist(); 570 } 571 updateMainVisuals(); 572 } 573 574 function updatePlaylistCountUI() { 575 if (state.currentTab === 'credits') return; 576 const count = state.playlists[state.currentTab].length; 577 playlistCount.textContent = `${count} file${count !== 1 ? 's' : ''}`; 578 579 // Toggle clear playlist button visibility 580 if (count > 0) { 581 btnClearPlaylist.classList.remove('hidden-force'); 582 } else { 583 btnClearPlaylist.classList.add('hidden-force'); 584 } 585 } 586 587 function renderPlaylist() { 588 if (state.currentTab === 'credits') return; 589 const fullList = state.playlists[state.currentTab]; 590 591 // Filter list based on search query 592 const list = state.searchQuery 593 ? fullList.filter(track => track.name.toLowerCase().includes(state.searchQuery)) 594 : fullList; 595 596 if (fullList.length === 0) { 597 emptyState.classList.remove('hidden-force'); 598 emptyStateText.textContent = state.currentTab === 'audio' ? 'No music uploaded.' : 'No videos uploaded.'; 599 Array.from(playlistContainer.children).forEach(child => { 600 if(child.id !== 'empty-state') child.remove(); 601 }); 602 return; 603 } else if (list.length === 0) { 604 emptyState.classList.remove('hidden-force'); 605 emptyStateText.textContent = 'No results found.'; 606 Array.from(playlistContainer.children).forEach(child => { 607 if(child.id !== 'empty-state') child.remove(); 608 }); 609 return; 610 } 611
612 emptyState.classList.add('hidden-force'); 613 614 Array.from(playlistContainer.children).forEach(child => { 615 if(child.id !== 'empty-state') child.remove(); 616 }); 617 618 list.forEach((track) => { 619 const originalIndex = fullList.indexOf(track); 620 const isActive = state.currentTrack && state.currentTrack.id === track.id; 621 const item = document.createElement('div'); 622 623 // Dynamic styling for active item 624 let containerClasses = isActive 625 ? 'bg-indigo-600/20 border-indigo-500/40' 626 : 'hover:bg-gray-800 border-transparent'; 627 628 const iconClass = track.type === 'video' ? 'ph-film-strip' : 'ph-music-notes-simple'; 629 const activeIconClass = state.isPlaying && isActive ? 'ph-waveform' : 'ph-play-circle'; 630 631 item.className = `group flex items-center justify-between p-2 rounded-xl cursor-pointer transition-all duration-200 border ${containerClasses}`; 632 633 item.innerHTML = ` 634 <div class="flex items-center gap-3 flex-1 min-w-0"> 635 <div class="w-10 h-10 shrink-0 rounded-lg bg-gray-950 border border-gray-800 flex items-center justify-center text-gray-400 group-hover:bg-indigo-500 group-hover:text-white transition-all relative overflow-hidden shadow-inner"> 636 ${track.type === 'video' ? `<div class="absolute inset-0 bg-[url('${track.url}')] bg-cover opacity-30"></div>` : ''} 637 <i class="ph-fill ${isActive ? activeIconClass : iconClass} text-lg relative z-10 ${isActive ? 'text-indigo-400' : ''}"></i> 638 </div> 639 <div class="flex flex-col flex-1 min-w-0"> 640 <span class="text-xs sm:text-sm font-semibold truncate ${isActive ? 'text-indigo-400' : 'text-gray-200 group-hover:text-white'} transition-colors w-full block">${track.name}</span> 641 <span class="text-[10px] sm:text-xs text-gray-500 mt-0.5">${track.type === 'video' ? 'Video File' : 'Audio File'}</span> 642 </div> 643 </div> 644 <button class="remove-btn text-gray-600 hover:text-red-400 p-2 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity rounded-full hover:bg-gray-800 shrink-0" title="Remove"> 645 <i class="ph-bold ph-trash"></i> 646 </button> 647 `; 648 649 item.addEventListener('click', (e) => { 650 if (e.target.closest('.remove-btn')) return; 651 if (isActive) { 652 togglePlay(); 653 } else { 654 loadTrack(state.currentTab, originalIndex); 655 playMedia(); 656 } 657 }); 658 659 item.querySelector('.remove-btn').addEventListener('click', (e) => { 660 e.stopPropagation(); 661 removeTrack(state.currentTab, originalIndex); 662 }); 663 664 playlistContainer.appendChild(item); 665 }); 666 667 // Scroll active item into view (only if not currently searching, to prevent jumpiness) 668 if (state.currentTrack && state.currentTrack.type === state.currentTab && !state.searchQuery) { 669 const activeIndex = state.currentIndex[state.currentTab]; 670 if (activeIndex >= 0 && playlistContainer.children[activeIndex + 1]) { 671 setTimeout(() => { 672 playlistContainer.children[activeIndex + 1].scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 673 }, 50); 674 } 675 } 676 } 677 678 function loadTrack(type, index) { 679 const list = state.playlists[type]; 680 if (index < 0 || index >= list.length) return; 681 682 state.currentTrack = list[index]; 683 state.currentIndex[type] = index; 684 685 player.src = state.currentTrack.url; 686 player.load(); 687 688 updateBottomPlayerUI(); 689 updateMainVisuals(); 690 renderPlaylist(); 691 692 // Reset progress 693 progressBar.value = 0; 694 progressBarMobile.value = 0;
695 updateRangeBackground(progressBar); 696 updateRangeBackground(progressBarMobile); 697 } 698 699 function updateBottomPlayerUI() { 700 if (!state.currentTrack) return; 701 702 currentTitle.textContent = state.currentTrack.name; 703 currentType.textContent = state.currentTrack.type === 'video' ? 'Video playback' : 'Audio playback'; 704 705 if (state.currentTrack.type === 'video') { 706 miniIcon.classList.add('hidden-force'); 707 miniVideoPreview.classList.remove('hidden-force'); 708 miniVideoPreview.src = state.currentTrack.url; 709 710 btnFullscreen.classList.remove('text-gray-500', 'pointer-events-none'); 711 btnFullscreen.classList.add('text-gray-300', 'hover:text-white'); 712 bottomThumbnail.classList.add('border-indigo-500/50'); 713 } else { 714 miniIcon.classList.remove('hidden-force');
715 miniVideoPreview.classList.add('hidden-force'); 716 miniIcon.className = 'ph-fill ph-music-notes text-xl sm:text-2xl text-indigo-400'; 717 718 btnFullscreen.classList.add('text-gray-500', 'pointer-events-none'); 719 btnFullscreen.classList.remove('text-gray-300', 'hover:text-white'); 720 bottomThumbnail.classList.remove('border-indigo-500/50'); 721 } 722 } 723 724 function updateMainVisuals() { 725 // Hide all views first 726 viewAudio.classList.add('hidden-force'); 727 viewVideoEmpty.classList.add('hidden-force'); 728 viewCredits.classList.add('hidden-force'); 729 730 // If we are in the CREDITS tab 731 if (state.currentTab === 'credits') { 732 viewCredits.classList.remove('hidden-force'); 733 viewAudio.classList.remove('playing-audio'); 734 735 if (state.currentTrack && state.currentTrack.type === 'video') { 736 player.classList.remove('hidden-force'); 737 } else {
738 player.classList.add('hidden-force'); 739 } 740 } 741 // If we are in the AUDIO tab 742 else if (state.currentTab === 'audio') { 743 viewAudio.classList.remove('hidden-force'); 744 player.classList.add('hidden-force'); // Hide video element globally 745 746 // Logic for Audio View content based on playback 747 if (state.currentTrack && state.currentTrack.type === 'audio') { 748 audioTitle.textContent = state.currentTrack.name; 749 audioSubtitle.textContent = state.isPlaying ? 'Now Playing' : 'Paused'; 750 if(state.isPlaying) { 751 viewAudio.classList.add('playing-audio'); 752 } else { 753 viewAudio.classList.remove('playing-audio'); 754 } 755 } else if (state.currentTrack && state.currentTrack.type === 'video') { 756 viewAudio.classList.remove('playing-audio'); 757 audioTitle.textContent = "Music Mode"; 758 audioSubtitle.textContent = `Playing video: ${state.currentTrack.name}`; 759 } else { 760 viewAudio.classList.remove('playing-audio'); 761 audioTitle.textContent = "Music Mode"; 762 audioSubtitle.textContent = "Select a track to play"; 763 } 764 } 765 // If we are in the VIDEO tab 766 else if (state.currentTab === 'video') { 767 viewAudio.classList.remove('playing-audio'); 768 769 if (state.currentTrack && state.currentTrack.type === 'video') { 770 player.classList.remove('hidden-force'); 771 } else {
772 player.classList.add('hidden-force'); 773 viewVideoEmpty.classList.remove('hidden-force'); 774 775 const msg = (state.currentTrack && state.currentTrack.type === 'audio') 776 ? `Playing audio: ${state.currentTrack.name}` 777 : "Select a video from your playlist"; 778 viewVideoEmpty.querySelector('p').textContent = msg; 779 } 780 } 781 } 782 783 function playMedia() { 784 const playPromise = player.play(); 785 if (playPromise !== undefined) { 786 playPromise.catch(error => { 787 if (error.name === 'AbortError') { 788 console.log('Playback interrupted expectedly.'); 789 } else { 790 console.error('Playback error:', error); 791 } 792 }); 793 } 794 } 795 796 function togglePlay() { 797 if (!state.currentTrack) { 798 if (state.playlists[state.currentTab].length > 0) { 799 loadTrack(state.currentTab, 0); 800 playMedia(); 801 } 802 return; 803 } 804 if (player.paused) playMedia(); 805 else player.pause(); 806 } 807 808 function playNext() { 809 if (!state.currentTrack) return; 810 811 const type = state.currentTrack.type; 812 const list = state.playlists[type]; 813 if (list.length === 0) return; 814 815 let nextIndex; 816 if (state.isShuffle) { 817 nextIndex = Math.floor(Math.random() * list.length); 818 if (nextIndex === state.currentIndex[type] && list.length > 1) { 819 nextIndex = (nextIndex + 1) % list.length; 820 } 821 } else { 822 nextIndex = (state.currentIndex[type] + 1) % list.length; 823 } 824 825 loadTrack(type, nextIndex); 826 playMedia(); 827 } 828 829 function playPrev() { 830 if (!state.currentTrack) return; 831 832 const type = state.currentTrack.type; 833 const list = state.playlists[type]; 834 if (list.length === 0) return; 835 836 if (player.currentTime > 3) { 837 player.currentTime = 0; 838 return; 839 } 840 841 let prevIndex; 842 if (state.isShuffle) { 843 prevIndex = Math.floor(Math.random() * list.length); 844 } else { 845 prevIndex = state.currentIndex[type] - 1; 846 if (prevIndex < 0) prevIndex = list.length - 1; 847 } 848 849 loadTrack(type, prevIndex); 850 playMedia(); 851 } 852 853 function handleTrackEnded() { 854 if (state.repeatMode === 2) { // Repeat One 855 player.currentTime = 0; 856 playMedia(); 857 } else if (state.repeatMode === 1) { // Repeat All 858 playNext(); 859 } else { // None 860 const type = state.currentTrack.type; 861 const list = state.playlists[type]; 862 if (state.currentIndex[type] === list.length - 1 && !state.isShuffle) { 863 player.pause(); 864 player.currentTime = 0; 865 } else { 866 playNext(); 867 } 868 } 869 } 870 871 function removeTrack(type, index) { 872 const list = state.playlists[type]; 873 URL.revokeObjectURL(list[index].url); 874 list.splice(index, 1); 875 876 if (state.currentTrack && state.currentTrack.type === type) { 877 if (state.currentIndex[type] === index) { 878 player.pause(); 879 player.src = ''; 880 state.currentTrack = null; 881 state.currentIndex[type] = -1; 882 883 currentTitle.textContent = 'No active media'; 884 currentType.textContent = 'Ready to play';
885 miniIcon.classList.remove('hidden-force'); 886 miniIcon.className = 'ph-fill ph-speaker-hifi text-xl sm:text-2xl text-gray-500'; 887 miniVideoPreview.classList.add('hidden-force'); 888 timeCurrent.textContent = '0:00'; 889 timeTotal.textContent = '0:00'; 890 progressBar.value = 0; 891 updateRangeBackground(progressBar); 892 btnFullscreen.classList.add('pointer-events-none', 'text-gray-500'); 893 } else if (state.currentIndex[type] > index) { 894 state.currentIndex[type]--; 895 } 896 } 897 898 updatePlaylistCountUI(); 899 if (state.currentTab === type) renderPlaylist(); 900 updateMainVisuals(); 901 } 902 903 function updateVolumeIcon() { 904 if (player.muted || player.volume === 0) iconVolume.className = 'ph-fill ph-speaker-x text-xl'; 905 else if (player.volume < 0.5) iconVolume.className = 'ph-fill ph-speaker-low text-xl'; 906 else iconVolume.className = 'ph-fill ph-speaker-high text-xl'; 907 } 908 909 function formatTime(seconds) { 910 if (isNaN(seconds)) return "0:00"; 911 const m = Math.floor(seconds / 60); 912 const s = Math.floor(seconds % 60); 913 return `${m}:${s.toString().padStart(2, '0')}`; 914 } 915
916 function updateRangeBackground(slider) { 917 const percentage = (slider.value - slider.min) / (slider.max - slider.min) * 100; 918 slider.style.setProperty('--track-color', `linear-gradient(to right, #6366f1 0%, #6366f1 ${percentage}%, #4b5563 ${percentage}%, #4b5563 100%)`); 919 } 920 }); 921 </script>
921 922</body> 923</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.