mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 14:39:06 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ec1179ede |
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import { getRememberedAspectRatio, rememberAspectRatio } from '../imageDimensions';
|
||||||
|
|
||||||
|
/** stand-in for a loaded HTMLImageElement */
|
||||||
|
function makeImage(naturalWidth: number, naturalHeight: number) {
|
||||||
|
return { naturalWidth, naturalHeight } as HTMLImageElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('image dimensions', () => {
|
||||||
|
test('remembers the aspect ratio of a loaded image', () => {
|
||||||
|
expect(getRememberedAspectRatio('http://ontime.local/image.png')).toBe(null);
|
||||||
|
|
||||||
|
rememberAspectRatio('http://ontime.local/image.png', makeImage(1920, 1080));
|
||||||
|
|
||||||
|
expect(getRememberedAspectRatio('http://ontime.local/image.png')).toBe(1920 / 1080);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles images which have not loaded', () => {
|
||||||
|
rememberAspectRatio('http://ontime.local/broken.png', makeImage(0, 0));
|
||||||
|
expect(getRememberedAspectRatio('http://ontime.local/broken.png')).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles missing values', () => {
|
||||||
|
expect(getRememberedAspectRatio(undefined)).toBe(null);
|
||||||
|
expect(getRememberedAspectRatio('')).toBe(null);
|
||||||
|
expect(() => rememberAspectRatio('', makeImage(100, 100))).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('forgets the least recently used entries', () => {
|
||||||
|
for (let i = 0; i < 600; i++) {
|
||||||
|
rememberAspectRatio(`http://ontime.local/${i}.png`, makeImage(100, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(getRememberedAspectRatio('http://ontime.local/0.png')).toBe(null);
|
||||||
|
expect(getRememberedAspectRatio('http://ontime.local/599.png')).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Images in the cuesheet live inside a virtualised table:
|
||||||
|
* rows are unmounted when they leave the viewport and mounted again when they come back.
|
||||||
|
* A re-mounted image has no dimensions until it is available,
|
||||||
|
* which makes the row change height and the table shift under the user.
|
||||||
|
*
|
||||||
|
* We remember the aspect ratio of the images we have already seen
|
||||||
|
* so that we can reserve the space they will take.
|
||||||
|
* This only holds two numbers per image: we leave the image data itself to the browser cache,
|
||||||
|
* which knows better than us when memory should be released.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** how many aspect ratios we remember, this is only a few bytes per entry */
|
||||||
|
const maxSize = 500;
|
||||||
|
|
||||||
|
const aspectRatios = new Map<string, number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the aspect ratio of a previously loaded image, if we have seen it before
|
||||||
|
*/
|
||||||
|
export function getRememberedAspectRatio(src: string | null | undefined): number | null {
|
||||||
|
if (!src) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return aspectRatios.get(src) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the aspect ratio of a loaded image
|
||||||
|
*/
|
||||||
|
export function rememberAspectRatio(src: string | null | undefined, image: HTMLImageElement) {
|
||||||
|
if (!src || image.naturalHeight === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the map iteration order is our LRU queue, re-adding the entry marks it as recently used
|
||||||
|
aspectRatios.delete(src);
|
||||||
|
aspectRatios.set(src, image.naturalWidth / image.naturalHeight);
|
||||||
|
|
||||||
|
while (aspectRatios.size > maxSize) {
|
||||||
|
const oldest = aspectRatios.keys().next();
|
||||||
|
if (oldest.done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
aspectRatios.delete(oldest.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-2
@@ -1,7 +1,8 @@
|
|||||||
import { memo } from 'react';
|
import { memo, useState } from 'react';
|
||||||
|
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
import Button from '../../../../common/components/buttons/Button';
|
||||||
import Input from '../../../../common/components/input/input/Input';
|
import Input from '../../../../common/components/input/input/Input';
|
||||||
|
import { getRememberedAspectRatio, rememberAspectRatio } from '../../../../common/utils/imageDimensions';
|
||||||
|
|
||||||
import style from './EditableImage.module.scss';
|
import style from './EditableImage.module.scss';
|
||||||
|
|
||||||
@@ -14,6 +15,14 @@ interface EditableImageProps {
|
|||||||
export default memo(EditableImage);
|
export default memo(EditableImage);
|
||||||
|
|
||||||
function EditableImage({ initialValue, readOnly, updateValue }: EditableImageProps) {
|
function EditableImage({ initialValue, readOnly, updateValue }: EditableImageProps) {
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
/**
|
||||||
|
* The cuesheet is virtualised: rows are unmounted once they leave the viewport.
|
||||||
|
* When the row comes back, we reserve the space the image took
|
||||||
|
* so that the table does not shift while the browser makes it available.
|
||||||
|
*/
|
||||||
|
const knownAspectRatio = getRememberedAspectRatio(initialValue);
|
||||||
|
|
||||||
const handleUpdate = (newValue: string) => {
|
const handleUpdate = (newValue: string) => {
|
||||||
if (newValue === initialValue) {
|
if (newValue === initialValue) {
|
||||||
return;
|
return;
|
||||||
@@ -62,7 +71,17 @@ function EditableImage({ initialValue, readOnly, updateValue }: EditableImagePro
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{Boolean(initialValue) && <img loading='lazy' src={initialValue} className={style.image} />}
|
<img
|
||||||
|
src={initialValue}
|
||||||
|
alt=''
|
||||||
|
className={style.image}
|
||||||
|
onLoad={(event) => {
|
||||||
|
rememberAspectRatio(initialValue, event.currentTarget);
|
||||||
|
setIsLoaded(true);
|
||||||
|
}}
|
||||||
|
/** until the image is available, we reserve the space it took the last time we saw it */
|
||||||
|
style={!isLoaded && knownAspectRatio !== null ? { aspectRatio: knownAspectRatio, width: '100%' } : undefined}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user