Kaynağa Gözat

Feature/redesign frontend (#18)

* Initial version of redesign

* Ux improvements in playlist edit.

* Restore other action buttons after saving playlist

* Fix Playlist play bug

* Improved add to playlist select + Minor bugfixes

* Safari iOS fixes

* Minor desktop height fixes

* Minor bugfixes

* Various styling fixes. Added fullscreen function for sticky sections

* fix send coordinate bug, pull image from gh

* add changelog

---------

Co-authored-by: Tuan Nguyen <anhtuan.nguyen@me.com>
Thom Koopman 1 yıl önce
ebeveyn
işleme
496b5e3095
8 değiştirilmiş dosya ile 2254 ekleme ve 1128 silme
  1. 9 1
      CHANGELOG.md
  2. 26 8
      app.py
  3. 2 1
      docker-compose.yml
  4. BIN
      static/IMG_9753.png
  5. BIN
      static/UI_1.3.png
  6. 1327 0
      static/main.js
  7. 670 201
      static/style.css
  8. 220 917
      templates/index.html

+ 9 - 1
CHANGELOG.md

@@ -2,7 +2,15 @@
 
 All notable changes to this project will be documented in this file.
 
-## [1.2.0] - Spirograph functionality
+## [1.3.0] Revamped UI
+
+Massive thanks to Thokoop for helping us redesigning the UI just within a few days! The new design looks gorgeous on both PC and mobile. 
+
+![New UI](./static/UI_1.3.png)
+
+![New UI mobile](./static/IMG_9753.png)
+
+## [1.2.0] - Playlist mode, Spirograph functionality
 
 ### Added
 

+ 26 - 8
app.py

@@ -174,7 +174,7 @@ def run_theta_rho_file(file_path):
     # Reset theta after execution or stopping
     reset_theta()
     ser.write("FINISHED\n".encode())
-    
+
 def get_clear_pattern_file(pattern_name):
     """Return a .thr file path based on pattern_name."""
     if pattern_name == "random":
@@ -222,12 +222,12 @@ def run_theta_rho_files(
                 clear_file_path = get_clear_pattern_file(clear_pattern)
                 print(f"Running clear pattern: {clear_file_path}")
                 run_theta_rho_file(clear_file_path)
-            
+
             if not stop_requested:
                 # Run the main pattern
                 print(f"Running pattern {idx + 1} of {len(file_paths)}: {path}")
                 run_theta_rho_file(path)
-            
+
             if idx < len(file_paths) -1:
                 if stop_requested:
                     print("Execution stopped before running the next clear pattern.")
@@ -347,7 +347,7 @@ def run_theta_rho():
     try:
         # Build a list of files to run in sequence
         files_to_run = []
-        
+
         if pre_execution == 'clear_in':
             files_to_run.append('./patterns/clear_from_in.thr')
         elif pre_execution == 'clear_out':
@@ -481,7 +481,6 @@ def send_coordinate():
 
         # Send the coordinate to the Arduino
         send_coordinate_batch(ser, [(theta, rho)])
-        reset_theta()
         return jsonify({"success": True})
     except Exception as e:
         return jsonify({"success": False, "error": str(e)}), 500
@@ -499,7 +498,7 @@ def serial_status():
         'connected': ser.is_open if ser else False,
         'port': ser_port  # Include the port name
     })
-    
+
 if not os.path.exists(PLAYLISTS_FILE):
     with open(PLAYLISTS_FILE, "w") as f:
         json.dump({}, f, indent=2)
@@ -635,7 +634,26 @@ def delete_playlist():
         "success": True,
         "message": f"Playlist '{playlist_name}' deleted"
     })
-    
+
+@app.route('/add_to_playlist', methods=['POST'])
+def add_to_playlist():
+    data = request.json
+    playlist_name = data.get('playlist_name')
+    pattern = data.get('pattern')
+
+    # Load existing playlists
+    with open('playlists.json', 'r') as f:
+        playlists = json.load(f)
+
+    # Add pattern to the selected playlist
+    if playlist_name in playlists:
+        playlists[playlist_name].append(pattern)
+        with open('playlists.json', 'w') as f:
+            json.dump(playlists, f)
+        return jsonify(success=True)
+    else:
+        return jsonify(success=False, error='Playlist not found'), 404
+
 @app.route("/run_playlist", methods=["POST"])
 def run_playlist():
     """
@@ -706,7 +724,7 @@ def run_playlist():
         return jsonify({"success": True, "message": f"Playlist '{playlist_name}' is now running."})
     except Exception as e:
         return jsonify({"success": False, "error": str(e)}), 500
-    
+
 @app.route('/set_speed', methods=['POST'])
 def set_speed():
     """Set the speed for the Arduino."""

+ 2 - 1
docker-compose.yml

@@ -1,6 +1,7 @@
 services:
   flask-app:
-    build: .
+    # build: . # Uncomment this if you need to build 
+    image: ghcr.io/tuanchris/dune-weaver:main # Use latest production image
     restart: always
     ports:
       - "8080:8080" # Map port 8080 of the container to 8080 of the host

BIN
static/IMG_9753.png


BIN
static/UI_1.3.png


+ 1327 - 0
static/main.js

@@ -0,0 +1,1327 @@
+// Global variables
+let selectedFile = null;
+let playlist = [];
+let selectedPlaylistIndex = null;
+let allFiles = [];
+
+// Define constants for log message types
+const LOG_TYPE = {
+    SUCCESS: 'success',
+    WARNING: 'warning',
+    ERROR: 'error',
+    INFO: 'info',
+    DEBUG: 'debug'
+};
+
+// Enhanced logMessage with notification system
+function logMessage(message, type = LOG_TYPE.DEBUG) {
+    const log = document.getElementById('status_log');
+    const header = document.querySelector('header');
+
+    if (!header) {
+        console.error('Error: <header> element not found');
+        return;
+    }
+
+    // Debug messages only go to the status log
+    if (type === LOG_TYPE.DEBUG) {
+        if (!log) {
+            console.error('Error: #status_log element not found');
+            return;
+        }
+        const entry = document.createElement('p');
+        entry.textContent = message;
+        log.appendChild(entry);
+        log.scrollTop = log.scrollHeight; // Scroll to the bottom of the log
+        return;
+    }
+
+    // Clear any existing notifications
+    const existingNotification = header.querySelector('.notification');
+    if (existingNotification) {
+        existingNotification.remove();
+    }
+
+    // Create a notification for other message types
+    const notification = document.createElement('div');
+    notification.className = `notification ${type}`;
+    notification.textContent = message;
+
+    // Add a close button
+    const closeButton = document.createElement('button');
+    closeButton.textContent = '×';
+    closeButton.className = 'close-button';
+    closeButton.onclick = () => {
+        notification.classList.remove('show');
+        setTimeout(() => notification.remove(), 250); // Match transition duration
+    };
+    notification.appendChild(closeButton);
+
+    // Append the notification to the header
+    header.appendChild(notification);
+
+    // Trigger the transition
+    requestAnimationFrame(() => {
+        notification.classList.add('show');
+    });
+
+    // Auto-remove the notification after 5 seconds
+    setTimeout(() => {
+        if (notification.parentNode) {
+            notification.classList.remove('show');
+            setTimeout(() => notification.remove(), 250); // Match transition duration
+        }
+    }, 5000);
+
+    // Also log the message to the status log if available
+    if (log) {
+        const entry = document.createElement('p');
+        entry.textContent = message;
+        log.appendChild(entry);
+        log.scrollTop = log.scrollHeight; // Scroll to the bottom of the log
+    }
+}
+
+function toggleDebugLog() {
+    const statusLog = document.getElementById('status_log');
+    const debugButton = document.getElementById('debug_button');
+
+    if (statusLog.style.display === 'block') {
+        statusLog.style.display = 'none';
+        debugButton.classList.remove('active');
+    } else {
+        statusLog.style.display = 'block';
+        debugButton.classList.add( 'active');
+        statusLog.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Smooth scrolling to the log
+    }
+}
+
+// File selection logic
+async function selectFile(file, listItem) {
+    selectedFile = file;
+
+    // Highlight the selected file
+    document.querySelectorAll('#theta_rho_files li').forEach(li => li.classList.remove('selected'));
+    listItem.classList.add('selected');
+
+    // Update the Remove button visibility
+    const removeButton = document.querySelector('#pattern-preview-container .remove-button');
+    if (file.startsWith('custom_patterns/')) {
+        removeButton.classList.remove('hidden');
+    } else {
+        removeButton.classList.add('hidden');
+    }
+
+    logMessage(`Selected file: ${file}`);
+    await previewPattern(file);
+
+    // Populate the playlist dropdown after selecting a pattern
+    await populatePlaylistDropdown();
+}
+
+// Fetch and display Theta-Rho files
+async function loadThetaRhoFiles() {
+    try {
+        logMessage('Loading Theta-Rho files...');
+        const response = await fetch('/list_theta_rho_files');
+        let files = await response.json();
+
+        files = files.filter(file => file.endsWith('.thr'));
+        // Sort files with custom_patterns on top and all alphabetically sorted
+        const sortedFiles = files.sort((a, b) => {
+            const isCustomA = a.startsWith('custom_patterns/');
+            const isCustomB = b.startsWith('custom_patterns/');
+
+            if (isCustomA && !isCustomB) return -1; // a comes first
+            if (!isCustomA && isCustomB) return 1;  // b comes first
+            return a.localeCompare(b);             // Alphabetical comparison
+        });
+
+        allFiles = sortedFiles; // Update global files
+        displayFiles(sortedFiles); // Display sorted files
+
+        logMessage('Theta-Rho files loaded and sorted successfully.');
+    } catch (error) {
+        logMessage(`Error loading Theta-Rho files: ${error.message}`, 'error');
+    }
+}
+
+// Display files in the UI
+function displayFiles(files) {
+    const ul = document.getElementById('theta_rho_files');
+    if (!ul) {
+        logMessage('Error: File list container not found');
+        return;
+    }
+    ul.innerHTML = ''; // Clear existing list
+
+    files.forEach(file => {
+        const li = document.createElement('li');
+        li.textContent = file;
+        li.classList.add('file-item');
+
+        // Attach file selection handler
+        li.onclick = () => selectFile(file, li);
+
+        ul.appendChild(li);
+    });
+}
+
+// Filter files by search input
+function searchPatternFiles() {
+    const searchInput = document.getElementById('search_pattern').value.toLowerCase();
+    const filteredFiles = allFiles.filter(file => file.toLowerCase().includes(searchInput));
+    displayFiles(filteredFiles);
+}
+
+// Upload a new Theta-Rho file
+async function uploadThetaRho() {
+    const fileInput = document.getElementById('upload_file');
+    const file = fileInput.files[0];
+    if (!file) {
+        logMessage('No file selected for upload.', LOG_TYPE.ERROR);
+        return;
+    }
+
+    try {
+        logMessage(`Uploading file: ${file.name}...`);
+        const formData = new FormData();
+        formData.append('file', file);
+
+        const response = await fetch('/upload_theta_rho', {
+            method: 'POST',
+            body: formData
+        });
+
+        const result = await response.json();
+        if (result.success) {
+            logMessage(`File uploaded successfully: ${file.name}`, LOG_TYPE.SUCCESS);
+            fileInput.value = '';
+            await loadThetaRhoFiles();
+        } else {
+            logMessage(`Failed to upload file: ${file.name}`, LOG_TYPE.ERROR);
+        }
+    } catch (error) {
+        logMessage(`Error uploading file: ${error.message}`);
+    }
+}
+
+async function runThetaRho() {
+    if (!selectedFile) {
+        logMessage("No file selected to run.");
+        return;
+    }
+
+    // Get the selected pre-execution action
+    const preExecutionAction = document.querySelector('input[name="pre_execution"]:checked').value;
+
+    logMessage(`Running file: ${selectedFile} with pre-execution action: ${preExecutionAction}...`);
+    const response = await fetch('/run_theta_rho', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ file_name: selectedFile, pre_execution: preExecutionAction })
+    });
+
+    const result = await response.json();
+    if (result.success) {
+        logMessage(`Pattern running: ${selectedFile}`, LOG_TYPE.SUCCESS);
+    } else {
+        logMessage(`Failed to run file: ${selectedFile}`,LOG_TYPE.ERROR);
+    }
+}
+
+async function stopExecution() {
+    logMessage('Stopping execution...');
+    const response = await fetch('/stop_execution', { method: 'POST' });
+    const result = await response.json();
+    if (result.success) {
+        logMessage('Execution stopped.',LOG_TYPE.SUCCESS);
+    } else {
+        logMessage('Failed to stop execution.',LOG_TYPE.ERROR);
+    }
+}
+
+function removeCurrentPattern() {
+    if (!selectedFile) {
+        logMessage('No file selected to remove.', LOG_TYPE.ERROR);
+        return;
+    }
+
+    if (!selectedFile.startsWith('custom_patterns/')) {
+        logMessage('Only custom patterns can be removed.', LOG_TYPE.WARNING);
+        return;
+    }
+
+    removeCustomPattern(selectedFile);
+}
+
+// Delete the selected file
+async function removeCustomPattern(fileName) {
+    const userConfirmed = confirm(`Are you sure you want to delete the pattern "${fileName}"?`);
+    if (!userConfirmed) return;
+
+    try {
+        logMessage(`Deleting pattern: ${fileName}...`);
+        const response = await fetch('/delete_theta_rho_file', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({ file_name: fileName })
+        });
+
+        const result = await response.json();
+        if (result.success) {
+            logMessage(`File deleted successfully: ${selectedFile}`, LOG_TYPE.SUCCESS);
+
+            // Close the preview container
+            const previewContainer = document.getElementById('pattern-preview-container');
+            if (previewContainer) {
+                previewContainer.classList.add('hidden');
+                previewContainer.classList.remove('visible');
+            }
+
+            // Clear the selected file and refresh the file list
+            selectedFile = null;
+            await loadThetaRhoFiles(); // Refresh the file list
+        } else {
+            logMessage(`Failed to delete pattern "${fileName}": ${result.error}`, LOG_TYPE.ERROR);
+        }
+    } catch (error) {
+        logMessage(`Error deleting pattern: ${error.message}`);
+    }
+}
+
+// Preview a Theta-Rho file
+async function previewPattern(fileName) {
+    try {
+        logMessage(`Fetching data to preview file: ${fileName}...`);
+        const response = await fetch('/preview_thr', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({ file_name: fileName })
+        });
+
+        const result = await response.json();
+        if (result.success) {
+            const coordinates = result.coordinates;
+            renderPattern(coordinates);
+
+            // Update coordinate display
+            const firstCoord = coordinates[0];
+            const lastCoord = coordinates[coordinates.length - 1];
+            document.getElementById('first_coordinate').textContent = `First Coordinate: θ=${firstCoord[0]}, ρ=${firstCoord[1]}`;
+            document.getElementById('last_coordinate').textContent = `Last Coordinate: θ=${lastCoord[0]}, ρ=${lastCoord[1]}`;
+
+            // Show the preview container
+            const previewContainer = document.getElementById('pattern-preview-container');
+            if (previewContainer) {
+                previewContainer.classList.remove('hidden');
+                previewContainer.classList.add('visible');
+            }
+
+            // Close the "Add to Playlist" container if it is open
+            const addToPlaylistContainer = document.getElementById('add-to-playlist-container');
+            if (addToPlaylistContainer && !addToPlaylistContainer.classList.contains('hidden')) {
+                toggleSecondaryButtons('add-to-playlist-container'); // Hide the container
+            }
+
+        } else {
+            logMessage(`Failed to fetch preview for file: ${fileName}`, LOG_TYPE.WARNING);
+        }
+    } catch (error) {
+        logMessage(`Error previewing pattern: ${error.message}`, LOG_TYPE.WARNING);
+    }
+}
+
+// Render the pattern on a canvas
+function renderPattern(coordinates) {
+    const canvas = document.getElementById('patternPreviewCanvas');
+    if (!canvas) {
+        logMessage('Error: Canvas not found');
+        return;
+    }
+
+    const ctx = canvas.getContext('2d');
+
+    // Account for device pixel ratio
+    const dpr = window.devicePixelRatio || 1;
+    const rect = canvas.getBoundingClientRect();
+
+    canvas.width = rect.width * dpr;  // Scale canvas width for high DPI
+    canvas.height = rect.height * dpr;  // Scale canvas height for high DPI
+
+    ctx.scale(dpr, dpr);  // Scale drawing context
+
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+    const centerX = rect.width / 2;  // Use bounding client rect dimensions
+    const centerY = rect.height / 2;
+    const maxRho = Math.max(...coordinates.map(coord => coord[1]));
+    const scale = Math.min(rect.width, rect.height) / (2 * maxRho); // Scale to fit
+
+    ctx.beginPath();
+    ctx.strokeStyle = 'white';
+    coordinates.forEach(([theta, rho], index) => {
+        const x = centerX + rho * Math.cos(theta) * scale;
+        const y = centerY - rho * Math.sin(theta) * scale;
+        if (index === 0) ctx.moveTo(x, y);
+        else ctx.lineTo(x, y);
+    });
+    ctx.stroke();
+    logMessage('Pattern preview rendered.');
+}
+
+
+async function moveToCenter() {
+    logMessage('Moving to center...', LOG_TYPE.INFO);
+    const response = await fetch('/move_to_center', { method: 'POST' });
+    const result = await response.json();
+    if (result.success) {
+        logMessage('Moved to center successfully.', LOG_TYPE.SUCCESS);
+    } else {
+        logMessage(`Failed to move to center: ${result.error}`, LOG_TYPE.ERROR);
+    }
+}
+
+async function moveToPerimeter() {
+    logMessage('Moving to perimeter...', LOG_TYPE.INFO);
+    const response = await fetch('/move_to_perimeter', { method: 'POST' });
+    const result = await response.json();
+    if (result.success) {
+        logMessage('Moved to perimeter successfully.', LOG_TYPE.SUCCESS);
+    } else {
+        logMessage(`Failed to move to perimeter: ${result.error}`, LOG_TYPE.ERROR);
+    }
+}
+
+async function sendCoordinate() {
+    const theta = parseFloat(document.getElementById('theta_input').value);
+    const rho = parseFloat(document.getElementById('rho_input').value);
+
+    if (isNaN(theta) || isNaN(rho)) {
+        logMessage('Invalid input: θ and ρ must be numbers.', LOG_TYPE.ERROR);
+        return;
+    }
+
+    logMessage(`Sending coordinate: θ=${theta}, ρ=${rho}...`);
+    const response = await fetch('/send_coordinate', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ theta, rho })
+    });
+
+    const result = await response.json();
+    if (result.success) {
+        logMessage(`Coordinate executed successfully: θ=${theta}, ρ=${rho}`, LOG_TYPE.SUCCESS);
+    } else {
+        logMessage(`Failed to execute coordinate: ${result.error}`, LOG_TYPE.ERROR);
+    }
+}
+
+async function sendHomeCommand() {
+    const response = await fetch('/send_home', { method: 'POST' });
+    const result = await response.json();
+    if (result.success) {
+        logMessage('HOME command sent successfully.', LOG_TYPE.SUCCESS);
+    } else {
+        logMessage('Failed to send HOME command.', LOG_TYPE.ERROR);
+    }
+}
+
+async function runClearIn() {
+    await runFile('clear_from_in.thr');
+}
+
+async function runClearOut() {
+    await runFile('clear_from_out.thr');
+}
+
+async function runFile(fileName) {
+    const response = await fetch(`/run_theta_rho_file/${fileName}`, { method: 'POST' });
+    const result = await response.json();
+    if (result.success) {
+        logMessage(`Running file: ${fileName}`, LOG_TYPE.SUCCESS);
+    } else {
+        logMessage(`Failed to run file: ${fileName}`, LOG_TYPE.ERROR);
+    }
+}
+
+// Serial Connection Status
+async function checkSerialStatus() {
+    const response = await fetch('/serial_status');
+    const status = await response.json();
+    const statusElement = document.getElementById('serial_status');
+    const statusHeaderElement = document.getElementById('serial_status_header');
+    const serialPortsContainer = document.getElementById('serial_ports_container');
+
+    const connectButton = document.querySelector('button[onclick="connectSerial()"]');
+    const disconnectButton = document.querySelector('button[onclick="disconnectSerial()"]');
+    const restartButton = document.querySelector('button[onclick="restartSerial()"]');
+
+    if (status.connected) {
+        const port = status.port || 'Unknown'; // Fallback if port is undefined
+        statusElement.textContent = `Connected to ${port}`;
+        statusElement.classList.add('connected');
+        statusElement.classList.remove('not-connected');
+        logMessage(`Reconnected to serial port: ${port}`);
+
+        // Update header status
+        statusHeaderElement.classList.add('connected');
+        statusHeaderElement.classList.remove('not-connected');
+
+        // Hide Available Ports and show disconnect/restart buttons
+        serialPortsContainer.style.display = 'none';
+        connectButton.style.display = 'none';
+        disconnectButton.style.display = 'inline-block';
+        restartButton.style.display = 'inline-block';
+    } else {
+        statusElement.textContent = 'Not connected';
+        statusElement.classList.add('not-connected');
+        statusElement.classList.remove('connected');
+        logMessage('No active serial connection.');
+
+        // Update header status
+        statusHeaderElement.classList.add('not-connected');
+        statusHeaderElement.classList.remove('connected');
+
+        // Show Available Ports and the connect button
+        serialPortsContainer.style.display = 'block';
+        connectButton.style.display = 'inline-block';
+        disconnectButton.style.display = 'none';
+        restartButton.style.display = 'none';
+
+        // Attempt to auto-load available ports
+        await loadSerialPorts();
+    }
+}
+
+async function loadSerialPorts() {
+    const response = await fetch('/list_serial_ports');
+    const ports = await response.json();
+    const select = document.getElementById('serial_ports');
+    select.innerHTML = '';
+    ports.forEach(port => {
+        const option = document.createElement('option');
+        option.value = port;
+        option.textContent = port;
+        select.appendChild(option);
+    });
+    logMessage('Serial ports loaded.');
+}
+
+async function connectSerial() {
+    const port = document.getElementById('serial_ports').value;
+    const response = await fetch('/connect_serial', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ port })
+    });
+    const result = await response.json();
+    if (result.success) {
+        logMessage(`Connected to serial port: ${port}`, LOG_TYPE.SUCCESS);
+        // Refresh the status
+        await checkSerialStatus();
+    } else {
+        logMessage(`Error connecting to serial port: ${result.error}`, LOG_TYPE.ERROR);
+    }
+}
+
+async function disconnectSerial() {
+    const response = await fetch('/disconnect_serial', { method: 'POST' });
+    const result = await response.json();
+    if (result.success) {
+        logMessage('Serial port disconnected.', LOG_TYPE.SUCCESS);
+        // Refresh the status
+        await checkSerialStatus();
+    } else {
+        logMessage(`Error disconnecting: ${result.error}`, LOG_TYPE.ERROR);
+    }
+}
+
+async function restartSerial() {
+    const port = document.getElementById('serial_ports').value;
+    const response = await fetch('/restart_serial', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ port })
+    });
+    const result = await response.json();
+    if (result.success) {
+        document.getElementById('serial_status').textContent = `Restarted connection to ${port}`;
+        logMessage('Serial connection restarted.', LOG_TYPE.SUCCESS);
+
+        // No need to change visibility for restart
+    } else {
+        logMessage(`Error restarting serial connection: ${result.error}`, LOG_TYPE.ERROR);
+    }
+}
+
+
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+//  PART A: Loading / listing playlists from the server
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+async function loadAllPlaylists() {
+    try {
+        const response = await fetch('/list_all_playlists'); // GET
+        const allPlaylists = await response.json();          // e.g. ["My Playlist", "Summer", ...]
+        displayAllPlaylists(allPlaylists);
+    } catch (err) {
+        logMessage(`Error loading playlists: ${err}`, LOG_TYPE.ERROR);
+    }
+}
+
+// Function to display all playlists with Load, Run, and Delete buttons
+function displayAllPlaylists(playlists) {
+    const ul = document.getElementById('all_playlists');
+    ul.innerHTML = ''; // Clear current list
+
+    playlists.forEach(playlistName => {
+        const li = document.createElement('li');
+        li.textContent = playlistName;
+        li.classList.add('playlist-item'); // Add a class for styling
+
+        // Attach click event to handle selection
+        li.onclick = () => {
+            // Remove 'selected' class from all items
+            document.querySelectorAll('#all_playlists li').forEach(item => {
+                item.classList.remove('selected');
+            });
+
+            // Add 'selected' class to the clicked item
+            li.classList.add('selected');
+
+            // Open the playlist editor for the selected playlist
+            openPlaylistEditor(playlistName);
+        };
+
+        ul.appendChild(li);
+    });
+}
+
+// Cancel changes and close the editor
+function cancelPlaylistChanges() {
+    playlist = [...originalPlaylist]; // Revert to the original playlist
+    isPlaylistChanged = false;
+    toggleSaveCancelButtons(false); // Hide the save and cancel buttons
+    refreshPlaylistUI(); // Refresh the UI with the original state
+    closeStickySection('playlist-editor'); // Close the editor
+}
+
+// Open the playlist editor
+function openPlaylistEditor(playlistName) {
+    logMessage(`Opening editor for playlist: ${playlistName}`);
+    const editorSection = document.getElementById('playlist-editor');
+
+    // Update the displayed playlist name
+    document.getElementById('playlist_name_display').textContent = playlistName;
+
+    // Store the current playlist name for renaming
+    document.getElementById('playlist_name_input').value = playlistName;
+
+    editorSection.classList.remove('hidden');
+    editorSection.classList.add('visible');
+
+    loadPlaylist(playlistName);
+}
+
+
+// Function to run the selected playlist with specified parameters
+async function runPlaylist() {
+    const playlistName = document.getElementById('playlist_name_display').textContent;
+
+    if (!playlistName) {
+        logMessage("No playlist selected to run.");
+        return;
+    }
+
+    const pauseTimeInput = document.getElementById('pause_time').value;
+    const clearPatternSelect = document.getElementById('clear_pattern').value;
+    const runMode = document.querySelector('input[name="run_mode"]:checked').value;
+    const shuffle = document.getElementById('shuffle_playlist').checked;
+
+    const pauseTime = parseFloat(pauseTimeInput);
+    if (isNaN(pauseTime) || pauseTime < 0) {
+        logMessage("Invalid pause time. Please enter a non-negative number.", LOG_TYPE.WARNING);
+        return;
+    }
+
+    logMessage(`Running playlist: ${playlistName} with pause_time=${pauseTime}, clear_pattern=${clearPatternSelect}, run_mode=${runMode}, shuffle=${shuffle}.`);
+
+    try {
+        const response = await fetch('/run_playlist', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({
+                playlist_name: playlistName,
+                pause_time: pauseTime,
+                clear_pattern: clearPatternSelect,
+                run_mode: runMode,
+                shuffle: shuffle
+            })
+        });
+
+        const result = await response.json();
+
+        if (result.success) {
+            logMessage(`Playlist "${playlistName}" is now running.`, LOG_TYPE.SUCCESS);
+        } else {
+            logMessage(`Failed to run playlist "${playlistName}": ${result.error}`, LOG_TYPE.ERROR);
+        }
+    } catch (error) {
+        logMessage(`Error running playlist "${playlistName}": ${error.message}`, LOG_TYPE.ERROR);
+    }
+}
+
+// Track changes in the playlist
+let originalPlaylist = [];
+let isPlaylistChanged = false;
+
+// Load playlist and set the original state
+async function loadPlaylist(playlistName) {
+    try {
+        logMessage(`Loading playlist: ${playlistName}`);
+        const response = await fetch(`/get_playlist?name=${encodeURIComponent(playlistName)}`);
+
+        if (!response.ok) {
+            throw new Error(`HTTP error! Status: ${response.status}`);
+        }
+
+        const data = await response.json();
+
+        if (!data.name) {
+            throw new Error('Playlist name is missing in the response.');
+        }
+
+        // Populate playlist items and set original state
+        playlist = data.files || [];
+        originalPlaylist = [...playlist]; // Clone the playlist as the original
+        isPlaylistChanged = false; // Reset change tracking
+        toggleSaveCancelButtons(false); // Hide the save and cancel buttons initially
+        refreshPlaylistUI();
+        logMessage(`Loaded playlist: "${playlistName}" with ${playlist.length} file(s).`);
+    } catch (err) {
+        logMessage(`Error loading playlist: ${err.message}`, LOG_TYPE.ERROR);
+        console.error('Error details:', err);
+    }
+}
+
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+//  PART B: Creating or Saving (Overwriting) a Playlist
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+// Instead of separate create/modify functions, we’ll unify them:
+async function savePlaylist() {
+    const name =  document.getElementById('playlist_name_display').textContent
+    if (!name) {
+        logMessage("Please enter a playlist name.");
+        return;
+    }
+    if (playlist.length === 0) {
+        logMessage("No files in this playlist. Add files first.");
+        return;
+    }
+
+    logMessage(`Saving playlist "${name}" with ${playlist.length} file(s)...`);
+
+    try {
+        // We can use /create_playlist or /modify_playlist. They do roughly the same in our single-file approach.
+        // Let's use /create_playlist to always overwrite or create anew.
+        const response = await fetch('/create_playlist', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({
+                name: name,
+                files: playlist
+            })
+        });
+        const result = await response.json();
+        if (result.success) {
+            logMessage(`Playlist "${name}" with ${playlist.length} patterns saved`, LOG_TYPE.SUCCESS);
+            // Reload the entire list of playlists to reflect changes
+            // Check for changes and refresh the UI
+            detectPlaylistChanges();
+            refreshPlaylistUI();
+
+            // Restore default action buttons
+            toggleSaveCancelButtons(false);
+        } else {
+            logMessage(`Failed to save playlist: ${result.error}`, LOG_TYPE.ERROR);
+        }
+    } catch (err) {
+        logMessage(`Error saving playlist: ${err}`);
+    }
+}
+
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+//  PART C: Renaming and Deleting a playlist
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// Toggle the rename playlist input
+function populatePlaylistDropdown() {
+    return fetch('/list_all_playlists')
+        .then(response => response.json())
+        .then(playlists => {
+            const select = document.getElementById('select-playlist');
+            select.innerHTML = ''; // Clear existing options
+
+            // Retrieve the saved playlist from the cookie
+            const savedPlaylist = getCookie('selected_playlist');
+
+            playlists.forEach(playlist => {
+                const option = document.createElement('option');
+                option.value = playlist;
+                option.textContent = playlist;
+
+                // Mark the saved playlist as selected
+                if (playlist === savedPlaylist) {
+                    option.selected = true;
+                }
+
+                select.appendChild(option);
+            });
+
+            // Attach the onchange event listener after populating the dropdown
+            select.addEventListener('change', function () {
+                const selectedPlaylist = this.value;
+                setCookie('selected_playlist', selectedPlaylist, 7); // Save to cookie
+                logMessage(`Selected playlist saved: ${selectedPlaylist}`);
+            });
+
+            logMessage('Playlist dropdown populated, event listener attached, and saved playlist restored.');
+        })
+        .catch(error => logMessage(`Error fetching playlists: ${error.message}`, LOG_TYPE.ERROR));
+}
+populatePlaylistDropdown().then(() => {
+    loadSettingsFromCookies(); // Restore selected playlist after populating the dropdown
+});
+
+// Confirm and save the renamed playlist
+async function confirmAddPlaylist() {
+    const playlistNameInput = document.getElementById('new_playlist_name');
+    const playlistName = playlistNameInput.value.trim();
+
+    if (!playlistName) {
+        logMessage('Playlist name cannot be empty.', LOG_TYPE.ERROR);
+        return;
+    }
+
+    try {
+        logMessage(`Adding new playlist: "${playlistName}"...`);
+        const response = await fetch('/create_playlist', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({
+                name: playlistName,
+                files: [] // New playlist starts empty
+            })
+        });
+
+        const result = await response.json();
+        if (result.success) {
+            logMessage(`Playlist "${playlistName}" created successfully.`,  LOG_TYPE.SUCCESS);
+
+            // Clear the input field
+            playlistNameInput.value = '';
+
+            // Refresh the playlist list
+            loadAllPlaylists();
+
+            // Hide the add playlist container
+            toggleSecondaryButtons('add-playlist-container');
+        } else {
+            logMessage(`Failed to create playlist: ${result.error}`, LOG_TYPE.ERROR);
+        }
+    } catch (error) {
+        logMessage(`Error creating playlist: ${error.message}`);
+    }
+}
+
+
+async function confirmRenamePlaylist() {
+    const newName = document.getElementById('playlist_name_input').value.trim();
+    const currentName = document.getElementById('playlist_name_display').textContent;
+
+    if (!newName) {
+        logMessage("New playlist name cannot be empty.", LOG_TYPE.ERROR);
+        return;
+    }
+
+    if (newName === currentName) {
+        logMessage("New playlist name is the same as the current name. No changes made.",  LOG_TYPE.WARNING);
+        toggleSecondaryButtons('rename-playlist-container'); // Close the rename container
+        return;
+    }
+
+    try {
+        // Step 1: Create/Modify the playlist with the new name
+        const createResponse = await fetch('/modify_playlist', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({
+                name: newName,
+                files: playlist // Ensure `playlist` contains the current list of files
+            })
+        });
+
+        const createResult = await createResponse.json();
+        if (createResult.success) {
+            logMessage(createResult.message, LOG_TYPE.SUCCESS);
+
+            // Step 2: Delete the old playlist
+            const deleteResponse = await fetch('/delete_playlist', {
+                method: 'DELETE',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ name: currentName })
+            });
+
+            const deleteResult = await deleteResponse.json();
+            if (deleteResult.success) {
+                logMessage(deleteResult.message);
+
+                // Update the UI with the new name
+                document.getElementById('playlist_name_display').textContent = newName;
+
+                // Refresh playlists list
+                loadAllPlaylists();
+
+                // Close the rename container and restore original action buttons
+                toggleSecondaryButtons('rename-playlist-container');
+            } else {
+                logMessage(`Failed to delete old playlist: ${deleteResult.error}`, LOG_TYPE.ERROR);
+            }
+        } else {
+            logMessage(`Failed to rename playlist: ${createResult.error}`, LOG_TYPE.ERROR);
+        }
+    } catch (error) {
+        logMessage(`Error renaming playlist: ${error.message}`);
+    }
+}
+
+// Delete the currently opened playlist
+async function deleteCurrentPlaylist() {
+    const playlistName = document.getElementById('playlist_name_display').textContent;
+
+    if (!confirm(`Are you sure you want to delete the playlist "${playlistName}"? This action cannot be undone.`)) {
+        return;
+    }
+
+    try {
+        const response = await fetch('/delete_playlist', {
+            method: 'DELETE',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({ name: playlistName })
+        });
+
+        const result = await response.json();
+        if (result.success) {
+            logMessage(`Playlist "${playlistName}" deleted.`, LOG_TYPE.INFO);
+            closeStickySection('playlist-editor');
+            loadAllPlaylists();
+        } else {
+            logMessage(`Failed to delete playlist: ${result.error}`,  LOG_TYPE.ERROR);
+        }
+    } catch (error) {
+        logMessage(`Error deleting playlist: ${error.message}`);
+    }
+}
+
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+//  PART D: Local playlist array UI
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+// Refresh the playlist UI and detect changes
+function refreshPlaylistUI() {
+    const ul = document.getElementById('playlist_items');
+    if (!ul) {
+        logMessage('Error: Playlist container not found');
+        return;
+    }
+    ul.innerHTML = ''; // Clear existing items
+
+    if (playlist.length === 0) {
+        // Add a placeholder if the playlist is empty
+        const emptyLi = document.createElement('li');
+        emptyLi.textContent = 'No items in the playlist.';
+        emptyLi.classList.add('empty-placeholder'); // Optional: Add a class for styling
+        ul.appendChild(emptyLi);
+        return;
+    }
+
+    playlist.forEach((file, index) => {
+        const li = document.createElement('li');
+
+        // Add filename in a span
+        const filenameSpan = document.createElement('span');
+        filenameSpan.textContent = file;
+        filenameSpan.classList.add('filename'); // Add a class for styling
+        li.appendChild(filenameSpan);
+
+        // Move Up button
+        const moveUpBtn = document.createElement('button');
+        moveUpBtn.textContent = '▲'; // Up arrow symbol
+        moveUpBtn.classList.add('move-button');
+        moveUpBtn.onclick = () => {
+            if (index > 0) {
+                const temp = playlist[index - 1];
+                playlist[index - 1] = playlist[index];
+                playlist[index] = temp;
+                detectPlaylistChanges(); // Check for changes
+                refreshPlaylistUI();
+            }
+        };
+        li.appendChild(moveUpBtn);
+
+        // Move Down button
+        const moveDownBtn = document.createElement('button');
+        moveDownBtn.textContent = '▼'; // Down arrow symbol
+        moveDownBtn.classList.add('move-button');
+        moveDownBtn.onclick = () => {
+            if (index < playlist.length - 1) {
+                const temp = playlist[index + 1];
+                playlist[index + 1] = playlist[index];
+                playlist[index] = temp;
+                detectPlaylistChanges(); // Check for changes
+                refreshPlaylistUI();
+            }
+        };
+        li.appendChild(moveDownBtn);
+
+        // Remove button
+        const removeBtn = document.createElement('button');
+        removeBtn.textContent = '✖';
+        removeBtn.classList.add('remove-button');
+        removeBtn.onclick = () => {
+            playlist.splice(index, 1);
+            detectPlaylistChanges(); // Check for changes
+            refreshPlaylistUI();
+        };
+        li.appendChild(removeBtn);
+
+        ul.appendChild(li);
+    });
+}
+
+// Toggle the visibility of the save and cancel buttons
+function toggleSaveCancelButtons(show) {
+    const actionButtons = document.querySelector('#playlist-editor .action-buttons');
+    if (actionButtons) {
+        // Show/hide all buttons except Save and Cancel
+        actionButtons.querySelectorAll('button:not(.save-cancel)').forEach(button => {
+            button.style.display = show ? 'none' : 'inline-block';
+        });
+
+        // Show/hide Save and Cancel buttons
+        actionButtons.querySelectorAll('.save-cancel').forEach(button => {
+            button.style.display = show ? 'inline-block' : 'none';
+        });
+    } else {
+        logMessage('Error: Action buttons container not found.', LOG_TYPE.ERROR);
+    }
+}
+
+// Detect changes in the playlist
+function detectPlaylistChanges() {
+    isPlaylistChanged = JSON.stringify(originalPlaylist) !== JSON.stringify(playlist);
+    toggleSaveCancelButtons(isPlaylistChanged);
+}
+
+
+// Toggle the "Add to Playlist" section
+function toggleSecondaryButtons(containerId, onShowCallback = null) {
+    const container = document.getElementById(containerId);
+    if (!container) {
+        logMessage(`Error: Element with ID "${containerId}" not found`);
+        return;
+    }
+
+    // Find the .action-buttons element preceding the container
+    const previousActionButtons = container.previousElementSibling?.classList.contains('action-buttons')
+        ? container.previousElementSibling
+        : null;
+
+    if (container.classList.contains('hidden')) {
+        // Show the container
+        container.classList.remove('hidden');
+
+        // Hide the previous .action-buttons element
+        if (previousActionButtons) {
+            previousActionButtons.style.display = 'none';
+        }
+
+        // Optional callback for custom logic when showing the container
+        if (onShowCallback) {
+            onShowCallback();
+        }
+    } else {
+        // Hide the container
+        container.classList.add('hidden');
+
+        // Restore the previous .action-buttons element
+        if (previousActionButtons) {
+            previousActionButtons.style.display = 'flex';
+        }
+    }
+}
+
+// Add the selected pattern to the selected playlist
+async function saveToPlaylist() {
+    const playlist = document.getElementById('select-playlist').value;
+    if (!playlist) {
+        logMessage('No playlist selected.', LOG_TYPE.ERROR);
+        return;
+    }
+    if (!selectedFile) {
+        logMessage('No pattern selected to add.', LOG_TYPE.ERROR);
+        return;
+    }
+
+    try {
+        logMessage(`Adding pattern "${selectedFile}" to playlist "${playlist}"...`);
+        const response = await fetch('/add_to_playlist', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({ playlist_name: playlist, pattern: selectedFile })
+        });
+
+        const result = await response.json();
+        if (result.success) {
+            logMessage(`Pattern "${selectedFile}" successfully added to playlist "${playlist}".`, LOG_TYPE.SUCCESS);
+
+            // Reset the UI state via toggleSecondaryButtons
+            toggleSecondaryButtons('add-to-playlist-container', () => {
+                const selectPlaylist = document.getElementById('select-playlist');
+                selectPlaylist.value = ''; // Clear the selection
+            });
+        } else {
+            logMessage(`Failed to add pattern to playlist: ${result.error}`, LOG_TYPE.ERROR);
+        }
+    } catch (error) {
+        logMessage(`Error adding pattern to playlist: ${error.message}`);
+    }
+}
+
+async function changeSpeed() {
+    const speedInput = document.getElementById('speed_input');
+    const speed = parseFloat(speedInput.value);
+
+    if (isNaN(speed) || speed <= 0) {
+        logMessage('Invalid speed. Please enter a positive number.');
+        return;
+    }
+
+    logMessage(`Setting speed to: ${speed}...`);
+    const response = await fetch('/set_speed', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ speed })
+    });
+
+    const result = await response.json();
+    if (result.success) {
+        document.getElementById('speed_status').textContent = `Current Speed: ${speed}`;
+        logMessage(`Speed set to: ${speed}`, LOG_TYPE.SUCCESS);
+    } else {
+        logMessage(`Failed to set speed: ${result.error}`, LOG_TYPE.ERROR);
+    }
+}
+
+// Function to close any sticky section
+function closeStickySection(sectionId) {
+    const section = document.getElementById(sectionId);
+    if (section) {
+        section.classList.remove('visible');
+        section.classList.remove('fullscreen');
+        section.classList.add('hidden');
+        // Reset the fullscreen button text if it exists
+        const fullscreenButton = section.querySelector('.fullscreen-button');
+        if (fullscreenButton) {
+            fullscreenButton.textContent = '⛶'; // Reset to enter fullscreen icon/text
+        }
+
+        logMessage(`Closed section: ${sectionId}`);
+
+        if(sectionId === 'playlist-editor') {
+            document.querySelectorAll('#all_playlists .playlist-item').forEach(item => {
+                item.classList.remove('selected');
+            });
+        }
+
+        if(sectionId === 'pattern-preview-container') {
+            document.querySelectorAll('#theta_rho_files .file-item').forEach(item => {
+                item.classList.remove('selected');
+            });
+        }
+
+    } else {
+        logMessage(`Error: Section with ID "${sectionId}" not found`);
+    }
+}
+
+function attachFullScreenListeners() {
+    // Add event listener to all fullscreen buttons
+    document.querySelectorAll('.fullscreen-button').forEach(button => {
+        button.addEventListener('click', function () {
+            const stickySection = this.closest('.sticky'); // Find the closest sticky section
+            if (stickySection) {
+                stickySection.classList.toggle('fullscreen'); // Toggle fullscreen class
+
+                // Update button icon or text
+                if (stickySection.classList.contains('fullscreen')) {
+                    this.textContent = '-'; // Exit fullscreen icon/text
+                } else {
+                    this.textContent = '⛶'; // Enter fullscreen icon/text
+                }
+            } else {
+                console.error('Error: Fullscreen button is not inside a sticky section.');
+            }
+        });
+    });
+}
+
+
+// Utility function to manage cookies
+function setCookie(name, value, days) {
+    const date = new Date();
+    date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
+    document.cookie = `${name}=${value};expires=${date.toUTCString()};path=/`;
+}
+
+function getCookie(name) {
+    const nameEQ = `${name}=`;
+    const cookies = document.cookie.split(';');
+    for (let i = 0; i < cookies.length; i++) {
+        let cookie = cookies[i].trim();
+        if (cookie.startsWith(nameEQ)) {
+            return cookie.substring(nameEQ.length);
+        }
+    }
+    return null;
+}
+
+
+// Save settings to cookies
+function saveSettingsToCookies() {
+    // Save the pause time
+    const pauseTime = document.getElementById('pause_time').value;
+    setCookie('pause_time', pauseTime, 7);
+
+    // Save the clear pattern
+    const clearPattern = document.getElementById('clear_pattern').value;
+    setCookie('clear_pattern', clearPattern, 7);
+
+    // Save the run mode
+    const runMode = document.querySelector('input[name="run_mode"]:checked').value;
+    setCookie('run_mode', runMode, 7);
+
+    // Save shuffle playlist checkbox state
+    const shufflePlaylist = document.getElementById('shuffle_playlist').checked;
+    setCookie('shuffle_playlist', shufflePlaylist, 7);
+
+    // Save pre-execution action
+    const preExecution = document.querySelector('input[name="pre_execution"]:checked').value;
+    setCookie('pre_execution', preExecution, 7);
+
+    logMessage('Settings saved.');
+}
+
+// Load settings from cookies
+function loadSettingsFromCookies() {
+    // Load the pause time
+    const pauseTime = getCookie('pause_time');
+    if (pauseTime !== null) {
+        document.getElementById('pause_time').value = pauseTime;
+    }
+
+    // Load the clear pattern
+    const clearPattern = getCookie('clear_pattern');
+    if (clearPattern !== null) {
+        document.getElementById('clear_pattern').value = clearPattern;
+    }
+
+    // Load the run mode
+    const runMode = getCookie('run_mode');
+    if (runMode !== null) {
+        document.querySelector(`input[name="run_mode"][value="${runMode}"]`).checked = true;
+    }
+
+    // Load the shuffle playlist checkbox state
+    const shufflePlaylist = getCookie('shuffle_playlist');
+    if (shufflePlaylist !== null) {
+        document.getElementById('shuffle_playlist').checked = shufflePlaylist === 'true';
+    }
+
+    // Load the pre-execution action
+    const preExecution = getCookie('pre_execution');
+    if (preExecution !== null) {
+        document.querySelector(`input[name="pre_execution"][value="${preExecution}"]`).checked = true;
+    }
+
+    // Load the selected playlist
+    const selectedPlaylist = getCookie('selected_playlist');
+    if (selectedPlaylist !== null) {
+        const playlistDropdown = document.getElementById('select-playlist');
+        if (playlistDropdown && [...playlistDropdown.options].some(option => option.value === selectedPlaylist)) {
+            playlistDropdown.value = selectedPlaylist;
+        }
+    }
+
+    logMessage('Settings loaded from cookies.');
+}
+
+// Call this function to save settings when a value is changed
+function attachSettingsSaveListeners() {
+    // Add event listeners to inputs
+    document.getElementById('pause_time').addEventListener('input', saveSettingsToCookies);
+    document.getElementById('clear_pattern').addEventListener('change', saveSettingsToCookies);
+    document.querySelectorAll('input[name="run_mode"]').forEach(input => {
+        input.addEventListener('change', saveSettingsToCookies);
+    });
+    document.getElementById('shuffle_playlist').addEventListener('change', saveSettingsToCookies);
+    document.querySelectorAll('input[name="pre_execution"]').forEach(input => {
+        input.addEventListener('change', saveSettingsToCookies);
+    });
+}
+
+
+// Tab switching logic with cookie storage
+function switchTab(tabName) {
+    // Store the active tab in a cookie
+    setCookie('activeTab', tabName, 7); // Store for 7 days
+
+    // Deactivate all tab content
+    document.querySelectorAll('.tab-content').forEach(tab => {
+        tab.classList.remove('active');
+    });
+
+    // Activate the selected tab content
+    const activeTab = document.getElementById(`${tabName}-tab`);
+    if (activeTab) {
+        activeTab.classList.add('active');
+    } else {
+        console.error(`Error: Tab "${tabName}" not found.`);
+    }
+
+    // Deactivate all nav buttons
+    document.querySelectorAll('.bottom-nav .tab-button').forEach(button => {
+        button.classList.remove('active');
+    });
+
+    // Activate the selected nav button
+    const activeNavButton = document.getElementById(`nav-${tabName}`);
+    if (activeNavButton) {
+        activeNavButton.classList.add('active');
+    } else {
+        console.error(`Error: Nav button for "${tabName}" not found.`);
+    }
+}
+
+// Initialization
+document.addEventListener('DOMContentLoaded', () => {
+    const activeTab = getCookie('activeTab') || 'patterns'; // Default to 'patterns' tab
+    switchTab(activeTab); // Load the active tab
+    checkSerialStatus(); // Check serial connection status
+    loadThetaRhoFiles(); // Load files on page load
+    loadAllPlaylists(); // Load all playlists on page load
+    loadSettingsFromCookies(); // Load saved settings
+    attachSettingsSaveListeners(); // Attach event listeners to save changes
+    attachFullScreenListeners();
+});

+ 670 - 201
static/style.css

@@ -1,277 +1,687 @@
+:root {
+    --background-primary: #f9f9f9;
+    --background-secondary: #fff;
+    --background-tertiary: #ddd;
+    --background-accent: rgba(74, 144, 226, 0.75);
+    --background-info: var(--background-accent);
+    --background-success: rgba(76, 175, 80, 0.8);
+    --background-warning: rgba(255, 152, 0, 0.8);
+    --background-error: rgba(229, 57, 53, 0.8);
+
+    --theme-primary: #6A9AD9;
+    --theme-primary-hover: #A0CCF2;
+    --theme-secondary: #C4B4A0;
+    --theme-secondary-hover: #4E453F;
+
+    --color-info: var(--theme-primary);
+    --color-success: #4CAF50CC;
+    --color-warning: #FF9800CC;
+    --color-error: #E53935CC;
+
+    --text-primary: #333;
+    --text-secondary: #fff;
+
+    --border-primary: var(--background-tertiary);
+    --border-secondary: grey;
+    --border-accent: var(--theme-primary);
+    --border-hover: var(--theme-primary-hover);
+
+    --shadow-primary: 0 0 20px var(--border-secondary);
+
+    --transition-fast: 0.1s ease-in-out;
+    --transition-medium: 0.250s ease;
+    --transition-slow: 1s ease;
+}
+
+/* General
+
 /* General Styling */
 body {
-    font-family: 'Roboto', sans-serif;
     margin: 0;
-    padding: 0;
-    background-color: #f4f4f9;
-    color: #333;
+    font-family: 'Roboto', sans-serif;
+    background: var(--background-primary);
+    display: flex;
+    flex-direction: column;
+    position: relative;
 }
 
 body * {
     box-sizing: border-box;
 }
 
-h1 {
-    text-align: center;
-    color: #4A90E2;
-    margin: 20px 0;
+h1, h2 {
+    margin: 0;
+}
+
+header {
+    position: sticky;
+    height: 50px;
+    top: 0;
+    z-index: 10;
+    background: var(--background-primary);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+}
+
+h1, h2 {
+    color: var(--theme-secondary-hover);
+    transition: var(--transition-slow) color;
 }
 
-h2 {
-    color: #4A90E2;
+h3 {
     margin: 10px 0;
 }
 
-/* Container Layout */
-.container {
+
+/* Inputs */
+input, select {
+    display: block;
+    width: 100%;
+    padding: 10px;
+    margin-bottom: 10px;
+    border: 1px solid var(--border-primary);
+    border-radius: 5px;
+    font-size: 1rem;
+}
+
+/* Custom Input Wrapper */
+.custom-input {
     display: flex;
-    flex-wrap: wrap;
-    justify-content: space-between;
-    gap: 20px;
-    width: 90%;
-    max-width: 1200px;
-    margin: 0 auto;
+    align-items: center;
+    gap: 10px;
+    font-size: 1rem;
+    color: var(--text-primary);
+    cursor: pointer;
+    flex: 1 1 auto;
 }
 
-/* Columns */
-.left-column {
-    flex: 1;
-    min-width: 300px;
+/* Hide the Native Input */
+.custom-input input {
+    display: none;
 }
 
-.right-column {
-    flex: 1;
-    min-width: 300px;
+/* Checkbox and Radio Styles */
+.custom-checkbox,
+.custom-radio {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    border: 2px solid var(--theme-primary);
+    background-color: var(--background-secondary);
+    position: relative;
+    transition: background-color 0.3s ease, border-color 0.3s ease;
+}
+
+/* Checkbox Specific */
+.custom-checkbox {
+    border-radius: 4px;
+}
+.custom-checkbox::after {
+    content: '';
+    width: 10px;
+    height: 10px;
+    background-color: var(--theme-primary);
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) scale(0);
+    transition: transform 0.2s ease-in-out;
+}
+
+/* Radio Specific */
+.custom-radio {
+    border-radius: 50%;
+}
+.custom-radio::after {
+    content: '';
+    width: 10px;
+    height: 10px;
+    background-color: var(--theme-primary);
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) scale(0);
+    border-radius: 50%;
+    transition: transform 0.2s ease-in-out;
 }
 
-/* Sections */
-.section {
-    background: #fff;
-    border: 1px solid #ddd;
-    border-radius: 8px;
-    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
-    padding: 20px;
-    margin-bottom: 20px;
+/* Checked State */
+.custom-input input:checked + .custom-checkbox::after,
+.custom-input input:checked + .custom-radio::after {
+    transform: translate(-50%, -50%) scale(1);
 }
 
-/* Buttons Inline */
-.button-group {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 10px;
-    padding: 10px 15px;
+.custom-input input:checked + .custom-checkbox,
+.custom-input input:checked + .custom-radio {
+    background-color: var(--theme-primary);
+    border-color: var(--theme-primary-hover);
 }
 
-.button-group label {
-    align-content: center;
+/* Focus State */
+.custom-input input:focus-visible + .custom-checkbox,
+.custom-input input:focus-visible + .custom-radio {
+    outline: 2px dashed var(--theme-primary);
+    outline-offset: 2px;
 }
 
+/* Hover Effects */
+.custom-checkbox:hover,
+.custom-radio:hover {
+    border-color: var(--theme-primary-hover);
+}
+
+/* Buttons */
 button {
-    background: #4A90E2;
-    color: #fff;
-    cursor: pointer;
+    background: var(--theme-primary);
+    color: var(--text-secondary);
     padding: 10px 15px;
-    font-size: 1em;
-    border: 1px solid #ddd;
+    border: none;
+    font-weight: bold;
     border-radius: 5px;
-    outline: none;
-    transition: all 0.3s ease;
+    cursor: pointer;
+    font-size: 1rem;
+    transition: background 0.3s ease,color 0.3s ease;
+}
+
+button:not(.close-button, .fullscreen-button, .move-button, .remove-button):hover {
+    background: var(--background-info);
 }
 
-button:hover {
-    background: #357ABD;
+button.cancel {
+    flex-grow: 0;
 }
 
-.small-button {
-    font-size: 0.8rem;
+button.cancel:hover {
+    background: var(--color-error);
 }
 
-/* Delete Button */
-.delete-button {
-    background: #e74c3c; /* Red color */
-    color: #fff;
-    cursor: pointer;
+button.cta:hover {
+    background: var(--color-success);
+}
+
+button.warn:hover {
+    background: var(--color-warning);
+}
+
+button.warning:hover{}
+
+/* App Layout */
+.app {
+    min-height: calc(100vh - 110px);
+    display: flex;
+    flex-direction: column;
+}
+
+.hidden:not(.sticky) {
+    display: none !important;
+}
+
+/* Tabs */
+.tab-content {
+    display: none;
+    flex: 1;
+    overflow-y: auto;
+    background: var(--background-secondary);
+}
+
+.tab-content.active {
+    display: flex;
+    position: relative;
+    flex-direction: column;
+}
+
+section {
+    padding: 15px;
+    display: flex;
+    flex-direction: column;
+}
+
+section.main {
+    flex-grow: 1;
+}
+
+section.debug {
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+}
+
+section.sticky {
+    position: fixed;
+    background-color: rgba(255, 255, 255, 0.5);
+    backdrop-filter: blur(10px);
+    bottom: 60px;
+    border-top: 1px solid var(--border-primary);
+    box-shadow: var(--shadow-primary);
+    transform: translateY(0);
+    transition: 250ms transform, 250ms height;
+    visibility: visible;
+    max-height: 60vh;
+    width: 100%;
+    z-index: 10;
+}
+
+section.sticky.fullscreen {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100vw;
+    max-height: none;
+}
+
+section.sticky.hidden {
+    transform: translateY(100%);
+    visibility: hidden;
+    width: 100%;
+    position: absolute;
+    overflow:hidden;
+    height: 0;
+    padding: 0;
+}
+
+section .header {
+    position: relative;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+}
+
+section .header h2 {
+    flex-grow: 1;
+}
+
+/* Close Button Styling */
+.close-button,
+.fullscreen-button {
+    background: none;
     border: none;
-    padding: 10px 15px;
-    border-radius: 5px;
-    font-size: 1em;
-    transition: all 0.3s ease;
+    font-size: 1.5rem;
+    font-weight: bold;
+    color: var(--text-primary);
+    cursor: pointer;
+    line-height: 1;
+    padding: 0;
+    height: 100%;
+    width: auto;
+    aspect-ratio: 1 / 1;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-left: 10px;
 }
 
-.delete-button:hover {
-    background: #c0392b; /* Darker red on hover */
+.close-button:hover {
+    color: var(--color-error);
 }
 
-select, input[type="file"] {
+.fullscreen-button:hover {
+    color: var(--color-warning);
+}
+
+section .header .add-button {
+    height: 35px;
+    width: 35px;
+    font-size: 1.5rem;
+    padding: 0;
+}
+
+/* Playlist */
+.add-to-playlist {
+    margin-top: 15px;
+}
+
+.add-to-playlist button {
+    margin-bottom: 10px;
+}
+
+.add-to-container {
+    display: flex;
+    flex-wrap: wrap;
+    margin-bottom: 20px;
+}
+
+#add-to-playlist-container select,
+#add-to-playlist-container button {
+    margin-top: 10px;
     display: block;
     width: 100%;
-    max-width: 300px;
+}
+
+.playlist-parameters {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+.playlist-parameters .row {
+    display: flex;
+    gap: 10px;
+}
+
+#clear_pattern {
+    margin: 0;
+}
+
+.playlist-parameters .input-group input,
+.playlist-parameters .input-group select {
+    width: 100%; /* Ensure inputs/selects stretch to full width */
     padding: 10px;
-    margin: 10px 0;
-    font-size: 1em;
-    border: 1px solid #ddd;
+    border: 1px solid var(--border-primary);
     border-radius: 5px;
-    outline: none;
+    font-size: 1rem;
+}
+
+.empty-placeholder {
+    color: gray;
+    font-style: italic;
+    text-align: center;
+    padding: 10px;
 }
 
+/* Style for the filename span */
+.filename {
+    flex-grow: 1; /* Use available space */
+    font-size: 1rem;
+    color: var(--text-primary);
+    margin-right: 10px; /* Space between filename and buttons */
+    word-wrap: break-word;
+    width: 100%;
+    display: flex;
+    align-items: center;
+}
+
+
 /* File List */
-ul {
+.file-list {
     list-style: none;
     padding: 0;
     margin: 0;
+    border: 1px solid var(--border-primary);
+    border-radius: 5px;
+    overflow-y: auto;
+    background: var(--background-primary);
+    flex-grow: 1;
 }
 
-li {
+.file-list li {
+    display: flex;
     padding: 10px;
-    border-bottom: 1px solid #ddd;
+    border-bottom: 1px solid var(--border-primary);
     cursor: pointer;
-    transition: all 0.3s ease;
+    transition: background-color 0.3s ease;
 }
 
-li:hover {
-    background: #f1f1f1;
+.file-list li:hover {
+    background-color: #f1f1f1;
 }
 
-li.selected {
-    background: #4A90E2;
-    color: white;
+.file-list li.selected {
+    background: var(--theme-primary);
+    color: var(--text-secondary);
     font-weight: bold;
-    padding: 5px;
+}
+
+.file-list li.selected .filename {
+    font-weight: bold;
+    color: var(--text-secondary);
+}
+
+.file-list button {
+    margin-left: 5px;
+    background: none;
+    color: black;
+    font-weight: bold;
+    height: 40px;
+    width: 40px;
+    flex: 0 0 auto;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.file-list button:hover:not(:focus) {
+    background: var(--background-primary);
+    box-shadow: inset 0 0 4px var(--border-secondary);
+}
+
+.file-list button.remove-button {
+    color: var(--color-error);
+}
+
+.title-container {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.rename-button {
+    margin-left: 10px;
+    background: var(--theme-primary-hover);
+    color: var(--text-secondary);
+    border: none;
     border-radius: 5px;
+    padding: 5px 10px;
+    cursor: pointer;
+    transition: background 0.3s ease;
 }
 
-/* Status Log */
+.rename-button:hover {
+    background: #285A8E;
+}
+
+/* Bottom Navigation */
+.bottom-nav {
+    display: flex;
+    position: sticky;
+    justify-content: space-around;
+    bottom: 0;
+    height: 60px;
+    width: 100%;
+    border-top: 1px solid var(--theme-primary);
+    flex-wrap: wrap;
+    z-index: 10;
+}
+
+.tab-button {
+    flex: 1;
+    padding: 20px 10px;
+    text-align: center;
+    font-size: 1rem;
+    font-weight: bold;
+    color: var(--text-secondary);
+    background: none;
+    border: none;
+    cursor: pointer;
+    transition: background 0.3s ease;
+    background: var(--background-info);
+    backdrop-filter: blur(2px);
+    border-radius: 0;
+}
+
+.bottom-nav .tab-button.active {
+    background: rgba(255, 255, 255, 0.75);
+    color: var(--theme-primary);
+}
+
+/* Quick Action Buttons */
+.action-buttons {
+    display: flex;
+    gap: 10px;
+    flex-wrap: wrap;
+    width: 100%;
+}
+
+.action-buttons button {
+    flex: 1;
+}
+
+.action-buttons button.cta {
+    flex-grow: 1;
+}
+
+button#debug_button {
+    width: 40px;
+    padding: 0;
+    height: 40px;
+    background: transparent;
+    font-size: 1.5rem;
+    margin-left: 40px;
+    flex: 0 0 auto;
+    transition: 250ms all;
+}
+
+button#debug_button:hover,
+button#debug_button.active {
+    box-shadow: inset 0 0 4px var(--border-secondary);
+}
+
+#settings-tab button.cancel {
+    flex-basis: 100%;
+}
+
+/* Preview Canvas */
+#patternPreviewCanvas {
+    width: 100%;
+    max-width: 300px;
+    aspect-ratio: 1/1;
+    border: 1px solid var(--border-primary);
+    background: var(--theme-secondary);
+    border-radius: 100%;
+    padding: 30px;
+}
+
+#pattern-preview {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    margin-bottom: 20px;
+}
+
+#pattern-preview-container.fullscreen #patternPreviewCanvas {
+    width: initial;
+    max-width: calc(100vw - 30px);
+}
+
+/* Debug Log */
 #status_log {
-    display: none;
     background: #000;
+    color: var(--text-secondary);
     font-family: monospace;
-    border-top: 1px solid #ddd;
+    font-size: 0.9rem;
+    border-top: 1px solid var(--border-primary);
     padding: 10px;
-    max-height: 150px;
-    overflow-y: scroll; /* Use scroll explicitly */
-    box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
-    -webkit-overflow-scrolling: touch; /* Enables smooth scrolling on mobile */
-    touch-action: auto; /* Ensures touch gestures are handled properly */
+    max-height: 200px;
+    overflow-y: scroll;
+    display: none;
     width: 100%;
 }
 
-
 #status_log p {
     margin: 0;
-    font-size: 0.9em;
-    color: #ddd;
 }
 
-#theta_rho_files {
-    max-height: 400px;
-    overflow-y: auto;
-    border: 1px solid #ddd;
-    border-radius: 5px;
-    background-color: #fff;
-    padding: 10px;
+.control-group {
+    display: flex;
+    margin-bottom: 10px;
+    flex-wrap: wrap;
+    width: 100%;
+    align-items: center;
+    justify-content: space-between;
+    gap: 0 10px;
 }
 
-/* Search Bar */
-#search_pattern {
-    display: block;
+.control-group input {
+    margin-bottom: 0;
+}
+
+.control-group h3 {
     width: 100%;
-    max-width: 300px;
-    padding: 10px;
-    margin: 10px 0;
-    font-size: 1em;
-    border: 1px solid #ddd;
-    border-radius: 5px;
-    outline: none;
-    transition: all 0.3s ease;
 }
 
-#search_pattern:focus {
-    border-color: #4A90E2;
-    box-shadow: 0 0 5px rgba(74, 144, 226, 0.5);
+.control-group .item {
+    display: flex;
+    align-items: center;
+    flex: 1;
 }
 
-/* Radio Buttons and Labels */
-.pre-execution-toggles {
-    margin: 10px 0;
-    padding: 10px;
-    background: #fff;
-    border: 1px solid #ddd;
-    border-radius: 8px;
-    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+.control-group .item.cta {
+    justify-content: flex-end;
 }
 
-.pre-execution-toggles h3 {
-    margin: 0 0 10px 0;
-    color: #4A90E2;
-    font-size: 1.1em;
+.control-group .item.column {
+    flex-direction: column;
+    text-align: center;
 }
 
-.pre-execution-toggles label {
-    display: flex;
-    align-items: center;
-    gap: 10px;
-    margin: 5px 0;
-    font-size: 1em;
-    color: #333;
-    cursor: pointer;
-    transition: all 0.3s ease;
+.control-group .item label {
+    padding: 5px;
+}
+
+#serial_ports_container > * {
+    display: inline-block;
+}
+
+#serial_ports_container select {
+    margin: 10px;
+    flex-basis: 100px;
+    flex-grow: 0;
+}
+
+#serial_ports {
+    width: auto;
+    min-width: 200px;
 }
 
-.pre-execution-toggles label:hover {
-    color: #4A90E2;
+#serial_status_container {
+    margin-bottom: 10px;
 }
 
-input[type="radio"] {
-    appearance: none;
-    width: 16px;
-    height: 16px;
-    border: 2px solid #4A90E2;
+#serial_status_header::before {
+    content: '';
+    width: 20px;
+    height: 20px;
     border-radius: 50%;
-    outline: none;
-    cursor: pointer;
-    transition: all 0.3s ease;
+    margin-right: 8px;
+    background-color: var(--text-primary);
+    display: inline-block;
+    transition: var(--transition-slow) background-color;
 }
 
-input[type="radio"]:checked {
-    background-color: #4A90E2;
-    border-color: #4A90E2;
-    box-shadow: 0 0 3px rgba(74, 144, 226, 0.5);
+#serial_status_header.connected::before {
+    background-color: var(--color-success);
 }
 
-#speed_input {
-    min-width: 80px;
+#serial_status_header.not-connected::before {
+    background-color: var(--color-error);
 }
 
-.coordinate-input {
-    display: flex;
-    align-items: center;
-    gap: 10px;
-    margin-top: 10px;
+
+#serial_ports_buttons {
+    display: inline-block;
 }
 
-.coordinate-input input {
-    font-size: 1rem;
-    padding: 5px;
-    width: 80px;
+.status.connected {
+    color: var(--color-success);
+    font-weight: bold;
+}
+
+.status.not-connected {
+    color: var(--color-error);
+    font-weight: bold;
 }
 
 /* Speed Control Section */
 .speed-control {
     display: flex;
-    align-items: center;
-    gap: 15px;
-    margin-top: 10px;
-    background: #fff; /* Matches section background */
-    border: 1px solid #ddd;
-    border-radius: 8px;
-    padding: 15px;
-    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
 }
 
 .speed-control label {
     font-weight: bold;
     font-size: 1rem;
-    color: #333;
+    color: var(--text-primary);
     flex-shrink: 0;
 }
 
@@ -279,36 +689,20 @@ input[type="radio"]:checked {
     width: 100px; /* Consistent input width */
     padding: 8px;
     font-size: 1rem;
-    border: 1px solid #ddd;
+    border: 1px solid var(--border-primary);
     border-radius: 5px;
     outline: none;
     transition: all 0.3s ease;
 }
 
-.speed-control input[type="number"]:focus {
-    border-color: #4A90E2; /* Highlighted border */
-    box-shadow: 0 0 5px rgba(74, 144, 226, 0.5);
-}
-
-.speed-control button {
-    background: #4A90E2;
-    color: #fff;
-    padding: 10px 15px;
-    font-size: 1rem;
-    border: none;
-    border-radius: 5px;
-    cursor: pointer;
-    transition: all 0.3s ease;
-}
-
-.speed-control button:hover {
-    background: #357ABD;
+input[type="number"]:focus {
+    border-color: var(--theme-primary);
+    box-shadow: 0 0 4px var(--background-info);
 }
 
 #speed_status {
     margin-top: 10px;
     font-size: 0.9rem;
-    color: #444;
 }
 
 #serial_ports_container > * {
@@ -331,16 +725,40 @@ input[type="radio"]:checked {
     display: inline-block;
 }
 
-.status.connected {
-    color: #4CAF50;
+/* Notification Styles */
+.notification {
+    display: flex;
+    position: absolute;
+    top: 0;
+    left: 0;
     font-weight: bold;
+    z-index: 1000;
+    color: var(--text-secondary);
+    width: 100%;
+    height: 100%;
+    justify-content: center;
+    align-items: center;
+    backdrop-filter: blur(2px);
+    opacity: 0;
+    transition: opacity 250ms ease-in-out;
+}
+.notification.show {
+    opacity: 1; /* Fully visible */
 }
 
-.status.not-connected {
-    color: #E53935;
-    font-weight: bold;
+.notification .close-button {
+    color: var(--text-secondary);
+    font-size: 2rem;
+    top: 0;
+    right: 0;
 }
 
+/* Notification Types */
+.notification.success { background-color: var(--background-success); }
+.notification.warning { background-color: var(--background-warning); }
+.notification.error { background-color: var(--background-error); }
+.notification.info { background-color: var(--background-info); }
+
 .footer {
     align-items: center;
     display: flex;
@@ -350,31 +768,82 @@ input[type="radio"]:checked {
     width: 100%;
 }
 
-.footer #github {
+#github {
     align-content: center;
     display: flex;
     font-size: 0.8em;
 }
 
-.footer #github img {
+#github img {
     margin: 0 5px
 }
 
-/* Responsive Layout for Small Screens */
-@media (max-width: 768px) {
+/* Responsive Design */
+@media (max-width: 1023px) {
     body {
         font-size: 0.9rem;
     }
 
-    .container {
-        flex-direction: column; /* Stack columns vertically */
+    .tab-button {
+        font-size: 0.9rem;
     }
 
-    .left-column, .right-column {
-        width: 100%;
+    .footer {
+        display: none;
     }
 
-    .footer {
-        flex-direction: column;
+    button.cancel {
+        background: var(--color-error);
+    }
+
+    button.cta {
+        background: var(--color-success);
+    }
+
+    button.warn {
+        background: var(--color-warning);
+    }
+
+    button.cancel:hover,
+    button.warn:hover,
+    button.cta:hover {
+        background: var(--theme-primary);
     }
 }
+
+/* On larger screens, display all tabs in a 3-column grid */
+@media screen and (min-width: 1024px) {
+    .app {
+        display: grid;
+        grid-template-columns: repeat(3, 1fr);
+        gap: 0 16px;
+        height: calc(100vh - 60px);
+    }
+
+    .bottom-nav {
+        display: none;
+    }
+
+    #status_log {
+        grid-column: span 3;
+        align-self: flex-end;
+        height: 100%;
+    }
+
+    section.sticky {
+        position: sticky;
+        bottom: 0;
+    }
+
+    /* Show all tabs in grid layout */
+    .tab-content {
+        display: flex !important; /* Always display tab-content */
+        flex-direction: column;
+        border: 1px solid var(--border-primary);
+        background-color: var(--background-primary);
+        border-radius: 8px;
+        overflow-y: auto;
+        overflow-x: hidden;
+        position: relative;
+    }
+}

+ 220 - 917
templates/index.html

@@ -2,146 +2,115 @@
 <html lang="en">
 <head>
     <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
     <title>Dune Weaver Controller</title>
     <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
     <link rel="stylesheet" href="../static/style.css">
 </head>
 <body>
-    <h1>Dune Weaver Controller</h1>
-    <div class="container">
-        <!-- Left Column -->
-        <div class="left-column">
-            <div class="section">
-                <h2>Serial Connection</h2>
-                <div id="serial_status_container">Status: <span id="serial_status" class="status"> Not connected</span></div>
-                <div id="serial_ports_container">
-                    <label for="serial_ports">Available Ports:</label>
-                    <select id="serial_ports"></select>
-                    <button onclick="connectSerial()">Connect</button>
-                </div>
-                <div id="serial_ports_buttons" class="button-group">
-                    <button onclick="disconnectSerial()">Disconnect</button>
-                    <button onclick="restartSerial()">Restart</button>
-                </div>
-                <div class="button-group">
-                    <label for="speed_input">Speed:</label>
-                    <input type="number" id="speed_input" placeholder="1-100" min="1" step="1" max="5000">
-                    <button class="small-button"  onclick="changeSpeed()">Set Speed</button>
-                </div>
-            </div>
-            <div class="section"> 
-                <h2>Mode</h2>
-                <label>
-                <input type="radio" name="mode" class="modeSelector" value="single_run" checked> Single Run
-                </label>
-                <label>
-                <input type="radio" name="mode" class="modeSelector" value="create_playlist"> Create Playlist
-                </label>
-                <label>
-                <input type="radio" name="mode" class="modeSelector" value="run_playlist"> Run Playlist
-                </label>
+<header>
+    <h1 id="serial_status_header" class="status">Dune Weaver Controller</h1>
+</header>
+<div class="app">
+    <!-- Home Tab -->
+    <main class="tab-content" id="patterns-tab">
+        <section class="main">
+            <div class="header">
+                <h2>Patterns</h2>
+                <button class="add-button cta" onclick="toggleSecondaryButtons('add-pattern-container')">+</button>
             </div>
-            <div class="section">
-                <h2>Quick Actions</h2>
-                <div class="button-group">
-                    <button onclick="sendHomeCommand()">Home Device</button>
-                    <button onclick="moveToCenter()">Move to Center</button>
-                    <button onclick="moveToPerimeter()">Move to Perimeter</button>
-                    <button onclick="runClearIn()">Clear from In</button>
-                    <button onclick="runClearOut()">Clear from Out</button>
-                    <button onclick="runSideway()">Clear Sideway</button>
-                    <button onclick="stopExecution()" class="delete-button">Stop</button>
-                </div>
-                <div class="coordinate-input button-group">
-                    <label for="theta_input">θ:</label>
-                    <input type="number" id="theta_input" placeholder="Theta">
-                    <label for="rho_input">ρ:</label>
-                    <input type="number" id="rho_input" placeholder="Rho">
-                    <button class="small-button" onclick="sendCoordinate()">Send to coordinate</button>
+            <div id="add-pattern-container" class="add-to-container hidden">
+                <div class="action-buttons">
+                    <label for="upload_file">Upload pattern file (.thr):</label>
+                    <input type="file" id="upload_file">
+                    <button class="cancel" onclick="toggleSecondaryButtons('add-pattern-container')">Cancel</button>
+                    <button class="cta" onclick="uploadThetaRho()">Upload</button>
                 </div>
             </div>
-            <div class="section">
+            <input type="text" id="search_pattern" placeholder="Search files..." oninput="searchPatternFiles()">
+            <ul id="theta_rho_files" class="file-list"></ul>
+        </section>
+
+        <!-- Patterns Preview Section -->
+        <section id="pattern-preview-container" class="sticky hidden">
+            <div class="header">
                 <h2>Preview</h2>
-                <canvas id="patternPreviewCanvas" style="width: 100%;"></canvas>
-                <p id="first_coordinate">First Coordinate: Not available</p>
-                <p id="last_coordinate">Last Coordinate: Not available</p>
+                <button class="fullscreen-button">⛶</button>
+                <button class="close-button" onclick="closeStickySection('pattern-preview-container')">&times;</button>
+            </div>
+            <div id="pattern-preview">
+                <canvas id="patternPreviewCanvas"></canvas>
+                <div id="first_coordinate"></div>
+                <div id="last_coordinate"></div>
             </div>
-        </div>
 
-        <!-- Right Column -->
-        <div class="right-column">
+            <!-- Action Buttons -->
+            <div class="action-buttons">
+                <button onclick="runThetaRho()" class="cta" >Run</button>
+                <button id="toggle-playlist-button" onclick="toggleSecondaryButtons('add-to-playlist-container', populatePlaylistDropdown)">Add to Playlist</button>
+                <button onclick="removeCurrentPattern()" class="cancel remove-button hidden">Delete</button>
+            </div>
 
-                <div class="section modeSection single_run create_playlist">
-                    <h2 class="modeSection single_run">Single Run</h2>
-                    <h2 class="modeSection create_playlist" style="display: none;"> Create Playlist</h2>
-                    <h2 class="modeSection run_playlist" style="display: none;"> Run Playlist</h2>
-                    <input class="modeSection single_run create_playlist" type="text" id="search_pattern" placeholder="Search files..." oninput="searchPatternFiles()">
-                    <ul  class="modeSection single_run create_playlist" id="theta_rho_files"></ul>
-                    <div class="pre-execution-toggles modeSection single_run">
-                        <h3>Pre-Execution Action</h3>
-                        <label>
-                            <input type="radio" name="pre_execution" value="clear_in" id="clear_in"> Clear from In
-                        </label>
-                        <label>
-                            <input type="radio" name="pre_execution" value="clear_out" id="clear_out"> Clear from Out
-                        </label>
-                        <label>
-                            <input type="radio" name="pre_execution" value="clear_sideway" id="clear_out"> Clear sideway
-                        </label>
-                        <label>
-                            <input type="radio" name="pre_execution" value="none" id="no_action" checked> None
-                        </label>
-                    </div>
-                    <div class="button-group modeSection single_run">
-                        <button id="run_button" disabled>Run Selected File</button>
-                    </div>
-                    <div class="button-group modeSection create_playlist" style="display: none;">
-                        <button onclick="addToPlaylist()">Add File to Playlist</button>
-                    </div>
+            <!-- Add to Playlist Section -->
+            <div id="add-to-playlist-container" class="hidden">
+                <h2>Select Playlist</h2>
+                <select id="select-playlist"></select>
+                <div class="action-buttons">
+                    <button onclick="toggleSecondaryButtons('add-to-playlist-container')" class="cancel">Cancel</button>
+                    <button onclick="saveToPlaylist()" class="cta">Save</button>
                 </div>
-                <div class="section modeSection single_run">
-                    <h2>Upload new files</h2>
-                    <div class="button-group">
-                        <input type="file" id="upload_file">
-                        <div class="button-group">
-                            <button onclick="uploadThetaRho()">Upload</button>
-                            <button id="delete_selected_button" class="delete-button" onclick="deleteSelectedFile()" disabled>Delete Selected File</button>
-                        </div>
-                    </div>
+            </div>
+        </section>
+    </main>
+
+    <!-- Playlists Tab -->
+    <main class="tab-content" id="playlists-tab">
+        <section>
+            <div class="header">
+                <h2>Playlists</h2>
+                <button class="cta add-button" onclick="toggleSecondaryButtons('add-playlist-container')">+</button>
+            </div>
+            <div id="add-playlist-container" class="add-to-container hidden">
+                <input type="text" id="new_playlist_name" placeholder="Enter new playlist name" />
+                <div class="action-buttons">
+                    <button onclick="confirmAddPlaylist()" class="cta">Save</button>
+                    <button onclick="toggleSecondaryButtons('add-playlist-container')" class="cancel">Cancel</button>
                 </div>
-                <div class="section modeSection create_playlist run_playlist" style="display: none;">
-                    <h2>Create Playlist</h2>
-                    <!-- A list showing the files that have been added to the playlist -->
-                    <ul id="playlist_items"></ul>
-                  
-                    <!-- Buttons to reorder files in the playlist -->
-                    <div class="button-group">
-                      <button onclick="movePlaylistItemUp()">Up</button>
-                      <button onclick="movePlaylistItemDown()">Down</button>
-                      <button onclick="removeFromPlaylist()" class="delete-button">Remove</button>
-                    </div>
-                                        
-                    <div class="playlist-name-input">
-                        <label for="playlist_name">Playlist Name:</label>
-                        <input type="text" id="playlist_name" placeholder="Enter playlist name" />
-                      </div>
-                    
-                      <div class="button-group">
-                        <!-- Button to add the selected file (from #theta_rho_files) to the playlist -->
-                    
-                        <!-- Button to create/finalize the playlist -->
-                        <button onclick="savePlaylist()">Create Playlist</button>
-                    </div>
+            </div>
+            <ul id="all_playlists" class="file-list">
+                <!-- Playlists will be dynamically inserted here -->
+            </ul>
+        </section>
+
+        <!-- Playlist Parameters -->
+        <section class="main">
+            <h2>Playlist Settings</h2>
+            <div class="playlist-parameters">
+                <h3>Run Playlist:</h3>
+                <div class="control-group">
+                    <label class="custom-input">
+                        <input type="radio" id="run_single" name="run_mode" value="single" checked>
+                        <span class="custom-radio"></span>
+                        Once
+                    </label>
+                    <label class="custom-input">
+                        <input type="radio" id="run_indefinite" name="run_mode" value="indefinite">
+                        <span class="custom-radio"></span>
+                        Indefinitely
+                    </label>
+                    <label class="custom-input">
+                        <input type="checkbox" id="shuffle_playlist">
+                        <span class="custom-checkbox"></span>
+                        Shuffle
+                    </label>
                 </div>
-                <div class="section modeSection create_playlist run_playlist" style="display: none;">
-                    <h2>Playlists</h2>
-                    
-                    <!-- Parameter Inputs -->
-                    <div class="playlist-parameters">
+                <h3>Between patterns:</h3>
+                <div class="control-group">
+                    <div class="item column">
                         <label for="pause_time">Pause Time (seconds):</label>
                         <input type="number" id="pause_time" min="0" step="0.1" value="0">
-                        
+                    </div>
+                    <div class="item column">
                         <label for="clear_pattern">Clear Pattern:</label>
                         <select id="clear_pattern">
                             <option value="none">None</option>
@@ -150,805 +119,139 @@
                             <option value="clear_sideway">Clear Sideway</option>
                             <option value="random">Random</option>
                         </select>
-                        
-                        <!-- New Parameters -->
-                        <div class="run-options">
-                            <label>Run Mode:</label>
-                            <label>
-                                <input type="radio" name="run_mode" value="single" checked> Single Run
-                            </label>
-                            <label>
-                                <input type="radio" name="run_mode" value="indefinite"> Indefinite Run
-                            </label>
-                            
-                            <label>
-                                <input type="checkbox" id="shuffle_playlist"> Shuffle Playlist
-                            </label>
-                        </div>
                     </div>
-                    
-                    <!-- Playlists List -->
-                    <ul id="all_playlists"></ul>
                 </div>
             </div>
-        </div>
-    </div>
-    
-    <div class="footer">
-        <div id="github">
-            <span>Help us improve! <a href="https://github.com/tuanchris/dune-weaver/pulls" target="_blank">Submit a Pull Request</a> or <a href="https://github.com/tuanchris/dune-weaver/issues/new" target="_blank">Report a Bug</a>.</span>
-            <a href="https://github.com/tuanchris/dune-weaver/issues" target="_blank">
-                <img src="https://img.shields.io/github/issues/tuanchris/dune-weaver?style=flat-square" alt="GitHub Issues">
-            </a>
-        </div>
-
-        <button id="debug_button" class="small-button" onclick="toggleDebugLog()">
-            Show Debug log
-        </button>
-    </div>
-
-    <div id="status_log">
-        <!-- Messages will be appended here -->
-    </div>
-    <script>
-        let selectedFile = null;
-
-        function logMessage(message) {
-            const log = document.getElementById('status_log');
-            const entry = document.createElement('p');
-            entry.textContent = message;
-            log.appendChild(entry);
-            log.scrollTop = log.scrollHeight; // Keep log scrolled to the bottom
-        }
-
-        async function selectFile(file, listItem) {
-            selectedFile = file;
-
-            // Highlight the selected file
-            document.querySelectorAll('#theta_rho_files li').forEach(li => li.classList.remove('selected'));
-            listItem.classList.add('selected');
-
-            // Enable buttons
-            document.getElementById('run_button').disabled = false;
-            document.getElementById('delete_selected_button').disabled = false;
-
-            logMessage(`Selected file: ${file}`);
-
-            // Fetch and preview the selected file
-            await previewPattern(file);
-        }
-
-        async function uploadThetaRho() {
-            const fileInput = document.getElementById('upload_file');
-            const file = fileInput.files[0];
-            if (!file) {
-                logMessage('No file selected for upload.');
-                return;
-            }
-
-            logMessage(`Uploading file: ${file.name}...`);
-            const formData = new FormData();
-            formData.append('file', file);
-
-            const response = await fetch('/upload_theta_rho', {
-                method: 'POST',
-                body: formData
-            });
-
-            const result = await response.json();
-            if (result.success) {
-                logMessage(`File uploaded successfully: ${file.name}`);
-                await loadThetaRhoFiles();
-            } else {
-                logMessage(`Failed to upload file: ${file.name}`);
-            }
-        }
-
-        async function deleteSelectedFile() {
-            if (!selectedFile) {
-                logMessage("No file selected for deletion.");
-                return;
-            }
-
-            const userConfirmed = confirm(`Are you sure you want to delete the selected file "${selectedFile}"?`);
-            if (!userConfirmed) return;
-
-            logMessage(`Deleting file: ${selectedFile}...`);
-            const response = await fetch('/delete_theta_rho_file', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ file_name: selectedFile }),
-            });
-
-            const result = await response.json();
-            if (result.success) {
-                const ul = document.getElementById('theta_rho_files');
-                const selectedItem = Array.from(ul.children).find(li => li.classList.contains('selected'));
-                if (selectedItem) selectedItem.remove();
-
-                selectedFile = null;
-                document.getElementById('run_button').disabled = true;
-                document.getElementById('delete_selected_button').disabled = true;
-
-                logMessage(`File deleted successfully: ${result.file_name}`);
-            } else {
-                logMessage(`Failed to delete file: ${selectedFile}`);
-            }
-        }
-
-        async function runThetaRho() {
-            if (!selectedFile) {
-                logMessage("No file selected to run.");
-                return;
-            }
-
-            // Get the selected pre-execution action
-            const preExecutionAction = document.querySelector('input[name="pre_execution"]:checked').value;
-
-            logMessage(`Running file: ${selectedFile} with pre-execution action: ${preExecutionAction}...`);
-            const response = await fetch('/run_theta_rho', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ file_name: selectedFile, pre_execution: preExecutionAction })
-            });
-
-            const result = await response.json();
-            if (result.success) {
-                logMessage(`File running: ${selectedFile}`);
-            } else {
-                logMessage(`Failed to run file: ${selectedFile}`);
-            }
-        }
-
-        async function stopExecution() {
-            logMessage('Stopping execution...');
-            const response = await fetch('/stop_execution', { method: 'POST' });
-            const result = await response.json();
-            if (result.success) {
-                logMessage('Execution stopped.');
-            } else {
-                logMessage('Failed to stop execution.');
-            }
-        }
-
-        async function loadSerialPorts() {
-            const response = await fetch('/list_serial_ports');
-            const ports = await response.json();
-            const select = document.getElementById('serial_ports');
-            select.innerHTML = '';
-            ports.forEach(port => {
-                const option = document.createElement('option');
-                option.value = port;
-                option.textContent = port;
-                select.appendChild(option);
-            });
-            logMessage('Serial ports loaded.');
-        }
-
-        async function connectSerial() {
-            const port = document.getElementById('serial_ports').value;
-            const response = await fetch('/connect_serial', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ port })
-            });
-            const result = await response.json();
-            if (result.success) {
-                logMessage(`Connected to serial port: ${port}`);
-                // Refresh the status
-                await checkSerialStatus();
-            } else {
-                logMessage(`Error connecting to serial port: ${result.error}`);
-            }
-        }
-
-        async function disconnectSerial() {
-            const response = await fetch('/disconnect_serial', { method: 'POST' });
-            const result = await response.json();
-            if (result.success) {
-                logMessage('Serial port disconnected.');
-                // Refresh the status
-                await checkSerialStatus();
-            } else {
-                logMessage(`Error disconnecting: ${result.error}`);
-            }
-        }
-
-        async function restartSerial() {
-            const port = document.getElementById('serial_ports').value;
-            const response = await fetch('/restart_serial', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ port })
-            });
-            const result = await response.json();
-            if (result.success) {
-                document.getElementById('serial_status').textContent = `Restarted connection to ${port}`;
-                logMessage('Serial connection restarted.');
-
-                // No need to change visibility for restart
-            } else {
-                logMessage(`Error restarting serial connection: ${result.error}`);
-            }
-        }
-
-        async function sendHomeCommand() {
-            const response = await fetch('/send_home', { method: 'POST' });
-            const result = await response.json();
-            if (result.success) {
-                logMessage('HOME command sent successfully.');
-            } else {
-                logMessage('Failed to send HOME command.');
-            }
-        }
-
-
-        let allFiles = []; // Store all files for filtering
-
-        async function loadThetaRhoFiles() {
-            logMessage('Loading Theta-Rho files...');
-            const response = await fetch('/list_theta_rho_files');
-            let files = await response.json();
-
-            // Filter only .thr files
-            files = files.filter(file => file.endsWith('.thr'));
-
-            // Separate files into categories
-            const customPatternsFiles = files.filter(file => file.startsWith('custom_patterns/'));
-            const otherFiles = files.filter(file => !file.startsWith('custom_patterns/'));
-
-            // Sort the files
-            const sortedFiles = [
-                ...customPatternsFiles.sort(), // Custom patterns first
-                ...otherFiles.sort() // Remaining files sorted alphabetically
-            ];
-
-            allFiles = sortedFiles; // Store the sorted list of files
-            displayFiles(allFiles); // Display the sorted files
-
-            logMessage('Theta-Rho files loaded successfully.');
-        }
-
-        function displayFiles(files) {
-            const ul = document.getElementById('theta_rho_files');
-            ul.innerHTML = ''; // Clear current list
-
-            files.forEach(file => {
-                const li = document.createElement('li');
-                li.textContent = file;
-
-                // Highlight the selected file when clicked
-                li.onclick = () => selectFile(file, li);
-
-                ul.appendChild(li);
-            });
-        }
-
-        function searchPatternFiles() {
-            const searchInput = document.getElementById('search_pattern').value.toLowerCase();
-            const filteredFiles = allFiles.filter(file => file.toLowerCase().includes(searchInput));
-            displayFiles(filteredFiles); // Display only matching files
-        }
-
-        async function moveToCenter() {
-            logMessage('Moving to center...');
-            const response = await fetch('/move_to_center', { method: 'POST' });
-            const result = await response.json();
-            if (result.success) {
-                logMessage('Moved to center successfully.');
-            } else {
-                logMessage(`Failed to move to center: ${result.error}`);
-            }
-        }
-
-        async function moveToPerimeter() {
-            logMessage('Moving to perimeter...');
-            const response = await fetch('/move_to_perimeter', { method: 'POST' });
-            const result = await response.json();
-            if (result.success) {
-                logMessage('Moved to perimeter successfully.');
-            } else {
-                logMessage(`Failed to move to perimeter: ${result.error}`);
-            }
-        }
-
-        async function sendCoordinate() {
-            const theta = parseFloat(document.getElementById('theta_input').value);
-            const rho = parseFloat(document.getElementById('rho_input').value);
-
-            if (isNaN(theta) || isNaN(rho)) {
-                logMessage('Invalid input: θ and ρ must be numbers.');
-                return;
-            }
-
-            logMessage(`Sending coordinate: θ=${theta}, ρ=${rho}...`);
-            const response = await fetch('/send_coordinate', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ theta, rho })
-            });
-
-            const result = await response.json();
-            if (result.success) {
-                logMessage(`Coordinate executed successfully: θ=${theta}, ρ=${rho}`);
-            } else {
-                logMessage(`Failed to execute coordinate: ${result.error}`);
-            }
-        }
-
-        async function previewPattern(fileName) {
-            logMessage(`Fetching data to preview file: ${fileName}...`);
-            const response = await fetch('/preview_thr', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ file_name: fileName })
-            });
-
-            const result = await response.json();
-            if (result.success) {
-                const coordinates = result.coordinates;
-
-                // Update coordinates display
-                if (coordinates.length > 0) {
-                    const firstCoord = coordinates[0];
-                    const lastCoord = coordinates[coordinates.length - 1];
-                    document.getElementById('first_coordinate').textContent = `First Coordinate: θ=${firstCoord[0]}, ρ=${firstCoord[1]}`;
-                    document.getElementById('last_coordinate').textContent = `Last Coordinate: θ=${lastCoord[0]}, ρ=${lastCoord[1]}`;
-                } else {
-                    document.getElementById('first_coordinate').textContent = 'First Coordinate: Not available';
-                    document.getElementById('last_coordinate').textContent = 'Last Coordinate: Not available';
-                }
-
-                renderPattern(coordinates);
-            } else {
-                logMessage(`Failed to fetch preview for file: ${result.error}`);
-                // Clear the coordinate display on error
-                document.getElementById('first_coordinate').textContent = 'First Coordinate: Not available';
-                document.getElementById('last_coordinate').textContent = 'Last Coordinate: Not available';
-            }
-        }
-
-        function renderPattern(coordinates) {
-            const canvas = document.getElementById('patternPreviewCanvas');
-            const ctx = canvas.getContext('2d');
-
-            // Make canvas full screen
-            canvas.width = window.innerWidth;
-            canvas.height = window.innerHeight;
-
-            // Clear the canvas
-            ctx.clearRect(0, 0, canvas.width, canvas.height);
-
-            // Convert polar to Cartesian and draw the pattern
-            const centerX = canvas.width / 2;
-            const centerY = canvas.height / 2;
-            const maxRho = Math.max(...coordinates.map(coord => coord[1]));
-            const scale = Math.min(canvas.width, canvas.height) / (2 * maxRho); // Scale to fit within the screen
-
-            ctx.beginPath();
-            coordinates.forEach(([theta, rho], index) => {
-                const x = centerX + rho * Math.cos(theta) * scale;
-                const y = centerY - rho * Math.sin(theta) * scale; // Invert y-axis for canvas
-                if (index === 0) {
-                    ctx.moveTo(x, y);
-                } else {
-                    ctx.lineTo(x, y);
-                }
-            });
-            ctx.closePath();
-            ctx.stroke();
-            logMessage('Pattern preview rendered at full screen.');
-        }
-
-        function toggleDebugLog() {
-            const statusLog = document.getElementById('status_log');
-            const debugButton = document.getElementById('debug_button');
-
-            if (statusLog.style.display === 'block') {
-                statusLog.style.display = 'none';
-                debugButton.textContent = 'Show Debug Log'; // Update the button label
-            } else {
-                statusLog.style.display = 'block';
-                debugButton.textContent = 'Hide Debug Log'; // Update the button label
-                statusLog.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Smooth scrolling to the log
-            }
-        }
-
-        async function checkSerialStatus() {
-            const response = await fetch('/serial_status');
-            const status = await response.json();
-            const statusElement = document.getElementById('serial_status');
-            const serialPortsContainer = document.getElementById('serial_ports_container');
-
-            const connectButton = document.querySelector('button[onclick="connectSerial()"]');
-            const disconnectButton = document.querySelector('button[onclick="disconnectSerial()"]');
-            const restartButton = document.querySelector('button[onclick="restartSerial()"]');
-
-            if (status.connected) {
-                const port = status.port || 'Unknown'; // Fallback if port is undefined
-                statusElement.textContent = `Connected to ${port}`;
-                statusElement.classList.add('connected');
-                statusElement.classList.remove('not-connected');
-                logMessage(`Reconnected to serial port: ${port}`);
-
-                // Hide Available Ports and show disconnect/restart buttons
-                serialPortsContainer.style.display = 'none';
-                connectButton.style.display = 'none';
-                disconnectButton.style.display = 'inline-block';
-                restartButton.style.display = 'inline-block';
-            } else {
-                statusElement.textContent = 'Not connected';
-                statusElement.classList.add('not-connected');
-                statusElement.classList.remove('connected');
-                logMessage('No active serial connection.');
-
-                // Show Available Ports and the connect button
-                serialPortsContainer.style.display = 'block';
-                connectButton.style.display = 'inline-block';
-                disconnectButton.style.display = 'none';
-                restartButton.style.display = 'none';
-
-                // Attempt to auto-load available ports
-                await loadSerialPorts();
-            }
-        }
-
-        async function runFile(fileName) {
-            const response = await fetch(`/run_theta_rho_file/${fileName}`, { method: 'POST' });
-            const result = await response.json();
-            if (result.success) {
-                logMessage(`Running file: ${fileName}`);
-            } else {
-                logMessage(`Failed to run file: ${fileName}`);
-            }
-        }
-
-        async function runClearIn() {
-            await runFile('clear_from_in.thr');
-        }
-
-        async function runClearOut() {
-            await runFile('clear_from_out.thr');
-        }
-
-        async function runSideway() {
-            await runFile('clear_sideway.thr');
-        }
-        
-        // Call this function on page load
-        checkSerialStatus();
-
-        // Initial load of serial ports and Theta-Rho files
-        loadSerialPorts();
-        loadThetaRhoFiles();
-
-        document.getElementById('run_button').onclick = runThetaRho;
-        
-        document.querySelectorAll('.modeSelector').forEach((radio) => {
-            radio.addEventListener('change', function () {
-              // Hide all .modeSection first
-              document.querySelectorAll('.modeSection').forEach((section) => {
-                section.style.display = 'none';
-              });
-          
-              // Figure out which mode was selected
-              const selectedValue = this.value; // or document.querySelector('.modeSelector:checked').value
-          
-              // Show every element that has .modeSection AND the selected mode's class
-              document
-                .querySelectorAll(`.modeSection.${selectedValue}`)
-                .forEach((elem) => {
-                  elem.style.display = 'block';
-                });
-            });
-          });
-
-        async function changeSpeed() {
-            const speedInput = document.getElementById('speed_input');
-            const speed = parseFloat(speedInput.value);
-
-            if (isNaN(speed) || speed <= 0) {
-                logMessage('Invalid speed. Please enter a positive number.');
-                return;
-            }
-
-            logMessage(`Setting speed to: ${speed}...`);
-            const response = await fetch('/set_speed', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ speed })
-            });
-
-            const result = await response.json();
-            if (result.success) {
-                document.getElementById('speed_status').textContent = `Current Speed: ${speed}`;
-                logMessage(`Speed set to: ${speed}`);
-            } else {
-                logMessage(`Failed to set speed: ${result.error}`);
-            }
-        }
-        // Keep track of the files in the new playlist
-        let playlist = [];
-        // Currently selected item in the playlist
-        let selectedPlaylistIndex = null;
-        
-        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        //  PART A: Loading / listing playlists from the server
-        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        async function loadAllPlaylists() {
-            try {
-            const response = await fetch('/list_all_playlists'); // GET
-            const allPlaylists = await response.json();          // e.g. ["My Playlist", "Summer", ...]
-            displayAllPlaylists(allPlaylists);
-            } catch (err) {
-            logMessage(`Error loading playlists: ${err}`);
-            }
-        }
-        
-        // Function to display all playlists with Load, Run, and Delete buttons
-        function displayAllPlaylists(allPlaylists) {
-            const ul = document.getElementById('all_playlists');
-            ul.innerHTML = ''; // Clear current list
-
-            allPlaylists.forEach((playlistName) => {
-                const li = document.createElement('li');
-                li.classList.add('playlist-item'); // For styling
-
-                // Playlist Name
-                const nameSpan = document.createElement('span');
-                nameSpan.textContent = playlistName;
-                li.appendChild(nameSpan);
-
-                // "Load" button
-                const loadBtn = document.createElement('button');
-                loadBtn.textContent = 'Load';
-                loadBtn.onclick = () => loadPlaylist(playlistName);
-                loadBtn.classList.add('load-button'); // For styling
-                li.appendChild(loadBtn);
-
-                // "Run" button
-                const runBtn = document.createElement('button');
-                runBtn.textContent = 'Run';
-                runBtn.onclick = () => runPlaylist(playlistName);
-                runBtn.classList.add('run-button'); // For styling
-                li.appendChild(runBtn);
-
-                // "Delete" button
-                const deleteBtn = document.createElement('button');
-                deleteBtn.textContent = 'Delete';
-                deleteBtn.onclick = () => deletePlaylist(playlistName);
-                deleteBtn.classList.add('delete-button'); // For styling
-                li.appendChild(deleteBtn);
-
-                ul.appendChild(li);
-            });
-        }
-
-        // Function to run the selected playlist with specified parameters
-        async function runPlaylist(playlistName) {
-            if (!playlistName) {
-                logMessage("No playlist selected to run.");
-                return;
-            }
-
-            // Get the parameters from the UI
-            const pauseTimeInput = document.getElementById('pause_time').value;
-            const clearPatternSelect = document.getElementById('clear_pattern').value;
-            const runMode = document.querySelector('input[name="run_mode"]:checked').value;
-            const shuffle = document.getElementById('shuffle_playlist').checked;
-
-            // Validate pause time
-            const pauseTime = parseFloat(pauseTimeInput);
-            if (isNaN(pauseTime) || pauseTime < 0) {
-                logMessage("Invalid pause time. Please enter a non-negative number.");
-                return;
-            }
-
-            // Map clear_pattern select value to backend expected values
-            let clearPattern = clearPatternSelect;
-            if (clearPatternSelect === "none") {
-                clearPattern = null;
-            }
-
-            logMessage(`Running playlist: ${playlistName} with pause_time=${pauseTime}, clear_pattern=${clearPattern || "None"}, run_mode=${runMode}, shuffle=${shuffle}`);
+        </section>
+
+        <!-- Playlist View/Edit Section -->
+        <section id="playlist-editor" class="sticky hidden">
+            <div class="header">
+                <h2 id="playlist_title">Playlist: <span id="playlist_name_display"></span></h2>
+                <button class="fullscreen-button" onclick="toggleFullscreen(this)">⛶</button>
+                <button class="close-button" onclick="closeStickySection('playlist-editor')">&times;</button>
+            </div>
+            <ul id="playlist_items" class="file-list">
+            </ul>
+            <hr/>
+            <div class="action-buttons">
+                <button onclick="runPlaylist()" class="cta">Play</button>
+                <button onclick="toggleSecondaryButtons('rename-playlist-container')">Rename</button>
+                <button onclick="deleteCurrentPlaylist()" class="cancel">Delete</button>
+                <!-- Save and Cancel buttons -->
+                <button onclick="savePlaylist()" class="save-cancel cta" style="display: none;">Save</button>
+                <button onclick="cancelPlaylistChanges()" class="save-cancel cancel" style="display: none;">Cancel</button>
+            </div>
+            <!-- Playlist Rename Section -->
+            <div id="rename-playlist-container" class="hidden">
+                <input type="text" id="playlist_name_input" placeholder="Enter new playlist name">
+                <div class="action-buttons">
+                    <button onclick="confirmRenamePlaylist()" class="cta">Save</button>
+                    <button onclick="toggleSecondaryButtons('rename-playlist-container')" class="cancel">Cancel</button>
+                </div>
+            </div>
+        </section>
+    </main>
 
-            try {
-                const response = await fetch('/run_playlist', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({
-                        playlist_name: playlistName,
-                        pause_time: pauseTime,
-                        clear_pattern: clearPattern,
-                        run_mode: runMode, // 'single' or 'indefinite'
-                        shuffle: shuffle      // true or false
-                    })
-                });
+    <!-- Device / Settings Tab -->
+    <main class="tab-content" id="settings-tab">
+        <section>
+            <div class="header">
+                <h2>Serial Connection</h2>
+            </div>
+            <div id="serial_status_container">Status: <span id="serial_status" class="status"> Not connected</span></div>
+            <div id="serial_ports_container">
+                <label for="serial_ports">Available Ports:</label>
+                <select id="serial_ports"></select>
+                <button onclick="connectSerial()" class="cta">Connect</button>
+            </div>
+            <div id="serial_ports_buttons" class="button-group">
+                <button onclick="disconnectSerial()" class="cancel">Disconnect</button>
+                <button onclick="restartSerial()" class="warn">Restart</button>
+            </div>
+        </section>
 
-                const result = await response.json();
+        <section class="main">
+            <div class="header">
+                <h2>Device Controls</h2>
+            </div>
 
-                if (result.success) {
-                    logMessage(`Playlist "${playlistName}" is now running.`);
-                } else {
-                    logMessage(`Failed to run playlist "${playlistName}": ${result.error}`);
-                }
-            } catch (error) {
-                logMessage(`Error running playlist "${playlistName}": ${error}`);
-            }
-        }
-        
-        async function loadPlaylist(playlistName) {
-            // This fetches the named playlist from /get_playlist?name=...
-            try {
-            const response = await fetch(`/get_playlist?name=${encodeURIComponent(playlistName)}`);
-            const data = await response.json(); // { "name": "XYZ", "files": [...] }
-        
-            if (data.error) {
-                logMessage(`Error loading playlist "${playlistName}": ${data.error}`);
-                return;
-            }
-        
-            // Clear current local playlist; replace with loaded data
-            playlist = data.files || [];
-            selectedPlaylistIndex = null;
-            document.getElementById('playlist_name').value = data.name; // Fill in the name field
-        
-            refreshPlaylistUI();
-            logMessage(`Loaded playlist: ${data.name} with ${playlist.length} file(s).`);
-            } catch (err) {
-            logMessage(`Error loading playlist: ${err}`);
-            }
-        }
-        
-        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        //  PART B: Creating or Saving (Overwriting) a Playlist
-        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        // Instead of separate create/modify functions, we’ll unify them:
-        async function savePlaylist() {
-            const name = document.getElementById('playlist_name').value.trim();
-            if (!name) {
-            logMessage("Please enter a playlist name.");
-            return;
-            }
-            if (playlist.length === 0) {
-            logMessage("No files in this playlist. Add files first.");
-            return;
-            }
-        
-            logMessage(`Saving playlist "${name}" with ${playlist.length} file(s)...`);
-        
-            try {
-            // We can use /create_playlist or /modify_playlist. They do roughly the same in our single-file approach.
-            // Let's use /create_playlist to always overwrite or create anew.
-            const response = await fetch('/create_playlist', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({
-                name: name,
-                files: playlist
-                })
-            });
-            const result = await response.json();
-            if (result.success) {
-                logMessage(result.message);
-                // Reload the entire list of playlists to reflect changes
-                loadAllPlaylists();
-            } else {
-                logMessage(`Failed to save playlist: ${result.error}`);
-            }
-            } catch (err) {
-            logMessage(`Error saving playlist: ${err}`);
-            }
-        }
-        
-        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        //  PART C: Deleting a playlist
-        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        async function deletePlaylist(playlistName) {
-            if (!confirm(`Delete playlist "${playlistName}"? This cannot be undone.`)) {
-            return;
-            }
-            try {
-            const response = await fetch('/delete_playlist', {
-                method: 'DELETE',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ name: playlistName })
-            });
-            const result = await response.json();
-            if (result.success) {
-                logMessage(result.message);
-                loadAllPlaylists(); // Refresh the UI
-            } else {
-                logMessage(`Failed to delete playlist: ${result.error}`);
-            }
-            } catch (err) {
-            logMessage(`Error deleting playlist: ${err}`);
-            }
-        }
-        
-        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        //  PART D: Local playlist array UI
-        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        // Called when the user clicks "Add File to Playlist"
-        function addToPlaylist() {
-            if (!selectedFile) {
-            logMessage("No file selected to add to the playlist.");
-            return;
-            }
-            // Add the current selectedFile to the local array
-            playlist.push(selectedFile);
-            logMessage(`Added "${selectedFile}" to the playlist.`);
-            refreshPlaylistUI();
-        }
-        
-        function refreshPlaylistUI() {
-            const ul = document.getElementById('playlist_items');
-            ul.innerHTML = '';
-        
-            playlist.forEach((file, index) => {
-            const li = document.createElement('li');
-            li.textContent = file;
-        
-            // When you click on a file in the playlist, mark it as selected
-            li.onclick = () => selectPlaylistItem(index);
-        
-            // Highlight the currently selected playlist item
-            if (index === selectedPlaylistIndex) {
-                li.classList.add('selected');
-            }
-            ul.appendChild(li);
-            });
-        }
-        
-        function selectPlaylistItem(index) {
-            selectedPlaylistIndex = index;
-            refreshPlaylistUI();
-            logMessage(`Selected playlist file: ${playlist[index]}`);
-        }
-        
-        function movePlaylistItemUp() {
-            if (selectedPlaylistIndex === null || selectedPlaylistIndex <= 0) return;
-            const temp = playlist[selectedPlaylistIndex - 1];
-            playlist[selectedPlaylistIndex - 1] = playlist[selectedPlaylistIndex];
-            playlist[selectedPlaylistIndex] = temp;
-            selectedPlaylistIndex--;
-            refreshPlaylistUI();
-        }
-        
-        function movePlaylistItemDown() {
-            if (selectedPlaylistIndex === null || selectedPlaylistIndex >= playlist.length - 1) return;
-            const temp = playlist[selectedPlaylistIndex + 1];
-            playlist[selectedPlaylistIndex + 1] = playlist[selectedPlaylistIndex];
-            playlist[selectedPlaylistIndex] = temp;
-            selectedPlaylistIndex++;
-            refreshPlaylistUI();
-        }
-        
-        function removeFromPlaylist() {
-            if (selectedPlaylistIndex === null) {
-            logMessage("No item selected in the playlist to remove.");
-            return;
-            }
-            const removedFile = playlist[selectedPlaylistIndex];
-            playlist.splice(selectedPlaylistIndex, 1);
-            selectedPlaylistIndex = null;
-            refreshPlaylistUI();
-            logMessage(`Removed "${removedFile}" from the playlist.`);
-        }
-        
-        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        //  PART E: Page load initialization
-        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        // Make sure we load the existing playlists list on page load
-        document.addEventListener('DOMContentLoaded', () => {
-            loadAllPlaylists();
-        });
-         
-    </script>
+            <div class="action-buttons">
+                <button onclick="stopExecution()" class="cancel">Stop Current Pattern</button>
+                <button onclick="runClearIn()">Clear In</button>
+                <button onclick="runClearOut()">Clear Out</button>
+                <button onclick="moveToCenter()">Move to Center</button>
+                <button onclick="moveToPerimeter()">Move to Perimeter</button>
+            </div>
+            <h3>Send to Coordinate</h3>
+            <div class="control-group">
+                <div class="item">
+                    <label for="theta_input">θ:</label>
+                    <input type="number" id="theta_input" placeholder="Theta">
+                </div>
+                <div class="item">
+                    <label for="rho_input">ρ:</label>
+                    <input type="number" id="rho_input" placeholder="Rho">
+                </div>
+                <div class="item cta">
+                    <button onclick="sendCoordinate()">Send</button>
+                </div>
+            </div>
+            <h3>Pre-Execution Action</h3>
+            <div class="control-group">
+                <label class="custom-input">
+                    <input type="radio" name="pre_execution" value="clear_in" id="clear_in">
+                    <span class="custom-radio"></span>
+                    Clear from In
+                </label>
+                <label class="custom-input">
+                    <input type="radio" name="pre_execution" value="clear_out" id="clear_out">
+                    <span class="custom-radio"></span>
+                    Clear from Out
+                </label>
+                <label class="custom-input">
+                    <input type="radio" name="pre_execution" value="none" id="no_action" checked>
+                    <span class="custom-radio"></span>
+                    None
+                </label>
+            </div>
+            <div class="control-group">
+                <h3>Speed</h3>
+                <div class="item">
+                    <input type="number" id="speed_input" placeholder="1-100" min="1" step="1" max="100">
+                </div>
+                <div class="item cta">
+                    <button class="small-button"  onclick="changeSpeed()">Set Speed</button>
+                </div>
+            </div>
+        </section>
+
+        <section class="debug">
+            <div id="github">
+                <span>Help us improve! <a href="https://github.com/tuanchris/dune-weaver/pulls" target="_blank">Submit a Pull Request</a> or <a
+                        href="https://github.com/tuanchris/dune-weaver/issues/new"
+                        target="_blank">Report a Bug</a>.</span>
+                <a href="https://github.com/tuanchris/dune-weaver/issues" target="_blank">
+                    <img src="https://img.shields.io/github/issues/tuanchris/dune-weaver?style=flat-square"
+                         alt="GitHub Issues">
+                </a>
+            </div>
+            <button id="debug_button" onclick="toggleDebugLog()">🪲</button>
+        </section>
+    </main>
+</div>
+
+<!-- Tab Navigation -->
+<nav class="bottom-nav">
+    <button class="tab-button" onclick="switchTab('patterns')" id="nav-patterns">Patterns</button>
+    <button class="tab-button" onclick="switchTab('playlists')" id="nav-playlists">Playlists</button>
+    <button class="tab-button" onclick="switchTab('settings')" id="nav-settings">Device</button>
+</nav>
+
+<div id="status_log">
+    <!-- Messages will be appended here -->
+</div>
+
+<script src="../static/main.js"></script>
 </body>
 </html>