@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
188 lines (187 loc) • 6.26 kB
JavaScript
"use client";
import FlexBasic_default from "../../Flex/FlexBasic.mjs";
import Center from "../../Flex/Center.mjs";
import TooltipGroup from "../../base-ui/Tooltip/TooltipGroup.mjs";
import ActionIcon from "../../ActionIcon/ActionIcon.mjs";
import DropdownMenu from "../../base-ui/DropdownMenu/DropdownMenu.mjs";
import { useTranslation } from "../../i18n/useTranslation.mjs";
import { downloadBlob } from "../../utils/downloadBlob.mjs";
import { styles } from "../style.mjs";
import { naturalScale } from "./geometry.mjs";
import { toast } from "../../base-ui/Toast/imperative.mjs";
import image_default from "../../i18n/resources/en/image.mjs";
import { getClipboardBlob } from "../../utils/blobToPng.mjs";
import ActualSizeIcon from "./ActualSizeIcon.mjs";
import { memo, useCallback, useMemo, useState, useSyncExternalStore } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
import { Copy, Download, Ellipsis, FlipHorizontal, FlipVertical, RotateCcw, RotateCw, ZoomIn, ZoomOut } from "lucide-react";
//#region src/Image/viewer/Toolbar.tsx
const getFileNameFromUrl = (url) => {
try {
const match = new URL(url).pathname.match(/\/([^/]+)$/);
return match ? decodeURIComponent(match[1]) : "image";
} catch {
return "image";
}
};
const getExtensionFromMimeType = (mimeType) => {
return {
"image/svg+xml": "svg",
"image/png": "png",
"image/jpeg": "jpg",
"image/jpg": "jpg",
"image/webp": "webp",
"image/gif": "gif"
}[mimeType?.toLowerCase()] || mimeType?.split("/")[1]?.split("+")[0] || "png";
};
const usePercentage = (scale, matchScale) => {
const subscribe = useCallback((onChange) => scale.on("change", onChange), [scale]);
const getSnapshot = useCallback(() => Math.round(scale.get() / matchScale * 100), [matchScale, scale]);
return useSyncExternalStore(subscribe, getSnapshot);
};
const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVertical, natural, onMoreOpenChange, rotateLeft, rotateRight, rotation, scale, source, toggleActualSize, toolbarAddon, zoomIn, zoomOut }) => {
const { t } = useTranslation(image_default);
const [containerEl, setContainerEl] = useState(null);
const [copyLoading, setCopyLoading] = useState(false);
const [downloadLoading, setDownloadLoading] = useState(false);
const matchScale = naturalScale(natural, fitRect, rotation);
const percentage = usePercentage(scale, matchScale);
const canToggleActualSize = Math.abs(matchScale - 1) > .01;
const showFitAffordance = canToggleActualSize && percentage === 100;
const handleDownload = useCallback(async () => {
setDownloadLoading(true);
try {
const blob = await (await fetch(source, { mode: "cors" })).blob();
const blobUrl = URL.createObjectURL(blob);
let fileName = getFileNameFromUrl(source);
const ext = getExtensionFromMimeType(blob.type);
if (!fileName.includes(".")) fileName = `${fileName}.${ext}`;
else if (fileName.endsWith(".svg+xml")) fileName = fileName.replace(/\.svg\+xml$/i, ".svg");
await downloadBlob(blobUrl, fileName);
URL.revokeObjectURL(blobUrl);
toast.success(t("image.downloadSuccess"));
} catch {
toast.error(t("image.downloadFailed"));
} finally {
setDownloadLoading(false);
}
}, [source, t]);
const handleCopy = useCallback(async () => {
setCopyLoading(true);
try {
const blob = await (await fetch(source, { mode: "cors" })).blob();
const clipboardBlob = await getClipboardBlob(blob);
await navigator.clipboard.write([new ClipboardItem(clipboardBlob)]);
toast.success(t("image.copySuccess"));
} catch {
toast.error(t("image.copyFailed"));
} finally {
setCopyLoading(false);
}
}, [source, t]);
const moreItems = useMemo(() => [
{
icon: FlipHorizontal,
key: "flip-horizontal",
label: t("image.flipHorizontal"),
onClick: flipHorizontal
},
{
icon: FlipVertical,
key: "flip-vertical",
label: t("image.flipVertical"),
onClick: flipVertical
},
{
icon: RotateCcw,
key: "rotate-left",
label: t("image.rotateLeft"),
onClick: rotateLeft
},
{
icon: RotateCw,
key: "rotate-right",
label: t("image.rotateRight"),
onClick: rotateRight
},
{
icon: Copy,
key: "copy",
label: t("image.copy"),
onClick: handleCopy
}
], [
flipHorizontal,
flipVertical,
handleCopy,
rotateLeft,
rotateRight,
t
]);
return /* @__PURE__ */ jsx(TooltipGroup, {
popupContainer: containerEl ?? void 0,
children: /* @__PURE__ */ jsx("div", {
className: styles.toolbar,
ref: setContainerEl,
children: /* @__PURE__ */ jsxs(FlexBasic_default, {
horizontal: true,
align: "center",
className: styles.toolbarRow,
gap: 8,
children: [
/* @__PURE__ */ jsx(ActionIcon, {
disabled: !canZoomOut,
icon: ZoomOut,
style: { borderRadius: 999 },
title: t("image.zoomOut"),
onClick: zoomOut
}),
/* @__PURE__ */ jsxs(Center, {
horizontal: true,
className: styles.toolbarPercentage,
children: [percentage, "%"]
}),
/* @__PURE__ */ jsx(ActionIcon, {
disabled: !canZoomIn,
icon: ZoomIn,
style: { borderRadius: 999 },
title: t("image.zoomIn"),
onClick: zoomIn
}),
/* @__PURE__ */ jsx(ActionIcon, {
"data-actual-size": showFitAffordance ? "fit" : "actual",
disabled: !canToggleActualSize,
icon: ActualSizeIcon,
style: { borderRadius: 999 },
title: showFitAffordance ? t("image.fitToScreen") : t("image.actualSize"),
onClick: toggleActualSize
}),
/* @__PURE__ */ jsx(ActionIcon, {
icon: Download,
loading: downloadLoading,
style: { borderRadius: 999 },
title: t("image.download"),
onClick: handleDownload
}),
/* @__PURE__ */ jsx(DropdownMenu, {
items: moreItems,
placement: "top",
portalProps: { container: containerEl ?? void 0 },
onOpenChange: (open) => onMoreOpenChange?.(open),
children: /* @__PURE__ */ jsx(ActionIcon, {
icon: Ellipsis,
loading: copyLoading,
style: { borderRadius: 999 },
title: t("image.more")
})
}),
toolbarAddon
]
})
})
});
});
Toolbar.displayName = "Toolbar";
//#endregion
export { Toolbar as default };
//# sourceMappingURL=Toolbar.mjs.map