import React, { MouseEventHandler, useContext, useState } from "react"; import { track } from "@vercel/analytics"; import ImageIcon from "../assets/icons/image-16.svg"; import LinkIcon from "../assets/icons/link-16.svg"; import ChevronDownIcon from "../assets/icons/chevron-down-16.svg"; import ClipboardIcon from "../assets/icons/clipboard-16.svg"; import ArrowsExpandingIcon from "../assets/icons/arrows-expanding-16.svg"; import { FrameContext } from "../store/FrameContextStore"; import { derivedFlashMessageAtom, flashShownAtom } from "../store/flash"; import { fileNameAtom } from "../store"; import download from "../util/download"; import { toPng, toSvg, toBlob } from "../lib/image"; import useHotkeys from "../../../../utils/useHotkeys"; import usePngClipboardSupported from "../util/usePngClipboardSupported"; import { useAtom, useAtomValue } from "jotai"; import { EXPORT_SIZE_OPTIONS, SIZE_LABELS, exportSizeAtom } from "../store/image"; import { autoDetectLanguageAtom, selectedLanguageAtom } from "../store/code"; import { LANGUAGES } from "../util/languages"; import { ButtonGroup } from "@/components/button-group"; import { Button } from "@/components/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/dropdown-menu"; import { DownloadIcon } from "@raycast/icons"; import { Kbd, Kbds } from "@/components/kbd"; const ExportButton: React.FC = () => { const [dropdownOpen, setDropdownOpen] = useState(false); const pngClipboardSupported = usePngClipboardSupported(); const frameContext = useContext(FrameContext); const [, setFlashMessage] = useAtom(derivedFlashMessageAtom); const [, setFlashShown] = useAtom(flashShownAtom); const customFileName = useAtomValue(fileNameAtom); const fileName = customFileName.replaceAll(" ", "-") || "ray-so-export"; const [exportSize, setExportSize] = useAtom(exportSizeAtom); const selectedLanguage = useAtomValue(selectedLanguageAtom); const autoDetectLanguage = useAtomValue(autoDetectLanguageAtom); const savePng = async () => { if (!frameContext?.current) { throw new Error("Couldn't find a frame to export"); } setFlashMessage({ icon: , message: "Exporting PNG" }); const dataUrl = await toPng(frameContext.current, { pixelRatio: exportSize, }); download(dataUrl, `${fileName}.png`); setFlashShown(false); }; const copyPng = async () => { setFlashMessage({ icon: , message: "Copying PNG" }); if (!frameContext?.current) { throw new Error("Couldn't find a frame to export"); } const clipboardItem = new ClipboardItem( { "image/png": toBlob(frameContext.current, { pixelRatio: exportSize, }).then((blob) => { if (!blob) { throw new Error("expected toBlob to return a blob"); } return blob; }), } ); await navigator.clipboard.write([clipboardItem]); setFlashMessage({ icon: , message: "PNG Copied to clipboard!", timeout: 2000 }); }; const saveSvg = async () => { if (!frameContext?.current) { throw new Error("Couldn't find a frame to export"); } setFlashMessage({ icon: , message: "Exporting SVG" }); const dataUrl = await toSvg(frameContext.current); download(dataUrl, `${fileName}.svg`); setFlashShown(false); }; const dropdownHandler: (handler: () => void) => (event: Event) => void = (handler) => { return (event) => { event.preventDefault(); handler(); setDropdownOpen(false); }; }; const handleExportClick: MouseEventHandler = (event) => { event.preventDefault(); const params = new URLSearchParams(window.location.hash.replace("#", "?")); track("Export", { theme: params.get("theme") || "candy", background: params.get("background") || "true", darkMode: params.get("darkMode") || "true", padding: params.get("padding") || "64", language: Object.keys(LANGUAGES).find((key) => LANGUAGES[key].name === selectedLanguage?.name) || "auto", autoDetectLanguage: autoDetectLanguage.toString(), title: params.get("title") || "untitled", width: params.get("width") || "auto", size: SIZE_LABELS[exportSize], }); savePng(); }; const copyUrl = async () => { setFlashMessage({ icon: , message: "Copying URL" }); const url = window.location.toString(); let urlToCopy = url; const encodedUrl = encodeURIComponent(url); try { const response = await fetch(`/api/shorten-url?url=${encodedUrl}&ref=codeImage`); if (response.ok) { const data = await response.json(); if (data.link) { urlToCopy = data.link; } } } catch { // Very long URLs are rejected before reaching the route, with a non-JSON body. // Fall back to copying the unshortened URL instead of leaving the copy unfinished. } navigator.clipboard.writeText(urlToCopy); setFlashMessage({ icon: , message: "URL Copied to clipboard!", timeout: 2000 }); }; useHotkeys("ctrl+k,cmd+k", (event) => { event.preventDefault(); setDropdownOpen((open) => !open); }); useHotkeys("ctrl+s,cmd+s", (event) => { event.preventDefault(); savePng(); }); useHotkeys("ctrl+c,cmd+c", (event) => { if (pngClipboardSupported) { event.preventDefault(); copyPng(); } }); useHotkeys("ctrl+shift+c,cmd+shift+c", (event) => { event.preventDefault(); copyUrl(); }); useHotkeys("ctrl+shift+s,cmd+shift+s", (event) => { event.preventDefault(); saveSvg(); }); return ( setDropdownOpen(open)}> Save PNG{" "} ⌘ S Save SVG ⌘ ⇧ S {pngClipboardSupported && ( Copy Image ⌘ C )} Copy URL ⌘ ⇧ C Size {EXPORT_SIZE_OPTIONS.map((size) => ( setExportSize(size)}> {SIZE_LABELS[size]} ))} ); }; export default ExportButton;