(null);
+
+ useEffect(() => {
+ if (!open) return;
+ const handlePointerDown = (e: MouseEvent) => {
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
+ setOpen(false);
+ }
+ };
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ e.stopPropagation();
+ setOpen(false);
+ }
+ };
+ document.addEventListener('mousedown', handlePointerDown);
+ document.addEventListener('keydown', handleKeyDown);
+ return () => {
+ document.removeEventListener('mousedown', handlePointerDown);
+ document.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [open]);
+
+ return (
+
+
+
+
+
+ {open && (
+
+ {items.map((item) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, onClose, onSliceWithBambuddy }: ModelViewerModalProps) {
const { t } = useTranslation();
const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: api.getSettings });
@@ -296,27 +388,38 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
isLibrary && settings?.use_slicer_api && onSliceWithBambuddy && sliceableType,
);
- const handleOpenInSlicer = async () => {
+ const slicerDropdownTypes: SlicerType[] = useBambuddySlicer
+ ? ['bambu_studio', 'orcaslicer']
+ : [preferredSlicer === 'orcaslicer' ? 'bambu_studio' : 'orcaslicer'];
+ const slicerName = (slicer: SlicerType) =>
+ slicer === 'orcaslicer' ? t('settings.slicerOrcaSlicer') : t('settings.slicerBambuStudio');
+ const slicerDropdownItems = slicerDropdownTypes.map((slicer) => ({
+ key: slicer,
+ label: t('modelViewer.openInSlicerWith', { slicer: slicerName(slicer) }),
+ onClick: () => handleOpenInSlicer(slicer),
+ }));
+
+ const handleOpenInSlicer = async (slicer: SlicerType) => {
if (!canOpenInSlicer) return;
const filename = title || 'model';
try {
if (isLibrary) {
const { token } = await api.createLibrarySlicerToken(libraryFileId!);
const path = api.getLibrarySlicerDownloadUrl(libraryFileId!, token, filename);
- openInSlicer(`${window.location.origin}${path}`, preferredSlicer);
+ openInSlicer(`${window.location.origin}${path}`, slicer);
} else {
const { token } = await api.createArchiveSlicerToken(archiveId!);
const path = api.getArchiveSlicerDownloadUrl(archiveId!, token, filename);
- openInSlicer(`${window.location.origin}${path}`, preferredSlicer);
+ openInSlicer(`${window.location.origin}${path}`, slicer);
}
} catch {
// Fallback to direct URL (works when auth is disabled)
if (isLibrary) {
const downloadUrl = `${window.location.origin}${api.getLibraryFileDownloadUrl(libraryFileId!)}`;
- openInSlicer(downloadUrl, preferredSlicer);
+ openInSlicer(downloadUrl, slicer);
} else {
const downloadUrl = `${window.location.origin}${api.getArchiveForSlicer(archiveId!, filename)}`;
- openInSlicer(downloadUrl, preferredSlicer);
+ openInSlicer(downloadUrl, slicer);
}
}
};
@@ -344,12 +447,30 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
{useBambuddySlicer ? (
-
+ canOpenInSlicer ? (
+ }
+ label={t('slice.action')}
+ dropdownLabel={t('modelViewer.moreSlicerOptions')}
+ onPrimary={() => onSliceWithBambuddy?.()}
+ items={slicerDropdownItems}
+ />
+ ) : (
+
+ )
+ ) : canOpenInSlicer ? (
+ }
+ label={t('modelViewer.openInSlicer')}
+ dropdownLabel={t('modelViewer.moreSlicerOptions')}
+ onPrimary={() => handleOpenInSlicer(preferredSlicer)}
+ items={slicerDropdownItems}
+ />
) : (
-