PrintOptions.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { useState } from 'react';
  2. import { Settings, ChevronDown, ChevronUp, Flame } from 'lucide-react';
  3. import { useTranslation } from 'react-i18next';
  4. import type { PrintOptionsProps, PrintOptions as PrintOptionsType, PreheatOverride } from './types';
  5. type OptionConfig = {
  6. key: keyof PrintOptionsType;
  7. label: string;
  8. desc: string;
  9. dualNozzleOnly?: boolean;
  10. };
  11. /**
  12. * Print options toggle panel with collapsible UI.
  13. * Shows bed levelling, flow/vibration calibration, layer inspection, timelapse,
  14. * and (for dual-nozzle printers only) nozzle offset calibration.
  15. */
  16. export function PrintOptionsPanel({
  17. options,
  18. onChange,
  19. defaultExpanded = false,
  20. showDualNozzleOptions = false,
  21. }: PrintOptionsProps) {
  22. const { t } = useTranslation();
  23. const [isExpanded, setIsExpanded] = useState(defaultExpanded);
  24. // Labels/descriptions reuse the settings.default* namespace — identical strings,
  25. // already translated across all locales. Only nozzle_offset_cali is new (#1682).
  26. const printOptionsConfig: OptionConfig[] = [
  27. { key: 'bed_levelling', label: t('settings.defaultBedLevelling'), desc: t('settings.defaultBedLevellingDesc') },
  28. { key: 'flow_cali', label: t('settings.defaultFlowCali'), desc: t('settings.defaultFlowCaliDesc') },
  29. { key: 'vibration_cali', label: t('settings.defaultVibrationCali'), desc: t('settings.defaultVibrationCaliDesc') },
  30. { key: 'layer_inspect', label: t('settings.defaultLayerInspect'), desc: t('settings.defaultLayerInspectDesc') },
  31. { key: 'timelapse', label: t('settings.defaultTimelapse'), desc: t('settings.defaultTimelapseDesc') },
  32. { key: 'nozzle_offset_cali', label: t('settings.defaultNozzleOffsetCali'), desc: t('settings.defaultNozzleOffsetCaliDesc'), dualNozzleOnly: true },
  33. ];
  34. const visibleOptions = printOptionsConfig.filter(o => !o.dualNozzleOnly || showDualNozzleOptions);
  35. const handleToggle = (key: keyof PrintOptionsType) => {
  36. onChange({ ...options, [key]: !options[key] });
  37. };
  38. const handlePreheatOverride = (next: PreheatOverride) => {
  39. onChange({
  40. ...options,
  41. preheat_override: next,
  42. // Clearing override→off also clears the chamber-target override so the
  43. // backend doesn't carry a stale value if the user re-enables later.
  44. ...(next === 'off' ? { preheat_chamber_target_override: null } : {}),
  45. });
  46. };
  47. const handlePreheatTarget = (raw: string) => {
  48. if (raw === '') {
  49. onChange({ ...options, preheat_chamber_target_override: null });
  50. return;
  51. }
  52. const parsed = parseInt(raw, 10);
  53. if (Number.isNaN(parsed)) return;
  54. onChange({
  55. ...options,
  56. preheat_chamber_target_override: Math.max(0, Math.min(60, parsed)),
  57. });
  58. };
  59. return (
  60. <div className="mb-4">
  61. <button
  62. type="button"
  63. onClick={() => setIsExpanded(!isExpanded)}
  64. className="flex items-center gap-2 text-sm text-bambu-gray hover:text-white transition-colors w-full"
  65. >
  66. <Settings className="w-4 h-4" />
  67. <span>{t('queue.bulkEdit.printOptions')}</span>
  68. {isExpanded ? (
  69. <ChevronUp className="w-4 h-4 ml-auto" />
  70. ) : (
  71. <ChevronDown className="w-4 h-4 ml-auto" />
  72. )}
  73. </button>
  74. {isExpanded && (
  75. <div className="mt-2 bg-bambu-dark rounded-lg p-3 space-y-2">
  76. {visibleOptions.map(({ key, label, desc }) => (
  77. <label key={key} className="flex items-center justify-between cursor-pointer group">
  78. <div>
  79. <span className="text-sm text-white">{label}</span>
  80. <p className="text-xs text-bambu-gray">{desc}</p>
  81. </div>
  82. <div
  83. className={`relative w-10 h-5 rounded-full transition-colors ${
  84. options[key as 'bed_levelling'] ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
  85. }`}
  86. onClick={() => handleToggle(key)}
  87. >
  88. <div
  89. className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
  90. options[key as 'bed_levelling'] ? 'translate-x-5' : 'translate-x-0.5'
  91. }`}
  92. />
  93. </div>
  94. </label>
  95. ))}
  96. {/* Preheat / heat-soak per-item override (#1468). Defaults to
  97. 'inherit' which means the global Settings → Workflow toggle
  98. decides. Forcing 'on' or 'off' overrides per-print; the chamber
  99. target override (optional °C input, visible when not 'off')
  100. bypasses the per-filament-type derivation. */}
  101. <div className="pt-2 mt-1 border-t border-bambu-dark-tertiary/60">
  102. <div className="flex items-center gap-2 mb-1.5">
  103. <Flame className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400" />
  104. <span className="text-sm text-white">{t('settings.preheatTitle', 'Preheat & Heat Soak')}</span>
  105. </div>
  106. <p className="text-xs text-bambu-gray mb-2">
  107. {t('settings.preheatPerItemDesc', 'Heat the bed and chamber before this print starts. Defaults to the global Settings → Workflow toggle.')}
  108. </p>
  109. <div className="flex gap-1.5 mb-2">
  110. {(['inherit', 'on', 'off'] as PreheatOverride[]).map((opt) => (
  111. <button
  112. key={opt}
  113. type="button"
  114. onClick={() => handlePreheatOverride(opt)}
  115. className={`flex-1 px-2 py-1.5 text-xs rounded transition-colors ${
  116. options.preheat_override === opt
  117. ? 'bg-bambu-green text-white'
  118. : 'bg-bambu-dark-tertiary text-bambu-gray hover:text-white'
  119. }`}
  120. >
  121. {t(`settings.preheatOverride_${opt}`, opt === 'inherit' ? 'Inherit' : opt === 'on' ? 'On' : 'Off')}
  122. </button>
  123. ))}
  124. </div>
  125. {options.preheat_override !== 'off' && (
  126. <div className="flex items-center gap-2">
  127. <label className="text-xs text-bambu-gray flex-1">
  128. {t('settings.preheatTargetOverride', 'Chamber target override (°C, blank = filament default)')}
  129. </label>
  130. <input
  131. type="number"
  132. min={0}
  133. max={60}
  134. step={1}
  135. value={options.preheat_chamber_target_override ?? ''}
  136. onChange={(e) => handlePreheatTarget(e.target.value)}
  137. placeholder="—"
  138. className="w-16 px-2 py-1 bg-bambu-dark-tertiary border border-bambu-dark-tertiary rounded text-white text-xs text-right focus:outline-none focus:border-bambu-green"
  139. />
  140. </div>
  141. )}
  142. </div>
  143. </div>
  144. )}
  145. </div>
  146. );
  147. }