import { useState, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import type { TFunction } from 'i18next'; import { Search, Filter, RefreshCw, Droplet, Settings2, Printer as PrinterIcon, Layers, X, Loader2, Clock } from 'lucide-react'; import { api } from '../api/client'; import type { OrcaProfileListResponse, OrcaProfileMeta, Printer } from '../api/client'; import { Button } from './Button'; import { FilterDropdown } from '../pages/ProfilesPage'; import { formatRelativeTime } from '../utils/date'; /** * Read-only profile browser for the Orca Cloud tab. * * Visual parity with Bambu Cloud's CloudProfilesView: same filter bar layout, * same 3-column grouped list (Filament / Process / Printer), same card * styling. Differences vs the Bambu version are removals — no * Create / Edit / Duplicate / Delete / Compare / Templates buttons because * Orca's `/sync/push` and `/sync/delete` endpoints aren't wired in this * shipping cut (planned for a follow-up). The "Owner" filter is also * dropped: every profile that lives in a user's Orca Cloud account is * user-authored, the system/builtin distinction doesn't apply. */ type ProfileType = 'all' | 'filament' | 'printer' | 'process'; interface PresetMeta { printer: string | null; nozzle: string | null; layerHeight: string | null; filamentType: string | null; } // Mirror of ProfilesPage.tsx::extractMetadata. Inlined rather than exported // because the patterns are conservative (Bambu printer-name regex etc.) and // Orca profile naming follows the same conventions (Orca is a BambuStudio // fork; profile names like "Bambu PLA Basic @BBL X1C" carry over verbatim). function extractMetadata(name: string): PresetMeta { const printerMatch = name.match(/@?\s*(?:BBL\s+)?(?:Bambu\s+Lab\s+)?([XPAH][1-9][A-Z]?(?:\s*(?:Carbon|mini))?|H2D)/i); const nozzleMatch = name.match(/(\d+\.?\d*)\s*(?:mm\s*)?nozzle|nozzle\s*(\d+\.?\d*)/i); const layerMatch = name.match(/(\d+\.?\d*)mm\s*(?:Standard|Fine|Extra Fine|Draft|Quality)?/i); const filamentMatch = name.match(/\b(PLA|PETG|ABS|ASA|TPU|PC|PA|PVA|HIPS|PP|PET(?:-?CF)?|PA(?:-?CF)?|PLA(?:-?CF)?)\b/i); return { printer: printerMatch ? printerMatch[1].trim() : null, nozzle: nozzleMatch ? (nozzleMatch[1] || nozzleMatch[2]) + 'mm' : null, layerHeight: layerMatch ? layerMatch[1] + 'mm' : null, filamentType: filamentMatch ? filamentMatch[1].toUpperCase() : null, }; } interface OrcaCloudProfilesViewProps { settings: OrcaProfileListResponse; lastSyncTime?: Date; onRefresh: () => void; isRefreshing: boolean; printers: Printer[]; t: TFunction; } export function OrcaCloudProfilesView({ settings, lastSyncTime, onRefresh, isRefreshing, printers, t, }: OrcaCloudProfilesViewProps) { const [searchQuery, setSearchQuery] = useState(''); const [filterType, setFilterType] = useState('all'); const [filterPrinter, setFilterPrinter] = useState('all'); const [filterNozzle, setFilterNozzle] = useState('all'); const [filterFilament, setFilterFilament] = useState('all'); const [filterLayerHeight, setFilterLayerHeight] = useState('all'); const [selectedSetting, setSelectedSetting] = useState(null); const allPresetsWithMeta = useMemo(() => { const combined = [ ...settings.filament.map(s => ({ ...s, type: 'filament' as const })), ...settings.printer.map(s => ({ ...s, type: 'printer' as const })), ...settings.process.map(s => ({ ...s, type: 'process' as const })), ]; return combined.map(s => ({ ...s, meta: extractMetadata(s.name) })); }, [settings]); const filterOptions = useMemo(() => { const nozzles = new Set(); const filaments = new Set(); const layerHeights = new Set(); allPresetsWithMeta.forEach(p => { if (p.meta.nozzle) nozzles.add(p.meta.nozzle); if (p.meta.filamentType) filaments.add(p.meta.filamentType); if (p.meta.layerHeight) layerHeights.add(p.meta.layerHeight); }); return { printers: printers.map(p => ({ id: p.id.toString(), name: p.name })), nozzles: Array.from(nozzles).sort((a, b) => parseFloat(a) - parseFloat(b)), filaments: Array.from(filaments).sort(), layerHeights: Array.from(layerHeights).sort((a, b) => parseFloat(a) - parseFloat(b)), }; }, [allPresetsWithMeta, printers]); const selectedPrinterModel = useMemo(() => { if (filterPrinter === 'all') return null; const printer = printers.find(p => p.id.toString() === filterPrinter); return printer?.model || null; }, [filterPrinter, printers]); const filteredPresets = useMemo(() => { return allPresetsWithMeta .filter(s => filterType === 'all' || s.type === filterType) .filter(s => { if (filterPrinter === 'all' || !selectedPrinterModel) return true; const presetPrinter = s.meta.printer?.toLowerCase() || ''; const configuredModel = selectedPrinterModel.toLowerCase(); return presetPrinter.includes(configuredModel) || configuredModel.includes(presetPrinter); }) .filter(s => filterNozzle === 'all' || s.meta.nozzle === filterNozzle) .filter(s => filterFilament === 'all' || s.meta.filamentType === filterFilament) .filter(s => filterLayerHeight === 'all' || s.meta.layerHeight === filterLayerHeight) .filter(s => searchQuery === '' || s.name.toLowerCase().includes(searchQuery.toLowerCase())) .sort((a, b) => a.name.localeCompare(b.name)); }, [allPresetsWithMeta, filterType, filterPrinter, selectedPrinterModel, filterNozzle, filterFilament, filterLayerHeight, searchQuery]); const clearFilters = () => { setFilterType('all'); setFilterPrinter('all'); setFilterNozzle('all'); setFilterFilament('all'); setFilterLayerHeight('all'); setSearchQuery(''); }; const hasActiveFilters = filterType !== 'all' || filterPrinter !== 'all' || filterNozzle !== 'all' || filterFilament !== 'all' || filterLayerHeight !== 'all' || searchQuery !== ''; const totalCount = settings.filament.length + settings.printer.length + settings.process.length; const presetsByType = (type: ProfileType) => filteredPresets.filter(p => p.type === type); return ( <> {/* Search and Filters — mirrors the layout of profiles.cloudView in the Bambu Cloud tab so the two tabs feel like the same surface. */}
setSearchQuery(e.target.value)} placeholder={t('profiles.cloudView.searchPlaceholder')} className="w-full pl-10 pr-4 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray-dark focus:border-bambu-green focus:outline-none" />
setFilterType(v as ProfileType)} /> {filterOptions.printers.length > 0 && ( ({ value: p.id, label: p.name })), ]} onChange={setFilterPrinter} /> )} {filterOptions.nozzles.length > 0 && ( ({ value: n, label: n })), ]} onChange={setFilterNozzle} /> )} {filterOptions.filaments.length > 0 && (filterType === 'all' || filterType === 'filament') && ( ({ value: f, label: f })), ]} onChange={setFilterFilament} /> )} {filterOptions.layerHeights.length > 0 && (filterType === 'all' || filterType === 'process') && ( ({ value: l, label: l })), ]} onChange={setFilterLayerHeight} /> )} {hasActiveFilters && ( )}
{lastSyncTime && (
{t('profiles.cloudView.lastSynced')} {formatRelativeTime(lastSyncTime.toISOString(), 'system', t)}
)} {t('profiles.cloudView.showingCount', { showing: filteredPresets.length, total: totalCount })}
{filteredPresets.length === 0 ? (

{t('profiles.cloudView.noPresetsFound')}

{hasActiveFilters && ( )}
) : (
} title={t('profiles.cloudView.columns.filament')} presets={presetsByType('filament')} emptyText={t('profiles.cloudView.noFilamentPresets')} onSelect={setSelectedSetting} /> } title={t('profiles.cloudView.columns.process')} presets={presetsByType('process')} emptyText={t('profiles.cloudView.noProcessPresets')} onSelect={setSelectedSetting} /> } title={t('profiles.cloudView.columns.printer')} presets={presetsByType('printer')} emptyText={t('profiles.cloudView.noPrinterPresets')} onSelect={setSelectedSetting} />
)} {selectedSetting && ( setSelectedSetting(null)} t={t} /> )} ); } interface PresetColumnProps { icon: React.ReactNode; title: string; presets: (OrcaProfileMeta & { meta: PresetMeta })[]; emptyText: string; onSelect: (preset: OrcaProfileMeta) => void; } function PresetColumn({ icon, title, presets, emptyText, onSelect }: PresetColumnProps) { return (
{icon}

{title}

({presets.length})
{presets.length === 0 ? (

{emptyText}

) : ( presets.map((preset) => ( onSelect(preset)} /> )) )}
); } function PresetCard({ preset, onClick, }: { preset: OrcaProfileMeta & { meta: PresetMeta }; onClick: () => void; }) { return ( ); } interface OrcaPresetDetailModalProps { setting: OrcaProfileMeta; onClose: () => void; t: TFunction; } function OrcaPresetDetailModal({ setting, onClose, t }: OrcaPresetDetailModalProps) { const { data: detail, isLoading, error } = useQuery({ queryKey: ['orcaCloudProfileDetail', setting.setting_id], queryFn: () => api.orcaCloudGetProfile(setting.setting_id), retry: false, }); return (
e.stopPropagation()} >

{setting.name}

{setting.type}

{isLoading ? (
) : error ? (

{(error as Error).message}

) : detail ? (
              {JSON.stringify(detail.setting, null, 2)}
            
) : (

{t('profiles.cloudView.noPresetsFound')}

)}
); }