Implement comprehensive Mobile UX overhaul: portrait layout, haptics, swipes, and touch optimization
This commit is contained in:
parent
405efb6472
commit
69cdbd5b3b
|
|
@ -21,7 +21,10 @@
|
||||||
<h2>OPTIMAL ORIENTATION</h2>
|
<h2>OPTIMAL ORIENTATION</h2>
|
||||||
<p>For the best DJ experience, please rotate your device to <strong>landscape mode</strong> (sideways).</p>
|
<p>For the best DJ experience, please rotate your device to <strong>landscape mode</strong> (sideways).</p>
|
||||||
<p style="font-size: 0.9rem; color: #888;">Both decks and the crossfader will be visible simultaneously.</p>
|
<p style="font-size: 0.9rem; color: #888;">Both decks and the crossfader will be visible simultaneously.</p>
|
||||||
<button onclick="dismissLandscapePrompt()">GOT IT</button>
|
<button class="btn-secondary" onclick="dismissLandscapePrompt()"
|
||||||
|
style="margin-top: 15px; padding: 10px 20px; width: 100%; border: 1px solid var(--primary-cyan); color: var(--primary-cyan); font-family: 'Orbitron', sans-serif; cursor: pointer;">
|
||||||
|
USE PORTRAIT MODE
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- MAIN APP CONTAINER -->
|
<!-- MAIN APP CONTAINER -->
|
||||||
|
|
@ -41,6 +44,10 @@
|
||||||
<span class="tab-icon">💿</span>
|
<span class="tab-icon">💿</span>
|
||||||
<span>DECK B</span>
|
<span>DECK B</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="tab-btn fullscreen-btn" onclick="toggleFullScreen()" id="fullscreen-toggle">
|
||||||
|
<span class="tab-icon">📺</span>
|
||||||
|
<span>FULL</span>
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- 1. LEFT: LIBRARY -->
|
<!-- 1. LEFT: LIBRARY -->
|
||||||
|
|
|
||||||
92
script.js
92
script.js
|
|
@ -355,26 +355,107 @@ function toggleDeck(id) {
|
||||||
function switchTab(tabId) {
|
function switchTab(tabId) {
|
||||||
const container = document.querySelector('.app-container');
|
const container = document.querySelector('.app-container');
|
||||||
const buttons = document.querySelectorAll('.tab-btn');
|
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');
|
container.classList.remove('show-library', 'show-deck-A', 'show-deck-B');
|
||||||
buttons.forEach(btn => btn.classList.remove('active'));
|
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
|
// Add active class and button state
|
||||||
container.classList.add('show-' + tabId);
|
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
|
// Find the button and activate it
|
||||||
buttons.forEach(btn => {
|
buttons.forEach(btn => {
|
||||||
if (btn.getAttribute('onclick').includes(tabId)) {
|
const onClickAttr = btn.getAttribute('onclick');
|
||||||
|
if (onClickAttr && onClickAttr.includes(tabId)) {
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redraw waveforms if switching to a deck
|
// Redraw waveforms if switching to a deck
|
||||||
if (tabId.startsWith('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);
|
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)
|
// Waveform Generation (Optimized for Speed)
|
||||||
|
|
@ -558,6 +639,7 @@ function formatTime(seconds) {
|
||||||
|
|
||||||
// Playback Logic
|
// Playback Logic
|
||||||
function playDeck(id) {
|
function playDeck(id) {
|
||||||
|
vibrate(15);
|
||||||
// Server-side audio mode
|
// Server-side audio mode
|
||||||
if (SERVER_SIDE_AUDIO) {
|
if (SERVER_SIDE_AUDIO) {
|
||||||
if (!socket) initSocket();
|
if (!socket) initSocket();
|
||||||
|
|
@ -606,6 +688,7 @@ function playDeck(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function pauseDeck(id) {
|
function pauseDeck(id) {
|
||||||
|
vibrate(15);
|
||||||
// Server-side audio mode
|
// Server-side audio mode
|
||||||
if (SERVER_SIDE_AUDIO) {
|
if (SERVER_SIDE_AUDIO) {
|
||||||
if (!socket) initSocket();
|
if (!socket) initSocket();
|
||||||
|
|
@ -787,6 +870,7 @@ function changeFilter(id, type, val) {
|
||||||
|
|
||||||
// Hot Cue Functionality
|
// Hot Cue Functionality
|
||||||
function handleCue(id, cueNum) {
|
function handleCue(id, cueNum) {
|
||||||
|
vibrate(15);
|
||||||
if (!decks[id].localBuffer) {
|
if (!decks[id].localBuffer) {
|
||||||
console.warn(`[Deck ${id}] No track loaded - cannot set cue`);
|
console.warn(`[Deck ${id}] No track loaded - cannot set cue`);
|
||||||
return;
|
return;
|
||||||
|
|
@ -840,6 +924,7 @@ function clearCue(id, cueNum) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLoop(id, action) {
|
function setLoop(id, action) {
|
||||||
|
vibrate(15);
|
||||||
if (!decks[id].localBuffer) {
|
if (!decks[id].localBuffer) {
|
||||||
console.warn(`[Deck ${id}] No track loaded - cannot set loop`);
|
console.warn(`[Deck ${id}] No track loaded - cannot set loop`);
|
||||||
return;
|
return;
|
||||||
|
|
@ -2414,6 +2499,7 @@ function monitorTrackEnd() {
|
||||||
monitorTrackEnd();
|
monitorTrackEnd();
|
||||||
// Reset Deck to Default Settings
|
// Reset Deck to Default Settings
|
||||||
function resetDeck(id) {
|
function resetDeck(id) {
|
||||||
|
vibrate(20);
|
||||||
console.log(`🔄 Resetting Deck ${id} to defaults...`);
|
console.log(`🔄 Resetting Deck ${id} to defaults...`);
|
||||||
|
|
||||||
if (!audioCtx) {
|
if (!audioCtx) {
|
||||||
|
|
|
||||||
120
style.css
120
style.css
|
|
@ -11,6 +11,10 @@
|
||||||
--glow-opacity: 0.3;
|
--glow-opacity: 0.3;
|
||||||
--glow-spread: 30px;
|
--glow-spread: 30px;
|
||||||
--glow-border: 5px;
|
--glow-border: 5px;
|
||||||
|
|
||||||
|
/* Mobile-specific adjustments */
|
||||||
|
--touch-target-size: 44px;
|
||||||
|
--touch-thumb-size: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
@ -3232,6 +3236,122 @@ body.listening-active .landscape-prompt {
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
opacity: 0.8;
|
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 */
|
/* Landscape orientation prompt */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue