Browse Source

Fix floating camera drag/resize not working on iOS/iPadOS (#687)

  The embedded camera viewer only handled mouse events (mousedown,
  mousemove, mouseup) for window dragging and resizing. On iOS/iPadOS,
  touch input doesn't trigger mouse events, so the camera window
  couldn't be repositioned — touch just scrolled the page underneath.

  Add touch event handlers (touchstart, touchmove, touchend, touchcancel)
  alongside mouse handlers for both the header drag handle and the
  bottom-right resize handle. Uses preventDefault on touchmove to
  prevent page scrolling during drag.
maziggy 2 months ago
parent
commit
cfec18eca4

+ 1 - 0
CHANGELOG.md

@@ -32,6 +32,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Local Profiles Not Shown in AMS Slot Configuration** — Imported local filament profiles were hidden in the AMS slot configure modal when a printer model was set. The `compatible_printers` filter parsed the stored JSON array as a semicolon-delimited string, so the matching always failed and every local preset was silently skipped. Removed the filter entirely — user-imported profiles should be available on any printer.
 - **Interface Aliases Not Shown in Virtual Printer Interface Select** — Interface aliases (e.g. `eth0:1`) added for multi-virtual-printer setups were invisible in the bind IP dropdown. The Docker image didn't include `iproute2`, so the `ip` command wasn't available and the code fell back to ioctl-based enumeration which can only return one IP per interface. Added `iproute2` to the Docker image.
 - **P2S Camera Stream Disconnects After a Few Seconds** ([#661](https://github.com/maziggy/bambuddy/issues/661)) — The P2S firmware drops RTSP sessions after a few seconds with an I/O error. The backend treated this as a fatal failure, ending the MJPEG stream and forcing the frontend through a full reconnection cycle (stop → start → brief connection → fail → repeat). Added transparent auto-reconnection: when ffmpeg's RTSP connection dies, it respawns immediately and continues streaming MJPEG frames to the browser without interruption. Reported by @ddetton, confirmed by @DMoenning.
+- **iOS/iPadOS Cannot Reposition Floating Camera** ([#687](https://github.com/maziggy/bambuddy/issues/687)) — The floating camera viewer (embedded camera window on the dashboard) could not be dragged or resized on iOS/iPadOS because it only handled mouse events. Touch input scrolled the page underneath instead of moving the camera window. Added touch event support (`touchstart`/`touchmove`/`touchend`) to both the header drag handle and the resize handle, with `preventDefault` to stop page scrolling during drag. Reported by @dsmitty166.
 - **Debug Logging Endpoint 500 Error** — The `GET /api/v1/support/debug-logging` endpoint returned a 500 Internal Server Error when the database contained a timezone-aware timestamp written by a previous version. The duration calculation subtracted a timezone-aware datetime from a naive `datetime.now()`, raising `TypeError`. Now strips timezone info when reading the stored timestamp.
 - **Bed Cooled Notification Never Fires** ([#497](https://github.com/maziggy/bambuddy/issues/497)) — The bed cooldown monitor always timed out after 30 minutes without sending a notification. After print completion, P1S (and likely other models) sends partial MQTT status updates that don't include `bed_temper`, so the cached bed temperature stayed frozen at the end-of-print value and never dropped below the threshold. The monitor now sends periodic `pushall` commands to the printer to force fresh temperature data. Also added debug logging to the polling loop for future diagnostics.
 - **Notification Provider Missing Event Toggles on Create** ([#497](https://github.com/maziggy/bambuddy/issues/497)) — When creating a new notification provider, the `on_bed_cooled` toggle and all 7 queue event toggles (`on_queue_job_added`, `on_queue_job_assigned`, `on_queue_job_started`, `on_queue_job_waiting`, `on_queue_job_skipped`, `on_queue_job_failed`, `on_queue_completed`) were silently discarded. The create endpoint manually listed each field but omitted these 8 toggles, so they always defaulted to `false` regardless of user selection. Editing an existing provider worked correctly.

+ 43 - 0
frontend/src/components/EmbeddedCameraViewer.tsx

@@ -470,12 +470,27 @@ export function EmbeddedCameraViewer({ printerId, printerName, viewerIndex = 0,
     });
   };
 
+  const handleDragTouchStart = (e: React.TouchEvent) => {
+    if ((e.target as HTMLElement).closest('.no-drag')) return;
+    const touch = e.touches[0];
+    setIsDragging(true);
+    setDragOffset({
+      x: touch.clientX - state.x,
+      y: touch.clientY - state.y,
+    });
+  };
+
   // Resize handlers
   const handleResizeMouseDown = (e: React.MouseEvent) => {
     e.stopPropagation();
     setIsResizing(true);
   };
 
+  const handleResizeTouchStart = (e: React.TouchEvent) => {
+    e.stopPropagation();
+    setIsResizing(true);
+  };
+
   useEffect(() => {
     const handleMouseMove = (e: MouseEvent) => {
       if (isDragging) {
@@ -494,6 +509,26 @@ export function EmbeddedCameraViewer({ printerId, printerName, viewerIndex = 0,
       }
     };
 
+    const handleTouchMove = (e: TouchEvent) => {
+      if (!isDragging && !isResizing) return;
+      e.preventDefault();
+      const touch = e.touches[0];
+      if (isDragging) {
+        setState((prev) => ({
+          ...prev,
+          x: Math.max(0, Math.min(touch.clientX - dragOffset.x, window.innerWidth - prev.width)),
+          y: Math.max(0, Math.min(touch.clientY - dragOffset.y, window.innerHeight - prev.height)),
+        }));
+      } else if (isResizing && containerRef.current) {
+        const rect = containerRef.current.getBoundingClientRect();
+        setState((prev) => ({
+          ...prev,
+          width: Math.max(200, Math.min(touch.clientX - rect.left, window.innerWidth - prev.x - 10)),
+          height: Math.max(150, Math.min(touch.clientY - rect.top, window.innerHeight - prev.y - 10)),
+        }));
+      }
+    };
+
     const handleMouseUp = () => {
       setIsDragging(false);
       setIsResizing(false);
@@ -502,9 +537,15 @@ export function EmbeddedCameraViewer({ printerId, printerName, viewerIndex = 0,
     if (isDragging || isResizing) {
       document.addEventListener('mousemove', handleMouseMove);
       document.addEventListener('mouseup', handleMouseUp);
+      document.addEventListener('touchmove', handleTouchMove, { passive: false });
+      document.addEventListener('touchend', handleMouseUp);
+      document.addEventListener('touchcancel', handleMouseUp);
       return () => {
         document.removeEventListener('mousemove', handleMouseMove);
         document.removeEventListener('mouseup', handleMouseUp);
+        document.removeEventListener('touchmove', handleTouchMove);
+        document.removeEventListener('touchend', handleMouseUp);
+        document.removeEventListener('touchcancel', handleMouseUp);
       };
     }
   }, [isDragging, isResizing, dragOffset]);
@@ -527,6 +568,7 @@ export function EmbeddedCameraViewer({ printerId, printerName, viewerIndex = 0,
       <div
         className="flex items-center justify-between px-3 py-2 bg-bambu-dark border-b border-bambu-dark-tertiary cursor-grab active:cursor-grabbing"
         onMouseDown={handleMouseDown}
+        onTouchStart={handleDragTouchStart}
       >
         <div className="flex items-center gap-2 text-sm text-white truncate">
           <GripVertical className="w-4 h-4 text-bambu-gray flex-shrink-0" />
@@ -685,6 +727,7 @@ export function EmbeddedCameraViewer({ printerId, printerName, viewerIndex = 0,
             <div
               className="absolute bottom-0 right-0 w-6 h-6 cursor-se-resize no-drag hover:bg-white/10 rounded-tl transition-colors"
               onMouseDown={handleResizeMouseDown}
+              onTouchStart={handleResizeTouchStart}
               title="Drag to resize"
             >
               <svg

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BwyddZpl.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-C7vUGUA4.js"></script>
+    <script type="module" crossorigin src="/assets/index-BwyddZpl.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index--YKaUCwD.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff