mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-22 06:32:27 -05:00
feat: show model sources as provider badges
Cards and the model page show the provider's logo on a round badge instead of a text chip; the chip shows in its tooltip.
This commit is contained in:
1 parent
70e3821e1a
commit
0aff9e9a9e
4 files changed
+111
-27
No files matched your search
@@ -92,6 +92,11 @@ const LOGOS: Record<ProviderId, Logo> = {
|
||||
},
|
||||
};
|
||||
|
||||
/** Whether a provider name, such as a model's source, is one there's a logo for. */
|
||||
export function hasProviderLogo(id: string | null | undefined): id is ProviderId {
|
||||
return !!id && Object.hasOwn(LOGOS, id);
|
||||
}
|
||||
|
||||
export function ProviderLogo({ id, size }: { id: ProviderId; size: number }) {
|
||||
const { viewBox = "0 0 24 24", color, path, fillRule, layers } = LOGOS[id];
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Box from "@mui/material/Box";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { useTheme, type SxProps, type Theme } from "@mui/material/styles";
|
||||
import markColor from "../assets/logos/thingport-mark-color.svg";
|
||||
import markOnDark from "../assets/logos/thingport-mark-on-dark.svg";
|
||||
import { printProviderInfo, providerChipSx } from "../constants/importProviders";
|
||||
import { hasProviderLogo, ProviderLogo } from "./Layout/ConfigurationDialog/providerLogos";
|
||||
|
||||
// Read by screen readers, not shown.
|
||||
const visuallyHidden = {
|
||||
position: "absolute",
|
||||
width: "1px",
|
||||
height: "1px",
|
||||
overflow: "hidden",
|
||||
clip: "rect(0 0 0 0)",
|
||||
whiteSpace: "nowrap",
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
/** The model's source provider; null for a direct upload, shown with Thingport's own mark. */
|
||||
provider: string | null | undefined;
|
||||
/** The badge's width and height, in px. */
|
||||
size?: number;
|
||||
sx?: SxProps<Theme>;
|
||||
};
|
||||
|
||||
/** Where a model came from, as the provider's logo on a round badge; its chip shows on hover or focus. */
|
||||
export default function ProviderBadge({ provider, size = 28, sx }: Props) {
|
||||
const { t } = useTranslation("models");
|
||||
const theme = useTheme();
|
||||
const info = printProviderInfo(provider);
|
||||
const label = provider ? t("card.importedFrom", { provider: info.label }) : t("card.uploadedDirectly");
|
||||
const logoSize = Math.round(size * 0.6);
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<Box
|
||||
sx={{
|
||||
px: 1,
|
||||
py: 0.375,
|
||||
borderRadius: 1,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.4,
|
||||
...providerChipSx(info),
|
||||
}}
|
||||
>
|
||||
{info.label}
|
||||
</Box>
|
||||
}
|
||||
// The chip is the whole tooltip, so the tooltip's own box stays out of sight.
|
||||
slotProps={{ tooltip: { sx: { bgcolor: "transparent", p: 0, boxShadow: 3, borderRadius: 1 } } }}
|
||||
>
|
||||
<Box
|
||||
tabIndex={0}
|
||||
sx={[
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
borderRadius: "50%",
|
||||
bgcolor: "background.paper",
|
||||
color: "text.primary",
|
||||
border: 1,
|
||||
borderColor: "divider",
|
||||
boxShadow: 2,
|
||||
transition: "transform 150ms ease",
|
||||
"&:hover, &:focus-visible": { transform: "scale(1.08)" },
|
||||
"&:focus-visible": { outline: `2px solid ${theme.palette.primary.main}`, outlineOffset: 2 },
|
||||
"@media (prefers-reduced-motion: reduce)": {
|
||||
transition: "none",
|
||||
"&:hover, &:focus-visible": { transform: "none" },
|
||||
},
|
||||
},
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
>
|
||||
<Box component="span" sx={visuallyHidden}>
|
||||
{label}
|
||||
</Box>
|
||||
{hasProviderLogo(provider) ? (
|
||||
<ProviderLogo id={provider} size={logoSize} />
|
||||
) : (
|
||||
<Box
|
||||
component="img"
|
||||
src={theme.palette.mode === "dark" ? markOnDark : markColor}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
sx={{ width: logoSize, height: logoSize, objectFit: "contain", display: "block" }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { type ResolvedTheme } from "../../constants/settingsOptions";
|
||||
import { dividerBorderColor } from "../../theme";
|
||||
import { MODEL_EXTS } from "../../constants/fileTypes";
|
||||
import { extOf } from "../../utils/fileExtensions";
|
||||
import ProviderBadge from "../../components/ProviderBadge";
|
||||
import { renderPreviewContent } from "../../components/media/renderPreviewContent";
|
||||
import { ModelSnapshot } from "../../components/media/ModelViewer/ModelSnapshot";
|
||||
import { usePageHeader } from "../../components/Layout/PageHeaderContext";
|
||||
@@ -167,7 +168,7 @@ export default function ModelDetailPage({ theme, onSelectCategory, onUnauthorize
|
||||
with huge intrinsic pixel dimensions could still force this column wider than its "2fr"
|
||||
share (and skew the whole two-column layout) even though the image itself is capped
|
||||
below. */}
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Box sx={{ minWidth: 0, position: "relative" }}>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
@@ -273,6 +274,11 @@ export default function ModelDetailPage({ theme, onSelectCategory, onUnauthorize
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<ProviderBadge
|
||||
provider={print.source_provider}
|
||||
size={40}
|
||||
sx={{ position: "absolute", top: 12, left: 12, zIndex: 1 }}
|
||||
/>
|
||||
|
||||
{images.length > 1 && (
|
||||
<Stack direction="row" spacing={1} sx={{ mt: 1.5, overflowX: "auto", pb: 0.5 }}>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { type Print, printsApi } from "../../api/prints";
|
||||
import { type PreviewMode } from "../../api/settings";
|
||||
import { type ResolvedTheme } from "../../constants/settingsOptions";
|
||||
import { renderPreviewContent } from "../../components/media/renderPreviewContent";
|
||||
import { printProviderInfo, providerChipSx } from "../../constants/importProviders";
|
||||
import ProviderBadge from "../../components/ProviderBadge";
|
||||
import { SELF_AUTHOR_ID } from "../../constants/selfAuthor";
|
||||
import { useGravatarUrl } from "../../hooks/useGravatarUrl";
|
||||
import StarToggle from "../../components/StarToggle";
|
||||
@@ -85,7 +85,6 @@ export default function ModelCard({
|
||||
const authorName =
|
||||
author?.name || author?.handle || item.creator || (showViewerAsAuthor ? viewer!.display_name : null);
|
||||
const authorAvatarUrl = author?.avatar_url || (showViewerAsAuthor ? viewerAvatarUrl : undefined);
|
||||
const providerInfo = printProviderInfo(item.source_provider);
|
||||
// Only mounted while hovered, so idle cards don't load images or tick.
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const contextMenu = useCardContextMenu();
|
||||
@@ -125,30 +124,7 @@ export default function ModelCard({
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
item.source_provider
|
||||
? t("models:card.importedFrom", { provider: providerInfo.label })
|
||||
: t("models:card.uploadedDirectly")
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
left: 8,
|
||||
px: 1,
|
||||
py: 0.375,
|
||||
borderRadius: 1,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.4,
|
||||
...providerChipSx(providerInfo),
|
||||
}}
|
||||
>
|
||||
{providerInfo.label}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<ProviderBadge provider={item.source_provider} sx={{ position: "absolute", top: 8, left: 8 }} />
|
||||
|
||||
<Stack
|
||||
className="model-card-actions"
|
||||
|
||||
Reference in new issue
Block a user