UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

188 lines (187 loc) 6.26 kB
"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