diff --git a/index.html b/index.html index cd411c2..48fa3ac 100644 --- a/index.html +++ b/index.html @@ -21,7 +21,10 @@
For the best DJ experience, please rotate your device to landscape mode (sideways).
Both decks and the crossfader will be visible simultaneously.
- + @@ -41,6 +44,10 @@ DECK B + diff --git a/script.js b/script.js index 70c4af6..9d90f25 100644 --- a/script.js +++ b/script.js @@ -355,26 +355,107 @@ function toggleDeck(id) { function switchTab(tabId) { const container = document.querySelector('.app-container'); const buttons = document.querySelectorAll('.tab-btn'); + const sections = document.querySelectorAll('.library-section, .deck'); - // Remove all tab classes + // Remove all tab and active classes container.classList.remove('show-library', 'show-deck-A', 'show-deck-B'); buttons.forEach(btn => btn.classList.remove('active')); + sections.forEach(sec => sec.classList.remove('active')); + + // Normalize IDs (deck-A -> deckA for class) + const normalizedId = tabId.replace('-', ''); // Add active class and button state container.classList.add('show-' + tabId); + // Activate target section + const targetSection = document.getElementById(tabId) || document.querySelector('.' + tabId + '-section'); + if (targetSection) targetSection.classList.add('active'); + // Find the button and activate it buttons.forEach(btn => { - if (btn.getAttribute('onclick').includes(tabId)) { + const onClickAttr = btn.getAttribute('onclick'); + if (onClickAttr && onClickAttr.includes(tabId)) { btn.classList.add('active'); } }); // Redraw waveforms if switching to a deck if (tabId.startsWith('deck')) { - const id = tabId.split('-')[1]; + const id = tabId.includes('-') ? tabId.split('-')[1] : (tabId.includes('A') ? 'A' : 'B'); setTimeout(() => drawWaveform(id), 100); } + + // Haptic feedback + vibrate(10); +} + +function dismissLandscapePrompt() { + const prompt = document.getElementById('landscape-prompt'); + if (prompt) prompt.classList.add('dismissed'); + vibrate(10); +} + +// Mobile Haptic Helper +function vibrate(ms) { + if (navigator.vibrate) { + navigator.vibrate(ms); + } +} + +// Fullscreen Toggle +function toggleFullScreen() { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen().catch(err => { + console.error(`Error attempting to enable full-screen mode: ${err.message}`); + }); + document.getElementById('fullscreen-toggle').classList.add('active'); + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + } + document.getElementById('fullscreen-toggle').classList.remove('active'); + } + vibrate(20); +} + +// Touch Swiping Logic +let touchStartX = 0; +let touchEndX = 0; + +document.addEventListener('touchstart', e => { + touchStartX = e.changedTouches[0].screenX; +}, false); + +document.addEventListener('touchend', e => { + touchEndX = e.changedTouches[0].screenX; + handleSwipe(); +}, false); + +function handleSwipe() { + const threshold = 100; + const swipeDistance = touchEndX - touchStartX; + + // Get current tab + const activeBtn = document.querySelector('.tab-btn.active'); + if (!activeBtn) return; + + const tabs = ['library', 'deck-A', 'deck-B']; + let currentIndex = -1; + + if (activeBtn.getAttribute('onclick').includes('library')) currentIndex = 0; + else if (activeBtn.getAttribute('onclick').includes('deck-A')) currentIndex = 1; + else if (activeBtn.getAttribute('onclick').includes('deck-B')) currentIndex = 2; + + if (currentIndex === -1) return; + + if (swipeDistance > threshold) { + // Swipe Right (Go Left) + if (currentIndex > 0) switchTab(tabs[currentIndex - 1]); + } else if (swipeDistance < -threshold) { + // Swipe Left (Go Right) + if (currentIndex < tabs.length - 1) switchTab(tabs[currentIndex + 1]); + } } // Waveform Generation (Optimized for Speed) @@ -558,6 +639,7 @@ function formatTime(seconds) { // Playback Logic function playDeck(id) { + vibrate(15); // Server-side audio mode if (SERVER_SIDE_AUDIO) { if (!socket) initSocket(); @@ -606,6 +688,7 @@ function playDeck(id) { } function pauseDeck(id) { + vibrate(15); // Server-side audio mode if (SERVER_SIDE_AUDIO) { if (!socket) initSocket(); @@ -787,6 +870,7 @@ function changeFilter(id, type, val) { // Hot Cue Functionality function handleCue(id, cueNum) { + vibrate(15); if (!decks[id].localBuffer) { console.warn(`[Deck ${id}] No track loaded - cannot set cue`); return; @@ -840,6 +924,7 @@ function clearCue(id, cueNum) { } function setLoop(id, action) { + vibrate(15); if (!decks[id].localBuffer) { console.warn(`[Deck ${id}] No track loaded - cannot set loop`); return; @@ -2414,6 +2499,7 @@ function monitorTrackEnd() { monitorTrackEnd(); // Reset Deck to Default Settings function resetDeck(id) { + vibrate(20); console.log(`🔄 Resetting Deck ${id} to defaults...`); if (!audioCtx) { diff --git a/style.css b/style.css index 4abe5bf..c5b216f 100644 --- a/style.css +++ b/style.css @@ -11,6 +11,10 @@ --glow-opacity: 0.3; --glow-spread: 30px; --glow-border: 5px; + + /* Mobile-specific adjustments */ + --touch-target-size: 44px; + --touch-thumb-size: 28px; } body { @@ -3232,6 +3236,122 @@ body.listening-active .landscape-prompt { transform: scale(0.95); opacity: 0.8; } + + /* Thicker slider tracks for mobile */ + input[type=range] { + min-height: var(--touch-target-size); + } + + .volume-fader { + width: var(--touch-target-size) !important; + } + + /* Larger slider thumbs for touch */ + input[type=range]::-webkit-slider-thumb { + width: var(--touch-thumb-size) !important; + height: var(--touch-thumb-size) !important; + } + + input[type=range]::-moz-range-thumb { + width: var(--touch-thumb-size) !important; + height: var(--touch-thumb-size) !important; + } + + .xfader::-webkit-slider-thumb, + .volume-fader::-webkit-slider-thumb, + .filter-slider::-webkit-slider-thumb { + width: var(--touch-thumb-size) !important; + height: var(--touch-thumb-size) !important; + } +} + +/* Portrait Mobile Layout Stacking */ +@media (max-width: 1024px) and (orientation: portrait) { + .app-container { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; + } + + /* Hide non-active sections in portrait tabs */ + .library-section, + .deck, + .mixer-section { + display: none; + } + + .library-section.active, + .deck.active { + display: flex !important; + flex: 1; + width: 100%; + padding: 10px; + } + + /* Mixer section is usually docked at bottom in portrait */ + .mixer-section { + display: block !important; + height: 80px; + background: rgba(0, 0, 0, 0.4); + padding: 10px; + position: fixed; + bottom: 70px; + /* Above mobile tabs */ + left: 0; + right: 0; + z-index: 100; + backdrop-filter: blur(10px); + } + + .mobile-tabs { + bottom: 0; + height: 70px; + } + + /* Optimize deck layout for portrait */ + .deck { + flex-direction: column; + overflow-y: auto; + } + + .controls-grid { + grid-template-columns: 1fr 1fr; + gap: 10px; + } + + .disk-container { + height: 180px; + } + + .dj-disk { + width: 160px; + height: 160px; + } +} + +/* Landscape Mobile Tweaks */ +@media (max-width: 1024px) and (orientation: landscape) { + .app-container { + grid-template-columns: 1fr 1fr; + /* Two columns for decks */ + } + + .library-section { + display: none; + /* Only show in its own tab/overlay in landscape mobile if needed */ + } +} + +/* Fullscreen Button Styles */ +.fullscreen-btn { + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +.fullscreen-btn.active { + background: rgba(0, 255, 0, 0.2) !important; + box-shadow: 0 0 15px rgba(0, 255, 0, 0.4); } /* Landscape orientation prompt */