@mui/material#Alert TypeScript Examples

The following examples show how to use @mui/material#Alert. 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
WifiDeviceNotification: FunctionComponent<WifiDeviceNotificationProps> = (
  props
) => {
  const { newNetworkDevices, removeDeviceFromNewList, onDeviceChange } = props;
  const { appStatus } = useAppState();

  return (
    <>
      {appStatus === AppStatus.Interactive &&
        newNetworkDevices.map((dnsDevice) => {
          const handleClose = () => {
            removeDeviceFromNewList(dnsDevice.name);
          };

          return (
            <Snackbar
              key={dnsDevice.name}
              open
              autoHideDuration={6000}
              onClose={handleClose}
            >
              <Alert onClose={handleClose} severity="info">
                New Device {dnsDevice.name} ({dnsDevice.ip})
                <Button
                  size="small"
                  onClick={() => {
                    onDeviceChange(dnsDevice);
                    removeDeviceFromNewList(dnsDevice.name);
                  }}
                >
                  Select
                </Button>
              </Alert>
            </Snackbar>
          );
        })}
    </>
  );
}
Example #2
Source File: index.ts    From Cromwell with MIT License 6 votes vote down vote up
MuiProductCard = withElements(BaseProductCard, {
  Button: IconButton,
  AddCartButton: IconButton,
  AddWishlistButton: IconButton,
  Alert,
  Rating,
  QuantityField,
  Tooltip: Tooltip,
} as ProductCardProps['elements'], {
  notifierOptions: {
    position: toastify.POSITION.TOP_RIGHT,
    className: notifierStyles.muiToast,
    Wrapper: NotifierWrapper,
  }
} as ProductCardProps)
Example #3
Source File: Profile.tsx    From abrechnung with GNU Affero General Public License v3.0 6 votes vote down vote up
export default function Profile() {
    const user = useRecoilValue(userData);
    const isGuest = useRecoilValue(isGuestUser);
    useTitle("Abrechnung - Profile");

    return (
        <MobilePaper>
            <Typography component="h3" variant="h5">
                Profile
            </Typography>
            {isGuest && (
                <Alert severity="info">
                    You are a guest user on this Abrechnung and therefore not permitted to create new groups or group
                    invites.
                </Alert>
            )}
            <List>
                <ListItem>
                    <ListItemText primary="Username" secondary={user.username} />
                </ListItem>
                <ListItem>
                    <ListItemText primary="E-Mail" secondary={user.email} />
                </ListItem>
                <ListItem>
                    <ListItemText
                        primary="Registered"
                        secondary={DateTime.fromISO(user.registered_at).toLocaleString(DateTime.DATETIME_FULL)}
                    />
                </ListItem>
            </List>
        </MobilePaper>
    );
}
Example #4
Source File: ErrorAlert.tsx    From mui-toolpad with MIT License 6 votes vote down vote up
export default function ErrorAlert({ error }: ErrorAlertProps) {
  const message: string =
    typeof (error as any)?.message === 'string' ? (error as any).message : String(error);
  const stack: string | null =
    typeof (error as any)?.stack === 'string' ? (error as any).stack : null;

  const [expanded, setExpanded] = React.useState(false);
  const toggleExpanded = React.useCallback(() => setExpanded((actual) => !actual), []);
  return (
    <Alert
      severity="error"
      sx={{
        // The content of the Alert doesn't overflow nicely
        // TODO: does this need to go in core?
        '& .MuiAlert-message': { minWidth: 0 },
      }}
      action={
        stack ? (
          <IconButton color="inherit" onClick={toggleExpanded}>
            {expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
          </IconButton>
        ) : null
      }
    >
      <AlertTitle>{message}</AlertTitle>
      <Collapse in={expanded}>
        <Box sx={{ overflow: 'auto' }}>
          <pre>{stack}</pre>
        </Box>
      </Collapse>
    </Alert>
  );
}
Example #5
Source File: index.tsx    From ExpressLRS-Configurator with GNU General Public License v3.0 6 votes vote down vote up
ShowAlerts: FunctionComponent<ShowAlertsProps> = memo(
  ({ messages, severity }) => {
    const isError = (e: any): e is Error => {
      return e && e.stack && e.message;
    };
    const renderMessage = (message: string | undefined | null | Error) => {
      return (
        <>
          {message && message?.toString()?.length > 0 && (
            <Alert severity={severity!}>
              {isError(message) ? message.message : message}
            </Alert>
          )}
        </>
      );
    };
    return (
      <>
        {Array.isArray(messages) &&
          messages.map((message, idx) => (
            <div key={idx}>{renderMessage(message)}</div>
          ))}
        {!Array.isArray(messages) && renderMessage(messages)}
      </>
    );
  }
)
Example #6
Source File: index.tsx    From mui-toolpad with MIT License 6 votes vote down vote up
function EditorContent({ appId }: EditorContentProps) {
  const domLoader = useDomLoader();

  return (
    <EditorRoot>
      {domLoader.dom ? (
        <FileEditor appId={appId} />
      ) : (
        <Box flex={1} display="flex" alignItems="center" justifyContent="center">
          {domLoader.error ? (
            <Alert severity="error">{domLoader.error}</Alert>
          ) : (
            <CircularProgress />
          )}
        </Box>
      )}
    </EditorRoot>
  );
}
Example #7
Source File: toast.tsx    From NekoMaid with MIT License 6 votes vote down vote up
toast = (props: (SnackbarProps & { autoHideDuration?: number }) | string = { }, type?: AlertColor) => {
  if (typeof props === 'string') props = { message: props }
  if (type) props.children = <Paper elevation={5}><Alert severity={type} sx={{ width: '100%' }} variant={'filled' as any}>{props.message}</Alert></Paper>
  if (!props.autoHideDuration) props.autoHideDuration = 4000
  const obj = toasts[i] = {
    ...props,
    key: i,
    open: true,
    sx: { position: 'relative', top: 0, left: 0, marginBottom: '12px !important' },
    onClose () {
      toasts[obj.key] = { ...obj, open: false }
      update?.(i++)
      setTimeout(() => {
        delete toasts[obj.key]
        update?.(i++)
      }, 500)
    }
  }
  update?.(++i)
}
Example #8
Source File: index.ts    From Cromwell with MIT License 6 votes vote down vote up
MuiCheckout = withElements(Checkout, {
  Loadbox,
  PlacedOrder: (props) => React.createElement(Alert, { severity: "success" }, props.children),
  RadioGroup: RadioGroup,
  Button: Button,
  AddCouponButton: (props) => React.createElement(Button, { style: { marginRight: '15px' }, ...props }),
  RemoveCouponButton: (props) => React.createElement(IconButton, { style: { marginLeft: '10px' }, ...props }),
  CouponAppliedIcon: () => React.createElement(CheckCircleOutlineIcon, {
    style: {
      color: '#357a38',
      marginRight: '15px',
    }
  }),
  CouponProblemIcon: () => React.createElement(GppBadIcon, {
    style: {
      color: '#b2102f',
      marginRight: '15px',
    }
  }),
  RemoveCouponIcon: CloseIcon,
  TextField: (props) => React.createElement(TextField, {
    size: 'small',
    fullWidth: true,
    style: { margin: '10px 0' },
    ...props,
  }),
} as CheckoutProps['elements'], {
  notifierOptions: {
    position: toastify.POSITION.TOP_RIGHT,
    className: notifierStyles.muiToast,
    Wrapper: NotifierWrapper,
  }
} as CheckoutProps)
Example #9
Source File: ImproveThisPageTag.tsx    From frontend with MIT License 6 votes vote down vote up
export default function ImproveThisPageTag({ githubUrl, figmaUrl }: Props) {
  const { t } = useTranslation()
  if (!githubUrl && !figmaUrl) return null
  return (
    <Container maxWidth="sm">
      <Box mt={8}>
        <Alert variant="outlined" color="info" severity="info">
          <AlertTitle>{t('improve-this-page')}</AlertTitle>
          {githubUrl && (
            <Button
              href={githubUrl}
              size="small"
              variant="text"
              target="_blank"
              rel="noreferrer noopener"
              startIcon={<GitHub fontSize="small" />}>
              {t('github-link-text')}
            </Button>
          )}
          {figmaUrl && (
            <Button
              href={figmaUrl}
              size="small"
              variant="text"
              target="_blank"
              rel="noreferrer noopener"
              startIcon={<Web fontSize="small" />}>
              {t('figma-link-text')}
            </Button>
          )}
        </Alert>
      </Box>
    </Container>
  )
}
Example #10
Source File: DocsDetailsModules.tsx    From ui-schema with MIT License 6 votes vote down vote up
DocsDetailsModules: React.ComponentType<{ modules: TsDocModuleCollection | undefined }> = ({modules}) => {
    const repoRoot = 'https://github.com/ui-schema/ui-schema/tree/master/packages/'
    return <>
        <LinkableHeadline level={1} customId={'module-apis'} mb={4} mt={0}>
            Module APIs
        </LinkableHeadline>
        <Box mb={4}>
            <Alert severity={'warning'} variant={'outlined'}>
                <Typography gutterBottom variant={'body2'}>Experimental documentation generator, there may be missing or bad-UX parts in the generated code documentation.</Typography>
                <Typography variant={'body2'}>Additionally, not all source code is converted to full-Typescript yet, for the pure-JS parts nothing can be generated.</Typography>
            </Alert>
        </Box>

        {modules ?
            <TsDocs
                modules={modules}
                repoRoot={repoRoot}
                renderer={renderer}
                warnOnTag={warnOnTag}
            /> : null}
    </>
}
Example #11
Source File: index.tsx    From yearn-watch-legacy with GNU Affero General Public License v3.0 6 votes vote down vote up
ErrorAlert = (props: ErrorAlertProps) => {
    const { message, details } = props;
    let detailsLabel = details;
    if (details && details instanceof Error) {
        detailsLabel = sanitizeErrors(details.message);
    } else if (details) {
        detailsLabel = sanitizeErrors(details);
    }

    return (
        <div>
            <Alert severity="error">
                <AlertTitle>Error</AlertTitle>
                {message}{' '}
                {detailsLabel && (
                    <React.Fragment>
                        — <strong>{detailsLabel}</strong>
                    </React.Fragment>
                )}
            </Alert>
        </div>
    );
}
Example #12
Source File: everyDay.tsx    From Search-Next with GNU General Public License v3.0 6 votes vote down vote up
EveryDay: React.FC<EveryDayProps> = ({ data }) => {
  const [url, setUrl] = React.useState<string>('');

  React.useEffect(() => {
    if (data && data?.url) {
      setUrl(data?.url);
    }
  }, [data]);

  return (
    <div>
      <Alert severity="info">每天更新背景,来源:必应壁纸</Alert>
      {url && (
        <div
          className={classNames(
            'm-2 rounded overflow-hidden',
            css`
              .ant-image {
                display: block;
              }
            `,
          )}
        >
          <Image src={url} />
        </div>
      )}
    </div>
  );
}
Example #13
Source File: Snackbar.tsx    From frontend with MIT License 6 votes vote down vote up
function SnackBar() {
  const { getAlerts } = AlertStore

  const handleSnackBarClose = (
    _event: React.SyntheticEvent | Event,
    reason: SnackbarCloseReason,
  ) => {
    if (reason === 'clickaway') {
      return
    }
    AlertStore.hide()
  }
  const handleClose = () => AlertStore.hide()

  return (
    <>
      {getAlerts.map(({ id, show, duration, type, message }) => {
        return (
          <Snackbar
            key={id}
            open={show}
            anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
            autoHideDuration={duration}
            onClose={handleSnackBarClose}>
            <Alert severity={type} onClose={handleClose}>
              {message}
            </Alert>
          </Snackbar>
        )
      })}
    </>
  )
}
Example #14
Source File: items-page.tsx    From sdk with MIT License 6 votes vote down vote up
export function ItemsPage() {
	const connection = useContext(ConnectorContext)
	const { items, fetching, error } = useFetchItems(connection.sdk, connection.walletAddress)

	return (
		<Page header="My Items">
			<CommentedBlock sx={{ my: 2 }} comment={<GetItemsComment/>}>
				{
					error && <CommentedBlock sx={{ my: 2 }}>
						<Alert severity="error">
							<AlertTitle>Items fetch error</AlertTitle>
							{error.message || error.toString()}
						</Alert>
					</CommentedBlock>
				}
				{
					fetching ? <Box sx={{
						my: 4,
						display: 'flex',
						justifyContent: "center",
					}}>
						<CircularProgress/>
					</Box> : ( items && <Box sx={{my: 2}}>
						<ItemsList items={items}/>
					</Box> )
				}
			</CommentedBlock>
		</Page>
	)
}
Example #15
Source File: connection-status.tsx    From example with MIT License 6 votes vote down vote up
export function ConnectionStatus() {
	const connection = useContext(ConnectorContext)

	switch (connection?.state.status) {
		case "connected":
			return <Alert severity="success" icon={<Icon icon={faLink}/>}>
				<AlertTitle>Current Status: connected</AlertTitle>
				Application is connected to wallet <Address
				address={connection.state.connection.address}
				trim={false}
			/>
			</Alert>
		case "disconnected":
			const error = connectionErrorMessage(connection?.state.error)
			return <Alert severity="error" icon={<Icon icon={faLinkSlash}/>}>
				<AlertTitle>Disconnected</AlertTitle>
				Application currently not connected to any wallet
				{ error && <Box sx={{ mt: 1 }}>Last attempt error: {error}</Box> }
			</Alert>
		case "connecting":
			return <Alert severity="info">
				<AlertTitle>Connecting...</AlertTitle>
				Connection to wallet in process
			</Alert>
		case "initializing":
			return <Alert severity="info">
				<AlertTitle>Initializing...</AlertTitle>
				Connector initialization
			</Alert>
		default:
			return null
	}
}
Example #16
Source File: Home.tsx    From mui-toolpad with MIT License 5 votes vote down vote up
export default function Home() {
  const { data: apps = [], status, error } = client.useQuery('getApps', []);

  const [createDialogOpen, setCreateDialogOpen] = React.useState(false);

  const [deletedApp, setDeletedApp] = React.useState<null | App>(null);

  return (
    <ToolpadShell>
      <AppDeleteDialog app={deletedApp} onClose={() => setDeletedApp(null)} />
      <Container>
        <Typography variant="h2">Apps</Typography>
        <CreateAppDialog open={createDialogOpen} onClose={() => setCreateDialogOpen(false)} />

        <Toolbar disableGutters>
          <Button onClick={() => setCreateDialogOpen(true)}>Create New</Button>
        </Toolbar>

        <Box
          sx={{
            display: 'grid',
            gridTemplateColumns: {
              lg: 'repeat(4, 1fr)',
              md: 'repeat(3, 1fr)',
              sm: 'repeat(2, fr)',
              xs: 'repeat(1, fr)',
            },
            gap: 2,
          }}
        >
          {(() => {
            switch (status) {
              case 'loading':
                return <AppCard />;
              case 'error':
                return <Alert severity="error">{(error as Error)?.message}</Alert>;
              case 'success':
                return apps.length > 0
                  ? apps.map((app) => (
                      <AppCard key={app.id} app={app} onDelete={() => setDeletedApp(app)} />
                    ))
                  : 'No apps yet';
              default:
                return '';
            }
          })()}
        </Box>
      </Container>
    </ToolpadShell>
  );
}
Example #17
Source File: index.tsx    From Search-Next with GNU General Public License v3.0 5 votes vote down vote up
Data: React.FC<PageProps> = ({ route, children, ...props }) => {
  const history = useNavigate();
  const location = useLocation();
  const [list, setList] = React.useState<Router[]>([]);
  const [percent, setPercent] = React.useState<number>(0);
  const [accountData, setAccountData] = React.useState<AuthData[]>([]);
  const [occupySize, setOccupySize] = React.useState<string>('0KB');

  const getOccupySize = () => {
    const size = storageSize();
    /* JavaScript中的字符串是UTF-16,所以每个字符需要两个字节的内存。
      这意味着,虽然许多浏览器有5 MB的限制,但你只能存储2.5 M字符。
      浏览器可以自由地将数据存储在磁盘上,因此不可能测量实际字节数。
      开发者通常只关心他们可以存储多少字符,因为这是他们可以在应用中衡量的。
    */
    setPercent((size / 5242880) * 100 * 2);
    setOccupySize(formatSize(size * 2));
  };

  React.useEffect(() => {
    getOccupySize();
    const data: AuthData = getAccount();
    setList(route?.routes || []);
  }, []);

  return (
    <div>
      <div className="mb-4">
        <div className="mt-2">
          <div className="flex justify-between mb-2 text-sm">
            <p>占用空间</p>
            <p>
              已使用: <span>{occupySize}</span> / <span>5MB</span>
            </p>
          </div>
          <BorderLinearProgress variant="determinate" value={percent} />
        </div>
      </div>
      <Alert severity="warning" style={{ marginBottom: '8px' }}>
        不同浏览器支持的 localStorage 和 sessionStorage
        容量上限不同。当前容量设定为标准容量 5MB。
        <a href="http://dev-test.nemikor.com/web-storage/support-test/">
          测试浏览器本地存储容量上限。
        </a>
      </Alert>
      <ContentList>
        {list.map((i) => (
          <ItemCard
            key={i.path}
            title={i.title}
            icon={i.icon}
            onClick={() => history(i.path)}
          ></ItemCard>
        ))}
      </ContentList>
    </div>
  );
}
Example #18
Source File: NodeAttributeEditor.tsx    From mui-toolpad with MIT License 5 votes vote down vote up
export default function NodeAttributeEditor({
  node,
  namespace = 'attributes',
  name,
  argType,
}: NodeAttributeEditorProps) {
  const domApi = useDomApi();

  const handlePropChange = React.useCallback(
    (newValue: BindableAttrValue<unknown> | null) => {
      domApi.setNodeNamespacedProp(node, namespace as any, name, newValue);
    },
    [domApi, node, namespace, name],
  );

  const propValue: BindableAttrValue<unknown> | null = (node as any)[namespace]?.[name] ?? null;

  const bindingId = `${node.id}${namespace ? `.${namespace}` : ''}.${name}`;
  const { bindings, pageState } = usePageEditorState();
  const liveBinding = bindings[bindingId];
  const globalScope = pageState;

  const controlSpec = argType.control ?? getDefaultControl(argType.typeDef);
  const Control = controlSpec ? propertyControls[controlSpec.type] : null;

  // NOTE: Doesn't make much sense to bind controlled props. In the future we might opt
  // to make them bindable to other controlled props only
  const isBindable = !argType.onChangeHandler;

  return Control ? (
    <BindableEditor
      liveBinding={liveBinding}
      globalScope={globalScope}
      label={argType.label || name}
      disabled={!isBindable}
      propType={argType.typeDef}
      renderControl={(params) => <Control nodeId={node.id} argType={argType} {...params} />}
      value={propValue}
      onChange={handlePropChange}
    />
  ) : (
    <Alert severity="warning">
      {`No control for '${name}' (type '${argType.typeDef.type}' ${
        argType.control ? `, control: '${argType.control.type}'` : ''
      })`}
    </Alert>
  );
}
Example #19
Source File: index.tsx    From Search-Next with GNU General Public License v3.0 5 votes vote down vote up
Lab: React.FC<PageProps> = (props) => {
  const { route } = props;
  const history = useNavigate();
  const location = useLocation();
  const [list, setList] = React.useState<Router[]>([]);

  React.useEffect(() => {
    setList(route?.routes || []);
  }, []);

  return (
    <div {...props}>
      <Alert severity="info">
        <AlertTitle>提示</AlertTitle>
        实验室中的功能均处在开发中,不保证实际发布。
      </Alert>
      <ContentList>
        {list
          .filter((i) =>
            i?.status && ['beta', 'process'].includes(i?.status)
              ? isBeta()
              : true,
          )
          .map((i) => (
            <ItemCard
              key={i.path}
              title={
                <div className="flex items-center gap-1">
                  {i.title}
                  {i?.status === 'process' && (
                    <Chip
                      color="warning"
                      label={i?.status}
                      size="small"
                      variant="outlined"
                    />
                  )}
                </div>
              }
              icon={i.icon}
              onClick={() => history(i.path)}
            ></ItemCard>
          ))}
      </ContentList>
    </div>
  );
}
Example #20
Source File: UploadExplainationModal.tsx    From genshin-optimizer with MIT License 5 votes vote down vote up
export default function UploadExplainationModal({ modalShow, hide }: { modalShow: boolean, hide: () => void }) {
  return <ModalWrapper open={modalShow} onClose={hide} >
    <CardDark>
      <CardContent sx={{ py: 1 }}>
        <Grid container>
          <Grid item flexGrow={1}>
            <Typography variant="subtitle1">How do Upload Screenshots for parsing</Typography>
          </Grid>
          <Grid item>
            <CloseButton onClick={hide} />
          </Grid>
        </Grid>
      </CardContent>
      <Divider />
      <CardContent>
        <Alert variant="outlined" severity="warning">
          NOTE: Artifact Scanning currently only work for <strong>ENGLISH</strong> artifacts.
        </Alert>
        <Grid container spacing={1} mt={1}>
          <Grid item xs={8} md={4}>
            <Box component="img" alt="snippet of the screen to take" src={Snippet} width="100%" height="auto" />
          </Grid>
          <Grid item xs={12} md={8}>
            <Typography gutterBottom>Using screenshots can dramatically decrease the amount of time you manually input in stats on the Genshin Optimizer.</Typography>
            <Typography variant="h5">Where to snip the screenshot.</Typography>
            <Typography gutterBottom>In game, Open your bag, and navigate to the artifacts tab. Select the artifact you want to scan with Genshin Optimizer. <b>Only artifact from this screen can be scanned.</b></Typography>
            <Typography variant="h6">Single artifact</Typography>
            <Typography gutterBottom>To take a screenshot, in Windows, the shortcut is <strong>Shift + WindowsKey + S</strong>. Once you selected the region, the image is automatically included in your clipboard.</Typography>
            <Typography variant="h6">Multiple artifacts</Typography>
            <Typography gutterBottom>To take advantage of batch uploads, you can use a tool like <a href="https://picpick.app/" target="_blank" rel="noreferrer">PicPick</a> to create a macro to easily to screenshot a region to screenshot multiple artifacts at once.</Typography>
            <Typography variant="h5">What to include in the screenshot.</Typography>
            <Typography>As shown in the Image, starting from the top with the artifact name, all the way to the set name(the text in green). </Typography>
          </Grid>
          <Grid item xs={12} md={7}>
            <Typography variant="h5">Adding Screenshot to Genshin Optimizer</Typography>
            <Typography>At this point, you should have the artifact snippet either saved to your harddrive, or in your clipboard.</Typography>
            <Typography gutterBottom>You can click on the box next to "Browse" to browse the files in your harddrive for multiple screenshots.</Typography>
            <Typography>For single screenshots from the snippets, just press <strong>Ctrl + V</strong> to paste from your clipboard.</Typography>
            <Typography gutterBottom>You should be able to see a Preview of your artifact snippet, and after waiting a few seconds, the artifact set and the substats will be filled in in the <b>Artifact Editor</b>.</Typography>
            <Typography variant="h5">Finishing the Artifact</Typography>
            <Typography>Unfortunately, computer vision is not 100%. There will always be cases where something is not scanned properly. You should always double check the scanned artifact values! Once the artifact has been filled, Click on <strong>Add Artifact</strong> to finish editing the artifact.</Typography>
          </Grid>
          <Grid item xs={8} md={5}>
            <Box component="img" alt="main screen after importing stats" src={scan_art_main} width="100%" height="auto" />
          </Grid>
        </Grid>
      </CardContent>
      <Divider />
      <CardContent sx={{ py: 1 }}>
        <CloseButton large onClick={hide} />
      </CardContent>
    </CardDark>
  </ModalWrapper>
}
Example #21
Source File: NotFoundEditor.tsx    From mui-toolpad with MIT License 5 votes vote down vote up
export default function NotFoundEditor({ className, message, severity }: NotFoundEditorProps) {
  return (
    <Box className={className} sx={{ p: 3 }}>
      <Alert severity={severity ?? 'warning'}>{message}</Alert>
    </Box>
  );
}
Example #22
Source File: BuildAlert.tsx    From genshin-optimizer with MIT License 5 votes vote down vote up
export default function BuildAlert({ totBuildNumber, generatingBuilds, generationSkipped, generationProgress, generationDuration, characterName, maxBuildsToShow }) {
  const totalBuildNumberString = totBuildNumber?.toLocaleString() ?? totBuildNumber
  const totalUnskipped = totBuildNumber - generationSkipped
  const generationProgressString = generationProgress?.toLocaleString() ?? generationProgress
  const generationSkippedString = generationSkipped?.toLocaleString() ?? generationSkipped
  const totalUnskippedString = totalUnskipped?.toLocaleString() ?? totalUnskipped
  const generationSkippedText = !!generationSkipped && <span>(<b>{generationSkippedString}</b> skipped)</span>

  let color = "success" as "success" | "warning" | "error"
  let title = "" as ReactNode
  let subtitle = "" as ReactNode
  let progress = undefined as undefined | number

  if (generatingBuilds) {
    progress = generationProgress * 100 / (totalUnskipped)
    title = <Typography>Generating and testing <Monospace>{generationProgressString}/{totalUnskippedString}</Monospace> build configurations against the criteria for <b>{characterName}</b>. {generationSkippedText}</Typography>
    subtitle = <Typography>Time elapsed: <Monospace>{timeStringMs(Math.round(generationDuration))}</Monospace></Typography>
  } else if (!generatingBuilds && generationProgress) {//done
    progress = 100
    title = <Typography>Generated and tested <Monospace>{totalUnskippedString}</Monospace> Build configurations against the criteria for <b>{characterName}</b>. {generationSkippedText}</Typography>
    subtitle = <Typography>Total duration: <Monospace>{timeStringMs(Math.round(generationDuration))}</Monospace></Typography>
  } else {
    if (totBuildNumber === 0) {
      title = <Typography>Current configuration will not generate any builds for <b>{characterName}</b>. Please change your Artifact configurations, or add/include more Artifacts.</Typography>
      color = "error"
    } else if (totBuildNumber > warningBuildNumber) {
      title = <Typography>Current configuration will generate <Monospace>{totalBuildNumberString}</Monospace> potential builds for <b>{characterName}</b>. This might take quite a while to generate...</Typography>
      color = "warning"
    } else
      title = <Typography>Current configuration {totBuildNumber <= maxBuildsToShow ? "generated" : "will generate"} <Monospace>{totalBuildNumberString}</Monospace> builds for <b>{characterName}</b>.</Typography>
  }

  return <Alert severity={color} variant="filled" sx={{
    "& .MuiAlert-message": {
      flexGrow: 1
    }
  }}>
    {title && title}
    {subtitle && subtitle}
    {progress !== undefined && <Grid container spacing={1} alignItems="center">
      <Grid item>
        <Typography>{`${progress.toFixed(1)}%`}</Typography>
      </Grid>
      <Grid item flexGrow={1} >
        <BorderLinearProgress variant="determinate" value={progress} color="primary" />
      </Grid>
    </Grid>}
  </Alert>
}
Example #23
Source File: SetupScreen.tsx    From rewind with MIT License 5 votes vote down vote up
// TODO: Maybe tell which file is actually missing
export function SetupScreen() {
  // TODO: Add a guess for directory path
  const [directoryPath, setDirectoryPath] = useState<string | null>(null);
  const [saveEnabled, setSaveEnabled] = useState(false);

  const [updateOsuDirectory, updateState] = useUpdateOsuDirectoryMutation();
  const [showErrorMessage, setShowErrorMessage] = useState(false);

  const handleConfirmClick = useCallback(() => {
    if (directoryPath) {
      updateOsuDirectory({ osuStablePath: directoryPath });
    }
  }, [updateOsuDirectory, directoryPath]);

  useEffect(() => {
    if (updateState.isSuccess) {
      window.api.reboot();
    } else if (updateState.isError) {
      setShowErrorMessage(true);
    }
  }, [updateState, setShowErrorMessage]);

  const handleOnDirectoryChange = useCallback(
    (path: string | null) => {
      setDirectoryPath(path);
      setShowErrorMessage(false);
    },
    [setShowErrorMessage],
  );

  // Makes sure that the button is only clickable when it's allowed.
  useEffect(() => {
    setSaveEnabled(directoryPath !== null && !updateState.isLoading);
  }, [directoryPath, updateState.isLoading]);

  return (
    <Box
      sx={{
        height: "100vh",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <Paper elevation={1}>
        <Stack gap={2} sx={{ px: 6, py: 4 }}>
          <RewindLogo />
          {showErrorMessage && (
            <>
              <Alert severity="error">
                <div>Does not look a valid osu! directory!</div>
              </Alert>
            </>
          )}
          <DirectorySelection
            value={directoryPath}
            onChange={handleOnDirectoryChange}
            placeHolder={"Select your osu! directory"}
            badgeOnEmpty={true}
          />
          <Stack direction={"row-reverse"} gap={2}>
            <Button variant={"contained"} startIcon={<Loop />} disabled={!saveEnabled} onClick={handleConfirmClick}>
              Save & Restart
            </Button>
            <Button variant={"text"} onClick={() => window.open(setupWikiUrl)} startIcon={<Help />}>
              Help
            </Button>
          </Stack>
        </Stack>
      </Paper>
    </Box>
  );
}
Example #24
Source File: PersonDialog.tsx    From frontend with MIT License 5 votes vote down vote up
export default function PersonDialog({ label, type, onSubmit }: Props) {
  const { t } = useTranslation()
  const [open, setOpen] = useState(false)

  const handleClickOpen = () => setOpen(true)
  const handleClose = () => setOpen(false)

  return (
    <>
      <Button fullWidth variant="contained" color="info" onClick={handleClickOpen}>
        {label}
      </Button>
      <Dialog
        open={open}
        onClose={(e, reason) => {
          if (reason === 'backdropClick') return
          handleClose()
        }}
        onBackdropClick={() => false}>
        <DialogTitle>
          {label}
          <IconButton
            aria-label="close"
            onClick={handleClose}
            sx={{
              position: 'absolute',
              right: 8,
              top: 8,
              color: (theme) => theme.palette.grey[500],
            }}>
            <Close />
          </IconButton>
        </DialogTitle>
        <DialogContent>
          <Box sx={{ mb: 2 }}>
            {type === 'beneficiary' ? (
              <Alert severity="info">
                <AlertTitle>{t('campaigns:campaign.beneficiary.name')}</AlertTitle>
                Лице, в чиято полза се организира кампанията. От юридическа гледна точка,
                бенефициентът <strong>НЕ влиза</strong> във взаимоотношения с оператора при набиране
                на средства в негова полза. Всички договори, изисквания, банкова сметка на
                кампанията са на името на организатора. Възможно е бенефициентът по една кампания да
                е и неговият организатор.
              </Alert>
            ) : (
              <Alert severity="warning">
                <AlertTitle>{t('campaigns:campaign.coordinator.name')}</AlertTitle>
                Организаторът е физическото или юридическо лице, с което се сключва договор за
                набиране на средства, след като негова заявка за кампания е одобрена. Набраните
                средства се прехвърлят в неговата банкова сметка, от него се изискват отчети за
                разходените средства. Когато дадено лице иска да стане организатор на кампании,
                преминава през процес на верификация, за да се избегнат измамите. Организаторът също
                може да е и бенефициент по дадена кампания.
              </Alert>
            )}
          </Box>
          <PersonForm
            {...type}
            onSubmit={(...args) => {
              onSubmit(...args)
              handleClose()
            }}
          />
        </DialogContent>
      </Dialog>
    </>
  )
}
Example #25
Source File: index.tsx    From ExpressLRS-Configurator with GNU General Public License v3.0 5 votes vote down vote up
BuildNotificationsList: FunctionComponent<BuildNotificationsListProps> = memo(
  ({ notifications }) => {
    const toSeverity = (
      item: BuildProgressNotificationType
    ): 'error' | 'info' | 'success' => {
      switch (item) {
        case BuildProgressNotificationType.Error:
          return 'error';
        case BuildProgressNotificationType.Info:
          return 'info';
        case BuildProgressNotificationType.Success:
          return 'success';
        default:
          return 'info';
      }
    };
    // TODO: this should be used for translations
    const toText = (step: BuildFirmwareStep): string => {
      switch (step) {
        case BuildFirmwareStep.VERIFYING_BUILD_SYSTEM:
          return 'Verifying build system';
        case BuildFirmwareStep.DOWNLOADING_FIRMWARE:
          return 'Downloading firmware';
        case BuildFirmwareStep.BUILDING_USER_DEFINES:
          return 'Building user_defines.txt';
        case BuildFirmwareStep.BUILDING_FIRMWARE:
          return 'Compiling firmware';
        case BuildFirmwareStep.FLASHING_FIRMWARE:
          return 'Flashing device';
        default:
          return '';
      }
    };
    return (
      <>
        {notifications.map((item, idx) => {
          return (
            <React.Fragment key={`${idx}-${item.step}`}>
              <Alert sx={styles.notification} severity={toSeverity(item.type)}>
                {item?.step !== undefined &&
                  item.step !== null &&
                  toText(item.step)}
              </Alert>
            </React.Fragment>
          );
        })}
      </>
    );
  }
)
Example #26
Source File: SnackbarContext.tsx    From firecms with MIT License 5 votes vote down vote up
SnackbarProvider: React.FC<PropsWithChildren<{}>> = ({ children }: PropsWithChildren<{}>) => {

    const [isOpen, setIsOpen] = useState(false);
    const [title, setTitle] = useState<string | undefined>(undefined);
    const [message, setMessage] = useState<string | undefined>(undefined);
    const [type, setType] = useState<SnackbarMessageType | undefined>(undefined);

    const close = () => {
        setIsOpen(false);
        setTitle(undefined);
        setMessage(undefined);
        setType(undefined);
    };

    const open = (props: {
        type: SnackbarMessageType;
        title?: string;
        message: string;
    }) => {
        const { type, message, title } = props;
        setType(type);
        setMessage(message);
        setTitle(title);
        setIsOpen(true);
    };

    return (
        <SnackbarContext.Provider
            value={{
                isOpen,
                close,
                open
            }}
        >

            {children}

            <Snackbar open={isOpen}
                      autoHideDuration={3000}
                      onClose={(_) => close()}>
                <Alert elevation={1}
                       variant="filled"
                       onClose={(_) => close()}
                       severity={type}>

                    {title && <div>{title}</div>}
                    {message && <div>{message}</div>}

                </Alert>
            </Snackbar>

        </SnackbarContext.Provider>
    );
}
Example #27
Source File: index.tsx    From Cromwell with MIT License 5 votes vote down vote up
function CustomAlert(props: AlertProps) {
    return <Alert elevation={6} variant="filled" {...props} />;
}
Example #28
Source File: index.tsx    From ExpressLRS-Configurator with GNU General Public License v3.0 5 votes vote down vote up
BuildResponse: FunctionComponent<BuildResponseProps> = memo(
  ({ response, firmwareVersionData }) => {
    // TODO: translations
    const toTitle = (errorType: BuildFirmwareErrorType | undefined): string => {
      if (errorType === null || errorType === undefined) {
        return 'Error';
      }
      switch (errorType) {
        case BuildFirmwareErrorType.GenericError:
          return 'Error';
        case BuildFirmwareErrorType.GitDependencyError:
          return 'Git dependency error';
        case BuildFirmwareErrorType.PythonDependencyError:
          return 'Python dependency error';
        case BuildFirmwareErrorType.PlatformioDependencyError:
          return 'Platformio dependency error';
        case BuildFirmwareErrorType.BuildError:
          return 'Build error';
        case BuildFirmwareErrorType.FlashError:
          return 'Flash error';
        case BuildFirmwareErrorType.TargetMismatch:
          return 'The target you are trying to flash does not match the devices current target, if you are sure you want to do this, click Force Flash below';
        default:
          return '';
      }
    };
    return (
      <>
        {response !== undefined && response.success && (
          <Alert severity="success">Success!</Alert>
        )}
        {response !== undefined && !response.success && (
          <Alert sx={styles.errorMessage} severity="error">
            <AlertTitle>
              {toTitle(
                response?.errorType ?? BuildFirmwareErrorType.GenericError
              )}
            </AlertTitle>
            <p>
              An error has occured, see the above log for the exact error
              message. If you have not already done so, visit{' '}
              <DocumentationLink
                firmwareVersion={firmwareVersionData}
                url="https://www.expresslrs.org/{version}/"
              >
                Expresslrs.org
              </DocumentationLink>{' '}
              and read the{' '}
              <DocumentationLink
                firmwareVersion={firmwareVersionData}
                url="https://www.expresslrs.org/{version}/quick-start/getting-started/"
              >
                Flashing Guide
              </DocumentationLink>{' '}
              for your particular device as well as the{' '}
              <DocumentationLink
                firmwareVersion={firmwareVersionData}
                url="https://www.expresslrs.org/{version}/quick-start/troubleshooting/#flashingupdating"
              >
                Troubleshooting Guide
              </DocumentationLink>
              . If you are still having issues after reviewing the
              documentation, please copy the build logs above to an online paste
              site and post in the #help-and-support channel on the{' '}
              <DocumentationLink
                firmwareVersion={firmwareVersionData}
                url="https://discord.gg/dS6ReFY"
              >
                ExpressLRS Discord
              </DocumentationLink>{' '}
              with a link to the logs and other relevant information like your
              device, which flashing method you were using, and what steps you
              have already taken to resolve the issue.
            </p>
          </Alert>
        )}
      </>
    );
  }
)
Example #29
Source File: ArtifactEditor.tsx    From genshin-optimizer with MIT License 4 votes vote down vote up
export default function ArtifactEditor({ artifactIdToEdit = "", cancelEdit, allowUpload = false, allowEmpty = false, disableEditSetSlot: disableEditSlotProp = false }:
  { artifactIdToEdit?: string, cancelEdit: () => void, allowUpload?: boolean, allowEmpty?: boolean, disableEditSetSlot?: boolean }) {
  const { t } = useTranslation("artifact")

  const artifactSheets = usePromise(ArtifactSheet.getAll, [])

  const { database } = useContext(DatabaseContext)

  const [show, setShow] = useState(false)

  const [dirtyDatabase, setDirtyDatabase] = useForceUpdate()
  useEffect(() => database.followAnyArt(setDirtyDatabase), [database, setDirtyDatabase])

  const [editorArtifact, artifactDispatch] = useReducer(artifactReducer, undefined)
  const artifact = useMemo(() => editorArtifact && parseArtifact(editorArtifact), [editorArtifact])

  const [modalShow, setModalShow] = useState(false)

  const [{ processed, outstanding }, dispatchQueue] = useReducer(queueReducer, { processed: [], outstanding: [] })
  const firstProcessed = processed[0] as ProcessedEntry | undefined
  const firstOutstanding = outstanding[0] as OutstandingEntry | undefined

  const processingImageURL = usePromise(firstOutstanding?.imageURL, [firstOutstanding?.imageURL])
  const processingResult = usePromise(firstOutstanding?.result, [firstOutstanding?.result])

  const remaining = processed.length + outstanding.length

  const image = firstProcessed?.imageURL ?? processingImageURL
  const { artifact: artifactProcessed, texts } = firstProcessed ?? {}
  // const fileName = firstProcessed?.fileName ?? firstOutstanding?.fileName ?? "Click here to upload Artifact screenshot files"

  const disableEditSetSlot = disableEditSlotProp || !!artifact?.location

  useEffect(() => {
    if (!artifact && artifactProcessed)
      artifactDispatch({ type: "overwrite", artifact: artifactProcessed })
  }, [artifact, artifactProcessed, artifactDispatch])

  useEffect(() => {
    const numProcessing = Math.min(maxProcessedCount - processed.length, maxProcessingCount, outstanding.length)
    const processingCurrent = numProcessing && !outstanding[0].result
    outstanding.slice(0, numProcessing).forEach(processEntry)
    if (processingCurrent)
      dispatchQueue({ type: "processing" })
  }, [processed.length, outstanding])

  useEffect(() => {
    if (processingResult)
      dispatchQueue({ type: "processed", ...processingResult })
  }, [processingResult, dispatchQueue])

  const uploadFiles = useCallback((files: FileList) => {
    setShow(true)
    dispatchQueue({ type: "upload", files: [...files].map(file => ({ file, fileName: file.name })) })
  }, [dispatchQueue, setShow])
  const clearQueue = useCallback(() => dispatchQueue({ type: "clear" }), [dispatchQueue])

  useEffect(() => {
    const pasteFunc = (e: any) => uploadFiles(e.clipboardData.files)
    allowUpload && window.addEventListener('paste', pasteFunc);
    return () => {
      if (allowUpload) window.removeEventListener('paste', pasteFunc)
    }
  }, [uploadFiles, allowUpload])

  const onUpload = useCallback(
    e => {
      uploadFiles(e.target.files)
      e.target.value = null // reset the value so the same file can be uploaded again...
    },
    [uploadFiles],
  )

  const { old, oldType }: { old: ICachedArtifact | undefined, oldType: "edit" | "duplicate" | "upgrade" | "" } = useMemo(() => {
    const databaseArtifact = dirtyDatabase && artifactIdToEdit && database._getArt(artifactIdToEdit)
    if (databaseArtifact) return { old: databaseArtifact, oldType: "edit" }
    if (artifact === undefined) return { old: undefined, oldType: "" }
    const { duplicated, upgraded } = dirtyDatabase && database.findDuplicates(artifact)
    return { old: duplicated[0] ?? upgraded[0], oldType: duplicated.length !== 0 ? "duplicate" : "upgrade" }
  }, [artifact, artifactIdToEdit, database, dirtyDatabase])

  const { artifact: cachedArtifact, errors } = useMemo(() => {
    if (!artifact) return { artifact: undefined, errors: [] as Displayable[] }
    const validated = validateArtifact(artifact, artifactIdToEdit)
    if (old) {
      validated.artifact.location = old.location
      validated.artifact.exclude = old.exclude
    }
    return validated
  }, [artifact, artifactIdToEdit, old])

  // Overwriting using a different function from `databaseArtifact` because `useMemo` does not
  // guarantee to trigger *only when* dependencies change, which is necessary in this case.
  useEffect(() => {
    if (artifactIdToEdit === "new") {
      setShow(true)
      artifactDispatch({ type: "reset" })
    }
    const databaseArtifact = artifactIdToEdit && dirtyDatabase && database._getArt(artifactIdToEdit)
    if (databaseArtifact) {
      setShow(true)
      artifactDispatch({ type: "overwrite", artifact: deepClone(databaseArtifact) })
    }
  }, [artifactIdToEdit, database, dirtyDatabase])

  const sheet = artifact ? artifactSheets?.[artifact.setKey] : undefined
  const reset = useCallback(() => {
    cancelEdit?.();
    dispatchQueue({ type: "pop" })
    artifactDispatch({ type: "reset" })
  }, [cancelEdit, artifactDispatch])
  const update = useCallback((newValue: Partial<IArtifact>) => {
    const newSheet = newValue.setKey ? artifactSheets![newValue.setKey] : sheet!

    function pick<T>(value: T | undefined, available: readonly T[], prefer?: T): T {
      return (value && available.includes(value)) ? value : (prefer ?? available[0])
    }

    if (newValue.setKey) {
      newValue.rarity = pick(artifact?.rarity, newSheet.rarity, Math.max(...newSheet.rarity) as ArtifactRarity)
      newValue.slotKey = pick(artifact?.slotKey, newSheet.slots)
    }
    if (newValue.rarity)
      newValue.level = artifact?.level ?? 0
    if (newValue.level)
      newValue.level = clamp(newValue.level, 0, 4 * (newValue.rarity ?? artifact!.rarity))
    if (newValue.slotKey)
      newValue.mainStatKey = pick(artifact?.mainStatKey, Artifact.slotMainStats(newValue.slotKey))

    if (newValue.mainStatKey) {
      newValue.substats = [0, 1, 2, 3].map(i =>
        (artifact && artifact.substats[i].key !== newValue.mainStatKey) ? artifact!.substats[i] : { key: "", value: 0 })
    }
    artifactDispatch({ type: "update", artifact: newValue })
  }, [artifact, artifactSheets, sheet, artifactDispatch])
  const setSubstat = useCallback((index: number, substat: ISubstat) => {
    artifactDispatch({ type: "substat", index, substat })
  }, [artifactDispatch])
  const isValid = !errors.length
  const canClearArtifact = (): boolean => window.confirm(t`editor.clearPrompt` as string)
  const { rarity = 5, level = 0, slotKey = "flower" } = artifact ?? {}
  const { currentEfficiency = 0, maxEfficiency = 0 } = cachedArtifact ? Artifact.getArtifactEfficiency(cachedArtifact, allSubstatFilter) : {}
  const preventClosing = processed.length || outstanding.length
  const onClose = useCallback(
    (e) => {
      if (preventClosing) e.preventDefault()
      setShow(false)
      cancelEdit()
    }, [preventClosing, setShow, cancelEdit])

  const theme = useTheme();
  const grmd = useMediaQuery(theme.breakpoints.up('md'));

  const element = artifact ? allElementsWithPhy.find(ele => artifact.mainStatKey.includes(ele)) : undefined
  const color = artifact
    ? element ?? "success"
    : "primary"

  return <ModalWrapper open={show} onClose={onClose} >
    <Suspense fallback={<Skeleton variant="rectangular" sx={{ width: "100%", height: show ? "100%" : 64 }} />}><CardDark >
      <UploadExplainationModal modalShow={modalShow} hide={() => setModalShow(false)} />
      <CardHeader
        title={<Trans t={t} i18nKey="editor.title" >Artifact Editor</Trans>}
        action={<CloseButton disabled={!!preventClosing} onClick={onClose} />}
      />
      <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
        <Grid container spacing={1} columns={{ xs: 1, md: 2 }} >
          {/* Left column */}
          <Grid item xs={1} display="flex" flexDirection="column" gap={1}>
            {/* set & rarity */}
            <ButtonGroup sx={{ display: "flex", mb: 1 }}>
              {/* Artifact Set */}
              <ArtifactSetSingleAutocomplete
                size="small"
                disableClearable
                artSetKey={artifact?.setKey ?? ""}
                setArtSetKey={setKey => update({ setKey: setKey as ArtifactSetKey })}
                sx={{ flexGrow: 1 }}
                disabled={disableEditSetSlot}
              />
              {/* rarity dropdown */}
              <ArtifactRarityDropdown rarity={artifact ? rarity : undefined} onChange={r => update({ rarity: r })} filter={r => !!sheet?.rarity?.includes?.(r)} disabled={disableEditSetSlot || !sheet} />
            </ButtonGroup>

            {/* level */}
            <Box component="div" display="flex">
              <CustomNumberTextField id="filled-basic" label="Level" variant="filled" sx={{ flexShrink: 1, flexGrow: 1, mr: 1, my: 0 }} margin="dense" size="small"
                value={level} disabled={!sheet} placeholder={`0~${rarity * 4}`} onChange={l => update({ level: l })}
              />
              <ButtonGroup >
                <Button onClick={() => update({ level: level - 1 })} disabled={!sheet || level === 0}>-</Button>
                {rarity ? [...Array(rarity + 1).keys()].map(i => 4 * i).map(i => <Button key={i} onClick={() => update({ level: i })} disabled={!sheet || level === i}>{i}</Button>) : null}
                <Button onClick={() => update({ level: level + 1 })} disabled={!sheet || level === (rarity * 4)}>+</Button>
              </ButtonGroup>
            </Box>

            {/* slot */}
            <Box component="div" display="flex">
              <ArtifactSlotDropdown disabled={disableEditSetSlot || !sheet} slotKey={slotKey} onChange={slotKey => update({ slotKey })} />
              <CardLight sx={{ p: 1, ml: 1, flexGrow: 1 }}>
                <Suspense fallback={<Skeleton width="60%" />}>
                  <Typography color="text.secondary">
                    {sheet?.getSlotName(artifact!.slotKey) ? <span><ImgIcon src={sheet.slotIcons[artifact!.slotKey]} /> {sheet?.getSlotName(artifact!.slotKey)}</span> : t`editor.unknownPieceName`}
                  </Typography>
                </Suspense>
              </CardLight>
            </Box>

            {/* main stat */}
            <Box component="div" display="flex">
              <DropdownButton startIcon={element ? uncoloredEleIcons[element] : (artifact?.mainStatKey ? StatIcon[artifact.mainStatKey] : undefined)}
                title={<b>{artifact ? KeyMap.getArtStr(artifact.mainStatKey) : t`mainStat`}</b>} disabled={!sheet} color={color} >
                {Artifact.slotMainStats(slotKey).map(mainStatK =>
                  <MenuItem key={mainStatK} selected={artifact?.mainStatKey === mainStatK} disabled={artifact?.mainStatKey === mainStatK} onClick={() => update({ mainStatKey: mainStatK })} >
                    <ListItemIcon>{StatIcon[mainStatK]}</ListItemIcon>
                    <ListItemText>{KeyMap.getArtStr(mainStatK)}</ListItemText>
                  </MenuItem>)}
              </DropdownButton>
              <CardLight sx={{ p: 1, ml: 1, flexGrow: 1 }}>
                <Typography color="text.secondary">
                  {artifact ? `${cacheValueString(Artifact.mainStatValue(artifact.mainStatKey, rarity, level), KeyMap.unit(artifact.mainStatKey))}${KeyMap.unit(artifact.mainStatKey)}` : t`mainStat`}
                </Typography>
              </CardLight>
            </Box>

            {/* Current/Max Substats Efficiency */}
            <SubstatEfficiencyDisplayCard valid={isValid} efficiency={currentEfficiency} t={t} />
            {currentEfficiency !== maxEfficiency && <SubstatEfficiencyDisplayCard max valid={isValid} efficiency={maxEfficiency} t={t} />}

            {/* Image OCR */}
            {allowUpload && <CardLight>
              <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
                {/* TODO: artifactDispatch not overwrite */}
                <Suspense fallback={<Skeleton width="100%" height="100" />}>
                  <Grid container spacing={1} alignItems="center">
                    <Grid item flexGrow={1}>
                      <label htmlFor="contained-button-file">
                        <InputInvis accept="image/*" id="contained-button-file" multiple type="file" onChange={onUpload} />
                        <Button component="span" startIcon={<PhotoCamera />}>
                          Upload Screenshot (or Ctrl-V)
                        </Button>
                      </label>
                    </Grid>
                    <Grid item>
                      <Button color="info" sx={{ px: 2, minWidth: 0 }} onClick={() => setModalShow(true)}><Typography><FontAwesomeIcon icon={faQuestionCircle} /></Typography></Button>
                    </Grid>
                  </Grid>
                  {image && <Box display="flex" justifyContent="center">
                    <Box component="img" src={image} width="100%" maxWidth={350} height="auto" alt="Screenshot to parse for artifact values" />
                  </Box>}
                  {remaining > 0 && <CardDark sx={{ pl: 2 }} ><Grid container spacing={1} alignItems="center" >
                    {!firstProcessed && firstOutstanding && <Grid item>
                      <CircularProgress size="1em" />
                    </Grid>}
                    <Grid item flexGrow={1}>
                      <Typography>
                        <span>
                          Screenshots in file-queue: <b>{remaining}</b>
                          {/* {process.env.NODE_ENV === "development" && ` (Debug: Processed ${processed.length}/${maxProcessedCount}, Processing: ${outstanding.filter(entry => entry.result).length}/${maxProcessingCount}, Outstanding: ${outstanding.length})`} */}
                        </span>
                      </Typography>
                    </Grid>
                    <Grid item>
                      <Button size="small" color="error" onClick={clearQueue}>Clear file-queue</Button>
                    </Grid>
                  </Grid></CardDark>}
                </Suspense>
              </CardContent>
            </CardLight>}
          </Grid>

          {/* Right column */}
          <Grid item xs={1} display="flex" flexDirection="column" gap={1}>
            {/* substat selections */}
            {[0, 1, 2, 3].map((index) => <SubstatInput key={index} index={index} artifact={cachedArtifact} setSubstat={setSubstat} />)}
            {texts && <CardLight><CardContent>
              <div>{texts.slotKey}</div>
              <div>{texts.mainStatKey}</div>
              <div>{texts.mainStatVal}</div>
              <div>{texts.rarity}</div>
              <div>{texts.level}</div>
              <div>{texts.substats}</div>
              <div>{texts.setKey}</div>
            </CardContent></CardLight>}
          </Grid>
        </Grid>

        {/* Duplicate/Updated/Edit UI */}
        {old && <Grid container sx={{ justifyContent: "space-around" }} spacing={1} >
          <Grid item xs={12} md={5.5} lg={4} ><CardLight>
            <Typography sx={{ textAlign: "center" }} py={1} variant="h6" color="text.secondary" >{oldType !== "edit" ? (oldType === "duplicate" ? t`editor.dupArt` : t`editor.upArt`) : t`editor.beforeEdit`}</Typography>
            <ArtifactCard artifactObj={old} />
          </CardLight></Grid>
          {grmd && <Grid item md={1} display="flex" alignItems="center" justifyContent="center" >
            <CardLight sx={{ display: "flex" }}><ChevronRight sx={{ fontSize: 40 }} /></CardLight>
          </Grid>}
          <Grid item xs={12} md={5.5} lg={4} ><CardLight>
            <Typography sx={{ textAlign: "center" }} py={1} variant="h6" color="text.secondary" >{t`editor.preview`}</Typography>
            <ArtifactCard artifactObj={cachedArtifact} />
          </CardLight></Grid>
        </Grid>}

        {/* Error alert */}
        {!isValid && <Alert variant="filled" severity="error" >{errors.map((e, i) => <div key={i}>{e}</div>)}</Alert>}

        {/* Buttons */}
        <Grid container spacing={2}>
          <Grid item>
            {oldType === "edit" ?
              <Button startIcon={<Add />} onClick={() => {
                database.updateArt(editorArtifact!, old!.id);
                if (allowEmpty) reset()
                else {
                  setShow(false)
                  cancelEdit()
                }
              }} disabled={!editorArtifact || !isValid} color="primary">
                {t`editor.btnSave`}
              </Button> :
              <Button startIcon={<Add />} onClick={() => {
                database.createArt(artifact!);
                if (allowEmpty) reset()
                else {
                  setShow(false)
                  cancelEdit()
                }
              }} disabled={!artifact || !isValid} color={oldType === "duplicate" ? "warning" : "primary"}>
                {t`editor.btnAdd`}
              </Button>}
          </Grid>
          <Grid item flexGrow={1}>
            {allowEmpty && <Button startIcon={<Replay />} disabled={!artifact} onClick={() => { canClearArtifact() && reset() }} color="error">{t`editor.btnClear`}</Button>}
          </Grid>
          <Grid item>
            {process.env.NODE_ENV === "development" && <Button color="info" startIcon={<Shuffle />} onClick={async () => artifactDispatch({ type: "overwrite", artifact: await randomizeArtifact() })}>{t`editor.btnRandom`}</Button>}
          </Grid>
          {old && oldType !== "edit" && <Grid item>
            <Button startIcon={<Update />} onClick={() => { database.updateArt(editorArtifact!, old.id); allowEmpty ? reset() : setShow(false) }} disabled={!editorArtifact || !isValid} color="success">{t`editor.btnUpdate`}</Button>
          </Grid>}
        </Grid>
      </CardContent>
    </CardDark ></Suspense>
  </ModalWrapper>
}