import React, { useEffect, useMemo, useRef, useState } from 'react'; import { makeStyles, TableCell, TableRow, Chip, Typography, Box, useTheme, ButtonBase, } from '@material-ui/core'; import { useRecoilValue } from 'recoil'; import { ExpandMore, ExpandLess } from '@material-ui/icons'; import { modsText } from '../../helpers/static-text'; import { isOutdated, isInstalled, isBroken } from '../../services'; import ModActions from './ModActions'; import { missingDependencyIdsState, addonModList, enabledModList, isFiltering, modIsLoadingState, } from '../../store'; type Props = { mod: Mod; }; const useStyles = makeStyles((theme) => ({ brokenRow: { '&:nth-of-type(odd)': { backgroundColor: theme.palette.error.dark, }, backgroundColor: theme.palette.error.dark, modText: { fontWeight: theme.typography.fontWeightBold, color: theme.palette.secondary.light, }, }, missingDependencyRow: { '&:nth-of-type(odd)': { backgroundColor: theme.palette.secondary.dark, }, backgroundColor: theme.palette.secondary.dark, }, modAuthor: { color: theme.palette.text.disabled, display: 'inline-block', }, tableCell: { paddingTop: theme.spacing(1), paddingBottom: theme.spacing(1), }, tableRow: { '&:nth-of-type(odd)': { opacity: '#4b4b4b', }, transition: 'background-color 0.2s', }, loading: { backgroundColor: theme.palette.grey[600], }, addonRow: { backgroundColor: theme.palette.grey[900], }, versionChip: { width: '100%', padding: 0, paddingLeft: 0, paddingRight: 0, '& span': { paddingLeft: 0, paddingRight: 0, }, }, modText: { display: 'block', marginTop: -5, marginBottom: -theme.spacing(0), wordWrap: 'break-word', }, outdatedChip: { ...theme.typography.caption, textAlign: 'center', width: '100%', lineHeight: 0, paddingTop: theme.spacing(4), paddingBottom: theme.spacing(2), marginTop: -theme.spacing(4), borderRadius: 16, background: theme.palette.secondary.main, fontWeight: theme.typography.fontWeightBold, }, addonExpander: { width: '100%', justifyContent: 'start', borderRadius: theme.shape.borderRadius, backgroundColor: theme.palette.background.default, transition: `${theme.transitions.duration.shortest}ms`, '&:hover': { backgroundColor: theme.palette.action.hover, }, }, })); const ModTableRow: React.FunctionComponent<Props> = ({ mod }) => { const styles = useStyles(); const theme = useTheme(); const missingDependencyNames = useRecoilValue(missingDependencyIdsState(mod)); const isModOutdated = isOutdated(mod); const isModBroken = isBroken(mod); const addonMods = useRecoilValue(addonModList); const [isAddonsExpanded, setIsAddonsExpanded] = useState(false); const isAddon = mod.parent && !mod.localVersion; const enabledMods = useRecoilValue(enabledModList); const forceExpandAddons = useRecoilValue(isFiltering); const shouldExpandAddons = forceExpandAddons || isAddonsExpanded; const rowRef = useRef<HTMLTableRowElement>(null); const isLoading = useRecoilValue(modIsLoadingState(mod.uniqueName)); useEffect(() => { if (!isLoading || !rowRef.current) return; rowRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest', }); }, [isLoading]); const addons = useMemo( () => addonMods.filter((addon) => addon.parent === mod.uniqueName), [addonMods, mod.uniqueName] ); const conflictingMods = useMemo( () => mod.conflicts && mod.conflicts.length > 0 ? enabledMods .filter((enabledMod) => mod.conflicts?.includes(enabledMod.uniqueName) ) .map((enabledMod) => enabledMod.name) : [], [enabledMods, mod.conflicts] ); const isModConflicting = mod.isEnabled && conflictingMods.length > 0; const handleExpandClick = () => setIsAddonsExpanded((isExpanded) => !isExpanded); const getVersionColor = () => { if (isModBroken) { return 'default'; } if (isModOutdated) { return 'secondary'; } if (isInstalled(mod)) { return 'primary'; } return 'default'; }; const getVersion = () => { if (isInstalled(mod)) { return mod.localVersion; } if (mod.remoteVersion) { return mod.remoteVersion; } return modsText.versionNotAvailable; }; const getClassName = () => { let className = styles.tableRow; if (isModBroken || isModConflicting) { className += ` ${styles.brokenRow}`; } else if (isLoading) { className += ` ${styles.loading}`; } else if (missingDependencyNames.length > 0) { className += ` ${styles.missingDependencyRow}`; } else if (isAddon) { className += ` ${styles.addonRow}`; } return className; }; const getModText = () => { if (isModBroken) { return modsText.modLoadError(mod.errors); } if (missingDependencyNames.length > 0) { return modsText.missingDependencyWarning( missingDependencyNames.join(', ') ); } if (isModConflicting) { return modsText.conflictingModWarning(conflictingMods.join(', ')); } return mod.description; }; return ( <> <TableRow className={getClassName()} key={mod.uniqueName} ref={rowRef}> <TableCell className={styles.tableCell}> <Box display="flex"> {isAddon && ( <Box bgcolor={theme.palette.background.paper} width="8px" minWidth="8px" marginRight={2} marginLeft={1} borderRadius="8px" /> )} <Box width="100%"> <Typography variant="subtitle1"> <Box display="inline-block" mr={2}> {mod.name} </Box> <Typography className={styles.modAuthor} variant="caption"> {' by '} {mod.author} </Typography> <Typography variant="caption" /> </Typography> <Box color={ isModBroken || isModConflicting ? theme.palette.secondary.light : theme.palette.text.secondary } > <Typography className={styles.modText} variant="caption"> {getModText()} </Typography> </Box> {addons.length > 0 && !forceExpandAddons && ( <ButtonBase className={styles.addonExpander} onClick={handleExpandClick} > <Box display="flex" alignItems="center" borderRadius={theme.shape.borderRadius} maxWidth="100%" > {shouldExpandAddons ? <ExpandLess /> : <ExpandMore />} <Typography variant="caption" noWrap> {addons.length} {' addons available: '} {addons.map((addon) => addon.name).join(', ')} </Typography> </Box> </ButtonBase> )} </Box> </Box> </TableCell> <TableCell className={styles.tableCell} align="right"> {mod.downloadCount || '-'} </TableCell> <TableCell className={styles.tableCell}> <Chip color={getVersionColor()} label={getVersion()} className={styles.versionChip} /> {!isModBroken && isModOutdated && ( <div className={styles.outdatedChip}>{modsText.outdated}</div> )} </TableCell> <TableCell className={styles.tableCell}> <ModActions mod={mod} /> </TableCell> </TableRow> {shouldExpandAddons && addons.map((addon) => ( <ModTableRow key={addon.uniqueName} mod={addon} /> ))} </> ); }; export default ModTableRow;