diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a885d58..4a14ed71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -611,6 +611,13 @@ Foundational work: faster reviews, narrower diffs, and a safety net under the pa * The artist hero in the **Queue Info** panel was rendered in a **16:10** wrap with **`object-fit: cover`**, so portrait photos always lost top and bottom equally — perceived as cropped even on roughly square sources. Now **1:1**, symmetric crop. * The **ArtistDetail** avatar extracted the cover's accent colour on every image load and painted a 36px **`boxShadow`** ring around the photo. The glow is gone (the state, the reset effect, and the per-page **`extractCoverColors`** import are dropped too; the utility itself stays in place for **`useFsDynamicAccent`**). +### Share Top Albums — full-resolution preview, Square preview fits the modal + +**By [@Psychotoxical](https://github.com/Psychotoxical), thanks to zunoz for the report on the Psysonic Discord, PR [#740](https://github.com/Psychotoxical/psysonic/pull/740)** + +* The **Square** preview was clipped at the bottom — the preview frame capped only `maxHeight: 52vh` while letting the 1:1 canvas span the full modal width, so the canvas overflowed the cap and `overflow: hidden` removed the last grid row with nothing to scroll into. Both dimensions are now capped per format (Square → **`maxWidth: 52vh`**, Story → **`min(320px, calc(52vh * 9 / 16))`**, Twitter remains modal-width-bound), so the preview always fits without clipping. +* The preview also looked **blurry** — the canvas was rendered at **540 px** wide and CSS upscaled it back to ~676 px in the 720 px modal, while cover thumbnails were decoded at only **256 px** and stretched into ~300 px tiles. The preview canvas now renders at the **full export width (1080)** and decodes covers at the **export tile size (600)**, so text is crisp and album thumbnails downsample cleanly. + ## [1.45.0] - 2026-05-04 ## Added diff --git a/src/components/StatsExportModal.tsx b/src/components/StatsExportModal.tsx index a447c7b7..9ba57722 100644 --- a/src/components/StatsExportModal.tsx +++ b/src/components/StatsExportModal.tsx @@ -282,22 +282,24 @@ export default function StatsExportModal({ open, albums, meta, onClose }: Props) } const PreviewFrame = ({ format, children }: { format: ExportFormat; children: React.ReactNode }) => { - const aspect = useMemo(() => { - if (format === 'story') return '9 / 16'; - if (format === 'square') return '1 / 1'; - return '16 / 9'; + // Aspect-aware bounds: cap BOTH dimensions so the preview always fits inside + // the modal at any ratio. The earlier version capped only `maxHeight`, so + // Square (1:1) tried to span the full modal width — the 1:1 canvas then + // overflowed `maxHeight: 52vh` and the bottom rows were clipped by + // `overflow: hidden` with no way to scroll them into view. + const { aspect, maxWidth } = useMemo(() => { + if (format === 'story') return { aspect: '9 / 16', maxWidth: 'min(320px, calc(52vh * 9 / 16))' }; + if (format === 'square') return { aspect: '1 / 1', maxWidth: '52vh' }; + return { aspect: '16 / 9', maxWidth: undefined as string | undefined }; }, [format]); - // Cap preview height so the modal stays manageable on a typical desktop. - // Narrow ratios (Story) become height-bounded; wide ratios (Twitter) are - // width-bounded by the modal itself. return (
= { twitter: { w: 1200, h: 675 }, }; -const PREVIEW_MAX_WIDTH = 540; +// Preview canvas resolution. 540 left text and album covers visibly upscaled +// (and so blurry) when CSS stretched the canvas back up to the modal width +// — match the full export width for Square/Story (1080) so the preview is +// pixel-crisp at any modal size, and only Twitter scales down (1200 → 1080). +const PREVIEW_MAX_WIDTH = 1080; /** Reads a `--var` from `document.documentElement`, with optional fallback. */ function readThemeVar(name: string, fallback: string): string { @@ -252,7 +256,10 @@ export async function renderAlbumCardCanvas(opts: ExportAlbumCardOptions): Promi } // ── Tiles ──────────────────────────────────────────────────────────────── - const desiredTilePx = preview ? 256 : 600; + // Match the export tile resolution so preview covers downsample crisply + // into the (now full-width) canvas instead of upscaling 256 → ~300 and + // blurring every album thumbnail. + const desiredTilePx = 600; const needed = gridSize * gridSize; const tilesAlbums = albums.slice(0, needed); const covers = await Promise.all(tilesAlbums.map(a => loadAlbumCover(a, desiredTilePx)));