@mui/material#CardContent TypeScript Examples

The following examples show how to use @mui/material#CardContent. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.tsx    From ExpressLRS-Configurator with GNU General Public License v3.0 7 votes vote down vote up
CardTitle: FunctionComponent<CardTitleProps> = memo(({ icon, title }) => {
  return (
    <CardContent>
      <Grid container spacing={1} alignItems="center">
        <Grid item>{icon}</Grid>
        <Grid item flexGrow={1}>
          <Typography variant="h6">{title}</Typography>
        </Grid>
      </Grid>
    </CardContent>
  );
})
Example #2
Source File: BlogEntryPreview.tsx    From firecms with MIT License 6 votes vote down vote up
export function ProductPreview({ productValues }: { productValues: EntityValues<Product> }) {

    if (!productValues)
        return <></>;

    return <CardActionArea style={{
        width: "400px",
        height: "400px",
        margin: "16px",
        boxShadow: "rgb(0 0 0 / 8%) 0px 8px 12px -4px"
    }}>
        <CardContent style={{
            height: "100%",
            display: "flex",
            flexDirection: "column"
        }}>
            <Box flexGrow={1} flexShrink={1} flexBasis={296} p={2}
                 maxHeight={296}>
                <StorageImage storagePath={productValues.main_image}/>
            </Box>
            <Typography gutterBottom
                        variant="h6"
                        noWrap
                        style={{
                            marginTop: "16px"
                        }}>
                {productValues.name}
            </Typography>

            <Typography variant="body2"
                        color="textSecondary"
                        component="div">
                {productValues.price} {productValues.currency}
            </Typography>
        </CardContent>
    </CardActionArea>;

}
Example #3
Source File: index.tsx    From ExpressLRS-Configurator with GNU General Public License v3.0 6 votes vote down vote up
LogsView: FunctionComponent = () => {
  const onLogs = () => {
    ipcRenderer.send(IpcRequest.OpenLogsFolder);
  };
  return (
    <MainLayout>
      <Card>
        <CardTitle icon={<ListIcon />} title="Logs" />
        <Divider />
        <CardContent>
          <Button
            color="primary"
            size="large"
            variant="contained"
            onClick={onLogs}
          >
            Open logs folder
          </Button>
        </CardContent>
      </Card>
    </MainLayout>
  );
}
Example #4
Source File: StatDisplayComponent.tsx    From genshin-optimizer with MIT License 6 votes vote down vote up
function Section({ displayNs, sectionKey }: { displayNs: DisplaySub<NodeDisplay>, sectionKey: string }) {
  const { data, oldData } = useContext(DataContext)
  const header = usePromise(getDisplayHeader(data, sectionKey), [data, sectionKey])
  const displayNsReads = useMemo(() => objectMap(displayNs, (n, nodeKey) => customRead(["display", sectionKey, nodeKey])), [displayNs, sectionKey]);
  if (!header) return <CardDark></CardDark>

  const { title, icon, action } = header
  return <CardDark >
    <CardHeader avatar={icon && <ImgIcon size={2} sx={{ m: -1 }} src={icon} />} title={title} action={action} titleTypographyProps={{ variant: "subtitle1" }} />
    <Divider />
    <CardContent>
      {Object.entries(displayNs).map(([nodeKey, n]) => <NodeFieldDisplay key={nodeKey} node={n} oldValue={oldData ? oldData.get(displayNsReads[nodeKey]!).value : undefined} />)}
    </CardContent>
  </CardDark>
}
Example #5
Source File: AppOverview.tsx    From mui-toolpad with MIT License 6 votes vote down vote up
function PageCard({ page }: PageCardProps) {
  return (
    <Card sx={{ gridColumn: 'span 1' }}>
      <CardActionArea component={Link} to={`/pages/${page.id}`}>
        <CardContent>
          <Typography gutterBottom variant="h5" component="div">
            {page.name}
          </Typography>
          <Typography variant="body2" color="text.secondary">
            {page.attributes.title.value}
          </Typography>
        </CardContent>
      </CardActionArea>
    </Card>
  );
}
Example #6
Source File: Dashboard.tsx    From NekoMaid with MIT License 6 votes vote down vote up
TopCard: React.FC<{ title: string, content: React.ReactNode, icon: React.ReactNode, color: string }> = ({ title, content, icon, children, color }) =>
  <Card sx={{ height: '100%' }}>
    <CardContent>
      <Grid container spacing={3} sx={{ justifyContent: 'space-between', flexWrap: 'nowrap' }}>
        <Grid item sx={{ overflow: 'hidden' }}>
          <Typography color='textSecondary' gutterBottom variant='h6'>{title}</Typography>
          <Typography color='textPrimary' variant='h4' noWrap sx={{ textOverflow: 'ellipsis' }}>{content}</Typography>
        </Grid>
        <Grid item sx={{ paddingLeft: '0 !important' }}>
          <Avatar sx={{ backgroundColor: color, height: 50, width: 50 }}>{icon}</Avatar>
        </Grid>
      </Grid>
      {children}
    </CardContent>
  </Card>
Example #7
Source File: PrincipleCard.tsx    From frontend with MIT License 6 votes vote down vote up
export default function PrincipleCard({ Icon, heading, content }: PrincipleCardProps) {
  return (
    <StyledCard elevation={0}>
      <CardHeader
        className={classes.cardHeader}
        avatar={<Icon className={classes.icon} />}
        title={
          <Typography variant="body2" component="div" className={classes.heading}>
            {heading}
          </Typography>
        }
        classes={{
          action: classes.cardHeaderAction,
          content: classes.cardHeaderTitleRoot,
        }}
      />
      <CardContent className={classes.contentContainer}>
        <Typography className={classes.content} variant="body1">
          {content}
        </Typography>
      </CardContent>
    </StyledCard>
  )
}
Example #8
Source File: item-card.tsx    From sdk with MIT License 6 votes vote down vote up
export function ItemCard({ item }: IItemCardProps) {
	return (
		<Card sx={{ width: 200 }}>
			<CardHeader
				sx={{
					display: "flex",
					overflow: "hidden",
					"& .MuiCardHeader-content": {
						overflow: "hidden"
					}
				}}
				title={<Typography noWrap gutterBottom variant="h6" component="h4">
					{item.meta ? item.meta.name : "No metadata"}
				</Typography>}
			/>
			<ItemMedia url={getMetaImageUrl(item.meta?.content)}/>
			<CardContent>
				<Typography variant="body2" color="text.secondary" sx={{ textAlign: "right" }}>
					<strong>Supply: {item.supply}</strong>
				</Typography>
			</CardContent>
			<CardActions>
				<Button
					size="small"
					component={Link}
					to={`/sell/${item.id}`}
				>
					Sell
				</Button>
				{/*<Button size="small" color={"warning"}>Burn</Button>*/}
			</CardActions>
		</Card>
	)
}
Example #9
Source File: index.tsx    From ExpressLRS-Configurator with GNU General Public License v3.0 5 votes vote down vote up
SupportView: FunctionComponent = () => {
  return (
    <MainLayout>
      <Card>
        <CardTitle icon={<SettingsIcon />} title="Support" />
        <Divider />
        <CardContent sx={styles.listContainer}>
          <p>Need help? Confused? Join the Community!</p>
          <ul className="linksList">
            <li>
              <Button
                target="_blank"
                variant="contained"
                rel="noreferrer noreferrer"
                href="https://www.expresslrs.org/"
              >
                ExpressLRS Documentation
              </Button>
            </li>
            <li>
              <Button
                target="_blank"
                variant="contained"
                rel="noreferrer noreferrer"
                href="https://discord.gg/dS6ReFY"
              >
                Discord Chat
              </Button>
            </li>
            <li>
              <Button
                target="_blank"
                variant="contained"
                rel="noreferrer noreferrer"
                href="https://www.facebook.com/groups/636441730280366"
              >
                Facebook Group
              </Button>
            </li>
          </ul>
        </CardContent>
        <Divider />
        <CardTitle icon={<SettingsIcon />} title="Troubleshooting" />
        <Divider />
        <CardContent>
          <ClearPlatformioDependencies />
          <ClearFirmwareFiles />
        </CardContent>
        <Divider />
        <CardTitle icon={<SettingsIcon />} title="Legal disclaimer" />
        <Divider />
        <CardContent>
          <p>
            The use and operation of this type of device may require a license,
            and some countries may forbid its use. It is entirely up to the end
            user to ensure compliance with local regulations. This is
            experimental software / hardware and there is no guarantee of
            stability or reliability. USE AT YOUR OWN RISK.
          </p>
        </CardContent>
      </Card>
    </MainLayout>
  );
}
Example #10
Source File: CharacterCard.tsx    From genshin-optimizer with MIT License 5 votes vote down vote up
export default function CharacterCard({ characterKey, artifactChildren, weaponChildren, characterChildren, onClick, onClickHeader, footer, isTeammateCard }: CharacterCardProps) {
  const { teamData: teamDataContext } = useContext(DataContext)
  const teamData = useTeamData(teamDataContext ? "" : characterKey) ?? (teamDataContext as TeamData | undefined)
  const { character, characterSheet, target: data } = teamData?.[characterKey] ?? {}
  const onClickHandler = useCallback(() => characterKey && onClick?.(characterKey, "overview"), [characterKey, onClick])
  const actionWrapperFunc = useCallback(
    children => <CardActionArea onClick={onClickHandler} sx={{ flexGrow: 1, display: "flex", flexDirection: "column" }}>{children}</CardActionArea>,
    [onClickHandler],
  )
  const characterDispatch = useCharacterReducer(characterKey)
  if (!teamData || !character || !characterSheet || !data) return null;
  const dataContextObj: dataContextObj = {
    character,
    data,
    characterSheet,
    mainStatAssumptionLevel: 0,
    teamData,
    characterDispatch
  }

  return <Suspense fallback={<Skeleton variant="rectangular" sx={{ width: "100%", height: "100%", minHeight: 350 }} />}>
    <DataContext.Provider value={dataContextObj}>
      <CardLight sx={{ height: "100%", display: "flex", flexDirection: "column" }}>
        <Box sx={{ display: "flex", position: "absolute", zIndex: 2, opacity: 0.7 }}>
          <IconButton sx={{ p: 0.5 }} onClick={event => characterDispatch({ favorite: !character.favorite })}>
            {character.favorite ? <Favorite /> : <FavoriteBorder />}
          </IconButton>
        </Box>
        <ConditionalWrapper condition={!!onClick} wrapper={actionWrapperFunc} >
          <Header onClick={!onClick ? onClickHeader : undefined} />
          <CardContent sx={{ width: "100%", display: "flex", flexDirection: "column", gap: 1, flexGrow: 1 }}>
            <Artifacts />
            {!isTeammateCard && <Grid container columns={4} spacing={0.75}>
              <Grid item xs={1} height="100%">
                <WeaponCardPico weaponId={character.equippedWeapon} />
              </Grid>
              {range(0, 2).map(i => <Grid key={i} item xs={1} height="100%"><CharacterCardPico characterKey={character.team[i]} index={i} /></Grid>)}
            </Grid>}
            {isTeammateCard && <WeaponFullCard weaponId={character.equippedWeapon} />}
            {!isTeammateCard && <Stats />}
            {weaponChildren}
            {artifactChildren}
            {characterChildren}
          </CardContent>
        </ConditionalWrapper>
        {footer}
      </CardLight>
    </DataContext.Provider>
  </Suspense>
}
Example #11
Source File: Dashboard.tsx    From NekoMaid with MIT License 5 votes vote down vote up
Players: React.FC<{ players?: CurrentStatus['players'] }> = React.memo(({ players }) => {
  const his = useHistory()
  const plugin = usePlugin()
  const globalData = useGlobalData()
  const [page, setPage] = useState(1)
  const [id, update] = useState(0)
  return <Card>
    <CardHeader title={lang.dashboard.onlinePlayers} />
    <Divider />
    <CardContent>
      {players?.length === 0
        ? <Empty />
        : <>
        <List sx={{ paddingTop: 0 }}>
          {players
            ? players.slice((page - 1) * 8, page * 8).map(p => {
              const name = typeof p === 'string' ? p : p.name
              return <Tooltip key={name} title={'IP: ' + ((p as any).ip || lang.unknown)}>
                <ListItem
                  secondaryAction={<>
                    <IconButton
                      edge='end'
                      size='small'
                      onClick={() => dialog(lang.dashboard.confirmKick(<span className='bold'>{name}</span>), lang.reason)
                        .then(it => it != null && plugin.emit('dashboard:kick', (res: boolean) => {
                          action(res)
                          if (!players) return
                          players.splice(players.indexOf(it!), 1)
                          update(id + 1)
                        }, name, it || null))
                      }
                    ><ExitToApp /></IconButton>
                    <IconButton edge='end' onClick={() => his.push('/NekoMaid/playerList/' + name)} size='small'><MoreHoriz /></IconButton>
                  </>
                  }
                >
                  <ListItemAvatar>
                    <Avatar
                      src={getSkin(globalData, name, true)}
                      imgProps={{ crossOrigin: 'anonymous', onClick () { his.push('/NekoMaid/playerList/' + name) }, style: { width: 40, height: 40 } }}
                      sx={{ cursor: 'pointer' }}
                      variant='rounded'
                    />
                  </ListItemAvatar>
                  <ListItemText primary={name} />
                </ListItem>
              </Tooltip>
            })
            : <LoadingList />
          }
        </List>
        {players && <Pagination
          page={page}
          onChange={(_, it) => setPage(it)}
          count={Math.max(Math.ceil(players.length / 8), 1)}
          sx={{ display: 'flex', justifyContent: 'flex-end' }}
        />}
      </>}
    </CardContent>
  </Card>
})
Example #12
Source File: AdminPage.tsx    From frontend with MIT License 5 votes vote down vote up
export default function AdminPage() {
  const { t } = useTranslation()
  const router = useRouter()
  const { data: session, status } = useSession()
  if (status !== 'authenticated') {
    return (
      <AdminLayout>
        <AdminContainer title={t('nav.admin.index')}>
          <Box p={3}>
            <Typography variant="h6">Not authenticated</Typography>
          </Box>
        </AdminContainer>
      </AdminLayout>
    )
  }

  if (!isAdmin(session)) {
    return (
      <AdminLayout>
        <AdminContainer title={t('nav.admin.index')}>
          <Box p={3}>
            <Typography variant="h6">Not authorized</Typography>
          </Box>
        </AdminContainer>
      </AdminLayout>
    )
  }

  return (
    <AdminLayout>
      <AdminContainer title={t('nav.admin.index')}>
        <Box p={4}>
          <Typography variant="h6">{'Добре дошли!'}</Typography>
        </Box>
        <Grid container spacing={2} rowSpacing={4} px={4} pb={4}>
          {menuItems.map(({ label, href, icon: Icon }, index) => (
            <Grid xs={12} sm={6} md={4} lg={2} item key={index}>
              <Card sx={{ maxWidth: 345 }}>
                <CardActionArea onClick={() => router.push(href)}>
                  <CardContent>
                    <Box textAlign="center">
                      <Icon fontSize="medium" />
                    </Box>
                    <Typography gutterBottom variant="h6" component="div" textAlign="center">
                      {label}
                    </Typography>
                  </CardContent>
                </CardActionArea>
              </Card>
            </Grid>
          ))}
        </Grid>
      </AdminContainer>
    </AdminLayout>
  )
}
Example #13
Source File: itemCard.tsx    From Search-Next with GNU General Public License v3.0 5 votes vote down vote up
ItemCard: React.FC<ItemCardProps> = ({
  title,
  desc,
  icon,
  onClick,
  action,
  size = 'medium',
  ...props
}) => {
  const Content = (
    <CardContent
      className={classNames(
        size === 'medium' && 'px-4 py-3',
        size === 'small' && 'px-3 py-2',
        onClick && 'cursor-pointer',
      )}
      onClick={() => (onClick ? onClick() : null)}
    >
      <div className="flex">
        <div className="flex-grow flex items-center justify-start">
          {icon && <div className="mr-1">{icon}</div>}
          <div>
            <p className="text-sm mb-0">{title}</p>
            {desc && <p className="text-xs mb-0 text-gray-700">{desc}</p>}
          </div>
        </div>
        <div className="flex items-center" onClick={(e) => e.stopPropagation()}>
          {action ? action : <KeyboardArrowRight fontSize="small" />}
        </div>
      </div>
    </CardContent>
  );

  return (
    <StyleCard type="border">
      {action ? (
        Content
      ) : (
        <CardActionArea onClick={() => (onClick ? onClick() : null)}>
          {Content}
        </CardActionArea>
      )}
    </StyleCard>
  );
}
Example #14
Source File: FireCMSHomePage.tsx    From firecms with MIT License 4 votes vote down vote up
/**
 * Default entry view for the CMS under the path "/"
 * This components takes navigation as an input and renders cards
 * for each entry, including title and description.
 * @constructor
 * @category Components
 */
export function FireCMSHomePage() {

    const classes = useStyles();
    const navigationContext = useNavigation();
    if (!navigationContext.navigation)
        return <></>;

    const {
        navigationEntries,
        groups
    } = computeTopNavigation(navigationContext, false);

    const allGroups: Array<string | null> = [...groups];
    if (navigationEntries.filter(e => !e.group).length > 0) {
        allGroups.push(null);
    }

    function buildNavigationCard(entry: TopNavigationEntry) {
        return (
            <Grid item xs={12}
                  sm={6}
                  md={4}
                  key={`nav_${entry.group}_${entry.name}`}>
                <Paper variant={"outlined"}>

                    <CardActionArea
                        className={classes.card}
                        component={ReactLink}
                        to={entry.url}>
                        <CardContent
                            className={classes.flexGrow}>

                            <PlaylistPlayIcon color={"disabled"}/>
                            <Typography gutterBottom variant="h5"
                                        component="h2">
                                {entry.name}
                            </Typography>

                            {entry.description && <Typography variant="body2"
                                                              color="textSecondary"
                                                              component="div">
                                <Markdown source={entry.description}/>
                            </Typography>}
                        </CardContent>

                        <CardActions style={{ alignSelf: "flex-end" }}>
                            <Box p={1}>
                                <ArrowForwardIcon color="primary"/>
                            </Box>
                        </CardActions>

                    </CardActionArea>
                </Paper>
            </Grid>
        );
    }

    return (
        <Container>
            {allGroups.map((group, index) => (
                <Box mt={6} mb={6} key={`group_${index}`}>
                    {allGroups.length > 0 && <>
                        <Typography color={"textSecondary"}
                                    className={"weight-500"}>
                            {group?.toUpperCase() ?? "Collections".toUpperCase()}
                        </Typography>
                        <Divider/>
                    </>}

                    <Box mt={2}>
                        <Grid container spacing={2}>
                            {group && navigationEntries
                                .filter((entry) => entry.group === group)
                                .map((entry) => buildNavigationCard(entry))
                            }
                            {!group && navigationEntries
                                .filter((entry) => !entry.group)
                                .map((entry) => buildNavigationCard(entry))
                            }
                        </Grid>
                    </Box>
                </Box>
            ))}
        </Container>
    );
}
Example #15
Source File: ThemeList.tsx    From Cromwell with MIT License 4 votes vote down vote up
render() {
        const { isLoading, packages, installedThemes, cmsConfig, isChangingTheme } = this.state;
        return (
            <div className={styles.ThemeList} ref={this.pageRef}>
                <Button
                    className={styles.addBtn}
                    onClick={this.handleOpenMarket}
                    variant="contained"
                    color="primary"
                    startIcon={<AddCircleOutlineIcon />}
                >Add themes</Button>
                {isLoading && (
                    <div className={styles.list}>
                        {Array(2).fill(1).map((it, index) => (
                            <Skeleton key={index} variant="rectangular" height="388px" width="300px" style={{ margin: '0 10px 20px 10px' }} > </Skeleton>
                        ))}
                    </div>
                )}
                {!isLoading &&
                    <div className={styles.list}>
                        {packages.map(info => {
                            const isActive = Boolean(cmsConfig && cmsConfig.themeName === info.name);
                            const entity = installedThemes?.find(ent => ent.name === info.name);
                            const isInstalled = entity?.isInstalled ?? false;
                            const availableUpdate = this.themeUpdates[info.name];
                            const isUnderUpdate = this.themeUnderUpdate[info.name];

                            return (
                                <div className={`${styles.themeCard} ${commonStyles.paper}`} key={info.name}>
                                    <CardActionArea
                                        className={styles.cardActionArea}
                                        style={{ opacity: isUnderUpdate ? 0.5 : 1 }}
                                    >
                                        <div
                                            style={{ backgroundImage: `url("data:image/png;base64,${info.image}")` }}
                                            className={styles.themeImage}
                                        ></div>
                                        <CardContent className={styles.mainInfo}>
                                            <Badge color="secondary" badgeContent={isActive ? 'Active' : null}>
                                                <Typography gutterBottom variant="h5" component="h2" className={styles.themeTitle}>
                                                    {info.title}
                                                </Typography>
                                            </Badge>
                                            <p className={styles.version}
                                                onClick={this.handleShowUpdate(entity, info, availableUpdate)}
                                                style={{ cursor: availableUpdate ? 'pointer' : 'initial' }}
                                            >{(info?.version ?? '') +
                                                (availableUpdate ? ' > ' + availableUpdate.version + ' Open info' : '')}</p>
                                            <Typography variant="body2" color="textSecondary" component="p">
                                                {info.excerpt}
                                            </Typography>
                                        </CardContent>
                                    </CardActionArea>
                                    <CardActions
                                        style={{ opacity: isUnderUpdate ? 0.5 : 1 }}
                                        className={styles.themeActions}
                                        disableSpacing
                                    >
                                        {!isInstalled && (
                                            <Button
                                                disabled={isUnderUpdate || isChangingTheme}
                                                size="small" color="primary" variant="contained"
                                                onClick={this.handleActivateTheme(info.name)}
                                            >Install theme</Button>
                                        )}
                                        {isInstalled && isActive && (
                                            <Button
                                                disabled={isUnderUpdate || isChangingTheme}
                                                size="small" color="primary" variant="contained"
                                                onClick={() => {
                                                    const route = `${themeEditPageInfo.baseRoute}`;
                                                    this.props.history.push(route);
                                                }}
                                            >
                                                Edit theme
                                            </Button>
                                        )}
                                        {availableUpdate && (
                                            <Button
                                                disabled={isUnderUpdate || isChangingTheme}
                                                size="small" color="primary" variant="contained"
                                                onClick={() => this.startUpdate(info)}
                                            >Update</Button>
                                        )}
                                        {isInstalled && !isActive && (
                                            <Button size="small" color="primary" variant="contained"
                                                onClick={() => this.handleSetActiveTheme(info)}
                                                disabled={isUnderUpdate || isChangingTheme}
                                            >Set active</Button>
                                        )}
                                        <Button size="small" color="primary" variant="outlined"
                                            disabled={isUnderUpdate || isChangingTheme}
                                            onClick={() => this.handleDelete(info)}
                                        >Delete</Button>
                                        <Button size="small" color="primary" variant="outlined"
                                            onClick={() => this.openTheme(info)}
                                        >Info</Button>
                                        {isUnderUpdate && (
                                            <LinearProgress className={styles.updateProgress} />
                                        )}
                                    </CardActions>
                                </div>
                            )
                        })}
                    </div>}
                <LoadingStatus isActive={isChangingTheme} />
                {/* <ManagerLogger isActive={isChangingTheme} /> */}
                <Modal
                    open={!!this.state.updateModalInfo}
                    onClose={() => this.setState({ updateModalInfo: null })}
                    className={commonStyles.center}
                    blurSelector="#root"
                >
                    <UpdateModalContent
                        underUpdate={this.themeUnderUpdate}
                        {...(this.state?.updateModalInfo ?? {})}
                        onStartUpdate={this.startUpdate}
                        onClose={() => this.setState({ updateModalInfo: null })}
                    />
                </Modal>
                <Modal
                    open={!!this.state.openedTheme}
                    blurSelector="#root"
                    className={commonStyles.center}
                    onClose={() => this.setState({ openedTheme: undefined })}
                >
                    {this.state?.openedTheme && (
                        <MarketModal
                            installedModules={this.state?.installedThemes ?? []}
                            data={this.state.openedTheme}
                            noInstall
                        />
                    )}
                </Modal>
            </div>
        )

    }
Example #16
Source File: index.tsx    From ExpressLRS-Configurator with GNU General Public License v3.0 4 votes vote down vote up
ConfiguratorView: FunctionComponent<ConfiguratorViewProps> = (props) => {
  const {
    gitRepository,
    selectedDevice,
    networkDevices,
    onDeviceChange,
    deviceType,
  } = props;

  const [viewState, setViewState] = useState<ViewState>(
    ViewState.Configuration
  );

  const { setAppStatus } = useAppState();

  const [progressNotifications, setProgressNotifications] = useState<
    BuildProgressNotification[]
  >([]);
  const progressNotificationsRef = useRef<BuildProgressNotification[]>([]);
  const [
    lastProgressNotification,
    setLastProgressNotification,
  ] = useState<BuildProgressNotification | null>(null);

  useBuildProgressNotificationsSubscription({
    onSubscriptionData: (options) => {
      const args = options.subscriptionData.data?.buildProgressNotifications;
      if (args !== undefined) {
        const newNotificationsList = [
          ...progressNotificationsRef.current,
          args,
        ];
        progressNotificationsRef.current = newNotificationsList;
        setProgressNotifications(newNotificationsList);
        setLastProgressNotification(args);
      }
    },
  });

  /*
    We batch log events in order to save React.js state updates and rendering performance.
   */
  const [logs, setLogs] = useState<string>('');
  const logsRef = useRef<string[]>([]);
  const eventsBatcherRef = useRef<EventsBatcher<string> | null>(null);
  useEffect(() => {
    eventsBatcherRef.current = new EventsBatcher<string>(200);
    eventsBatcherRef.current.onBatch((newLogs) => {
      const newLogsList = [...logsRef.current, ...newLogs];
      logsRef.current = newLogsList;
      setLogs(newLogsList.join(''));
    });
  }, []);
  useBuildLogUpdatesSubscription({
    fetchPolicy: 'network-only',
    onSubscriptionData: (options) => {
      const args = options.subscriptionData.data?.buildLogUpdates.data;
      if (args !== undefined && eventsBatcherRef.current !== null) {
        eventsBatcherRef.current.enqueue(args);
      }
    },
  });

  const [
    firmwareVersionData,
    setFirmwareVersionData,
  ] = useState<FirmwareVersionDataInput | null>(null);
  const [firmwareVersionErrors, setFirmwareVersionErrors] = useState<Error[]>(
    []
  );
  const onFirmwareVersionData = useCallback(
    (data: FirmwareVersionDataInput) => {
      setFirmwareVersionErrors([]);
      setFirmwareVersionData(data);
    },
    []
  );

  const [deviceTarget, setDeviceTarget] = useState<Target | null>(null);
  const [deviceTargetErrors, setDeviceTargetErrors] = useState<Error[]>([]);

  const onDeviceTarget = useCallback(
    (data: Target | null) => {
      setDeviceTargetErrors([]);
      setDeviceTarget(data);
      // if target was manually changed, set selected device to null
      onDeviceChange(null);
    },
    [onDeviceChange]
  );

  const [deviceTargets, setDeviceTargets] = useState<Device[] | null>(null);

  const [
    fetchDeviceTargets,
    {
      loading: loadingTargets,
      data: targetsResponse,
      error: targetsResponseError,
    },
  ] = useAvailableFirmwareTargetsLazyQuery({
    fetchPolicy: 'network-only',
  });

  const [
    fetchLuaScript,
    { data: luaScriptResponse, error: luaScriptResponseError },
  ] = useLuaScriptLazyQuery();

  const device = useMemo(() => {
    return deviceTargets?.find((d) => {
      return d.targets.find((target) => target.id === deviceTarget?.id);
    });
  }, [deviceTarget, deviceTargets]);

  useEffect(() => {
    if (
      firmwareVersionData === null ||
      validateFirmwareVersionData(firmwareVersionData).length > 0
    ) {
      setDeviceTargets(null);
    } else {
      fetchDeviceTargets({
        variables: {
          source: firmwareVersionData.source as FirmwareSource,
          gitBranch: firmwareVersionData.gitBranch!,
          gitTag: firmwareVersionData.gitTag!,
          gitCommit: firmwareVersionData.gitCommit!,
          localPath: firmwareVersionData.localPath!,
          gitPullRequest: firmwareVersionData.gitPullRequest,
          gitRepository: {
            url: gitRepository.url,
            owner: gitRepository.owner,
            repositoryName: gitRepository.repositoryName,
            rawRepoUrl: gitRepository.rawRepoUrl,
            srcFolder: gitRepository.srcFolder,
          },
        },
      });
    }
  }, [gitRepository, firmwareVersionData, fetchDeviceTargets]);

  useEffect(() => {
    if (targetsResponse?.availableFirmwareTargets) {
      setDeviceTargets([...targetsResponse.availableFirmwareTargets]);
    } else {
      setDeviceTargets(null);
    }
  }, [targetsResponse]);

  const [
    deviceOptionsFormData,
    setDeviceOptionsFormData,
  ] = useState<DeviceOptionsFormData>({
    userDefinesTxt: '',
    userDefinesMode: UserDefinesMode.UserInterface,
    userDefineOptions: [],
  });

  const handleDeviceOptionsResponse = async (
    deviceOptionsResponse: TargetDeviceOptionsQuery
  ) => {
    const storage = new ApplicationStorage();
    const deviceName = device?.name || null;
    const userDefineOptions = await mergeWithDeviceOptionsFromStorage(
      storage,
      deviceName,
      {
        ...deviceOptionsFormData,
        userDefineOptions: [...deviceOptionsResponse.targetDeviceOptions],
      }
    );

    // if a network device is selected, merge in its options
    if (selectedDevice && networkDevices.has(selectedDevice)) {
      const networkDevice = networkDevices.get(selectedDevice);
      userDefineOptions.userDefineOptions = userDefineOptions.userDefineOptions.map(
        (userDefineOption) => {
          const networkDeviceOption = networkDevice?.options.find(
            (item) => item.key === userDefineOption.key
          );

          const newUserDefineOption = { ...userDefineOption };
          if (networkDeviceOption) {
            newUserDefineOption.enabled = networkDeviceOption.enabled;
            newUserDefineOption.value = networkDeviceOption.value;
          }
          return newUserDefineOption;
        }
      );
    }

    setDeviceOptionsFormData(userDefineOptions);
  };
  const [
    fetchOptions,
    {
      loading: loadingOptions,
      data: deviceOptionsResponse,
      error: deviceOptionsResponseError,
    },
  ] = useTargetDeviceOptionsLazyQuery({
    fetchPolicy: 'network-only',
    onCompleted: (data) => {
      handleDeviceOptionsResponse(data).catch((err) => {
        console.error('failed to handle device options response', err);
      });
    },
  });

  useEffect(() => {
    if (
      deviceTarget === null ||
      firmwareVersionData === null ||
      validateFirmwareVersionData(firmwareVersionData).length > 0
    ) {
      setDeviceOptionsFormData({
        userDefinesTxt: '',
        userDefinesMode: UserDefinesMode.UserInterface,
        userDefineOptions: [],
      });
    } else {
      fetchOptions({
        variables: {
          target: deviceTarget.name,
          source: firmwareVersionData.source as FirmwareSource,
          gitBranch: firmwareVersionData.gitBranch!,
          gitTag: firmwareVersionData.gitTag!,
          gitCommit: firmwareVersionData.gitCommit!,
          localPath: firmwareVersionData.localPath!,
          gitPullRequest: firmwareVersionData.gitPullRequest,
          gitRepository: {
            url: gitRepository.url,
            owner: gitRepository.owner,
            repositoryName: gitRepository.repositoryName,
            rawRepoUrl: gitRepository.rawRepoUrl,
            srcFolder: gitRepository.srcFolder,
          },
        },
      });
    }
  }, [deviceTarget, firmwareVersionData, gitRepository, fetchOptions]);

  const onResetToDefaults = () => {
    const handleReset = async () => {
      if (deviceOptionsResponse === undefined || deviceTarget === null) {
        // eslint-disable-next-line no-alert
        alert(`deviceOptionsResponse is undefined`);
        return;
      }
      const deviceName = device?.name || null;
      if (deviceName) {
        const storage = new ApplicationStorage();
        await storage.removeDeviceOptions(deviceName);

        const userDefineOptions = await mergeWithDeviceOptionsFromStorage(
          storage,
          deviceName,
          {
            ...deviceOptionsFormData,
            userDefineOptions: [...deviceOptionsResponse.targetDeviceOptions],
          }
        );
        setDeviceOptionsFormData(userDefineOptions);
      }
    };
    handleReset().catch((err) => {
      console.error(`failed to reset device options form data: ${err}`);
    });
  };

  const onUserDefines = useCallback(
    (data: DeviceOptionsFormData) => {
      setDeviceOptionsFormData(data);
      if (deviceTarget !== null) {
        const storage = new ApplicationStorage();
        const deviceName = device?.name;
        if (deviceName) {
          persistDeviceOptions(storage, deviceName, data).catch((err) => {
            console.error(`failed to persist user defines: ${err}`);
          });
        }
      }
    },
    [deviceTarget, deviceTargets]
  );

  const [
    buildFlashFirmwareMutation,
    {
      loading: buildInProgress,
      data: response,
      error: buildFlashErrorResponse,
    },
  ] = useBuildFlashFirmwareMutation();

  useEffect(() => {
    const arg = response?.buildFlashFirmware?.firmwareBinPath;
    if (arg !== undefined && arg !== null && arg?.length > 0) {
      const body: OpenFileLocationRequestBody = {
        path: arg,
      };
      ipcRenderer.send(IpcRequest.OpenFileLocation, body);
    }
  }, [response]);

  const isTX = useMemo(() => {
    if (deviceTarget) {
      return deviceTarget.name?.indexOf('_TX_') > -1;
    }
    return false;
  }, [deviceTarget]);

  const hasLuaScript = useMemo(() => {
    return deviceType === DeviceType.ExpressLRS && isTX;
  }, [deviceType, isTX]);

  useEffect(() => {
    if (firmwareVersionData && isTX && hasLuaScript) {
      fetchLuaScript({
        variables: {
          source: firmwareVersionData.source as FirmwareSource,
          gitBranch: firmwareVersionData.gitBranch!,
          gitTag: firmwareVersionData.gitTag!,
          gitCommit: firmwareVersionData.gitCommit!,
          localPath: firmwareVersionData.localPath!,
          gitPullRequest: firmwareVersionData.gitPullRequest,
          gitRepository: {
            url: gitRepository.url,
            owner: gitRepository.owner,
            repositoryName: gitRepository.repositoryName,
            rawRepoUrl: gitRepository.rawRepoUrl,
            srcFolder: gitRepository.srcFolder,
          },
        },
      });
    }
  }, [gitRepository, firmwareVersionData, fetchLuaScript, isTX, hasLuaScript]);

  /*
    Display Electron.js confirmation dialog if user wants to shutdown the app
    when build is in progress.
   */
  useEffect(() => {
    const body: UpdateBuildStatusRequestBody = {
      buildInProgress,
    };
    ipcRenderer.send(IpcRequest.UpdateBuildStatus, body);
  }, [buildInProgress]);

  const [serialDevice, setSerialDevice] = useState<string | null>(null);
  const onSerialDevice = (newSerialDevice: string | null) => {
    setSerialDevice(newSerialDevice);
  };

  const [wifiDevice, setWifiDevice] = useState<string | null>(null);
  const onWifiDevice = useCallback((newWifiDevice: string | null) => {
    setWifiDevice(newWifiDevice);
  }, []);

  const [serialPortRequired, setSerialPortRequired] = useState<boolean>(false);
  const [wifiDeviceRequired, setWifiDeviceRequired] = useState<boolean>(false);

  useEffect(() => {
    if (
      deviceTarget &&
      (deviceTarget.flashingMethod === FlashingMethod.BetaflightPassthrough ||
        deviceTarget.flashingMethod === FlashingMethod.UART)
    ) {
      setSerialPortRequired(true);
    } else {
      setSerialPortRequired(false);
    }

    if (deviceTarget && deviceTarget.flashingMethod === FlashingMethod.WIFI) {
      setWifiDeviceRequired(true);
    } else {
      setWifiDeviceRequired(false);
    }
  }, [deviceTarget, deviceTarget, deviceTargets]);

  const [
    deviceOptionsValidationErrors,
    setDeviceOptionsValidationErrors,
  ] = useState<Error[] | null>(null);

  const reset = () => {
    logsRef.current = [];
    progressNotificationsRef.current = [];
    setLogs('');
    setFirmwareVersionErrors([]);
    setDeviceTargetErrors([]);
    setDeviceOptionsValidationErrors([]);

    setProgressNotifications([]);
    setLastProgressNotification(null);
  };

  const onBack = () => {
    reset();
    setViewState(ViewState.Configuration);
    setAppStatus(AppStatus.Interactive);
  };

  const getAbbreviatedDeviceName = (item: Device) => {
    return item.abbreviatedName?.slice(0, 16) ?? item.name?.slice(0, 16);
  };

  const [currentJobType, setCurrentJobType] = useState<BuildJobType>(
    BuildJobType.Build
  );
  const sendJob = (type: BuildJobType) => {
    reset();
    setCurrentJobType(type);

    // Validate firmware source
    if (firmwareVersionData === null) {
      setFirmwareVersionErrors([new Error('Please select firmware source')]);
      return;
    }
    const sourceErrors = validateFirmwareVersionData(firmwareVersionData);
    if (sourceErrors.length > 0) {
      setFirmwareVersionErrors(sourceErrors);
      return;
    }

    // Validate device target
    if (deviceTarget === null) {
      setDeviceTargetErrors([new Error('Please select a device target')]);
      return;
    }

    // Validate device options
    if (deviceOptionsFormData === null) {
      setDeviceTargetErrors([
        new Error('Please configure your device options'),
      ]);
      return;
    }

    switch (deviceOptionsFormData.userDefinesMode) {
      case UserDefinesMode.Manual:
        break;
      case UserDefinesMode.UserInterface:
        const errs = new UserDefinesValidator().validate(
          deviceOptionsFormData.userDefineOptions
        );
        if (errs.length > 0) {
          setDeviceOptionsValidationErrors(errs);
          return;
        }
        break;
      default:
        break;
    }

    let uploadPort: string | undefined;

    if (serialPortRequired && serialDevice != null) {
      uploadPort = serialDevice;
    } else if (wifiDeviceRequired && wifiDevice !== null) {
      uploadPort = wifiDevice;
    }

    const userDefines = deviceOptionsFormData.userDefineOptions.map((item) => ({
      key: item.key,
      value: item.value,
      enabled: item.enabled,
      enumValues: item.enumValues,
      type: item.type,
    }));

    if (device?.parent && device?.name) {
      const deviceName = getAbbreviatedDeviceName(device);
      // add the user define for the device name
      userDefines.push({
        key: UserDefineKey.DEVICE_NAME,
        value: deviceName,
        enabled: true,
        enumValues: null,
        type: UserDefineKind.Text,
      });
    }

    const input: BuildFlashFirmwareInput = {
      type,
      firmware: firmwareVersionData,
      target: deviceTarget.name,
      userDefinesTxt: deviceOptionsFormData.userDefinesTxt,
      userDefinesMode: deviceOptionsFormData.userDefinesMode,
      userDefines,
      serialDevice: uploadPort,
    };
    buildFlashFirmwareMutation({
      variables: {
        input,
        gitRepository: {
          url: gitRepository.url,
          owner: gitRepository.owner,
          repositoryName: gitRepository.repositoryName,
          rawRepoUrl: gitRepository.rawRepoUrl,
          srcFolder: gitRepository.srcFolder,
        },
      },
    });
    setViewState(ViewState.Compiling);
    setAppStatus(AppStatus.Busy);
  };

  useEffect(() => {
    if (
      !buildInProgress &&
      response?.buildFlashFirmware?.success !== undefined
    ) {
      window.scrollTo(0, document.body.scrollHeight);
    }
  }, [buildInProgress, response]);

  const onBuild = () => sendJob(BuildJobType.Build);
  const onBuildAndFlash = () => sendJob(BuildJobType.BuildAndFlash);
  const onForceFlash = () => sendJob(BuildJobType.ForceFlash);

  const deviceTargetRef = useRef<HTMLDivElement | null>(null);
  const deviceOptionsRef = useRef<HTMLDivElement | null>(null);

  const [
    deviceSelectErrorDialogOpen,
    setDeviceSelectErrorDialogOpen,
  ] = useState<boolean>(false);

  const handleSelectedDeviceChange = useCallback(
    (deviceName: string) => {
      const dnsDevice = networkDevices.get(deviceName);
      if (dnsDevice) {
        const dnsDeviceName = dnsDevice.deviceName?.toUpperCase();
        const dnsDeviceTarget = dnsDevice.target.toUpperCase();

        let deviceMatches: Device[] | undefined = [];

        // try to find the device by the deviceName
        deviceMatches = deviceTargets?.filter((item) => {
          return getAbbreviatedDeviceName(item).toUpperCase() === dnsDeviceName;
        });

        // if no matches found by deviceName, then use the target
        if (
          deviceMatches?.length === 0 &&
          dnsDeviceTarget.trim().length !== 0
        ) {
          deviceMatches = deviceTargets?.filter((item) => {
            // only match on a device that doesn't have a parent, which means it
            // is not an alias of another device
            return (
              !item.parent &&
              item.targets.find((target) => {
                const baseTargetName = target.name.split('_via_')[0];
                return baseTargetName.toUpperCase() === dnsDeviceTarget;
              })
            );
          });
        }

        // if no device is found that matches the target
        if (!deviceMatches || deviceMatches.length === 0) {
          console.error(
            `no device matches found for target ${dnsDeviceTarget}!`
          );
          setDeviceSelectErrorDialogOpen(true);
          return;
        }

        // if multiple device matches are found, then don't select any of them
        // we do not know which one is correct and do not want to pick the wrong device.
        if (deviceMatches.length > 1) {
          console.error(
            `multiple device matches found for target ${dnsDeviceTarget}!`
          );
          setDeviceSelectErrorDialogOpen(true);
          return;
        }

        const deviceMatch = deviceMatches[0];

        const dTarget =
          deviceMatch?.targets.find((target) => {
            return target.flashingMethod === FlashingMethod.WIFI;
          }) ||
          deviceMatch?.targets[0] ||
          null;

        if (dTarget !== deviceTarget) {
          setDeviceTarget(dTarget);
          deviceTargetRef?.current?.scrollIntoView({ behavior: 'smooth' });
        }

        setWifiDevice(dnsDevice.ip);
      }
    },
    [deviceTarget, deviceTargets, networkDevices]
  );

  useEffect(() => {
    if (selectedDevice) {
      handleSelectedDeviceChange(selectedDevice);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [selectedDevice]);

  const luaDownloadButton = () => {
    if (
      hasLuaScript &&
      luaScriptResponse &&
      luaScriptResponse.luaScript.fileLocation &&
      luaScriptResponse.luaScript.fileLocation.length > 0
    ) {
      return (
        <Button
          sx={styles.button}
          color="primary"
          size="large"
          variant="contained"
          href={luaScriptResponse?.luaScript.fileLocation ?? ''}
          download
        >
          Download LUA script
        </Button>
      );
    }
    return null;
  };

  const handleDeviceSelectErrorDialogClose = useCallback(() => {
    setDeviceSelectErrorDialogOpen(false);
  }, []);

  const saveBuildLogToFile = useCallback(async () => {
    const saveFileRequestBody: SaveFileRequestBody = {
      data: logs,
      defaultPath: `ExpressLRSBuildLog_${new Date()
        .toISOString()
        .replace(/[^0-9]/gi, '')}.txt`,
    };

    const result: SaveFileResponseBody = await ipcRenderer.invoke(
      IpcRequest.SaveFile,
      saveFileRequestBody
    );

    if (result.success) {
      const openFileLocationRequestBody: OpenFileLocationRequestBody = {
        path: result.path,
      };
      ipcRenderer.send(
        IpcRequest.OpenFileLocation,
        openFileLocationRequestBody
      );
    }
  }, [logs]);

  return (
    <MainLayout>
      {viewState === ViewState.Configuration && (
        <>
          <Card>
            <CardTitle icon={<SettingsIcon />} title="Firmware version" />
            <Divider />
            <CardContent>
              <FirmwareVersionForm
                onChange={onFirmwareVersionData}
                data={firmwareVersionData}
                gitRepository={gitRepository}
              />
              <ShowAlerts severity="error" messages={firmwareVersionErrors} />
            </CardContent>
            <Divider />

            <CardTitle icon={<SettingsIcon />} title="Target" />
            <Divider />
            <CardContent ref={deviceTargetRef}>
              {firmwareVersionData === null ||
                (validateFirmwareVersionData(firmwareVersionData).length >
                  0 && (
                  <Alert severity="info">
                    <AlertTitle>Notice</AlertTitle>
                    Please select a firmware version first
                  </Alert>
                ))}
              {!loadingTargets && !targetsResponseError && (
                <DeviceTargetForm
                  currentTarget={deviceTarget}
                  onChange={onDeviceTarget}
                  firmwareVersionData={firmwareVersionData}
                  deviceOptions={deviceTargets}
                />
              )}
              <Loader loading={loadingTargets} />
              {luaDownloadButton()}
              {hasLuaScript && (
                <ShowAlerts
                  severity="error"
                  messages={luaScriptResponseError}
                />
              )}
              <ShowAlerts severity="error" messages={targetsResponseError} />
              <ShowAlerts severity="error" messages={deviceTargetErrors} />
            </CardContent>
            <Divider />

            <CardTitle
              icon={<SettingsIcon />}
              title={
                <div ref={deviceOptionsRef}>
                  Device options{' '}
                  {deviceOptionsFormData.userDefinesMode ===
                    UserDefinesMode.UserInterface &&
                    deviceTarget !== null &&
                    !loadingOptions && (
                      <Tooltip
                        placement="top"
                        arrow
                        title={
                          <div>
                            Reset device options to the recommended defaults on
                            this device target. Except for your custom binding
                            phrase.
                          </div>
                        }
                      >
                        <Button onClick={onResetToDefaults} size="small">
                          Reset
                        </Button>
                      </Tooltip>
                    )}
                </div>
              }
            />
            <Divider />
            <CardContent>
              {!loadingOptions && (
                <DeviceOptionsForm
                  target={deviceTarget?.name ?? null}
                  deviceOptions={deviceOptionsFormData}
                  firmwareVersionData={firmwareVersionData}
                  onChange={onUserDefines}
                />
              )}
              {deviceOptionsFormData.userDefinesMode ===
                UserDefinesMode.UserInterface &&
                (firmwareVersionData === null ||
                  validateFirmwareVersionData(firmwareVersionData).length > 0 ||
                  deviceTarget === null) && (
                  <Alert severity="info">
                    <AlertTitle>Notice</AlertTitle>
                    Please select a firmware version and device target first
                  </Alert>
                )}
              <ShowAlerts
                severity="error"
                messages={deviceOptionsResponseError}
              />
              <ShowAlerts
                severity="error"
                messages={deviceOptionsValidationErrors}
              />
              <Loader loading={loadingOptions} />
            </CardContent>
            <Divider />

            <CardTitle icon={<SettingsIcon />} title="Actions" />
            <Divider />
            <CardContent>
              <UserDefinesAdvisor
                deviceOptionsFormData={deviceOptionsFormData}
              />

              <div>
                {serialPortRequired && (
                  <SerialDeviceSelect
                    serialDevice={serialDevice}
                    onChange={onSerialDevice}
                  />
                )}
                {wifiDeviceRequired && (
                  <WifiDeviceSelect
                    wifiDevice={wifiDevice}
                    wifiDevices={Array.from(networkDevices.values()).filter(
                      (item) => {
                        return deviceTarget?.name
                          ?.toUpperCase()
                          .startsWith(item.target.toUpperCase());
                      }
                    )}
                    onChange={onWifiDevice}
                  />
                )}
                <Button
                  sx={styles.button}
                  size="large"
                  variant="contained"
                  onClick={onBuild}
                >
                  Build
                </Button>
                {deviceTarget?.flashingMethod !== FlashingMethod.Radio && (
                  <SplitButton
                    sx={styles.button}
                    size="large"
                    variant="contained"
                    options={[
                      {
                        label: 'Build & Flash',
                        value: BuildJobType.BuildAndFlash,
                      },
                      {
                        label: 'Force Flash',
                        value: BuildJobType.ForceFlash,
                      },
                    ]}
                    onButtonClick={(value: string | null) => {
                      if (value === BuildJobType.BuildAndFlash) {
                        onBuildAndFlash();
                      } else if (value === BuildJobType.ForceFlash) {
                        onForceFlash();
                      }
                    }}
                  />
                )}
              </div>
            </CardContent>
          </Card>
          <Card>
            {networkDevices.size > 0 && (
              <Box>
                <Divider />
                <CardTitle icon={<NetworkWifi />} title="Network Devices" />
                <Divider />
                <CardContent>
                  <div>
                    <WifiDeviceList
                      wifiDevices={Array.from(networkDevices.values())}
                      onChange={(dnsDevice: MulticastDnsInformation) => {
                        onDeviceChange(dnsDevice);
                        handleSelectedDeviceChange(dnsDevice.name);
                      }}
                    />
                  </div>
                </CardContent>
              </Box>
            )}
          </Card>
          <Dialog
            open={deviceSelectErrorDialogOpen}
            onClose={handleDeviceSelectErrorDialogClose}
            aria-labelledby="alert-dialog-title"
            aria-describedby="alert-dialog-description"
          >
            <DialogTitle id="alert-dialog-title">
              Device Select Error
            </DialogTitle>
            <DialogContent>
              <DialogContentText id="alert-dialog-description">
                The target device could not be automatically selected, it must
                be done manually.
              </DialogContentText>
            </DialogContent>
            <DialogActions>
              <Button onClick={handleDeviceSelectErrorDialogClose}>
                Close
              </Button>
            </DialogActions>
          </Dialog>
        </>
      )}

      {viewState === ViewState.Compiling && (
        <Card>
          <CardTitle icon={<SettingsIcon />} title="Build" />
          <Divider />
          <CardContent>
            <BuildProgressBar
              inProgress={buildInProgress}
              jobType={currentJobType}
              progressNotification={lastProgressNotification}
            />
            <BuildNotificationsList notifications={progressNotifications} />

            <ShowAlerts severity="error" messages={buildFlashErrorResponse} />
          </CardContent>

          {logs.length > 0 && (
            <>
              <CardTitle
                icon={<SettingsIcon />}
                title={
                  <Box display="flex" justifyContent="space-between">
                    <Box>Logs</Box>
                    <Box>
                      <IconButton
                        aria-label="Copy log to clipboard"
                        title="Copy log to clipboard"
                        onClick={async () => {
                          await navigator.clipboard.writeText(logs);
                        }}
                      >
                        <ContentCopy />
                      </IconButton>
                      <IconButton
                        aria-label="Save log to file"
                        title="Save log to file"
                        onClick={saveBuildLogToFile}
                      >
                        <Save />
                      </IconButton>
                    </Box>
                  </Box>
                }
              />
              <Divider />
              <CardContent>
                <Box sx={styles.longBuildDurationWarning}>
                  <ShowTimeoutAlerts
                    severity="warning"
                    messages="Sometimes builds take at least a few minutes. It is normal, especially for the first time builds."
                    active={buildInProgress}
                    timeout={14 * 1000}
                  />
                </Box>
                <Logs data={logs} />
              </CardContent>
              <Divider />
            </>
          )}
          {response !== undefined && (
            <>
              <CardTitle icon={<SettingsIcon />} title="Result" />
              <Divider />
              <CardContent>
                {response?.buildFlashFirmware?.success &&
                  currentJobType === BuildJobType.BuildAndFlash &&
                  deviceTarget?.flashingMethod === FlashingMethod.WIFI && (
                    <>
                      <Alert sx={styles.buildNotification} severity="warning">
                        <AlertTitle>Warning</AlertTitle>
                        Please wait for LED to resume blinking before
                        disconnecting power
                      </Alert>
                    </>
                  )}
                <ShowAfterTimeout
                  timeout={
                    response?.buildFlashFirmware?.success &&
                    currentJobType === BuildJobType.BuildAndFlash &&
                    deviceTarget?.flashingMethod === FlashingMethod.WIFI
                      ? 15000
                      : 1000
                  }
                  active={!buildInProgress}
                >
                  <Box sx={styles.buildNotification}>
                    <BuildResponse
                      response={response?.buildFlashFirmware}
                      firmwareVersionData={firmwareVersionData}
                    />
                  </Box>
                  {response?.buildFlashFirmware?.success && hasLuaScript && (
                    <>
                      <Alert sx={styles.buildNotification} severity="info">
                        <AlertTitle>Update Lua Script</AlertTitle>
                        Make sure to update the Lua script on your radio
                      </Alert>
                    </>
                  )}
                </ShowAfterTimeout>
                {response?.buildFlashFirmware?.success &&
                  currentJobType === BuildJobType.Build && (
                    <>
                      <Alert sx={styles.buildNotification} severity="info">
                        <AlertTitle>Build notice</AlertTitle>
                        {deviceTarget?.flashingMethod !== FlashingMethod.Radio
                          ? 'Firmware binary file was opened in the file explorer'
                          : "Firmware binary file was opened in the file explorer, copy the firmware file to your radios's SD card and flash it to the transmitter using EdgeTX/OpenTX"}
                      </Alert>
                    </>
                  )}
              </CardContent>
              <Divider />
            </>
          )}
          {!buildInProgress && (
            <>
              <CardTitle icon={<SettingsIcon />} title="Actions" />
              <Divider />
              <CardContent>
                <Button
                  sx={styles.button}
                  color="primary"
                  size="large"
                  variant="contained"
                  onClick={onBack}
                >
                  Back
                </Button>

                {!response?.buildFlashFirmware.success && (
                  <Button
                    sx={styles.button}
                    size="large"
                    variant="contained"
                    onClick={() => {
                      sendJob(currentJobType);
                    }}
                  >
                    Retry
                  </Button>
                )}

                {!response?.buildFlashFirmware.success &&
                  response?.buildFlashFirmware.errorType ===
                    BuildFirmwareErrorType.TargetMismatch && (
                    <Button
                      sx={styles.button}
                      size="large"
                      variant="contained"
                      onClick={onForceFlash}
                    >
                      Force Flash
                    </Button>
                  )}

                {response?.buildFlashFirmware.success && luaDownloadButton()}
              </CardContent>
            </>
          )}
        </Card>
      )}
    </MainLayout>
  );
}
Example #17
Source File: MangaCard.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 4 votes vote down vote up
MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => {
    const {
        manga: {
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            id, title, thumbnailUrl, downloadCount, unreadCount: unread, inLibrary,
        },
        gridLayout,
        dimensions,
    } = props;
    const { options: { showUnreadBadge, showDownloadBadge } } = useLibraryOptionsContext();

    const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
    const [useCache] = useLocalStorage<boolean>('useCache', true);
    const [ItemWidth] = useLocalStorage<number>('ItemWidth', 300);

    if (gridLayout !== 2) {
        const colomns = Math.round(dimensions / ItemWidth);
        return (
            // @ts-ignore gridsize type isnt allowed to be a decimal but it works fine
            <Grid item xs={12 / colomns} sm={12 / colomns} md={12 / colomns} lg={12 / colomns}>
                <Link to={`/manga/${id}/`} style={(gridLayout === 1) ? { textDecoration: 'none' } : {}}>
                    <Box
                        sx={{
                            display: 'flex',
                            flexDirection: 'column',
                        }}
                    >
                        <Card
                            sx={{
                            // force standard aspect ratio of manga covers
                                aspectRatio: '225/350',
                                display: 'flex',
                            }}
                            ref={ref}
                        >
                            <CardActionArea
                                sx={{
                                    position: 'relative',
                                    height: '100%',
                                }}
                            >

                                <BadgeContainer>
                                    {inLibrary && (
                                        <Typography
                                            sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}
                                        >
                                            In library
                                        </Typography>
                                    )}
                                    { showUnreadBadge && unread! > 0 && (
                                        <Typography
                                            sx={{ backgroundColor: 'primary.dark' }}
                                        >
                                            {unread}
                                        </Typography>
                                    )}
                                    { showDownloadBadge && downloadCount! > 0 && (
                                        <Typography sx={{
                                            backgroundColor: 'success.dark',
                                        }}
                                        >
                                            {downloadCount}
                                        </Typography>
                                    )}
                                </BadgeContainer>
                                <SpinnerImage
                                    alt={title}
                                    src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
                                    imgStyle={inLibrary
                                        ? {
                                            height: '100%',
                                            width: '100%',
                                            objectFit: 'cover',
                                            filter: 'brightness(0.4)',
                                        }
                                        : {
                                            height: '100%',
                                            width: '100%',
                                            objectFit: 'cover',
                                        }}
                                    spinnerStyle={{
                                        display: 'grid',
                                        placeItems: 'center',
                                    }}
                                />
                                {(gridLayout === 1) ? (<></>) : (
                                    <>
                                        <BottomGradient />
                                        <BottomGradientDoubledDown />
                                    </>
                                )}
                                {(gridLayout === 1) ? (
                                    <></>
                                ) : (
                                    <MangaTitle>
                                        {truncateText(title, 61)}
                                    </MangaTitle>
                                )}
                            </CardActionArea>
                        </Card>
                        {(gridLayout === 1) ? (
                            <MangaTitle
                                sx={{
                                    position: 'relative',
                                }}
                            >
                                {truncateText(title, 61)}
                            </MangaTitle>
                        ) : (<></>)}
                    </Box>
                </Link>
            </Grid>
        );
    }
    return (
        <Grid item xs={12} sm={12} md={12} lg={12}>
            <Link to={`/manga/${id}/`} style={{ textDecoration: 'none', color: 'unset' }}>
                <CardContent sx={{
                    display: 'flex',
                    justifyContent: 'space-between',
                    alignItems: 'center',
                    padding: 2,
                    '&:hover': {
                        backgroundColor: 'action.hover',
                        transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
                    },
                    '&:active': {
                        backgroundColor: 'action.selected',
                        transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
                    },
                    position: 'relative',
                }}
                >
                    <Avatar
                        variant="rounded"
                        sx={inLibrary
                            ? {
                                width: 56,
                                height: 56,
                                flex: '0 0 auto',
                                marginRight: 2,
                                imageRendering: 'pixelated',
                                filter: 'brightness(0.4)',
                            }
                            : {
                                width: 56,
                                height: 56,
                                flex: '0 0 auto',
                                marginRight: 2,
                                imageRendering: 'pixelated',
                            }}
                        src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
                    />
                    <Box
                        sx={{
                            display: 'flex',
                            flexDirection: 'row',
                            flexGrow: 1,
                            width: 'min-content',
                        }}
                    >
                        <Typography variant="h5" component="h2">
                            {truncateText(title, 61)}
                        </Typography>
                    </Box>
                    <BadgeContainer sx={{ position: 'relative' }}>
                        {inLibrary && (
                            <Typography
                                sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}
                            >
                                In library
                            </Typography>
                        )}
                        { showUnreadBadge && unread! > 0 && (
                            <Typography
                                sx={{ backgroundColor: 'primary.dark' }}
                            >
                                {unread}
                            </Typography>
                        )}
                        { showDownloadBadge && downloadCount! > 0 && (
                            <Typography sx={{
                                backgroundColor: 'success.dark',
                            }}
                            >
                                {downloadCount}
                            </Typography>
                        )}
                    </BadgeContainer>
                </CardContent>
            </Link>
        </Grid>
    );
})
Example #18
Source File: ArtifactCard.tsx    From genshin-optimizer with MIT License 4 votes vote down vote up
export default function ArtifactCard({ artifactId, artifactObj, onClick, onDelete, mainStatAssumptionLevel = 0, effFilter = allSubstatFilter, probabilityFilter, disableEditSetSlot = false, editor = false, canExclude = false, canEquip = false, extraButtons }: Data): JSX.Element | null {
  const { t } = useTranslation(["artifact", "ui"]);
  const { database } = useContext(DatabaseContext)
  const databaseArtifact = useArtifact(artifactId)
  const sheet = usePromise(ArtifactSheet.get((artifactObj ?? databaseArtifact)?.setKey), [artifactObj, databaseArtifact])
  const equipOnChar = (charKey: CharacterKey | "") => database.setArtLocation(artifactId!, charKey)
  const editable = !artifactObj
  const [showEditor, setshowEditor] = useState(false)
  const onHideEditor = useCallback(() => setshowEditor(false), [setshowEditor])
  const onShowEditor = useCallback(() => editable && setshowEditor(true), [editable, setshowEditor])

  const wrapperFunc = useCallback(children => <CardActionArea onClick={() => artifactId && onClick?.(artifactId)} sx={{ flexGrow: 1, display: "flex", flexDirection: "column" }} >{children}</CardActionArea>, [onClick, artifactId],)
  const falseWrapperFunc = useCallback(children => <Box sx={{ flexGrow: 1, display: "flex", flexDirection: "column" }} >{children}</Box>, [])

  const art = artifactObj ?? databaseArtifact
  if (!art) return null

  const { id, lock, slotKey, rarity, level, mainStatKey, substats, exclude, location = "" } = art
  const mainStatLevel = Math.max(Math.min(mainStatAssumptionLevel, rarity * 4), level)
  const mainStatUnit = KeyMap.unit(mainStatKey)
  const levelVariant = "roll" + (Math.floor(Math.max(level, 0) / 4) + 1)
  const { currentEfficiency, maxEfficiency } = Artifact.getArtifactEfficiency(art, effFilter)
  const artifactValid = maxEfficiency !== 0
  const slotName = sheet?.getSlotName(slotKey) || "Unknown Piece Name"
  const slotDesc = sheet?.getSlotDesc(slotKey)
  const slotDescTooltip = slotDesc && <InfoTooltip title={<Box>
    <Typography variant='h6'>{slotName}</Typography>
    <Typography>{slotDesc}</Typography>
  </Box>} />
  const setEffects = sheet?.setEffects
  const setDescTooltip = sheet && setEffects && <InfoTooltip title={
    <span>
      {Object.keys(setEffects).map(setNumKey => <span key={setNumKey}>
        <Typography variant="h6"><SqBadge color="success">{t(`artifact:setEffectNum`, { setNum: setNumKey })}</SqBadge></Typography>
        <Typography>{sheet.setEffectDesc(setNumKey as any)}</Typography>
      </span>)}
    </span>
  } />
  return <Suspense fallback={<Skeleton variant="rectangular" sx={{ width: "100%", height: "100%", minHeight: 350 }} />}>
    {editor && <Suspense fallback={false}>
      <ArtifactEditor
        artifactIdToEdit={showEditor ? artifactId : ""}
        cancelEdit={onHideEditor}
        disableEditSetSlot={disableEditSetSlot}
      />
    </Suspense>}
    <CardLight sx={{ height: "100%", display: "flex", flexDirection: "column" }}>
      <ConditionalWrapper condition={!!onClick} wrapper={wrapperFunc} falseWrapper={falseWrapperFunc}>
        <Box className={`grad-${rarity}star`} sx={{ position: "relative", width: "100%" }}>
          {!onClick && <IconButton color="primary" disabled={!editable} onClick={() => database.updateArt({ lock: !lock }, id)} sx={{ position: "absolute", right: 0, bottom: 0, zIndex: 2 }}>
            {lock ? <Lock /> : <LockOpen />}
          </IconButton>}
          <Box sx={{ pt: 2, px: 2, position: "relative", zIndex: 1 }}>
            {/* header */}
            <Box component="div" sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}>
              <Chip size="small" label={<strong>{` +${level}`}</strong>} color={levelVariant as any} />
              <Typography component="span" noWrap sx={{ backgroundColor: "rgba(100,100,100,0.35)", borderRadius: "1em", px: 1 }}><strong>{slotName}</strong></Typography>
              <Box flexGrow={1} sx={{ textAlign: "right" }}>
                {slotDescTooltip}
              </Box>
            </Box>
            <Typography color="text.secondary" variant="body2">
              <SlotNameWithIcon slotKey={slotKey} />
            </Typography>
            <Typography variant="h6" color={`${KeyMap.getVariant(mainStatKey)}.main`}>
              <span>{StatIcon[mainStatKey]} {KeyMap.get(mainStatKey)}</span>
            </Typography>
            <Typography variant="h5">
              <strong>
                <ColorText color={mainStatLevel !== level ? "warning" : undefined}>{cacheValueString(Artifact.mainStatValue(mainStatKey, rarity, mainStatLevel) ?? 0, KeyMap.unit(mainStatKey))}{mainStatUnit}</ColorText>
              </strong>
            </Typography>
            <Stars stars={rarity} colored />
            {/* {process.env.NODE_ENV === "development" && <Typography color="common.black">{id || `""`} </Typography>} */}
          </Box>
          <Box sx={{ height: "100%", position: "absolute", right: 0, top: 0 }}>
            <Box
              component="img"
              src={sheet?.slotIcons[slotKey] ?? ""}
              width="auto"
              height="100%"
              sx={{ float: "right" }}
            />
          </Box>
        </Box>
        <CardContent sx={{ flexGrow: 1, display: "flex", flexDirection: "column", pt: 1, pb: 0, width: "100%" }}>
          {substats.map((stat: ICachedSubstat) => <SubstatDisplay key={stat.key} stat={stat} effFilter={effFilter} rarity={rarity} />)}
          <Box sx={{ display: "flex", my: 1 }}>
            <Typography color="text.secondary" component="span" variant="caption" sx={{ flexGrow: 1 }}>{t`artifact:editor.curSubEff`}</Typography>
            <PercentBadge value={currentEfficiency} max={900} valid={artifactValid} />
          </Box>
          {currentEfficiency !== maxEfficiency && <Box sx={{ display: "flex", mb: 1 }}>
            <Typography color="text.secondary" component="span" variant="caption" sx={{ flexGrow: 1 }}>{t`artifact:editor.maxSubEff`}</Typography>
            <PercentBadge value={maxEfficiency} max={900} valid={artifactValid} />
          </Box>}
          <Box flexGrow={1} />
          {probabilityFilter && <strong>Probability: {(probability(art, probabilityFilter) * 100).toFixed(2)}%</strong>}
          <Typography color="success.main">{sheet?.name ?? "Artifact Set"} {setDescTooltip}</Typography>
        </CardContent>
      </ConditionalWrapper>
      <Box sx={{ p: 1, display: "flex", gap: 1, justifyContent: "space-between", alignItems: "center" }}>
        {editable && canEquip
          ? <CharacterAutocomplete sx={{ flexGrow: 1 }} size="small" showDefault
            defaultIcon={<BusinessCenter />} defaultText={t("ui:inventory")}
            value={location} onChange={equipOnChar} />
          : <LocationName location={location} />}
        {editable && <ButtonGroup sx={{ height: "100%" }}>
          {editor && <Tooltip title={<Typography>{t`artifact:edit`}</Typography>} placement="top" arrow>
            <Button color="info" size="small" onClick={onShowEditor} >
              <FontAwesomeIcon icon={faEdit} className="fa-fw" />
            </Button>
          </Tooltip>}
          {canExclude && <Tooltip title={<Typography>{t`artifact:excludeArtifactTip`}</Typography>} placement="top" arrow>
            <Button onClick={() => database.updateArt({ exclude: !exclude }, id)} color={exclude ? "error" : "success"} size="small" >
              <FontAwesomeIcon icon={exclude ? faBan : faChartLine} className="fa-fw" />
            </Button>
          </Tooltip>}
          {!!onDelete && <Button color="error" size="small" onClick={() => onDelete(id)} disabled={lock}>
            <FontAwesomeIcon icon={faTrashAlt} className="fa-fw" />
          </Button>}
          {extraButtons}
        </ButtonGroup>}
      </Box>
    </CardLight >
  </Suspense>
}
Example #19
Source File: BlockEditor.tsx    From NekoMaid with MIT License 4 votes vote down vote up
BlockEditor: React.FC = () => {
  const theme = useTheme()
  const plugin = usePlugin()
  const his = useHistory()
  const loc = useLocation()
  const globalData = useGlobalData()
  const drawerWidth = useDrawerWidth()
  const [block, setBlock] = useState<Block>()
  const [types, setTypes] = useState<string[]>([])
  const [worlds, setWorlds] = useState<string[]>([])
  const params = { world: '', x: 0, y: 0, z: 0 }
  if (loc.pathname.startsWith('/NekoMaid/block/')) {
    const arr = loc.pathname.split('/')
    if (arr.length > 6) {
      params.world = arr[3]
      params.x = +arr[4]
      params.y = +arr[5]
      params.z = +arr[6]
    } else his.push('/NekoMaid/block')
  }
  useEffect(() => {
    const off = plugin.emit('item:blocks', (types: string[], worlds: string[]) => {
      setTypes(types)
      setWorlds(worlds)
    })
      .on('block:select', (world, x, y, z) => his.push(`/NekoMaid/block/${world}/${x}/${y}/${z}`))
    return () => void off()
  }, [])
  const update = () => {
    if (params.world) {
      plugin.emit('block:fetch', (block: Block) => {
        if (!block) {
          failed()
          his.push('/NekoMaid/block')
          return
        }
        if (globalData.hasNBTAPI && block.nbt) block.nbt = stringify(parse(block.nbt), { pretty: true })
        setBlock(block)
      }, params.world, params.x, params.y, params.z)
    }
  }
  const updateWithAction = (res: boolean) => {
    action(res)
    update()
  }
  useEffect(update, [params.world, params.x, params.y, params.z])
  return <Box sx={{ minHeight: '100%', py: 3 }}>
    <Toolbar />
    <Container maxWidth={false}>
      <Grid container spacing={3} sx={{ width: { sm: `calc(100vw - ${drawerWidth}px - ${theme.spacing(3)})` } }}>
        <Grid item lg={6} md={12} xl={6} xs={12}>
          <Card sx={{ '& .CodeMirror-dialog, .CodeMirror-scrollbar-filler': { backgroundColor: theme.palette.background.paper + '!important' } }}>
            <CardHeader
              title={lang.blockEditor.title}
              sx={{ position: 'relative' }}
              action={<Box sx={cardActionStyles}>
                <IconButton
                  size='small'
                  disabled={!block || (!block.data && !block.nbt)}
                  onClick={() => block && plugin.emit('block:save', (res: boolean) => {
                    action(res)
                    update()
                  }, params.world, params.x, params.y, params.z, block.nbt || null, block.data || null)}
                ><Save /></IconButton>
                <IconButton
                  size='small'
                  disabled={!block}
                  onClick={() => {
                    update()
                    success()
                  }}
                ><Refresh /></IconButton>
              </Box>}
            />
            <Divider />
            {block
              ? <>
                <CardContent sx={{ display: 'flex', width: '100%', justifyContent: 'center' }}>
                  <ItemViewer item={{ type: block.type }} />
                  <Autocomplete
                    options={types}
                    sx={{ maxWidth: 300, marginLeft: 1, flexGrow: 1 }}
                    value={block.type}
                    onChange={(_, it) => it && plugin.emit('block:type', (res: boolean) => {
                      action(res)
                      update()
                    }, params.world, params.x, params.y, params.z, (block.type = it))}
                    getOptionLabel={it => {
                      const locatedName = getName(it.toLowerCase())
                      return (locatedName ? locatedName + ' ' : '') + it
                    }}
                    renderInput={(params) => <TextField {...params} label={lang.itemEditor.itemType} size='small' variant='standard' />}
                  />
                </CardContent>
                {block.data != null && <Accordion sx={{ '&::before': { opacity: '1!important' } }} disableGutters>
                  <AccordionSummary expandIcon={<ExpandMore />}><Typography>{lang.data}</Typography></AccordionSummary>
                  <AccordionDetails sx={{ padding: 0, '& .CodeMirror': { width: '100%', height: 350 } }}>
                    <UnControlled
                      value={block.data}
                      options={{
                        mode: 'javascript',
                        phrases: lang.codeMirrorPhrases,
                        theme: theme.palette.mode === 'dark' ? 'material' : 'one-light'
                      }}
                      onChange={(_: any, __: any, data: string) => (block.data = data)}
                    />
                  </AccordionDetails>
                </Accordion>}
                {block.nbt != null && <Accordion sx={{ '&::before': { opacity: '1!important', display: '!important' } }} disableGutters>
                  <AccordionSummary expandIcon={<ExpandMore />}><Typography>NBT</Typography></AccordionSummary>
                  <AccordionDetails sx={{ padding: 0, '& .CodeMirror': { width: '100%', height: 350 } }}>
                    <UnControlled
                      value={block.nbt}
                      options={{
                        mode: 'javascript',
                        phrases: lang.codeMirrorPhrases,
                        theme: theme.palette.mode === 'dark' ? 'material' : 'one-light'
                      }}
                      onChange={(_: any, __: any, data: string) => (block.nbt = data)}
                    />
                  </AccordionDetails>
                </Accordion>}
              </>
              : <CardContent>{worlds.length ? <BlockSelector worlds={worlds} /> : <Empty />}</CardContent>}
          </Card>
        </Grid>
        {block?.inventory?.length
          ? <Grid item lg={6} md={12} xl={6} xs={12}>
            <Card>
              <CardHeader
                title={minecraft[('container.' + block.inventoryType || '').toLowerCase()] || lang.blockEditor.container}
                sx={{ position: 'relative' }}
              />
              <Divider />
              <CardContent sx={{ whiteSpace: 'nowrap', overflowX: 'auto', textAlign: 'center' }}>
                {block.inventory.map((it, i) => <React.Fragment key={i}><ItemViewer
                  item={it}
                  data={{ type: InvType.BLOCK, solt: i, ...params }}
                  onDrag={() => plugin.emit('block:setItem', update, params.world, params.x, params.y, params.z, i, null, -1)}
                  onDrop={(item, obj) => plugin.emit('block:setItem', update, params.world, params.x, params.y, params.z, i,
                    JSON.stringify(item), compare(obj, params) ? obj.solt : -1)}
                  onEdit={item => item !== false && plugin.emit('block:setItem', updateWithAction, params.world, params.x, params.y,
                    params.z, i, item && JSON.stringify(item), -1)}
                />{!((i + 1) % 9) && <br />}</React.Fragment>)}
              </CardContent>
            </Card>
          </Grid>
          : undefined}
      </Grid>
    </Container>
  </Box>
}