mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
Post work PR #262
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
cd frontend
|
||||
npx tsc
|
||||
npm run lint
|
||||
npm run test:run
|
||||
cd ..
|
||||
|
||||
Reference in New Issue
Block a user