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;