Skip to content

Commit f4f6564

Browse files
committed
feat(vendors/logos): add logo core workflow without GitHub sync
1 parent c6e4217 commit f4f6564

22 files changed

Lines changed: 3300 additions & 445 deletions

client/public/locales/en/common.json

Lines changed: 101 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,12 @@
6565
"helpMargin": "Margins should be configured to match your label paper and printer, changing these will affect the size of the entire grid.",
6666
"helpPrinterMargin": "Safe-Zone should be set to how close to the paper edge your printer can print, changing these will not affect the entire grid.",
6767
"print": "Print",
68-
"exportLabels": "Export Labels",
6968
"columns": "Columns",
7069
"rows": "Rows",
7170
"paperSize": "Paper Size",
7271
"customSize": "Custom",
7372
"dimensions": "Dimensions",
7473
"amlLabelSize": "AML Label Size",
75-
"exportFormat": "Export Format",
76-
"exportFormatOptions": {
77-
"png": "PNG",
78-
"aml": "AML"
79-
},
80-
"exportAsZip": "Export as .zip",
81-
"exportDpi": "Export DPI",
82-
"exportDpiHelp": "Higher DPI makes sharper PNG/AML exports but increases file size and export time.",
8374
"showBorder": "Show Border",
8475
"previewScale": "Preview Scale",
8576
"skipItems": "Skip Items",
@@ -102,10 +93,6 @@
10293
"grid": "Grid"
10394
},
10495
"settings": "Presets",
105-
"spoolPrintPresets": "Spool Print Presets",
106-
"filamentPrintPresets": "Filament Print Presets",
107-
"spoolImagePresets": "Spool Image Presets",
108-
"filamentImagePresets": "Filament Image Presets",
10996
"defaultSettings": "Default",
11097
"addSettings": "Add New Preset",
11198
"newSetting": "New",
@@ -114,29 +101,71 @@
114101
"deleteSettingsConfirm": "Are you sure you want to delete this preset?",
115102
"settingsName": "Preset Name",
116103
"saveSetting": "Save Presets",
104+
"savePreset": "Save Preset",
117105
"saveAsImage": "Save as Image",
118-
"saveAsAmlLabels": "Save as AML (Labels)"
106+
"saveAsAmlLabels": "Save as AML (Labels)",
107+
"saveAsAmlPages": "Save as AML (Pages)",
108+
"spoolImagePresets": "Spool Image Presets",
109+
"filamentImagePresets": "Filament Image Presets",
110+
"exportFormat": "Export Format",
111+
"exportFormatOptions": {
112+
"png": "PNG",
113+
"aml": "AML"
114+
},
115+
"exportAsZip": "Export as .zip",
116+
"exportDpi": "Export DPI",
117+
"exportDpiHelp": "Higher DPI improves image quality for exports but increases file size.",
118+
"exportLabels": "Export Labels",
119+
"filenamePreview": "Filename Preview",
120+
"filenamePreviewAdditional": "Additional Example",
121+
"zipFilenamePreview": "ZIP Filename"
119122
},
120123
"qrcode": {
121124
"button": "Print Labels",
122-
"printFilamentTitle": "Print Filament Labels",
123-
"printSpoolTitle": "Print Spool Labels",
125+
"selectButton": "Print / Export Labels",
124126
"exportButton": "Export Labels",
127+
"selectTitle": "Export / Print Labels",
125128
"exportFilamentTitle": "Export Filament Labels",
126129
"exportSpoolTitle": "Export Spool Labels",
127130
"printFilamentTitle": "Print Filament Labels",
128131
"printSpoolTitle": "Print Spool Labels",
129-
"selectButton": "Export/Print",
130-
"selectTitle": "Export / Print Labels"
131132
"title": "Label Printing",
133+
"sectionLogo": "Logo",
134+
"sectionTitle": "Title",
135+
"sectionQRCode": "QR Code",
136+
"sectionInformation": "Information",
132137
"template": "Label Template",
133-
"filenameTemplate": "Filename Template",
134-
"filenameTemplateTooltipSpool": "Use {} to insert values of the spool object as text. Refer to the label template rules and available tags for more details.",
135-
"filenameTemplateTooltipFilament": "Use {} to insert values of the filament object as text. Refer to the label template rules and available tags for more details.",
136-
"templateHelp": "Use {} to insert values of the spool object as text. For example, {id} will be replaced with the spool id, or {filament.material} will be replaced with the material of the spool. if a value is missing it will be replaced with \"?\". A second set of {} can be used to remove this. In addition, any text between the sets of {} will be removed if the value is missing. For example, {Lot Nr: {lot_nr}} will only show the label if the spool has a lot number. Enclose text with double asterix ** to make it bold. Click the button to view a list of all available tags.",
137-
"templateHelpFilament": "Use {} to insert values of the filament object as text. For example, {id} will be replaced with the filament id, or {vendor.name} will be replaced with the vendor name. If a value is missing it will be replaced with \"?\". A second set of {} can be used to remove this. In addition, any text between the sets of {} will be removed if the value is missing. For example, {Article: {article_number}} will only show the label if a filament has an article number. Enclose text with double asterix ** to make it bold. Click the button to view a list of all available tags.",
138+
"titleTemplate": "Title Template",
139+
"infoTemplate": "Information Template",
140+
"templateHelp": "Use {} to insert values of the spool object as text. For example, {id} will be replaced with the spool id, or {filament.material} will be replaced with the material of the spool. if a value is missing it will be replaced with \"?\". A second set of {} can be used to remove this. In addition, any text between the sets of {} will be removed if the value is missing. For example, {Lot Nr: {lot_nr}} will only show the label if the spool has a lot number. Enclose text with double asterix ** to make it bold, and use ==text== to invert text. Click the button to view a list of all available tags.",
141+
"templateHelpFilament": "Use {} to insert values of the filament object as text. For example, {id} will be replaced with the filament id, or {vendor.name} will be replaced with the vendor name. If a value is missing it will be replaced with \"?\". A second set of {} can be used to remove this. In addition, any text between the sets of {} will be removed if the value is missing. For example, {Article: {article_number}} will only show the label if a filament has an article number. Enclose text with double asterix ** to make it bold, and use ==text== to invert text. Click the button to view a list of all available tags.",
138142
"textSize": "Label Text Size",
139-
"showContent": "Print Label",
143+
"infoTextSize": "Information Text Size",
144+
"titleTextSize": "Title Text Size",
145+
"titleMaxTextSize": "Title Max Text Size",
146+
"titleAreaHeight": "Title Area Height",
147+
"showContent": "Show Information on label",
148+
"showManufacturerLogo": "Show Manufacturer Logo",
149+
"showTitle": "Show Title",
150+
"titleFitToWidth": "Fit Title to Width",
151+
"titleFitToWidthOptions": {
152+
"on": "On",
153+
"off": "Off"
154+
},
155+
"appliedTextSize": "Applied Size",
156+
"logoSize": "Logo Size",
157+
"logoAlign": "Logo Horizontal Align",
158+
"titleAlign": "Title Horizontal Align",
159+
"qrCodePosition": "QR Code Position",
160+
"qrCodePositionLeft": "Left",
161+
"qrCodePositionRight": "Right",
162+
"qrCodeAlign": "QR Code Vertical Align",
163+
"qrCodeAlignTop": "Top",
164+
"qrCodeAlignCenter": "Center",
165+
"qrCodeAlignBottom": "Bottom",
166+
"qrCodeSize": "QR Code Size",
167+
"infoAlign": "Information Horizontal Align",
168+
"infoVerticalAlign": "Information Vertical Align",
140169
"useHTTPUrl": {
141170
"label": "QR code link",
142171
"tooltip": "Will use proprietary link that will work only if scanned from Spoolman's scanning feature (default). URL uses either the base URL specified in settings, or the current page URL if not set.",
@@ -151,12 +180,17 @@
151180
"no": "No",
152181
"simple": "Simple",
153182
"withIcon": "With Icon"
154-
}
183+
},
184+
"filenameTemplate": "Filename Template",
185+
"filenameTemplateTooltipSpool": "Use {} to insert values of the spool object as text. Refer to the label template rules and available tags for details.",
186+
"filenameTemplateTooltipFilament": "Use {} to insert values of the filament object as text. Refer to the label template rules and available tags for details.",
187+
"titleTemplateTooltipSpool": "Use {} to insert values of the spool object as text. Refer to the label template rules and available tags for details.",
188+
"titleTemplateTooltipFilament": "Use {} to insert values of the filament object as text. Refer to the label template rules and available tags for details."
155189
},
156190
"spoolSelect": {
157191
"title": "Select Spools",
158-
"description": "Select spools to export or print labels for.",
159-
"searchPlaceholder": "Search vendor, filament, material, location, lot #",
192+
"description": "Select spools to print labels for.",
193+
"searchPlaceholder": "Search vendor, name, material, lot #",
160194
"showArchived": "Show Archived",
161195
"noSpoolsSelected": "You have not selected any spools.",
162196
"selectAll": "Select/Unselect All",
@@ -165,7 +199,7 @@
165199
},
166200
"filamentSelect": {
167201
"title": "Select Filaments",
168-
"description": "Select filaments to export or print labels for.",
202+
"description": "Select filaments to print labels for.",
169203
"searchPlaceholder": "Search vendor, name, material, article #",
170204
"noFilamentsSelected": "You have not selected any filaments.",
171205
"selectAll": "Select/Unselect All",
@@ -307,14 +341,27 @@
307341
"vendor": "Manufacturers",
308342
"fields": {
309343
"id": "ID",
344+
"logo": "Logo",
345+
"logo_url": "Logo URL",
346+
"print_logo_url": "Print Logo URL",
347+
"logo_preview": "Logo Preview",
348+
"print_logo_preview": "Print Logo Preview",
310349
"name": "Name",
311350
"empty_spool_weight": "Empty Spool Weight",
312351
"external_id": "External ID",
313352
"registered": "Registered",
314-
"comment": "Comment"
353+
"comment": "Comment",
354+
"logo_suggestions": "Logo Suggestions",
355+
"print_logo_suggestions": "Print Logo Suggestions",
356+
"logo_suggestions_placeholder": "Select a suggested logo path",
357+
"logo_suggestions_none": "None"
315358
},
316359
"fields_help": {
317-
"empty_spool_weight": "The weight of an empty spool from this manufacturer."
360+
"empty_spool_weight": "The weight of an empty spool from this manufacturer.",
361+
"logo_url": "Optional custom logo used in the UI. Supports absolute URLs or local paths like /vendor-logos/web/bambu-lab-web.png.",
362+
"print_logo_url": "Optional custom logo used for label rendering. Supports absolute URLs or local paths like /vendor-logos/print/bambu-lab.png.",
363+
"logo_suggestions": "Checks the logo database for files with names similar to this manufacturer.",
364+
"print_logo_suggestions": "Checks the print-logo database for files with names similar to this manufacturer."
318365
},
319366
"titles": {
320367
"create": "Create Manufacturer",
@@ -324,8 +371,21 @@
324371
"show": "Show Manufacturer",
325372
"show_title": "[Manufacturer #{{id}}] {{name}}"
326373
},
374+
"buttons": {
375+
"sync_logos": "Sync Logos",
376+
"clear_logo_url": "Clear URL",
377+
"convert_logo_to_print": "Convert Logo to Print",
378+
"convert_logo_to_print_help": "Creates a black-and-white print logo from the current Logo URL and stores it as a separate local print logo file."
379+
},
327380
"form": {
328-
"vendor_updated": "This manufacturer has been updated by someone/something else since you opened this page. Saving will overwrite those changes!"
381+
"vendor_updated": "This manufacturer has been updated by someone/something else since you opened this page. Saving will overwrite those changes!",
382+
"logo_sync_no_match": "No matching logos found for this manufacturer name.",
383+
"logo_sync_applied": "Suggested logo paths applied.",
384+
"logo_preview_auto_notice": "using auto-matched logo from bundled logo pack",
385+
"logo_preview_default_notice": "no logo defined, using default generated text logo",
386+
"logo_convert_requires_web_logo": "Set a Logo URL first.",
387+
"logo_convert_success": "Generated print logo from web logo.",
388+
"logo_convert_error": "Could not generate a print logo from this Logo URL."
329389
}
330390
},
331391
"home": {
@@ -360,6 +420,16 @@
360420
"round_prices": {
361421
"label": "Round prices",
362422
"tooltip": "Round prices to the nearest whole number."
423+
},
424+
"logo_sync": {
425+
"title": "Global Manufacturer Logo Sync",
426+
"description": "Syncs missing logo URLs across the whole manufacturer database using the bundled logo manifest. Existing logo URLs are not overwritten.",
427+
"where": "Source: /vendor-logos/manifest.json (bundled static web/print logo files). Matching ignores minor punctuation/case differences in names.",
428+
"button": "Sync Logos Now",
429+
"not_ready": "Logo manifest is not available yet.",
430+
"load_error": "Could not load manufacturers for logo sync.",
431+
"done": "Logo sync complete. Matched {{matched}}, updated {{updated}}.",
432+
"scope_note": "This action syncs the whole manufacturer database. You can sync a single manufacturer from its Edit page."
363433
}
364434
},
365435
"extra_fields": {

client/src/components/otherModels.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Tooltip } from "antd";
33
import { ColumnFilterItem } from "antd/es/table/interface";
44
import { IFilament } from "../pages/filaments/model";
55
import { IVendor } from "../pages/vendors/model";
6-
import { getAPIURL } from "../utils/url";
6+
import { getAPIURL, getBasePath } from "../utils/url";
77

88
export function useSpoolmanFilamentFilter(enabled: boolean = false) {
99
return useQuery<IFilament[], unknown, ColumnFilterItem[]>({
@@ -134,6 +134,27 @@ export function useSpoolmanVendors(enabled: boolean = false) {
134134
});
135135
}
136136

137+
export function useSpoolmanVendorExternalIds(enabled: boolean = false) {
138+
return useQuery<IVendor[], unknown, string[]>({
139+
enabled: enabled,
140+
queryKey: ["vendorExternalIds"],
141+
queryFn: async () => {
142+
const response = await fetch(getAPIURL() + "/vendor");
143+
if (!response.ok) {
144+
throw new Error("Network response was not ok");
145+
}
146+
return response.json();
147+
},
148+
select: (data) => {
149+
const externalIds = data
150+
.map((vendor) => vendor.external_id)
151+
.filter((externalId): externalId is string => externalId !== null && externalId !== undefined && externalId !== "")
152+
.sort();
153+
return [...new Set(externalIds)];
154+
},
155+
});
156+
}
157+
137158
export function useSpoolmanMaterials(enabled: boolean = false) {
138159
return useQuery<string[]>({
139160
enabled: enabled,
@@ -201,3 +222,22 @@ export function useSpoolmanLocations(enabled: boolean = false) {
201222
},
202223
});
203224
}
225+
226+
interface VendorLogoManifest {
227+
web_files?: string[];
228+
print_files?: string[];
229+
}
230+
231+
export function useVendorLogoManifest(enabled: boolean = true) {
232+
return useQuery<VendorLogoManifest>({
233+
enabled,
234+
queryKey: ["vendor-logo-manifest"],
235+
queryFn: async () => {
236+
const response = await fetch(`${getBasePath()}/vendor-logos/manifest.json`, { cache: "no-store" });
237+
if (!response.ok) {
238+
return { web_files: [], print_files: [] };
239+
}
240+
return response.json();
241+
},
242+
});
243+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { CSSProperties, useEffect, useMemo, useState } from "react";
2+
import { IVendor } from "../pages/vendors/model";
3+
import { getVendorLogoCandidates } from "../utils/vendorLogo";
4+
5+
interface VendorLogoProps {
6+
vendor?: IVendor;
7+
usePrintLogo?: boolean;
8+
showFallbackText?: boolean;
9+
imgStyle?: CSSProperties;
10+
fallbackStyle?: CSSProperties;
11+
}
12+
13+
export function VendorLogo({
14+
vendor,
15+
usePrintLogo = false,
16+
showFallbackText = false,
17+
imgStyle,
18+
fallbackStyle,
19+
}: VendorLogoProps) {
20+
const candidates = useMemo(() => getVendorLogoCandidates(vendor, usePrintLogo), [vendor, usePrintLogo]);
21+
const [currentCandidateIndex, setCurrentCandidateIndex] = useState(0);
22+
useEffect(() => {
23+
setCurrentCandidateIndex(0);
24+
}, [vendor, usePrintLogo]);
25+
26+
const currentSrc = candidates[currentCandidateIndex];
27+
const fallbackText = vendor?.name ?? "";
28+
29+
if (currentSrc) {
30+
return (
31+
<img
32+
src={currentSrc}
33+
alt={vendor?.name ? `${vendor.name} logo` : "Manufacturer logo"}
34+
style={imgStyle}
35+
onError={() => {
36+
setCurrentCandidateIndex((idx) => idx + 1);
37+
}}
38+
/>
39+
);
40+
}
41+
42+
if (showFallbackText && fallbackText) {
43+
return <div style={fallbackStyle}>{fallbackText}</div>;
44+
}
45+
46+
return null;
47+
}
48+
49+
export default VendorLogo;

0 commit comments

Comments
 (0)