|
|
@@ -59,7 +59,7 @@ import {
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
import { api, discoveryApi, firmwareApi, withStreamToken } from '../api/client';
|
|
|
import { formatDateOnly, formatETA, formatDuration, parseUTCDate } from '../utils/date';
|
|
|
-import type { Printer, PrinterCreate, AMSUnit, DiscoveredPrinter, FirmwareUpdateInfo, FirmwareUploadStatus, LinkedSpoolInfo, SpoolAssignment, HMSError } from '../api/client';
|
|
|
+import type { Printer, PrinterCreate, PrinterStatus, AMSUnit, DiscoveredPrinter, FirmwareUpdateInfo, FirmwareUploadStatus, LinkedSpoolInfo, SpoolAssignment, HMSError } from '../api/client';
|
|
|
import { Card, CardContent } from '../components/Card';
|
|
|
import { Button } from '../components/Button';
|
|
|
import { ConfirmModal } from '../components/ConfirmModal';
|
|
|
@@ -76,6 +76,7 @@ import { AssignSpoolModal } from '../components/AssignSpoolModal';
|
|
|
import { ConfigureAmsSlotModal } from '../components/ConfigureAmsSlotModal';
|
|
|
import { useToast } from '../contexts/ToastContext';
|
|
|
import { ChamberLight } from '../components/icons/ChamberLight';
|
|
|
+import { PlateClearedIcon } from '../components/icons/PlateClearedIcon';
|
|
|
import { SkipObjectsModal, SkipObjectsIcon } from '../components/SkipObjectsModal';
|
|
|
import { FileUploadModal } from '../components/FileUploadModal';
|
|
|
import { PrintModal } from '../components/PrintModal';
|
|
|
@@ -1644,6 +1645,33 @@ function PrinterCard({
|
|
|
enabled: status?.connected && status?.state !== 'RUNNING',
|
|
|
});
|
|
|
const lastPrint = lastPrints?.[0];
|
|
|
+ const isPrintingOrPaused = status?.state === 'RUNNING' || status?.state === 'PAUSE';
|
|
|
+ const needsPlateClear = requirePlateClear && status?.awaiting_plate_clear === true;
|
|
|
+ const showClearPlateButton = status?.connected && needsPlateClear && !isPrintingOrPaused;
|
|
|
+ const plateStatus = (() => {
|
|
|
+ if (!requirePlateClear || !status?.connected) return null;
|
|
|
+ if (isPrintingOrPaused) {
|
|
|
+ return {
|
|
|
+ label: t('printers.plateStatus.inUse'),
|
|
|
+ className: 'bg-blue-500/20 text-blue-400',
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (status.awaiting_plate_clear) {
|
|
|
+ return {
|
|
|
+ label: t('printers.plateStatus.notCleared'),
|
|
|
+ className: 'bg-yellow-500/20 text-yellow-400',
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ label: t('printers.plateStatus.cleared'),
|
|
|
+ className: 'bg-status-ok/20 text-status-ok',
|
|
|
+ };
|
|
|
+ })();
|
|
|
+ const plateStatusPill = plateStatus ? (
|
|
|
+ <span className={`inline-flex flex-shrink-0 items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${plateStatus.className}`}>
|
|
|
+ {plateStatus.label}
|
|
|
+ </span>
|
|
|
+ ) : null;
|
|
|
|
|
|
// Determine if this card should be hidden (use cached connected state to prevent flicker)
|
|
|
const shouldHide = hideIfDisconnected && isConnected === false;
|
|
|
@@ -1762,6 +1790,19 @@ function PrinterCard({
|
|
|
onError: (error: Error) => showToast(error.message || t('printers.toast.failedToResumePrint'), 'error'),
|
|
|
});
|
|
|
|
|
|
+ const clearPlateMutation = useMutation({
|
|
|
+ mutationFn: () => api.clearPlate(printer.id),
|
|
|
+ onSuccess: () => {
|
|
|
+ showToast(t('queue.clearPlateSuccess'));
|
|
|
+ queryClient.setQueryData(['printerStatus', printer.id], (old: PrinterStatus | undefined) =>
|
|
|
+ old ? { ...old, awaiting_plate_clear: false } : old
|
|
|
+ );
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['printerStatus', printer.id] });
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['queue', printer.id] });
|
|
|
+ },
|
|
|
+ onError: (error: Error) => showToast(error.message || t('printers.toast.failedToSendCommand'), 'error'),
|
|
|
+ });
|
|
|
+
|
|
|
// Chamber light mutation with optimistic update
|
|
|
const chamberLightMutation = useMutation({
|
|
|
mutationFn: (on: boolean) => api.setChamberLight(printer.id, on),
|
|
|
@@ -2604,10 +2645,34 @@ function PrinterCard({
|
|
|
style={{ width: `${status.progress || 0}%` }}
|
|
|
/>
|
|
|
</div>
|
|
|
- <span className="text-xs text-white">{Math.round(status.progress || 0)}%</span>
|
|
|
+ <div className="flex flex-shrink-0 items-center gap-1.5">
|
|
|
+ <span className="text-xs text-white">{Math.round(status.progress || 0)}%</span>
|
|
|
+ {plateStatusPill}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
) : (
|
|
|
- <p className="text-xs text-bambu-gray">{getStatusDisplay(status.state, status.stg_cur_name)}</p>
|
|
|
+ <div className="flex items-center justify-between gap-2">
|
|
|
+ <div className="min-w-0 flex-1 flex items-center gap-1.5">
|
|
|
+ <p className="min-w-0 truncate text-xs text-bambu-gray">{getStatusDisplay(status.state, status.stg_cur_name)}</p>
|
|
|
+ {plateStatusPill}
|
|
|
+ </div>
|
|
|
+ {showClearPlateButton && (
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={() => clearPlateMutation.mutate()}
|
|
|
+ disabled={clearPlateMutation.isPending || !hasPermission('printers:clear_plate')}
|
|
|
+ aria-label={t('printers.plateStatus.markCleared')}
|
|
|
+ className="inline-flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-yellow-500/20 border border-yellow-400/40 text-yellow-400 hover:bg-yellow-500/30 transition-colors disabled:opacity-50"
|
|
|
+ title={!hasPermission('printers:clear_plate') ? t('printers.permission.noControl') : t('printers.plateStatus.markCleared')}
|
|
|
+ >
|
|
|
+ {clearPlateMutation.isPending ? (
|
|
|
+ <Loader2 className="w-3 h-3 animate-spin" />
|
|
|
+ ) : (
|
|
|
+ <PlateClearedIcon className="w-3 h-3" />
|
|
|
+ )}
|
|
|
+ </button>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
)}
|
|
|
</div>
|
|
|
) : (
|
|
|
@@ -2652,7 +2717,10 @@ function PrinterCard({
|
|
|
<div className="flex-1 min-w-0">
|
|
|
{status.current_print && (status.state === 'RUNNING' || status.state === 'PAUSE') ? (
|
|
|
<>
|
|
|
- <p className="text-sm text-bambu-gray mb-1">{getStatusDisplay(status.state, status.stg_cur_name)}</p>
|
|
|
+ <div className="mb-1 flex items-center gap-2">
|
|
|
+ <p className="text-sm text-bambu-gray">{getStatusDisplay(status.state, status.stg_cur_name)}</p>
|
|
|
+ {plateStatusPill}
|
|
|
+ </div>
|
|
|
<p className="text-white text-sm mb-2 truncate">
|
|
|
{formatPrintName(status.subtask_name || status.current_print || null, status.gcode_file, t)}
|
|
|
</p>
|
|
|
@@ -2694,9 +2762,12 @@ function PrinterCard({
|
|
|
) : (
|
|
|
<>
|
|
|
<p className="text-sm text-bambu-gray mb-1">{t('printers.sort.status')}</p>
|
|
|
- <p className="text-white text-sm mb-2">
|
|
|
- {getStatusDisplay(status.state, status.stg_cur_name)}
|
|
|
- </p>
|
|
|
+ <div className="mb-2 flex items-center gap-2">
|
|
|
+ <p className="text-white text-sm">
|
|
|
+ {getStatusDisplay(status.state, status.stg_cur_name)}
|
|
|
+ </p>
|
|
|
+ {plateStatusPill}
|
|
|
+ </div>
|
|
|
<div className="flex items-center justify-between text-sm">
|
|
|
<div className="flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3">
|
|
|
<div className="bg-bambu-dark-tertiary h-2 rounded-full" />
|
|
|
@@ -2818,6 +2889,23 @@ function PrinterCard({
|
|
|
);
|
|
|
})()}
|
|
|
|
|
|
+ {viewMode === 'expanded' && showClearPlateButton && (
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={() => clearPlateMutation.mutate()}
|
|
|
+ disabled={clearPlateMutation.isPending || !hasPermission('printers:clear_plate')}
|
|
|
+ className="mt-2 w-full inline-flex items-center justify-center gap-2 px-3 py-1.5 rounded-lg bg-yellow-500/20 border border-yellow-400/40 text-yellow-400 hover:bg-yellow-500/30 transition-colors text-xs font-medium disabled:opacity-50"
|
|
|
+ title={!hasPermission('printers:clear_plate') ? t('printers.permission.noControl') : t('printers.plateStatus.markCleared')}
|
|
|
+ >
|
|
|
+ {clearPlateMutation.isPending ? (
|
|
|
+ <Loader2 className="w-3 h-3 animate-spin" />
|
|
|
+ ) : (
|
|
|
+ <PlateClearedIcon className="w-4 h-4" />
|
|
|
+ )}
|
|
|
+ {t('printers.plateStatus.markCleared')}
|
|
|
+ </button>
|
|
|
+ )}
|
|
|
+
|
|
|
{/* Controls - Fans + Print Buttons */}
|
|
|
{viewMode === 'expanded' && (() => {
|
|
|
// Determine print state for control buttons
|
|
|
@@ -2841,9 +2929,9 @@ function PrinterCard({
|
|
|
<div className="flex-1 h-px bg-bambu-dark-tertiary/30" />
|
|
|
</div>
|
|
|
|
|
|
- <div className="flex items-center justify-between gap-2 max-[550px]:items-start">
|
|
|
+ <div className="flex flex-wrap items-start justify-between gap-x-2 gap-y-2">
|
|
|
{/* Left: Fan Status - always visible, dynamic coloring */}
|
|
|
- <div className="flex items-center gap-2 min-w-0 max-[550px]:flex-wrap max-[550px]:items-start max-[550px]:gap-1.5">
|
|
|
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1.5 min-w-0">
|
|
|
{/* Part Cooling Fan */}
|
|
|
<div
|
|
|
className={`flex items-center gap-1 px-1.5 py-1 rounded ${partFan && partFan > 0 ? 'bg-cyan-500/10' : 'bg-bambu-dark'}`}
|