mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 15:35:39 +02:00
Fix multi-plate thumbnail display in queue and add archive plate browsing
- Queue now shows correct plate thumbnail based on selected plate_id (previously always showed plate 1 regardless of selection) - Archive cards now support plate browsing for multi-plate 3MF files: - Left/right arrows to cycle through plates on hover - Dot indicators at bottom show current plate (clickable) - Lazy-loads plate data only when user hovers - Added getArchivePlateThumbnail and getLibraryFilePlateThumbnail API methods Closes #166
This commit is contained in:
@@ -5,6 +5,11 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
## [0.1.6-final] - Not released
|
||||
|
||||
### New Features
|
||||
- **Archive Plate Browsing** - Browse plate thumbnails directly in archive cards (Issue #166):
|
||||
- Hover over archive card to reveal plate navigation for multi-plate files
|
||||
- Left/right arrows to cycle through plate thumbnails
|
||||
- Dot indicators show current plate (clickable to jump to specific plate)
|
||||
- Lazy-loads plate data only when user hovers
|
||||
- **GitHub Profile Backup** - Automatically backup your Cloud profiles, K-profiles and settings to a GitHub repository:
|
||||
- Configure GitHub repository URL and Personal Access Token
|
||||
- Schedule backups hourly, daily, or weekly
|
||||
@@ -84,6 +89,9 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- Tri-state toggles: unchanged / on / off for each setting
|
||||
|
||||
### Fixes
|
||||
- **Multi-Plate Thumbnail in Queue** - Fixed queue items showing wrong thumbnail for multi-plate files (Issue #166):
|
||||
- Queue now displays the correct plate thumbnail based on selected plate
|
||||
- Previously always showed plate 1 thumbnail regardless of selection
|
||||
- **HMS Error Notifications** - Get notified when printer errors occur (Issue #84):
|
||||
- Automatic notifications for HMS errors (AMS issues, nozzle problems, etc.)
|
||||
- Human-readable error messages (853 error codes translated)
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
- Photo attachments & failure analysis
|
||||
- Timelapse editor (trim, speed, music)
|
||||
- Re-print to any connected printer with AMS mapping (auto-match or manual slot selection, multi-plate support)
|
||||
- Plate thumbnail browsing for multi-plate archives (hover to navigate between plates)
|
||||
- Archive comparison (side-by-side diff)
|
||||
|
||||
### 📊 Monitoring & Control
|
||||
|
||||
@@ -261,4 +261,44 @@ describe('ArchivesPage', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('plate navigation', () => {
|
||||
it('renders archive cards with thumbnails', async () => {
|
||||
render(<ArchivesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
// Archive cards should render with their thumbnails
|
||||
expect(screen.getByText('Benchy')).toBeInTheDocument();
|
||||
// Thumbnail images should be present (archive cards have img elements)
|
||||
const images = document.querySelectorAll('img[alt="Benchy"]');
|
||||
expect(images.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches plate data for multi-plate archives on hover', async () => {
|
||||
// Setup handler for plates endpoint
|
||||
server.use(
|
||||
http.get('/api/v1/archives/:id/plates', ({ params }) => {
|
||||
return HttpResponse.json({
|
||||
archive_id: Number(params.id),
|
||||
filename: 'test.3mf',
|
||||
plates: [
|
||||
{ index: 0, name: 'Plate 1', objects: ['Object A'], has_thumbnail: true, thumbnail_url: '/thumb1.png', print_time_seconds: 3600, filament_used_grams: 10, filaments: [] },
|
||||
{ index: 1, name: 'Plate 2', objects: ['Object B'], has_thumbnail: true, thumbnail_url: '/thumb2.png', print_time_seconds: 1800, filament_used_grams: 5, filaments: [] },
|
||||
],
|
||||
is_multi_plate: true,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
render(<ArchivesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Benchy')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Archives with multi-plate support will show navigation on hover
|
||||
// The plates API is called lazily when hovering
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -215,6 +215,27 @@ describe('QueuePage', () => {
|
||||
expect(printerElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders queue items with plate_id correctly', async () => {
|
||||
// Override with queue items that have plate_id set
|
||||
server.use(
|
||||
http.get('/api/v1/queue/', () => {
|
||||
return HttpResponse.json([
|
||||
{
|
||||
...mockQueueItems[0],
|
||||
plate_id: 2,
|
||||
archive_name: 'Multi-plate Print',
|
||||
},
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
render(<QueuePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Multi-plate Print')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty state', () => {
|
||||
|
||||
@@ -1984,6 +1984,8 @@ export const api = {
|
||||
method: 'POST',
|
||||
}),
|
||||
getArchiveThumbnail: (id: number) => `${API_BASE}/archives/${id}/thumbnail?v=${Date.now()}`,
|
||||
getArchivePlateThumbnail: (id: number, plateIndex: number) =>
|
||||
`${API_BASE}/archives/${id}/plate-thumbnail/${plateIndex}`,
|
||||
getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`,
|
||||
getArchiveGcode: (id: number) => `${API_BASE}/archives/${id}/gcode`,
|
||||
getArchivePlatePreview: (id: number) => `${API_BASE}/archives/${id}/plate-preview`,
|
||||
@@ -3041,6 +3043,8 @@ export const api = {
|
||||
request<{ status: string; message: string }>(`/library/files/${id}`, { method: 'DELETE' }),
|
||||
getLibraryFileDownloadUrl: (id: number) => `${API_BASE}/library/files/${id}/download`,
|
||||
getLibraryFileThumbnailUrl: (id: number) => `${API_BASE}/library/files/${id}/thumbnail`,
|
||||
getLibraryFilePlateThumbnail: (id: number, plateIndex: number) =>
|
||||
`${API_BASE}/library/files/${id}/plate-thumbnail/${plateIndex}`,
|
||||
getLibraryFileGcodeUrl: (id: number) => `${API_BASE}/library/files/${id}/gcode`,
|
||||
moveLibraryFiles: (fileIds: number[], folderId: number | null) =>
|
||||
request<{ status: string; moved: number }>('/library/files/move', {
|
||||
|
||||
@@ -41,6 +41,8 @@ import {
|
||||
GitCompare,
|
||||
Loader2,
|
||||
FolderKanban,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from 'lucide-react';
|
||||
import { api } from '../api/client';
|
||||
import { openInSlicer } from '../utils/slicer';
|
||||
@@ -133,9 +135,23 @@ function ArchiveCard({
|
||||
const [showDeleteSource3mfConfirm, setShowDeleteSource3mfConfirm] = useState(false);
|
||||
const [showDeleteF3dConfirm, setShowDeleteF3dConfirm] = useState(false);
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
||||
const [currentPlateIndex, setCurrentPlateIndex] = useState<number | null>(null);
|
||||
const [showPlateNav, setShowPlateNav] = useState(false);
|
||||
const source3mfInputRef = useRef<HTMLInputElement>(null);
|
||||
const f3dInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Fetch plates data for multi-plate browsing (lazy - only when hovering)
|
||||
const { data: platesData } = useQuery({
|
||||
queryKey: ['archive-plates', archive.id],
|
||||
queryFn: () => api.getArchivePlates(archive.id),
|
||||
enabled: showPlateNav, // Only fetch when user hovers to see navigation
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
});
|
||||
|
||||
const plates = platesData?.plates ?? [];
|
||||
const isMultiPlate = platesData?.is_multi_plate ?? false;
|
||||
const displayPlateIndex = currentPlateIndex ?? 0;
|
||||
|
||||
const source3mfUploadMutation = useMutation({
|
||||
mutationFn: (file: File) => api.uploadSource3mf(archive.id, file),
|
||||
onSuccess: (data) => {
|
||||
@@ -505,11 +521,19 @@ function ArchiveCard({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Thumbnail */}
|
||||
<div className="aspect-video bg-bambu-dark relative flex-shrink-0 overflow-hidden rounded-t-xl">
|
||||
{/* Thumbnail with plate navigation */}
|
||||
<div
|
||||
className="aspect-video bg-bambu-dark relative flex-shrink-0 overflow-hidden rounded-t-xl"
|
||||
onMouseEnter={() => setShowPlateNav(true)}
|
||||
onMouseLeave={() => setShowPlateNav(false)}
|
||||
>
|
||||
{archive.thumbnail_path ? (
|
||||
<img
|
||||
src={api.getArchiveThumbnail(archive.id)}
|
||||
src={
|
||||
currentPlateIndex !== null && plates.length > 0
|
||||
? api.getArchivePlateThumbnail(archive.id, plates[displayPlateIndex]?.index ?? 0)
|
||||
: api.getArchiveThumbnail(archive.id)
|
||||
}
|
||||
alt={archive.print_name || archive.filename}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
@@ -518,6 +542,63 @@ function ArchiveCard({
|
||||
<Image className="w-12 h-12 text-bambu-dark-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
{/* Plate navigation - only show for multi-plate archives */}
|
||||
{isMultiPlate && plates.length > 1 && (
|
||||
<>
|
||||
{/* Left arrow */}
|
||||
<button
|
||||
className={`absolute left-1 top-1/2 -translate-y-1/2 p-1 rounded-full bg-black/60 hover:bg-black/80 transition-all ${
|
||||
isMobile ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setCurrentPlateIndex((prev) => {
|
||||
const current = prev ?? 0;
|
||||
return current > 0 ? current - 1 : plates.length - 1;
|
||||
});
|
||||
}}
|
||||
title="Previous plate"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 text-white" />
|
||||
</button>
|
||||
{/* Right arrow */}
|
||||
<button
|
||||
className={`absolute right-1 top-1/2 -translate-y-1/2 p-1 rounded-full bg-black/60 hover:bg-black/80 transition-all ${
|
||||
isMobile ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setCurrentPlateIndex((prev) => {
|
||||
const current = prev ?? 0;
|
||||
return current < plates.length - 1 ? current + 1 : 0;
|
||||
});
|
||||
}}
|
||||
title="Next plate"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4 text-white" />
|
||||
</button>
|
||||
{/* Dots indicator */}
|
||||
<div
|
||||
className={`absolute bottom-1 left-1/2 -translate-x-1/2 flex gap-1 px-2 py-1 rounded-full bg-black/50 transition-all ${
|
||||
isMobile ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
{plates.map((plate, idx) => (
|
||||
<button
|
||||
key={plate.index}
|
||||
className={`w-2 h-2 rounded-full transition-colors ${
|
||||
idx === displayPlateIndex ? 'bg-bambu-green' : 'bg-white/50 hover:bg-white/80'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setCurrentPlateIndex(idx);
|
||||
}}
|
||||
title={plate.name || `Plate ${plate.index}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* Context menu button - visible on mobile, shows on hover for desktop */}
|
||||
<button
|
||||
className={`absolute top-2 left-2 p-1.5 rounded bg-black/50 hover:bg-black/70 transition-all ${
|
||||
|
||||
@@ -344,17 +344,25 @@ function SortableQueueItem({
|
||||
<div className="w-8" />
|
||||
)}
|
||||
|
||||
{/* Thumbnail */}
|
||||
{/* Thumbnail - use plate-specific thumbnail if plate_id is set */}
|
||||
<div className="w-14 h-14 flex-shrink-0 bg-bambu-dark rounded-lg overflow-hidden">
|
||||
{item.archive_thumbnail ? (
|
||||
<img
|
||||
src={api.getArchiveThumbnail(item.archive_id!)}
|
||||
src={
|
||||
item.plate_id != null
|
||||
? api.getArchivePlateThumbnail(item.archive_id!, item.plate_id)
|
||||
: api.getArchiveThumbnail(item.archive_id!)
|
||||
}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : item.library_file_thumbnail ? (
|
||||
<img
|
||||
src={api.getLibraryFileThumbnailUrl(item.library_file_id!)}
|
||||
src={
|
||||
item.plate_id != null
|
||||
? api.getLibraryFilePlateThumbnail(item.library_file_id!, item.plate_id)
|
||||
: api.getLibraryFileThumbnailUrl(item.library_file_id!)
|
||||
}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
|
||||
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-DuO_7tQ3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-nwJjDqT-.css">
|
||||
<script type="module" crossorigin src="/assets/index-abyjQYWw.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DFuUL8IF.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user