Преглед на файлове

customize app name, fix last previews not loading, support multiple uploads

tuanchris преди 5 месеца
родител
ревизия
63a548c84b
променени са 12 файла, в които са добавени 235 реда и са изтрити 82 реда
  1. 24 6
      main.py
  2. 6 0
      modules/core/state.py
  3. 92 36
      static/js/index.js
  4. 55 2
      static/js/settings.js
  5. 3 3
      templates/base.html
  6. 1 1
      templates/design.html
  7. 1 1
      templates/image2sand.html
  8. 6 29
      templates/index.html
  9. 1 1
      templates/playlists.html
  10. 44 1
      templates/settings.html
  11. 1 1
      templates/table_control.html
  12. 1 1
      templates/wled.html

+ 24 - 6
main.py

@@ -211,11 +211,11 @@ async def websocket_cache_progress_endpoint(websocket: WebSocket):
 # FastAPI routes
 @app.get("/")
 async def index(request: Request):
-    return templates.TemplateResponse("index.html", {"request": request})
+    return templates.TemplateResponse("index.html", {"request": request, "app_name": state.app_name})
 
 @app.get("/settings")
 async def settings(request: Request):
-    return templates.TemplateResponse("settings.html", {"request": request})
+    return templates.TemplateResponse("settings.html", {"request": request, "app_name": state.app_name})
 
 @app.get("/list_serial_ports")
 async def list_ports():
@@ -857,6 +857,24 @@ async def set_clear_pattern_speed(request: dict):
         logger.error(f"Failed to set clear pattern speed: {str(e)}")
         raise HTTPException(status_code=500, detail=str(e))
 
+@app.get("/api/app-name")
+async def get_app_name():
+    """Get current application name."""
+    return {"app_name": state.app_name}
+
+@app.post("/api/app-name")
+async def set_app_name(request: dict):
+    """Update application name."""
+    app_name = request.get("app_name", "").strip()
+    if not app_name:
+        app_name = "Dune Weaver"  # Reset to default if empty
+    
+    state.app_name = app_name
+    state.save()
+    
+    logger.info(f"Application name updated to: {app_name}")
+    return {"success": True, "app_name": app_name}
+
 @app.post("/preview_thr_batch")
 async def preview_thr_batch(request: dict):
     start = time.time()
@@ -927,19 +945,19 @@ async def preview_thr_batch(request: dict):
 @app.get("/playlists")
 async def playlists(request: Request):
     logger.debug("Rendering playlists page")
-    return templates.TemplateResponse("playlists.html", {"request": request})
+    return templates.TemplateResponse("playlists.html", {"request": request, "app_name": state.app_name})
 
 @app.get("/image2sand")
 async def image2sand(request: Request):
-    return templates.TemplateResponse("image2sand.html", {"request": request})
+    return templates.TemplateResponse("image2sand.html", {"request": request, "app_name": state.app_name})
 
 @app.get("/wled")
 async def wled(request: Request):
-    return templates.TemplateResponse("wled.html", {"request": request})
+    return templates.TemplateResponse("wled.html", {"request": request, "app_name": state.app_name})
 
 @app.get("/table_control")
 async def table_control(request: Request):
-    return templates.TemplateResponse("table_control.html", {"request": request})
+    return templates.TemplateResponse("table_control.html", {"request": request, "app_name": state.app_name})
 
 @app.get("/cache-progress")
 async def get_cache_progress_endpoint():

+ 6 - 0
modules/core/state.py

@@ -49,6 +49,10 @@ class AppState:
         self._clear_pattern_speed = 200  # Default speed for clearing patterns
         self.custom_clear_from_in = None  # Custom clear from center pattern
         self.custom_clear_from_out = None  # Custom clear from perimeter pattern
+        
+        # Application name setting
+        self.app_name = "Dune Weaver"  # Default app name
+        
         self.load()
 
     @property
@@ -173,6 +177,7 @@ class AppState:
             "custom_clear_from_out": self.custom_clear_from_out,
             "port": self.port,
             "wled_ip": self.wled_ip,
+            "app_name": self.app_name,
         }
 
     def from_dict(self, data):
@@ -202,6 +207,7 @@ class AppState:
         self.custom_clear_from_out = data.get("custom_clear_from_out", None)
         self.port = data.get("port", None)
         self.wled_ip = data.get('wled_ip', None)
+        self.app_name = data.get("app_name", "Dune Weaver")
 
     def save(self):
         """Save the current state to a JSON file."""

+ 92 - 36
static/js/index.js

@@ -343,7 +343,7 @@ function initPreviewObserver() {
             }
         });
     }, {
-        rootMargin: '200px 0px', // Reduced margin for more precise loading
+        rootMargin: '200px 0px',
         threshold: 0.1
     });
 }
@@ -402,6 +402,16 @@ async function addPatternToBatch(pattern, element) {
     // Process batch immediately if it's full or if it's a new upload
     if (pendingPatterns.size >= LAZY_BATCH_SIZE || isNewUpload) {
         processPendingBatch();
+    } else {
+        // Set a timeout to process smaller batches if they don't fill up
+        if (batchTimeout) {
+            clearTimeout(batchTimeout);
+        }
+        batchTimeout = setTimeout(() => {
+            if (pendingPatterns.size > 0) {
+                processPendingBatch();
+            }
+        }, 500); // Process after 500ms if batch doesn't fill up
     }
 }
 
@@ -416,6 +426,8 @@ function updatePreviewElement(element, imageUrl) {
         requestAnimationFrame(() => {
             img.style.opacity = '1';
         });
+        // Mark element as loaded to prevent duplicate loading attempts
+        element.dataset.loaded = 'true';
     };
     img.src = imageUrl;
     img.alt = 'Pattern Preview';
@@ -425,6 +437,12 @@ function updatePreviewElement(element, imageUrl) {
 async function processPendingBatch() {
     if (pendingPatterns.size === 0) return;
     
+    // Clear any pending timeout since we're processing now
+    if (batchTimeout) {
+        clearTimeout(batchTimeout);
+        batchTimeout = null;
+    }
+    
     // Create a copy of current pending patterns and clear the original
     const currentBatch = new Map(pendingPatterns);
     pendingPatterns.clear();
@@ -1273,57 +1291,95 @@ function updateCurrentlyPlayingUI(status) {
 
 // Setup upload event handlers
 function setupUploadEventHandlers() {
-    // Upload file input handler
+    // Upload file input handler - supports multiple files
     document.getElementById('patternFileInput').addEventListener('change', async function(e) {
-        const file = e.target.files[0];
-        if (!file) return;
+        const files = e.target.files;
+        if (!files || files.length === 0) return;
 
-        try {
-            const formData = new FormData();
-            formData.append('file', file);
-
-            const response = await fetch('/upload_theta_rho', {
-                method: 'POST',
-                body: formData
-            });
+        const totalFiles = files.length;
+        const fileArray = Array.from(files);
+        let successCount = 0;
+        let failCount = 0;
+        
+        // Show initial progress message
+        showStatusMessage(`Uploading ${totalFiles} pattern${totalFiles > 1 ? 's' : ''}...`);
 
-            const result = await response.json();
-            if (result.success) {
-                showStatusMessage(`Pattern "${file.name}" uploaded successfully`);
+        try {
+            // Upload files sequentially to avoid overwhelming the server
+            for (let i = 0; i < fileArray.length; i++) {
+                const file = fileArray[i];
                 
-                // Clear any existing cache for this pattern to ensure fresh loading
-                const newPatternPath = `custom_patterns/${file.name}`;
-                previewCache.delete(newPatternPath);
+                try {
+                    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) {
+                        successCount++;
+                        
+                        // Clear any existing cache for this pattern to ensure fresh loading
+                        const newPatternPath = `custom_patterns/${file.name}`;
+                        previewCache.delete(newPatternPath);
+                        
+                        logMessage(`Successfully uploaded: ${file.name}`, LOG_TYPE.SUCCESS);
+                    } else {
+                        failCount++;
+                        logMessage(`Failed to upload ${file.name}: ${result.error}`, LOG_TYPE.ERROR);
+                    }
+                } catch (fileError) {
+                    failCount++;
+                    logMessage(`Error uploading ${file.name}: ${fileError.message}`, LOG_TYPE.ERROR);
+                }
+                
+                // Update progress
+                const progress = i + 1;
+                showStatusMessage(`Uploading patterns... ${progress}/${totalFiles}`);
+            }
+            
+            // Show final result
+            if (successCount > 0) {
+                const message = failCount > 0 
+                    ? `Uploaded ${successCount} pattern${successCount > 1 ? 's' : ''}, ${failCount} failed`
+                    : `Successfully uploaded ${successCount} pattern${successCount > 1 ? 's' : ''}`;
+                showStatusMessage(message);
                 
                 // Add a small delay to allow backend preview generation to complete
                 await new Promise(resolve => setTimeout(resolve, 1000));
                 
-                // Refresh the pattern list (force refresh since new pattern was uploaded)
+                // Refresh the pattern list (force refresh since new patterns were uploaded)
                 await loadPatterns(true);
                 
-                // Clear the file input
-                e.target.value = '';
-                
-                // Trigger preview loading for newly uploaded patterns with extended retry
+                // Trigger preview loading for newly uploaded patterns
                 setTimeout(() => {
-                    const newPatternCard = document.querySelector(`[data-pattern="${newPatternPath}"]`);
-                    if (newPatternCard) {
-                        const previewContainer = newPatternCard.querySelector('.pattern-preview');
-                        if (previewContainer) {
-                            // Clear any existing retry count and force reload
-                            previewContainer.dataset.retryCount = '0';
-                            previewContainer.dataset.hasTriedIndividual = 'false';
-                            previewContainer.dataset.isNewUpload = 'true';
-                            addPatternToBatch(newPatternPath, previewContainer);
+                    fileArray.forEach(file => {
+                        const newPatternPath = `custom_patterns/${file.name}`;
+                        const newPatternCard = document.querySelector(`[data-pattern="${newPatternPath}"]`);
+                        if (newPatternCard) {
+                            const previewContainer = newPatternCard.querySelector('.pattern-preview');
+                            if (previewContainer) {
+                                previewContainer.dataset.retryCount = '0';
+                                previewContainer.dataset.hasTriedIndividual = 'false';
+                                previewContainer.dataset.isNewUpload = 'true';
+                                addPatternToBatch(newPatternPath, previewContainer);
+                            }
                         }
-                    }
+                    });
                 }, 500);
             } else {
-                showStatusMessage(`Failed to upload pattern: ${result.error}`, 'error');
+                showStatusMessage(`Failed to upload all ${totalFiles} pattern${totalFiles > 1 ? 's' : ''}`, 'error');
             }
+            
+            // Clear the file input
+            e.target.value = '';
+            
         } catch (error) {
-            console.error('Error uploading pattern:', error);
-            showStatusMessage(`Error uploading pattern: ${error.message}`, 'error');
+            console.error('Error during batch upload:', error);
+            showStatusMessage(`Error uploading patterns: ${error.message}`, 'error');
         }
     });
 

+ 55 - 2
static/js/settings.js

@@ -177,8 +177,11 @@ document.addEventListener('DOMContentLoaded', async () => {
         fetch('/api/custom_clear_patterns').then(response => response.json()).catch(() => ({ custom_clear_from_in: null, custom_clear_from_out: null })),
         
         // Load current clear pattern speed
-        fetch('/api/clear_pattern_speed').then(response => response.json()).catch(() => ({ clear_pattern_speed: 200 }))
-    ]).then(([statusData, wledData, updateData, ports, patterns, clearPatterns, clearSpeedData]) => {
+        fetch('/api/clear_pattern_speed').then(response => response.json()).catch(() => ({ clear_pattern_speed: 200 })),
+        
+        // Load current app name
+        fetch('/api/app-name').then(response => response.json()).catch(() => ({ app_name: 'Dune Weaver' }))
+    ]).then(([statusData, wledData, updateData, ports, patterns, clearPatterns, clearSpeedData, appNameData]) => {
         // Update connection status
         setCachedConnectionStatus(statusData);
         updateConnectionUI(statusData);
@@ -277,6 +280,12 @@ document.addEventListener('DOMContentLoaded', async () => {
         if (clearPatternSpeedInput && clearSpeedData && clearSpeedData.clear_pattern_speed) {
             clearPatternSpeedInput.value = clearSpeedData.clear_pattern_speed;
         }
+        
+        // Update app name
+        const appNameInput = document.getElementById('appNameInput');
+        if (appNameInput && appNameData.app_name) {
+            appNameInput.value = appNameData.app_name;
+        }
     }).catch(error => {
         logMessage(`Error initializing settings page: ${error.message}`, LOG_TYPE.ERROR);
     });
@@ -287,6 +296,50 @@ document.addEventListener('DOMContentLoaded', async () => {
 
 // Setup event listeners
 function setupEventListeners() {
+    // Save App Name
+    const saveAppNameButton = document.getElementById('saveAppName');
+    const appNameInput = document.getElementById('appNameInput');
+    if (saveAppNameButton && appNameInput) {
+        saveAppNameButton.addEventListener('click', async () => {
+            const appName = appNameInput.value.trim() || 'Dune Weaver';
+            
+            try {
+                const response = await fetch('/api/app-name', {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify({ app_name: appName })
+                });
+                
+                if (response.ok) {
+                    const data = await response.json();
+                    showStatusMessage('Application name updated successfully. Refresh the page to see changes.', 'success');
+                    
+                    // Update the page title and header immediately
+                    document.title = `Settings - ${data.app_name}`;
+                    const headerTitle = document.querySelector('h1.text-gray-800');
+                    if (headerTitle) {
+                        // Update just the text content, preserving the connection status dot
+                        const textNode = headerTitle.childNodes[0];
+                        if (textNode && textNode.nodeType === Node.TEXT_NODE) {
+                            textNode.textContent = data.app_name;
+                        }
+                    }
+                } else {
+                    throw new Error('Failed to save application name');
+                }
+            } catch (error) {
+                showStatusMessage(`Failed to save application name: ${error.message}`, 'error');
+            }
+        });
+        
+        // Handle Enter key in app name input
+        appNameInput.addEventListener('keypress', (e) => {
+            if (e.key === 'Enter') {
+                saveAppNameButton.click();
+            }
+        });
+    }
+    
     // Save/Clear WLED configuration
     const saveWledConfig = document.getElementById('saveWledConfig');
     const wledIpInput = document.getElementById('wledIpInput');

+ 3 - 3
templates/base.html

@@ -18,7 +18,7 @@
       onload="this.rel='stylesheet'"
       rel="stylesheet"
     />
-    <title>{% block title %}Kinetic Sand Table{% endblock %}</title>
+    <title>{% block title %}{{ app_name or 'Dune Weaver' }}{% endblock %}</title>
     <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
     <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
     <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
@@ -266,7 +266,7 @@
     <div
       class="relative flex size-full min-h-screen flex-col group/design-root overflow-x-hidden"
     >
-      <div class="layout-container flex h-full grow flex-col">
+      <div class="layout-container flex min-h-full grow flex-col">
         <header
           class="fixed top-0 left-0 right-0 z-10 flex items-center justify-between whitespace-nowrap border-b border-solid border-b-gray-200 bg-white px-4 sm:px-6 md:px-10 py-3 sm:py-4 shadow-sm"
         >
@@ -278,7 +278,7 @@
             <h1
               class="text-gray-800 text-xl font-bold leading-tight tracking-tight flex items-center gap-2"
             >
-              Dune Weaver
+              {{ app_name or 'Dune Weaver' }}
               <span
                 id="connectionStatusDot"
                 class="inline-block size-2 rounded-full bg-red-500 ml-2 align-middle"

+ 1 - 1
templates/design.html

@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 
-{% block title %}Design - Kinetic Sand Table{% endblock %}
+{% block title %}Design - {{ app_name or 'Dune Weaver' }}{% endblock %}
 
 {% block additional_styles %}
 .design-canvas {

+ 1 - 1
templates/image2sand.html

@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 
-{% block title %}Image2Sand - Kinetic Sand Table{% endblock %}
+{% block title %}Image2Sand - {{ app_name or 'Dune Weaver' }}{% endblock %}
 
 {% block additional_styles %}
 <style>

+ 6 - 29
templates/index.html

@@ -182,7 +182,7 @@
 {% endblock %}
 
 {% block content %}
-<div class="layout-content-container flex flex-col w-full max-w-5xl h-[calc(100vh-8rem)]">
+<div class="layout-content-container flex flex-col w-full max-w-5xl mb-24">
     <div class="flex-none bg-gray-50 py-4">
         <div class="flex flex-wrap items-center justify-between gap-4 mt-2 sm:mt-8">
             <h2 class="text-gray-900 text-3xl font-bold leading-tight tracking-tight">
@@ -219,6 +219,7 @@
                         type="file"
                         id="patternFileInput"
                         accept=".thr"
+                        multiple
                         class="hidden"
                 />
                 <button
@@ -231,10 +232,12 @@
             </div>
         </div>
     </div>
-    <section class="flex-1 overflow-y-auto px-4 py-6">
+    <section class="px-4 py-6">
         <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 sm:gap-5 md:gap-6">
             <!-- All patterns will be populated here -->
         </div>
+        <!-- Spacer to allow scrolling past last patterns -->
+        <div style="height: 200px; background: transparent;"></div>
     </section>
 </div>
 
@@ -385,33 +388,7 @@
 {% block scripts %}
 <script src="/static/js/index.js"></script>
 <script>
-// Add event listener for file input change
-document.getElementById('patternFileInput').addEventListener('change', async function(e) {
-  const file = e.target.files[0];
-  if (!file) return;
-
-  try {
-    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) {
-      // Refresh the pattern list
-      loadPatterns();
-      // Clear the file input
-      e.target.value = '';
-    } else {
-      console.error('Failed to upload pattern:', result.error);
-    }
-  } catch (error) {
-    console.error('Error uploading pattern:', error);
-  }
-});
+// File upload is handled in index.js
 
 // Update delete button state when pattern is selected
 function updateDeleteButtonState(patternName) {

+ 1 - 1
templates/playlists.html

@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 
-{% block title %}Playlists - Kinetic Sand Table{% endblock %}
+{% block title %}Playlists - {{ app_name or 'Dune Weaver' }}{% endblock %}
 
 {% block additional_styles %}
 /* Minimal custom styles - rely on Tailwind utilities */

+ 44 - 1
templates/settings.html

@@ -1,4 +1,4 @@
-{% extends "base.html" %} {% block title %}Settings - Kinetic Sand Table{%
+{% extends "base.html" %} {% block title %}Settings - {{ app_name or 'Dune Weaver' }}{%
 endblock %}
 
 {% block additional_styles %}
@@ -125,6 +125,49 @@ endblock %}
       Settings
     </h1>
   </div>
+  <section class="bg-white rounded-xl shadow-sm overflow-hidden">
+    <h2
+      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+    >
+      Application Settings
+    </h2>
+    <div class="px-6 py-5 space-y-6">
+      <label class="flex flex-col gap-1.5">
+        <span class="text-slate-700 text-sm font-medium leading-normal"
+          >Application Name</span
+        >
+        <div class="flex gap-3 items-center">
+          <div class="relative flex-1">
+            <input
+              id="appNameInput"
+              class="form-input flex w-full min-w-0 resize-none overflow-hidden rounded-lg text-slate-900 focus:outline-0 focus:ring-2 focus:ring-sky-500 border border-slate-300 bg-white focus:border-sky-500 h-10 placeholder:text-slate-400 px-4 pr-10 text-base font-normal leading-normal transition-colors"
+              placeholder="e.g., Dune Weaver"
+              value="Dune Weaver"
+            />
+            <button
+              type="button"
+              onclick="document.getElementById('appNameInput').value='Dune Weaver';"
+              class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700"
+              aria-label="Reset to default"
+              title="Reset to default"
+            >
+              <span class="material-icons">restart_alt</span>
+            </button>
+          </div>
+          <button
+            id="saveAppName"
+            class="flex items-center justify-center gap-2 min-w-[140px] cursor-pointer rounded-lg h-10 px-4 bg-sky-600 hover:bg-sky-700 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors flex-shrink-0"
+          >
+            <span class="material-icons text-lg">save</span>
+            <span class="truncate">Save Name</span>
+          </button>
+        </div>
+        <p class="text-xs text-slate-500 mt-2">
+          This name will appear in the browser tab and at the top of every page.
+        </p>
+      </label>
+    </div>
+  </section>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
       class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"

+ 1 - 1
templates/table_control.html

@@ -1,5 +1,5 @@
 {% extends "base.html" %}
-{% block title %}Table Control - Kinetic Sand Table{% endblock %}
+{% block title %}Table Control - {{ app_name or 'Dune Weaver' }}{% endblock %}
 
 {% block additional_styles %}
 /* Dark mode styles for table control page */

+ 1 - 1
templates/wled.html

@@ -1,4 +1,4 @@
-{% extends "base.html" %} {% block title %}WLED - Kinetic Sand Table{% endblock %}
+{% extends "base.html" %} {% block title %}WLED - {{ app_name or 'Dune Weaver' }}{% endblock %}
 
 {% block additional_styles %}
 /* Dark mode styles for WLED page */