|
|
@@ -82,14 +82,20 @@ import { formatFileSize } from '../utils/file';
|
|
|
type TFunction = (key: string, options?: Record<string, unknown>) => string;
|
|
|
|
|
|
/**
|
|
|
- * Check if an archive filename represents a sliced/printable file.
|
|
|
- * Matches: .gcode, .gcode.3mf, .gcode.anything
|
|
|
+ * Check if an archive represents a sliced/printable file.
|
|
|
+ * Uses filename (.gcode, .gcode.3mf) as primary check, then falls back to
|
|
|
+ * metadata — a .3mf with total_layers or print_time is sliced (contains gcode),
|
|
|
+ * while a raw source .3mf (CAD export) has neither.
|
|
|
*/
|
|
|
-function isSlicedFile(filename: string | null | undefined): boolean {
|
|
|
- if (!filename) return false;
|
|
|
- const lower = filename.toLowerCase();
|
|
|
- // Match .gcode at end OR .gcode. followed by anything (like .gcode.3mf)
|
|
|
- return lower.endsWith('.gcode') || lower.includes('.gcode.');
|
|
|
+function isSlicedFile(archive: { filename?: string | null; total_layers?: number | null; print_time_seconds?: number | null }): boolean {
|
|
|
+ const filename = archive.filename;
|
|
|
+ if (filename) {
|
|
|
+ const lower = filename.toLowerCase();
|
|
|
+ if (lower.endsWith('.gcode') || lower.includes('.gcode.')) return true;
|
|
|
+ }
|
|
|
+ // .3mf can be either sliced or source — check for gcode metadata
|
|
|
+ if (archive.total_layers || archive.print_time_seconds) return true;
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
function getArchiveFileType(filename: string | null | undefined): string | undefined {
|
|
|
@@ -342,7 +348,7 @@ function ArchiveCard({
|
|
|
setContextMenu({ x: e.clientX, y: e.clientY });
|
|
|
};
|
|
|
|
|
|
- const isGcodeFile = isSlicedFile(archive.filename);
|
|
|
+ const isGcodeFile = isSlicedFile(archive);
|
|
|
|
|
|
const contextMenuItems: ContextMenuItem[] = [
|
|
|
// For gcode files: show Print option
|
|
|
@@ -867,17 +873,17 @@ function ArchiveCard({
|
|
|
{/* File type badge */}
|
|
|
<span
|
|
|
className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
|
|
|
- isSlicedFile(archive.filename)
|
|
|
+ isSlicedFile(archive)
|
|
|
? 'bg-bambu-green/20 text-bambu-green'
|
|
|
: 'bg-orange-500/20 text-orange-400'
|
|
|
}`}
|
|
|
title={
|
|
|
- isSlicedFile(archive.filename)
|
|
|
+ isSlicedFile(archive)
|
|
|
? t('archives.card.slicedFile')
|
|
|
: t('archives.card.sourceFile')
|
|
|
}
|
|
|
>
|
|
|
- {isSlicedFile(archive.filename) ? t('archives.card.gcode') : t('archives.card.source')}
|
|
|
+ {isSlicedFile(archive) ? t('archives.card.gcode') : t('archives.card.source')}
|
|
|
</span>
|
|
|
{archive.project_name && (
|
|
|
<span
|
|
|
@@ -1020,7 +1026,7 @@ function ArchiveCard({
|
|
|
|
|
|
{/* Actions */}
|
|
|
<div className="flex gap-1 mt-3">
|
|
|
- {isSlicedFile(archive.filename) ? (
|
|
|
+ {isSlicedFile(archive) ? (
|
|
|
// Sliced file - can print directly
|
|
|
<>
|
|
|
<Button
|
|
|
@@ -1564,7 +1570,7 @@ function ArchiveListRow({
|
|
|
setContextMenu({ x: e.clientX, y: e.clientY });
|
|
|
};
|
|
|
|
|
|
- const isGcodeFile = isSlicedFile(archive.filename);
|
|
|
+ const isGcodeFile = isSlicedFile(archive);
|
|
|
|
|
|
const contextMenuItems: ContextMenuItem[] = [
|
|
|
...(isGcodeFile ? [
|
|
|
@@ -1922,7 +1928,7 @@ function ArchiveListRow({
|
|
|
{formatFileSize(archive.file_size)}
|
|
|
</div>
|
|
|
<div className="col-span-2 flex justify-end gap-1">
|
|
|
- {isSlicedFile(archive.filename) && (
|
|
|
+ {isSlicedFile(archive) && (
|
|
|
<Button
|
|
|
variant="ghost"
|
|
|
size="sm"
|
|
|
@@ -2599,7 +2605,7 @@ export function ArchivesPage() {
|
|
|
const matchesTag = !filterTag || archiveTags.includes(filterTag);
|
|
|
|
|
|
// File type filter (gcode = sliced, source = project file only)
|
|
|
- const isGcodeFile = isSlicedFile(a.filename);
|
|
|
+ const isGcodeFile = isSlicedFile(a);
|
|
|
const matchesFileType = filterFileType === 'all' ||
|
|
|
(filterFileType === 'gcode' && isGcodeFile) ||
|
|
|
(filterFileType === 'source' && !isGcodeFile);
|