split out html, css, and js

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

index.html+2-595
script.jsnew
styles.cssnew

index.html
 5  	<meta charset="UTF-8">
 6  	<meta name="viewport" content="width=device-width, initial-scale=1.0">
 7  	<title>vibe capsule</title>
   -	<style>
   -		:root {
   -			--icee: #dddddd;
   -			--blueberry: #4c7ae6;
   -			--bubblegum: #f421ff;
   -			--asphalt: #080a0c;
   -			--fog: #ddddddcc;
   -		}
   -
   -		* {
   -			scrollbar-color: var(--fog) var(--asphalt);
   -		}
   -
   -		body {
   -			margin: 0;
   -			padding: 0;
   -			font-family: "MS Sans Serif", Arial, sans-serif;
   -			height: 100vh;
   -			display: flex;
   -			flex-direction: column;
   -			font-size: 24px;
   -			background-color: var(--asphalt);
   -		}
   -
   -		.window-container {
   -			background: transparent;
   -			height: 100%;
   -			display: flex;
   -			flex-direction: column;
   -		}
   -
   -		.player-container {
   -			flex-grow: 1;
   -			display: flex;
   -			flex-direction: column;
   -			overflow: hidden;
   -			position: relative;
   -		}
   -
   -		.controls {
   -			display: flex;
   -			justify-content: center;
   -			padding-bottom: 10px;
   -			height: 52px;
   -			background: transparent;
   -			position: absolute;
   -			bottom: 0;
   -			left: 0;
   -			right: 0;
   -			box-sizing: border-box;
   -			z-index: 3;
   -		}
   -
   -		.playlist {
   -			flex-grow: 1;
   -			overflow-y: auto;
   -			padding-bottom: 62px;
   -			cursor: initial;
   -		}
   -
   -		.playlist-item {
   -			padding: 5px 10px;
   -			display: flex;
   -			justify-content: space-between;
   -			align-items: center;
   -			color: var(--icee);
   -			min-height: 45px;
   -		}
   -
   -		.playlist-item-content {
   -			flex-grow: 1;
   -			display: flex;
   -			flex-direction: column;
   -			gap: 2px;
   -		}
   -
   -		.playlist-item-title {
   -			font-size: 20px;
   -			line-height: 1.2;
   -		}
   -
   -		.playlist-item-title.current {
   -			font-weight: bold;
   -			color: var(--blueberry);
   -		}
   -
   -		.playlist-item-artist {
   -			font-size: 16px;
   -			color: var(--fog);
   -			line-height: 1.2;
   -		}
   -
   -		.playlist-item-artist.current {
   -			font-weight: bold;
   -			color: var(--blueberry);
   -			opacity: 1;
   -		}
   -
   -		.playlist-item:hover {
   -			background-color: var(--bubblegum);
   -			cursor: pointer;
   -			color: var(--asphalt);
   -		}
   -
   -		.playlist-item:hover .playlist-item-artist {
   -			color: var(--asphalt);
   -		}
   -
   -		.playlist-item:hover .playlist-item-title.current,
   -		.playlist-item:hover .playlist-item-artist.current {
   -			color: var(--asphalt);
   -		}
   -
   -		.current-song {
   -			text-align: left;
   -			padding-left: 10px;
   -			font-weight: bold;
   -			color: var(--blueberry);
   -			min-height: 35px;
   -			display: flex;
   -			align-items: center;
   -		}
   -
   -		.current-song span {
   -			white-space: nowrap;
   -			overflow: hidden;
   -			text-overflow: ellipsis;
   -		}
   -
   -		#audioPlayer {
   -			display: none;
   -		}
   -
   -		.progress-container {
   -			width: 100%;
   -			font-size: 24px;
   -			cursor: grab;
   -			margin-top: 3px;
   -			height: 35px;
   -			display: flex;
   -			align-items: center;
   -			padding: 0 12px;
   -			box-sizing: border-box;
   -			position: relative;
   -		}
   -
   -		.progress-container:hover {
   -			cursor: grab;
   -		}
   -
   -		.progress-bar {
   -			width: 100%;
   -			height: 4px;
   -			background-color: var(--icee);
   -			position: relative;
   -			border-radius: 1px;
   -			--progress: 0%;
   -			--circle-size: 16px;
   -		}
   -
   -		.progress-bar::before {
   -			content: '';
   -			position: absolute;
   -			left: 0;
   -			top: 0;
   -			height: 100%;
   -			width: var(--progress);
   -			background-color: var(--bubblegum);
   -			border-radius: 1px;
   -			transition: none;
   -		}
   -
   -		.progress-bar::after {
   -			content: '';
   -			position: absolute;
   -			left: calc(var(--progress) - var(--circle-size) / 2);
   -			top: 50%;
   -			transform: translateY(-50%);
   -			width: var(--circle-size);
   -			height: var(--circle-size);
   -			background-color: var(--bubblegum);
   -			border-radius: 50%;
   -			box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
   -			transition: none;
   -		}
   -
   -		.progress-text {
   -			display: none;
   -		}
   -
   -		.progress-text-left {
   -			display: none;
   -		}
   -
   -		.progress-text-center {
   -			display: none;
   -		}
   -
   -		.control-button {
   -			width: 120px;
   -			height: 42px;
   -			background-size: contain;
   -			background-repeat: no-repeat;
   -			background-position: center;
   -			border: none;
   -			cursor: pointer;
   -			margin: 0 5px;
   -			border-radius: 4px;
   -		}
   -
   -		#playPause {
   -			background-image: url('resources/play.png');
   -		}
   -
   -		#playPause.pause {
   -			background-image: url('resources/pause.png');
   -		}
   -
   -		#prev {
   -			background-image: url('resources/prev.png');
   -		}
   -
   -		#next {
   -			background-image: url('resources/next.png');
   -		}
   -	</style>
   -
 8 +	<link rel="stylesheet" href="styles.css">
 9  </head>
10  <body>
11  	<div class="window-container">

28  
29  	<audio id="audioPlayer"></audio>
30  
   -	<script>
   -		const playPauseBtn = document.getElementById('playPause');
   -		const prevBtn = document.getElementById('prev');
   -		const nextBtn = document.getElementById('next');
   -		const playlist = document.getElementById('playlist');
   -		const currentSongDisplay = document.getElementById('currentSong');
   -		const progressBar = document.getElementById('progressBar');
   -		const progressContainer = document.getElementById('progressContainer');
   -		const audio = document.getElementById('audioPlayer');
   -
   -		let currentSongIndex = 0;
   -		let isPlaying = false;
   -		let progressInterval;
   -		let playerReady = false;
   -		let songs = [];
   -		let animationFrameId = null;
   -		let prePlaySeekTime = 0;
   -
   -		// Load tracks from tracks.json
   -		fetch('tracks.json')
   -			.then(response => {
   -				if (!response.ok) {
   -					throw new Error('tracks.json not found');
   -				}
   -				return response.json();
   -			})
   -			.then(data => {
   -				songs = data;
   -				if (songs.length > 0) {
   -					playerReady = true;
   -					updateCurrentSongDisplay(`Ready to play: ${songs[0].artist} - ${songs[0].title}`);
   -					renderPlaylist();
   -				} else {
   -					updateCurrentSongDisplay('No tracks found');
   -				}
   -			})
   -			.catch(error => {
   -				console.error('Error loading tracks:', error);
   -				updateCurrentSongDisplay('Error: tracks.json not found. Run scan.py first.');
   -			});
   -
   -		// Audio event listeners
   -		audio.addEventListener('play', () => {
   -			startProgressBar();
   -			const song = songs[currentSongIndex];
   -			updateCurrentSongDisplay(song.looping ?
   -				`Looping: ${song.artist} - ${song.title}` :
   -				`Now playing: ${song.artist} - ${song.title}`);
   -
   -			// Update media session metadata
   -			if ('mediaSession' in navigator) {
   -				navigator.mediaSession.metadata = new MediaMetadata({
   -					title: song.title,
   -					artist: song.artist
   -				});
   -			}
   -		});
   -
   -		audio.addEventListener('pause', () => {
   -			stopProgressBar();
   -			const song = songs[currentSongIndex];
   -			updateCurrentSongDisplay(`Paused: ${song.artist} - ${song.title}`);
   -		});
   -
   -		audio.addEventListener('ended', () => {
   -			if (songs[currentSongIndex].looping) {
   -				audio.currentTime = 0;
   -				audio.play();
   -			} else {
   -				nextSong();
   -			}
   -		});
   -
   -		audio.addEventListener('error', (e) => {
   -			console.error('Audio error:', e);
   -			updateCurrentSongDisplay(`Error loading: ${songs[currentSongIndex].filename}`);
   -			// Try next song after a brief delay
   -			setTimeout(() => nextSong(), 1000);
   -		});
   -
   -		audio.addEventListener('loadedmetadata', () => {
   -			resetProgressBar();
   -		});
   -
   -		function renderPlaylist() {
   -			playlist.innerHTML = '';
   -			const currentDisplayText = currentSongDisplay.textContent;
   -			const isInitialized = currentDisplayText !== 'No song playing';
   -
   -			songs.forEach((song, index) => {
   -				const item = document.createElement('div');
   -				item.classList.add('playlist-item');
   -
   -				const contentDiv = document.createElement('div');
   -				contentDiv.classList.add('playlist-item-content');
   -
   -				const titleDiv = document.createElement('div');
   -				titleDiv.classList.add('playlist-item-title');
   -				if (isInitialized && index === currentSongIndex) {
   -					titleDiv.classList.add('current');
   -				}
   -				titleDiv.textContent = song.title;
   -
   -				const artistDiv = document.createElement('div');
   -				artistDiv.classList.add('playlist-item-artist');
   -				if (isInitialized && index === currentSongIndex) {
   -					artistDiv.classList.add('current');
   -				}
   -				artistDiv.textContent = song.artist;
   -
   -				contentDiv.appendChild(titleDiv);
   -				contentDiv.appendChild(artistDiv);
   -
   -				const loopIcon = document.createElement('span');
   -				loopIcon.textContent = '🔁';
   -				loopIcon.style.display = (song.looping || false) ? 'inline' : 'none';
   -
   -				item.appendChild(contentDiv);
   -				item.appendChild(loopIcon);
   -				item.addEventListener('click', () => toggleLooping(index));
   -				playlist.appendChild(item);
   -			});
   -		}
   -
   -		function toggleLooping(index) {
   -			if (!playerReady) return;
   -			if (index === currentSongIndex) {
   -				// If it's ready to play but not playing yet, just play it
   -				if (!isPlaying && currentSongDisplay.textContent.includes('Ready to play')) {
   -					audio.play();
   -					isPlaying = true;
   -					updatePlayPauseButton();
   -					return;
   -				}
   -				// Toggle looping
   -				songs[index].looping = !(songs[index].looping || false);
   -				renderPlaylist();
   -
   -				const song = songs[index];
   -				// Update the display text based on current state
   -				if (isPlaying) {
   -					updateCurrentSongDisplay(song.looping ?
   -						`Looping: ${song.artist} - ${song.title}` :
   -						`Now playing: ${song.artist} - ${song.title}`);
   -				} else {
   -					updateCurrentSongDisplay(`Paused: ${song.artist} - ${song.title}`);
   -				}
   -			} else {
   -				playSong(index);
   -			}
   -		}
   -
   -		function playSong(index) {
   -			if (!playerReady) return;
   -			// Clear looping from all songs except the new one if it was already looping
   -			const wasLooping = songs[index].looping || false;
   -			songs.forEach(song => song.looping = false);
   -			if (wasLooping) {
   -				songs[index].looping = true;
   -			}
   -
   -			currentSongIndex = index;
   -			const song = songs[currentSongIndex];
   -			audio.src = `tracks/${song.filename}`;
   -			audio.play();
   -			isPlaying = true;
   -			updatePlayPauseButton();
   -			renderPlaylist();
   -		}
   -
   -		function updateCurrentSongDisplay(text) {
   -			currentSongDisplay.innerHTML = `<span>${text}</span>`;
   -		}
   -
   -		function togglePlayPause() {
   -			if (!playerReady) return;
   -			if (isPlaying) {
   -				audio.pause();
   -				isPlaying = false;
   -			} else {
   -				// If no song is loaded, load the first one
   -				if (!audio.src || audio.src === '') {
   -					playSong(currentSongIndex);
   -				} else {
   -					audio.play();
   -					isPlaying = true;
   -				}
   -			}
   -			updatePlayPauseButton();
   -		}
   -
   -		function updatePlayPauseButton() {
   -			playPauseBtn.classList.toggle('pause', isPlaying);
   -		}
   -
   -		function nextSong() {
   -			if (!playerReady) return;
   -			currentSongIndex = (currentSongIndex + 1) % songs.length;
   -			playSong(currentSongIndex);
   -		}
   -
   -		function prevSong() {
   -			if (!playerReady) return;
   -			if (audio.currentTime <= 3) {
   -				currentSongIndex = (currentSongIndex - 1 + songs.length) % songs.length;
   -				playSong(currentSongIndex);
   -			} else {
   -				audio.currentTime = 0;
   -			}
   -		}
   -
   -		function startProgressBar() {
   -			stopProgressBar();
   -
   -			function animate() {
   -				updateProgressBar();
   -				animationFrameId = requestAnimationFrame(animate);
   -			}
   -
   -			animate();
   -		}
   -
   -		function stopProgressBar() {
   -			if (animationFrameId !== null) {
   -				cancelAnimationFrame(animationFrameId);
   -				animationFrameId = null;
   -			}
   -		}
   -
   -		function resetProgressBar() {
   -			progressBar.style.setProperty('--progress', '0%');
   -		}
   -
   -		function updateProgressBar() {
   -			if (audio.duration && !isDragging) {
   -				const currentTime = audio.currentTime;
   -				const duration = audio.duration;
   -				const progressPercentage = (currentTime / duration) * 100;
   -				const displayPercentage = isNaN(progressPercentage) ? 0 : progressPercentage;
   -
   -				progressBar.style.setProperty('--progress', `${displayPercentage}%`);
   -			}
   -		}
   -
   -		let isDragging = false;
   -		let wasPlayingBeforeDrag = false;
   -		let pendingSeekPercentage = null;
   -
   -		function updateVisualProgress(event) {
   -			if (!playerReady) return;
   -
   -			const rect = progressBar.getBoundingClientRect();
   -			const clickPosition = event.clientX - rect.left;
   -			const clickPercentage = Math.max(0, Math.min(1, clickPosition / rect.width));
   -
   -			progressBar.style.setProperty('--progress', `${clickPercentage * 100}%`);
   -			return clickPercentage;
   -		}
   -
   -		function applySeek(clickPercentage) {
   -			if (!playerReady) return;
   -
   -			// If audio hasn't been loaded yet, load it but don't play
   -			if (!audio.src || audio.src === '') {
   -				const song = songs[currentSongIndex];
   -				audio.src = `tracks/${song.filename}`;
   -
   -				// Wait for metadata to be loaded before seeking
   -				audio.addEventListener('loadedmetadata', function setInitialTime() {
   -					const duration = audio.duration;
   -					const seekTime = duration * clickPercentage;
   -					audio.currentTime = seekTime;
   -					prePlaySeekTime = seekTime;
   -					audio.removeEventListener('loadedmetadata', setInitialTime);
   -				}, { once: true });
   -			} else if (audio.duration) {
   -				const duration = audio.duration;
   -				const seekTime = duration * clickPercentage;
   -				audio.currentTime = seekTime;
   -				prePlaySeekTime = seekTime;
   -			}
   -		}
   -
   -		function onProgressMouseDown(event) {
   -			if (!playerReady) return;
   -			isDragging = true;
   -			wasPlayingBeforeDrag = isPlaying;
   -			progressContainer.style.cursor = 'grabbing';
   -			document.body.style.cursor = 'grabbing';
   -			pendingSeekPercentage = updateVisualProgress(event);
   -			event.preventDefault();
   -		}
   -
   -		function onProgressMouseMove(event) {
   -			if (isDragging) {
   -				pendingSeekPercentage = updateVisualProgress(event);
   -			}
   -		}
   -
   -		function onProgressMouseUp(event) {
   -			if (isDragging) {
   -				isDragging = false;
   -				progressContainer.style.cursor = '';
   -				document.body.style.cursor = '';
   -
   -				// Apply the seek now that drag is complete
   -				if (pendingSeekPercentage !== null) {
   -					applySeek(pendingSeekPercentage);
   -					pendingSeekPercentage = null;
   -				}
   -
   -				// If it was "Ready to play" (not playing before), start playing now
   -				if (!wasPlayingBeforeDrag && !isPlaying && audio.src) {
   -					audio.play();
   -					isPlaying = true;
   -					updatePlayPauseButton();
   -				}
   -			}
   -		}
   -
   -		playPauseBtn.addEventListener('click', togglePlayPause);
   -		nextBtn.addEventListener('click', nextSong);
   -		prevBtn.addEventListener('click', prevSong);
   -		progressContainer.addEventListener('mousedown', onProgressMouseDown);
   -		document.addEventListener('mousemove', onProgressMouseMove);
   -		document.addEventListener('mouseup', onProgressMouseUp);
   -
   -		// Keyboard controls
   -		document.addEventListener('keydown', function(event) {
   -			if (!playerReady) return;
   -
   -			// Spacebar: play/pause
   -			if (event.code === 'Space') {
   -				event.preventDefault();
   -				togglePlayPause();
   -			}
   -		});
   -
   -		// Media key controls
   -		navigator.mediaSession.metadata = new MediaMetadata({
   -			title: 'vibe capsule',
   -			artist: 'MP3 Player'
   -		});
   -
   -		navigator.mediaSession.setActionHandler('play', () => {
   -			if (playerReady && !isPlaying) {
   -				togglePlayPause();
   -			}
   -		});
   -
   -		navigator.mediaSession.setActionHandler('pause', () => {
   -			if (playerReady && isPlaying) {
   -				togglePlayPause();
   -			}
   -		});
   -
   -		navigator.mediaSession.setActionHandler('previoustrack', () => {
   -			if (playerReady) {
   -				prevSong();
   -			}
   -		});
   -
   -		navigator.mediaSession.setActionHandler('nexttrack', () => {
   -			if (playerReady) {
   -				nextSong();
   -			}
   -		});
   -	</script>
31 +	<script src="script.js"></script>
32  </body>
33  </html>
script.js
  1 +const playPauseBtn = document.getElementById('playPause');
  2 +const prevBtn = document.getElementById('prev');
  3 +const nextBtn = document.getElementById('next');
  4 +const playlist = document.getElementById('playlist');
  5 +const currentSongDisplay = document.getElementById('currentSong');
  6 +const progressBar = document.getElementById('progressBar');
  7 +const progressContainer = document.getElementById('progressContainer');
  8 +const audio = document.getElementById('audioPlayer');
  9 +
 10 +let currentSongIndex = 0;
 11 +let isPlaying = false;
 12 +let progressInterval;
 13 +let playerReady = false;
 14 +let songs = [];
 15 +let animationFrameId = null;
 16 +let prePlaySeekTime = 0;
 17 +
 18 +// Load tracks from tracks.json
 19 +fetch('tracks.json')
 20 +	.then(response => {
 21 +		if (!response.ok) {
 22 +			throw new Error('tracks.json not found');
 23 +		}
 24 +		return response.json();
 25 +	})
 26 +	.then(data => {
 27 +		songs = data;
 28 +		if (songs.length > 0) {
 29 +			playerReady = true;
 30 +			updateCurrentSongDisplay(`Ready to play: ${songs[0].artist} - ${songs[0].title}`);
 31 +			renderPlaylist();
 32 +		} else {
 33 +			updateCurrentSongDisplay('No tracks found');
 34 +		}
 35 +	})
 36 +	.catch(error => {
 37 +		console.error('Error loading tracks:', error);
 38 +		updateCurrentSongDisplay('Error: tracks.json not found. Run scan.py first.');
 39 +	});
 40 +
 41 +// Audio event listeners
 42 +audio.addEventListener('play', () => {
 43 +	startProgressBar();
 44 +	const song = songs[currentSongIndex];
 45 +	updateCurrentSongDisplay(song.looping ?
 46 +		`Looping: ${song.artist} - ${song.title}` :
 47 +		`Now playing: ${song.artist} - ${song.title}`);
 48 +
 49 +	// Update media session metadata
 50 +	if ('mediaSession' in navigator) {
 51 +		navigator.mediaSession.metadata = new MediaMetadata({
 52 +			title: song.title,
 53 +			artist: song.artist
 54 +		});
 55 +	}
 56 +});
 57 +
 58 +audio.addEventListener('pause', () => {
 59 +	stopProgressBar();
 60 +	const song = songs[currentSongIndex];
 61 +	updateCurrentSongDisplay(`Paused: ${song.artist} - ${song.title}`);
 62 +});
 63 +
 64 +audio.addEventListener('ended', () => {
 65 +	if (songs[currentSongIndex].looping) {
 66 +		audio.currentTime = 0;
 67 +		audio.play();
 68 +	} else {
 69 +		nextSong();
 70 +	}
 71 +});
 72 +
 73 +audio.addEventListener('error', (e) => {
 74 +	console.error('Audio error:', e);
 75 +	updateCurrentSongDisplay(`Error loading: ${songs[currentSongIndex].filename}`);
 76 +	// Try next song after a brief delay
 77 +	setTimeout(() => nextSong(), 1000);
 78 +});
 79 +
 80 +audio.addEventListener('loadedmetadata', () => {
 81 +	resetProgressBar();
 82 +});
 83 +
 84 +function renderPlaylist() {
 85 +	playlist.innerHTML = '';
 86 +	const currentDisplayText = currentSongDisplay.textContent;
 87 +	const isInitialized = currentDisplayText !== 'No song playing';
 88 +
 89 +	songs.forEach((song, index) => {
 90 +		const item = document.createElement('div');
 91 +		item.classList.add('playlist-item');
 92 +
 93 +		const contentDiv = document.createElement('div');
 94 +		contentDiv.classList.add('playlist-item-content');
 95 +
 96 +		const titleDiv = document.createElement('div');
 97 +		titleDiv.classList.add('playlist-item-title');
 98 +		if (isInitialized && index === currentSongIndex) {
 99 +			titleDiv.classList.add('current');
100 +		}
101 +		titleDiv.textContent = song.title;
102 +
103 +		const artistDiv = document.createElement('div');
104 +		artistDiv.classList.add('playlist-item-artist');
105 +		if (isInitialized && index === currentSongIndex) {
106 +			artistDiv.classList.add('current');
107 +		}
108 +		artistDiv.textContent = song.artist;
109 +
110 +		contentDiv.appendChild(titleDiv);
111 +		contentDiv.appendChild(artistDiv);
112 +
113 +		const loopIcon = document.createElement('span');
114 +		loopIcon.textContent = '🔁';
115 +		loopIcon.style.display = (song.looping || false) ? 'inline' : 'none';
116 +
117 +		item.appendChild(contentDiv);
118 +		item.appendChild(loopIcon);
119 +		item.addEventListener('click', () => toggleLooping(index));
120 +		playlist.appendChild(item);
121 +	});
122 +}
123 +
124 +function toggleLooping(index) {
125 +	if (!playerReady) return;
126 +	if (index === currentSongIndex) {
127 +		if (!isPlaying && currentSongDisplay.textContent.includes('Ready to play')) {
128 +			audio.play();
129 +			isPlaying = true;
130 +			updatePlayPauseButton();
131 +			return;
132 +		}
133 +		// Toggle looping
134 +		songs[index].looping = !(songs[index].looping || false);
135 +		renderPlaylist();
136 +
137 +		const song = songs[index];
138 +		// Update the display text based on current state
139 +		if (isPlaying) {
140 +			updateCurrentSongDisplay(song.looping ?
141 +				`Looping: ${song.artist} - ${song.title}` :
142 +				`Now playing: ${song.artist} - ${song.title}`);
143 +		} else {
144 +			updateCurrentSongDisplay(`Paused: ${song.artist} - ${song.title}`);
145 +		}
146 +	} else {
147 +		playSong(index);
148 +	}
149 +}
150 +
151 +function playSong(index) {
152 +	if (!playerReady) return;
153 +	// Clear looping from all songs except the new one if it was already looping
154 +	const wasLooping = songs[index].looping || false;
155 +	songs.forEach(song => song.looping = false);
156 +	if (wasLooping) {
157 +		songs[index].looping = true;
158 +	}
159 +
160 +	currentSongIndex = index;
161 +	const song = songs[currentSongIndex];
162 +	audio.src = `tracks/${song.filename}`;
163 +	audio.play();
164 +	isPlaying = true;
165 +	updatePlayPauseButton();
166 +	renderPlaylist();
167 +}
168 +
169 +function updateCurrentSongDisplay(text) {
170 +	currentSongDisplay.innerHTML = `<span>${text}</span>`;
171 +}
172 +
173 +function togglePlayPause() {
174 +	if (!playerReady) return;
175 +	if (isPlaying) {
176 +		audio.pause();
177 +		isPlaying = false;
178 +	} else {
179 +		// If no song is loaded, load the first one
180 +		if (!audio.src || audio.src === '') {
181 +			playSong(currentSongIndex);
182 +		} else {
183 +			audio.play();
184 +			isPlaying = true;
185 +		}
186 +	}
187 +	updatePlayPauseButton();
188 +}
189 +
190 +function updatePlayPauseButton() {
191 +	playPauseBtn.classList.toggle('pause', isPlaying);
192 +}
193 +
194 +function nextSong() {
195 +	if (!playerReady) return;
196 +	currentSongIndex = (currentSongIndex + 1) % songs.length;
197 +	playSong(currentSongIndex);
198 +}
199 +
200 +function prevSong() {
201 +	if (!playerReady) return;
202 +	if (audio.currentTime <= 3) {
203 +		currentSongIndex = (currentSongIndex - 1 + songs.length) % songs.length;
204 +		playSong(currentSongIndex);
205 +	} else {
206 +		audio.currentTime = 0;
207 +	}
208 +}
209 +
210 +function startProgressBar() {
211 +	stopProgressBar();
212 +
213 +	function animate() {
214 +		updateProgressBar();
215 +		animationFrameId = requestAnimationFrame(animate);
216 +	}
217 +
218 +	animate();
219 +}
220 +
221 +function stopProgressBar() {
222 +	if (animationFrameId !== null) {
223 +		cancelAnimationFrame(animationFrameId);
224 +		animationFrameId = null;
225 +	}
226 +}
227 +
228 +function resetProgressBar() {
229 +	progressBar.style.setProperty('--progress', '0%');
230 +}
231 +
232 +function updateProgressBar() {
233 +	if (audio.duration && !isDragging) {
234 +		const currentTime = audio.currentTime;
235 +		const duration = audio.duration;
236 +		const progressPercentage = (currentTime / duration) * 100;
237 +		const displayPercentage = isNaN(progressPercentage) ? 0 : progressPercentage;
238 +
239 +		progressBar.style.setProperty('--progress', `${displayPercentage}%`);
240 +	}
241 +}
242 +
243 +let isDragging = false;
244 +let wasPlayingBeforeDrag = false;
245 +let pendingSeekPercentage = null;
246 +
247 +function updateVisualProgress(event) {
248 +	if (!playerReady) return;
249 +
250 +	const rect = progressBar.getBoundingClientRect();
251 +	const clickPosition = event.clientX - rect.left;
252 +	const clickPercentage = Math.max(0, Math.min(1, clickPosition / rect.width));
253 +
254 +	progressBar.style.setProperty('--progress', `${clickPercentage * 100}%`);
255 +	return clickPercentage;
256 +}
257 +
258 +function applySeek(clickPercentage) {
259 +	if (!playerReady) return;
260 +
261 +	// If audio hasn't been loaded yet, load it but don't play
262 +	if (!audio.src || audio.src === '') {
263 +		const song = songs[currentSongIndex];
264 +		audio.src = `tracks/${song.filename}`;
265 +
266 +		// Wait for metadata to be loaded before seeking
267 +		audio.addEventListener('loadedmetadata', function setInitialTime() {
268 +			const duration = audio.duration;
269 +			const seekTime = duration * clickPercentage;
270 +			audio.currentTime = seekTime;
271 +			prePlaySeekTime = seekTime;
272 +			audio.removeEventListener('loadedmetadata', setInitialTime);
273 +		}, { once: true });
274 +	} else if (audio.duration) {
275 +		const duration = audio.duration;
276 +		const seekTime = duration * clickPercentage;
277 +		audio.currentTime = seekTime;
278 +		prePlaySeekTime = seekTime;
279 +	}
280 +}
281 +
282 +function onProgressMouseDown(event) {
283 +	if (!playerReady) return;
284 +	isDragging = true;
285 +	wasPlayingBeforeDrag = isPlaying;
286 +	progressContainer.style.cursor = 'grabbing';
287 +	document.body.style.cursor = 'grabbing';
288 +	pendingSeekPercentage = updateVisualProgress(event);
289 +	event.preventDefault();
290 +}
291 +
292 +function onProgressMouseMove(event) {
293 +	if (isDragging) {
294 +		pendingSeekPercentage = updateVisualProgress(event);
295 +	}
296 +}
297 +
298 +function onProgressMouseUp(event) {
299 +	if (isDragging) {
300 +		isDragging = false;
301 +		progressContainer.style.cursor = '';
302 +		document.body.style.cursor = '';
303 +
304 +		// Apply the seek now that drag is complete
305 +		if (pendingSeekPercentage !== null) {
306 +			applySeek(pendingSeekPercentage);
307 +			pendingSeekPercentage = null;
308 +		}
309 +
310 +		// If it was "Ready to play" (not playing before), start playing now
311 +		if (!wasPlayingBeforeDrag && !isPlaying && audio.src) {
312 +			audio.play();
313 +			isPlaying = true;
314 +			updatePlayPauseButton();
315 +		}
316 +	}
317 +}
318 +
319 +playPauseBtn.addEventListener('click', togglePlayPause);
320 +nextBtn.addEventListener('click', nextSong);
321 +prevBtn.addEventListener('click', prevSong);
322 +progressContainer.addEventListener('mousedown', onProgressMouseDown);
323 +document.addEventListener('mousemove', onProgressMouseMove);
324 +document.addEventListener('mouseup', onProgressMouseUp);
325 +
326 +// Keyboard controls
327 +document.addEventListener('keydown', function(event) {
328 +	if (!playerReady) return;
329 +
330 +	// Spacebar: play/pause
331 +	if (event.code === 'Space') {
332 +		event.preventDefault();
333 +		togglePlayPause();
334 +	}
335 +});
336 +
337 +// Media key controls
338 +navigator.mediaSession.metadata = new MediaMetadata({
339 +	title: 'vibe capsule',
340 +	artist: 'MP3 Player'
341 +});
342 +
343 +navigator.mediaSession.setActionHandler('play', () => {
344 +	if (playerReady && !isPlaying) {
345 +		togglePlayPause();
346 +	}
347 +});
348 +
349 +navigator.mediaSession.setActionHandler('pause', () => {
350 +	if (playerReady && isPlaying) {
351 +		togglePlayPause();
352 +	}
353 +});
354 +
355 +navigator.mediaSession.setActionHandler('previoustrack', () => {
356 +	if (playerReady) {
357 +		prevSong();
358 +	}
359 +});
360 +
361 +navigator.mediaSession.setActionHandler('nexttrack', () => {
362 +	if (playerReady) {
363 +		nextSong();
364 +	}
365 +});
styles.css
  1 +:root {
  2 +	--icee: #dddddd;
  3 +	--blueberry: #4c7ae6;
  4 +	--bubblegum: #f421ff;
  5 +	--asphalt: #080a0c;
  6 +	--fog: #ddddddcc;
  7 +}
  8 +
  9 +* {
 10 +	scrollbar-color: var(--fog) var(--asphalt);
 11 +}
 12 +
 13 +body {
 14 +	margin: 0;
 15 +	padding: 0;
 16 +	font-family: "MS Sans Serif", Arial, sans-serif;
 17 +	height: 100vh;
 18 +	display: flex;
 19 +	flex-direction: column;
 20 +	font-size: 24px;
 21 +	background-color: var(--asphalt);
 22 +}
 23 +
 24 +.window-container {
 25 +	background: transparent;
 26 +	height: 100%;
 27 +	display: flex;
 28 +	flex-direction: column;
 29 +}
 30 +
 31 +.player-container {
 32 +	flex-grow: 1;
 33 +	display: flex;
 34 +	flex-direction: column;
 35 +	overflow: hidden;
 36 +	position: relative;
 37 +}
 38 +
 39 +.controls {
 40 +	display: flex;
 41 +	justify-content: center;
 42 +	padding-bottom: 10px;
 43 +	height: 52px;
 44 +	background: transparent;
 45 +	position: absolute;
 46 +	bottom: 0;
 47 +	left: 0;
 48 +	right: 0;
 49 +	box-sizing: border-box;
 50 +	z-index: 3;
 51 +}
 52 +
 53 +.playlist {
 54 +	flex-grow: 1;
 55 +	overflow-y: auto;
 56 +	padding-bottom: 62px;
 57 +	cursor: initial;
 58 +}
 59 +
 60 +.playlist-item {
 61 +	padding: 5px 10px;
 62 +	display: flex;
 63 +	justify-content: space-between;
 64 +	align-items: center;
 65 +	color: var(--icee);
 66 +	min-height: 45px;
 67 +}
 68 +
 69 +.playlist-item-content {
 70 +	flex-grow: 1;
 71 +	display: flex;
 72 +	flex-direction: column;
 73 +	gap: 2px;
 74 +}
 75 +
 76 +.playlist-item-title {
 77 +	font-size: 20px;
 78 +	line-height: 1.2;
 79 +}
 80 +
 81 +.playlist-item-title.current {
 82 +	font-weight: bold;
 83 +	color: var(--blueberry);
 84 +}
 85 +
 86 +.playlist-item-artist {
 87 +	font-size: 16px;
 88 +	color: var(--fog);
 89 +	line-height: 1.2;
 90 +}
 91 +
 92 +.playlist-item-artist.current {
 93 +	font-weight: bold;
 94 +	color: var(--blueberry);
 95 +	opacity: 1;
 96 +}
 97 +
 98 +.playlist-item:hover {
 99 +	background-color: var(--bubblegum);
100 +	cursor: pointer;
101 +	color: var(--asphalt);
102 +}
103 +
104 +.playlist-item:hover .playlist-item-artist {
105 +	color: var(--asphalt);
106 +}
107 +
108 +.playlist-item:hover .playlist-item-title.current,
109 +.playlist-item:hover .playlist-item-artist.current {
110 +	color: var(--asphalt);
111 +}
112 +
113 +.current-song {
114 +	text-align: left;
115 +	padding-left: 10px;
116 +	font-weight: bold;
117 +	color: var(--blueberry);
118 +	min-height: 35px;
119 +	display: flex;
120 +	align-items: center;
121 +}
122 +
123 +.current-song span {
124 +	white-space: nowrap;
125 +	overflow: hidden;
126 +	text-overflow: ellipsis;
127 +}
128 +
129 +#audioPlayer {
130 +	display: none;
131 +}
132 +
133 +.progress-container {
134 +	width: 100%;
135 +	font-size: 24px;
136 +	cursor: grab;
137 +	margin-top: 3px;
138 +	height: 35px;
139 +	display: flex;
140 +	align-items: center;
141 +	padding: 0 12px;
142 +	box-sizing: border-box;
143 +	position: relative;
144 +}
145 +
146 +.progress-container:hover {
147 +	cursor: grab;
148 +}
149 +
150 +.progress-bar {
151 +	width: 100%;
152 +	height: 4px;
153 +	background-color: var(--icee);
154 +	position: relative;
155 +	border-radius: 1px;
156 +	--progress: 0%;
157 +	--circle-size: 16px;
158 +}
159 +
160 +.progress-bar::before {
161 +	content: '';
162 +	position: absolute;
163 +	left: 0;
164 +	top: 0;
165 +	height: 100%;
166 +	width: var(--progress);
167 +	background-color: var(--bubblegum);
168 +	border-radius: 1px;
169 +	transition: none;
170 +}
171 +
172 +.progress-bar::after {
173 +	content: '';
174 +	position: absolute;
175 +	left: calc(var(--progress) - var(--circle-size) / 2);
176 +	top: 50%;
177 +	transform: translateY(-50%);
178 +	width: var(--circle-size);
179 +	height: var(--circle-size);
180 +	background-color: var(--bubblegum);
181 +	border-radius: 50%;
182 +	box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
183 +	transition: none;
184 +}
185 +
186 +.progress-text {
187 +	display: none;
188 +}
189 +
190 +.progress-text-left {
191 +	display: none;
192 +}
193 +
194 +.progress-text-center {
195 +	display: none;
196 +}
197 +
198 +.control-button {
199 +	width: 120px;
200 +	height: 42px;
201 +	background-size: contain;
202 +	background-repeat: no-repeat;
203 +	background-position: center;
204 +	border: none;
205 +	cursor: pointer;
206 +	margin: 0 5px;
207 +	border-radius: 4px;
208 +}
209 +
210 +#playPause {
211 +	background-image: url('resources/play.png');
212 +}
213 +
214 +#playPause.pause {
215 +	background-image: url('resources/pause.png');
216 +}
217 +
218 +#prev {
219 +	background-image: url('resources/prev.png');
220 +}
221 +
222 +#next {
223 +	background-image: url('resources/next.png');
224 +}