Kaynağa Gözat

Allow renaming playlists

tuanchris 2 ay önce
ebeveyn
işleme
3a3b0fa848
4 değiştirilmiş dosya ile 192 ekleme ve 15 silme
  1. 20 0
      main.py
  2. 27 0
      modules/core/playlist_manager.py
  3. 128 15
      static/js/playlists.js
  4. 17 0
      templates/playlists.html

+ 20 - 0
main.py

@@ -309,6 +309,10 @@ class LEDConfigRequest(BaseModel):
 class DeletePlaylistRequest(BaseModel):
     playlist_name: str
 
+class RenamePlaylistRequest(BaseModel):
+    old_name: str
+    new_name: str
+
 class ThetaRhoRequest(BaseModel):
     file_name: str
     pre_execution: Optional[str] = "none"
@@ -1171,6 +1175,22 @@ async def delete_playlist(request: DeletePlaylistRequest):
         "message": f"Playlist '{request.playlist_name}' deleted"
     }
 
+@app.post("/rename_playlist")
+async def rename_playlist(request: RenamePlaylistRequest):
+    """Rename an existing playlist."""
+    success, message = playlist_manager.rename_playlist(request.old_name, request.new_name)
+    if not success:
+        raise HTTPException(
+            status_code=400,
+            detail=message
+        )
+
+    return {
+        "success": True,
+        "message": message,
+        "new_name": request.new_name
+    }
+
 class AddToPlaylistRequest(BaseModel):
     playlist_name: str
     pattern: str

+ 27 - 0
modules/core/playlist_manager.py

@@ -86,6 +86,33 @@ def add_to_playlist(playlist_name, pattern):
     logger.info(f"Added pattern '{pattern}' to playlist '{playlist_name}'")
     return True
 
+def rename_playlist(old_name, new_name):
+    """Rename an existing playlist."""
+    if not new_name or not new_name.strip():
+        logger.warning("Cannot rename playlist: new name is empty")
+        return False, "New name cannot be empty"
+
+    new_name = new_name.strip()
+
+    playlists_dict = load_playlists()
+    if old_name not in playlists_dict:
+        logger.warning(f"Cannot rename non-existent playlist: {old_name}")
+        return False, "Playlist not found"
+
+    if old_name == new_name:
+        return True, "Name unchanged"
+
+    if new_name in playlists_dict:
+        logger.warning(f"Cannot rename playlist: '{new_name}' already exists")
+        return False, "A playlist with that name already exists"
+
+    # Copy files to new key and delete old key
+    playlists_dict[new_name] = playlists_dict[old_name]
+    del playlists_dict[old_name]
+    save_playlists(playlists_dict)
+    logger.info(f"Renamed playlist '{old_name}' to '{new_name}'")
+    return True, f"Playlist renamed to '{new_name}'"
+
 async def run_playlist(playlist_name, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False):
     """Run a playlist with the given options."""
     if pattern_manager.pattern_lock.locked():

+ 128 - 15
static/js/playlists.js

@@ -539,12 +539,12 @@ async function loadPlaylists() {
         if (response.ok) {
             allPlaylists = await response.json();
             displayPlaylists();
-            // Auto-select last selected
+            // Auto-select last selected using data attribute
             const last = getLastSelectedPlaylist();
             if (last && allPlaylists.includes(last)) {
                 setTimeout(() => {
                     const nav = document.getElementById('playlistsNav');
-                    const el = Array.from(nav.querySelectorAll('a')).find(a => a.textContent.trim() === last);
+                    const el = nav.querySelector(`a[data-playlist-name="${last}"]`);
                     if (el) el.click();
                 }, 0);
             }
@@ -574,12 +574,13 @@ function displayPlaylists() {
     allPlaylists.forEach(playlist => {
         const playlistItem = document.createElement('a');
         playlistItem.className = 'flex items-center gap-3 px-3 py-2.5 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100 transition-colors duration-150 cursor-pointer';
+        playlistItem.dataset.playlistName = playlist; // Add data attribute for easy lookup
         playlistItem.innerHTML = `
             <span class="material-icons text-lg text-gray-500 dark:text-gray-400">queue_music</span>
             <span class="text-sm font-medium flex-1 truncate">${playlist}</span>
             <span class="material-icons text-lg text-gray-400 dark:text-gray-500">chevron_right</span>
         `;
-        
+
         playlistItem.addEventListener('click', () => selectPlaylist(playlist, playlistItem));
         playlistsNav.appendChild(playlistItem);
     });
@@ -600,16 +601,22 @@ async function selectPlaylist(playlistName, element) {
     // Update current playlist
     currentPlaylist = playlistName;
     
-    // Update header with playlist name and delete button
+    // Update header with playlist name, rename and delete buttons
     const header = document.getElementById('currentPlaylistTitle');
     header.innerHTML = `
         <h1 class="text-gray-900 dark:text-gray-100 text-2xl font-semibold leading-tight truncate">${playlistName}</h1>
-        <button id="deletePlaylistBtn" class="p-1 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/20 text-gray-500 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 transition-all duration-150 flex-shrink-0" title="Delete playlist">
-            <span class="material-icons text-lg">delete</span>
-        </button>
+        <div class="flex items-center gap-1 flex-shrink-0">
+            <button id="renamePlaylistBtn" class="p-1 rounded-lg hover:bg-blue-100 dark:hover:bg-blue-900/20 text-gray-500 dark:text-gray-500 hover:text-blue-500 dark:hover:text-blue-400 transition-all duration-150" title="Rename playlist">
+                <span class="material-icons text-lg">edit</span>
+            </button>
+            <button id="deletePlaylistBtn" class="p-1 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/20 text-gray-500 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 transition-all duration-150" title="Delete playlist">
+                <span class="material-icons text-lg">delete</span>
+            </button>
+        </div>
     `;
-    
-    // Add delete button event listener
+
+    // Add button event listeners
+    document.getElementById('renamePlaylistBtn').addEventListener('click', () => openRenameModal(playlistName));
     document.getElementById('deletePlaylistBtn').addEventListener('click', () => deletePlaylist(playlistName));
 
     // Enable buttons
@@ -648,7 +655,7 @@ async function loadPlaylistPatterns(playlistName) {
 // Display patterns in the current playlist
 async function displayPlaylistPatterns(patterns) {
     const patternsGrid = document.getElementById('patternsGrid');
-    
+
     if (patterns.length === 0) {
         patternsGrid.innerHTML = `
             <div class="flex items-center justify-center col-span-full py-12 text-gray-500 dark:text-gray-400">
@@ -660,16 +667,16 @@ async function displayPlaylistPatterns(patterns) {
 
     // Clear grid and add all pattern cards
     patternsGrid.innerHTML = '';
-    
+
     patterns.forEach(pattern => {
         const patternCard = createPatternCard(pattern, true);
         patternsGrid.appendChild(patternCard);
         patternCard.dataset.pattern = pattern;
-        
+
         // Set up lazy loading for patterns outside viewport
         intersectionObserver.observe(patternCard);
     });
-    
+
     // After DOM is updated, immediately load previews for visible patterns
     // Use requestAnimationFrame to ensure DOM layout is complete
     requestAnimationFrame(() => {
@@ -719,10 +726,10 @@ async function loadVisiblePlaylistPreviews() {
 function createPatternCard(pattern, showRemove = false) {
     const card = document.createElement('div');
     card.className = 'flex flex-col gap-3 group cursor-pointer relative';
-    
+
     const previewContainer = document.createElement('div');
     previewContainer.className = 'w-full aspect-square bg-cover rounded-full shadow-sm group-hover:shadow-md transition-shadow duration-150 border border-gray-200 dark:border-gray-700 pattern-preview relative';
-    
+
     // Check in-memory cache first
     const previewData = previewCache.get(pattern);
     if (previewData && !previewData.error && previewData.image_data) {
@@ -1515,6 +1522,97 @@ async function deletePlaylist(playlistName) {
     }
 }
 
+// Open rename modal
+function openRenameModal(playlistName) {
+    const modal = document.getElementById('renamePlaylistModal');
+    const input = document.getElementById('renamePlaylistInput');
+
+    // Set the current name
+    input.value = playlistName;
+    input.dataset.oldName = playlistName;
+
+    // Show modal
+    modal.classList.remove('hidden');
+
+    // Focus and select input
+    const focusInput = () => {
+        input.focus();
+        input.select();
+    };
+
+    focusInput();
+    requestAnimationFrame(focusInput);
+    setTimeout(focusInput, 50);
+}
+
+// Close rename modal
+function closeRenameModal() {
+    const modal = document.getElementById('renamePlaylistModal');
+    const input = document.getElementById('renamePlaylistInput');
+
+    modal.classList.add('hidden');
+    input.value = '';
+    delete input.dataset.oldName;
+}
+
+// Rename playlist
+async function renamePlaylist() {
+    const input = document.getElementById('renamePlaylistInput');
+    const oldName = input.dataset.oldName;
+    const newName = input.value.trim();
+
+    if (!newName) {
+        showStatusMessage('Please enter a playlist name', 'warning');
+        return;
+    }
+
+    if (newName === oldName) {
+        closeRenameModal();
+        return;
+    }
+
+    try {
+        const response = await fetch('/rename_playlist', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({
+                old_name: oldName,
+                new_name: newName
+            })
+        });
+
+        if (response.ok) {
+            const data = await response.json();
+            showStatusMessage(`Playlist renamed to "${newName}"`, 'success');
+            closeRenameModal();
+
+            // Update current playlist reference
+            if (currentPlaylist === oldName) {
+                currentPlaylist = newName;
+
+                // Update last selected playlist
+                saveLastSelectedPlaylist(newName);
+            }
+
+            // Reload playlists and reselect
+            await loadPlaylists();
+
+            // Find and click the renamed playlist using data attribute
+            setTimeout(() => {
+                const nav = document.getElementById('playlistsNav');
+                const el = nav.querySelector(`a[data-playlist-name="${newName}"]`);
+                if (el) el.click();
+            }, 100);
+        } else {
+            const data = await response.json();
+            throw new Error(data.detail || 'Failed to rename playlist');
+        }
+    } catch (error) {
+        logMessage(`Error renaming playlist: ${error.message}`, LOG_TYPE.ERROR);
+        showStatusMessage(error.message || 'Failed to rename playlist', 'error');
+    }
+}
+
 // Setup event listeners
 function setupEventListeners() {
     // Mobile back button event listeners
@@ -1638,6 +1736,15 @@ function setupEventListeners() {
         }
     });
 
+    // Rename modal event listeners
+    document.getElementById('cancelRenameBtn').addEventListener('click', closeRenameModal);
+    document.getElementById('confirmRenameBtn').addEventListener('click', renamePlaylist);
+    document.getElementById('renamePlaylistInput').addEventListener('keypress', (e) => {
+        if (e.key === 'Enter') {
+            renamePlaylist();
+        }
+    });
+
     // Close modals when clicking outside
     document.getElementById('addPlaylistModal').addEventListener('click', (e) => {
         if (e.target.id === 'addPlaylistModal') {
@@ -1652,6 +1759,12 @@ function setupEventListeners() {
             document.getElementById('addPatternsModal').classList.add('hidden');
         }
     });
+
+    document.getElementById('renamePlaylistModal').addEventListener('click', (e) => {
+        if (e.target.id === 'renamePlaylistModal') {
+            closeRenameModal();
+        }
+    });
 }
 
 // Initialize playlists page

+ 17 - 0
templates/playlists.html

@@ -298,6 +298,23 @@ html:not(.dark) #availablePatternsGrid .text-xs {
   </div>
 </div>
 
+<!-- Rename Playlist Modal -->
+<div id="renamePlaylistModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
+  <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4">
+    <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Rename Playlist</h3>
+    <div class="space-y-4">
+      <div>
+        <label for="renamePlaylistInput" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">New Name</label>
+        <input id="renamePlaylistInput" type="text" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 px-3 py-2" placeholder="Enter new playlist name">
+      </div>
+      <div class="flex gap-3 justify-end">
+        <button id="cancelRenameBtn" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors duration-150">Cancel</button>
+        <button id="confirmRenameBtn" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors duration-150">Rename</button>
+      </div>
+    </div>
+  </div>
+</div>
+
 <!-- Add Patterns Modal -->
 <div id="addPatternsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
   <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-4xl mx-4 max-h-[80vh] overflow-hidden flex flex-col">