Add auto-print G-code injection for queue items (#422)

Per-model start/end G-code snippets configurable in Settings (Workflow
  tab). Queue items get "Inject G-code" toggle — scheduler injects
  snippets into a temp 3MF copy before FTP upload. Supports Farmloop,
  SwapMod, AutoClear, Printflow 3D and similar bed-clearing systems.
  Original files are never modified.
This commit is contained in:
maziggy
2026-04-05 11:14:26 +02:00
parent aa9b60a265
commit f006472f79
29 changed files with 778 additions and 182 deletions
@@ -30,6 +30,7 @@ const createMockQueueItem = (overrides: Partial<PrintQueueItem> = {}): PrintQueu
scheduled_time: null,
require_previous_success: false,
auto_off_after: false,
gcode_injection: false,
manual_start: false,
ams_mapping: null,
plate_id: null,
@@ -415,4 +415,31 @@ describe('QueuePage', () => {
});
});
});
describe('gcode injection badge', () => {
it('shows G-code badge when gcode_injection is true', async () => {
const itemsWithGcode = mockQueueItems.map((item, i) =>
i === 0 ? { ...item, gcode_injection: true } : item
);
server.use(
http.get('/api/v1/queue/', () => HttpResponse.json(itemsWithGcode)),
);
render(<QueuePage />);
await waitFor(() => {
expect(screen.getByText('G-code')).toBeInTheDocument();
});
});
it('does not show G-code badge when gcode_injection is false', async () => {
render(<QueuePage />);
await waitFor(() => {
expect(screen.getByText('Test Print 1')).toBeInTheDocument();
});
expect(screen.queryByText('G-code')).not.toBeInTheDocument();
});
});
});
+9
View File
@@ -831,6 +831,7 @@ export interface AppSettings {
queue_drying_block: boolean; // Block queue until drying completes
ambient_drying_enabled: boolean; // Auto-dry idle printers based on humidity regardless of queue
drying_presets: string; // JSON blob of drying presets per filament type
gcode_snippets: string; // JSON: per-model G-code injection snippets
// Print modal settings
per_printer_mapping_expanded: boolean; // Whether custom mapping is expanded by default in print modal
// Date/time format settings
@@ -1384,6 +1385,8 @@ export interface PrintQueueItem {
batch_name?: string | null;
// Shortest-job-first scheduling
been_jumped?: boolean;
// Auto-print G-code injection
gcode_injection?: boolean;
}
export interface PrintBatch {
@@ -1423,6 +1426,8 @@ export interface PrintQueueItemCreate {
layer_inspect?: boolean;
timelapse?: boolean;
use_ams?: boolean;
// Auto-print G-code injection
gcode_injection?: boolean;
// Batch: create multiple copies (creates a batch if > 1)
quantity?: number;
}
@@ -1446,6 +1451,8 @@ export interface PrintQueueItemUpdate {
layer_inspect?: boolean;
timelapse?: boolean;
use_ams?: boolean;
// Auto-print G-code injection
gcode_injection?: boolean;
}
export interface PrintQueueBulkUpdate {
@@ -1462,6 +1469,8 @@ export interface PrintQueueBulkUpdate {
layer_inspect?: boolean;
timelapse?: boolean;
use_ams?: boolean;
// Auto-print G-code injection
gcode_injection?: boolean;
}
export interface PrintQueueBulkUpdateResponse {
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Calendar, Clock, Hand, Power, Layers } from 'lucide-react';
import { Calendar, Clock, Hand, Power, Layers, Code } from 'lucide-react';
import type { ScheduleOptionsProps, ScheduleType } from './types';
import {
formatDateInput,
@@ -27,6 +27,7 @@ export function ScheduleOptionsPanel({
canControlPrinter = true,
showStagger = false,
printerCount = 0,
hasGcodeSnippets = false,
}: ScheduleOptionsProps) {
const { t } = useTranslation();
const [dateValue, setDateValue] = useState('');
@@ -251,6 +252,23 @@ export function ScheduleOptionsPanel({
</label>
</div>
{/* G-code injection */}
{hasGcodeSnippets && (
<div className="flex items-center gap-2">
<input
type="checkbox"
id="gcodeInjection"
checked={options.gcodeInjection}
onChange={(e) => onChange({ ...options, gcodeInjection: e.target.checked })}
className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
/>
<label htmlFor="gcodeInjection" className="text-sm flex items-center gap-1 text-bambu-gray">
<Code className="w-3.5 h-3.5" />
{t('printModal.gcodeInjection', 'Inject auto-print G-code')}
</label>
</div>
)}
{/* Stagger start */}
{showStagger && options.scheduleType !== 'manual' && (
<div className="space-y-3">
+23 -1
View File
@@ -1,5 +1,5 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { AlertCircle, AlertTriangle, Calendar, Layers, Loader2, Pencil, Printer, X } from 'lucide-react';
import { AlertCircle, AlertTriangle, Calendar, Code, Layers, Loader2, Pencil, Printer, X } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { PrintQueueItemCreate, PrintQueueItemUpdate, SpoolAssignment } from '../../api/client';
@@ -124,6 +124,7 @@ export function PrintModal({
scheduledTime,
requirePreviousSuccess: queueItem.require_previous_success,
autoOffAfter: queueItem.auto_off_after,
gcodeInjection: queueItem.gcode_injection ?? false,
staggerEnabled: false,
staggerGroupSize: DEFAULT_SCHEDULE_OPTIONS.staggerGroupSize,
staggerIntervalMinutes: DEFAULT_SCHEDULE_OPTIONS.staggerIntervalMinutes,
@@ -645,6 +646,7 @@ export function PrintModal({
library_file_id: isLibraryFile ? libraryFileId : undefined,
require_previous_success: scheduleOptions.requirePreviousSuccess,
auto_off_after: scheduleOptions.autoOffAfter,
gcode_injection: scheduleOptions.gcodeInjection,
manual_start: scheduleOptions.scheduleType === 'manual',
ams_mapping: printerId ? getMappingForPrinter(printerId) : undefined,
plate_id: plateOverride !== undefined ? plateOverride : selectedPlate,
@@ -678,6 +680,7 @@ export function PrintModal({
filament_overrides: filamentOverridesArray || null,
require_previous_success: scheduleOptions.requirePreviousSuccess,
auto_off_after: scheduleOptions.autoOffAfter,
gcode_injection: scheduleOptions.gcodeInjection,
manual_start: scheduleOptions.scheduleType === 'manual',
ams_mapping: undefined,
plate_id: plateId,
@@ -755,6 +758,7 @@ export function PrintModal({
target_location: null,
require_previous_success: scheduleOptions.requirePreviousSuccess,
auto_off_after: scheduleOptions.autoOffAfter,
gcode_injection: scheduleOptions.gcodeInjection,
manual_start: scheduleOptions.scheduleType === 'manual',
ams_mapping: printerMapping,
plate_id: plateId,
@@ -1130,9 +1134,27 @@ export function PrintModal({
canControlPrinter={hasPermission('printers:control')}
showStagger={mode === 'add-to-queue' && assignmentMode === 'printer' && selectedPrinters.length > 1}
printerCount={selectedPrinters.length}
hasGcodeSnippets={!!settings?.gcode_snippets}
/>
)}
{/* G-code injection for reprint mode (only shown when quantity > 1 — applies to queued copies) */}
{mode === 'reprint' && !!settings?.gcode_snippets && effectiveQuantity > 1 && (
<div className="flex items-center gap-2">
<input
type="checkbox"
id="gcodeInjectionReprint"
checked={scheduleOptions.gcodeInjection}
onChange={(e) => setScheduleOptions({ ...scheduleOptions, gcodeInjection: e.target.checked })}
className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
/>
<label htmlFor="gcodeInjectionReprint" className="text-sm flex items-center gap-1 text-bambu-gray">
<Code className="w-3.5 h-3.5" />
{t('printModal.gcodeInjection', 'Inject auto-print G-code')}
</label>
</div>
)}
{/* Error message */}
{updateQueueMutation.isError && (
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
@@ -69,6 +69,7 @@ export interface ScheduleOptions {
scheduledTime: string;
requirePreviousSuccess: boolean;
autoOffAfter: boolean;
gcodeInjection: boolean;
staggerEnabled: boolean;
staggerGroupSize: number;
staggerIntervalMinutes: number;
@@ -82,6 +83,7 @@ export const DEFAULT_SCHEDULE_OPTIONS: ScheduleOptions = {
scheduledTime: '',
requirePreviousSuccess: false,
autoOffAfter: false,
gcodeInjection: false,
staggerEnabled: false,
staggerGroupSize: 2,
staggerIntervalMinutes: 5,
@@ -214,4 +216,6 @@ export interface ScheduleOptionsProps {
showStagger?: boolean;
/** Number of selected printers (for stagger preview) */
printerCount?: number;
/** Whether G-code snippets are configured in settings */
hasGcodeSnippets?: boolean;
}
+9
View File
@@ -919,6 +919,7 @@ export default {
staged: 'Bereitgestellt',
requiresPrevious: 'Erfordert vorherigen Erfolg',
autoPowerOff: 'Automatisch ausschalten',
gcodeInjection: 'G-code',
},
// Empty state
empty: {
@@ -1612,6 +1613,13 @@ export default {
plateClear: 'Druckplatte-Bestätigung',
requirePlateClear: 'Druckplatte-Bestätigung erforderlich',
requirePlateClearDescription: 'Wenn aktiviert, wartet der Scheduler auf eine Druckplatte-Bestätigung pro Drucker, bevor geplante Drucke auf Druckern mit abgeschlossenen Aufträgen gestartet werden. Deaktivieren Sie dies für Farm-Workflows, bei denen die Platten physisch überprüft werden.',
gcodeInjection: 'G-code Injection',
gcodeInjectionDescription: 'Konfigurieren Sie benutzerdefinierten G-code, der am Anfang und/oder Ende von Drucken für Auto-Print-Systeme wie Farmloop, SwapMod, AutoClear und Printflow 3D eingefügt wird. Snippets werden pro Druckermodell konfiguriert und angewendet, wenn "G-code einfügen" bei einem Warteschlangen-Element aktiviert ist.',
gcodeInjectionNoPrinters: 'Keine Drucker gefunden. Fügen Sie Drucker hinzu, um G-code-Snippets zu konfigurieren.',
gcodeStartLabel: 'Start G-code',
gcodeEndLabel: 'End G-code',
gcodeStartPlaceholder: 'G-code, der vor dem Druckstart eingefügt wird...',
gcodeEndPlaceholder: 'G-code, der nach dem Druckende angefügt wird...',
staggerGroupSize: 'Group size',
staggerGroupSizeHelp: 'Printers to start simultaneously per group',
staggerInterval: 'Interval (minutes)',
@@ -3218,6 +3226,7 @@ export default {
staggerLastGroup: 'last group: {{count}}',
staggerTotal: 'total: {{minutes}} min',
staggerToPrinters: 'Gestaffelt an {{count}} Drucker senden',
gcodeInjection: 'Auto-Print G-code einfügen',
},
// Backup
+9
View File
@@ -919,6 +919,7 @@ export default {
staged: 'Staged',
requiresPrevious: 'Requires previous success',
autoPowerOff: 'Auto power off',
gcodeInjection: 'G-code',
},
// Empty state
empty: {
@@ -1613,6 +1614,13 @@ export default {
plateClear: 'Plate-Clear Confirmation',
requirePlateClear: 'Require plate-clear confirmation',
requirePlateClearDescription: 'When enabled, the scheduler waits for per-printer plate-clear confirmation before starting queued prints on printers with finished jobs. Disable for farm workflows where plates are verified physically.',
gcodeInjection: 'G-code Injection',
gcodeInjectionDescription: 'Configure custom G-code to inject at the start and/or end of prints for auto-print systems like Farmloop, SwapMod, AutoClear, and Printflow 3D. Snippets are configured per printer model and applied when "Inject G-code" is enabled on a queue item.',
gcodeInjectionNoPrinters: 'No printers found. Add printers to configure G-code snippets.',
gcodeStartLabel: 'Start G-code',
gcodeEndLabel: 'End G-code',
gcodeStartPlaceholder: 'G-code prepended before the print starts...',
gcodeEndPlaceholder: 'G-code appended after the print ends...',
staggerGroupSize: 'Group size',
staggerGroupSizeHelp: 'Printers to start simultaneously per group',
staggerInterval: 'Interval (minutes)',
@@ -3223,6 +3231,7 @@ export default {
staggerLastGroup: 'last group: {{count}}',
staggerTotal: 'total: {{minutes}} min',
staggerToPrinters: 'Stagger to {{count}} printers',
gcodeInjection: 'Inject auto-print G-code',
},
// Backup
+9
View File
@@ -919,6 +919,7 @@ export default {
staged: 'Préparé',
requiresPrevious: 'Nécessite succès précédent',
autoPowerOff: 'Extinction auto',
gcodeInjection: 'G-code',
},
// Empty state
empty: {
@@ -1612,6 +1613,13 @@ export default {
plateClear: 'Confirmation de plateau libre',
requirePlateClear: 'Exiger la confirmation de plateau libre',
requirePlateClearDescription: 'Lorsque activé, le planificateur attend la confirmation de plateau libre par imprimante avant de lancer les impressions en file d\'attente sur les imprimantes ayant terminé. Désactivez pour les workflows de ferme où les plateaux sont vérifiés physiquement.',
gcodeInjection: 'Injection de G-code',
gcodeInjectionDescription: 'Configurez du G-code personnalisé à injecter au début et/ou à la fin des impressions pour les systèmes d\'auto-impression comme Farmloop, SwapMod, AutoClear et Printflow 3D. Les snippets sont configurés par modèle d\'imprimante et appliqués lorsque « Injecter le G-code » est activé sur un élément de file d\'attente.',
gcodeInjectionNoPrinters: 'Aucune imprimante trouvée. Ajoutez des imprimantes pour configurer les snippets G-code.',
gcodeStartLabel: 'G-code de début',
gcodeEndLabel: 'G-code de fin',
gcodeStartPlaceholder: 'G-code ajouté avant le début de l\'impression...',
gcodeEndPlaceholder: 'G-code ajouté après la fin de l\'impression...',
staggerGroupSize: 'Group size',
staggerGroupSizeHelp: 'Printers to start simultaneously per group',
staggerInterval: 'Interval (minutes)',
@@ -3209,6 +3217,7 @@ export default {
staggerLastGroup: 'last group: {{count}}',
staggerTotal: 'total: {{minutes}} min',
staggerToPrinters: 'Échelonner sur {{count}} imprimantes',
gcodeInjection: 'Injecter le G-code auto-impression',
},
// Backup
+9
View File
@@ -919,6 +919,7 @@ export default {
staged: 'In staging',
requiresPrevious: 'Richiede successo precedente',
autoPowerOff: 'Spegnimento automatico',
gcodeInjection: 'G-code',
},
// Empty state
empty: {
@@ -1612,6 +1613,13 @@ export default {
plateClear: 'Conferma piatto libero',
requirePlateClear: 'Richiedi conferma piatto libero',
requirePlateClearDescription: 'Quando abilitato, lo scheduler attende la conferma per stampante che il piatto è libero prima di avviare le stampe in coda su stampanti con lavori completati. Disabilitare per flussi di lavoro in farm dove i piatti vengono verificati fisicamente.',
gcodeInjection: 'Iniezione G-code',
gcodeInjectionDescription: 'Configura G-code personalizzato da iniettare all\'inizio e/o alla fine delle stampe per sistemi di stampa automatica come Farmloop, SwapMod, AutoClear e Printflow 3D. Gli snippet sono configurati per modello di stampante e applicati quando "Inietta G-code" è abilitato su un elemento della coda.',
gcodeInjectionNoPrinters: 'Nessuna stampante trovata. Aggiungi stampanti per configurare gli snippet G-code.',
gcodeStartLabel: 'G-code iniziale',
gcodeEndLabel: 'G-code finale',
gcodeStartPlaceholder: 'G-code inserito prima dell\'inizio della stampa...',
gcodeEndPlaceholder: 'G-code aggiunto dopo la fine della stampa...',
staggerGroupSize: 'Group size',
staggerGroupSizeHelp: 'Printers to start simultaneously per group',
staggerInterval: 'Interval (minutes)',
@@ -3208,6 +3216,7 @@ export default {
staggerLastGroup: 'last group: {{count}}',
staggerTotal: 'total: {{minutes}} min',
staggerToPrinters: 'Scagliona a {{count}} stampanti',
gcodeInjection: 'Inietta G-code auto-stampa',
},
// Backup
+9
View File
@@ -918,6 +918,7 @@ export default {
staged: 'ステージ済み',
requiresPrevious: '前の成功が必要',
autoPowerOff: '自動電源オフ',
gcodeInjection: 'G-code',
},
// Empty state
empty: {
@@ -1611,6 +1612,13 @@ export default {
plateClear: 'プレートクリア確認',
requirePlateClear: 'プレートクリア確認を必須にする',
requirePlateClearDescription: '有効にすると、スケジューラーは完了したプリンターでキューの印刷を開始する前に、プリンターごとのプレートクリア確認を待ちます。プレートを物理的に確認するファームワークフローでは無効にしてください。',
gcodeInjection: 'G-codeインジェクション',
gcodeInjectionDescription: 'Farmloop、SwapMod、AutoClear、Printflow 3Dなどの自動印刷システム用に、印刷の開始と終了時にカスタムG-codeを挿入します。スニペットはプリンターモデルごとに設定し、キューアイテム��「G-codeを挿入」を有効にすると適用されます。',
gcodeInjectionNoPrinters: 'プリンターが見つかりません。G-codeスニペットを設定するにはプリンターを追加してください。',
gcodeStartLabel: '開始G-code',
gcodeEndLabel: '終了G-code',
gcodeStartPlaceholder: '印刷開始前に挿入されるG-code...',
gcodeEndPlaceholder: '印刷終了後に追加されるG-code...',
staggerGroupSize: 'Group size',
staggerGroupSizeHelp: 'Printers to start simultaneously per group',
staggerInterval: 'Interval (minutes)',
@@ -3221,6 +3229,7 @@ export default {
staggerLastGroup: 'last group: {{count}}',
staggerTotal: 'total: {{minutes}} min',
staggerToPrinters: '{{count}}台のプリンターに段階的に送信',
gcodeInjection: '自動印刷G-codeを挿入',
},
// Backup
+9
View File
@@ -919,6 +919,7 @@ export default {
staged: 'Preparado (início manual)',
requiresPrevious: 'Requer sucesso anterior',
autoPowerOff: 'Desligamento automático',
gcodeInjection: 'G-code',
},
// Empty state
empty: {
@@ -1612,6 +1613,13 @@ export default {
plateClear: 'Confirmação de placa livre',
requirePlateClear: 'Exigir confirmação de placa livre',
requirePlateClearDescription: 'Quando ativado, o agendador aguarda a confirmação de placa livre por impressora antes de iniciar impressões na fila em impressoras com trabalhos concluídos. Desative para fluxos de trabalho de fazenda onde as placas são verificadas fisicamente.',
gcodeInjection: 'Injeção de G-code',
gcodeInjectionDescription: 'Configure G-code personalizado para injetar no início e/ou no final das impressões para sistemas de impressão automática como Farmloop, SwapMod, AutoClear e Printflow 3D. Os snippets são configurados por modelo de impressora e aplicados quando "Injetar G-code" está ativado em um item da fila.',
gcodeInjectionNoPrinters: 'Nenhuma impressora encontrada. Adicione impressoras para configurar snippets de G-code.',
gcodeStartLabel: 'G-code inicial',
gcodeEndLabel: 'G-code final',
gcodeStartPlaceholder: 'G-code inserido antes do início da impressão...',
gcodeEndPlaceholder: 'G-code adicionado após o término da impressão...',
staggerGroupSize: 'Group size',
staggerGroupSizeHelp: 'Printers to start simultaneously per group',
staggerInterval: 'Interval (minutes)',
@@ -3208,6 +3216,7 @@ export default {
staggerLastGroup: 'last group: {{count}}',
staggerTotal: 'total: {{minutes}} min',
staggerToPrinters: 'Escalonar para {{count}} impressoras',
gcodeInjection: 'Injetar G-code de auto-impressão',
},
// Backup
+9
View File
@@ -919,6 +919,7 @@ export default {
staged: '已暂存',
requiresPrevious: '需要前一个成功',
autoPowerOff: '自动关机',
gcodeInjection: 'G-code',
},
// Empty state
empty: {
@@ -1612,6 +1613,13 @@ export default {
plateClear: '热床清空确认',
requirePlateClear: '需要热床清空确认',
requirePlateClearDescription: '启用后,调度器会在已完成打印的打印机上启动排队打印之前,等待每台打印机的热床清空确认。对于物理验证热床的农场工作流,请禁用此选项。',
gcodeInjection: 'G-code注入',
gcodeInjectionDescription: '为Farmloop、SwapMod、AutoClear和Printflow 3D等自动打印系统配置自定义G-code,在打印开始和/或结束时注入。代码片段按打印机型号配置,在队列项目上启用"注入G-code"时应用。',
gcodeInjectionNoPrinters: '未找到打印机。添加打印机以配置G-code代码片段。',
gcodeStartLabel: '开始G-code',
gcodeEndLabel: '结束G-code',
gcodeStartPlaceholder: '在打印开始前插入的G-code...',
gcodeEndPlaceholder: '在打印结束后追加的G-code...',
staggerGroupSize: 'Group size',
staggerGroupSizeHelp: 'Printers to start simultaneously per group',
staggerInterval: 'Interval (minutes)',
@@ -3208,6 +3216,7 @@ export default {
staggerLastGroup: 'last group: {{count}}',
staggerTotal: 'total: {{minutes}} min',
staggerToPrinters: '分批发送到 {{count}} 台打印机',
gcodeInjection: '注入自动打印G-code',
},
// Backup
+7
View File
@@ -52,6 +52,7 @@ import {
ChevronRight,
List,
GanttChart,
Code,
} from 'lucide-react';
import { api } from '../api/client';
import { type TimeFormat, formatETA, formatDuration, formatRelativeTime, parseUTCDate } from '../utils/date';
@@ -547,6 +548,12 @@ function SortableQueueItem({
{t('queue.badges.autoPowerOff')}
</span>
)}
{item.gcode_injection && (
<span className="text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 bg-emerald-500/10 text-emerald-400 rounded-full border border-emerald-500/20 flex items-center gap-1">
<Code className="w-2.5 h-2.5 sm:w-3 sm:h-3" />
{t('queue.badges.gcodeInjection')}
</span>
)}
</div>
{/* Progress bar for printing items - TODO: integrate with WebSocket */}
+92 -1
View File
@@ -1,5 +1,5 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock, ChevronDown, Save, Mail, Flame, Layers, ListOrdered } from 'lucide-react';
import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock, ChevronDown, Save, Mail, Flame, Layers, ListOrdered, Code } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { api } from '../api/client';
@@ -662,6 +662,7 @@ export function SettingsPage() {
// Ref for debounce timeout
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const pendingGcodeSnippetsRef = useRef<string | null>(null);
const isSavingRef = useRef(false);
const isInitialLoadRef = useRef(true);
@@ -3471,6 +3472,96 @@ export function SettingsPage() {
</CardContent>
</Card>
{/* G-code Injection (#422) */}
<Card>
<CardHeader>
<h3 className="text-base font-semibold text-white flex items-center gap-2">
<Code className="w-4 h-4 text-bambu-green" />
{t('settings.gcodeInjection', 'G-code Injection')}
</h3>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-xs text-bambu-gray">
{t('settings.gcodeInjectionDescription', 'Configure custom G-code to inject at the start and/or end of prints for auto-print systems like Farmloop, SwapMod, AutoClear, and Printflow 3D. Snippets are configured per printer model and applied when "Inject G-code" is enabled on a queue item.')}
</p>
{(() => {
const gcodeSnippets: Record<string, { start_gcode: string; end_gcode: string }> = (() => {
try {
return localSettings.gcode_snippets ? JSON.parse(localSettings.gcode_snippets) : {};
} catch {
return {};
}
})();
const printerModels = [...new Set((printers || []).filter((p) => p.model).map((p) => p.model as string))].sort();
const updateSnippet = (model: string, field: 'start_gcode' | 'end_gcode', value: string) => {
const updated = { ...gcodeSnippets };
if (!updated[model]) {
updated[model] = { start_gcode: '', end_gcode: '' };
}
updated[model][field] = value;
// Remove model entry if both fields are empty
if (!updated[model].start_gcode && !updated[model].end_gcode) {
delete updated[model];
}
const newValue = Object.keys(updated).length > 0 ? JSON.stringify(updated) : '';
// Update local state for immediate UI feedback, save on blur
setLocalSettings(prev => prev ? { ...prev, gcode_snippets: newValue } : null);
pendingGcodeSnippetsRef.current = newValue;
};
const saveGcodeSnippets = () => {
if (pendingGcodeSnippetsRef.current !== null) {
updateMutation.mutate({ gcode_snippets: pendingGcodeSnippetsRef.current });
pendingGcodeSnippetsRef.current = null;
}
};
if (printerModels.length === 0) {
return (
<p className="text-sm text-bambu-gray italic">
{t('settings.gcodeInjectionNoPrinters', 'No printers found. Add printers to configure G-code snippets.')}
</p>
);
}
return printerModels.map((model) => {
const snippet = gcodeSnippets[model] || { start_gcode: '', end_gcode: '' };
return (
<div key={model} className="space-y-2">
<h4 className="text-sm font-medium text-white">{model}</h4>
<div>
<label className="block text-xs text-bambu-gray mb-1">
{t('settings.gcodeStartLabel', 'Start G-code')}
</label>
<textarea
value={snippet.start_gcode}
onChange={(e) => updateSnippet(model, 'start_gcode', e.target.value)}
onBlur={saveGcodeSnippets}
placeholder={t('settings.gcodeStartPlaceholder', 'G-code prepended before the print starts...')}
rows={3}
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-xs font-mono focus:outline-none focus:border-bambu-green resize-y"
/>
</div>
<div>
<label className="block text-xs text-bambu-gray mb-1">
{t('settings.gcodeEndLabel', 'End G-code')}
</label>
<textarea
value={snippet.end_gcode}
onChange={(e) => updateSnippet(model, 'end_gcode', e.target.value)}
onBlur={saveGcodeSnippets}
placeholder={t('settings.gcodeEndPlaceholder', 'G-code appended after the print ends...')}
rows={3}
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-xs font-mono focus:outline-none focus:border-bambu-green resize-y"
/>
</div>
</div>
);
});
})()}
</CardContent>
</Card>
</div>
{/* Right Column */}