commit d0652e8c630134f16c298249fae1f343f383b4c7
parent c88e84f47a6760976db32400344a64d7de2fd07f
Author: Hunter
Date: Tue, 28 Oct 2025 09:59:06 -0400
split out html, css, and js
Diffstat:
| M | index.html | | | 597 | +------------------------------------------------------------------------------ |
| A | script.js | | | 365 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | styles.css | | | 224 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
3 files changed, 591 insertions(+), 595 deletions(-)
diff --git a/index.html b/index.html
@@ -5,233 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>
-
+ <link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="window-container">
@@ -254,373 +28,6 @@
<audio id="audioPlayer"></audio>
- <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>
+ <script src="script.js"></script>
</body>
</html>
diff --git a/script.js b/script.js
@@ -0,0 +1,365 @@
+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 (!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();
+ }
+});
diff --git a/styles.css b/styles.css
@@ -0,0 +1,224 @@
+: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');
+}