CameraTileControls.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { useMutation, useQueryClient } from '@tanstack/react-query';
  4. import { Pause, Play, Square } from 'lucide-react';
  5. import { api, type PrinterStatus } from '../api/client';
  6. import { useAuth } from '../contexts/AuthContext';
  7. import { useToast } from '../contexts/ToastContext';
  8. import { ConfirmModal } from './ConfirmModal';
  9. import { ChamberLight } from './icons/ChamberLight';
  10. interface CameraTileControlsProps {
  11. printerId: number;
  12. printerName: string;
  13. connected: boolean;
  14. printerState: string | null;
  15. chamberLight: boolean | null;
  16. }
  17. const BUTTON =
  18. 'flex h-7 w-7 items-center justify-center rounded-md bg-black/70 text-white backdrop-blur-sm transition-colors hover:bg-black/90 focus:outline-none focus:ring-2 focus:ring-bambu-green disabled:cursor-not-allowed disabled:opacity-50';
  19. /**
  20. * Pause/resume, stop and the chamber light on a camera wall tile (#3479).
  21. *
  22. * The same actions as the printer card's, behind the same printers:control
  23. * permission and the same confirmations: a user without the permission gets
  24. * no controls at all. Rendered beside the tile, not inside it, because the
  25. * tile itself is a button (it opens the camera).
  26. */
  27. export function CameraTileControls({
  28. printerId,
  29. printerName,
  30. connected,
  31. printerState,
  32. chamberLight,
  33. }: CameraTileControlsProps) {
  34. const { t } = useTranslation();
  35. const { hasPermission } = useAuth();
  36. const { showToast } = useToast();
  37. const queryClient = useQueryClient();
  38. const [confirm, setConfirm] = useState<'pause' | 'resume' | 'stop' | null>(null);
  39. const refreshStatus = () => queryClient.invalidateQueries({ queryKey: ['printerStatus', printerId] });
  40. const stop = useMutation({
  41. mutationFn: () => api.stopPrint(printerId),
  42. onSuccess: () => {
  43. showToast(t('printers.toast.printStopped'));
  44. refreshStatus();
  45. },
  46. onError: (error: Error) => showToast(error.message || t('printers.toast.failedToStopPrint'), 'error'),
  47. });
  48. const pause = useMutation({
  49. mutationFn: () => api.pausePrint(printerId),
  50. onSuccess: () => {
  51. showToast(t('printers.toast.printPaused'));
  52. refreshStatus();
  53. },
  54. onError: (error: Error) => showToast(error.message || t('printers.toast.failedToPausePrint'), 'error'),
  55. });
  56. const resume = useMutation({
  57. mutationFn: () => api.resumePrint(printerId),
  58. onSuccess: () => {
  59. showToast(t('printers.toast.printResumed'));
  60. refreshStatus();
  61. },
  62. onError: (error: Error) => showToast(error.message || t('printers.toast.failedToResumePrint'), 'error'),
  63. });
  64. // Optimistic, as on the card: the light's state comes back over the
  65. // printer's next status push, which can take a few seconds.
  66. const light = useMutation({
  67. mutationFn: (on: boolean) => api.setChamberLight(printerId, on),
  68. onMutate: async (on) => {
  69. await queryClient.cancelQueries({ queryKey: ['printerStatus', printerId] });
  70. const previous = queryClient.getQueryData<PrinterStatus>(['printerStatus', printerId]);
  71. if (previous) {
  72. queryClient.setQueryData<PrinterStatus>(['printerStatus', printerId], { ...previous, chamber_light: on });
  73. }
  74. return { previous };
  75. },
  76. onError: (error: Error, _on, context) => {
  77. if (context?.previous) queryClient.setQueryData(['printerStatus', printerId], context.previous);
  78. showToast(error.message || t('printers.toast.failedToControlChamberLight'), 'error');
  79. },
  80. });
  81. if (!hasPermission('printers:control') || !connected) return null;
  82. const isPaused = printerState === 'PAUSE';
  83. const isPrinting = printerState === 'RUNNING' || isPaused;
  84. const busy = stop.isPending || pause.isPending || resume.isPending;
  85. const lightOn = chamberLight ?? false;
  86. return (
  87. <>
  88. <div className="flex gap-1">
  89. {isPrinting && (
  90. <>
  91. <button
  92. type="button"
  93. className={BUTTON}
  94. disabled={busy}
  95. onClick={() => setConfirm(isPaused ? 'resume' : 'pause')}
  96. title={isPaused ? t('printers.resume') : t('printers.pause')}
  97. aria-label={isPaused ? t('printers.resume') : t('printers.pause')}
  98. >
  99. {isPaused ? <Play className="h-3.5 w-3.5" /> : <Pause className="h-3.5 w-3.5" />}
  100. </button>
  101. <button
  102. type="button"
  103. className={`${BUTTON} hover:text-red-400`}
  104. disabled={busy}
  105. onClick={() => setConfirm('stop')}
  106. title={t('printers.stop')}
  107. aria-label={t('printers.stop')}
  108. >
  109. <Square className="h-3.5 w-3.5" />
  110. </button>
  111. </>
  112. )}
  113. <button
  114. type="button"
  115. className={BUTTON}
  116. disabled={light.isPending}
  117. aria-pressed={lightOn}
  118. onClick={() => light.mutate(!lightOn)}
  119. title={lightOn ? t('printers.chamberLightOff') : t('printers.chamberLightOn')}
  120. aria-label={lightOn ? t('printers.chamberLightOff') : t('printers.chamberLightOn')}
  121. >
  122. <ChamberLight on={lightOn} className="h-4 w-4" />
  123. </button>
  124. </div>
  125. {confirm === 'stop' && (
  126. <ConfirmModal
  127. title={t('printers.confirm.stopTitle')}
  128. message={t('printers.confirm.stopMessage', { name: printerName })}
  129. confirmText={t('printers.confirm.stopButton')}
  130. variant="danger"
  131. onConfirm={() => {
  132. stop.mutate();
  133. setConfirm(null);
  134. }}
  135. onCancel={() => setConfirm(null)}
  136. />
  137. )}
  138. {confirm === 'pause' && (
  139. <ConfirmModal
  140. title={t('printers.confirm.pauseTitle')}
  141. message={t('printers.confirm.pauseMessage', { name: printerName })}
  142. confirmText={t('printers.confirm.pauseButton')}
  143. onConfirm={() => {
  144. pause.mutate();
  145. setConfirm(null);
  146. }}
  147. onCancel={() => setConfirm(null)}
  148. />
  149. )}
  150. {confirm === 'resume' && (
  151. <ConfirmModal
  152. title={t('printers.confirm.resumeTitle')}
  153. message={t('printers.confirm.resumeMessage', { name: printerName })}
  154. confirmText={t('printers.confirm.resumeButton')}
  155. onConfirm={() => {
  156. resume.mutate();
  157. setConfirm(null);
  158. }}
  159. onCancel={() => setConfirm(null)}
  160. />
  161. )}
  162. </>
  163. );
  164. }