Add swipe-to-switch-printer and auto-navigate on tag scan

Swiping left/right on the SpoolBuddy touchscreen now cycles through
  online printers instead of triggering browser navigation. Auto-navigates
  to dashboard when a new NFC tag is detected on any other page, with
  edge detection so a persistent tag doesn't block manual navigation.
This commit is contained in:
maziggy
2026-03-26 10:44:28 +01:00
parent 57a774c32f
commit ceb599ec00
4 changed files with 128 additions and 90 deletions
+1
View File
@@ -20,6 +20,7 @@ All notable changes to Bambuddy will be documented in this file.
- **SpoolBuddy Assign-to-AMS Material Mismatch Warnings** — The SpoolBuddy "Assign to AMS" modal now warns when the spool's material or slicer profile doesn't match the target slot's current filament. Shows a confirmation dialog with five warning levels: exact material mismatch, partial material match, profile-only mismatch, and combined material+profile mismatches. Respects the global `disable_filament_warnings` setting. Previously, assigning a spool to an occupied slot proceeded without any validation, matching the behavior already present in the main Assign Spool modal.
- **Spool Assignment Changes Sync Across Tabs** — Assigning or unassigning a spool now broadcasts a WebSocket event to all connected clients. Other open browser tabs and the SpoolBuddy frontend update automatically without requiring a page reload.
- **SpoolBuddy Auto-Navigate on Tag Scan** — When an NFC tag is detected while the SpoolBuddy UI is on a non-dashboard page (Settings, AMS, Write Tag, etc.), the frontend automatically navigates back to the main dashboard to show the scanned spool. Also wakes the screen if the display was blanked.
- **SpoolBuddy Swipe to Switch Printers** — Swiping left/right on the SpoolBuddy touchscreen now cycles through online printers instead of triggering browser back/forward navigation. The selected printer updates in the top bar dropdown. Requires at least two online printers; single-printer setups are unaffected.
- **SpoolBuddy System Tab** — Added a "System" tab to SpoolBuddy Settings showing live OS stats from the Raspberry Pi: CPU temperature, core count, load average, memory usage, disk usage, OS distro/kernel/architecture, Python version, and system uptime. Stats are collected by the daemon every heartbeat (10s) using stdlib-only reads from `/proc` and `/sys` — no additional dependencies required. Usage bars turn amber at 70% and red at 90%; CPU temperature is color-coded green/amber/red.
### Fixed
@@ -1,12 +1,12 @@
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueries } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { SpoolBuddyTopBar } from './SpoolBuddyTopBar';
import { SpoolBuddyBottomNav } from './SpoolBuddyBottomNav';
import { SpoolBuddyStatusBar } from './SpoolBuddyStatusBar';
import { useSpoolBuddyState } from '../../hooks/useSpoolBuddyState';
import { api, spoolbuddyApi } from '../../api/client';
import { api, spoolbuddyApi, type Printer } from '../../api/client';
import { VirtualKeyboard } from '../VirtualKeyboard';
export function SpoolBuddyLayout() {
@@ -124,6 +124,41 @@ export function SpoolBuddyLayout() {
return () => clearInterval(interval);
}, [displayBlankTimeout]);
// Online printers list for swipe-to-switch
const { data: printers = [] } = useQuery({
queryKey: ['printers'],
queryFn: () => api.getPrinters(),
});
const statusQueries = useQueries({
queries: printers.map((printer: Printer) => ({
queryKey: ['printerStatus', printer.id],
queryFn: () => api.getPrinterStatus(printer.id),
refetchInterval: 10000,
})),
});
const onlinePrinters = useMemo(() => {
return printers.filter((_: Printer, i: number) => statusQueries[i]?.data?.connected);
}, [printers, statusQueries]);
// Swipe left/right to cycle through online printers
const touchStartRef = useRef<{ x: number; y: number } | null>(null);
const SWIPE_THRESHOLD = 50;
const handleTouchStart = useCallback((e: React.TouchEvent) => {
touchStartRef.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
}, []);
const handleTouchEnd = useCallback((e: React.TouchEvent) => {
if (!touchStartRef.current || onlinePrinters.length < 2) return;
const dx = e.changedTouches[0].clientX - touchStartRef.current.x;
const dy = e.changedTouches[0].clientY - touchStartRef.current.y;
touchStartRef.current = null;
if (Math.abs(dx) < SWIPE_THRESHOLD || Math.abs(dy) > Math.abs(dx)) return;
const currentIdx = onlinePrinters.findIndex((p: Printer) => p.id === selectedPrinterId);
const nextIdx = dx < 0
? (currentIdx + 1) % onlinePrinters.length // swipe left → next
: (currentIdx - 1 + onlinePrinters.length) % onlinePrinters.length; // swipe right → prev
setSelectedPrinterId(onlinePrinters[nextIdx].id);
}, [onlinePrinters, selectedPrinterId, setSelectedPrinterId]);
// CSS brightness filter (software dimming)
const brightnessStyle = displayBrightness < 100
? { filter: `brightness(${displayBrightness / 100})` } as const
@@ -133,7 +168,9 @@ export function SpoolBuddyLayout() {
<>
<div
className="w-screen h-screen bg-bambu-dark text-white flex flex-col overflow-hidden"
style={brightnessStyle}
style={{ ...brightnessStyle, overscrollBehaviorX: 'none' }}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
<SpoolBuddyTopBar
selectedPrinterId={selectedPrinterId}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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-Bu1bGLqb.js"></script>
<script type="module" crossorigin src="/assets/index-DRDn9t3d.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CZLTApPU.css">
</head>
<body>