PageSourceSearch

https://rockcesar.github.io/media/

html rockcesar.github.io collected 2026-10-03 09:51:35 UTC 49,439 bytes, 923 lines download raw bytes

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.