|
|
@@ -13,7 +13,6 @@ import { CALIBRATION_MODES, CALIBRATION_MODE_ACTIVE, CALIBRATION_MODE_INACTIVE }
|
|
|
import { PreheatFilamentTargetsEditor } from '../components/PreheatFilamentTargetsEditor';
|
|
|
import type { APIKey, AppSettings, AppSettingsUpdate, PrinterHASensor, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus, GitHubBackupStatus, CloudAuthStatus, UserCreate, UserUpdate, UserResponse, StorageUsageResponse, CalibrationMode } from '../api/client';
|
|
|
import { Card, CardContent, CardDensityProvider, CardHeader } from '../components/Card';
|
|
|
-import { SlicerBundlesPanel } from '../components/SlicerBundlesPanel';
|
|
|
import { SlicerPipelinesPanel } from '../components/SlicerPipelinesPanel';
|
|
|
import { CameraTokensSection } from './CameraTokensPage';
|
|
|
import { StreamOverlayBuilder } from '../components/StreamOverlayBuilder';
|
|
|
@@ -5025,113 +5024,6 @@ export function SettingsPage() {
|
|
|
</CardContent>
|
|
|
</Card>
|
|
|
|
|
|
- {/* G-code Injection (#422) */}
|
|
|
- <Card id="card-gcode">
|
|
|
- <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-3">
|
|
|
- <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: '' };
|
|
|
- const hasContent = !!(snippet.start_gcode || snippet.end_gcode);
|
|
|
- return (
|
|
|
- <Collapsible
|
|
|
- key={model}
|
|
|
- defaultOpen={hasContent}
|
|
|
- className="border border-bambu-dark-tertiary rounded-lg px-3 py-2"
|
|
|
- summary={
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <h4 className="text-sm font-medium text-white">{model}</h4>
|
|
|
- {hasContent && (
|
|
|
- <span className="text-xs px-1.5 py-0.5 rounded bg-bambu-green/20 text-bambu-green">
|
|
|
- {t('settings.gcodeConfigured', 'Configured')}
|
|
|
- </span>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- }
|
|
|
- >
|
|
|
- <div className="space-y-2">
|
|
|
- <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>
|
|
|
- </Collapsible>
|
|
|
- );
|
|
|
- });
|
|
|
- })()}
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
-
|
|
|
</div>
|
|
|
{/* Right Column */}
|
|
|
<div className="lg:w-1/2 space-y-3">
|
|
|
@@ -5363,12 +5255,6 @@ export function SettingsPage() {
|
|
|
</CardContent>
|
|
|
</Card>
|
|
|
|
|
|
- {/* Slicer Preset Bundles — only meaningful when the sidecar is in use,
|
|
|
- since uploads / lists round-trip through it. Hide it entirely when
|
|
|
- use_slicer_api is off so the Settings page doesn't show a panel that
|
|
|
- can't do anything. */}
|
|
|
- {(localSettings.use_slicer_api ?? false) && <SlicerBundlesPanel />}
|
|
|
-
|
|
|
{/* Auto-Drying */}
|
|
|
<Card>
|
|
|
<CardHeader>
|
|
|
@@ -5649,6 +5535,113 @@ export function SettingsPage() {
|
|
|
</div>
|
|
|
</CardContent>
|
|
|
</Card>
|
|
|
+
|
|
|
+ {/* G-code Injection (#422) */}
|
|
|
+ <Card id="card-gcode">
|
|
|
+ <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-3">
|
|
|
+ <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: '' };
|
|
|
+ const hasContent = !!(snippet.start_gcode || snippet.end_gcode);
|
|
|
+ return (
|
|
|
+ <Collapsible
|
|
|
+ key={model}
|
|
|
+ defaultOpen={hasContent}
|
|
|
+ className="border border-bambu-dark-tertiary rounded-lg px-3 py-2"
|
|
|
+ summary={
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <h4 className="text-sm font-medium text-white">{model}</h4>
|
|
|
+ {hasContent && (
|
|
|
+ <span className="text-xs px-1.5 py-0.5 rounded bg-bambu-green/20 text-bambu-green">
|
|
|
+ {t('settings.gcodeConfigured', 'Configured')}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <div className="space-y-2">
|
|
|
+ <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>
|
|
|
+ </Collapsible>
|
|
|
+ );
|
|
|
+ });
|
|
|
+ })()}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|