| | class MusicPlayer {
|
| | constructor() {
|
| | this.audio = new Audio();
|
| | this.currentTrackIndex = 0;
|
| | this.isPlaying = false;
|
| | this.isShuffled = false;
|
| | this.shuffledIndexes = [];
|
| |
|
| |
|
| | this.playBtn = document.getElementById('play-btn');
|
| | this.prevBtn = document.getElementById('prev-btn');
|
| | this.nextBtn = document.getElementById('next-btn');
|
| | this.shuffleBtn = document.getElementById('shuffle-btn');
|
| | this.volumeControl = document.getElementById('volume');
|
| | this.progressBar = document.getElementById('progress-bar');
|
| | this.progress = document.getElementById('progress');
|
| | this.currentTimeSpan = document.getElementById('current-time');
|
| | this.durationSpan = document.getElementById('duration');
|
| | this.currentTrackName = document.getElementById('current-track-name');
|
| | this.trackList = document.getElementById('track-list');
|
| | this.likedList = document.getElementById('liked-list');
|
| | this.tabButtons = document.querySelectorAll('.tab-btn');
|
| | this.searchInput = document.getElementById('search-input');
|
| | this.dropArea = document.getElementById('drop-area');
|
| | this.fileInput = document.getElementById('file-input');
|
| |
|
| | this.tracks = Array.from(this.trackList.getElementsByClassName('track-item'));
|
| | this.allTracks = [...this.tracks];
|
| | this.favoriteButtons = document.querySelectorAll('.favorite-btn');
|
| |
|
| | this.initEventListeners();
|
| | }
|
| |
|
| | initEventListeners() {
|
| |
|
| | this.searchInput.addEventListener('input', (e) => this.searchTracks(e.target.value));
|
| |
|
| |
|
| | this.playBtn.addEventListener('click', () => this.togglePlay());
|
| | this.prevBtn.addEventListener('click', () => this.playPrevious());
|
| | this.nextBtn.addEventListener('click', () => this.playNext());
|
| | this.shuffleBtn.addEventListener('click', () => this.toggleShuffle());
|
| |
|
| |
|
| | if (this.dropArea && this.fileInput) {
|
| |
|
| | ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
| | this.dropArea.addEventListener(eventName, (e) => {
|
| | e.preventDefault();
|
| | e.stopPropagation();
|
| | });
|
| | });
|
| |
|
| |
|
| | ['dragenter', 'dragover'].forEach(eventName => {
|
| | this.dropArea.addEventListener(eventName, () => {
|
| | this.dropArea.classList.add('highlight');
|
| | });
|
| | });
|
| |
|
| | ['dragleave', 'drop'].forEach(eventName => {
|
| | this.dropArea.addEventListener(eventName, () => {
|
| | this.dropArea.classList.remove('highlight');
|
| | });
|
| | });
|
| |
|
| |
|
| | this.dropArea.addEventListener('drop', (e) => {
|
| | const files = e.dataTransfer.files;
|
| | if (files.length) {
|
| | this.uploadFiles(files);
|
| | }
|
| | });
|
| |
|
| |
|
| | this.fileInput.addEventListener('change', (e) => {
|
| | const files = e.target.files;
|
| | if (files.length) {
|
| | this.uploadFiles(files);
|
| | }
|
| | });
|
| | }
|
| |
|
| |
|
| | this.tabButtons.forEach(button => {
|
| | button.addEventListener('click', () => {
|
| |
|
| | this.tabButtons.forEach(btn => btn.classList.remove('active'));
|
| | document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
| |
|
| |
|
| | button.classList.add('active');
|
| | const tabName = button.dataset.tab;
|
| | const content = document.querySelector(`.tab-content[data-tab="${tabName}"]`);
|
| | if (content) {
|
| | content.classList.add('active');
|
| |
|
| | this.tracks = Array.from(content.getElementsByClassName('track-item'));
|
| |
|
| | this.tracks.forEach((track, index) => {
|
| | track.addEventListener('click', (e) => {
|
| | if (!e.target.closest('.favorite-btn')) {
|
| | this.currentTrackIndex = index;
|
| | this.loadAndPlayTrack();
|
| | }
|
| | });
|
| | });
|
| | }
|
| | });
|
| | });
|
| |
|
| |
|
| | this.volumeControl.addEventListener('input', (e) => {
|
| | this.audio.volume = e.target.value;
|
| | });
|
| |
|
| |
|
| | this.progressBar.addEventListener('click', (e) => {
|
| | const rect = this.progressBar.getBoundingClientRect();
|
| | const percent = (e.clientX - rect.left) / rect.width;
|
| | this.audio.currentTime = percent * this.audio.duration;
|
| | });
|
| |
|
| |
|
| | this.audio.addEventListener('timeupdate', () => this.updateProgress());
|
| | this.audio.addEventListener('ended', () => this.playNext());
|
| |
|
| |
|
| | this.tracks.forEach((track, index) => {
|
| | track.addEventListener('click', (e) => {
|
| | if (!e.target.closest('.favorite-btn')) {
|
| | this.currentTrackIndex = index;
|
| | this.loadAndPlayTrack();
|
| | }
|
| | });
|
| | });
|
| |
|
| |
|
| | this.favoriteButtons.forEach(btn => {
|
| | btn.addEventListener('click', async (e) => {
|
| | e.preventDefault();
|
| | const track = btn.dataset.track;
|
| | const response = await fetch(`/favorite/${track}`, {
|
| | method: 'POST',
|
| | headers: {
|
| | 'Content-Type': 'application/json'
|
| | }
|
| | });
|
| |
|
| | const result = await response.json();
|
| | if (result.status === 'added') {
|
| | btn.classList.add('active');
|
| | } else {
|
| | btn.classList.remove('active');
|
| | }
|
| | });
|
| | });
|
| | }
|
| |
|
| | togglePlay() {
|
| | if (this.isPlaying) {
|
| | this.pause();
|
| | } else {
|
| | if (this.audio.src) {
|
| | this.play();
|
| | } else if (this.tracks.length > 0) {
|
| | this.loadAndPlayTrack();
|
| | }
|
| | }
|
| | }
|
| |
|
| | play() {
|
| | this.audio.play();
|
| | this.isPlaying = true;
|
| | this.playBtn.innerHTML = '<i class="fas fa-pause"></i>';
|
| | this.tracks[this.currentTrackIndex].classList.add('active');
|
| | }
|
| |
|
| | pause() {
|
| | this.audio.pause();
|
| | this.isPlaying = false;
|
| | this.playBtn.innerHTML = '<i class="fas fa-play"></i>';
|
| | }
|
| |
|
| | playPrevious() {
|
| | if (this.tracks.length === 0) return;
|
| | this.tracks[this.currentTrackIndex].classList.remove('active');
|
| |
|
| | if (this.isShuffled) {
|
| | const currentShuffleIndex = this.shuffledIndexes.indexOf(this.currentTrackIndex);
|
| | const prevShuffleIndex = (currentShuffleIndex - 1 + this.tracks.length) % this.tracks.length;
|
| | this.currentTrackIndex = this.shuffledIndexes[prevShuffleIndex];
|
| | } else {
|
| | this.currentTrackIndex = (this.currentTrackIndex - 1 + this.tracks.length) % this.tracks.length;
|
| | }
|
| |
|
| | this.loadAndPlayTrack();
|
| | }
|
| |
|
| | playNext() {
|
| | if (this.tracks.length === 0) return;
|
| | this.tracks[this.currentTrackIndex].classList.remove('active');
|
| |
|
| | if (this.isShuffled) {
|
| | const currentShuffleIndex = this.shuffledIndexes.indexOf(this.currentTrackIndex);
|
| | const nextShuffleIndex = (currentShuffleIndex + 1) % this.tracks.length;
|
| | this.currentTrackIndex = this.shuffledIndexes[nextShuffleIndex];
|
| | } else {
|
| | this.currentTrackIndex = (this.currentTrackIndex + 1) % this.tracks.length;
|
| | }
|
| |
|
| | this.loadAndPlayTrack();
|
| | }
|
| |
|
| | loadAndPlayTrack() {
|
| | if (!this.tracks || this.tracks.length === 0 || this.currentTrackIndex >= this.tracks.length) {
|
| | console.error('No tracks available or invalid track index');
|
| | return;
|
| | }
|
| |
|
| | const track = this.tracks[this.currentTrackIndex];
|
| | if (!track || !track.dataset.src) {
|
| | console.error('Invalid track data');
|
| | return;
|
| | }
|
| |
|
| | const trackSrc = track.dataset.src;
|
| | this.audio.src = trackSrc;
|
| | this.currentTrackName.textContent = track.textContent.trim();
|
| |
|
| |
|
| | document.querySelectorAll('.track-item').forEach(t => t.classList.remove('active'));
|
| |
|
| | track.classList.add('active');
|
| |
|
| | this.play();
|
| | }
|
| |
|
| | updateProgress() {
|
| | const duration = this.audio.duration;
|
| | const currentTime = this.audio.currentTime;
|
| |
|
| | if (duration) {
|
| |
|
| | const progressPercent = (currentTime / duration) * 100;
|
| | this.progress.style.width = progressPercent + '%';
|
| |
|
| |
|
| | this.currentTimeSpan.textContent = this.formatTime(currentTime);
|
| | this.durationSpan.textContent = this.formatTime(duration);
|
| | }
|
| | }
|
| |
|
| | formatTime(seconds) {
|
| | const minutes = Math.floor(seconds / 60);
|
| | const remainingSeconds = Math.floor(seconds % 60);
|
| | return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
| | }
|
| |
|
| | toggleShuffle() {
|
| | this.isShuffled = !this.isShuffled;
|
| |
|
| | if (this.isShuffled) {
|
| |
|
| | this.shuffleBtn.classList.add('active');
|
| |
|
| |
|
| | this.shuffledIndexes = Array.from({ length: this.tracks.length }, (_, i) => i);
|
| | this.shuffleArray(this.shuffledIndexes);
|
| |
|
| |
|
| | const currentIndex = this.shuffledIndexes.indexOf(this.currentTrackIndex);
|
| | if (currentIndex !== -1) {
|
| |
|
| | [this.shuffledIndexes[0], this.shuffledIndexes[currentIndex]] =
|
| | [this.shuffledIndexes[currentIndex], this.shuffledIndexes[0]];
|
| | }
|
| | } else {
|
| |
|
| | this.shuffleBtn.classList.remove('active');
|
| | }
|
| | }
|
| |
|
| | shuffleArray(array) {
|
| |
|
| | for (let i = array.length - 1; i > 0; i--) {
|
| | const j = Math.floor(Math.random() * (i + 1));
|
| | [array[i], array[j]] = [array[j], array[i]];
|
| | }
|
| | return array;
|
| | }
|
| |
|
| | searchTracks(query) {
|
| | query = query.toLowerCase();
|
| | const activeTab = document.querySelector('.tab-content.active');
|
| | const tracksList = activeTab.getElementsByClassName('track-item');
|
| |
|
| | Array.from(tracksList).forEach(track => {
|
| | const trackName = track.querySelector('.track-name').textContent.toLowerCase();
|
| | if (trackName.includes(query)) {
|
| | track.style.display = '';
|
| | } else {
|
| | track.style.display = 'none';
|
| | }
|
| | });
|
| | }
|
| |
|
| | async uploadFiles(files) {
|
| | const uploadList = document.getElementById('upload-list');
|
| | const uploadProgressContainer = document.getElementById('upload-progress-container');
|
| |
|
| | if (uploadProgressContainer) {
|
| | uploadProgressContainer.style.display = 'block';
|
| | }
|
| |
|
| | if (uploadList) {
|
| | uploadList.innerHTML = '';
|
| | }
|
| |
|
| |
|
| | for (const file of files) {
|
| |
|
| | if (!file.name.match(/\.(mp3|wav|m4a|webm)$/i)) {
|
| | this.showUploadStatus(file.name, 'Недопустимый формат файла', 'error');
|
| | continue;
|
| | }
|
| |
|
| |
|
| | const formData = new FormData();
|
| | formData.append('file', file);
|
| |
|
| | try {
|
| |
|
| | this.showUploadStatus(file.name, 'Загрузка...', 'loading');
|
| |
|
| |
|
| | const response = await fetch('/upload', {
|
| | method: 'POST',
|
| | body: formData
|
| | });
|
| |
|
| | const result = await response.json();
|
| |
|
| | if (result.status === 'success') {
|
| | this.showUploadStatus(file.name, 'Загружено успешно', 'success');
|
| |
|
| |
|
| | this.addTrackToList(result.filename);
|
| | } else {
|
| | this.showUploadStatus(file.name, result.message || 'Ошибка загрузки', 'error');
|
| | }
|
| | } catch (error) {
|
| | console.error('Ошибка загрузки файла:', error);
|
| | this.showUploadStatus(file.name, 'Ошибка загрузки', 'error');
|
| | }
|
| | }
|
| | }
|
| |
|
| | showUploadStatus(filename, message, status) {
|
| | const uploadList = document.getElementById('upload-list');
|
| | if (!uploadList) return;
|
| |
|
| | const statusItem = document.createElement('div');
|
| | statusItem.className = `upload-status ${status}`;
|
| | statusItem.innerHTML = `
|
| | <span class="filename">${filename}</span>
|
| | <span class="message">${message}</span>
|
| | `;
|
| |
|
| | uploadList.appendChild(statusItem);
|
| | }
|
| |
|
| | addTrackToList(filename) {
|
| |
|
| | const trackList = document.getElementById('track-list');
|
| | if (!trackList) return;
|
| |
|
| | const newTrack = document.createElement('li');
|
| | newTrack.className = 'track-item';
|
| | newTrack.dataset.src = `/play/${filename}`;
|
| |
|
| | newTrack.innerHTML = `
|
| | <span class="track-name">${filename}</span>
|
| | <button class="favorite-btn" data-track="${filename}">
|
| | <i class="fas fa-heart"></i>
|
| | </button>
|
| | `;
|
| |
|
| |
|
| | newTrack.addEventListener('click', (e) => {
|
| | if (!e.target.closest('.favorite-btn')) {
|
| | this.currentTrackIndex = this.tracks.length;
|
| | this.tracks.push(newTrack);
|
| | this.loadAndPlayTrack();
|
| | }
|
| | });
|
| |
|
| |
|
| | const favoriteBtn = newTrack.querySelector('.favorite-btn');
|
| | favoriteBtn.addEventListener('click', async (e) => {
|
| | e.preventDefault();
|
| | const track = favoriteBtn.dataset.track;
|
| | const response = await fetch(`/favorite/${track}`, {
|
| | method: 'POST',
|
| | headers: {
|
| | 'Content-Type': 'application/json'
|
| | }
|
| | });
|
| |
|
| | const result = await response.json();
|
| | if (result.status === 'added') {
|
| | favoriteBtn.classList.add('active');
|
| | } else {
|
| | favoriteBtn.classList.remove('active');
|
| | }
|
| | });
|
| |
|
| | trackList.appendChild(newTrack);
|
| |
|
| |
|
| | this.tracks = Array.from(trackList.getElementsByClassName('track-item'));
|
| | }
|
| | }
|
| |
|
| |
|
| | document.addEventListener('DOMContentLoaded', () => {
|
| | const player = new MusicPlayer();
|
| | }); |