add modern progress bar
Hunter
Tuesday, October 28th, 2025 at 9:36 AM
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) {