ModelSourcesPage.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. import { useSearchParams } from 'react-router-dom';
  2. import { useQuery } from '@tanstack/react-query';
  3. import { useTranslation } from 'react-i18next';
  4. import { Globe, Library } from 'lucide-react';
  5. import { api } from '../api/client';
  6. import { ManyfoldTab } from '../components/ManyfoldTab';
  7. import { useAuth } from '../contexts/AuthContext';
  8. import { MakerworldPage } from './MakerworldPage';
  9. type SourceTab = 'makerworld' | 'manyfold';
  10. /**
  11. * Model Sources (#1471): the places models come into the library from, one tab
  12. * each. MakerWorld takes pasted links; Manyfold is a self-hosted library that
  13. * is browsed. Each tab shows only to users with that source's permission, and
  14. * Manyfold only once it is connected, except to those who can connect it.
  15. */
  16. export function ModelSourcesPage() {
  17. const { t } = useTranslation();
  18. const { hasPermission } = useAuth();
  19. const [searchParams, setSearchParams] = useSearchParams();
  20. const canViewManyfold = hasPermission('manyfold:view');
  21. const manyfoldStatus = useQuery({
  22. queryKey: ['manyfold-status'],
  23. queryFn: () => api.getManyfoldStatus(),
  24. enabled: canViewManyfold,
  25. });
  26. const showManyfold =
  27. canViewManyfold && (manyfoldStatus.data?.configured === true || hasPermission('settings:update'));
  28. const tabs: { id: SourceTab; label: string; icon: typeof Globe }[] = [];
  29. if (hasPermission('makerworld:view')) tabs.push({ id: 'makerworld', label: t('modelSources.tabMakerworld'), icon: Globe });
  30. if (showManyfold) tabs.push({ id: 'manyfold', label: t('modelSources.tabManyfold'), icon: Library });
  31. const requested = searchParams.get('tab');
  32. // A Manyfold-only user, before Manyfold is connected, has no tab; show them
  33. // the Manyfold tab's "not connected" note rather than an empty page.
  34. const active =
  35. tabs.find((tab) => tab.id === requested)?.id ?? tabs[0]?.id ?? (canViewManyfold ? 'manyfold' : undefined);
  36. return (
  37. <div className="p-4 md:p-8 max-w-screen-2xl">
  38. <div className="mb-6">
  39. <h1 className="text-2xl font-bold text-white flex items-center gap-3">
  40. <Globe className="w-7 h-7 text-bambu-green" />
  41. {t('modelSources.title')}
  42. </h1>
  43. </div>
  44. {tabs.length > 1 && (
  45. <div className="flex border-b border-bambu-dark-tertiary mb-6" role="tablist">
  46. {tabs.map(({ id, label, icon: Icon }) => (
  47. <button
  48. key={id}
  49. type="button"
  50. role="tab"
  51. aria-selected={active === id}
  52. onClick={() => setSearchParams({ tab: id }, { replace: true })}
  53. className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors border-b-2 -mb-px ${
  54. active === id
  55. ? 'text-bambu-green border-bambu-green'
  56. : 'text-bambu-gray hover:text-white border-transparent'
  57. }`}
  58. >
  59. <Icon className="w-4 h-4" />
  60. {label}
  61. </button>
  62. ))}
  63. </div>
  64. )}
  65. {active === 'makerworld' && <MakerworldPage embedded />}
  66. {active === 'manyfold' && <ManyfoldTab />}
  67. </div>
  68. );
  69. }