{"version":3,"file":"Toolbar.mjs","names":["imageMessages","Flexbox"],"sources":["../../../src/Image/viewer/Toolbar.tsx"],"sourcesContent":["'use client';\n\nimport {\n  Copy,\n  Download,\n  Ellipsis,\n  FlipHorizontal,\n  FlipVertical,\n  RotateCcw,\n  RotateCw,\n  ZoomIn,\n  ZoomOut,\n} from 'lucide-react';\nimport type { MotionValue } from 'motion/react';\nimport { memo, type ReactNode, useCallback, useMemo, useState, useSyncExternalStore } from 'react';\n\nimport ActionIcon from '@/ActionIcon';\nimport DropdownMenu, { type DropdownItem } from '@/base-ui/DropdownMenu';\nimport { toast } from '@/base-ui/Toast';\nimport { Center, Flexbox } from '@/Flex';\nimport imageMessages from '@/i18n/resources/en/image';\nimport { useTranslation } from '@/i18n/useTranslation';\nimport { TooltipGroup } from '@/Tooltip';\nimport { getClipboardBlob } from '@/utils/blobToPng';\nimport { downloadBlob } from '@/utils/downloadBlob';\n\nimport { styles } from '../style';\nimport ActualSizeIcon from './ActualSizeIcon';\nimport { naturalScale, type Rect, type Rotation, type Size } from './geometry';\n\nconst getFileNameFromUrl = (url: string): string => {\n  try {\n    const pathname = new URL(url).pathname;\n    const match = pathname.match(/\\/([^/]+)$/);\n    return match ? decodeURIComponent(match[1]) : 'image';\n  } catch {\n    return 'image';\n  }\n};\n\nconst getExtensionFromMimeType = (mimeType: string): string => {\n  const map: Record<string, string> = {\n    'image/svg+xml': 'svg',\n    'image/png': 'png',\n    'image/jpeg': 'jpg',\n    'image/jpg': 'jpg',\n    'image/webp': 'webp',\n    'image/gif': 'gif',\n  };\n  return map[mimeType?.toLowerCase()] || mimeType?.split('/')[1]?.split('+')[0] || 'png';\n};\n\nconst usePercentage = (scale: MotionValue<number>, matchScale: number): number => {\n  const subscribe = useCallback((onChange: () => void) => scale.on('change', onChange), [scale]);\n  const getSnapshot = useCallback(\n    () => Math.round((scale.get() / matchScale) * 100),\n    [matchScale, scale],\n  );\n\n  return useSyncExternalStore(subscribe, getSnapshot);\n};\n\nexport interface ToolbarProps {\n  canZoomIn: boolean;\n  canZoomOut: boolean;\n  fitRect: Rect;\n  flipHorizontal: () => void;\n  flipVertical: () => void;\n  natural: Size;\n  onMoreOpenChange?: (open: boolean) => void;\n  rotateLeft: () => void;\n  rotateRight: () => void;\n  rotation: Rotation;\n  scale: MotionValue<number>;\n  source: string;\n  toggleActualSize: () => void;\n  toolbarAddon?: ReactNode;\n  zoomIn: () => void;\n  zoomOut: () => void;\n}\n\nconst Toolbar = memo<ToolbarProps>(\n  ({\n    canZoomIn,\n    canZoomOut,\n    fitRect,\n    flipHorizontal,\n    flipVertical,\n    natural,\n    onMoreOpenChange,\n    rotateLeft,\n    rotateRight,\n    rotation,\n    scale,\n    source,\n    toggleActualSize,\n    toolbarAddon,\n    zoomIn,\n    zoomOut,\n  }) => {\n    const { t } = useTranslation(imageMessages);\n    const [containerEl, setContainerEl] = useState<HTMLElement | null>(null);\n    const [copyLoading, setCopyLoading] = useState(false);\n    const [downloadLoading, setDownloadLoading] = useState(false);\n\n    const matchScale = naturalScale(natural, fitRect, rotation);\n    const percentage = usePercentage(scale, matchScale);\n    // An image small enough that computeFit never scaled it down is already at\n    // 100% when fitted, so the two states the toggle switches between are the\n    // same state and the control has nothing to do.\n    const canToggleActualSize = Math.abs(matchScale - 1) > 0.01;\n    // The control reads as \"go to actual size\" by default, and only flips to\n    // the fit affordance when it can actually take you back — an image that\n    // fits at 100% is at both states at once and would otherwise offer to\n    // \"fit to screen\" while already fitted.\n    const showFitAffordance = canToggleActualSize && percentage === 100;\n\n    const handleDownload = useCallback(async () => {\n      setDownloadLoading(true);\n      try {\n        const response = await fetch(source, { mode: 'cors' });\n        const blob = await response.blob();\n        const blobUrl = URL.createObjectURL(blob);\n        let fileName = getFileNameFromUrl(source);\n        const ext = getExtensionFromMimeType(blob.type);\n        if (!fileName.includes('.')) {\n          fileName = `${fileName}.${ext}`;\n        } else if (fileName.endsWith('.svg+xml')) {\n          fileName = fileName.replace(/\\.svg\\+xml$/i, '.svg');\n        }\n        await downloadBlob(blobUrl, fileName);\n        URL.revokeObjectURL(blobUrl);\n        toast.success(t('image.downloadSuccess'));\n      } catch {\n        toast.error(t('image.downloadFailed'));\n      } finally {\n        setDownloadLoading(false);\n      }\n    }, [source, t]);\n\n    const handleCopy = useCallback(async () => {\n      setCopyLoading(true);\n      try {\n        const response = await fetch(source, { mode: 'cors' });\n        const blob = await response.blob();\n        const clipboardBlob = await getClipboardBlob(blob);\n        await navigator.clipboard.write([new ClipboardItem(clipboardBlob)]);\n        toast.success(t('image.copySuccess'));\n      } catch {\n        toast.error(t('image.copyFailed'));\n      } finally {\n        setCopyLoading(false);\n      }\n    }, [source, t]);\n\n    const moreItems = useMemo<DropdownItem[]>(\n      () => [\n        {\n          icon: FlipHorizontal,\n          key: 'flip-horizontal',\n          label: t('image.flipHorizontal'),\n          onClick: flipHorizontal,\n        },\n        {\n          icon: FlipVertical,\n          key: 'flip-vertical',\n          label: t('image.flipVertical'),\n          onClick: flipVertical,\n        },\n        { icon: RotateCcw, key: 'rotate-left', label: t('image.rotateLeft'), onClick: rotateLeft },\n        {\n          icon: RotateCw,\n          key: 'rotate-right',\n          label: t('image.rotateRight'),\n          onClick: rotateRight,\n        },\n        { icon: Copy, key: 'copy', label: t('image.copy'), onClick: handleCopy },\n      ],\n      [flipHorizontal, flipVertical, handleCopy, rotateLeft, rotateRight, t],\n    );\n\n    return (\n      <TooltipGroup popupContainer={containerEl ?? undefined}>\n        <div className={styles.toolbar} ref={setContainerEl}>\n          <Flexbox horizontal align=\"center\" className={styles.toolbarRow} gap={8}>\n            <ActionIcon\n              disabled={!canZoomOut}\n              icon={ZoomOut}\n              style={{ borderRadius: 999 }}\n              title={t('image.zoomOut')}\n              onClick={zoomOut}\n            />\n            <Center horizontal className={styles.toolbarPercentage}>\n              {percentage}%\n            </Center>\n            <ActionIcon\n              disabled={!canZoomIn}\n              icon={ZoomIn}\n              style={{ borderRadius: 999 }}\n              title={t('image.zoomIn')}\n              onClick={zoomIn}\n            />\n            <ActionIcon\n              data-actual-size={showFitAffordance ? 'fit' : 'actual'}\n              disabled={!canToggleActualSize}\n              icon={ActualSizeIcon}\n              style={{ borderRadius: 999 }}\n              title={showFitAffordance ? t('image.fitToScreen') : t('image.actualSize')}\n              onClick={toggleActualSize}\n            />\n            <ActionIcon\n              icon={Download}\n              loading={downloadLoading}\n              style={{ borderRadius: 999 }}\n              title={t('image.download')}\n              onClick={handleDownload}\n            />\n            <DropdownMenu\n              items={moreItems}\n              placement=\"top\"\n              portalProps={{ container: containerEl ?? undefined }}\n              onOpenChange={(open) => onMoreOpenChange?.(open)}\n            >\n              <ActionIcon\n                icon={Ellipsis}\n                loading={copyLoading}\n                style={{ borderRadius: 999 }}\n                title={t('image.more')}\n              />\n            </DropdownMenu>\n            {toolbarAddon}\n          </Flexbox>\n        </div>\n      </TooltipGroup>\n    );\n  },\n);\n\nToolbar.displayName = 'Toolbar';\n\nexport default Toolbar;\n"],"mappings":"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"}