Mobile UX improvements: Remove emojis, convert to British English, fix queue navigation and floating buttons

- Removed all emojis from UI (replaced with text labels)
- Converted all American English to British English (initialise, optimise, visualiser)
- Fixed mobile queue section visibility in portrait/landscape modes
- Added proper CSS rules for show-queue-A and show-queue-B classes
- Repositioned floating action buttons to prevent overlap with mobile tabs
- Added responsive button sizing for mobile (50px) and small screens (45px)
- Stacked buttons vertically on screens ≤480px
- Disabled all body::before border effects that were blocking UI
- Fixed crossfader width: 70% in portrait, 100% in landscape
- Removed unnecessary .md files (COMPARISON.md, PYQT5_FEATURES.md, QUICKSTART.md, README_PYQT5.md)
- Updated README.md with British English and removed emojis
This commit is contained in:
ComputerTech 2026-02-02 02:37:56 +00:00
parent c2085291c0
commit 6246b26925
8 changed files with 328 additions and 973 deletions

View File

@ -1,190 +0,0 @@
# TechDJ: Web vs Native Comparison
## Memory Usage Analysis
### Chrome Web Panel (~400MB)
```
Chrome Process Breakdown:
├── Browser Process: ~100MB
├── Renderer Process: ~150MB
│ ├── V8 JavaScript Engine: ~50MB
│ ├── Blink Rendering: ~40MB
│ ├── DOM + CSS: ~30MB
│ └── Web Audio Buffers: ~30MB
├── GPU Process: ~80MB
├── Audio Buffers (2 decks): ~100MB
│ └── Decoded PCM audio in memory
└── Network/Extensions: ~70MB
```
### PyQt5 Native App (~120-150MB)
```
PyQt5 Process Breakdown:
├── Python Runtime: ~30MB
├── PyQt5 Framework: ~40MB
├── Audio Engine (sounddevice): ~20MB
├── Cached Audio (streaming): ~30MB
│ └── Only active chunks in memory
├── UI Rendering: ~15MB
└── Libraries (numpy, etc.): ~15MB
```
## Performance Comparison
| Metric | Chrome Web | PyQt5 Native | Improvement |
|--------|------------|--------------|-------------|
| **RAM Usage** | ~400MB | ~120-150MB | **62% less** |
| **CPU Usage (idle)** | ~5-8% | ~1-2% | **75% less** |
| **CPU Usage (mixing)** | ~15-25% | ~8-12% | **50% less** |
| **Audio Latency** | 50-100ms | <10ms | **90% better** |
| **Startup Time** | 3-5s | 1-2s | **60% faster** |
| **Battery Impact** | High | Low | **~40% longer** |
## Feature Parity
| Feature | Web Panel | PyQt5 Native |
|---------|-----------|--------------|
| Dual Decks | ✅ | ✅ |
| Crossfader | ✅ | ✅ |
| Waveform Display | ✅ | ✅ |
| Hot Cues | ✅ | ✅ |
| Speed/Pitch Control | ✅ | ✅ |
| Volume Control | ✅ | ✅ |
| EQ (3-band) | ✅ | 🚧 (planned) |
| Filters (LP/HP) | ✅ | 🚧 (planned) |
| Loop Controls | ✅ | 🚧 (planned) |
| Auto-Loop | ✅ | 🚧 (planned) |
| Library Search | ✅ | ✅ |
| Mobile Support | ✅ | ❌ |
| Remote Access | ✅ | ❌ |
| Offline Mode | ❌ | ✅ (cached) |
| Broadcast Streaming | ✅ | 🚧 (planned) |
## Use Case Recommendations
### Use Chrome Web Panel When:
- 🌐 DJing remotely (different computer)
- 📱 Using mobile device (phone/tablet)
- 👥 Multiple DJs need access
- 🎧 Streaming to web listeners
- 💻 No installation permissions
- 🔄 Frequent updates/testing
### Use PyQt5 Native When:
- 💻 DJing on your laptop
- 🔋 Battery life is important
- ⚡ Low latency is critical
- 💾 RAM is limited (<8GB)
- 🎵 Offline DJing (cached songs)
- 🎮 Using MIDI controllers (future)
## Technical Details
### Audio Processing Architecture
**Web Panel (Web Audio API):**
```javascript
// Loads entire song into memory
audioCtx.decodeAudioData(arrayBuffer, (buffer) => {
// buffer contains full PCM data (~50MB for 5min song)
deck.localBuffer = buffer;
});
// Creates new source for each play
const source = audioCtx.createBufferSource();
source.buffer = deck.localBuffer; // Full song in RAM
source.connect(filters);
```
**PyQt5 Native (sounddevice):**
```python
# Streams audio in small chunks
def audio_callback(outdata, frames, time_info, status):
# Only processes 2048 frames at a time (~46ms)
chunk = audio_data[position:position+frames]
outdata[:] = chunk # Minimal memory usage
position += frames
```
### Memory Efficiency Example
**5-minute song (320kbps MP3):**
- File size: ~12MB (compressed)
- Chrome Web Audio: ~50MB (decoded PCM in RAM)
- PyQt5 Native: ~0.1MB (streaming chunks)
**With 2 decks loaded:**
- Chrome: ~100MB just for audio buffers
- PyQt5: ~0.2MB for active chunks
## Hybrid Setup (Best of Both Worlds)
You can run **both** simultaneously:
```
┌─────────────────────┐
│ PyQt5 Native App │ (Your laptop - low RAM)
│ (Local DJing) │
└─────────────────────┘
│ Broadcasts to
┌─────────────────────┐
│ Flask Server │ (Relay server)
│ (5000/5001) │
└─────────────────────┘
│ Streams to
┌─────────────────────┐
│ Web Listeners │ (Audience - any device)
│ (Mobile/Browser) │
└─────────────────────┘
```
**Benefits:**
- DJ uses native app (low memory, low latency)
- Listeners use web interface (easy access)
- Best performance for both use cases
## Installation Size Comparison
| Component | Size |
|-----------|------|
| Chrome Browser | ~200MB |
| PyQt5 + Dependencies | ~150MB |
| **Total for Web** | ~200MB |
| **Total for Native** | ~150MB |
## Conclusion
**For your laptop:** PyQt5 Native is the clear winner
- ✅ 62% less RAM usage (400MB → 150MB)
- ✅ 75% less CPU usage
- ✅ 90% lower audio latency
- ✅ Better battery life
- ✅ Works offline with cached songs
**Keep the web panel for:**
- Mobile DJing
- Remote sessions
- Listener streaming
- Quick access without installation
## Next Steps
1. **Try the PyQt5 app:**
```bash
./launch_qt.sh
```
2. **Compare memory usage:**
```bash
# Chrome
ps aux | grep chrome | awk '{sum+=$6} END {print sum/1024 " MB"}'
# PyQt5
ps aux | grep techdj_qt | awk '{print $6/1024 " MB"}'
```
3. **Test both side-by-side** and see the difference!

View File

@ -1,233 +0,0 @@
# TechDJ PyQt5 - Complete Feature List
## ✨ Now a Perfect Replica of the Web DJ Panel!
### 🎨 Visual Features (Matching Web Panel)
#### **Main Layout**
- ✅ 3-column grid (Library | Deck A | Deck B)
- ✅ Crossfader spanning both decks at bottom
- ✅ Exact color scheme (#0a0a12 background, #00f3ff cyan, #bc13fe magenta)
- ✅ Neon glow effects
- ✅ Orbitron and Rajdhani fonts
#### **Deck Components**
- ✅ Animated vinyl disks (rotating when playing)
- ✅ Waveform display with playhead
- ✅ Hot cues (4 per deck) with glow
- ✅ Loop controls (IN/OUT/EXIT)
- ✅ Volume faders
- ✅ 3-band EQ (HI/MID/LO) with vertical sliders
- ✅ Filters (Low-pass/High-pass)
- ✅ Speed/pitch control with bend buttons
- ✅ Transport buttons (PLAY/PAUSE/SYNC/RESET)
- ✅ Time display with current/total time
- ✅ Track name display
#### **Library**
- ✅ Cyan border and glow
- ✅ Search/filter box
- ✅ Track list with hover effects
- ✅ Refresh button
- ✅ Double-click to load
#### **Crossfader**
- ✅ Gradient (cyan → gray → magenta)
- ✅ Large handle
- ✅ "A" and "B" labels
- ✅ Metallic styling
---
### 🆕 NEW: Floating Action Buttons (Bottom-Right)
Just like the web panel, now has 4 floating buttons:
1. **📡 Streaming Button**
- Opens streaming panel
- Magenta neon glow
- Tooltip: "Live Streaming"
2. **⚙️ Settings Button**
- Opens settings panel
- Magenta neon glow
- Tooltip: "Settings"
3. **📁 Upload Button**
- Upload MP3 files to server
- File dialog integration
- Tooltip: "Upload MP3"
4. **⌨️ Keyboard Button**
- Keyboard shortcuts reference
- Tooltip: "Keyboard Shortcuts"
---
### 🆕 NEW: Streaming Panel
**Features:**
- ✅ Start/Stop broadcast button (red → green when live)
- ✅ Broadcast status indicator
- ✅ Listener count display (👂 with big number)
- ✅ Stream URL with copy button
- ✅ Auto-start on play checkbox
- ✅ Quality selector (128k/96k/64k/48k/32k)
- ✅ Connection to Flask server via Socket.IO
- ✅ Cyan border matching web panel
**Functionality:**
- Connects to Flask server on port 5000
- Sends broadcast start/stop commands
- Configurable bitrate
- Real-time listener count (when implemented)
---
### 🆕 NEW: Settings Panel
**Features:**
- ✅ Repeat Deck A/B checkboxes
- ✅ Auto-Crossfade checkbox
- ✅ Shuffle Library checkbox
- ✅ Quantize checkbox
- ✅ Auto-play next checkbox
- ✅ **✨ Glow Deck A (Cyan)** checkbox
- ✅ **✨ Glow Deck B (Magenta)** checkbox
- ✅ **✨ Glow Intensity** slider (1-100)
- ✅ Magenta border matching web panel
**Neon Glow Effects:**
- Toggle cyan glow for Deck A
- Toggle magenta glow for Deck B
- Adjustable intensity (1-100%)
- Real-time visual feedback
- Matches web panel's glow feature exactly!
---
### 🎯 Feature Comparison
| Feature | Web Panel | PyQt5 Native | Status |
|---------|-----------|--------------|--------|
| **UI & Layout** |
| Dual Decks | ✅ | ✅ | ✅ Complete |
| Crossfader | ✅ | ✅ | ✅ Complete |
| Library | ✅ | ✅ | ✅ Complete |
| Waveforms | ✅ | ✅ | ✅ Complete |
| Vinyl Disks | ✅ | ✅ | ✅ Complete |
| Neon Colors | ✅ | ✅ | ✅ Complete |
| **Controls** |
| Hot Cues | ✅ | ✅ | ✅ Complete |
| Loop Controls | ✅ | ✅ | ✅ Complete |
| Volume | ✅ | ✅ | ✅ Complete |
| EQ (3-band) | ✅ | ✅ | ✅ Complete |
| Filters | ✅ | ✅ | ✅ Complete |
| Speed/Pitch | ✅ | ✅ | ✅ Complete |
| **Features** |
| Streaming Panel | ✅ | ✅ | ✅ **NEW!** |
| Settings Panel | ✅ | ✅ | ✅ **NEW!** |
| Glow Effects | ✅ | ✅ | ✅ **NEW!** |
| Broadcast | ✅ | ✅ | ✅ **NEW!** |
| Upload Files | ✅ | ✅ | ✅ **NEW!** |
| Floating Buttons | ✅ | ✅ | ✅ **NEW!** |
| **Performance** |
| RAM Usage | ~400MB | ~150MB | 62% less |
| CPU Usage | High | Low | 75% less |
| Latency | 50-100ms | <10ms | 90% better |
---
### 🎮 How to Use
#### **Open Streaming Panel:**
1. Click 📡 button (bottom-right)
2. Configure quality
3. Click "START BROADCAST"
4. Share URL with listeners
#### **Enable Glow Effects:**
1. Click ⚙️ button (bottom-right)
2. Check "✨ Glow Deck A (Cyan)"
3. Check "✨ Glow Deck B (Magenta)"
4. Adjust intensity slider
5. Watch your decks glow! ✨
#### **Upload Songs:**
1. Click 📁 button (bottom-right)
2. Select MP3 file
3. File uploads to server
4. Library refreshes automatically
---
### 🚀 Launch the App
```bash
./launch_qt.sh
```
Or directly:
```bash
python3 techdj_qt.py
```
---
### 📊 Memory Usage
**Before (Web Panel):**
- Chrome: ~400MB RAM
- Multiple processes
- High CPU usage
**After (PyQt5 Native):**
- PyQt5: ~150MB RAM
- Single process
- Low CPU usage
- **62% memory savings!**
---
### ✨ Visual Highlights
1. **Neon Aesthetic** - Exact cyan/magenta colors from web panel
2. **Animated Vinyl** - Smooth rotation when playing
3. **Glow Effects** - Adjustable intensity, just like web version
4. **Floating Buttons** - Bottom-right corner, matching web layout
5. **Panels** - Slide-out streaming and settings panels
6. **Professional Look** - Pixel-perfect replica of web design
---
### 🎉 What's New in This Update
✅ Added floating action buttons (📡⚙️📁⌨️)
✅ Added streaming panel with broadcast controls
✅ Added settings panel with all options
✅ Added **neon glow effects** (Deck A cyan, Deck B magenta)
✅ Added glow intensity slider
✅ Added file upload functionality
✅ Added Socket.IO integration for broadcasting
✅ Added listener count display
✅ Added stream URL with copy button
✅ Added quality selector
✅ Made it a **perfect visual replica** of the web panel!
---
### 🔮 Coming Soon
- [ ] VU meters with real-time visualization
- [ ] Keyboard shortcuts panel
- [ ] BPM detection and sync
- [ ] Auto-mix functionality
- [ ] Effects (reverb, delay, etc.)
- [ ] Recording/export
- [ ] MIDI controller support
---
**Now you have a complete, pixel-perfect PyQt5 replica of TechDJ with all the features, including broadcast and neon glow effects!** 🎧✨
**Memory usage: ~150MB (vs ~400MB in Chrome) - 62% savings!**

View File

@ -1,216 +0,0 @@
# TechDJ - Quick Start Guide
## 🎧 You Now Have TWO DJ Applications!
### 1⃣ Web DJ Panel (Chrome) - ~400MB RAM
**Best for:** Mobile, remote DJing, streaming to listeners
**Start:**
```bash
python3 server.py
# Open http://localhost:5000 in Chrome
```
### 2⃣ PyQt5 Native App - ~150MB RAM ⚡
**Best for:** Laptop DJing, low memory, better performance
**Start:**
```bash
./launch_qt.sh
```
---
## 📊 Quick Comparison
| Feature | Web Panel | Native App |
|---------|-----------|------------|
| **RAM** | ~400MB | ~150MB (62% less!) |
| **Latency** | 50-100ms | <10ms (90% better!) |
| **Mobile** | ✅ Yes | ❌ No |
| **Offline** | ❌ No | ✅ Yes (cached) |
| **Installation** | None | Requires setup |
---
## 🚀 First Time Setup (Native App)
1. **Install system dependencies:**
```bash
# Ubuntu/Debian
sudo apt-get install portaudio19-dev python3-pyqt5 python3-pip
# Fedora
sudo dnf install portaudio-devel python3-qt5 python3-pip
# Arch
sudo pacman -S portaudio python-pyqt5 python-pip
```
2. **Install Python packages:**
```bash
pip3 install --user -r requirements.txt
```
3. **Launch:**
```bash
./launch_qt.sh
```
---
## 📁 File Structure
```
techdj/
├── server.py # Flask server (required for both)
├── index.html # Web DJ panel
├── script.js # Web DJ logic
├── style.css # Web DJ styling
├── techdj_qt.py # PyQt5 native app ⭐ NEW
├── launch_qt.sh # Easy launcher ⭐ NEW
├── compare_memory.py # Memory comparison tool ⭐ NEW
├── README_PYQT5.md # Native app docs ⭐ NEW
├── COMPARISON.md # Detailed comparison ⭐ NEW
└── music/ # Your MP3 library
```
---
## 🎵 How the Native App Works
1. **Fetches library** from Flask server (`http://localhost:5000/library.json`)
2. **Downloads songs** to local cache (`~/.techdj_cache/`)
3. **Processes audio locally** using sounddevice (efficient!)
4. **Caches songs** for instant loading next time
**Memory savings:**
- Chrome loads entire song into RAM (~50MB per 5min song)
- PyQt5 streams in small chunks (~0.1MB at a time)
- **Result: 99% less audio memory usage!**
---
## 🔧 Troubleshooting
### Native app won't start?
```bash
# Check dependencies
python3 -c "import PyQt5; import sounddevice; import soundfile; print('✅ All good!')"
# Check audio devices
python3 -c "import sounddevice as sd; print(sd.query_devices())"
```
### Can't connect to server?
```bash
# Make sure server is running
curl http://localhost:5000/library.json
# Start server if needed
python3 server.py
```
### Audio crackling/glitches?
- Increase buffer size in `techdj_qt.py` (line 56):
```python
blocksize=4096 # Increase from 2048
```
---
## 📈 Compare Memory Usage
Run both versions and check the difference:
```bash
python3 compare_memory.py
```
Example output:
```
TechDJ Memory Usage Comparison
============================================================
🌐 Chrome (Web Panel):
Total Memory: 387.2 MB
Processes: 8
💻 PyQt5 Native App:
Total Memory: 142.5 MB
Processes: 1
============================================================
📊 Comparison:
Memory Saved: 244.7 MB (63.2%)
Chrome: ████████████████████████████████████████ 387MB
PyQt5: ███████████████ 143MB
✅ PyQt5 uses 63% less memory!
============================================================
```
---
## 🎯 Recommended Setup
**For best results, use BOTH:**
1. **DJ on your laptop** with PyQt5 native app (low memory, fast)
2. **Keep web panel** for mobile/remote access
3. **Listeners** connect to web interface (port 5001)
```
You (Laptop) Server Audience
┌──────────┐ ┌──────┐ ┌──────────┐
│ PyQt5 │────────▶│Flask │────────▶│ Web │
│ Native │ control │5000/ │ stream │Listeners │
│ ~150MB │ │5001 │ │ Mobile │
└──────────┘ └──────┘ └──────────┘
```
---
## 🎉 What's Next?
### Try it out:
```bash
# Terminal 1: Start server
python3 server.py
# Terminal 2: Launch native app
./launch_qt.sh
# Terminal 3: Compare memory
python3 compare_memory.py
```
### Future enhancements for native app:
- [ ] EQ and filters
- [ ] Loop controls
- [ ] Broadcast to web listeners
- [ ] BPM detection
- [ ] MIDI controller support
- [ ] Effects (reverb, delay)
---
## 📚 Documentation
- **Native App Guide:** `README_PYQT5.md`
- **Detailed Comparison:** `COMPARISON.md`
- **Web Panel:** Original `README.md`
---
## 💡 Tips
1. **Cache management:** Songs download once, then load instantly
2. **Offline DJing:** Works without internet after songs are cached
3. **Battery life:** Native app uses ~40% less battery than Chrome
4. **Latency:** Native app has <10ms latency vs 50-100ms in browser
---
**Enjoy your lightweight, native DJ experience! 🎧⚡**

View File

@ -129,7 +129,7 @@ You should see output like:
### DJ workflow ### DJ workflow
1. Open the DJ Panel: `http://localhost:5000` 1. Open the DJ Panel: `http://localhost:5000`
2. Click **INITIALIZE SYSTEM** 2. Click **INITIALISE SYSTEM**
3. Load/play music 3. Load/play music
- Upload MP3s (folder/upload button) - Upload MP3s (folder/upload button)
- Or download from URLs (paste into deck input / download controls) - Or download from URLs (paste into deck input / download controls)
@ -140,7 +140,7 @@ You should see output like:
TechDJ can relay live streams from other DJ servers: TechDJ can relay live streams from other DJ servers:
1. Open the DJ Panel: `http://localhost:5000` 1. Open the DJ Panel: `http://localhost:5000`
2. Click the streaming panel (📡 LIVE STREAM) 2. Click the streaming panel (LIVE STREAM)
3. In the "Remote Stream Relay" section, paste a remote stream URL (e.g., `http://remote.dj/stream.mp3`) 3. In the "Remote Stream Relay" section, paste a remote stream URL (e.g., `http://remote.dj/stream.mp3`)
4. Click **START RELAY** 4. Click **START RELAY**
5. Your listeners will receive the relayed stream 5. Your listeners will receive the relayed stream
@ -249,7 +249,7 @@ Example Cloudflare page rule:
- Crossfader isnt fully on the silent side - Crossfader isnt fully on the silent side
- Volumes arent at 0 - Volumes arent at 0
- Check `http://<DJ_MACHINE_IP>:5001/stream_debug` and see if `transcoder_bytes_out` increases - Check `http://<DJ_MACHINE_IP>:5001/stream_debug` and see if `transcoder_bytes_out` increases
### Spectrum visualizer not showing ### Spectrum visualiser not showing
- Ensure the listener page is loaded and audio is enabled. - Ensure the listener page is loaded and audio is enabled.
- Check browser console for errors related to Web Audio API. - Check browser console for errors related to Web Audio API.

View File

@ -1,143 +0,0 @@
# TechDJ PyQt5 - Native DJ Application
A lightweight native alternative to the web-based TechDJ panel, built with PyQt5.
## Features
**Lightweight** - Uses ~120-150MB RAM (vs ~400MB for Chrome)
**Local Audio Processing** - Downloads songs from Flask server and processes locally
**Low Latency** - Instant response to controls, perfect for live DJing
**Full DJ Features**:
- Dual decks with independent playback
- Crossfader mixing
- Waveform display
- Hot cues (4 per deck)
- Speed/pitch control
- Volume control per deck
- Real-time audio visualization
## Installation
1. Install dependencies:
```bash
pip install -r requirements.txt
```
Note: On Linux, you may also need to install PortAudio:
```bash
# Ubuntu/Debian
sudo apt-get install portaudio19-dev python3-pyqt5
# Fedora
sudo dnf install portaudio-devel python3-qt5
# Arch
sudo pacman -S portaudio python-pyqt5
```
## Usage
1. Start the Flask server (in one terminal):
```bash
python server.py
```
2. Launch the PyQt5 DJ app (in another terminal):
```bash
python techdj_qt.py
```
## How It Works
1. **Library Management**: Fetches song list from Flask server at `http://localhost:5000/library.json`
2. **Song Download**: Downloads MP3 files to local cache (`~/.techdj_cache/`) on first use
3. **Local Playback**: Uses `sounddevice` for efficient real-time audio processing
4. **Caching**: Songs are cached locally for instant loading on subsequent uses
## Architecture
```
┌─────────────────────┐
│ PyQt5 Native App │ (~150MB RAM)
│ │
│ • Fetches library │
│ • Downloads & caches│
│ • Local audio mix │
│ • Real-time DSP │
└─────────────────────┘
│ HTTP (library + downloads)
┌─────────────────────┐
│ Flask Server │
│ (port 5000/5001) │
│ │
│ • Serves library │
│ • Serves MP3 files │
│ • Handles streaming │
└─────────────────────┘
```
## Memory Comparison
| Version | RAM Usage | CPU Usage | Latency |
|---------|-----------|-----------|---------|
| **Chrome Web Panel** | ~400MB | High | ~50-100ms |
| **PyQt5 Native** | ~120-150MB | Low | <10ms |
## Keyboard Shortcuts
- **Space**: Play/Pause active deck
- **1-4**: Hot cues for Deck A
- **5-8**: Hot cues for Deck B
- **Q/W**: Volume Deck A/B
- **A/S**: Speed Deck A/B
## Cache Management
Songs are cached in `~/.techdj_cache/`. To clear the cache:
```bash
rm -rf ~/.techdj_cache/
```
## Troubleshooting
### Audio Issues
If you get audio errors, check your audio devices:
```python
import sounddevice as sd
print(sd.query_devices())
```
### Connection Issues
Make sure the Flask server is running on port 5000:
```bash
curl http://localhost:5000/library.json
```
## Comparison with Web Panel
| Feature | Web Panel | PyQt5 Native |
|---------|-----------|--------------|
| Memory Usage | ~400MB | ~150MB |
| Installation | None (browser) | Requires Python + deps |
| Mobile Support | Excellent | None |
| Remote Access | Easy | Requires VPN/SSH |
| Audio Latency | Higher | Lower |
| Offline Mode | No | Yes (cached songs) |
| Battery Impact | Higher | Lower |
## Future Enhancements
- [ ] Broadcast to Flask server for web listeners
- [ ] BPM detection
- [ ] Auto-sync between decks
- [ ] Effects (reverb, delay, etc.)
- [ ] Recording/export
- [ ] MIDI controller support
- [ ] Playlist management
## License
Same as TechDJ main project

View File

@ -11,13 +11,13 @@
<body> <body>
<div id="start-overlay"> <div id="start-overlay">
<h1 class="overlay-title">TECHDJ PROTOCOL</h1> <h1 class="overlay-title">TECHDJ PROTOCOL</h1>
<button id="start-btn" onclick="initSystem()">INITIALIZE SYSTEM</button> <button id="start-btn" onclick="initSystem()">INITIALISE SYSTEM</button>
<p style="color:#666; margin-top:20px; font-family:'Rajdhani'">v2.0 // NEON CORE</p> <p style="color:#666; margin-top:20px; font-family:'Rajdhani'">v2.0 // NEON CORE</p>
</div> </div>
<!-- Landscape Orientation Prompt --> <!-- Landscape Orientation Prompt -->
<div class="landscape-prompt" id="landscape-prompt"> <div class="landscape-prompt" id="landscape-prompt">
<div class="rotate-icon">📱→🔄</div> <div class="rotate-icon">ROTATE</div>
<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>
@ -33,19 +33,27 @@
<!-- Mobile Tabs --> <!-- Mobile Tabs -->
<nav class="mobile-tabs"> <nav class="mobile-tabs">
<button class="tab-btn active" onclick="switchTab('library')"> <button class="tab-btn active" onclick="switchTab('library')">
<span class="tab-icon">📁</span> <span class="tab-icon"></span>
<span>LIBRARY</span> <span>LIBRARY</span>
</button> </button>
<button class="tab-btn" onclick="switchTab('deck-A')"> <button class="tab-btn" onclick="switchTab('deck-A')">
<span class="tab-icon">💿</span> <span class="tab-icon"></span>
<span>DECK A</span> <span>DECK A</span>
</button> </button>
<button class="tab-btn" onclick="switchTab('queue-A')">
<span class="tab-icon"></span>
<span>QUEUE A</span>
</button>
<button class="tab-btn" onclick="switchTab('deck-B')"> <button class="tab-btn" onclick="switchTab('deck-B')">
<span class="tab-icon">💿</span> <span class="tab-icon"></span>
<span>DECK B</span> <span>DECK B</span>
</button> </button>
<button class="tab-btn" onclick="switchTab('queue-B')">
<span class="tab-icon"></span>
<span>QUEUE B</span>
</button>
<button class="tab-btn fullscreen-btn" onclick="toggleFullScreen()" id="fullscreen-toggle"> <button class="tab-btn fullscreen-btn" onclick="toggleFullScreen()" id="fullscreen-toggle">
<span class="tab-icon">📺</span> <span class="tab-icon"></span>
<span>FULL</span> <span>FULL</span>
</button> </button>
</nav> </nav>
@ -53,8 +61,8 @@
<!-- 1. LEFT: LIBRARY --> <!-- 1. LEFT: LIBRARY -->
<section class="library-section"> <section class="library-section">
<div class="lib-header"> <div class="lib-header">
<input type="text" id="lib-search" placeholder="🔍 FILTER LIBRARY..." onkeyup="filterLibrary()"> <input type="text" id="lib-search" placeholder="FILTER LIBRARY..." onkeyup="filterLibrary()">
<button class="refresh-btn" onclick="refreshLibrary()" title="Refresh Library">🔄</button> <button class="refresh-btn" onclick="refreshLibrary()" title="Refresh Library">REFRESH</button>
</div> </div>
@ -175,19 +183,8 @@
<button class="big-btn play-btn" onclick="playDeck('A')">PLAY</button> <button class="big-btn play-btn" onclick="playDeck('A')">PLAY</button>
<button class="big-btn pause-btn" onclick="pauseDeck('A')">PAUSE</button> <button class="big-btn pause-btn" onclick="pauseDeck('A')">PAUSE</button>
<button class="big-btn sync-btn" onclick="syncDecks('A')">SYNC</button> <button class="big-btn sync-btn" onclick="syncDecks('A')">SYNC</button>
<button class="big-btn reset-btn" onclick="resetDeck('A')" title="Reset all settings to default">🔄 <button class="big-btn reset-btn" onclick="resetDeck('A')"
RESET</button> title="Reset all settings to default">RESET</button>
</div>
<!-- QUEUE for Deck A -->
<div class="queue-panel" id="queue-panel-A">
<div class="queue-header">
<span class="queue-title">📋 QUEUE A</span>
<button class="queue-clear-btn" onclick="clearQueue('A')" title="Clear queue">🗑️</button>
</div>
<div class="queue-list" id="queue-list-A">
<div class="queue-empty">Drop tracks here or click "Queue to A" in library</div>
</div>
</div> </div>
</div> </div>
@ -302,22 +299,32 @@
<button class="big-btn play-btn" onclick="playDeck('B')">PLAY</button> <button class="big-btn play-btn" onclick="playDeck('B')">PLAY</button>
<button class="big-btn pause-btn" onclick="pauseDeck('B')">PAUSE</button> <button class="big-btn pause-btn" onclick="pauseDeck('B')">PAUSE</button>
<button class="big-btn sync-btn" onclick="syncDecks('B')">SYNC</button> <button class="big-btn sync-btn" onclick="syncDecks('B')">SYNC</button>
<button class="big-btn reset-btn" onclick="resetDeck('B')" title="Reset all settings to default">🔄 <button class="big-btn reset-btn" onclick="resetDeck('B')"
RESET</button> title="Reset all settings to default">RESET</button>
</div>
<!-- QUEUE for Deck B -->
<div class="queue-panel" id="queue-panel-B">
<div class="queue-header">
<span class="queue-title">📋 QUEUE B</span>
<button class="queue-clear-btn" onclick="clearQueue('B')" title="Clear queue">🗑️</button>
</div>
<div class="queue-list" id="queue-list-B">
<div class="queue-empty">Drop tracks here or click "Queue to B" in library</div>
</div>
</div> </div>
</div> </div>
<!-- STANDALONE QUEUE SECTIONS FOR MOBILE -->
<section class="queue-section" id="queue-A">
<div class="queue-page-header">
<h2 class="queue-page-title">QUEUE A</h2>
<button class="queue-clear-btn" onclick="clearQueue('A')" title="Clear queue">Clear All</button>
</div>
<div class="queue-list" id="queue-list-A">
<div class="queue-empty">Drop tracks here or click "Queue to A" in library</div>
</div>
</section>
<section class="queue-section" id="queue-B">
<div class="queue-page-header">
<h2 class="queue-page-title">QUEUE B</h2>
<button class="queue-clear-btn" onclick="clearQueue('B')" title="Clear queue">Clear All</button>
</div>
<div class="queue-list" id="queue-list-B">
<div class="queue-empty">Drop tracks here or click "Queue to B" in library</div>
</div>
</section>
<!-- 4. BOTTOM: CROSSFADER --> <!-- 4. BOTTOM: CROSSFADER -->
<div class="mixer-section"> <div class="mixer-section">
<input type="range" class="xfader" id="crossfader" min="0" max="100" value="50" <input type="range" class="xfader" id="crossfader" min="0" max="100" value="50"
@ -329,13 +336,13 @@
<!-- Live Streaming Panel --> <!-- Live Streaming Panel -->
<div class="streaming-panel" id="streaming-panel"> <div class="streaming-panel" id="streaming-panel">
<div class="streaming-header"> <div class="streaming-header">
<span>📡 LIVE STREAM</span> <span>LIVE STREAM</span>
<button class="close-streaming" onclick="toggleStreamingPanel()"></button> <button class="close-streaming" onclick="toggleStreamingPanel()">X</button>
</div> </div>
<div class="streaming-content"> <div class="streaming-content">
<div class="broadcast-controls"> <div class="broadcast-controls">
<button class="broadcast-btn" id="broadcast-btn" onclick="toggleBroadcast()"> <button class="broadcast-btn" id="broadcast-btn" onclick="toggleBroadcast()">
<span class="broadcast-icon">🔴</span> <span class="broadcast-icon"></span>
<span id="broadcast-text">START BROADCAST</span> <span id="broadcast-text">START BROADCAST</span>
</button> </button>
<div class="broadcast-status" id="broadcast-status">Offline</div> <div class="broadcast-status" id="broadcast-status">Offline</div>
@ -343,7 +350,7 @@
<div class="listener-info"> <div class="listener-info">
<div class="listener-count"> <div class="listener-count">
<span class="count-icon">👂</span> <span class="count-icon"></span>
<span id="listener-count">0</span> <span id="listener-count">0</span>
<span class="count-label">Listeners</span> <span class="count-label">Listeners</span>
</div> </div>
@ -353,7 +360,7 @@
<label>Share this URL:</label> <label>Share this URL:</label>
<div class="url-copy-group"> <div class="url-copy-group">
<input type="text" id="stream-url" readonly value="http://localhost:5001"> <input type="text" id="stream-url" readonly value="http://localhost:5001">
<button onclick="copyStreamUrl(event)" class="copy-btn">📋</button> <button onclick="copyStreamUrl(event)" class="copy-btn">COPY</button>
</div> </div>
</div> </div>
@ -382,7 +389,7 @@
<!-- Listener Mode (Hidden by default) --> <!-- Listener Mode (Hidden by default) -->
<div class="listener-mode" id="listener-mode" style="display: none;"> <div class="listener-mode" id="listener-mode" style="display: none;">
<div class="listener-header"> <div class="listener-header">
<h1>🎧 TECHDJ LIVE</h1> <h1>TECHDJ LIVE</h1>
<div class="live-indicator"> <div class="live-indicator">
<span class="pulse-dot"></span> <span class="pulse-dot"></span>
<span>LIVE</span> <span>LIVE</span>
@ -396,13 +403,13 @@
<!-- Enable Audio Button (shown when autoplay is blocked) --> <!-- Enable Audio Button (shown when autoplay is blocked) -->
<button class="enable-audio-btn" id="enable-audio-btn" style="display: none;" <button class="enable-audio-btn" id="enable-audio-btn" style="display: none;"
onclick="enableListenerAudio()"> onclick="enableListenerAudio()">
<span class="audio-icon">🔊</span> <span class="audio-icon"></span>
<span class="audio-text">ENABLE AUDIO</span> <span class="audio-text">ENABLE AUDIO</span>
<span class="audio-subtitle">Click to start listening</span> <span class="audio-subtitle">Click to start listening</span>
</button> </button>
<div class="volume-control"> <div class="volume-control">
<label>🔊 Volume</label> <label>Volume</label>
<input type="range" id="listener-volume" min="0" max="100" value="80" <input type="range" id="listener-volume" min="0" max="100" value="80"
oninput="setListenerVolume(this.value)"> oninput="setListenerVolume(this.value)">
</div> </div>
@ -415,45 +422,46 @@
<!-- Settings Panel --> <!-- Settings Panel -->
<div class="settings-panel" id="settings-panel"> <div class="settings-panel" id="settings-panel">
<div class="settings-header"> <div class="settings-header">
<span>⚙️ SETTINGS</span> <span>SETTINGS</span>
<button class="close-settings" onclick="toggleSettings()"></button> <button class="close-settings" onclick="toggleSettings()">X</button>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="setting-item"><label><input type="checkbox" id="repeat-A" <div class="setting-item"><label><input type="checkbox" id="repeat-A"
onchange="toggleRepeat('A', this.checked)">🔁 Repeat Deck A</label></div> onchange="toggleRepeat('A', this.checked)">Repeat Deck A</label></div>
<div class="setting-item"><label><input type="checkbox" id="repeat-B" <div class="setting-item"><label><input type="checkbox" id="repeat-B"
onchange="toggleRepeat('B', this.checked)"><EFBFBD><EFBFBD> Repeat Deck B</label></div> onchange="toggleRepeat('B', this.checked)">Repeat Deck B</label></div>
<div class="setting-item"><label><input type="checkbox" id="auto-mix" <div class="setting-item"><label><input type="checkbox" id="auto-mix"
onchange="toggleAutoMix(this.checked)">🎛️ Auto-Crossfade</label></div> onchange="toggleAutoMix(this.checked)">Auto-Crossfade</label></div>
<div class="setting-item"><label><input type="checkbox" id="shuffle-mode" <div class="setting-item"><label><input type="checkbox" id="shuffle-mode"
onchange="toggleShuffle(this.checked)">🔀 Shuffle Library</label></div> onchange="toggleShuffle(this.checked)">Shuffle Library</label></div>
<div class="setting-item"><label><input type="checkbox" id="quantize" <div class="setting-item"><label><input type="checkbox" id="quantize"
onchange="toggleQuantize(this.checked)">📐 Quantize</label></div> onchange="toggleQuantize(this.checked)">Quantise</label></div>
<div class="setting-item"><label><input type="checkbox" id="auto-play" checked <div class="setting-item"><label><input type="checkbox" id="auto-play" checked
onchange="toggleAutoPlay(this.checked)">▶️ Auto-play next</label></div> onchange="toggleAutoPlay(this.checked)">Auto-play next</label></div>
<div class="setting-item"><label><input type="checkbox" id="glow-A" <div class="setting-item"><label><input type="checkbox" id="glow-A"
onchange="updateManualGlow('A', this.checked)">Glow Deck A (Cyan)</label></div> onchange="updateManualGlow('A', this.checked)">Glow Deck A (Cyan)</label></div>
<div class="setting-item"><label><input type="checkbox" id="glow-B" <div class="setting-item"><label><input type="checkbox" id="glow-B"
onchange="updateManualGlow('B', this.checked)">Glow Deck B (Magenta)</label></div> onchange="updateManualGlow('B', this.checked)">Glow Deck B (Magenta)</label></div>
<div class="setting-item" style="flex-direction: column; align-items: flex-start;"> <div class="setting-item" style="flex-direction: column; align-items: flex-start;">
<label>Glow Intensity</label> <label>Glow Intensity</label>
<input type="range" id="glow-intensity" min="1" max="100" value="30" style="width: 100%;" <input type="range" id="glow-intensity" min="1" max="100" value="30" style="width: 100%;"
oninput="updateGlowIntensity(this.value)"> oninput="updateGlowIntensity(this.value)">
</div> </div>
<div class="setting-item"> <div class="setting-item">
<button class="btn-primary" onclick="openKeyboardSettings()" <button class="btn-primary" onclick="openKeyboardSettings()"
style="width: 100%; padding: 12px; margin-top: 10px;"> style="width: 100%; padding: 12px; margin-top: 10px;">
⌨️ CUSTOM KEYBOARD MAPS CUSTOM KEYBOARD MAPS
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<button class="keyboard-btn" onclick="openKeyboardSettings()" title="Keyboard Shortcuts (H)">⌨️</button> <button class="keyboard-btn" onclick="openKeyboardSettings()" title="Keyboard Shortcuts (H)">KB</button>
<button class="streaming-btn" onclick="toggleStreamingPanel()" title="Live Streaming">📡</button> <button class="streaming-btn" onclick="toggleStreamingPanel()" title="Live Streaming">STREAM</button>
<button class="upload-btn" onclick="document.getElementById('file-upload').click()" title="Upload MP3">📁</button> <button class="upload-btn" onclick="document.getElementById('file-upload').click()"
title="Upload MP3">UPLOAD</button>
<input type="file" id="file-upload" accept="audio/mp3,audio/mpeg" multiple style="display:none" <input type="file" id="file-upload" accept="audio/mp3,audio/mpeg" multiple style="display:none"
onchange="handleFileUpload(event)"> onchange="handleFileUpload(event)">
<button class="settings-btn" onclick="toggleSettings()">⚙️</button> <button class="settings-btn" onclick="toggleSettings()">SET</button>
</body> </body>
</html> </html>

130
script.js
View File

@ -253,7 +253,7 @@ function initSystem() {
}); });
}); });
// Initialize mobile view // Initialise mobile view
if (window.innerWidth <= 1024) { if (window.innerWidth <= 1024) {
switchTab('library'); switchTab('library');
} }
@ -290,7 +290,7 @@ function animateVUMeters() {
const barCount = 32; const barCount = 32;
const barWidth = width / barCount; const barWidth = width / barCount;
// Initialize smoothed values if needed // Initialise smoothed values if needed
if (!vuMeterState[id].smoothedValues.length) { if (!vuMeterState[id].smoothedValues.length) {
vuMeterState[id].smoothedValues = new Array(barCount).fill(0); vuMeterState[id].smoothedValues = new Array(barCount).fill(0);
vuMeterState[id].peakValues = new Array(barCount).fill(0); vuMeterState[id].peakValues = new Array(barCount).fill(0);
@ -355,10 +355,10 @@ 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'); const sections = document.querySelectorAll('.library-section, .deck, .queue-section');
// Remove all tab and active 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', 'show-queue-A', 'show-queue-B');
buttons.forEach(btn => btn.classList.remove('active')); buttons.forEach(btn => btn.classList.remove('active'));
sections.forEach(sec => sec.classList.remove('active')); sections.forEach(sec => sec.classList.remove('active'));
@ -458,7 +458,7 @@ function handleSwipe() {
} }
} }
// Waveform Generation (Optimized for Speed) // Waveform Generation (Optimised for Speed)
function generateWaveformData(buffer) { function generateWaveformData(buffer) {
const rawData = buffer.getChannelData(0); const rawData = buffer.getChannelData(0);
const samples = 1000; const samples = 1000;
@ -542,7 +542,7 @@ function drawWaveform(id) {
} }
} }
// BPM Detection (Optimized: Only check middle 60 seconds for speed) // BPM Detection (Optimised: Only check middle 60 seconds for speed)
function detectBPM(buffer) { function detectBPM(buffer) {
const sampleRate = buffer.sampleRate; const sampleRate = buffer.sampleRate;
const duration = buffer.duration; const duration = buffer.duration;
@ -702,7 +702,7 @@ function pauseDeck(id) {
// Browser-side audio mode (original code) // Browser-side audio mode (original code)
if (decks[id].type === 'local' && decks[id].localSource && decks[id].playing) { if (decks[id].type === 'local' && decks[id].localSource && decks[id].playing) {
if (!audioCtx) { if (!audioCtx) {
console.warn(`[Deck ${id}] Cannot calculate pause position - audioCtx not initialized`); console.warn(`[Deck ${id}] Cannot calculate pause position - audioCtx not initialised`);
decks[id].playing = false; decks[id].playing = false;
} else { } else {
const playbackRate = decks[id].localSource.playbackRate.value; const playbackRate = decks[id].localSource.playbackRate.value;
@ -1174,13 +1174,13 @@ function renderLibrary(songs) {
// QUEUE buttons // QUEUE buttons
const queueA = document.createElement('button'); const queueA = document.createElement('button');
queueA.className = 'load-btn queue-btn-a'; queueA.className = 'load-btn queue-btn-a';
queueA.textContent = '📋 Q-A'; queueA.textContent = 'Q-A';
queueA.title = 'Add to Queue A'; queueA.title = 'Add to Queue A';
queueA.addEventListener('click', () => addToQueue('A', t.file, t.title)); queueA.addEventListener('click', () => addToQueue('A', t.file, t.title));
const queueB = document.createElement('button'); const queueB = document.createElement('button');
queueB.className = 'load-btn queue-btn-b'; queueB.className = 'load-btn queue-btn-b';
queueB.textContent = '📋 Q-B'; queueB.textContent = 'Q-B';
queueB.title = 'Add to Queue B'; queueB.title = 'Add to Queue B';
queueB.addEventListener('click', () => addToQueue('B', t.file, t.title)); queueB.addEventListener('click', () => addToQueue('B', t.file, t.title));
@ -1256,7 +1256,7 @@ async function loadFromServer(id, url, title) {
if (!socket) initSocket(); if (!socket) initSocket();
socket.emit('audio_load_track', { deck: id, filename: filename }); socket.emit('audio_load_track', { deck: id, filename: filename });
console.log(`[Deck ${id}] 📡 Load command sent to server: ${filename}`); console.log(`[Deck ${id}] Load command sent to server: ${filename}`);
// We DON'T return here anymore. We continue below to load for the UI. // We DON'T return here anymore. We continue below to load for the UI.
} }
@ -1318,7 +1318,7 @@ async function loadFromServer(id, url, title) {
// AUTO-RESUME for broadcast continuity // AUTO-RESUME for broadcast continuity
if (wasPlaying && wasBroadcasting) { if (wasPlaying && wasBroadcasting) {
console.log(`[Deck ${id}] 🎵 Auto-resuming playback to maintain broadcast stream`); console.log(`[Deck ${id}] Auto-resuming playback to maintain broadcast stream`);
// Small delay to ensure buffer is fully ready // Small delay to ensure buffer is fully ready
setTimeout(() => { setTimeout(() => {
playDeck(id); playDeck(id);
@ -1378,7 +1378,7 @@ async function handleFileUpload(event) {
const files = event.target.files; const files = event.target.files;
if (!files || files.length === 0) return; if (!files || files.length === 0) return;
console.log(`📁 Uploading ${files.length} file(s)...`); console.log(`Uploading ${files.length} file(s)...`);
for (let file of files) { for (let file of files) {
if (!file.type.match('audio/mpeg') && !file.name.endsWith('.mp3')) { if (!file.type.match('audio/mpeg') && !file.name.endsWith('.mp3')) {
@ -1410,7 +1410,7 @@ async function handleFileUpload(event) {
} }
// Refresh library // Refresh library
console.log('🔄 Refreshing library...'); console.log('Refreshing library...');
await loadLibrary(); await loadLibrary();
alert(`${files.length} file(s) uploaded successfully!`); alert(`${files.length} file(s) uploaded successfully!`);
@ -1461,7 +1461,7 @@ window.addEventListener('DOMContentLoaded', () => {
if (prompt) prompt.classList.add('dismissed'); if (prompt) prompt.classList.add('dismissed');
} }
// Initialize glow intensity // Initialise glow intensity
updateGlowIntensity(settings.glowIntensity); updateGlowIntensity(settings.glowIntensity);
const glowAToggle = document.getElementById('glow-A'); const glowAToggle = document.getElementById('glow-A');
if (glowAToggle) glowAToggle.checked = settings.glowA; if (glowAToggle) glowAToggle.checked = settings.glowA;
@ -1570,7 +1570,7 @@ function getMp3FallbackUrl() {
return `${window.location.origin}/stream.mp3`; return `${window.location.origin}/stream.mp3`;
} }
// Initialize SocketIO connection // Initialise SocketIO connection
function initSocket() { function initSocket() {
if (socket) return socket; if (socket) return socket;
@ -1623,7 +1623,7 @@ function initSocket() {
}); });
socket.on('broadcast_started', () => { socket.on('broadcast_started', () => {
console.log('🎙️ Broadcast started notification received'); console.log('Broadcast started notification received');
// Update relay UI if it's a relay // Update relay UI if it's a relay
const relayStatus = document.getElementById('relay-status'); const relayStatus = document.getElementById('relay-status');
if (relayStatus && relayStatus.textContent.includes('Connecting')) { if (relayStatus && relayStatus.textContent.includes('Connecting')) {
@ -1645,12 +1645,12 @@ function initSocket() {
}); });
socket.on('deck_status', (data) => { socket.on('deck_status', (data) => {
console.log(`📡 Server: Deck ${data.deck_id} status update:`, data); console.log(`Server: Deck ${data.deck_id} status update:`, data);
// This is handled by a single status update too, but helpful for immediate feedback // This is handled by a single status update too, but helpful for immediate feedback
}); });
socket.on('error', (data) => { socket.on('error', (data) => {
console.error('📡 Server error:', data.message); console.error('Server error:', data.message);
alert(`SERVER ERROR: ${data.message}`); alert(`SERVER ERROR: ${data.message}`);
// Reset relay UI on error // Reset relay UI on error
document.getElementById('start-relay-btn').style.display = 'inline-block'; document.getElementById('start-relay-btn').style.display = 'inline-block';
@ -1681,7 +1681,7 @@ function updateUIFromMixerStatus(status) {
// Update loaded track if changed // Update loaded track if changed
if (deckStatus.filename && (!decks[id].currentFile || decks[id].currentFile !== deckStatus.filename)) { if (deckStatus.filename && (!decks[id].currentFile || decks[id].currentFile !== deckStatus.filename)) {
console.log(`📡 Server synced: Deck ${id} is playing ${deckStatus.filename}`); console.log(`Server synced: Deck ${id} is playing ${deckStatus.filename}`);
decks[id].currentFile = deckStatus.filename; decks[id].currentFile = deckStatus.filename;
decks[id].duration = deckStatus.duration; decks[id].duration = deckStatus.duration;
@ -1725,7 +1725,7 @@ function toggleStreamingPanel() {
const panel = document.getElementById('streaming-panel'); const panel = document.getElementById('streaming-panel');
panel.classList.toggle('active'); panel.classList.toggle('active');
// Initialize socket when panel is opened // Initialise socket when panel is opened
if (panel.classList.contains('active') && !socket) { if (panel.classList.contains('active') && !socket) {
initSocket(); initSocket();
} }
@ -1734,7 +1734,7 @@ function toggleStreamingPanel() {
// Toggle broadcast // Toggle broadcast
function toggleBroadcast() { function toggleBroadcast() {
if (!audioCtx) { if (!audioCtx) {
alert('Please initialize the system first (click INITIALIZE SYSTEM)'); alert('Please initialise the system first (click INITIALIZE SYSTEM)');
return; return;
} }
@ -1751,10 +1751,10 @@ function toggleBroadcast() {
// Start broadcasting // Start broadcasting
function startBroadcast() { function startBroadcast() {
try { try {
console.log('🎙️ Starting broadcast...'); console.log('Starting broadcast...');
if (!audioCtx) { if (!audioCtx) {
alert('Please initialize the system first!'); alert('Please initialise the system first!');
return; return;
} }
@ -1779,7 +1779,7 @@ function startBroadcast() {
// Check if any audio is playing // Check if any audio is playing
const anyPlaying = decks.A.playing || decks.B.playing; const anyPlaying = decks.A.playing || decks.B.playing;
if (!anyPlaying) { if (!anyPlaying) {
console.warn('⚠️ WARNING: No decks are currently playing! Start playing a track for audio to stream.'); console.warn('WARNING: No decks are currently playing! Start playing a track for audio to stream.');
} }
// Create MediaStreamDestination to capture audio output // Create MediaStreamDestination to capture audio output
@ -1820,7 +1820,7 @@ function startBroadcast() {
// Get selected quality from dropdown // Get selected quality from dropdown
const qualitySelect = document.getElementById('stream-quality'); const qualitySelect = document.getElementById('stream-quality');
const selectedBitrate = parseInt(qualitySelect.value) * 1000; // Convert kbps to bps const selectedBitrate = parseInt(qualitySelect.value) * 1000; // Convert kbps to bps
console.log(`🎚️ Starting broadcast at ${qualitySelect.value}kbps`); console.log(`Starting broadcast at ${qualitySelect.value}kbps`);
const preferredTypes = [ const preferredTypes = [
// Prefer MP4/AAC when available (broad device support) // Prefer MP4/AAC when available (broad device support)
@ -1843,7 +1843,7 @@ function startBroadcast() {
} }
currentStreamMimeType = chosenType; currentStreamMimeType = chosenType;
console.log(`🎛️ Using broadcast mimeType: ${currentStreamMimeType}`); console.log(`Using broadcast mimeType: ${currentStreamMimeType}`);
mediaRecorder = new MediaRecorder(stream, { mediaRecorder = new MediaRecorder(stream, {
mimeType: currentStreamMimeType, mimeType: currentStreamMimeType,
@ -1861,7 +1861,7 @@ function startBroadcast() {
// Warn if chunks are too small (likely silence) // Warn if chunks are too small (likely silence)
if (event.data.size < 100 && !silenceWarningShown) { if (event.data.size < 100 && !silenceWarningShown) {
console.warn('⚠️ Audio chunks are very small - might be silence. Make sure audio is playing!'); console.warn('Audio chunks are very small - might be silence. Make sure audio is playing!');
silenceWarningShown = true; silenceWarningShown = true;
} }
@ -1872,7 +1872,7 @@ function startBroadcast() {
// Log every second // Log every second
const now = Date.now(); const now = Date.now();
if (now - lastLogTime > 1000) { if (now - lastLogTime > 1000) {
console.log(`📡 Broadcasting: ${chunkCount} chunks sent (${(event.data.size / 1024).toFixed(1)} KB/chunk)`); console.log(`Broadcasting: ${chunkCount} chunks sent (${(event.data.size / 1024).toFixed(1)} KB/chunk)`);
lastLogTime = now; lastLogTime = now;
// Reset silence warning // Reset silence warning
@ -1883,13 +1883,13 @@ function startBroadcast() {
} else { } else {
// Debug why chunks aren't being sent // Debug why chunks aren't being sent
if (event.data.size === 0) { if (event.data.size === 0) {
console.warn('⚠️ Received empty audio chunk'); console.warn('Received empty audio chunk');
} }
if (!isBroadcasting) { if (!isBroadcasting) {
console.warn('⚠️ Broadcasting flag is false'); console.warn('Broadcasting flag is false');
} }
if (!socket) { if (!socket) {
console.warn('⚠️ Socket not connected'); console.warn('Socket not connected');
} }
} }
}; };
@ -1898,7 +1898,7 @@ function startBroadcast() {
console.error('❌ MediaRecorder error:', error); console.error('❌ MediaRecorder error:', error);
// Try to recover from error // Try to recover from error
if (isBroadcasting) { if (isBroadcasting) {
console.log('🔄 Attempting to recover from MediaRecorder error...'); console.log('Attempting to recover from MediaRecorder error...');
setTimeout(() => { setTimeout(() => {
if (isBroadcasting) { if (isBroadcasting) {
restartBroadcast(); restartBroadcast();
@ -1912,18 +1912,18 @@ function startBroadcast() {
}; };
mediaRecorder.onstop = (event) => { mediaRecorder.onstop = (event) => {
console.warn('⚠️ MediaRecorder stopped!'); console.warn('MediaRecorder stopped!');
console.log(` State: ${mediaRecorder.state}`); console.log(` State: ${mediaRecorder.state}`);
console.log(` isBroadcasting flag: ${isBroadcasting}`); console.log(` isBroadcasting flag: ${isBroadcasting}`);
// If we're supposed to be broadcasting but MediaRecorder stopped, restart it // If we're supposed to be broadcasting but MediaRecorder stopped, restart it
if (isBroadcasting) { if (isBroadcasting) {
console.error('❌ MediaRecorder stopped unexpectedly while broadcasting!'); console.error('❌ MediaRecorder stopped unexpectedly while broadcasting!');
console.log('🔄 Auto-recovery: Attempting to restart broadcast in 2 seconds...'); console.log('Auto-recovery: Attempting to restart broadcast in 2 seconds...');
setTimeout(() => { setTimeout(() => {
if (isBroadcasting) { if (isBroadcasting) {
console.log('🔄 Executing auto-recovery...'); console.log('Executing auto-recovery...');
restartBroadcast(); restartBroadcast();
} }
}, 2000); }, 2000);
@ -1931,11 +1931,11 @@ function startBroadcast() {
}; };
mediaRecorder.onpause = (event) => { mediaRecorder.onpause = (event) => {
console.warn('⚠️ MediaRecorder paused unexpectedly!'); console.warn('MediaRecorder paused unexpectedly!');
// If we're broadcasting and MediaRecorder paused, resume it // If we're broadcasting and MediaRecorder paused, resume it
if (isBroadcasting && mediaRecorder.state === 'paused') { if (isBroadcasting && mediaRecorder.state === 'paused') {
console.log('🔄 Auto-resuming MediaRecorder...'); console.log('Auto-resuming MediaRecorder...');
try { try {
mediaRecorder.resume(); mediaRecorder.resume();
console.log('✅ MediaRecorder resumed'); console.log('✅ MediaRecorder resumed');
@ -2054,7 +2054,7 @@ function stopBroadcast() {
// Restart broadcasting (for auto-recovery) // Restart broadcasting (for auto-recovery)
function restartBroadcast() { function restartBroadcast() {
console.log('🔄 Restarting broadcast...'); console.log('Restarting broadcast...');
// Clean up old MediaRecorder without changing UI state // Clean up old MediaRecorder without changing UI state
if (streamProcessor) { if (streamProcessor) {
@ -2233,7 +2233,7 @@ function initListenerMode() {
window.listenerMediaSource = null; window.listenerMediaSource = null;
window.listenerAudioEnabled = false; // Track if user has enabled audio window.listenerAudioEnabled = false; // Track if user has enabled audio
// Initialize socket and join // Initialise socket and join
initSocket(); initSocket();
socket.emit('join_listener'); socket.emit('join_listener');
@ -2241,11 +2241,11 @@ function initListenerMode() {
socket.on('broadcast_started', () => { socket.on('broadcast_started', () => {
const nowPlayingEl = document.getElementById('listener-now-playing'); const nowPlayingEl = document.getElementById('listener-now-playing');
if (nowPlayingEl) nowPlayingEl.textContent = '🎵 Stream is live!'; if (nowPlayingEl) nowPlayingEl.textContent = 'Stream is live!';
// Force a reload of the audio element to capture the fresh stream // Force a reload of the audio element to capture the fresh stream
if (window.listenerAudio) { if (window.listenerAudio) {
console.log('🔄 Broadcast started: Refreshing audio stream...'); console.log('Broadcast started: Refreshing audio stream...');
const wasPlaying = !window.listenerAudio.paused; const wasPlaying = !window.listenerAudio.paused;
window.listenerAudio.src = getMp3FallbackUrl(); window.listenerAudio.src = getMp3FallbackUrl();
window.listenerAudio.load(); window.listenerAudio.load();
@ -2259,7 +2259,7 @@ function initListenerMode() {
const nowPlayingEl = document.getElementById('listener-now-playing'); const nowPlayingEl = document.getElementById('listener-now-playing');
if (nowPlayingEl) { if (nowPlayingEl) {
if (data.active) { if (data.active) {
const status = data.remote_relay ? '🔗 Remote stream is live!' : '🎵 DJ stream is live!'; const status = data.remote_relay ? 'Remote stream is live!' : 'DJ stream is live!';
nowPlayingEl.textContent = status; nowPlayingEl.textContent = status;
} else { } else {
nowPlayingEl.textContent = 'Stream offline - waiting for DJ...'; nowPlayingEl.textContent = 'Stream offline - waiting for DJ...';
@ -2336,12 +2336,12 @@ async function enableListenerAudio() {
listenerAnalyserNode.connect(listenerGainNode); listenerAnalyserNode.connect(listenerGainNode);
window.listenerAudio._connectedToContext = true; window.listenerAudio._connectedToContext = true;
console.log('🔗 Connected audio element to AudioContext (with analyser)'); console.log('Connected audio element to AudioContext (with analyser)');
// Start visualizer after the graph exists // Start visualiser after the graph exists
startListenerVUMeter(); startListenerVUMeter();
} catch (e) { } catch (e) {
console.warn('⚠️ Could not connect to AudioContext:', e.message); console.warn('Could not connect to AudioContext:', e.message);
} }
} }
@ -2370,7 +2370,7 @@ async function enableListenerAudio() {
// MP3 stream: call play() immediately to capture the user gesture. // MP3 stream: call play() immediately to capture the user gesture.
if (audioText) audioText.textContent = 'STARTING...'; if (audioText) audioText.textContent = 'STARTING...';
console.log('▶️ Attempting to play audio...'); console.log('Attempting to play audio...');
const playPromise = window.listenerAudio.play(); const playPromise = window.listenerAudio.play();
// If not buffered yet, show buffering but don't block. // If not buffered yet, show buffering but don't block.
@ -2414,10 +2414,10 @@ async function enableListenerAudio() {
errorMsg = 'MP3 stream not supported or unavailable (NotSupportedError).'; errorMsg = 'MP3 stream not supported or unavailable (NotSupportedError).';
} }
stashedStatus.textContent = '⚠️ ' + errorMsg; stashedStatus.textContent = '' + errorMsg;
if (error.name === 'NotSupportedError') { if (error.name === 'NotSupportedError') {
stashedStatus.textContent = '⚠️ MP3 stream failed. Is ffmpeg installed on the server?'; stashedStatus.textContent = 'MP3 stream failed. Is ffmpeg installed on the server?';
} }
} }
} }
@ -2459,7 +2459,7 @@ function monitorTrackEnd() {
if (remaining <= 0.5) { if (remaining <= 0.5) {
// Don't pause during broadcast - let the track end naturally // Don't pause during broadcast - let the track end naturally
if (isBroadcasting) { if (isBroadcasting) {
console.log(`🎙️ Track ending during broadcast on Deck ${id} - continuing stream`); console.log(`Track ending during broadcast on Deck ${id} - continuing stream`);
if (settings[`repeat${id}`]) { if (settings[`repeat${id}`]) {
console.log(`🔁 Repeating track on Deck ${id}`); console.log(`🔁 Repeating track on Deck ${id}`);
seekTo(id, 0); seekTo(id, 0);
@ -2479,7 +2479,7 @@ function monitorTrackEnd() {
// Check queue for auto-play // Check queue for auto-play
if (queues[id] && queues[id].length > 0) { if (queues[id] && queues[id].length > 0) {
console.log(`📋 Auto-play: Loading next from Queue ${id}...`); console.log(`Auto-play: Loading next from Queue ${id}...`);
const next = queues[id].shift(); const next = queues[id].shift();
renderQueue(id); // Update queue UI renderQueue(id); // Update queue UI
@ -2491,7 +2491,7 @@ function monitorTrackEnd() {
}); });
} else { } else {
// No queue - just stop // No queue - just stop
console.log(`⏹️ Track ended, queue empty - stopping playback`); console.log(`Track ended, queue empty - stopping playback`);
decks[id].loading = false; decks[id].loading = false;
pauseDeck(id); pauseDeck(id);
decks[id].pausedAt = 0; decks[id].pausedAt = 0;
@ -2510,10 +2510,10 @@ monitorTrackEnd();
// Reset Deck to Default Settings // Reset Deck to Default Settings
function resetDeck(id) { function resetDeck(id) {
vibrate(20); vibrate(20);
console.log(`🔄 Resetting Deck ${id} to defaults...`); console.log(`Resetting Deck ${id} to defaults...`);
if (!audioCtx) { if (!audioCtx) {
console.warn('AudioContext not initialized'); console.warn('AudioContext not initialised');
return; return;
} }
@ -2595,7 +2595,7 @@ function resetDeck(id) {
function addToQueue(deckId, file, title) { function addToQueue(deckId, file, title) {
queues[deckId].push({ file, title }); queues[deckId].push({ file, title });
renderQueue(deckId); renderQueue(deckId);
console.log(`📋 Added "${title}" to Queue ${deckId} (${queues[deckId].length} tracks)`); console.log(`Added "${title}" to Queue ${deckId} (${queues[deckId].length} tracks)`);
// Sync with server if in server-side mode // Sync with server if in server-side mode
if (SERVER_SIDE_AUDIO && socket) { if (SERVER_SIDE_AUDIO && socket) {
@ -2607,7 +2607,7 @@ function addToQueue(deckId, file, title) {
function removeFromQueue(deckId, index) { function removeFromQueue(deckId, index) {
const removed = queues[deckId].splice(index, 1)[0]; const removed = queues[deckId].splice(index, 1)[0];
renderQueue(deckId); renderQueue(deckId);
console.log(`🗑️ Removed "${removed.title}" from Queue ${deckId}`); console.log(`Removed "${removed.title}" from Queue ${deckId}`);
// Sync with server if in server-side mode // Sync with server if in server-side mode
if (SERVER_SIDE_AUDIO && socket) { if (SERVER_SIDE_AUDIO && socket) {
@ -2620,7 +2620,7 @@ function clearQueue(deckId) {
const count = queues[deckId].length; const count = queues[deckId].length;
queues[deckId] = []; queues[deckId] = [];
renderQueue(deckId); renderQueue(deckId);
console.log(`🗑️ Cleared Queue ${deckId} (${count} tracks removed)`); console.log(`Cleared Queue ${deckId} (${count} tracks removed)`);
// Sync with server if in server-side mode // Sync with server if in server-side mode
if (SERVER_SIDE_AUDIO && socket) { if (SERVER_SIDE_AUDIO && socket) {
@ -2631,12 +2631,12 @@ function clearQueue(deckId) {
// Load next track from queue // Load next track from queue
function loadNextFromQueue(deckId) { function loadNextFromQueue(deckId) {
if (queues[deckId].length === 0) { if (queues[deckId].length === 0) {
console.log(`📋 Queue ${deckId} is empty`); console.log(`Queue ${deckId} is empty`);
return false; return false;
} }
const next = queues[deckId].shift(); const next = queues[deckId].shift();
console.log(`📋 Loading next from Queue ${deckId}: "${next.title}"`); console.log(`Loading next from Queue ${deckId}: "${next.title}"`);
loadFromServer(deckId, next.file, next.title); loadFromServer(deckId, next.file, next.title);
renderQueue(deckId); renderQueue(deckId);
@ -2723,7 +2723,7 @@ function renderQueue(deckId) {
const [moved] = queues[deckId].splice(fromIndex, 1); const [moved] = queues[deckId].splice(fromIndex, 1);
queues[deckId].splice(index, 0, moved); queues[deckId].splice(index, 0, moved);
renderQueue(deckId); renderQueue(deckId);
console.log(`🔄 Reordered Queue ${deckId}`); console.log(`Reordered Queue ${deckId}`);
} }
}; };
@ -2734,7 +2734,7 @@ function renderQueue(deckId) {
// Auto-load next track when current track ends // Auto-load next track when current track ends
function checkAndLoadNextFromQueue(deckId) { function checkAndLoadNextFromQueue(deckId) {
if (settings.autoPlay && queues[deckId].length > 0) { if (settings.autoPlay && queues[deckId].length > 0) {
console.log(`🎵 Auto-loading next track from Queue ${deckId}...`); console.log(`Auto-loading next track from Queue ${deckId}...`);
setTimeout(() => { setTimeout(() => {
loadNextFromQueue(deckId); loadNextFromQueue(deckId);
}, 500); }, 500);
@ -2799,7 +2799,7 @@ async function loadKeyboardMappings() {
keyboardMappings = data.keymaps; keyboardMappings = data.keymaps;
console.log('✅ Loaded custom keyboard mappings from server'); console.log('✅ Loaded custom keyboard mappings from server');
} else { } else {
console.log(' Using default keyboard mappings'); console.log('Using default keyboard mappings');
} }
} catch (e) { } catch (e) {
console.error('Failed to load keyboard mappings from server:', e); console.error('Failed to load keyboard mappings from server:', e);
@ -2899,7 +2899,7 @@ document.addEventListener('keydown', (e) => {
if (mapping) { if (mapping) {
e.preventDefault(); e.preventDefault();
console.log(`⌨️ Keyboard: ${key}${mapping.label}`); console.log(`Keyboard: ${key}${mapping.label}`);
executeKeyboardAction(mapping.action); executeKeyboardAction(mapping.action);
} }
}); });
@ -2935,7 +2935,7 @@ function createKeyboardSettingsPanel() {
panel.className = 'settings-panel active'; panel.className = 'settings-panel active';
panel.innerHTML = ` panel.innerHTML = `
<div class="panel-header"> <div class="panel-header">
<h2> Keyboard Shortcuts</h2> <h2>Keyboard Shortcuts</h2>
<button onclick="closeKeyboardSettings()"></button> <button onclick="closeKeyboardSettings()"></button>
</div> </div>
<div class="panel-content"> <div class="panel-content">
@ -3078,6 +3078,6 @@ function importKeyboardMappings() {
input.click(); input.click();
} }
// Initialize on load // Initialise on load
loadKeyboardMappings(); loadKeyboardMappings();
console.log('⌨️ Keyboard shortcuts enabled. Press H for help.'); console.log('Keyboard shortcuts enabled. Press H for help.');

259
style.css
View File

@ -33,50 +33,24 @@ body {
} }
body::before { body::before {
content: ''; display: none !important;
position: fixed; /* Completely disabled to prevent UI blocking */
inset: 0;
pointer-events: none;
z-index: 99999;
border: 1px solid rgba(80, 80, 80, 0.1);
box-sizing: border-box;
} }
body.playing-A::before { body.playing-A::before {
border: none; display: none !important;
box-shadow:
0 0 var(--glow-spread) rgba(0, 243, 255, var(--glow-opacity)),
inset 0 0 var(--glow-spread) rgba(0, 243, 255, calc(var(--glow-opacity) * 0.8));
animation: pulse-cyan 3s ease-in-out infinite;
} }
body.playing-B::before { body.playing-B::before {
border: none; display: none !important;
box-shadow:
0 0 var(--glow-spread) rgba(188, 19, 254, var(--glow-opacity)),
inset 0 0 var(--glow-spread) rgba(188, 19, 254, calc(var(--glow-opacity) * 0.8));
animation: pulse-magenta 3s ease-in-out infinite;
} }
body.playing-A.playing-B::before { body.playing-A.playing-B::before {
border: none; display: none !important;
box-shadow:
0 0 var(--glow-spread) rgba(0, 243, 255, var(--glow-opacity)),
0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, var(--glow-opacity)),
inset 0 0 var(--glow-spread) rgba(0, 243, 255, calc(var(--glow-opacity) * 0.6)),
inset 0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 0.6));
animation: pulse-both 3s ease-in-out infinite;
} }
body.listener-glow::before { body.listener-glow::before {
border: none; display: none !important;
box-shadow:
0 0 var(--glow-spread) rgba(0, 80, 255, calc(var(--glow-opacity) * 1.5)),
0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 1.5)),
0 0 calc(var(--glow-spread) * 2.5) rgba(0, 243, 255, calc(var(--glow-opacity) * 0.5)),
inset 0 0 var(--glow-spread) rgba(0, 80, 255, calc(var(--glow-opacity) * 1)),
inset 0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 1));
animation: pulse-listener 4s ease-in-out infinite;
} }
@keyframes pulse-listener { @keyframes pulse-listener {
@ -1621,7 +1595,8 @@ input[type=range] {
} }
body::before { body::before {
border-width: 2px; display: none !important;
/* Completely disabled */
} }
.app-container { .app-container {
@ -1659,6 +1634,14 @@ input[type=range] {
display: flex; display: flex;
} }
.app-container.show-queue-A #queue-A {
display: flex;
}
.app-container.show-queue-B #queue-B {
display: flex;
}
/* Mixer integration: Show mixer combined with active deck in a scrollable view */ /* Mixer integration: Show mixer combined with active deck in a scrollable view */
.app-container.show-deck-A .mixer-section, .app-container.show-deck-A .mixer-section,
.app-container.show-deck-B .mixer-section { .app-container.show-deck-B .mixer-section {
@ -1764,7 +1747,7 @@ input[type=range] {
.streaming-btn { .streaming-btn {
position: fixed; position: fixed;
bottom: 25px; bottom: 25px;
right: 100px; right: 175px;
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 50%; border-radius: 50%;
@ -1793,17 +1776,17 @@ input[type=range] {
.upload-btn { .upload-btn {
position: fixed; position: fixed;
bottom: 25px; bottom: 25px;
right: 170px; right: 100px;
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(145deg, #222, #111); background: linear-gradient(145deg, #222, #111);
border: 2px solid var(--primary-cyan); border: 2px solid #00ff00;
color: var(--primary-cyan); color: #00ff00;
font-size: 1.8rem; font-size: 1.8rem;
cursor: pointer; cursor: pointer;
z-index: 10000; z-index: 10000;
box-shadow: 0 0 20px rgba(0, 255, 255, 0.4); box-shadow: 0 0 20px rgba(0, 255, 0, 0.4);
transition: all 0.3s; transition: all 0.3s;
display: flex; display: flex;
align-items: center; align-items: center;
@ -1812,7 +1795,7 @@ input[type=range] {
.upload-btn:hover { .upload-btn:hover {
transform: scale(1.1); transform: scale(1.1);
box-shadow: 0 0 30px rgba(0, 255, 255, 0.6); box-shadow: 0 0 30px rgba(0, 255, 0, 0.6);
} }
.upload-btn:active { .upload-btn:active {
@ -2431,7 +2414,7 @@ body.listening-active .landscape-prompt {
.keyboard-btn { .keyboard-btn {
position: fixed; position: fixed;
bottom: 25px; bottom: 25px;
right: 100px; right: 250px;
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 50%; border-radius: 50%;
@ -2449,7 +2432,7 @@ body.listening-active .landscape-prompt {
} }
.keyboard-btn:hover { .keyboard-btn:hover {
transform: scale(1.1) rotate(5deg); transform: scale(1.1);
box-shadow: 0 0 30px rgba(255, 187, 0, 0.6); box-shadow: 0 0 30px rgba(255, 187, 0, 0.6);
} }
@ -2886,8 +2869,9 @@ body.listening-active .landscape-prompt {
overflow: hidden; overflow: hidden;
} }
/* Hide library in landscape - focus on DJing */ /* Hide library and queues in landscape - focus on DJing */
.library-section { .library-section,
.queue-section {
display: none !important; display: none !important;
} }
@ -3126,32 +3110,21 @@ body.listening-active .landscape-prompt {
font-size: 1.2rem !important; font-size: 1.2rem !important;
} }
/* Reduce edge border effect intensity */ /* Completely disable edge border effects */
body::before { body::before {
border: 2px solid rgba(80, 80, 80, 0.3) !important; display: none !important;
} }
body.playing-A::before { body.playing-A::before {
border: 10px solid var(--primary-cyan) !important; display: none !important;
box-shadow:
0 0 60px rgba(0, 243, 255, 1),
inset 0 0 60px rgba(0, 243, 255, 0.7) !important;
} }
body.playing-B::before { body.playing-B::before {
border: 10px solid var(--secondary-magenta) !important; display: none !important;
box-shadow:
0 0 60px rgba(188, 19, 254, 1),
inset 0 0 60px rgba(188, 19, 254, 0.7) !important;
} }
body.playing-A.playing-B::before { body.playing-A.playing-B::before {
border: 10px solid var(--primary-cyan) !important; display: none !important;
box-shadow:
0 0 60px rgba(0, 243, 255, 1),
0 0 80px rgba(188, 19, 254, 1),
inset 0 0 60px rgba(0, 243, 255, 0.6),
inset 0 0 80px rgba(188, 19, 254, 0.6) !important;
} }
} }
@ -3277,12 +3250,14 @@ body.listening-active .landscape-prompt {
/* Hide non-active sections in portrait tabs */ /* Hide non-active sections in portrait tabs */
.library-section, .library-section,
.deck, .deck,
.queue-section,
.mixer-section { .mixer-section {
display: none; display: none;
} }
.library-section.active, .library-section.active,
.deck.active { .deck.active,
.queue-section.active {
display: flex !important; display: flex !important;
flex: 1; flex: 1;
width: 100%; width: 100%;
@ -3309,7 +3284,7 @@ body.listening-active .landscape-prompt {
height: 70px; height: 70px;
} }
/* Optimize deck layout for portrait */ /* Optimise deck layout for portrait */
.deck { .deck {
flex-direction: column; flex-direction: column;
overflow-y: auto; overflow-y: auto;
@ -3794,7 +3769,7 @@ body.listening-active .landscape-prompt {
} }
.track-row.loaded-deck-a::before { .track-row.loaded-deck-a::before {
content: 'A'; content: 'A';
position: absolute; position: absolute;
left: 8px; left: 8px;
top: 50%; top: 50%;
@ -3817,7 +3792,7 @@ body.listening-active .landscape-prompt {
} }
.track-row.loaded-deck-b::before { .track-row.loaded-deck-b::before {
content: 'B'; content: 'B';
position: absolute; position: absolute;
right: 8px; right: 8px;
top: 50%; top: 50%;
@ -3846,7 +3821,7 @@ body.listening-active .landscape-prompt {
} }
.track-row.loaded-both::before { .track-row.loaded-both::before {
content: 'A'; content: 'A';
position: absolute; position: absolute;
left: 8px; left: 8px;
top: 50%; top: 50%;
@ -3858,7 +3833,7 @@ body.listening-active .landscape-prompt {
} }
.track-row.loaded-both::after { .track-row.loaded-both::after {
content: 'B'; content: 'B';
position: absolute; position: absolute;
right: 8px; right: 8px;
top: 50%; top: 50%;
@ -3875,3 +3850,157 @@ body.listening-active .landscape-prompt {
padding-left: 40px; padding-left: 40px;
padding-right: 40px; padding-right: 40px;
} }
/* ==========================================
STANDALONE QUEUE SECTIONS FOR MOBILE
========================================== */
.queue-section {
display: none;
flex-direction: column;
padding: 20px;
background: rgba(10, 10, 20, 0.95);
overflow-y: auto;
}
.queue-section.active {
display: flex !important;
flex: 1;
width: 100%;
}
.queue-page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
}
.queue-page-title {
font-family: 'Orbitron', sans-serif;
font-size: 1.5rem;
font-weight: bold;
color: #0ff;
margin: 0;
text-shadow: 0 0 15px rgba(0, 243, 255, 0.6);
}
#queue-B .queue-page-title {
color: #f0f;
text-shadow: 0 0 15px rgba(188, 19, 254, 0.6);
}
#queue-B .queue-item {
border-left-color: #f0f;
}
#queue-B .queue-number {
color: #f0f;
}
/* ==========================================
RESPONSIVE CROSSFADER WIDTH
========================================== */
/* Portrait mode - narrower crossfader */
@media (max-width: 1024px) and (orientation: portrait) {
.mixer-section {
padding: 10px 20px !important;
max-width: 70% !important;
margin: 0 auto !important;
left: 50% !important;
transform: translateX(-50%);
}
.xfader {
width: 100% !important;
}
}
/* Landscape mode - wider crossfader */
@media (max-width: 1024px) and (orientation: landscape) {
.mixer-section {
padding: 8px 40px !important;
width: 100% !important;
max-width: 100% !important;
}
.xfader {
width: 100% !important;
}
}
/* ==========================================
MOBILE FLOATING BUTTONS POSITIONING
========================================== */
@media (max-width: 1024px) {
/* Adjust floating buttons to not overlap with mobile tabs */
.keyboard-btn,
.streaming-btn,
.upload-btn,
.settings-btn {
bottom: 95px !important;
/* Above mobile tabs */
font-size: 0.9rem !important;
}
/* Compact button sizes on mobile */
.keyboard-btn,
.streaming-btn,
.upload-btn,
.settings-btn {
width: 50px !important;
height: 50px !important;
}
/* Adjust spacing for smaller buttons */
.keyboard-btn {
right: 220px !important;
}
.streaming-btn {
right: 155px !important;
}
.upload-btn {
right: 90px !important;
}
.settings-btn {
right: 25px !important;
}
}
/* Extra small screens - stack buttons vertically on right side */
@media (max-width: 480px) {
.keyboard-btn,
.streaming-btn,
.upload-btn,
.settings-btn {
right: 10px !important;
width: 45px !important;
height: 45px !important;
font-size: 0.8rem !important;
}
.keyboard-btn {
bottom: 250px !important;
}
.streaming-btn {
bottom: 195px !important;
}
.upload-btn {
bottom: 140px !important;
}
.settings-btn {
bottom: 85px !important;
}
}