mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 10:15:31 +00:00
Use suspense to ensure desktop images load atomically
This commit is contained in:
parent
94a681a8a0
commit
e2b5cb76bf
5 changed files with 132 additions and 20 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import WebampLazy from "../../js/webampLazy";
|
||||
import React from "react";
|
||||
import React, { Suspense } from "react";
|
||||
import WebampIcon from "./WebampIcon";
|
||||
// import Mp3Icon from "./Mp3Icon";
|
||||
import SkinIcon from "./SkinIcon";
|
||||
|
|
@ -66,23 +66,27 @@ const DemoDesktop = ({ webamp }: Props) => {
|
|||
marginLeft: HORIZONTAL_MARGIN,
|
||||
}}
|
||||
>
|
||||
{icons.map((icon, i) => {
|
||||
const row = Math.floor(i / columns);
|
||||
const column = i % columns;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
left: column * ICON_WIDTH,
|
||||
top: row * ICON_HEIGHT,
|
||||
width: ICON_WIDTH,
|
||||
position: "absolute",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Suspense
|
||||
fallback={null /* Wait for all icons to load before showing any */}
|
||||
>
|
||||
{icons.map((icon, i) => {
|
||||
const row = Math.floor(i / columns);
|
||||
const column = i % columns;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
left: column * ICON_WIDTH,
|
||||
top: row * ICON_HEIGHT,
|
||||
width: ICON_WIDTH,
|
||||
position: "absolute",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useState, useRef } from "react";
|
||||
import classnames from "classnames";
|
||||
import IconImage from "./IconImage";
|
||||
|
||||
interface Props {
|
||||
iconUrl: string;
|
||||
|
|
@ -37,7 +38,7 @@ const DesktopIcon = ({ iconUrl, onOpen, name, onDragStart }: Props) => {
|
|||
className={classnames("desktop-icon", { selected })}
|
||||
onDragStart={onDragStart}
|
||||
>
|
||||
<img src={iconUrl} style={{ width: 32, height: 32 }} />
|
||||
<IconImage src={iconUrl} />
|
||||
<div className="desktop-icon-title">{name}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useState, useRef } from "react";
|
||||
import classnames from "classnames";
|
||||
import IconImage from "./IconImage";
|
||||
|
||||
interface Props {
|
||||
iconUrl: string;
|
||||
|
|
@ -43,7 +44,7 @@ const DesktopLinkIcon = ({ iconUrl, href, name }: Props) => {
|
|||
}}
|
||||
className={classnames("desktop-icon", { selected })}
|
||||
>
|
||||
<img src={iconUrl} style={{ width: 32, height: 32 }} />
|
||||
<IconImage src={iconUrl} />
|
||||
<div className="desktop-icon-title">{name}</div>
|
||||
</a>
|
||||
);
|
||||
|
|
|
|||
9
packages/webamp/demo/js/IconImage.tsx
Normal file
9
packages/webamp/demo/js/IconImage.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React from "react";
|
||||
import SuspenseImage from "./SuspenseImage";
|
||||
|
||||
type Props = {
|
||||
src: string;
|
||||
};
|
||||
export default function IconImage({ src }: Props) {
|
||||
return <SuspenseImage src={src} style={{ width: 32, height: 32 }} />;
|
||||
}
|
||||
97
packages/webamp/demo/js/SuspenseImage.tsx
Normal file
97
packages/webamp/demo/js/SuspenseImage.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import React from "react";
|
||||
|
||||
// A Resource is an object with a read method returning the payload
|
||||
interface Resource<Payload> {
|
||||
read: () => Payload;
|
||||
}
|
||||
|
||||
type status = "pending" | "success" | "error";
|
||||
|
||||
// this function let us get a new function using the asyncFn we pass
|
||||
// this function also receives a payload and return us a resource with
|
||||
// that payload assigned as type
|
||||
function createResource<Payload>(
|
||||
asyncFn: () => Promise<Payload>
|
||||
): Resource<Payload> {
|
||||
// we start defining our resource is on a pending status
|
||||
let status: status = "pending";
|
||||
// and we create a variable to store the result
|
||||
let result: any;
|
||||
// then we immediately start running the `asyncFn` function
|
||||
// and we store the resulting promise
|
||||
const promise = asyncFn().then(
|
||||
(r: Payload) => {
|
||||
// once it's fulfilled we change the status to success
|
||||
// and we save the returned value as result
|
||||
status = "success";
|
||||
result = r;
|
||||
},
|
||||
(e: Error) => {
|
||||
// once it's rejected we change the status to error
|
||||
// and we save the returned error as result
|
||||
status = "error";
|
||||
result = e;
|
||||
}
|
||||
);
|
||||
// lately we return an error object with the read method
|
||||
return {
|
||||
read(): Payload {
|
||||
// here we will check the status value
|
||||
switch (status) {
|
||||
case "pending":
|
||||
// if it's still pending we throw the promise
|
||||
// throwing a promise is how Suspense know our component is not ready
|
||||
throw promise;
|
||||
case "error":
|
||||
// if it's error we throw the error
|
||||
throw result;
|
||||
case "success":
|
||||
// if it's success we return the result
|
||||
return result;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// First we need a type of cache to avoid creating resources for images
|
||||
// we have already fetched in the past
|
||||
const cache = new Map<string, any>();
|
||||
|
||||
// then we create our loadImage function, this function receives the source
|
||||
// of the image and returns a resource
|
||||
function loadImage(source: string): Resource<string> {
|
||||
// here we start getting the resource from the cache
|
||||
let resource = cache.get(source);
|
||||
// and if it's there we return it immediately
|
||||
if (resource) return resource;
|
||||
// but if it's not we create a new resource
|
||||
resource = createResource<string>(
|
||||
() =>
|
||||
// in our async function we create a promise
|
||||
new Promise((resolve, reject) => {
|
||||
// then create a new image element
|
||||
const img = new window.Image();
|
||||
// set the src to our source
|
||||
img.src = source;
|
||||
// and start listening for the load event to resolve the promise
|
||||
img.addEventListener("load", () => resolve(source));
|
||||
// and also the error event to reject the promise
|
||||
img.addEventListener("error", () =>
|
||||
reject(new Error(`Failed to load image ${source}`))
|
||||
);
|
||||
})
|
||||
);
|
||||
// before finishing we save the new resource in the cache
|
||||
cache.set(source, resource);
|
||||
// and return return it
|
||||
return resource;
|
||||
}
|
||||
|
||||
export default function SuspenseImage(
|
||||
props: React.ImgHTMLAttributes<HTMLImageElement>
|
||||
): JSX.Element {
|
||||
if (props.src != null) {
|
||||
loadImage(props.src).read();
|
||||
}
|
||||
return <img {...props} />;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue