Skip to content

Commit

Permalink
refactor(DeviceImage): better handle image loading
Browse files Browse the repository at this point in the history
  • Loading branch information
nurikk committed May 16, 2022
1 parent e217b35 commit 3e7f69f
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 24 deletions.
61 changes: 40 additions & 21 deletions src/components/device-image/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FunctionComponent, ImgHTMLAttributes } from "react";
import React, { FunctionComponent, ImgHTMLAttributes, Suspense, useEffect } from "react";
import genericDevice from "../../images/generic-zigbee-device.png";
import { Device, DeviceState, OTAState } from "../../types";
import cx from "classnames";
Expand All @@ -12,44 +12,63 @@ type DeviceImageProps = {
deviceStatus?: DeviceState;
type?: "img" | "svg";
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getFallbackDeviceImage = (device: Device): string => genericDevice;
const getZ2mDeviceImage = (device: Device): string => `https://www.zigbee2mqtt.io/images/devices/${sanitizeZ2MDeviceName(device?.definition?.model)}.jpg`;
const getConverterDeviceImage = (device: Device): string | undefined => device.definition?.icon;

const sanitizeModelIDForImageUrl = (modelName: string): string => modelName?.replace("/", "_");

const getZ2mDeviceImage = (device: Device): string => `https://www.zigbee2mqtt.io/images/devices/${sanitizeZ2MDeviceName(device?.definition?.model)}.jpg`;
const getConverterDeviceImage = (device: Device): string | undefined => device.definition?.icon;
const getSlsDeviceImage = (device: Device): string => (`https://slsys.github.io/Gateway/devices/png/${sanitizeModelIDForImageUrl(device.model_id)}.png`);


const AVAILABLE_GENERATORS = [
getConverterDeviceImage,
getZ2mDeviceImage,
getSlsDeviceImage,
getFallbackDeviceImage
]
];

type LazyImageProps = {
device: Device;
type?: "img" | "svg";
}
const LazyImage = (props: LazyImageProps) => {
const { device, type, ...rest } = props;

const { src } = useImage({
srcList: AVAILABLE_GENERATORS
.map(fn => fn(device))
.filter(Boolean) as string[]
});
if (type === "svg") {
return <image crossOrigin={"anonymous"} {...rest} href={src} />;
}
return <img crossOrigin={"anonymous"} src={src} className={style.img} />
}

const DeviceImage: FunctionComponent<DeviceImageProps & ImgHTMLAttributes<HTMLDivElement | SVGImageElement>> = (props) => {
const { t } = useTranslation("zigbee");

const { device = {} as Device, deviceStatus, type = "img", className, ...rest } = props;
const { src } = useImage({
srcList: AVAILABLE_GENERATORS.map(fn => fn(device)) as string[],
});


if (type === "svg") {
return <Suspense fallback={<image crossOrigin={"anonymous"} {...rest} href={genericDevice} />}>
<LazyImage type="svg" device={device} {...rest}></LazyImage>
</Suspense>
}
const otaState = (deviceStatus?.update ?? {}) as OTAState;
const otaSpinner = otaState.state === "updating" ? <i title={t("updating_firmware")} className="fa fa-sync fa-spin position-absolute bottom-0 right-0" /> : null;
const interviewSpinner = device.interviewing ? <i title={t("interviewing")} className="fa fa-spinner fa-spin position-absolute bottom-0 right-0" /> : null;
const unsuccessfulInterview = !device.interviewing && !device.interview_completed ? <i title={t("interview_failed")} className="fa fa-exclamation-triangle position-absolute top-0 right-0 text-danger" /> : null;
if (type === "svg") {
return <image crossOrigin={"anonymous"} {...rest} href={src} />;
} else {
return <div className={cx(className, "position-relative")} {...rest}>
<img crossOrigin={"anonymous"} src={src} className={style.img} />
{interviewSpinner}
{otaSpinner}
{unsuccessfulInterview}
</div>;
}
const unsuccessfulInterview = !device.interviewing && !device.interview_completed;


return <div className={cx(className, "position-relative")} {...rest}>
<Suspense fallback={<img src={genericDevice} className={style.img} />}>
<LazyImage device={device}></LazyImage>
</Suspense>
{interviewSpinner}
{otaSpinner}
{unsuccessfulInterview && <i title={t("interview_failed")} className="fa fa-exclamation-triangle position-absolute top-0 right-0 text-danger" />}
</div>;

}
export default DeviceImage;
7 changes: 4 additions & 3 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'react-notifications/lib/notifications.css';
import "@fortawesome/fontawesome-free/css/all.css";
import "./styles/styles.global.scss";
import { NotificationContainer } from 'react-notifications';
import React, { FunctionComponent, Suspense } from 'react';
import React, { FunctionComponent } from 'react';
import NiceModal from '@ebay/nice-modal-react';


Expand Down Expand Up @@ -55,7 +55,7 @@ api.connect();

const Main = () => {
const { theme } = store.getState();
return <Suspense fallback="loading">
return <>
<NotificationContainer />
<I18nextProvider i18n={i18n}>
<NiceModal.Provider>
Expand Down Expand Up @@ -88,7 +88,8 @@ const Main = () => {
</Provider >
</NiceModal.Provider>
</I18nextProvider>
</Suspense>
</>

}

ReactDOM.render(<Main />, document.getElementById("root"));

1 comment on commit 3e7f69f

@xrust83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.