Преглед изворни кода

add indefinite run and shuffle

Tuan Nguyen пре 1 година
родитељ
комит
a9c4e6b9bf
3 измењених фајлова са 444 додато и 105 уклоњено
  1. 72 31
      app.py
  2. 372 12
      templates/index.html
  3. 0 62
      templates/playlist.html

+ 72 - 31
app.py

@@ -190,43 +190,69 @@ def get_clear_pattern_file(pattern_name):
 def run_theta_rho_files(
     file_paths,
     pause_time=0,
-    clear_pattern=None
+    clear_pattern=None,
+    run_mode="single",
+    shuffle=False
 ):
     """
-    Runs multiple .thr files in sequence, optionally pausing between each pattern,
-    and optionally running a clear pattern between files. 
-    Now supports:
-      - no_clear: skips running a clear pattern
-      - random: chooses any of the 3 clear patterns randomly
+    Runs multiple .thr files in sequence with options for pausing, clearing, shuffling, and looping.
+
+    Parameters:
+    - file_paths (list): List of file paths to run.
+    - pause_time (float): Seconds to pause between patterns.
+    - clear_pattern (str): Specific clear pattern to run ("clear_in", "clear_out", "clear_sideway", or "random").
+    - run_mode (str): "single" for one-time run or "indefinite" for looping.
+    - shuffle (bool): Whether to shuffle the playlist before running.
     """
     global stop_requested
-    stop_requested = False  # reset stop flag at the start
+    stop_requested = False  # Reset stop flag at the start
 
-    for idx, path in enumerate(file_paths):
-        if stop_requested:
-            print("Execution stopped before starting next pattern.")
-            break
-
-        # Run the main pattern
-        print(f"Running pattern {idx + 1} of {len(file_paths)}: {path}")
-        run_theta_rho_file(path)
-
-        # If there are more files to run
-        if idx < len(file_paths) - 1:
-            # Pause after each pattern if requested
-            time.sleep(pause_time)
+    if shuffle:
+        random.shuffle(file_paths)
+        print("Playlist shuffled.")
 
+    while True:
+        for idx, path in enumerate(file_paths):
             if stop_requested:
-                print("Execution stopped before running the next pattern or clear.")
-                break
-
-            if not clear_pattern:
-                continue
-            # Otherwise, get the file (could be random or a known pattern)
-            clear_file_path = get_clear_pattern_file(clear_pattern)
-            print(f"Running clear pattern: {clear_file_path}")
-            run_theta_rho_file(clear_file_path)
+                print("Execution stopped before starting next pattern.")
+                return
+
+            # 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:
+                # Pause after each pattern if requested
+                if pause_time > 0:
+                    print(f"Pausing for {pause_time} seconds...")
+                    time.sleep(pause_time)
+
+                if stop_requested:
+                    print("Execution stopped before running the next clear pattern.")
+                    return
+
+                # Determine the clear pattern to run
+                clear_file_path = get_clear_pattern_file(clear_pattern)
+                print(f"Running clear pattern: {clear_file_path}")
+                run_theta_rho_file(clear_file_path)
+
+        # After completing the playlist
+        if run_mode == "indefinite":
+            print("Playlist completed. Restarting as per 'indefinite' run mode.")
+            if pause_time > 0:
+                print(f"Pausing for {pause_time} seconds before restarting...")
+                time.sleep(pause_time)
+            if shuffle:
+                random.shuffle(file_paths)
+                print("Playlist reshuffled for the next loop.")
+            continue
+        else:
+            print("Playlist completed.")
+            break
 
+    # Reset theta after execution or stopping
+    reset_theta()
+    ser.write("FINISHED\n".encode())
     print("All requested patterns completed (or stopped).")
 
 def reset_theta():
@@ -617,7 +643,9 @@ def run_playlist():
     {
         "playlist_name": "My Playlist",
         "pause_time": 1.0,                # Optional: seconds to pause between patterns
-        "clear_pattern": "random"          # Optional: "clear_in", "clear_out", "clear_sideway", or "random"
+        "clear_pattern": "random",         # Optional: "clear_in", "clear_out", "clear_sideway", or "random"
+        "run_mode": "single",              # 'single' or 'indefinite'
+        "shuffle": True                    # true or false
     }
     """
     data = request.get_json()
@@ -629,15 +657,26 @@ def run_playlist():
     playlist_name = data["playlist_name"]
     pause_time = data.get("pause_time", 0)
     clear_pattern = data.get("clear_pattern", None)
+    run_mode = data.get("run_mode", "single")  # Default to 'single' run
+    shuffle = data.get("shuffle", False)       # Default to no shuffle
 
     # Validate pause_time
     if not isinstance(pause_time, (int, float)) or pause_time < 0:
         return jsonify({"success": False, "error": "'pause_time' must be a non-negative number"}), 400
 
+    # Validate clear_pattern
     valid_patterns = ["clear_in", "clear_out", "clear_sideway", "random"]
     if clear_pattern not in valid_patterns and clear_pattern is not None:
         return jsonify({"success": False, "error": f"'clear_pattern' must be one of {valid_patterns} or null"}), 400
 
+    # Validate run_mode
+    if run_mode not in ["single", "indefinite"]:
+        return jsonify({"success": False, "error": "'run_mode' must be 'single' or 'indefinite'"}), 400
+
+    # Validate shuffle
+    if not isinstance(shuffle, bool):
+        return jsonify({"success": False, "error": "'shuffle' must be a boolean value"}), 400
+
     # Load playlists
     playlists = load_playlists()
 
@@ -657,7 +696,9 @@ def run_playlist():
             args=(file_paths,),
             kwargs={
                 'pause_time': pause_time,
-                'clear_pattern': clear_pattern
+                'clear_pattern': clear_pattern,
+                'run_mode': run_mode,
+                'shuffle': shuffle
             },
             daemon=True  # Daemonize thread to exit with the main program
         ).start()

+ 372 - 12
templates/index.html

@@ -79,24 +79,83 @@
                     <button id="run_button" disabled>Run Selected File</button>
                     <button onclick="stopExecution()" class="delete-button">Stop</button>
                 </div>
-            </div>
-            <div class="section">
-                <h2>Upload new files</h2>
-                <div class="button-group">
-                    <input type="file" id="upload_file">
+                <div class="section modeSection single_run">
+                    <h2>Upload new files</h2>
                     <div class="button-group">
-                        <button onclick="uploadThetaRho()">Upload</button>
-                        <button id="delete_selected_button" class="delete-button" onclick="deleteSelectedFile()" disabled>Delete Selected File</button>
+                        <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>
-            </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>
+                <div class="section modeSection create_playlist run_playlist" style="display: none;">
+                    <h2>Playlists</h2>
+                    
+                    <!-- Parameter Inputs -->
+                    <div class="playlist-parameters">
+                        <label for="pause_time">Pause Time (seconds):</label>
+                        <input type="number" id="pause_time" min="0" step="0.1" value="0">
+                        
+                        <label for="clear_pattern">Clear Pattern:</label>
+                        <select id="clear_pattern">
+                            <option value="none">None</option>
+                            <option value="clear_in">Clear from In</option>
+                            <option value="clear_out">Clear from Out</option>
+                            <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 id="status_log">
-            <!-- Messages will be appended here -->
-        </div>
+    <div id="status_log">
+        <h2>Status Log</h2>
+        <!-- Messages will be appended here -->
     </div>
     <script>
         let selectedFile = null;
@@ -529,9 +588,310 @@
         // 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';
+                });
+            });
+          });
+        // 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}`);
+
+            try {
+                const response = await fetch('/run_playlist', {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify({
+                        playlist_name: playlistName,
+                        pause_time: pauseTime,
+                        clear_between_files: clearPattern !== null,
+                        clear_pattern: clearPattern,
+                        run_mode: runMode, // 'single' or 'indefinite'
+                        shuffle: shuffle      // true or false
+                    })
+                });
+
+                const result = await response.json();
+
+                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>
 </body>
 </html>

+ 0 - 62
templates/playlist.html

@@ -1,62 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title>Sand Table Playlist</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>Manage and Run Playlists</h1>
-    <div class="container">
-
-        <!-- Section to build the playlist -->
-        <div class="left-column">
-            <div class="section">
-                <h2>Create Playlist</h2>
-                <p>Add .thr files to your playlist below.</p>
-                <input type="text" id="search_pattern" placeholder="Search files..." oninput="searchPatternFiles()">
-                <ul id="theta_rho_files"></ul>
-            </br>
-                <button onclick="addToPlaylist()">Add to Playlist</button>
-                <ul id="playlist_files"></ul>
-            </div>
-            <div class="section">
-                <h2>Preview</h2>
-                <canvas id="patternPreviewCanvas" style="width: 100%; height: 100%;"></canvas>
-                <p id="first_coordinate">First Coordinate: Not available</p>
-                <p id="last_coordinate">Last Coordinate: Not available</p>
-            </div>
-        </div>
-
-        <!-- Section to set parameters -->
-        <div class="right-column">
-            <div class="section">
-                <h2>Pattern Files</h2>
-                <label for="pause_time">Pause between patterns (min):</label>
-                <input type="number" id="pause_time" value="0" step="1" min="0">
-
-                <br><br>
-                <label for="clear_pattern">Clear Pattern:</label>
-                <select id="clear_pattern">
-                    <option value="no_clear">No Clear</option>
-                    <option value="clear_from_in">Clear from In</option>
-                    <option value="clear_from_out">Clear from Out</option>
-                    <option value="clear_sideway">Clear Sideway</option>
-                    <option value="random">Random</option>
-                </select>
-                <div class="button-group">
-                    <button onclick="runPlaylist()">Run Playlist</button>
-                    <button onclick="stopExecution()" class="delete-button">Stop</button>
-                </div>
-            </div>
-        </div>
-
-        <!-- Status/Log -->
-        <div id="status_log">
-            <h2>Playlist Status</h2>
-            <!-- Messages will be appended here -->
-        </div>
-    </div>
-</body>
-</html>