mirror of
https://github.com/ReaJason/MemShellParty.git
synced 2026-09-22 07:00:43 +08:00
208 lines
5.6 KiB
TypeScript
208 lines
5.6 KiB
TypeScript
import type { VariantProps } from "class-variance-authority";
|
|
|
|
import { Check, Copy } from "lucide-react";
|
|
import { type HTMLProps, type ReactNode, useCallback, useEffect, useState } from "react";
|
|
import CopyToClipboard from "react-copy-to-clipboard";
|
|
import { useTranslation } from "react-i18next";
|
|
import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
import java from "react-syntax-highlighter/dist/esm/languages/prism/java";
|
|
import materialDark from "react-syntax-highlighter/dist/esm/styles/prism/material-dark";
|
|
import { toast } from "sonner";
|
|
|
|
import { Button, type buttonVariants } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
SyntaxHighlighter.registerLanguage("java", java);
|
|
interface CopyButtonProps extends React.ComponentProps<"button"> {
|
|
value: string;
|
|
src?: string;
|
|
}
|
|
|
|
export interface CopyOption {
|
|
label: string;
|
|
value: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function CopyButton({
|
|
value,
|
|
className,
|
|
...buttonProps
|
|
}: Readonly<CopyButtonProps & VariantProps<typeof buttonVariants>>) {
|
|
const [hasCopied, setHasCopied] = useState(false);
|
|
const { t } = useTranslation(["common"]);
|
|
|
|
useEffect(() => {
|
|
if (hasCopied) {
|
|
const timer = setTimeout(() => {
|
|
setHasCopied(false);
|
|
}, 1000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [hasCopied]);
|
|
|
|
const handleCopy = useCallback(() => {
|
|
if (!hasCopied) {
|
|
setHasCopied(true);
|
|
toast.success(t("copySuccess"), { duration: 1000 });
|
|
}
|
|
}, [hasCopied, t]);
|
|
|
|
return (
|
|
<CopyToClipboard.CopyToClipboard text={value} onCopy={handleCopy}>
|
|
<Button
|
|
{...buttonProps}
|
|
variant="ghost"
|
|
size="icon"
|
|
type="button"
|
|
className={cn("h-7 w-7 [&_svg]:h-4 [&_svg]:w-4", className)}
|
|
disabled={hasCopied || buttonProps.disabled}
|
|
>
|
|
{hasCopied ? <Check /> : <Copy />}
|
|
</Button>
|
|
</CopyToClipboard.CopyToClipboard>
|
|
);
|
|
}
|
|
|
|
export function CopyMenuButton({
|
|
options,
|
|
className,
|
|
...buttonProps
|
|
}: Readonly<
|
|
{ options: CopyOption[] } & React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>
|
|
>) {
|
|
const [hasCopied, setHasCopied] = useState(false);
|
|
const { t } = useTranslation(["common"]);
|
|
|
|
useEffect(() => {
|
|
if (hasCopied) {
|
|
const timer = setTimeout(() => {
|
|
setHasCopied(false);
|
|
}, 1000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [hasCopied]);
|
|
|
|
const handleCopy = useCallback(() => {
|
|
if (!hasCopied) {
|
|
setHasCopied(true);
|
|
toast.success(t("copySuccess"), { duration: 1000 });
|
|
}
|
|
}, [hasCopied, t]);
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
render={
|
|
<Button
|
|
{...buttonProps}
|
|
variant="ghost"
|
|
size="icon"
|
|
type="button"
|
|
className={cn("h-7 w-7 [&_svg]:h-4 [&_svg]:w-4", className)}
|
|
/>
|
|
}
|
|
>
|
|
{hasCopied ? <Check /> : <Copy />}
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{options.map((option) => (
|
|
<CopyToClipboard.CopyToClipboard
|
|
key={option.label}
|
|
text={option.value}
|
|
onCopy={handleCopy}
|
|
>
|
|
<DropdownMenuItem disabled={option.disabled}>{option.label}</DropdownMenuItem>
|
|
</CopyToClipboard.CopyToClipboard>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|
|
|
|
export default function CodeViewer({
|
|
code,
|
|
header,
|
|
button,
|
|
language,
|
|
height,
|
|
showLineNumbers = true,
|
|
wrapLongLines = true,
|
|
copyLabel,
|
|
copyDisabled = false,
|
|
copyOptions,
|
|
}: Readonly<CodeViewerProps>) {
|
|
const lineProps: lineTagPropsFunction | HTMLProps<HTMLElement> | undefined = wrapLongLines
|
|
? { style: { overflowWrap: "break-word", whiteSpace: "pre-wrap" } }
|
|
: undefined;
|
|
return (
|
|
<div className="rounded-lg border">
|
|
<div
|
|
className={cn("flex items-center justify-end border-b p-2", header && "justify-between")}
|
|
>
|
|
{header}
|
|
<div className="flex items-center gap-2">
|
|
{button}
|
|
{copyOptions ? (
|
|
<CopyMenuButton
|
|
options={copyOptions}
|
|
variant="ghost"
|
|
size="sm"
|
|
aria-label={copyLabel}
|
|
title={copyLabel}
|
|
/>
|
|
) : (
|
|
<CopyButton
|
|
value={code}
|
|
variant="ghost"
|
|
size="sm"
|
|
aria-label={copyLabel}
|
|
title={copyLabel}
|
|
disabled={copyDisabled}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="wrap-all relative overflow-hidden text-xs">
|
|
<SyntaxHighlighter
|
|
language={language}
|
|
style={materialDark}
|
|
showLineNumbers={showLineNumbers}
|
|
wrapLongLines={wrapLongLines}
|
|
lineProps={lineProps}
|
|
customStyle={{
|
|
margin: 0,
|
|
padding: showLineNumbers ? 0 : "1em 1em",
|
|
borderRadius: "0 0 var(--radius) var(--radius)",
|
|
height: height,
|
|
whiteSpace: wrapLongLines ? "pre-wrap" : "pre",
|
|
overflowWrap: wrapLongLines ? "normal" : "break-word",
|
|
}}
|
|
>
|
|
{code}
|
|
</SyntaxHighlighter>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface CodeViewerProps {
|
|
code: string;
|
|
language: string;
|
|
header?: ReactNode;
|
|
button?: ReactNode;
|
|
height?: string | number;
|
|
showLineNumbers?: boolean;
|
|
wrapLongLines?: boolean;
|
|
lineProps?: (lineNumber: number) => React.HTMLProps<HTMLElement>;
|
|
copyLabel?: string;
|
|
copyDisabled?: boolean;
|
|
copyOptions?: CopyOption[];
|
|
}
|