soundwave-htmx / script.js
PsyEyes's picture
Давай сделаем красивый музыкальный плеер на HTMX и Tailwind визуально похожий на Spotify. Что нужно:
7421366 verified
// Music Player State
const playerState = {
isPlaying: false,
currentTrackIndex: -1,
volume: 80,
progress: 0,
tracks: [],
draggedElement: null
};
// DOM Elements
const elements = {
selectFolderBtn: document.getElementById('select-folder-btn'),
folderInput: document.getElementById('folder-input'),
tracksList: document.getElementById('tracks-list'),
playPauseBtn: document.getElementById('play-pause-btn'),
prevBtn: document.getElementById('prev-btn'),
nextBtn: document.getElementById('next-btn'),
progressBar: document.getElementById('progress-bar'),
volumeSlider: document.getElementById('volume-slider'),
currentTime: document.getElementById('current-time'),
totalTime: document.getElementById('total-time'),
currentTrackTitle: document.getElementById('current-track-title'),
currentTrackArtist: document.getElementById('current-track-artist'),
shuffleBtn: document.getElementById('shuffle-btn'),
repeatBtn: document.getElementById('repeat-btn')
};
// Initialize the application
document.addEventListener('DOMContentLoaded', () => {
setupEventListeners();
updatePlayerUI();
});
// Set up event listeners
function setupEventListeners() {
// Folder selection
elements.selectFolderBtn.addEventListener('click', () => {
elements.folderInput.click();
});
elements.folderInput.addEventListener('change', handleFolderSelection);
// Player controls
elements.playPauseBtn.addEventListener('click', togglePlayPause);
elements.prevBtn.addEventListener('click', playPreviousTrack);
elements.nextBtn.addEventListener('click', playNextTrack);
elements.progressBar.addEventListener('input', handleProgressChange);
elements.volumeSlider.addEventListener('input', handleVolumeChange);
// Shuffle and repeat
elements.shuffleBtn.addEventListener('click', toggleShuffle);
elements.repeatBtn.addEventListener('click', toggleRepeat);
// Keyboard shortcuts
document.addEventListener('keydown', handleKeyboardShortcuts);
}
// Handle folder selection
function handleFolderSelection(event) {
const files = Array.from(event.target.files);
const audioFiles = files.filter(file => file.type.startsWith('audio/'));
if (audioFiles.length === 0) {
alert('No audio files found in the selected folder.');
return;
}
playerState.tracks = audioFiles.map((file, index) => ({
id: index,
title: file.name.replace(/\.[^/.]+$/, ""), // Remove extension
artist: 'Unknown Artist',
album: 'Unknown Album',
duration: '0:00',
file: file,
url: URL.createObjectURL(file)
}));
renderPlaylist();
updatePlayerUI();
}
// Render playlist
function renderPlaylist() {
elements.tracksList.innerHTML = '';
playerState.tracks.forEach((track, index) => {
const trackElement = document.createElement('div');
trackElement.className = 'track-item grid grid-cols-12 gap-4 p-3 items-center cursor-pointer';
trackElement.draggable = true;
trackElement.dataset.index = index;
trackElement.innerHTML = `
<div class="col-span-1 flex items-center">
<span class="track-number">${index + 1}</span>
<button class="play-button hidden ml-2 text-green-500">
<i data-feather="play"></i>
</button>
</div>
<div class="col-span-5 truncate">${track.title}</div>
<div class="col-span-3 truncate text-gray-400">${track.album}</div>
<div class="col-span-2 text-gray-400">${track.duration}</div>
<div class="col-span-1 flex justify-end">
<button class="more-button text-gray-400 hover:text-white">
<i data-feather="more-horizontal"></i>
</button>
</div>
`;
// Add drag and drop events
trackElement.addEventListener('dragstart', handleDragStart);
trackElement.addEventListener('dragover', handleDragOver);
trackElement.addEventListener('dragenter', handleDragEnter);
trackElement.addEventListener('dragleave', handleDragLeave);
trackElement.addEventListener('drop', handleDrop);
trackElement.addEventListener('dragend', handleDragEnd);
// Add click event to play track
trackElement.addEventListener('click', (e) => {
if (!e.target.closest('.more-button') && !e.target.closest('.play-button')) {
playTrack(index);
}
});
elements.tracksList.appendChild(trackElement);
});
feather.replace();
}
// Drag and Drop Functions
function handleDragStart(e) {
playerState.draggedElement = this;
setTimeout(() => {
this.classList.add('dragging');
}, 0);
}
function handleDragOver(e) {
e.preventDefault();
}
function handleDragEnter(e) {
e.preventDefault();
this.classList.add('drag-over');
}
function handleDragLeave() {
this.classList.remove('drag-over');
}
function handleDrop(e) {
e.preventDefault();
this.classList.remove('drag-over');
if (playerState.draggedElement !== this) {
const draggedIndex = parseInt(playerState.draggedElement.dataset.index);
const targetIndex = parseInt(this.dataset.index);
// Reorder tracks array
const draggedTrack = playerState.tracks[draggedIndex];
playerState.tracks.splice(draggedIndex, 1);
playerState.tracks.splice(targetIndex, 0, draggedTrack);
// Update indices
playerState.tracks.forEach((track, index) => {
track.id = index;
});
// Update current track index if needed
if (playerState.currentTrackIndex === draggedIndex) {
playerState.currentTrackIndex = targetIndex;
} else if (playerState.currentTrackIndex === targetIndex) {
playerState.currentTrackIndex = draggedIndex;
}
renderPlaylist();
}
}
function handleDragEnd() {
this.classList.remove('dragging');
playerState.draggedElement = null;
document.querySelectorAll('.track-item').forEach(item => {
item.classList.remove('drag-over', 'drag-over-after');
});
}
// Player Control Functions
function togglePlayPause() {
playerState.isPlaying = !playerState.isPlaying;
updatePlayPauseButton();
if (playerState.isPlaying && playerState.currentTrackIndex === -1 && playerState.tracks.length > 0) {
playTrack(0);
}
}
function playTrack(index) {
if (index < 0 || index >= playerState.tracks.length) return;
playerState.currentTrackIndex = index;
playerState.isPlaying = true;
const track = playerState.tracks[index];
elements.currentTrackTitle.textContent = track.title;
elements.currentTrackArtist.textContent = track.artist;
updatePlayPauseButton();
renderPlaylist();
simulatePlayback();
}
function playNextTrack() {
if (playerState.tracks.length === 0) return;
let nextIndex = playerState.currentTrackIndex + 1;
if (nextIndex >= playerState.tracks.length) {
nextIndex = 0; // Loop to beginning
}
playTrack(nextIndex);
}
function playPreviousTrack() {
if (playerState.tracks.length === 0) return;
let prevIndex = playerState.currentTrackIndex - 1;
if (prevIndex < 0) {
prevIndex = playerState.tracks.length - 1; // Loop to end
}
playTrack(prevIndex);
}
function handleProgressChange() {
playerState.progress = parseInt(elements.progressBar.value);
updateTimeDisplay();
}
function handleVolumeChange() {
playerState.volume = parseInt(elements.volumeSlider.value);
elements.volumeSlider.style.background = `linear-gradient(to right, var(--primary-color) 0%, var(--primary-color) ${playerState.volume}%, rgba(255,255,255,0.3) ${playerState.volume}%, rgba(255,255,255,0.3) 100%)`;
}
function toggleShuffle() {
elements.shuffleBtn.classList.toggle('text-green-500');
}
function toggleRepeat() {
elements.repeatBtn.classList.toggle('text-green-500');
}
// Update UI Functions
function updatePlayPauseButton() {
const icon = elements.playPauseBtn.querySelector('i');
if (playerState.isPlaying) {
icon.setAttribute('data-feather', 'pause');
} else {
icon.setAttribute('data-feather', 'play');
}
feather.replace();
}
function updateTimeDisplay() {
// In a real app, this would be based on actual track time
const totalSeconds = 210; // Example: 3:30
const currentSeconds = Math.floor((playerState.progress / 100) * totalSeconds);
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
};
elements.currentTime.textContent = formatTime(currentSeconds);
elements.totalTime.textContent = formatTime(totalSeconds);
}
function updatePlayerUI() {
// Update volume slider background
elements.volumeSlider.style.background = `linear-gradient(to right, var(--primary-color) 0%, var(--primary-color) ${playerState.volume}%, rgba(255,255,255,0.3) ${playerState.volume}%, rgba(255,255,255,0.3) 100%)`;
// Update progress bar
elements.progressBar.value = playerState.progress;
updateTimeDisplay();
}
// Simulate playback progress
function simulatePlayback() {
if (!playerState.isPlaying) return;
const interval = setInterval(() => {
if (!playerState.isPlaying) {
clearInterval(interval);
return;
}
playerState.progress += 0.5;
if (playerState.progress >= 100) {
playerState.progress = 0;
playNextTrack();
}
updatePlayerUI();
}, 100);
}
// Keyboard Shortcuts
function handleKeyboardShortcuts(e) {
// Spacebar to play/pause
if (e.code === 'Space') {
e.preventDefault();
togglePlayPause();
}
// Left arrow for previous track
if (e.code === 'ArrowLeft') {
playPreviousTrack();
}
// Right arrow for next track
if (e.code === 'ArrowRight') {
playNextTrack();
}
}