@mui/icons-material#DeleteForever TypeScript Examples

The following examples show how to use @mui/icons-material#DeleteForever. 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: VariantsTab.tsx    From Cromwell with MIT License 4 votes vote down vote up
export default function VariantsTab({ product, setProdData, forceUpdate }: {
  product: TProduct;
  attributes: TAttribute[];
  setProdData: (data: TProduct) => void;
  forceUpdate: () => void;
}) {
  const [expandedVariants, setExpandedVariants] = useState<Record<string, boolean>>({});

  const usedAttributes = (product.attributes ?? []).filter(attr => attr.values?.length && attr.key)
    .map(attr => ({ ...attr, values: attr.values.filter(Boolean) }));

  if (product.variants?.length) {
    for (const variant of product.variants) {
      if (!variant.id) variant.id = getRandStr(8) as any;
    }
  }

  const handleAddProductVariant = () => {
    if (!usedAttributes.length) {
      toast.warn('No attributes selected');
      return;
    }
    setProdData({
      ...product,
      variants: [...(product?.variants ?? []), {
        id: getRandStr(8) as any,
      }],
    });
    forceUpdate();
  }

  return (
    <Box>
      <Box>
        {product?.variants?.map((variant) => (
          <Box key={variant.id} className={styles.paper} sx={{ my: 2 }}>
            <Box onClick={() => setExpandedVariants({
              ...expandedVariants,
              [variant.id]: !expandedVariants[variant.id]
            })}
              sx={{
                display: 'flex', justifyContent: 'space-between',
                alignItems: 'center', cursor: 'pointer', p: 1,
                borderBottom: expandedVariants[variant.id] && '1px solid #aaa'
              }}>
              <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                {usedAttributes.map(attr => (
                  <Select
                    label={attr.key}
                    key={attr.key}
                    options={[...attr.values.map(value => ({
                      label: value.value,
                      value: value.value,
                    })), {
                      label: 'any',
                      value: 'any'
                    }]}
                    value={variant.attributes?.[attr.key] ?? 'any'}
                    size="small"
                    sx={{ mr: 2 }}
                    onClick={event => event.stopPropagation()}
                    onChange={(event) => {
                      const newProduct = clone({ proto: true })(product);
                      newProduct.variants = newProduct.variants.map(v => {
                        if (v.id === variant.id) {
                          v.attributes = {
                            ...(v.attributes ?? {}),
                            [attr.key]: event.target.value,
                          }
                        }
                        return v;
                      });
                      setProdData(newProduct);
                      forceUpdate();
                    }}
                  />
                ))}
              </Box>
              <Box>
                <IconButton
                  onClick={() => {
                    const newProduct = clone({ proto: true })(product);
                    newProduct.variants = newProduct.variants.filter(v => variant.id !== v.id);
                    setProdData(newProduct);
                    forceUpdate();
                  }}
                >
                  <DeleteForever />
                </IconButton>
                <IconButton
                  style={{ transform: expandedVariants[variant.id] ? 'rotate(180deg)' : '' }}
                  aria-label="show more"
                >
                  <ExpandMoreIcon />
                </IconButton>
              </Box>
            </Box>
            <Collapse in={expandedVariants[variant.id]} timeout="auto" unmountOnExit>
              <Box sx={{ my: 1, p: 2 }}>
                <MainInfoCard
                  product={variant}
                  setProdData={(data: TProductVariant) => {
                    const newProduct = clone({ proto: true })(product);
                    if (!newProduct.variants) newProduct.variants = [];
                    newProduct.variants = newProduct.variants.map(v => {
                      if (v.id === variant.id) {
                        return { ...data };
                      }
                      return v;
                    });
                    setProdData(newProduct);
                    forceUpdate();
                  }}
                  isProductVariant
                />
              </Box>
            </Collapse>
          </Box>
        ))}
      </Box>
      <Box sx={{ width: '100%', my: 4 }}>
        <Button variant="contained" sx={{ mx: 'auto', display: 'block' }}
          onClick={handleAddProductVariant}>Add product variant</Button>
      </Box>
    </Box>
  )
}
Example #2
Source File: index.tsx    From genshin-optimizer with MIT License 4 votes vote down vote up
export default function PageCharacter(props) {
  // TODO: #412 We shouldn't be loading all the character translation files. Should have a separate lookup file for character name.
  const { t } = useTranslation(["page_character", ...allCharacterKeys.map(k => `char_${k}_gen`)])
  const { database } = useContext(DatabaseContext)
  const [state, stateDispatch] = useDBState("CharacterDisplay", initialState)
  const [searchTerm, setSearchTerm] = useState("")
  const deferredSearchTerm = useDeferredValue(searchTerm)
  const [pageIdex, setpageIdex] = useState(0)
  const invScrollRef = useRef<HTMLDivElement>(null)
  const setPage = useCallback(
    (e, value) => {
      invScrollRef.current?.scrollIntoView({ behavior: "smooth" })
      setpageIdex(value - 1);
    },
    [setpageIdex, invScrollRef],
  )

  const brPt = useMediaQueryUp()
  const maxNumToDisplay = numToShowMap[brPt]

  const [newCharacter, setnewCharacter] = useState(false)
  const [dbDirty, forceUpdate] = useForceUpdate()
  //set follow, should run only once
  useEffect(() => {
    ReactGA.send({ hitType: "pageview", page: '/characters' })
    return database.followAnyChar(forceUpdate)
  }, [forceUpdate, database])

  const characterSheets = usePromise(CharacterSheet.getAll, [])

  const deleteCharacter = useCallback(async (cKey: CharacterKey) => {
    const chararcterSheet = await CharacterSheet.get(cKey)
    let name = chararcterSheet?.name
    //use translated string
    if (typeof name === "object")
      name = i18next.t(`char_${cKey}_gen:name`)

    if (!window.confirm(t("removeCharacter", { value: name }))) return
    database.removeChar(cKey)
  }, [database, t])

  const editCharacter = useCharSelectionCallback()

  const navigate = useNavigate()

  const { element, weaponType } = state
  const sortConfigs = useMemo(() => characterSheets && characterSortConfigs(database, characterSheets), [database, characterSheets])
  const filterConfigs = useMemo(() => characterSheets && characterFilterConfigs(database, characterSheets), [database, characterSheets])
  const { charKeyList, totalCharNum } = useMemo(() => {
    const chars = database._getCharKeys()
    const totalCharNum = chars.length
    if (!sortConfigs || !filterConfigs) return { charKeyList: [], totalCharNum }
    const charKeyList = database._getCharKeys()
      .filter(filterFunction({ element, weaponType, favorite: "yes", name: deferredSearchTerm }, filterConfigs))
      .sort(sortFunction(state.sortType, state.ascending, sortConfigs))
      .concat(
        database._getCharKeys()
          .filter(filterFunction({ element, weaponType, favorite: "no", name: deferredSearchTerm }, filterConfigs))
          .sort(sortFunction(state.sortType, state.ascending, sortConfigs)))
    return dbDirty && { charKeyList, totalCharNum }
  },
    [dbDirty, database, sortConfigs, state.sortType, state.ascending, element, filterConfigs, weaponType, deferredSearchTerm])

  const { charKeyListToShow, numPages, currentPageIndex } = useMemo(() => {
    const numPages = Math.ceil(charKeyList.length / maxNumToDisplay)
    const currentPageIndex = clamp(pageIdex, 0, numPages - 1)
    return { charKeyListToShow: charKeyList.slice(currentPageIndex * maxNumToDisplay, (currentPageIndex + 1) * maxNumToDisplay), numPages, currentPageIndex }
  }, [charKeyList, pageIdex, maxNumToDisplay])

  const totalShowing = charKeyList.length !== totalCharNum ? `${charKeyList.length}/${totalCharNum}` : `${totalCharNum}`

  return <Box my={1} display="flex" flexDirection="column" gap={1}>
    <CardDark ref={invScrollRef} ><CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
      <Grid container spacing={1}>
        <Grid item>
          <WeaponToggle sx={{ height: "100%" }} onChange={weaponType => stateDispatch({ weaponType })} value={state.weaponType} size="small" />
        </Grid>
        <Grid item>
          <ElementToggle sx={{ height: "100%" }} onChange={element => stateDispatch({ element })} value={state.element} size="small" />
        </Grid>
        <Grid item flexGrow={1}>
          <TextField
            autoFocus
            value={searchTerm}
            onChange={(e: ChangeEvent<HTMLTextAreaElement>) => setSearchTerm(e.target.value)}
            label={t("characterName")}
          />
        </Grid>
        <Grid item >
          <SortByButton sx={{ height: "100%" }}
            sortKeys={characterSortKeys} value={state.sortType} onChange={sortType => stateDispatch({ sortType })}
            ascending={state.ascending} onChangeAsc={ascending => stateDispatch({ ascending })} />
        </Grid>
      </Grid>
      <Grid container alignItems="flex-end">
        <Grid item flexGrow={1}>
          <Pagination count={numPages} page={currentPageIndex + 1} onChange={setPage} />
        </Grid>
        <Grid item>
          <ShowingCharacter numShowing={charKeyListToShow.length} total={totalShowing} t={t} />
        </Grid>
      </Grid>
    </CardContent></CardDark>
    <Suspense fallback={<Skeleton variant="rectangular" sx={{ width: "100%", height: "100%", minHeight: 5000 }} />}>
      <Grid container spacing={1} columns={columns}>
        <Grid item xs={1} >
          <CardDark sx={{ height: "100%", minHeight: 400, width: "100%", display: "flex", flexDirection: "column" }}>
            <CardContent>
              <Typography sx={{ textAlign: "center" }}><Trans t={t} i18nKey="addNew" /></Typography>
            </CardContent>
            <CharacterSelectionModal newFirst show={newCharacter} onHide={() => setnewCharacter(false)} onSelect={editCharacter} />
            <Box sx={{
              flexGrow: 1,
              display: "flex",
              justifyContent: "center",
              alignItems: "center"
            }}
            >
              <Button onClick={() => setnewCharacter(true)} color="info" sx={{ borderRadius: "1em" }}>
                <Typography variant="h1"><FontAwesomeIcon icon={faPlus} className="fa-fw" /></Typography>
              </Button>
            </Box>
          </CardDark>
        </Grid>
        {charKeyListToShow.map(charKey =>
          <Grid item key={charKey} xs={1} >
            <CharacterCard
              characterKey={charKey}
              onClick={() => navigate(`${charKey}`)}
              footer={<><Divider /><Box sx={{ py: 1, px: 2, display: "flex", gap: 1, justifyContent: "space-between" }}>
                <BootstrapTooltip placement="top" title={<Typography>{t("tabs.talent")}</Typography>}>
                  <IconButton onClick={() => navigate(`${charKey}/talent`)}>
                    <FactCheck />
                  </IconButton>
                </BootstrapTooltip>
                <BootstrapTooltip placement="top" title={<Typography>{t("tabs.equip")}</Typography>}>
                  <IconButton onClick={() => navigate(`${charKey}/equip`)} >
                    <Checkroom />
                  </IconButton>
                </BootstrapTooltip>
                <BootstrapTooltip placement="top" title={<Typography>{t("tabs.teambuffs")}</Typography>}>
                  <IconButton onClick={() => navigate(`${charKey}/teambuffs`)} >
                    <Groups />
                  </IconButton>
                </BootstrapTooltip>
                <BootstrapTooltip placement="top" title={<Typography>{t("tabs.optimize")}</Typography>}>
                  <IconButton onClick={() => navigate(`${charKey}/optimize`)} >
                    <Calculate />
                  </IconButton>
                </BootstrapTooltip>
                <Divider orientation="vertical" />
                <BootstrapTooltip placement="top" title={<Typography>{t("delete")}</Typography>}>
                  <IconButton color="error" onClick={() => deleteCharacter(charKey)}>
                    <DeleteForever />
                  </IconButton>
                </BootstrapTooltip>
              </Box></>}
            />
          </Grid>)}
      </Grid>
    </Suspense>
    {numPages > 1 && <CardDark ><CardContent>
      <Grid container alignItems="flex-end">
        <Grid item flexGrow={1}>
          <Pagination count={numPages} page={currentPageIndex + 1} onChange={setPage} />
        </Grid>
        <Grid item>
          <ShowingCharacter numShowing={charKeyListToShow.length} total={totalShowing} t={t} />
        </Grid>
      </Grid>
    </CardContent></CardDark>}
  </Box>
}
Example #3
Source File: Files.tsx    From NekoMaid with MIT License 4 votes vote down vote up
Files: React.FC = () => {
  const plugin = usePlugin()
  const theme = useTheme()
  const his = useHistory()
  const loc = useLocation()
  const drawerWidth = useDrawerWidth()
  const tree = useRef<HTMLHRElement | null>(null)
  const editor = useRef<UnControlled | null>(null)
  const prevExpanded = useRef<string[]>([])
  const dirs = useRef<Record<string, boolean>>({ })
  // eslint-disable-next-line func-call-spacing
  const loading = useRef<Record<string, () => Promise<void>> & { '!#LOADING'?: boolean }>({ })
  const [id, setId] = useState(0)
  const [curPath, setCurPath] = useState('')
  const [progress, setProgress] = useState(-1)
  const [copyPath, setCopyPath] = useState('')
  const [expanded, setExpanded] = useState<string[]>([])
  const [compressFile, setCompressFile] = useState<string | null>(null)
  const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null)

  const isDir = !!dirs.current[curPath]
  const dirPath = isDir ? curPath : curPath.substring(0, curPath.lastIndexOf('/'))

  const spacing = theme.spacing(3)
  const refresh = () => {
    loading.current = { }
    dirs.current = { }
    prevExpanded.current = []
    setCurPath('')
    setExpanded([])
    setId(id + 1)
  }

  useEffect(() => {
    if (!tree.current) return
    const resize = () => {
      if (!tree.current) return
      const height = tree.current.style.maxHeight = (window.innerHeight - tree.current.offsetTop - parseInt(spacing)) + 'px'
      const style = (editor as any).current?.editor?.display?.wrapper?.style
      if (style) style.height = height
    }
    resize()
    window.addEventListener('resize', resize)
    return window.removeEventListener('resize', resize)
  }, [tree.current, spacing])

  return <Box sx={{ height: '100vh', py: 3 }}>
    <Toolbar />
    <Container maxWidth={false}>
      <Grid container spacing={3} sx={{ width: { sm: `calc(100vw - ${drawerWidth}px - ${theme.spacing(3)})` } }}>
        <Grid item lg={4} md={12} xl={3} xs={12}>
          <Card sx={{ minHeight: 400 }}>
            <CardHeader
              title={lang.files.filesList}
              sx={{ position: 'relative' }}
              action={<Box sx={{ position: 'absolute', right: theme.spacing(1), top: '50%', transform: 'translateY(-50%)' }}
            >
              <Tooltip title={lang.files.delete}><span>
                <IconButton
                  disabled={!curPath}
                  size='small'
                  onClick={() => dialog({
                    okButton: { color: 'error' },
                    content: <>{lang.files.confirmDelete(<span className='bold'>{curPath}</span>)}&nbsp;
                      <span className='bold' style={{ color: theme.palette.error.main }}>({lang.unrecoverable})</span></>
                  }).then(it => it && plugin.emit('files:update', (res: boolean) => {
                    action(res)
                    if (!res) return
                    refresh()
                    if (loc.pathname.replace(/^\/NekoMaid\/files\/?/, '') === curPath) his.push('/NekoMaid/files')
                  }, curPath))}
                ><DeleteForever /></IconButton>
              </span></Tooltip>
              <Tooltip title={lang.files.createFile}>
                <IconButton size='small' onClick={() => fileNameDialog(lang.files.createFile, curPath)
                  .then(it => it != null && his.push(`/NekoMaid/files/${dirPath ? dirPath + '/' : ''}${it}`))}>
              <Description /></IconButton></Tooltip>
              <Tooltip title={lang.files.createFolder}>
                <IconButton size='small' onClick={() => fileNameDialog(lang.files.createFolder, curPath)
                  .then(it => it != null && plugin.emit('files:createDirectory', (res: boolean) => {
                    action(res)
                    if (res) refresh()
                  }, dirPath + '/' + it))}><CreateNewFolder /></IconButton></Tooltip>
              <Tooltip title={lang.more}>
                <IconButton size='small' onClick={e => setAnchorEl(anchorEl ? null : e.currentTarget)}><MoreHoriz /></IconButton>
              </Tooltip>
            </Box>} />
            <Divider />
            <TreeView
              ref={tree}
              defaultCollapseIcon={<ArrowDropDown />}
              defaultExpandIcon={<ArrowRight />}
              sx={{ flexGrow: 1, width: '100%', overflowY: 'auto' }}
              expanded={expanded}
              onNodeToggle={(_: any, it: string[]) => {
                const l = loading.current
                if (it.length < prevExpanded.current.length || !l[it[0]]) {
                  setExpanded(it)
                  prevExpanded.current = it
                  return
                }
                l[it[0]]().then(() => {
                  prevExpanded.current.unshift(it[0])
                  setExpanded([...prevExpanded.current])
                  delete l[it[0]]
                })
                delete l[it[0]]
              }}
              onNodeSelect={(_: any, it: string) => {
                setCurPath(it[0] === '/' ? it.slice(1) : it)
                if (dirs.current[it] || loading.current['!#LOADING']) return
                if (it.startsWith('/')) it = it.slice(1)
                his.push('/NekoMaid/files/' + it)
              }}
            >
              <Item plugin={plugin} path='' loading={loading.current} dirs={dirs.current} key={id} />
            </TreeView>
          </Card>
        </Grid>
        <Grid item lg={8} md={12} xl={9} xs={12} sx={{ maxWidth: `calc(100vw - ${theme.spacing(1)})`, paddingBottom: 3 }}>
          <Editor plugin={plugin} editorRef={editor} loading={loading.current} dirs={dirs.current} refresh={refresh} />
        </Grid>
      </Grid>
    </Container>
    <Menu
      anchorEl={anchorEl}
      open={Boolean(anchorEl)}
      onClose={() => setAnchorEl(null)}
      anchorOrigin={anchorOrigin}
      transformOrigin={anchorOrigin}
    >
      <MenuItem onClick={() => {
        refresh()
        setAnchorEl(null)
      }}><ListItemIcon><Refresh /></ListItemIcon>{lang.refresh}</MenuItem>
      <MenuItem disabled={!curPath} onClick={() => {
        setAnchorEl(null)
        fileNameDialog(lang.files.rename, curPath).then(it => it != null && plugin.emit('files:rename', (res: boolean) => {
          action(res)
          if (res) refresh()
        }, curPath, dirPath + '/' + it))
      }}><ListItemIcon><DriveFileRenameOutline /></ListItemIcon>{lang.files.rename}</MenuItem>
      <MenuItem disabled={!curPath} onClick={() => {
        setAnchorEl(null)
        setCopyPath(curPath)
      }}>
        <ListItemIcon><FileCopy /></ListItemIcon>{lang.files.copy}
      </MenuItem>
      <MenuItem disabled={!copyPath} onClick={() => {
        setAnchorEl(null)
        toast(lang.files.pasting)
        plugin.emit('files:copy', (res: boolean) => {
          action(res)
          refresh()
        }, copyPath, dirPath)
      }}>
        <ListItemIcon><ContentPaste /></ListItemIcon>{lang.files.paste}
      </MenuItem>
      <MenuItem disabled={progress !== -1} component='label' htmlFor='NekoMaid-files-upload-input' onClick={() => setAnchorEl(null)}>
        <ListItemIcon><Upload /></ListItemIcon>{progress === -1 ? lang.files.upload : `${lang.files.uploading} (${progress.toFixed(2)}%)`}
      </MenuItem>
      <MenuItem disabled={isDir} onClick={() => {
        setAnchorEl(null)
        toast(lang.files.downloading)
        plugin.emit('files:download', (res: ArrayBuffer | null) => {
          if (res) window.open(address! + 'Download/' + res, '_blank')
          else failed()
        }, curPath)
      }}><ListItemIcon><Download /></ListItemIcon>{lang.files.download}</MenuItem>
      <MenuItem onClick={() => {
        setAnchorEl(null)
        setCompressFile(curPath)
      }}><ListItemIcon><Inbox /></ListItemIcon>{lang.files.compress}</MenuItem>
      <MenuItem onClick={() => {
        setAnchorEl(null)
        toast(lang.files.uncompressing)
        plugin.emit('files:compress', (res: boolean) => {
          action(res)
          refresh()
        }, curPath)
      }}><ListItemIcon><Outbox /></ListItemIcon>{lang.files.decompress}</MenuItem>
    </Menu>
    <Input id='NekoMaid-files-upload-input' type='file' sx={{ display: 'none' }} onChange={e => {
      const elm = e.target as HTMLInputElement
      const file = elm.files?.[0]
      elm.value = ''
      if (!file) return
      const size = file.size
      if (size > 128 * 1024 * 1024) return failed(lang.files.uploadTooBig)
      toast(lang.files.uploading)
      const name = dirPath + '/' + file.name
      if (dirs.current[name] != null) return failed(lang.files.exists)
      plugin.emit('files:upload', (res: string | null) => {
        if (!res) return failed(lang.files.exists)
        const formdata = new FormData()
        formdata.append('file', file)
        const xhr = new XMLHttpRequest()
        setProgress(0)
        xhr.open('put', address! + 'Upload/' + res)
        xhr.onreadystatechange = () => {
          if (xhr.readyState !== 4) return
          setProgress(-1)
          action(xhr.status === 200)
          refresh()
        }
        xhr.upload.onprogress = e => e.lengthComputable && setProgress(e.loaded / e.total * 100)
        xhr.send(formdata)
      }, name[0] === '/' ? name.slice(1) : name)
    }} />
    <CompressDialog file={compressFile} path={dirPath} dirs={dirs.current} onClose={() => setCompressFile(null)} refresh={refresh} plugin={plugin} />
  </Box>
}
Example #4
Source File: Plugins.tsx    From NekoMaid with MIT License 4 votes vote down vote up
Plugins: React.FC = () => {
  const plugin = usePlugin()
  const theme = useTheme()
  const { canLoadPlugin } = useGlobalData()
  const [plugins, setPlugins] = useState<Plugin[]>([])
  useEffect(() => {
    const offList = plugin.on('plugins:list', (plugins: Plugin[]) => {
      const arr: Plugin[] = []
      setPlugins(plugins.filter(it => {
        const res = canPluginBeDisabled(it.name)
        if (res) arr.push(it)
        return !res
      }).concat(arr))
    })
    plugin.emit('plugins:fetch')
    return () => {
      offList()
    }
  }, [])

  const map: Record<string, number> = { }
  let id = 0
  const data = plugins.map(it => {
    map[it.name] = id
    return { id: id++, name: it.name, category: 1 - (it.enabled as any) }
  })
  const links: Array<{ source: number, target: number }> = []
  plugins.forEach(it => {
    const source = map[it.name]
    it.depends.forEach(dep => {
      if (!(dep in map)) {
        map[dep] = id
        data.push({ id: id++, name: dep, category: 3 })
      }
      links.push({ source, target: map[dep] })
    })
    it.softDepends.forEach(dep => {
      if (!(dep in map)) {
        map[dep] = id
        data.push({ id: id++, name: dep, category: 2 })
      }
      links.push({ source, target: map[dep] })
    })
  })
  return <Box sx={{ minHeight: '100%', py: 3 }}>
    <Toolbar />
    <Container maxWidth={false}>
      <Grid container spacing={3}>
        <Grid item xs={12}>
          <Card>
            <CardHeader title={lang.plugins.title} />
            <Divider />
            <TableContainer>
              <Table>
                <TableHead>
                  <TableRow>
                    <TableCell sx={{ paddingRight: 0 }}>{lang.plugins.enable}</TableCell>
                    <TableCell>{lang.plugins.name}</TableCell>
                    <TableCell>{lang.plugins.version}</TableCell>
                    <TableCell>{lang.plugins.author}</TableCell>
                    <TableCell>{lang.plugins.description}</TableCell>
                    <TableCell align='right'>{lang.operations}</TableCell>
                  </TableRow>
                </TableHead>
                <TableBody>
                  {plugins.map(it => {
                    const canBeDisabled = canPluginBeDisabled(it.name)
                    const disabledForever = it.file.endsWith('.disabled')
                    return <TableRow key={it.name}>
                      <TableCell padding='checkbox'>
                        <Checkbox
                          color='primary'
                          checked={it.enabled}
                          disabled={disabledForever || canBeDisabled}
                          onChange={() => plugin.emit('plugins:enable', it.file, it.name, action)
                        } />
                      </TableCell>
                      <TableCell><Tooltip title={it.file}><span>{it.name}</span></Tooltip></TableCell>
                      <TableCell>{it.website
                        ? <Link underline='hover' rel='noopener' target='_blank' href={it.website}>{it.version}</Link>
                        : it.version
                      }</TableCell>
                      <TableCell>{it.author}</TableCell>
                      <TableCell>{it.description}</TableCell>
                      <TableCell align='right' sx={{ whiteSpace: 'nowrap' }}>
                        <Tooltip title={lang.plugins[disabledForever ? 'enablePlugin' : 'disableForever']}><span>
                          <IconButton
                            disabled={it.enabled || (it.loaded && !canLoadPlugin)}
                            onClick={() => plugin.emit('plugins:disableForever', it.file, action)}
                          >{disabledForever ? <LockOpen /> : <Lock />}</IconButton>
                        </span></Tooltip>
                        {disabledForever && <Tooltip title={lang.plugins.delete}><span>
                            <IconButton
                              color='error'
                              disabled={canBeDisabled}
                              onClick={() => dialog({
                                okButton: { color: 'error' },
                                content: <>{lang.plugins.confirmDelete(<span className='bold'>{it.file.replace(/\.disabled$/, '')}</span>)}&nbsp;
                                  <span className='bold' style={{ color: theme.palette.error.main }}>({lang.unrecoverable})</span></>
                              }).then(res => res && plugin.emit('plugins:delete', it.file, action))}
                            ><DeleteForever /></IconButton>
                          </span></Tooltip>}
                      </TableCell>
                    </TableRow>
                  })}
                </TableBody>
              </Table>
            </TableContainer>
          </Card>
        </Grid>
        <Grid item xs={12}>
          <Card>
            <CardHeader title={lang.plugins.dependency} />
            <Divider />
            <ReactECharts style={{ marginTop: theme.spacing(1), height: 450 }} theme={theme.palette.mode === 'dark' ? 'dark' : undefined} option={{
              backgroundColor: 'rgba(0, 0, 0, 0)',
              legend: { data: lang.plugins.categories },
              series: [
                {
                  edgeSymbol: ['none', 'arrow'],
                  symbolSize: 13,
                  type: 'graph',
                  layout: 'force',
                  data,
                  links,
                  categories: lang.plugins.categories.map(name => ({ name, base: name })),
                  roam: true,
                  label: {
                    show: true,
                    position: 'right',
                    formatter: '{b}'
                  },
                  labelLayout: {
                    hideOverlap: true
                  }
                }
              ]
            }} />
          </Card>
        </Grid>
      </Grid>
    </Container>
  </Box>
}