split out html, css, and js
Hunter
Tuesday, October 28th, 2025 at 9:59 AM
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>
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 +});
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 +}