mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-01 00:24:13 -05:00
179 lines
5.0 KiB
TypeScript
179 lines
5.0 KiB
TypeScript
import { HardDriveDownload } from "lucide-react";
|
|
import * as React from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useLocation, useMatches } from "react-router";
|
|
import { SendouButton } from "~/components/elements/Button";
|
|
import {
|
|
SendouChipRadio,
|
|
SendouChipRadioGroup,
|
|
} from "~/components/elements/ChipRadio";
|
|
import { SendouDialog } from "~/components/elements/Dialog";
|
|
import { SendouSwitch } from "~/components/elements/Switch";
|
|
import { useTheme } from "~/features/theme/core/provider";
|
|
import { SENDOU_INK_BASE_URL } from "~/utils/urls";
|
|
import { GraphicQrCodeContext } from "./Graphic";
|
|
import styles from "./ImageExportDialog.module.css";
|
|
|
|
const EXPORT_SCALE = 1.75;
|
|
|
|
type ThemeSelection = "light" | "dark" | "light-custom" | "dark-custom";
|
|
|
|
const THEME_SELECTIONS = [
|
|
{
|
|
value: "light",
|
|
translationKey: "common:imageExport.theme.light",
|
|
needsCustomTheme: false,
|
|
},
|
|
{
|
|
value: "dark",
|
|
translationKey: "common:imageExport.theme.dark",
|
|
needsCustomTheme: false,
|
|
},
|
|
{
|
|
value: "light-custom",
|
|
translationKey: "common:imageExport.theme.lightCustom",
|
|
needsCustomTheme: true,
|
|
},
|
|
{
|
|
value: "dark-custom",
|
|
translationKey: "common:imageExport.theme.darkCustom",
|
|
needsCustomTheme: true,
|
|
},
|
|
] as const;
|
|
|
|
interface ImageExportDialogProps {
|
|
/** Button that opens the dialog, e.g. a `SendouButton` (its own `onPress` also runs, useful for lazy loading the graphic's data) */
|
|
trigger: React.ReactNode;
|
|
heading: string;
|
|
/** Name of the downloaded file without the extension */
|
|
filename: string;
|
|
/** Path the QR code links to, defaults to the current page */
|
|
qrCodePath?: string;
|
|
/** Extra settings controls specific to the use case */
|
|
settings?: React.ReactNode;
|
|
/** The graphic to preview and export */
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
/**
|
|
* Dialog for exporting a graphic component as a .png image. Renders the given graphic
|
|
* as a preview with generic settings (color scheme, custom theme, QR code) and downloads
|
|
* a screenshot of it via snapdom. Graphics render their QR code via {@link GraphicQrCodeContext}.
|
|
*/
|
|
export function ImageExportDialog({
|
|
trigger,
|
|
heading,
|
|
...contentProps
|
|
}: ImageExportDialogProps) {
|
|
return (
|
|
<SendouDialog
|
|
trigger={trigger}
|
|
heading={heading}
|
|
showCloseButton
|
|
className={styles.dialog}
|
|
>
|
|
<ImageExportDialogContent {...contentProps} />
|
|
</SendouDialog>
|
|
);
|
|
}
|
|
|
|
function ImageExportDialogContent({
|
|
filename,
|
|
qrCodePath,
|
|
settings,
|
|
children,
|
|
}: Omit<ImageExportDialogProps, "trigger" | "heading">) {
|
|
const { t } = useTranslation(["common"]);
|
|
const { htmlThemeClass } = useTheme();
|
|
const location = useLocation();
|
|
const pageHasCustomTheme = usePageHasCustomTheme();
|
|
const [themeSelection, setThemeSelection] = React.useState<ThemeSelection>(
|
|
() => {
|
|
const mode = htmlThemeClass === "light" ? "light" : "dark";
|
|
|
|
return pageHasCustomTheme ? `${mode}-custom` : mode;
|
|
},
|
|
);
|
|
const [withQrCode, setWithQrCode] = React.useState(true);
|
|
const frameRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
const theme = themeSelection.startsWith("light") ? "light" : "dark";
|
|
const useCustomTheme = themeSelection.endsWith("-custom");
|
|
|
|
const qrCodeUrl = `${SENDOU_INK_BASE_URL}${qrCodePath ?? `${location.pathname}${location.search}`}`;
|
|
|
|
const handleDownload = async () => {
|
|
if (!frameRef.current) return;
|
|
|
|
const { snapdom } = await import("@zumer/snapdom");
|
|
|
|
await snapdom.download(frameRef.current, {
|
|
type: "png",
|
|
filename,
|
|
quality: 1,
|
|
scale: EXPORT_SCALE,
|
|
embedFonts: true,
|
|
// without this snapdom re-encodes images down to their rendered size, making e.g. the tier image look rough
|
|
compress: false,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="stack md">
|
|
<div className={styles.settings}>
|
|
<SendouChipRadioGroup wrap>
|
|
{THEME_SELECTIONS.filter(
|
|
(selection) => pageHasCustomTheme || !selection.needsCustomTheme,
|
|
).map((selection) => (
|
|
<SendouChipRadio
|
|
key={selection.value}
|
|
name="image-export-theme"
|
|
value={selection.value}
|
|
checked={themeSelection === selection.value}
|
|
onChange={() => setThemeSelection(selection.value)}
|
|
>
|
|
{t(selection.translationKey)}
|
|
</SendouChipRadio>
|
|
))}
|
|
</SendouChipRadioGroup>
|
|
<SendouSwitch isSelected={withQrCode} onChange={setWithQrCode}>
|
|
{t("common:imageExport.qrCode")}
|
|
</SendouSwitch>
|
|
{settings}
|
|
</div>
|
|
<SendouButton
|
|
icon={<HardDriveDownload />}
|
|
onPress={handleDownload}
|
|
className="mx-auto"
|
|
>
|
|
{t("common:imageExport.download")}
|
|
</SendouButton>
|
|
<div className={styles.scroller}>
|
|
<div
|
|
ref={frameRef}
|
|
className={styles.frame}
|
|
data-theme={theme}
|
|
data-default-theme={
|
|
pageHasCustomTheme && !useCustomTheme ? true : undefined
|
|
}
|
|
data-testid="image-export-frame"
|
|
>
|
|
<GraphicQrCodeContext.Provider value={withQrCode ? qrCodeUrl : null}>
|
|
{children}
|
|
</GraphicQrCodeContext.Provider>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function usePageHasCustomTheme() {
|
|
const matches = useMatches();
|
|
|
|
return matches.some((match) =>
|
|
Boolean(
|
|
(match.loaderData as { customTheme?: unknown } | undefined)?.customTheme,
|
|
),
|
|
);
|
|
}
|