mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-03 10:21:29 -05:00
Image export fixes
This commit is contained in:
parent
fed8de5f68
commit
1280a88d21
|
|
@ -72,6 +72,10 @@ same place as an auto margin would but leaves their text room to breathe.
|
|||
}
|
||||
|
||||
.headerTitle {
|
||||
/* the export freezes this box at its exact text width, so it has to claim the row's free
|
||||
space rather than shrink-wrap, or a glyph the raster measures wider gets ellipsised away.
|
||||
Basis stays auto so the title still contributes its text to the row's intrinsic width */
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: var(--font-lg);
|
||||
|
|
@ -210,10 +214,10 @@ same place as an auto margin would but leaves their text room to breathe.
|
|||
color: var(--graphic-text-dim);
|
||||
}
|
||||
|
||||
/* no ellipsis: the chips wrap and a username is capped well below the row width, so truncating
|
||||
here only ever fired on the frozen-width rounding described above */
|
||||
.playerName {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.weapons {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { HardDriveDownload } from "lucide-react";
|
||||
import { HardDriveDownload, Share2 } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useMatches } from "react-router";
|
||||
|
|
@ -16,6 +16,8 @@ import styles from "./ImageExportDialog.module.css";
|
|||
|
||||
const EXPORT_SCALE = 1.75;
|
||||
|
||||
const COARSE_POINTER_QUERY = "(pointer: coarse)";
|
||||
|
||||
type ThemeSelection = "light" | "dark" | "light-custom" | "dark-custom";
|
||||
|
||||
const THEME_SELECTIONS = [
|
||||
|
|
@ -87,6 +89,7 @@ function ImageExportDialogContent({
|
|||
const { htmlThemeClass } = useTheme();
|
||||
const location = useLocation();
|
||||
const pageHasCustomTheme = usePageHasCustomTheme();
|
||||
const isMobile = useIsMobile();
|
||||
const [themeSelection, setThemeSelection] = React.useState<ThemeSelection>(
|
||||
() => {
|
||||
const mode = htmlThemeClass === "light" ? "light" : "dark";
|
||||
|
|
@ -103,7 +106,28 @@ function ImageExportDialogContent({
|
|||
|
||||
const qrCodeUrl = `${SENDOU_INK_BASE_URL}${qrCodePath ?? `${location.pathname}${location.search}`}`;
|
||||
|
||||
const handleDownload = async () => {
|
||||
// snapdom re-downloads every image at export time rather than reusing what the preview
|
||||
// already painted, and silently drops any that fails. Warming them while the preview sits
|
||||
// idle keeps those fetches from racing the capture's own work for the main thread.
|
||||
// Runs after every render because settings can mount images that were not there before
|
||||
// (e.g. the build export's ability chunks); re-running once everything is cached is ~7ms.
|
||||
React.useEffect(() => {
|
||||
if (isDownloading) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
import("@zumer/snapdom").then(({ preCache }) => {
|
||||
if (cancelled || !frameRef.current) return;
|
||||
|
||||
preCache(frameRef.current).catch(() => {});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!frameRef.current) return;
|
||||
|
||||
setIsDownloading(true);
|
||||
|
|
@ -119,9 +143,13 @@ function ImageExportDialogContent({
|
|||
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,
|
||||
});
|
||||
|
||||
await saveImage(blob, `${filename}.png`);
|
||||
await saveImage(blob, `${filename}.png`, { canShare: isMobile });
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
|
|
@ -151,14 +179,16 @@ function ImageExportDialogContent({
|
|||
{settings}
|
||||
</div>
|
||||
<SendouButton
|
||||
icon={<HardDriveDownload />}
|
||||
onPress={handleDownload}
|
||||
icon={isMobile ? <Share2 /> : <HardDriveDownload />}
|
||||
onPress={handleExport}
|
||||
isDisabled={isDownloading}
|
||||
className="mx-auto"
|
||||
>
|
||||
{isDownloading
|
||||
? t("common:actions.loading")
|
||||
: t("common:imageExport.download")}
|
||||
: isMobile
|
||||
? t("common:actions.share")
|
||||
: t("common:imageExport.download")}
|
||||
</SendouButton>
|
||||
<div className={styles.scroller}>
|
||||
<div
|
||||
|
|
@ -179,11 +209,18 @@ function ImageExportDialogContent({
|
|||
);
|
||||
}
|
||||
|
||||
/** Shares the image if the platform supports it (iOS), otherwise downloads it */
|
||||
async function saveImage(blob: Blob, filename: string) {
|
||||
/**
|
||||
* Opens the share sheet when sharing is allowed (mobile) and the platform supports it,
|
||||
* otherwise downloads the image
|
||||
*/
|
||||
async function saveImage(
|
||||
blob: Blob,
|
||||
filename: string,
|
||||
{ canShare }: { canShare: boolean },
|
||||
) {
|
||||
const file = new File([blob], filename, { type: blob.type });
|
||||
|
||||
if (navigator.canShare?.({ files: [file] })) {
|
||||
if (canShare && navigator.canShare?.({ files: [file] })) {
|
||||
try {
|
||||
await navigator.share({ files: [file], title: filename });
|
||||
return;
|
||||
|
|
@ -203,6 +240,20 @@ async function saveImage(blob: Blob, filename: string) {
|
|||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function subscribeToPointerQuery(callback: () => void) {
|
||||
const mediaQueryList = window.matchMedia(COARSE_POINTER_QUERY);
|
||||
mediaQueryList.addEventListener("change", callback);
|
||||
return () => mediaQueryList.removeEventListener("change", callback);
|
||||
}
|
||||
|
||||
function useIsMobile() {
|
||||
return React.useSyncExternalStore(
|
||||
subscribeToPointerQuery,
|
||||
() => window.matchMedia(COARSE_POINTER_QUERY).matches,
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
||||
function usePageHasCustomTheme() {
|
||||
const matches = useMatches();
|
||||
|
||||
|
|
|
|||
|
|
@ -256,6 +256,8 @@
|
|||
}
|
||||
|
||||
.mateNameText {
|
||||
/* claims .mateName's free space, see .headerTitle */
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -53,7 +53,7 @@
|
|||
"@react-router/serve": "8.1.0",
|
||||
"@sentry/react-router": "10.67.0",
|
||||
"@tldraw/tldraw": "3.12.1",
|
||||
"@zumer/snapdom": "2.16.0",
|
||||
"@zumer/snapdom": "2.23.1",
|
||||
"better-sqlite3": "13.0.1",
|
||||
"chart.js": "4.5.1",
|
||||
"clsx": "2.1.1",
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ importers:
|
|||
specifier: 3.12.1
|
||||
version: 3.12.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@zumer/snapdom':
|
||||
specifier: 2.16.0
|
||||
version: 2.16.0
|
||||
specifier: 2.23.1
|
||||
version: 2.23.1
|
||||
better-sqlite3:
|
||||
specifier: 13.0.1
|
||||
version: 13.0.1
|
||||
|
|
@ -2850,9 +2850,8 @@ packages:
|
|||
'@vitest/utils@4.1.10':
|
||||
resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==}
|
||||
|
||||
'@zumer/snapdom@2.16.0':
|
||||
resolution: {integrity: sha512-ZfN3o6kcxb+jDhYe/JHFZf3g4UfDv3MGyTiK/sWvFU+vYlxa2KZVLZH7DxhKkobwGp66KMhRnXTqIKmaSgM50Q==}
|
||||
deprecated: Old versions have known fidelity bugs (text re-wrap, width freezing, KaTeX clipping). Upgrade to ^2.22 — drop-in for any 2.x. https://github.com/zumerlab/snapdom/releases
|
||||
'@zumer/snapdom@2.23.1':
|
||||
resolution: {integrity: sha512-r+6LRFXIpzTxlvGBjTr7mhvVW05TozgyE1QFmvO1hbchdbl4Km5/7DIkeyl2+nEVbz8Sp6/K2g4b86pXhCrFIQ==}
|
||||
|
||||
accepts@2.0.0:
|
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||
|
|
@ -7464,7 +7463,7 @@ snapshots:
|
|||
convert-source-map: 2.0.0
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@zumer/snapdom@2.16.0': {}
|
||||
'@zumer/snapdom@2.23.1': {}
|
||||
|
||||
accepts@2.0.0:
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user