PlateSelector.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import { Layers, Check, AlertTriangle, Square, CheckSquare } from 'lucide-react';
  2. import { useTranslation } from 'react-i18next';
  3. import type { PlateSelectorProps } from './types';
  4. import { formatDuration } from '../../utils/date';
  5. import { withStreamToken } from '../../api/client';
  6. import { getBedTypeInfo } from '../../utils/bedType';
  7. /**
  8. * Plate selection grid for multi-plate 3MF files.
  9. * Shows thumbnails, names, objects, and print times for each plate.
  10. * In multi-select mode (add-to-queue), plates have checkboxes for selecting a subset.
  11. * In single-select mode (reprint/edit), only one plate can be selected at a time.
  12. */
  13. export function PlateSelector({
  14. plates,
  15. isMultiPlate,
  16. selectedPlates,
  17. onToggle,
  18. onSelectAll,
  19. onDeselectAll,
  20. multiSelect,
  21. }: PlateSelectorProps) {
  22. const { t } = useTranslation();
  23. // Only show for multi-plate files with multiple plates
  24. if (!isMultiPlate || plates.length <= 1) {
  25. return null;
  26. }
  27. const allSelected = selectedPlates.size === plates.length;
  28. return (
  29. <div className="mb-4">
  30. <div className="flex items-center gap-2 mb-2">
  31. <Layers className="w-4 h-4 text-bambu-gray" />
  32. <span className="text-sm text-bambu-gray">Select Plate{multiSelect ? 's' : ''} to Print</span>
  33. {selectedPlates.size === 0 && (
  34. <span className="text-xs text-orange-400 flex items-center gap-1">
  35. <AlertTriangle className="w-3 h-3" />
  36. Selection required
  37. </span>
  38. )}
  39. {multiSelect && onSelectAll && onDeselectAll && (
  40. <button
  41. type="button"
  42. onClick={allSelected ? onDeselectAll : onSelectAll}
  43. className={`ml-auto text-xs px-2 py-0.5 rounded-full border transition-colors ${
  44. allSelected
  45. ? 'border-bambu-green bg-bambu-green/10 text-bambu-green'
  46. : 'border-bambu-dark-tertiary text-bambu-gray hover:border-bambu-gray'
  47. }`}
  48. >
  49. {allSelected
  50. ? t('queue.deselectAll')
  51. : t('queue.selectAllPlates', { count: plates.length })}
  52. </button>
  53. )}
  54. </div>
  55. <div className="grid grid-cols-2 gap-2">
  56. {plates.map((plate) => {
  57. const isSelected = selectedPlates.has(plate.index);
  58. return (
  59. <button
  60. key={plate.index}
  61. type="button"
  62. onClick={() => onToggle(plate.index)}
  63. className={`flex items-center gap-2 p-2 rounded-lg border transition-colors text-left ${
  64. isSelected
  65. ? 'border-bambu-green bg-bambu-green/10'
  66. : 'border-bambu-dark-tertiary bg-bambu-dark hover:border-bambu-gray'
  67. }`}
  68. >
  69. {multiSelect && (
  70. isSelected
  71. ? <CheckSquare className="w-4 h-4 text-bambu-green flex-shrink-0" />
  72. : <Square className="w-4 h-4 text-bambu-gray flex-shrink-0" />
  73. )}
  74. {plate.has_thumbnail && plate.thumbnail_url != null ? (
  75. <img
  76. src={withStreamToken(plate.thumbnail_url)}
  77. alt={`Plate ${plate.index}`}
  78. className="w-10 h-10 rounded object-cover bg-bambu-dark-tertiary"
  79. />
  80. ) : (
  81. <div className="w-10 h-10 rounded bg-bambu-dark-tertiary flex items-center justify-center">
  82. <Layers className="w-5 h-5 text-bambu-gray" />
  83. </div>
  84. )}
  85. <div className="min-w-0 flex-1">
  86. <p className="text-sm text-white font-medium truncate">
  87. {plate.name || `Plate ${plate.index}`}
  88. </p>
  89. <p className="text-xs text-bambu-gray truncate">
  90. {plate.objects.length > 0
  91. ? plate.objects.slice(0, 3).join(', ') +
  92. (plate.objects.length > 3 ? '...' : '')
  93. : `${plate.filaments.length} filament${plate.filaments.length !== 1 ? 's' : ''}`}
  94. {plate.print_time_seconds != null ? ` • ${formatDuration(plate.print_time_seconds)}` : ''}
  95. </p>
  96. {(() => {
  97. // Per-plate bed-type badge so multi-plate prints make the
  98. // required plate explicit at scheduling time (#1281).
  99. const bed = getBedTypeInfo(plate.bed_type);
  100. if (!bed) return null;
  101. return (
  102. <p className="text-xs text-bambu-gray flex items-center gap-1 mt-0.5 truncate" title={bed.label}>
  103. <img src={bed.icon} alt="" className="w-3.5 h-3.5 object-contain flex-shrink-0" />
  104. <span className="truncate">{bed.label}</span>
  105. </p>
  106. );
  107. })()}
  108. </div>
  109. {!multiSelect && isSelected && (
  110. <Check className="w-4 h-4 text-bambu-green flex-shrink-0" />
  111. )}
  112. </button>
  113. );
  114. })}
  115. </div>
  116. </div>
  117. );
  118. }