{THEME_SELECTIONS.filter(
(selection) => pageHasCustomTheme || !selection.needsCustomTheme,
).map((selection) => (
setThemeSelection(selection.value)}
>
{t(selection.translationKey)}
))}
{t("common:imageExport.qrCode")}
{settings}
: }
onClick={handleDownload}
isDisabled={exportAction !== null}
>
{exportAction === "download"
? t("common:actions.loading")
: isMobile
? t("common:actions.share")
: t("common:imageExport.download")}
{!isMobile && canCopyPngToClipboard() ? (
: }
onClick={handleCopy}
isDisabled={exportAction !== null}
>
{t("common:actions.copyToClipboard")}
) : null}
);
}
async function capturePng(frame: HTMLDivElement) {
const { snapdom } = await import("@zumer/snapdom");
// defensive/speculative fix
await document.fonts.ready;
return snapdom.toBlob(frame, {
type: "png",
quality: 1,
scale: EXPORT_SCALE,
// snapdom's own download helper forces this, without it the export size would vary by device
dpr: 1,
embedFonts: true,
// without this snapdom re-encodes images down to their rendered size, making e.g. the tier image look rough
compress: false,
// names ending in a glyph outside the graphic's font (emoji, Greek, ...) get measured with
// one fallback font in the page and another when rasterized, so a box frozen to its exact
// text width ends up an ellipsis short. This re-measures the clone and pins diverging boxes
reconcile: true,
});
}
function canCopyPngToClipboard() {
return typeof ClipboardItem !== "undefined";
}
/** Share sheet when allowed (mobile) and supported, otherwise a download. */
async function saveImage(
blob: Blob,
filename: string,
{ canShare }: { canShare: boolean },
) {
const file = new File([blob], filename, { type: blob.type });
if (canShare && navigator.canShare?.({ files: [file] })) {
try {
await navigator.share({ files: [file], title: filename });
return;
} catch (e) {
if (e instanceof Error && e.name === "AbortError") return;
}
}
// a blob url is used over a data url because iOS Safari fails to save big data urls
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
}
function useIsMobile() {
return useMediaQuery(COARSE_POINTER_QUERY);
}
function usePageHasCustomTheme() {
const matches = useMatches();
return matches.some((match) =>
Boolean(
(match.loaderData as { customTheme?: unknown } | undefined)?.customTheme,
),
);
}