فهرست منبع

fix: display friendly legacy printer model names in stream overlay (#3134)

Adam Spice 2 روز پیش
والد
کامیت
2b6dbe5ae5

+ 13 - 2
frontend/src/__tests__/pages/StreamOverlayPage.test.tsx

@@ -150,6 +150,17 @@ describe('StreamOverlayPage', () => {
       if (query === '?show=model') expect(screen.queryByText(/X1 Carbon/)).not.toBeInTheDocument();
     });
 
+    it.each([
+      ['BL-P001', 'X1C'],
+      ['O1D', 'H2D'],
+      ['H2D', 'H2D'],
+      ['Future printer', 'Future printer'],
+    ])('displays the saved model %s as %s', async (model, displayName) => {
+      server.use(http.get('/api/v1/printers/:id', () => HttpResponse.json({ ...mockPrinter, model })));
+      renderOverlayPage(1, '?show=model');
+      expect(await screen.findByText(displayName)).toBeInTheDocument();
+    });
+
     it.each([null, ''])('omits a missing model without adding a separator (%s)', async (model) => {
       server.use(http.get('/api/v1/printers/:id', () => HttpResponse.json({ ...mockPrinter, model })));
       renderOverlayPage(1, '?show=printer,model');
@@ -157,7 +168,7 @@ describe('StreamOverlayPage', () => {
       expect(screen.queryByText(/·/)).not.toBeInTheDocument();
     });
 
-    it.each([['H2D', 'Workshop · H2D'], [null, 'Workshop'], ['', 'Workshop']] as const)('reads the model (%s) from the OBS token feed without requesting printer details', async (model, identity) => {
+    it.each([['H2D', 'H2D'], ['BL-P001', 'X1C'], ['O1D', 'H2D'], [null, ''], ['', '']] as const)('reads the model %s from the OBS token feed without requesting printer details', async (model, displayName) => {
       let printerHit = false;
       server.use(
         http.get('/api/v1/printers/:id/overlay-status', () => HttpResponse.json({
@@ -170,7 +181,7 @@ describe('StreamOverlayPage', () => {
         }),
       );
       renderOverlayPage(1, '?token=obs-tok&show=printer,model');
-      expect(await screen.findByText(identity)).toBeInTheDocument();
+      expect(await screen.findByText(displayName ? `Workshop · ${displayName}` : 'Workshop')).toBeInTheDocument();
       expect(printerHit).toBe(false);
       if (!model) expect(screen.queryByText(/·/)).not.toBeInTheDocument();
     });

+ 1 - 45
frontend/src/pages/PrintersPage.tsx

@@ -1,6 +1,7 @@
 import { useState, useEffect, useLayoutEffect, useMemo, useRef, useCallback } from 'react';
 import { createPortal } from 'react-dom';
 import { compareFwVersions } from '../utils/firmwareVersion';
+import { mapModelCode } from '../utils/printerModel';
 import { formatPrintName } from '../utils/printName';
 import { isBedSlinger } from '../utils/bedSlinger';
 import { computePopoverPosition, type PopoverPosition } from '../utils/popoverPosition';
@@ -1747,51 +1748,6 @@ const MODELS_WITH_EXHAUST_LABEL: ReadonlySet<string> = new Set([
   'X2D',
 ]);
 
-// Map SSDP model codes to display names
-function mapModelCode(ssdpModel: string | null): string {
-  if (!ssdpModel) return '';
-  const modelMap: Record<string, string> = {
-    // H2 Series
-    'O1D': 'H2D',
-    'O1E': 'H2D Pro',
-    'O2D': 'H2D Pro',
-    'O1C': 'H2C',
-    'O1C2': 'H2C',
-    'O1S': 'H2S',
-    // X1 Series
-    'BL-P001': 'X1C',
-    'BL-P002': 'X1',
-    'BL-P003': 'X1E',
-    // X2 Series
-    'N6': 'X2D',
-    // A2 Series
-    'N9': 'A2L',
-    // P Series
-    'C11': 'P1S',
-    'C12': 'P1P',
-    'C13': 'P2S',
-    // A1 Series
-    'N2S': 'A1',
-    'N1': 'A1 Mini',
-    // Direct matches
-    'X1C': 'X1C',
-    'X1': 'X1',
-    'X1E': 'X1E',
-    'X2D': 'X2D',
-    'P1S': 'P1S',
-    'P1P': 'P1P',
-    'P2S': 'P2S',
-    'A1': 'A1',
-    'A1 Mini': 'A1 Mini',
-    'A2L': 'A2L',
-    'H2D': 'H2D',
-    'H2D Pro': 'H2D Pro',
-    'H2C': 'H2C',
-    'H2S': 'H2S',
-  };
-  return modelMap[ssdpModel] || ssdpModel;
-}
-
 // ─── AMS Name Hover Card ──────────────────────────────────────────────────────
 // Wraps the AMS label (e.g. "AMS-A") and shows a popup with:
 //  • User-defined friendly name (editable, protected by printers:update)

+ 2 - 1
frontend/src/pages/StreamOverlayPage.tsx

@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
 import { Layers, Clock, Timer, Printer, Flame, Square, Box } from 'lucide-react';
 import { api, ApiError, withStreamToken } from '../api/client';
 import { formatDuration, formatETA, type TimeFormat } from '../utils/date';
+import { mapModelCode } from '../utils/printerModel';
 
 type TFunction = (key: string, options?: Record<string, unknown>) => string;
 
@@ -217,7 +218,7 @@ export function StreamOverlayPage() {
   );
   const printerIdentity = [
     config.showPrinter ? printer?.name : null,
-    config.showModel ? printer?.model : null,
+    config.showModel ? mapModelCode(printer?.model ?? null) : null,
   ].filter(Boolean).join(' · ');
   const status = kiosk ? overlay : statusData;
   const timeFormat: TimeFormat = (kiosk ? overlay?.time_format : settings?.time_format) || 'system';

+ 1 - 15
frontend/src/pages/spoolbuddy/SpoolBuddyAmsPage.tsx

@@ -8,6 +8,7 @@ import { api } from '../../api/client';
 import type { PrinterStatus, AMSTray, SpoolAssignment } from '../../api/client';
 import { getGlobalTrayId, getFillBarColor, getSpoolmanFillLevel, getFallbackSpoolTag, formatSlotLabel, isBambuLabSpool, resolveSlotNozzleDiameter } from '../../utils/amsHelpers';
 import { getSwatchStyle, resolveSpoolColorName } from '../../utils/colors';
+import { mapModelCode } from '../../utils/printerModel';
 import { spoolSwatchStyle } from '../../components/spoolbuddy/spoolPaint';
 
 /**
@@ -39,21 +40,6 @@ function getAmsName(amsId: number): string {
   return `AMS ${amsId}`;
 }
 
-function mapModelCode(ssdpModel: string | null): string {
-  if (!ssdpModel) return '';
-  const modelMap: Record<string, string> = {
-    'O1D': 'H2D', 'O1E': 'H2D Pro', 'O2D': 'H2D Pro', 'O1C': 'H2C', 'O1C2': 'H2C', 'O1S': 'H2S',
-    'BL-P001': 'X1C', 'BL-P002': 'X1', 'BL-P003': 'X1E',
-    'N6': 'X2D',
-    'N9': 'A2L',
-    'C11': 'P1S', 'C12': 'P1P', 'C13': 'P2S',
-    'N2S': 'A1', 'N1': 'A1 Mini',
-    'X1C': 'X1C', 'X1': 'X1', 'X1E': 'X1E', 'X2D': 'X2D', 'P1S': 'P1S', 'P1P': 'P1P', 'P2S': 'P2S',
-    'A1': 'A1', 'A1 Mini': 'A1 Mini', 'A2L': 'A2L', 'H2D': 'H2D', 'H2D Pro': 'H2D Pro', 'H2C': 'H2C', 'H2S': 'H2S',
-  };
-  return modelMap[ssdpModel] || ssdpModel;
-}
-
 function isTrayEmpty(tray: AMSTray): boolean {
   return !tray.tray_type || tray.tray_type === '';
 }

+ 44 - 0
frontend/src/utils/printerModel.ts

@@ -0,0 +1,44 @@
+// Map SSDP model codes to display names
+export function mapModelCode(ssdpModel: string | null): string {
+  if (!ssdpModel) return '';
+  const modelMap: Record<string, string> = {
+    // H2 Series
+    'O1D': 'H2D',
+    'O1E': 'H2D Pro',
+    'O2D': 'H2D Pro',
+    'O1C': 'H2C',
+    'O1C2': 'H2C',
+    'O1S': 'H2S',
+    // X1 Series
+    'BL-P001': 'X1C',
+    'BL-P002': 'X1',
+    'BL-P003': 'X1E',
+    // X2 Series
+    'N6': 'X2D',
+    // A2 Series
+    'N9': 'A2L',
+    // P Series
+    'C11': 'P1S',
+    'C12': 'P1P',
+    'C13': 'P2S',
+    // A1 Series
+    'N2S': 'A1',
+    'N1': 'A1 Mini',
+    // Direct matches
+    'X1C': 'X1C',
+    'X1': 'X1',
+    'X1E': 'X1E',
+    'X2D': 'X2D',
+    'P1S': 'P1S',
+    'P1P': 'P1P',
+    'P2S': 'P2S',
+    'A1': 'A1',
+    'A1 Mini': 'A1 Mini',
+    'A2L': 'A2L',
+    'H2D': 'H2D',
+    'H2D Pro': 'H2D Pro',
+    'H2C': 'H2C',
+    'H2S': 'H2S',
+  };
+  return modelMap[ssdpModel] || ssdpModel;
+}