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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- MAIN APP CONTAINER -->
|
||||
|
|
@ -41,6 +44,10 @@
|
|||
<span class="tab-icon">💿</span>
|
||||
<span>DECK B</span>
|
||||
</button>
|
||||
<button class="tab-btn fullscreen-btn" onclick="toggleFullScreen()" id="fullscreen-toggle">
|
||||
<span class="tab-icon">📺</span>
|
||||
<span>FULL</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- 1. LEFT: LIBRARY -->
|
||||
|
|
|
|||
92
script.js
92
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) {
|
||||
|
|
|
|||
120
style.css
120
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 */
|
||||
|
|
|
|||
Loading…
Reference in New Issue