Implement comprehensive Mobile UX overhaul: portrait layout, haptics, swipes, and touch optimization

This commit is contained in:
ComputerTech 2026-01-18 20:00:13 +00:00
parent 405efb6472
commit 69cdbd5b3b
3 changed files with 217 additions and 4 deletions

View File

@ -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 -->

View File

@ -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
View File

@ -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 */