mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fix(stats-export): full-resolution preview, fit Square in modal (#740)
* fix(stats-export): full-resolution preview, fit Square in modal Reported on the Psysonic Discord — three connected issues with the **Share Top Albums** dialog on the Statistics page: 1. **Square preview clipped.** `PreviewFrame` capped only `maxHeight: 52vh` while letting `width: 100%` + `aspectRatio: 1/1` push the 1:1 canvas far past the cap; `overflow: hidden` then chopped the bottom rows with no way to scroll them in (Twitter is width-bound, Story is width-capped at 320px, so Square was the only ratio that overflowed). Cap **both** dimensions per format — Square gets `maxWidth: 52vh`, Story gets `min(320px, calc(52vh * 9/16))`, Twitter stays width-bound by the modal — so the preview always fits. 2. **Outer scrolling didn't reveal the bottom.** Same root cause: the modal-content scroll only exposed the action buttons; the canvas itself sat inside `overflow: hidden` with nothing more to scroll to. Fixed implicitly by (1). 3. **Preview is blurry.** `PREVIEW_MAX_WIDTH = 540` rendered a 540×540 canvas that CSS then stretched back to ~676px in a 720px-wide modal, and `desiredTilePx = 256` decoded covers at 256 px only to upscale them into ~300 px tiles. Render the preview canvas at the full export width (1080) and decode covers at the export tile size (600) so text is sharp and covers downsample crisply. * docs(changelog): stats-export preview fix (PR #740)
This commit is contained in:
committed by
GitHub
parent
6cc227d761
commit
fcae65db80
@@ -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 (
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
maxHeight: '52vh',
|
||||
aspectRatio: aspect,
|
||||
margin: '0 auto',
|
||||
maxWidth: format === 'story' ? '320px' : undefined,
|
||||
maxWidth,
|
||||
maxHeight: '52vh',
|
||||
background: 'var(--glass-bg)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid var(--glass-border)',
|
||||
|
||||
@@ -32,7 +32,11 @@ const DIMENSIONS: Record<ExportFormat, { w: number; h: number }> = {
|
||||
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)));
|
||||
|
||||
Reference in New Issue
Block a user