您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Transform Soundgasm player with a modern interface, reliable download, and copyable tags.
// ==UserScript== // @name Soundgasm Enhanced Player and Downloadeder // @namespace http://tampermonkey.net/ // @version 1.0 // @description Transform Soundgasm player with a modern interface, reliable download, and copyable tags. // @author You // @match https://soundgasm.net/u/*/* // @grant none // @run-at document-ready // @license mit // ==/UserScript== (function() { 'use strict'; let audioUrl = ''; let originalDescription = ''; let extractedTags = []; let actualTitle = ''; function init() { addCustomStyles(); const checkForAudio = setInterval(() => { const audioElement = document.querySelector('audio'); if (audioElement && audioElement.src) { clearInterval(checkForAudio); extractContentInfo(); createEnhancedPlayer(); } }, 500); } function extractContentInfo() { // Extract audio URL const audioElement = document.querySelector('audio'); if (audioElement && audioElement.src) { audioUrl = audioElement.src; } // Extract actual title from the correct element const titleElement = document.querySelector('#jp_container_1 .jp-title ul li'); if (titleElement) { actualTitle = titleElement.textContent.trim(); } else { actualTitle = document.title.replace(' : Soundgasm', '') || 'Audio Player'; } // Extract description from the correct element const descriptionElement = document.querySelector('#jp_container_1 .jp-description'); if (descriptionElement) { originalDescription = descriptionElement.textContent || descriptionElement.innerText || ''; } // Extract tags from both title and description const titleTags = extractTags(actualTitle); const descriptionTags = extractTags(originalDescription); extractedTags = [...new Set([...titleTags, ...descriptionTags])]; } function extractTags(text) { const tagRegex = /\[([^\]]+)\]/g; const tags = []; let match; while ((match = tagRegex.exec(text)) !== null) { tags.push(match[1].trim()); } return tags; } function addCustomStyles() { const style = document.createElement('style'); style.textContent = ` * { box-sizing: border-box; } body { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important; margin: 0 !important; padding: 20px !important; min-height: 100vh !important; color: #ffffff !important; overflow-x: hidden !important; } audio { display: none !important; } .enhanced-player-container { max-width: 900px; margin: 0 auto; background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(25px); border-radius: 24px; padding: 40px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); animation: slideUp 0.8s cubic-bezier(0.16, 1, 0.3, 1); } @keyframes slideUp { from { opacity: 0; transform: translateY(50px); } to { opacity: 1; transform: translateY(0); } } .player-title { text-align: center; margin-bottom: 40px; padding-bottom: 24px; border-bottom: 2px solid rgba(255, 255, 255, 0.15); } .player-title h1 { font-size: 2.8em !important; font-weight: 700 !important; margin: 0 0 12px 0 !important; background: linear-gradient(135deg, #ffffff, #e0e0e0); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: none !important; line-height: 1.2 !important; } .player-subtitle { font-size: 1.3em; opacity: 0.85; margin: 0 !important; font-weight: 400; } /* Interactive Waveform */ .waveform-container { height: 100px; background: rgba(0, 0, 0, 0.3); border-radius: 16px; margin: 30px 0 20px 0; padding: 15px; position: relative; overflow: hidden; cursor: pointer; transition: all 0.3s ease; } .waveform-container:hover { background: rgba(0, 0, 0, 0.4); transform: translateY(-2px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); } .waveform { display: flex; align-items: center; justify-content: space-between; height: 100%; gap: 2px; } .wave-bar { width: 4px; background: linear-gradient(to top, #667eea, #764ba2); border-radius: 2px; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; opacity: 0.6; } .wave-bar:hover { opacity: 1; transform: scaleY(1.1); } .wave-bar.active { background: linear-gradient(to top, #ff6b6b, #ffd93d); opacity: 1; box-shadow: 0 0 8px rgba(255, 107, 107, 0.5); } .wave-bar.playing { animation: pulse 1.5s ease-in-out infinite; } @keyframes pulse { 0%, 100% { transform: scaleY(1); opacity: 0.8; } 50% { transform: scaleY(1.3); opacity: 1; } } /* Seek Bar */ .progress-section { margin: 20px 0 30px 0; } .progress-bar-container { position: relative; height: 8px; background: rgba(255, 255, 255, 0.2); border-radius: 12px; cursor: pointer; margin: 20px 0; overflow: hidden; box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2); } .progress-bar-container:hover { height: 10px; margin: 19px 0; } .progress-bar { height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 50%, #ff6b6b 100%); border-radius: 12px; width: 0%; transition: width 0.1s linear; position: relative; box-shadow: 0 0 12px rgba(102, 126, 234, 0.4); } .progress-bar::after { content: ''; position: absolute; right: -8px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; background: white; border-radius: 50%; box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3); opacity: 0; transition: all 0.3s ease; } .progress-bar-container:hover .progress-bar::after { opacity: 1; transform: translateY(-50%) scale(1.2); } .time-display { display: flex; justify-content: space-between; font-size: 15px; font-weight: 500; opacity: 0.9; margin-top: 12px; } /* Control Buttons - Main Controls */ .main-controls { display: flex; align-items: center; justify-content: center; margin: 40px 0 30px 0; gap: 24px; } .control-btn { width: 64px; height: 64px; border-radius: 50%; border: none; background: rgba(255, 255, 255, 0.15); color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: 600; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(10px); border: 2px solid rgba(255, 255, 255, 0.2); position: relative; overflow: hidden; } .control-btn::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); transition: left 0.5s ease; } .control-btn:hover::before { left: 100%; } .control-btn:hover { background: rgba(255, 255, 255, 0.25); transform: translateY(-4px) scale(1.05); box-shadow: 0 12px 25px rgba(0, 0, 0, 0.3); border-color: rgba(255, 255, 255, 0.4); } .control-btn:active { transform: translateY(-2px) scale(0.98); } .play-btn { width: 88px !important; height: 88px !important; font-size: 36px !important; background: linear-gradient(135deg, #667eea, #764ba2) !important; box-shadow: 0 15px 35px rgba(102, 126, 234, 0.4); border: 2px solid rgba(255, 255, 255, 0.3) !important; } .play-btn:hover { background: linear-gradient(135deg, #5a6fd8, #6b42a0) !important; box-shadow: 0 20px 40px rgba(102, 126, 234, 0.6); transform: translateY(-6px) scale(1.08) !important; } /* Secondary Controls */ .secondary-controls { display: flex; align-items: center; justify-content: center; margin: 20px 0 30px 0; gap: 20px; } .secondary-btn { width: 56px; height: 56px; border-radius: 50%; border: none; background: rgba(255, 255, 255, 0.1); color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 20px; transition: all 0.3s ease; backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.15); opacity: 0.8; } .secondary-btn:hover { background: rgba(255, 255, 255, 0.2); transform: translateY(-3px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25); opacity: 1; } .btn-active { background: rgba(102, 126, 234, 0.7) !important; box-shadow: 0 0 15px rgba(102, 126, 234, 0.5); color: #ffffff !important; } /* Volume Section - More compact */ .volume-section { display: flex; align-items: center; justify-content: center; gap: 15px; margin: 25px 0; padding: 15px 20px; background: rgba(0, 0, 0, 0.15); border-radius: 12px; } .volume-slider { width: 150px; height: 4px; border-radius: 2px; background: rgba(255, 255, 255, 0.2); outline: none; cursor: pointer; -webkit-appearance: none; position: relative; } .volume-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: linear-gradient(135deg, #667eea, #764ba2); cursor: pointer; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); transition: all 0.3s ease; } .volume-slider::-webkit-slider-thumb:hover { transform: scale(1.2); box-shadow: 0 3px 10px rgba(102, 126, 234, 0.5); } .volume-icon { font-size: 20px; opacity: 0.9; cursor: pointer; transition: all 0.3s ease; min-width: 24px; } .volume-icon:hover { opacity: 1; transform: scale(1.1); } .volume-value { font-size: 14px; font-weight: 600; min-width: 40px; text-align: center; opacity: 0.8; } /* Tags Section - Below volume */ .tags-container { margin: 25px 0; background: rgba(0, 0, 0, 0.15); border-radius: 16px; padding: 20px; border: 1px solid rgba(255, 255, 255, 0.1); } .tags-title { font-size: 1.2em; font-weight: 600; margin-bottom: 12px; color: #ffffff; opacity: 0.9; } .tags-list { display: flex; flex-wrap: wrap; gap: 8px; } .tag { background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 6px 14px; border-radius: 16px; font-size: 13px; font-weight: 500; border: 1px solid rgba(255, 255, 255, 0.2); transition: all 0.3s ease; cursor: pointer; /* MODIFIED: Indicate clickable tag */ user-select: none; /* MODIFIED: Prevent text selection on click */ } .tag:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); background: linear-gradient(135deg, #5a6fd8, #6b42a0); } /* Description Section - Below tags */ .description-container { margin: 25px 0; background: rgba(0, 0, 0, 0.15); border-radius: 16px; padding: 20px; border: 1px solid rgba(255, 255, 255, 0.1); } .description-title { font-size: 1.2em; font-weight: 600; margin-bottom: 12px; color: #ffffff; opacity: 0.9; } .description-content { background: rgba(255, 255, 255, 0.05); padding: 16px; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.1); line-height: 1.6; font-size: 14px; max-height: 100px; overflow: hidden; transition: all 0.3s ease; position: relative; } .description-content.expanded { max-height: 500px; overflow-y: auto; } .description-content::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 30px; background: linear-gradient(transparent, rgba(0, 0, 0, 0.3)); pointer-events: none; opacity: 1; transition: opacity 0.3s ease; } .description-content.expanded::after { opacity: 0; } .expand-btn { background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; padding: 8px 16px; border-radius: 20px; font-size: 12px; font-weight: 500; cursor: pointer; margin-top: 10px; transition: all 0.3s ease; border: 1px solid rgba(255, 255, 255, 0.2); } .expand-btn:hover { background: linear-gradient(135deg, #5a6fd8, #6b42a0); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); } /* Info Panel */ .info-panel { background: rgba(0, 0, 0, 0.15); border-radius: 12px; padding: 20px; margin-top: 25px; border: 1px solid rgba(255, 255, 255, 0.1); } .info-row { display: flex; justify-content: space-between; align-items: center; margin: 8px 0; padding: 6px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .info-row:last-child { border-bottom: none; } .info-label { opacity: 0.8; font-weight: 500; font-size: 14px; } .info-value { font-weight: 600; font-size: 14px; color: #ffffff; } /* Responsive Design */ @media (max-width: 768px) { body { padding: 10px !important; } .enhanced-player-container { padding: 25px; margin: 5px; } .player-title h1 { font-size: 2.2em !important; } .main-controls { gap: 18px; } .control-btn { width: 56px; height: 56px; font-size: 20px; } .play-btn { width: 76px !important; height: 76px !important; font-size: 30px !important; } .secondary-controls { gap: 15px; } .secondary-btn { width: 48px; height: 48px; font-size: 18px; } .volume-section { flex-direction: column; gap: 12px; } .volume-slider { width: 180px; } .tags-list { justify-content: center; } } /* Loading States */ .loading { opacity: 0.7; pointer-events: none; } .loading .wave-bar { animation: loadingWave 1.5s ease-in-out infinite; } @keyframes loadingWave { 0%, 100% { transform: scaleY(0.3); opacity: 0.5; } 50% { transform: scaleY(1); opacity: 1; } } /* Scrollbar */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.1); border-radius: 4px; } ::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: linear-gradient(135deg, #5a6fd8, #6b42a0); } `; document.head.appendChild(style); } function createEnhancedPlayer() { if (!audioUrl) return; const userName = window.location.pathname.split('/')[2] || 'User'; // Clean title and description from tags for display const cleanTitle = actualTitle.replace(/\[[^\]]+\]/g, '').trim(); const cleanDescription = originalDescription.replace(/\[[^\]]+\]/g, '').trim(); const playerContainer = document.createElement('div'); playerContainer.className = 'enhanced-player-container'; playerContainer.innerHTML = ` <div class="player-title"> <h1>${cleanTitle}</h1> <div class="player-subtitle">by ${userName}</div> </div> <div class="waveform-container" id="waveformContainer"> <div class="waveform" id="waveform"> ${Array.from({length: 80}, (_, i) => ` <div class="wave-bar" data-index="${i}" style="height: ${Math.random() * 60 + 20}%;"></div> `).join('')} </div> </div> <div class="progress-section"> <div class="progress-bar-container" id="progressContainer"> <div class="progress-bar" id="progressBar"></div> </div> <div class="time-display"> <span id="currentTime">0:00</span> <span id="duration">0:00</span> </div> </div> <div class="main-controls"> <button class="control-btn" id="prevBtn" title="Previous (←)">⏮</button> <button class="control-btn play-btn" id="playPauseBtn" title="Play/Pause (Space)">▶</button> <button class="control-btn" id="nextBtn" title="Next (→)">⏭</button> </div> <div class="secondary-controls"> <button class="secondary-btn" id="shuffleBtn" title="Shuffle">🔀</button> <button class="secondary-btn" id="repeatBtn" title="Repeat">🔁</button> <button class="secondary-btn" id="downloadBtn" title="Download Audio">⬇</button> </div> <div class="volume-section"> <span class="volume-icon" id="volumeIcon">🔊</span> <input type="range" class="volume-slider" id="volumeSlider" min="0" max="100" value="50"> <span class="volume-value" id="volumeValue">50%</span> </div> ${extractedTags.length > 0 ? ` <div class="tags-container"> <div class="tags-title">Tags</div> <div class="tags-list"> ${extractedTags.map(tag => `<span class="tag">${tag}</span>`).join('')} </div> </div> ` : ''} ${cleanDescription ? ` <div class="description-container"> <div class="description-title">Description</div> <div class="description-content" id="descriptionContent"> ${cleanDescription.replace(/\n/g, '<br>')} </div> ${cleanDescription.length > 150 ? ` <button class="expand-btn" id="expandBtn">Show More</button> ` : ''} </div> ` : ''} <div class="info-panel"> <div class="info-row"> <span class="info-label">Status:</span> <span class="info-value" id="statusValue">Ready</span> </div> <div class="info-row"> <span class="info-label">Quality:</span> <span class="info-value" id="qualityValue">High</span> </div> <div class="info-row"> <span class="info-label">Format:</span> <span class="info-value" id="formatValue">MP3</span> </div> </div> `; document.body.innerHTML = ''; document.body.appendChild(playerContainer); const enhancedAudio = document.createElement('audio'); enhancedAudio.src = audioUrl; playerContainer.appendChild(enhancedAudio); initializePlayer(enhancedAudio); } function initializePlayer(audio) { // Get all controls const playPauseBtn = document.getElementById('playPauseBtn'); const progressBar = document.getElementById('progressBar'); const progressContainer = document.getElementById('progressContainer'); const currentTimeSpan = document.getElementById('currentTime'); const durationSpan = document.getElementById('duration'); const volumeSlider = document.getElementById('volumeSlider'); const volumeValue = document.getElementById('volumeValue'); const volumeIcon = document.getElementById('volumeIcon'); const statusValue = document.getElementById('statusValue'); const downloadBtn = document.getElementById('downloadBtn'); const waveformContainer = document.getElementById('waveformContainer'); const waveformBars = document.querySelectorAll('.wave-bar'); const expandBtn = document.getElementById('expandBtn'); const descriptionContent = document.getElementById('descriptionContent'); const shuffleBtn = document.getElementById('shuffleBtn'); const repeatBtn = document.getElementById('repeatBtn'); let isPlaying = false; let isDragging = false; let shuffleEnabled = false; let repeatEnabled = false; audio.volume = 0.5; // --- Player Logic --- function togglePlayPause() { if (isPlaying) { audio.pause(); } else { audio.play(); } } function updatePlayPauseUI(playing) { isPlaying = playing; playPauseBtn.innerHTML = isPlaying ? '⏸' : '▶'; statusValue.textContent = isPlaying ? 'Playing' : 'Paused'; if (isPlaying) { updateWaveformAnimation(); } else { waveformBars.forEach(bar => bar.classList.remove('playing')); } } function updateWaveform(progress) { const activeIndex = Math.floor((progress / 100) * waveformBars.length); waveformBars.forEach((bar, index) => { bar.classList.toggle('active', index <= activeIndex); }); } function updateWaveformAnimation() { const currentProgress = (audio.currentTime / audio.duration) * 100; const activeIndex = Math.floor((currentProgress / 100) * waveformBars.length); waveformBars.forEach((bar, index) => { bar.classList.toggle('playing', isPlaying && index === activeIndex); }); } function seek(e) { const rect = progressContainer.getBoundingClientRect(); const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); if (isFinite(audio.duration)) { audio.currentTime = ratio * audio.duration; } } // --- Event Listeners --- playPauseBtn.addEventListener('click', togglePlayPause); audio.addEventListener('play', () => updatePlayPauseUI(true)); audio.addEventListener('pause', () => updatePlayPauseUI(false)); audio.addEventListener('timeupdate', () => { if (!isDragging && isFinite(audio.duration)) { const progress = (audio.currentTime / audio.duration) * 100; progressBar.style.width = progress + '%'; currentTimeSpan.textContent = formatTime(audio.currentTime); updateWaveform(progress); updateWaveformAnimation(); } }); audio.addEventListener('loadedmetadata', () => { durationSpan.textContent = formatTime(audio.duration); }); audio.addEventListener('ended', () => { if (repeatEnabled) { audio.currentTime = 0; audio.play(); } else { updatePlayPauseUI(false); statusValue.textContent = 'Ended'; progressBar.style.width = '0%'; waveformBars.forEach(bar => bar.classList.remove('active', 'playing')); } }); ['loadstart', 'waiting'].forEach(event => audio.addEventListener(event, () => { statusValue.textContent = event === 'loadstart' ? 'Loading...' : 'Buffering...'; })); audio.addEventListener('canplay', () => statusValue.textContent = 'Ready'); audio.addEventListener('error', () => statusValue.textContent = 'Error'); progressContainer.addEventListener('mousedown', e => { isDragging = true; seek(e); }); document.addEventListener('mousemove', e => { if (isDragging) seek(e); }); document.addEventListener('mouseup', () => { isDragging = false; }); waveformContainer.addEventListener('click', (e) => { if (isFinite(audio.duration)) { const rect = waveformContainer.getBoundingClientRect(); const clickX = e.clientX - rect.left; const width = rect.width; const clickRatio = (clickX - 15) / (width - 30); const clampedRatio = Math.max(0, Math.min(1, clickRatio)); audio.currentTime = clampedRatio * audio.duration; } }); volumeSlider.addEventListener('input', (e) => { const volume = e.target.value / 100; audio.volume = volume; volumeValue.textContent = e.target.value + '%'; if (volume === 0) volumeIcon.textContent = '🔇'; else if (volume < 0.5) volumeIcon.textContent = '🔉'; else volumeIcon.textContent = '🔊'; }); volumeIcon.addEventListener('click', () => { if (audio.volume > 0) { audio.dataset.previousVolume = audio.volume; audio.volume = 0; volumeSlider.value = 0; } else { const previousVolume = parseFloat(audio.dataset.previousVolume) || 0.5; audio.volume = previousVolume; volumeSlider.value = previousVolume * 100; } volumeSlider.dispatchEvent(new Event('input')); }); document.getElementById('prevBtn').addEventListener('click', () => { audio.currentTime = Math.max(0, audio.currentTime - 10); }); document.getElementById('nextBtn').addEventListener('click', () => { audio.currentTime = Math.min(audio.duration, audio.currentTime + 10); }); shuffleBtn.addEventListener('click', () => { shuffleEnabled = !shuffleEnabled; shuffleBtn.classList.toggle('btn-active', shuffleEnabled); statusValue.textContent = `Shuffle ${shuffleEnabled ? 'ON' : 'OFF'}`; }); repeatBtn.addEventListener('click', () => { repeatEnabled = !repeatEnabled; repeatBtn.classList.toggle('btn-active', repeatEnabled); statusValue.textContent = `Repeat ${repeatEnabled ? 'ON' : 'OFF'}`; }); // --- NEW: RELIABLE DOWNLOAD --- downloadBtn.addEventListener('click', () => { if (!audioUrl) return; const originalText = downloadBtn.innerHTML; statusValue.textContent = 'Preparing download...'; downloadBtn.innerHTML = '⏬'; try { const link = document.createElement('a'); link.href = audioUrl; // Sanitize filename const fileName = (actualTitle.replace(/[\W_]+/g," ").trim() || 'audio') + '.mp3'; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); statusValue.textContent = 'Download started!'; } catch (error) { console.error('Download initiation failed:', error); statusValue.textContent = 'Download failed'; } finally { setTimeout(() => { downloadBtn.innerHTML = originalText; if (statusValue.textContent === 'Download started!') { statusValue.textContent = isPlaying ? 'Playing' : 'Ready'; } }, 2000); } }); if (expandBtn && descriptionContent) { expandBtn.addEventListener('click', () => { const isExpanded = descriptionContent.classList.toggle('expanded'); expandBtn.textContent = isExpanded ? 'Show Less' : 'Show More'; }); } // --- NEW: COPY TAGS TO CLIPBOARD --- const tagElements = document.querySelectorAll('.tag'); tagElements.forEach(tag => { tag.addEventListener('click', () => { const originalText = tag.textContent; navigator.clipboard.writeText(originalText).then(() => { const originalBg = tag.style.background; tag.textContent = 'Copied!'; tag.style.background = 'linear-gradient(135deg, #28a745, #218838)'; setTimeout(() => { tag.textContent = originalText; tag.style.background = originalBg; }, 1500); }).catch(err => { console.error('Failed to copy tag: ', err); }); }); }); document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; switch(e.code) { case 'Space': e.preventDefault(); togglePlayPause(); break; case 'ArrowLeft': e.preventDefault(); audio.currentTime = Math.max(0, audio.currentTime - 5); break; case 'ArrowRight': e.preventDefault(); audio.currentTime = Math.min(audio.duration, audio.currentTime + 5); break; case 'ArrowUp': e.preventDefault(); volumeSlider.value = Math.min(100, parseInt(volumeSlider.value) + 5); volumeSlider.dispatchEvent(new Event('input')); break; case 'ArrowDown': e.preventDefault(); volumeSlider.value = Math.max(0, parseInt(volumeSlider.value) - 5); volumeSlider.dispatchEvent(new Event('input')); break; case 'KeyM': e.preventDefault(); volumeIcon.click(); break; } }); // Initial animation for waveform setTimeout(() => { waveformBars.forEach((bar, i) => { bar.style.animationDelay = i * 0.02 + 's'; }); }, 500); } function formatTime(seconds) { if (!isFinite(seconds)) return '0:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();