Browse Source

Spoolbuddy - fixed horizontal swiping

maziggy 1 month ago
parent
commit
329223502a

+ 40 - 0
frontend/src/__tests__/components/spoolbuddy/SpoolBuddyQuickMenu.test.tsx

@@ -212,6 +212,46 @@ describe('SpoolBuddyQuickMenu', () => {
     });
   });
 
+  it('shows confirmation when toggling printer plug', async () => {
+    (api.getPrinters as ReturnType<typeof vi.fn>).mockResolvedValue([mockPrinter]);
+    (api.getSmartPlugs as ReturnType<typeof vi.fn>).mockResolvedValue([mockSmartPlug]);
+
+    render(<SpoolBuddyQuickMenu {...defaultProps} />);
+
+    await waitFor(() => {
+      expect(screen.getByText('Test P1S')).toBeInTheDocument();
+    });
+
+    fireEvent.click(screen.getByText('Test P1S'));
+
+    await waitFor(() => {
+      expect(screen.getByText(/Turn on Test P1S/)).toBeInTheDocument();
+    });
+  });
+
+  it('toggles plug after confirming', async () => {
+    (api.getPrinters as ReturnType<typeof vi.fn>).mockResolvedValue([mockPrinter]);
+    (api.getSmartPlugs as ReturnType<typeof vi.fn>).mockResolvedValue([mockSmartPlug]);
+
+    render(<SpoolBuddyQuickMenu {...defaultProps} />);
+
+    await waitFor(() => {
+      expect(screen.getByText('Test P1S')).toBeInTheDocument();
+    });
+
+    fireEvent.click(screen.getByText('Test P1S'));
+
+    await waitFor(() => {
+      expect(screen.getByRole('button', { name: 'Turn On' })).toBeInTheDocument();
+    });
+
+    fireEvent.click(screen.getByRole('button', { name: 'Turn On' }));
+
+    await waitFor(() => {
+      expect(api.controlSmartPlug).toHaveBeenCalledWith(10, 'toggle');
+    });
+  });
+
   it('disables system buttons when device offline', () => {
     render(<SpoolBuddyQuickMenu {...defaultProps} deviceOnline={false} />);
 

+ 49 - 46
frontend/src/components/spoolbuddy/SpoolBuddyQuickMenu.tsx

@@ -1,7 +1,7 @@
 import { useState, useEffect, useCallback } from 'react';
 import { useQuery } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
-import { Power, PowerOff, RotateCw, Monitor, ChevronDown, Loader2 } from 'lucide-react';
+import { PowerOff, RotateCw, Monitor, ChevronDown, Loader2 } from 'lucide-react';
 import { api, spoolbuddyApi, type Printer, type SmartPlug, type SmartPlugStatus } from '../../api/client';
 
 interface SpoolBuddyQuickMenuProps {
@@ -13,6 +13,10 @@ interface SpoolBuddyQuickMenuProps {
 
 type SystemCommand = 'reboot' | 'shutdown' | 'restart_daemon' | 'restart_browser';
 
+type PendingConfirm =
+  | { type: 'system'; command: SystemCommand }
+  | { type: 'plug'; plug: SmartPlug; printer: Printer; currentState: string | null };
+
 interface PlugState {
   plug: SmartPlug;
   printer: Printer;
@@ -22,7 +26,7 @@ interface PlugState {
 
 export function SpoolBuddyQuickMenu({ isOpen, onClose, deviceId, deviceOnline }: SpoolBuddyQuickMenuProps) {
   const { t } = useTranslation();
-  const [confirmAction, setConfirmAction] = useState<SystemCommand | null>(null);
+  const [pendingConfirm, setPendingConfirm] = useState<PendingConfirm | null>(null);
   const [commandBusy, setCommandBusy] = useState(false);
   const [plugStates, setPlugStates] = useState<Map<number, { loading: boolean; state: string | null }>>(new Map());
 
@@ -85,7 +89,7 @@ export function SpoolBuddyQuickMenu({ isOpen, onClose, deviceId, deviceOnline }:
   // Clear state when menu closes
   useEffect(() => {
     if (!isOpen) {
-      setConfirmAction(null);
+      setPendingConfirm(null);
       setCommandBusy(false);
     }
   }, [isOpen]);
@@ -129,11 +133,14 @@ export function SpoolBuddyQuickMenu({ isOpen, onClose, deviceId, deviceOnline }:
   }, [deviceId, onClose]);
 
   const executeConfirmed = useCallback(() => {
-    if (confirmAction) {
-      handleSystemCommand(confirmAction);
-      setConfirmAction(null);
+    if (!pendingConfirm) return;
+    if (pendingConfirm.type === 'system') {
+      handleSystemCommand(pendingConfirm.command);
+    } else {
+      handleTogglePlug(pendingConfirm.plug);
     }
-  }, [confirmAction, handleSystemCommand]);
+    setPendingConfirm(null);
+  }, [pendingConfirm, handleSystemCommand, handleTogglePlug]);
 
   if (!isOpen) return null;
 
@@ -158,37 +165,23 @@ export function SpoolBuddyQuickMenu({ isOpen, onClose, deviceId, deviceOnline }:
               <h3 className="text-xs font-semibold text-zinc-500 uppercase tracking-wide mb-2">
                 {t('spoolbuddy.quickMenu.printerPower', 'Printer Power')}
               </h3>
-              <div className="space-y-2">
+              <div className="space-y-1">
                 {printerPlugs.map(({ plug, printer, loading }) => {
                   const state = plugStates.get(plug.id);
                   const on = isPlugOn(state?.state ?? null);
                   return (
                     <button
                       key={plug.id}
-                      onClick={() => handleTogglePlug(plug)}
+                      onClick={() => setPendingConfirm({ type: 'plug', plug, printer, currentState: state?.state ?? null })}
                       disabled={loading}
-                      className="w-full flex items-center gap-3 p-3 rounded-xl bg-zinc-800/60 hover:bg-zinc-700/60 transition-colors min-h-[48px]"
+                      className="w-full flex items-center gap-2 px-3 py-1.5 rounded-lg bg-zinc-800/60 hover:bg-zinc-700/60 transition-colors min-h-[36px]"
                     >
-                      <div className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${
-                        on ? 'bg-green-500/20 text-green-400' : 'bg-zinc-700 text-zinc-500'
-                      }`}>
-                        {loading ? (
-                          <Loader2 className="w-4 h-4 animate-spin" />
-                        ) : on ? (
-                          <Power className="w-4 h-4" />
-                        ) : (
-                          <PowerOff className="w-4 h-4" />
-                        )}
-                      </div>
-                      <div className="flex-1 text-left">
-                        <div className="text-sm font-medium text-zinc-200">{printer.name}</div>
-                        <div className="text-xs text-zinc-500">{plug.name}</div>
-                      </div>
-                      <div className={`text-xs font-medium px-2 py-0.5 rounded-full ${
-                        on ? 'bg-green-500/20 text-green-400' : 'bg-zinc-700 text-zinc-500'
-                      }`}>
+                      <div className={`w-2 h-2 rounded-full shrink-0 ${on ? 'bg-green-500' : 'bg-zinc-600'}`} />
+                      {loading && <Loader2 className="w-3 h-3 animate-spin text-zinc-400 shrink-0" />}
+                      <span className="flex-1 text-sm text-zinc-200 text-left truncate">{printer.name}</span>
+                      <span className={`text-xs font-medium ${on ? 'text-green-400' : 'text-zinc-500'}`}>
                         {state?.state ?? '—'}
-                      </div>
+                      </span>
                     </button>
                   );
                 })}
@@ -205,26 +198,26 @@ export function SpoolBuddyQuickMenu({ isOpen, onClose, deviceId, deviceOnline }:
               <SystemButton
                 icon={<RotateCw className="w-4 h-4" />}
                 label={t('spoolbuddy.quickMenu.restartDaemon', 'Restart Daemon')}
-                onClick={() => setConfirmAction('restart_daemon')}
+                onClick={() => setPendingConfirm({ type: 'system', command: 'restart_daemon' })}
                 disabled={!deviceId || !deviceOnline || commandBusy}
               />
               <SystemButton
                 icon={<Monitor className="w-4 h-4" />}
                 label={t('spoolbuddy.quickMenu.restartBrowser', 'Restart Browser')}
-                onClick={() => setConfirmAction('restart_browser')}
+                onClick={() => setPendingConfirm({ type: 'system', command: 'restart_browser' })}
                 disabled={!deviceId || !deviceOnline || commandBusy}
               />
               <SystemButton
                 icon={<RotateCw className="w-4 h-4" />}
                 label={t('spoolbuddy.quickMenu.reboot', 'Reboot')}
-                onClick={() => setConfirmAction('reboot')}
+                onClick={() => setPendingConfirm({ type: 'system', command: 'reboot' })}
                 disabled={!deviceId || !deviceOnline || commandBusy}
                 variant="warning"
               />
               <SystemButton
                 icon={<PowerOff className="w-4 h-4" />}
                 label={t('spoolbuddy.quickMenu.shutdown', 'Shutdown')}
-                onClick={() => setConfirmAction('shutdown')}
+                onClick={() => setPendingConfirm({ type: 'system', command: 'shutdown' })}
                 disabled={!deviceId || !deviceOnline || commandBusy}
                 variant="danger"
               />
@@ -242,24 +235,28 @@ export function SpoolBuddyQuickMenu({ isOpen, onClose, deviceId, deviceOnline }:
       </div>
 
       {/* Confirmation Dialog */}
-      {confirmAction && (
+      {pendingConfirm && (
         <div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60">
           <div className="bg-zinc-800 rounded-2xl p-5 mx-4 max-w-sm w-full border border-zinc-700">
             <h3 className="text-lg font-semibold text-zinc-100 mb-2">
               {t('spoolbuddy.quickMenu.confirmTitle', 'Confirm')}
             </h3>
             <p className="text-sm text-zinc-400 mb-5">
-              {confirmAction === 'shutdown'
-                ? t('spoolbuddy.quickMenu.confirmShutdown', 'Are you sure you want to shut down the SpoolBuddy? You will need physical access to turn it back on.')
-                : confirmAction === 'reboot'
-                  ? t('spoolbuddy.quickMenu.confirmReboot', 'Are you sure you want to reboot the SpoolBuddy?')
-                  : confirmAction === 'restart_daemon'
-                    ? t('spoolbuddy.quickMenu.confirmRestartDaemon', 'Restart the SpoolBuddy daemon? NFC and scale will be temporarily unavailable.')
-                    : t('spoolbuddy.quickMenu.confirmRestartBrowser', 'Restart the kiosk browser? The display will briefly go blank.')}
+              {pendingConfirm.type === 'plug'
+                ? (isPlugOn(pendingConfirm.currentState)
+                    ? t('spoolbuddy.quickMenu.confirmPlugOff', 'Turn off {{name}}?', { name: pendingConfirm.printer.name })
+                    : t('spoolbuddy.quickMenu.confirmPlugOn', 'Turn on {{name}}?', { name: pendingConfirm.printer.name }))
+                : pendingConfirm.command === 'shutdown'
+                  ? t('spoolbuddy.quickMenu.confirmShutdown', 'Are you sure you want to shut down the SpoolBuddy? You will need physical access to turn it back on.')
+                  : pendingConfirm.command === 'reboot'
+                    ? t('spoolbuddy.quickMenu.confirmReboot', 'Are you sure you want to reboot the SpoolBuddy?')
+                    : pendingConfirm.command === 'restart_daemon'
+                      ? t('spoolbuddy.quickMenu.confirmRestartDaemon', 'Restart the SpoolBuddy daemon? NFC and scale will be temporarily unavailable.')
+                      : t('spoolbuddy.quickMenu.confirmRestartBrowser', 'Restart the kiosk browser? The display will briefly go blank.')}
             </p>
             <div className="flex gap-3">
               <button
-                onClick={() => setConfirmAction(null)}
+                onClick={() => setPendingConfirm(null)}
                 className="flex-1 px-4 py-2.5 rounded-lg text-sm font-medium bg-zinc-700 text-zinc-300 hover:bg-zinc-600 transition-colors min-h-[44px]"
               >
                 {t('common.cancel', 'Cancel')}
@@ -268,13 +265,19 @@ export function SpoolBuddyQuickMenu({ isOpen, onClose, deviceId, deviceOnline }:
                 onClick={executeConfirmed}
                 disabled={commandBusy}
                 className={`flex-1 px-4 py-2.5 rounded-lg text-sm font-medium text-white transition-colors min-h-[44px] ${
-                  confirmAction === 'shutdown' ? 'bg-red-600 hover:bg-red-700' :
-                  confirmAction === 'reboot' ? 'bg-amber-600 hover:bg-amber-700' :
-                  'bg-blue-600 hover:bg-blue-700'
+                  pendingConfirm.type === 'plug'
+                    ? (isPlugOn(pendingConfirm.currentState) ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700')
+                    : pendingConfirm.command === 'shutdown' ? 'bg-red-600 hover:bg-red-700'
+                    : pendingConfirm.command === 'reboot' ? 'bg-amber-600 hover:bg-amber-700'
+                    : 'bg-blue-600 hover:bg-blue-700'
                 } disabled:opacity-50`}
               >
                 {commandBusy ? <Loader2 className="w-4 h-4 animate-spin mx-auto" /> :
-                  t('spoolbuddy.quickMenu.confirm', 'Confirm')}
+                  pendingConfirm.type === 'plug'
+                    ? (isPlugOn(pendingConfirm.currentState)
+                        ? t('spoolbuddy.quickMenu.turnOff', 'Turn Off')
+                        : t('spoolbuddy.quickMenu.turnOn', 'Turn On'))
+                    : t('spoolbuddy.quickMenu.confirm', 'Confirm')}
               </button>
             </div>
           </div>

+ 4 - 0
frontend/src/i18n/locales/de.ts

@@ -4747,6 +4747,10 @@ export default {
       confirmRestartDaemon: 'SpoolBuddy-Daemon neustarten? NFC und Waage sind vorübergehend nicht verfügbar.',
       confirmRestartBrowser: 'Kiosk-Browser neustarten? Das Display wird kurz schwarz.',
       confirm: 'Bestätigen',
+      confirmPlugOn: '{{name}} einschalten?',
+      confirmPlugOff: '{{name}} ausschalten?',
+      turnOn: 'Einschalten',
+      turnOff: 'Ausschalten',
     },
   },
 

+ 4 - 0
frontend/src/i18n/locales/en.ts

@@ -4754,6 +4754,10 @@ export default {
       confirmRestartDaemon: 'Restart the SpoolBuddy daemon? NFC and scale will be temporarily unavailable.',
       confirmRestartBrowser: 'Restart the kiosk browser? The display will briefly go blank.',
       confirm: 'Confirm',
+      confirmPlugOn: 'Turn on {{name}}?',
+      confirmPlugOff: 'Turn off {{name}}?',
+      turnOn: 'Turn On',
+      turnOff: 'Turn Off',
     },
   },
 

+ 4 - 0
frontend/src/i18n/locales/fr.ts

@@ -4726,6 +4726,10 @@ export default {
       confirmRestartDaemon: 'Redémarrer le daemon SpoolBuddy ? Le NFC et la balance seront temporairement indisponibles.',
       confirmRestartBrowser: 'Redémarrer le navigateur kiosque ? L\'écran sera brièvement noir.',
       confirm: 'Confirmer',
+      confirmPlugOn: 'Allumer {{name}} ?',
+      confirmPlugOff: 'Éteindre {{name}} ?',
+      turnOn: 'Allumer',
+      turnOff: 'Éteindre',
     },
   },
 

+ 4 - 0
frontend/src/i18n/locales/it.ts

@@ -4725,6 +4725,10 @@ export default {
       confirmRestartDaemon: 'Riavviare il daemon SpoolBuddy? NFC e bilancia saranno temporaneamente non disponibili.',
       confirmRestartBrowser: 'Riavviare il browser kiosk? Lo schermo diventerà brevemente nero.',
       confirm: 'Conferma',
+      confirmPlugOn: 'Accendere {{name}}?',
+      confirmPlugOff: 'Spegnere {{name}}?',
+      turnOn: 'Accendi',
+      turnOff: 'Spegni',
     },
   },
 

+ 4 - 0
frontend/src/i18n/locales/ja.ts

@@ -4738,6 +4738,10 @@ export default {
       confirmRestartDaemon: 'SpoolBuddyデーモンを再起動しますか?NFCとスケールが一時的に使用できなくなります。',
       confirmRestartBrowser: 'キオスクブラウザを再起動しますか?画面が一時的に暗くなります。',
       confirm: '確認',
+      confirmPlugOn: '{{name}}をオンにしますか?',
+      confirmPlugOff: '{{name}}をオフにしますか?',
+      turnOn: 'オン',
+      turnOff: 'オフ',
     },
   },
 

+ 4 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -4725,6 +4725,10 @@ export default {
       confirmRestartDaemon: 'Reiniciar o daemon do SpoolBuddy? NFC e balança ficarão temporariamente indisponíveis.',
       confirmRestartBrowser: 'Reiniciar o navegador kiosk? A tela ficará brevemente preta.',
       confirm: 'Confirmar',
+      confirmPlugOn: 'Ligar {{name}}?',
+      confirmPlugOff: 'Desligar {{name}}?',
+      turnOn: 'Ligar',
+      turnOff: 'Desligar',
     },
   },
 

+ 4 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -4724,6 +4724,10 @@ export default {
       confirmRestartDaemon: '重启SpoolBuddy守护进程?NFC和秤将暂时不可用。',
       confirmRestartBrowser: '重启kiosk浏览器?屏幕将短暂变黑。',
       confirm: '确认',
+      confirmPlugOn: '开启 {{name}}?',
+      confirmPlugOff: '关闭 {{name}}?',
+      turnOn: '开启',
+      turnOff: '关闭',
     },
   },
 

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-B6Ntre74.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-Cb2GmT4j.js"></script>
+    <script type="module" crossorigin src="/assets/index-B6Ntre74.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-CA19cW_f.css">
   </head>
   <body>

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