Post work PR #262

This commit is contained in:
maziggy
2026-02-04 15:54:34 +01:00
parent 6420e9d7ad
commit 6a35e945ce
11 changed files with 251 additions and 138 deletions
+8 -1
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { WebGLPreview } from 'gcode-preview';
import { Loader2, Layers, ChevronLeft, ChevronRight, FileWarning } from 'lucide-react';
import { getAuthToken } from '../api/client';
interface GcodeViewerProps {
gcodeUrl: string;
@@ -63,7 +64,13 @@ export function GcodeViewer({
previewRef.current = preview;
// Fetch and process gcode
fetch(gcodeUrl)
const headers: HeadersInit = {};
const token = getAuthToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
fetch(gcodeUrl, { headers })
.then(async response => {
if (!response.ok) {
if (response.status === 404) {
+17 -7
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
@@ -6,6 +7,7 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
import JSZip from 'jszip';
import { Loader2, RotateCcw, ZoomIn, ZoomOut } from 'lucide-react';
import { Button } from './Button';
import { getAuthToken } from '../api/client';
interface BuildVolume {
x: number;
@@ -585,6 +587,7 @@ export function ModelViewer({
selectedPlateId = null,
className = '',
}: ModelViewerProps) {
const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement>(null);
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
const sceneRef = useRef<THREE.Scene | null>(null);
@@ -677,10 +680,17 @@ export function ModelViewer({
const normalizedType = (fileType || url.split('?')[0].split('.').pop() || '').toLowerCase();
// Build auth headers for fetch
const headers: HeadersInit = {};
const token = getAuthToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
if (normalizedType === 'stl') {
fetch(url)
fetch(url, { headers })
.then((res) => {
if (!res.ok) throw new Error('Failed to load file');
if (!res.ok) throw new Error(t('modelViewer.errors.failedToLoad'));
return res.arrayBuffer();
})
.then((buffer) => {
@@ -695,15 +705,15 @@ export function ModelViewer({
setLoading(false);
});
} else if (normalizedType === '3mf') {
fetch(url)
fetch(url, { headers })
.then((res) => {
if (!res.ok) throw new Error('Failed to load file');
if (!res.ok) throw new Error(t('modelViewer.errors.failedToLoad'));
return res.arrayBuffer();
})
.then(parse3MF)
.then((parsed) => {
if (parsed.objects.size === 0) {
throw new Error('No meshes found in 3MF file');
throw new Error(t('modelViewer.errors.noMeshes'));
}
setParsedData(parsed);
})
@@ -712,7 +722,7 @@ export function ModelViewer({
setLoading(false);
});
} else {
setError('Unsupported file format');
setError(t('modelViewer.errors.unsupportedFormat'));
setLoading(false);
}
@@ -743,7 +753,7 @@ export function ModelViewer({
plateRef.current = null;
gridRef.current = null;
};
}, [url, buildVolume, fileType]);
}, [url, buildVolume, fileType, t]);
useEffect(() => {
if (!sceneRef.current || !cameraRef.current || !controlsRef.current) return;
+21 -22
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { X, ExternalLink, Box, Code2, Loader2, Layers, Check, Maximize2, Minimize2 } from 'lucide-react';
import { ModelViewer } from './ModelViewer';
import { GcodeViewer } from './GcodeViewer';
@@ -26,6 +27,7 @@ interface Capabilities {
}
export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, onClose }: ModelViewerModalProps) {
const { t } = useTranslation();
const isLibrary = libraryFileId != null;
const [activeTab, setActiveTab] = useState<ViewTab | null>(null);
const [capabilities, setCapabilities] = useState<Capabilities | null>(null);
@@ -139,7 +141,7 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
const getPlateObjectCount = (plate: PlateMetadata): number => plate.object_count ?? plate.objects?.length ?? 0;
const totalObjectCount = plates.reduce((sum, plate) => sum + getPlateObjectCount(plate), 0);
const selectedObjectCount = selectedPlate ? getPlateObjectCount(selectedPlate) : totalObjectCount;
const objectCountLabel = selectedPlate ? `Plate ${selectedPlate.index}` : 'All plates';
const objectCountLabel = selectedPlate ? t('modelViewer.plateNumber', { number: selectedPlate.index }) : t('modelViewer.allPlates');
const hasObjectCount = plates.length > 0;
const platesGridRef = useRef<HTMLDivElement>(null);
const platesViewportRef = useRef<HTMLDivElement>(null);
@@ -288,14 +290,14 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
<h2 className="text-lg font-semibold text-white truncate">{title}</h2>
{hasObjectCount && (
<span className="text-xs text-bambu-gray bg-bambu-dark-tertiary/70 px-2 py-1 rounded whitespace-nowrap">
{objectCountLabel}: {selectedObjectCount} object{selectedObjectCount !== 1 ? 's' : ''}
{objectCountLabel}: {t('modelViewer.objectCount', { count: selectedObjectCount })}
</span>
)}
</div>
<div className="flex items-center gap-2">
<Button variant="secondary" size="sm" onClick={handleOpenInSlicer} disabled={!canOpenInSlicer}>
<ExternalLink className="w-4 h-4" />
Open in Slicer
{t('modelViewer.openInSlicer')}
</Button>
<Button
variant="secondary"
@@ -326,8 +328,8 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
}`}
>
<Box className="w-4 h-4" />
3D Model
{!capabilities.has_model && <span className="text-xs">(not available)</span>}
{t('modelViewer.tabs.model')}
{!capabilities.has_model && <span className="text-xs">({t('modelViewer.notAvailable')})</span>}
</button>
<button
onClick={() => capabilities.has_gcode && setActiveTab('gcode')}
@@ -341,8 +343,8 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
}`}
>
<Code2 className="w-4 h-4" />
G-code Preview
{!capabilities.has_gcode && <span className="text-xs">(not sliced)</span>}
{t('modelViewer.tabs.gcode')}
{!capabilities.has_gcode && <span className="text-xs">({t('modelViewer.notSliced')})</span>}
</button>
</div>
)}
@@ -366,7 +368,7 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
>
<div className="flex items-center gap-2 text-sm text-bambu-gray mb-2">
<Layers className="w-4 h-4" />
Plates
{t('modelViewer.plates')}
{platesLoading && <Loader2 className="w-3 h-3 animate-spin" />}
</div>
<div className={splitFullscreen ? 'flex flex-col min-h-0 flex-1' : undefined}>
@@ -396,9 +398,9 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
<Layers className={`${splitFullscreen ? 'w-4 h-4' : 'w-5 h-5'} text-bambu-gray`} />
</div>
<div className="min-w-0 flex-1">
<p className={`${splitFullscreen ? 'text-xs' : 'text-sm'} text-white font-medium truncate`}>All Plates</p>
<p className={`${splitFullscreen ? 'text-xs' : 'text-sm'} text-white font-medium truncate`}>{t('modelViewer.allPlates')}</p>
<p className={`${splitFullscreen ? 'text-[10px]' : 'text-xs'} text-bambu-gray truncate`}>
{plates.length} plate{plates.length !== 1 ? 's' : ''}
{t('modelViewer.plateCount', { count: plates.length })}
</p>
</div>
{selectedPlateId == null && (
@@ -433,13 +435,10 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
)}
<div className="min-w-0 flex-1">
<p className={`${splitFullscreen ? 'text-xs' : 'text-sm'} text-white font-medium truncate`}>
{plate.name || `Plate ${plate.index}`}
{plate.name || t('modelViewer.plateNumber', { number: plate.index })}
</p>
<p className={`${splitFullscreen ? 'text-[10px]' : 'text-xs'} text-bambu-gray truncate`}>
{(() => {
const objectCount = plate.object_count ?? plate.objects?.length ?? 0;
return `${objectCount} object${objectCount !== 1 ? 's' : ''}`;
})()}
{t('modelViewer.objectCount', { count: plate.object_count ?? plate.objects?.length ?? 0 })}
</p>
</div>
{selectedPlateId === plate.index && (
@@ -453,21 +452,21 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
<div className="mt-auto pt-3 flex items-center gap-4 text-xs text-bambu-gray overflow-x-auto">
{selectedPlate && (
<div className="flex items-center gap-3 whitespace-nowrap">
<span>Plate {selectedPlate.index}</span>
<span>{t('modelViewer.plateNumber', { number: selectedPlate.index })}</span>
{selectedPlate.print_time_seconds != null && (
<span>ETA {Math.round(selectedPlate.print_time_seconds / 60)} min</span>
<span>{t('modelViewer.eta', { minutes: Math.round(selectedPlate.print_time_seconds / 60) })}</span>
)}
{selectedPlate.filament_used_grams != null && (
<span>{selectedPlate.filament_used_grams.toFixed(1)} g</span>
)}
{selectedPlate.filaments.length > 0 && (
<span>{selectedPlate.filaments.length} filament{selectedPlate.filaments.length !== 1 ? 's' : ''}</span>
<span>{t('modelViewer.filamentCount', { count: selectedPlate.filaments.length })}</span>
)}
</div>
)}
{shouldPaginatePlates && (
<div className={`flex items-center gap-2 whitespace-nowrap ${selectedPlate ? 'ml-auto' : ''}`}>
<span>Page {platePage + 1} of {totalPlatePages}</span>
<span>{t('modelViewer.pagination.pageOf', { current: platePage + 1, total: totalPlatePages })}</span>
<div className="flex items-center gap-1">
<button
type="button"
@@ -479,7 +478,7 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
: 'border-bambu-dark-tertiary text-bambu-gray hover:text-white hover:border-bambu-gray'
}`}
>
Prev
{t('modelViewer.pagination.prev')}
</button>
{(() => {
const maxVisible = 5;
@@ -547,7 +546,7 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
: 'border-bambu-dark-tertiary text-bambu-gray hover:text-white hover:border-bambu-gray'
}`}
>
Next
{t('modelViewer.pagination.next')}
</button>
</div>
</div>
@@ -596,7 +595,7 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
/>
) : (
<div className="w-full h-full flex items-center justify-center text-bambu-gray">
No preview available for this file
{t('modelViewer.noPreview')}
</div>
)}
</div>
+32
View File
@@ -2543,6 +2543,38 @@ export default {
},
},
// Model Viewer
modelViewer: {
openInSlicer: 'Im Slicer öffnen',
tabs: {
model: '3D-Modell',
gcode: 'G-Code Vorschau',
},
notAvailable: 'nicht verfügbar',
notSliced: 'nicht geslicet',
plates: 'Platten',
allPlates: 'Alle Platten',
plateNumber: 'Platte {{number}}',
plateCount: '{{count}} Platte',
plateCount_other: '{{count}} Platten',
objectCount: '{{count}} Objekt',
objectCount_other: '{{count}} Objekte',
filamentCount: '{{count}} Filament',
filamentCount_other: '{{count}} Filamente',
eta: 'ETA {{minutes}} Min',
noPreview: 'Keine Vorschau für diese Datei verfügbar',
pagination: {
pageOf: 'Seite {{current}} von {{total}}',
prev: 'Zurück',
next: 'Weiter',
},
errors: {
failedToLoad: 'Datei konnte nicht geladen werden',
noMeshes: 'Keine Meshes in 3MF-Datei gefunden',
unsupportedFormat: 'Nicht unterstütztes Dateiformat',
},
},
// Maintenance type descriptions (built-in)
maintenanceDescriptions: {
lubricateRails: 'Schmiermittel auf Linearschienen für sanfte Bewegung auftragen',
+32
View File
@@ -2543,6 +2543,38 @@ export default {
},
},
// Model Viewer
modelViewer: {
openInSlicer: 'Open in Slicer',
tabs: {
model: '3D Model',
gcode: 'G-code Preview',
},
notAvailable: 'not available',
notSliced: 'not sliced',
plates: 'Plates',
allPlates: 'All Plates',
plateNumber: 'Plate {{number}}',
plateCount: '{{count}} plate',
plateCount_other: '{{count}} plates',
objectCount: '{{count}} object',
objectCount_other: '{{count}} objects',
filamentCount: '{{count}} filament',
filamentCount_other: '{{count}} filaments',
eta: 'ETA {{minutes}} min',
noPreview: 'No preview available for this file',
pagination: {
pageOf: 'Page {{current}} of {{total}}',
prev: 'Prev',
next: 'Next',
},
errors: {
failedToLoad: 'Failed to load file',
noMeshes: 'No meshes found in 3MF file',
unsupportedFormat: 'Unsupported file format',
},
},
// Maintenance type descriptions (built-in)
maintenanceDescriptions: {
lubricateRails: 'Apply lubricant to linear rails for smooth motion',
+32
View File
@@ -3074,4 +3074,36 @@ export default {
failedToCreateBackup: 'バックアップの作成に失敗しました',
backupRestored: 'バックアップの復元が完了しました',
},
// Model Viewer
modelViewer: {
openInSlicer: 'スライサーで開く',
tabs: {
model: '3Dモデル',
gcode: 'G-codeプレビュー',
},
notAvailable: '利用不可',
notSliced: '未スライス',
plates: 'プレート',
allPlates: '全プレート',
plateNumber: 'プレート {{number}}',
plateCount: '{{count}} プレート',
plateCount_other: '{{count}} プレート',
objectCount: '{{count}} オブジェクト',
objectCount_other: '{{count}} オブジェクト',
filamentCount: '{{count}} フィラメント',
filamentCount_other: '{{count}} フィラメント',
eta: '予想時間 {{minutes}} 分',
noPreview: 'このファイルのプレビューは利用できません',
pagination: {
pageOf: 'ページ {{current}} / {{total}}',
prev: '前へ',
next: '次へ',
},
errors: {
failedToLoad: 'ファイルの読み込みに失敗しました',
noMeshes: '3MFファイルにメッシュが見つかりません',
unsupportedFormat: 'サポートされていないファイル形式です',
},
},
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -23,8 +23,8 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-zuNzNq45.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-qXxId8sa.css">
<script type="module" crossorigin src="/assets/index-mqcja8gj.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CosC5iN4.css">
</head>
<body>
<div id="root"></div>
+1
View File
@@ -1,6 +1,7 @@
#!/bin/sh
cd frontend
npx tsc
npm run lint
npm run test:run
cd ..