add modern progress bar

Hunter
Tuesday, October 28th, 2025 at 9:36 AM

index.html+135-55

index.html
105  			color: var(--asphalt);
106  		}
107  
108 +		.playlist-item:hover .playlist-item-artist {
109 +			opacity: 1;
110 +		}
111 +
112  		.playlist-item:hover .playlist-item-title.current,
113  		.playlist-item:hover .playlist-item-artist.current {
114  			color: var(--asphalt);

137  		.progress-container {
138  			width: 100%;
139  			font-size: 24px;
    -			background-color: var(--icee);
    -			cursor: pointer;
140 +			cursor: grab;
141  			margin-top: 3px;
142  			height: 35px;
143 +			display: flex;
144 +			align-items: center;
145 +			padding: 0 12px;
146 +			box-sizing: border-box;
147 +			position: relative;
148  		}
149  
150  		.progress-container:hover {
    -			cursor: pointer;
151 +			cursor: grab;
152  		}
153  
154  		.progress-bar {
    -			width: 0%;
    -			height: 34px;
    -			background-color: var(--bubblegum);
    -			border-bottom: 1px solid var(--blush);
155 +			width: 100%;
156 +			height: 4px;
157 +			background-color: var(--icee);
158  			position: relative;
159 +			border-radius: 1px;
160 +			--progress: 0%;
161 +			--circle-size: 16px;
162  		}
163  
    -		.progress-text {
    -			text-shadow: 1px 1px 1px var(--asphalt);
164 +		.progress-bar::before {
165 +			content: '';
166 +			position: absolute;
167 +			left: 0;
168 +			top: 0;
169 +			height: 100%;
170 +			width: var(--progress);
171 +			background-color: var(--bubblegum);
172 +			border-radius: 1px;
173 +			transition: none;
174  		}
175  
    -		.progress-text-left {
176 +		.progress-bar::after {
177 +			content: '';
178  			position: absolute;
    -			left: 5px;
179 +			left: calc(var(--progress) - var(--circle-size) / 2);
180  			top: 50%;
181  			transform: translateY(-50%);
    -			color: var(--icee);
182 +			width: var(--circle-size);
183 +			height: var(--circle-size);
184 +			background-color: var(--bubblegum);
185 +			border-radius: 50%;
186 +			box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
187 +			transition: none;
188 +		}
189 +
190 +		.progress-text {
191 +			display: none;
192 +		}
193 +
194 +		.progress-text-left {
195 +			display: none;
196  		}
197  
198  		.progress-text-center {
    -			position: absolute;
    -			left: 50%;
    -			top: 50%;
    -			transform: translate(-50%, -50%);
    -			color: var(--icee);
    -			opacity: 0;
199 +			display: none;
200  		}
201  
202  		.control-button {

265  		let progressInterval;
266  		let playerReady = false;
267  		let songs = [];
268 +		let animationFrameId = null;
269 +		let prePlaySeekTime = 0;
270  
271  		// Load tracks from tracks.json
272  		fetch('tracks.json')

463  
464  		function startProgressBar() {
465  			stopProgressBar();
    -			progressInterval = setInterval(updateProgressBar, 100);
466 +
467 +			function animate() {
468 +				updateProgressBar();
469 +				animationFrameId = requestAnimationFrame(animate);
470 +			}
471 +
472 +			animate();
473  		}
474  
475  		function stopProgressBar() {
    -			clearInterval(progressInterval);
476 +			if (animationFrameId !== null) {
477 +				cancelAnimationFrame(animationFrameId);
478 +				animationFrameId = null;
479 +			}
480  		}
481  
482  		function resetProgressBar() {
    -			progressBar.style.width = '0%';
    -			const progressTextLeft = document.getElementById('progressTextLeft');
    -			const progressTextCenter = document.getElementById('progressTextCenter');
    -			progressTextLeft.textContent = '0%';
    -			progressTextCenter.textContent = '0%';
    -			progressTextLeft.style.opacity = '1';
    -			progressTextCenter.style.opacity = '0';
483 +			progressBar.style.setProperty('--progress', '0%');
484  		}
485  
486  		function updateProgressBar() {
    -			if (audio.duration) {
487 +			if (audio.duration && !isDragging) {
488  				const currentTime = audio.currentTime;
489  				const duration = audio.duration;
490  				const progressPercentage = (currentTime / duration) * 100;
    -				const displayPercentage = isNaN(progressPercentage) ? 0 : Math.round(progressPercentage);
    -				const percentageText = `${displayPercentage}%`;
491 +				const displayPercentage = isNaN(progressPercentage) ? 0 : progressPercentage;
492 +
493 +				progressBar.style.setProperty('--progress', `${displayPercentage}%`);
494 +			}
495 +		}
496  
    -				progressBar.style.width = `${displayPercentage}%`;
497 +		let isDragging = false;
498 +		let wasPlayingBeforeDrag = false;
499 +		let pendingSeekPercentage = null;
500  
    -				const progressTextLeft = document.getElementById('progressTextLeft');
    -				const progressTextCenter = document.getElementById('progressTextCenter');
501 +		function updateVisualProgress(event) {
502 +			if (!playerReady) return;
503  
    -				progressTextLeft.textContent = percentageText;
    -				progressTextCenter.textContent = percentageText;
504 +			const rect = progressBar.getBoundingClientRect();
505 +			const clickPosition = event.clientX - rect.left;
506 +			const clickPercentage = Math.max(0, Math.min(1, clickPosition / rect.width));
507  
    -				// Get the width of the text
    -				const textWidth = progressTextCenter.offsetWidth;
    -				const barWidth = progressBar.offsetWidth;
508 +			progressBar.style.setProperty('--progress', `${clickPercentage * 100}%`);
509 +			return clickPercentage;
510 +		}
511  
    -				// Show centered text only when there's room
    -				if (barWidth >= textWidth + 20) {
    -					progressTextLeft.style.opacity = '0';
    -					progressTextCenter.style.opacity = '1';
    -				} else {
    -					progressTextLeft.style.opacity = '1';
    -					progressTextCenter.style.opacity = '0';
    -				}
512 +		function applySeek(clickPercentage) {
513 +			if (!playerReady) return;
514 +
515 +			// If audio hasn't been loaded yet, load it but don't play
516 +			if (!audio.src || audio.src === '') {
517 +				const song = songs[currentSongIndex];
518 +				audio.src = `tracks/${song.filename}`;
519 +
520 +				// Wait for metadata to be loaded before seeking
521 +				audio.addEventListener('loadedmetadata', function setInitialTime() {
522 +					const duration = audio.duration;
523 +					const seekTime = duration * clickPercentage;
524 +					audio.currentTime = seekTime;
525 +					prePlaySeekTime = seekTime;
526 +					audio.removeEventListener('loadedmetadata', setInitialTime);
527 +				}, { once: true });
528 +			} else if (audio.duration) {
529 +				const duration = audio.duration;
530 +				const seekTime = duration * clickPercentage;
531 +				audio.currentTime = seekTime;
532 +				prePlaySeekTime = seekTime;
533  			}
534  		}
535  
    -		function seekTo(event) {
    -			if (!playerReady || !audio.duration) return;
    -			const rect = progressContainer.getBoundingClientRect();
    -			const clickPosition = event.clientX - rect.left;
    -			const clickPercentage = clickPosition / rect.width;
    -			const duration = audio.duration;
    -			const seekTime = duration * clickPercentage;
    -			audio.currentTime = seekTime;
536 +		function onProgressMouseDown(event) {
537 +			if (!playerReady) return;
538 +			isDragging = true;
539 +			wasPlayingBeforeDrag = isPlaying;
540 +			progressContainer.style.cursor = 'grabbing';
541 +			document.body.style.cursor = 'grabbing';
542 +			pendingSeekPercentage = updateVisualProgress(event);
543 +			event.preventDefault();
544 +		}
545 +
546 +		function onProgressMouseMove(event) {
547 +			if (isDragging) {
548 +				pendingSeekPercentage = updateVisualProgress(event);
549 +			}
550 +		}
551 +
552 +		function onProgressMouseUp(event) {
553 +			if (isDragging) {
554 +				isDragging = false;
555 +				progressContainer.style.cursor = '';
556 +				document.body.style.cursor = '';
557 +
558 +				// Apply the seek now that drag is complete
559 +				if (pendingSeekPercentage !== null) {
560 +					applySeek(pendingSeekPercentage);
561 +					pendingSeekPercentage = null;
562 +				}
563 +
564 +				// If it was "Ready to play" (not playing before), start playing now
565 +				if (!wasPlayingBeforeDrag && !isPlaying && audio.src) {
566 +					audio.play();
567 +					isPlaying = true;
568 +					updatePlayPauseButton();
569 +				}
570 +			}
571  		}
572  
573  		playPauseBtn.addEventListener('click', togglePlayPause);
574  		nextBtn.addEventListener('click', nextSong);
575  		prevBtn.addEventListener('click', prevSong);
    -		progressContainer.addEventListener('click', seekTo);
576 +		progressContainer.addEventListener('mousedown', onProgressMouseDown);
577 +		document.addEventListener('mousemove', onProgressMouseMove);
578 +		document.addEventListener('mouseup', onProgressMouseUp);
579  
580  		// Keyboard controls
581  		document.addEventListener('keydown', function(event) {