@mui/material#ButtonGroup TypeScript Examples

The following examples show how to use @mui/material#ButtonGroup. 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: SortByButton.tsx    From genshin-optimizer with MIT License 6 votes vote down vote up
// Assumes that all the sortKeys has corresponding translations in ui.json sortMap
export default function SortByButton({ sortKeys, value, onChange, ascending, onChangeAsc, ...props }: SortByButtonProps) {
  const { t } = useTranslation("ui")
  return <Box display="flex" alignItems="center" gap={1}>
    <Trans t={t} i18nKey={t("sortBy") as any}>Sort by: </Trans>
    <ButtonGroup {...props} >
      <DropdownButton title={<Trans t={t} i18nKey={t(`sortMap.${value}`) as any}>{{ value: t(`sortMap.${value}`) }}</Trans>}>
        {sortKeys.map(key =>
          <MenuItem key={key} selected={value === key} disabled={value === key} onClick={() => onChange(key)}>{t(`sortMap.${key}`) as any}</MenuItem>)}
      </DropdownButton>
      <Button onClick={() => onChangeAsc(!ascending)} startIcon={<FontAwesomeIcon icon={ascending ? faSortAmountDownAlt : faSortAmountUp} className="fa-fw" />}>
        {ascending ? <Trans t={t} i18nKey="ascending" >Ascending</Trans> : <Trans t={t} i18nKey="descending" >Descending</Trans>}
      </Button>
    </ButtonGroup>
  </Box>
}
Example #2
Source File: StatFilterCard.tsx    From genshin-optimizer with MIT License 6 votes vote down vote up
export function StatFilterItem({ statKey, statKeys = [], value = 0, close, setFilter, disabled = false }: {
  statKey?: string, statKeys: string[], value?: number, close?: () => void, setFilter: (statKey: string, value?: number) => void, disabled?: boolean
}) {
  const isFloat = KeyMap.unit(statKey) === "%"
  const onChange = useCallback(s => statKey && setFilter(statKey, s), [setFilter, statKey])
  return <ButtonGroup sx={{ width: "100%" }}>
    <DropdownButton
      title={statKey ? KeyMap.get(statKey) : "New Stat"}
      disabled={disabled}
      color={statKey ? "success" : "secondary"}
    >
      {statKeys.map(sKey => <MenuItem key={sKey} onClick={() => { close?.(); setFilter(sKey, value) }}>{KeyMap.get(sKey)}</MenuItem>)}
    </DropdownButton>
    <CustomNumberInputButtonGroupWrapper sx={{ flexBasis: 30, flexGrow: 1 }}>
      <CustomNumberInput
        disabled={!statKey}
        float={isFloat}
        value={value}
        placeholder="Min Value"
        onChange={onChange}
        sx={{ px: 2 }}
      />
    </CustomNumberInputButtonGroupWrapper>
    {!!close && <Button color="error" onClick={close} disabled={disabled}><FontAwesomeIcon icon={faTrashAlt} /></Button>}
  </ButtonGroup>
}
Example #3
Source File: LocaleSwitcher.tsx    From frontend with MIT License 6 votes vote down vote up
export default function LocaleSwitcher() {
  const { t } = useTranslation()
  const router = useRouter()
  const changeLang = useCallback(
    // Same route different language
    (locale: string) => () => router.push(router.route, router.asPath, { locale }),
    [],
  )
  return (
    <Box textAlign="center">
      <ButtonGroup disableRipple variant="outlined" aria-label="text primary button group">
        <Button onClick={changeLang('bg')}>{t('BG')}</Button>
        <Button onClick={changeLang('en')}>{t('EN')}</Button>
      </ButtonGroup>
    </Box>
  )
}
Example #4
Source File: EXPCalc.tsx    From genshin-optimizer with MIT License 6 votes vote down vote up
function BookDisplay(props) {
  let { bookKey, value = 0, setValue, required = 0 } = props
  return <CardLight>
    <CardContent sx={{ py: 1 }}>
      <Typography>{booksData[bookKey].name}</Typography>
    </CardContent>
    <Divider />
    <CardContent>
      <Grid container>
        <Grid item xs={3}><ImgFullwidth src={booksData[bookKey].img} /></Grid>
        <Grid item xs={9}>
          <ButtonGroup sx={{ display: "flex" }}>
            <TextButton>Amount</TextButton>
            <CustomNumberInputButtonGroupWrapper sx={{ flexBasis: 30, flexGrow: 1 }}>
              <CustomNumberInput
                value={value}
                onChange={(val) => setValue(Math.max(val ?? 0, 0))}
                sx={{ px: 2 }}
              />
            </CustomNumberInputButtonGroupWrapper>
          </ButtonGroup>
          <Box display="flex" justifyContent="space-between" mt={1}>
            <Typography>Required:</Typography>
            <Typography><b ><ColorText color={required ? "success" : ""}>{required}</ColorText></b></Typography>
          </Box>
        </Grid>
      </Grid>
    </CardContent>
  </CardLight >
}
Example #5
Source File: ConditionalSelector.tsx    From genshin-optimizer with MIT License 6 votes vote down vote up
function MultipleConditionalSelector({ conditional, disabled }: MultipleConditionalSelectorProps) {
  const { character, characterDispatch, data } = useContext(DataContext)
  const setConditional = useCallback((path: readonly string[], v?: string) => {
    const conditionalValues = deepClone(character.conditional)
    if (v) {
      layeredAssignment(conditionalValues, path, v)
    } else {
      deletePropPath(conditionalValues, path)
    }
    characterDispatch({ conditional: conditionalValues })
  }, [character, characterDispatch])

  return <ButtonGroup fullWidth orientation="vertical" disableElevation color="secondary" >
    {Object.entries(conditional.states).map(([stateKey, st]) => {
      const conditionalValue = data.get(st.value).value
      const isSelected = conditionalValue === stateKey
      return <Button
        color={isSelected ? "success" : "primary"}
        disabled={disabled}
        fullWidth
        key={stateKey}
        onClick={() => setConditional(st.path, conditionalValue ? undefined : stateKey)}
        size="small"
        startIcon={isSelected ? <CheckBox /> : <CheckBoxOutlineBlank />}
        sx={{ borderRadius: 0 }}
      >
        {getCondName(st.name)}
      </Button>
    })}
  </ButtonGroup>
}
Example #6
Source File: UseEquipped.tsx    From genshin-optimizer with MIT License 5 votes vote down vote up
function SelectItem({ characterKey, rank, maxRank, setRank, onRemove, numAbove }: {
  characterKey: CharacterKey,
  rank: number,
  maxRank: number,
  setRank: (r: number | undefined) => void,
  onRemove: () => void,
  numAbove: number,
}) {
  const { t } = useTranslation("page_character")
  const { database } = useContext(DatabaseContext)
  const character = useCharacter(characterKey)
  if (!character) return null
  const { equippedWeapon, equippedArtifacts } = character
  return <CardLight sx={{ p: 1 }}  >
    <Box sx={{ pb: 1, display: "flex", justifyContent: "space-between", gap: 1 }}>
      <SqBadge color="info">
        <Typography>#{rank}</Typography>
      </SqBadge>
      <SqBadge sx={{ flexGrow: 1 }} color={numAbove === (rank - 1) ? "warning" : (rank - 1) < numAbove ? "error" : "success"}>
        <Typography>{numAbove === (rank - 1) ? <Trans t={t} i18nKey="tabOptimize.useEquipped.modal.status.curr">Current character</Trans>
          : (rank - 1) < numAbove ? <Trans t={t} i18nKey="tabOptimize.useEquipped.modal.status.dont">Don't Use artifacts</Trans> :
            <Trans t={t} i18nKey="tabOptimize.useEquipped.modal.status.use">Use artifacts</Trans>}</Typography>
      </SqBadge>
      <Box>
        <ButtonGroup sx={{ flexGrow: 1 }} size="small">
          <CustomNumberInputButtonGroupWrapper >
            <CustomNumberInput onChange={setRank} value={rank}
              // startAdornment="Rank:"
              inputProps={{ min: 1, max: maxRank, sx: { textAlign: "center" } }}
              sx={{ width: "100%", height: "100%", pl: 2 }} />
          </CustomNumberInputButtonGroupWrapper>
          <Button disabled={rank === 1} onClick={() => setRank(1)} >
            <KeyboardDoubleArrowUp />
          </Button>
          <Button disabled={rank === 1} onClick={() => setRank(rank - 1)}  >
            <KeyboardArrowUp />
          </Button>
          <Button disabled={rank === maxRank} onClick={() => setRank(rank + 1)}  >
            <KeyboardArrowDown />
          </Button>
          <Button disabled={rank === maxRank} onClick={() => setRank(maxRank)}  >
            <KeyboardDoubleArrowDown />
          </Button>
          <Button color="error" onClick={onRemove}>
            <Close />
          </Button>
        </ButtonGroup>
      </Box>
    </Box>
    <Grid container columns={7} spacing={1}>
      <Grid item xs={1} >
        <CharacterCardPico characterKey={characterKey} />
      </Grid>
      <Grid item xs={1}><WeaponCardPico weaponId={equippedWeapon} /></Grid>
      {Object.entries(equippedArtifacts).map(([slotKey, aId]) => <Grid item xs={1} key={slotKey} ><ArtifactCardPico slotKey={slotKey} artifactObj={database._getArt(aId)} /></Grid>)}
    </Grid>

  </CardLight>
}
Example #7
Source File: index.tsx    From genshin-optimizer with MIT License 5 votes vote down vote up
function ResinCounter() {
  const [{ resin, date }, setResinState] = useDBState("ToolsDisplayResin", initToolsDisplayResin)
  const resinIncrement = useRef(undefined as undefined | NodeJS.Timeout)

  const setResin = (newResin: number) => {
    if (newResin >= RESIN_MAX) {
      resinIncrement.current && clearTimeout(resinIncrement.current)
      resinIncrement.current = undefined
    } else
      resinIncrement.current = setTimeout(() => console.log("set resin", newResin + 1), RESIN_RECH_MS);
    setResinState({ resin: newResin, date: new Date().getTime() })
  }

  useEffect(() => {
    if (resin < RESIN_MAX) {
      const now = Date.now()
      const resinToMax = RESIN_MAX - resin
      const resinSinceLastDate = Math.min(Math.floor((now - date) / (RESIN_RECH_MS)), resinToMax)
      const catchUpResin = resin + resinSinceLastDate
      const newDate = date + resinSinceLastDate * RESIN_RECH_MS
      setResinState({ resin: catchUpResin, date: newDate })
      if (catchUpResin < RESIN_MAX)
        resinIncrement.current = setTimeout(() => setResin(catchUpResin + 1), now - newDate);
    }
    return () => resinIncrement.current && clearTimeout(resinIncrement.current)
    // eslint-disable-next-line
  }, [])

  const nextResinDateNum = resin >= RESIN_MAX ? date : date + RESIN_RECH_MS;

  const resinFullDateNum = resin >= RESIN_MAX ? date : (date + (RESIN_MAX - resin) * RESIN_RECH_MS)
  const resinFullDate = new Date(resinFullDateNum)

  const nextDeltaString = timeString(Math.abs(nextResinDateNum - Date.now()))

  return <CardDark>
    <Grid container sx={{ px: 2, py: 1 }} spacing={2} >
      <Grid item>
        <ImgIcon src={Assets.resin.fragile} sx={{ fontSize: "2em" }} />
      </Grid>
      <Grid item >
        <Typography variant="h6">Resin Counter</Typography>
      </Grid>
    </Grid>
    <Divider />
    <CardContent>
      <Grid container spacing={2}>
        <Grid item>
          <Typography variant="h2">
            <ImgIcon src={Assets.resin.fragile} />
            <InputBase type="number" sx={{ width: "2em", fontSize: "4rem" }} value={resin} inputProps={{ min: 0, max: 999, sx: { textAlign: "right" } }} onChange={(e => setResin(parseInt(e.target.value)))} />
            <span>/{RESIN_MAX}</span>
          </Typography>
        </Grid>
        <Grid item flexGrow={1}>
          <ButtonGroup fullWidth >
            <Button onClick={() => setResin(0)} disabled={resin === 0}>0</Button>
            <Button onClick={() => setResin(resin - 1)} disabled={resin === 0}>-1</Button>
            <Button onClick={() => setResin(resin - 20)} disabled={resin < 20}>-20</Button>
            <Button onClick={() => setResin(resin - 40)} disabled={resin < 40}>-40</Button>
            <Button onClick={() => setResin(resin - 60)} disabled={resin < 60}>-60</Button>
            <Button onClick={() => setResin(resin + 1)}>+1</Button>
            <Button onClick={() => setResin(resin + 60)}>+60</Button>
            <Button onClick={() => setResin(RESIN_MAX)} disabled={resin === RESIN_MAX}>MAX {RESIN_MAX}</Button>
          </ButtonGroup>
          <Typography variant="subtitle1" sx={{ mt: 2 }}>
            {resin < RESIN_MAX ? <span>Next resin in {nextDeltaString}, full Resin at {resinFullDate.toLocaleTimeString()} {resinFullDate.toLocaleDateString()}</span> :
              <span>Resin has been full for at least {nextDeltaString}, since {resinFullDate.toLocaleTimeString()} {resinFullDate.toLocaleDateString()}</span>}
          </Typography>
        </Grid>
        <Grid item xs={12}>
          <Typography variant="caption">Because we do not provide a mechanism to synchronize resin time, actual resin recharge time might be as much as 8 minutes earlier than predicted.</Typography>
        </Grid>
      </Grid>
    </CardContent>
  </CardDark>
}
Example #8
Source File: index.tsx    From genshin-optimizer with MIT License 5 votes vote down vote up
function CharSelectDropdown() {
  const { t } = useTranslation("page_character")
  const { character, characterSheet, characterDispatch } = useContext(DataContext)
  const [showModal, setshowModal] = useState(false)
  const setCharacter = useCharSelectionCallback()
  const setLevel = useCallback((level) => {
    level = clamp(level, 1, 90)
    const ascension = ascensionMaxLevel.findIndex(ascenML => level <= ascenML)
    characterDispatch({ level, ascension })
  }, [characterDispatch])
  const setAscension = useCallback(() => {
    if (!character) return
    const { level = 1, ascension = 0 } = character
    const lowerAscension = ascensionMaxLevel.findIndex(ascenML => level !== 90 && level === ascenML)
    if (ascension === lowerAscension) characterDispatch({ ascension: ascension + 1 })
    else characterDispatch({ ascension: lowerAscension })
  }, [characterDispatch, character])
  const { elementKey = "anemo", level = 1, ascension = 0 } = character
  return <>
    <CharacterSelectionModal show={showModal} onHide={() => setshowModal(false)} onSelect={setCharacter} />
    <Grid container spacing={1}>
      <Grid item>
        <Button color="info" onClick={() => setshowModal(true)} startIcon={<ThumbSide src={characterSheet?.thumbImgSide} />} >{characterSheet?.name ?? t("selectCharacter")}</Button>
      </Grid>
      <Grid item>
        <ButtonGroup sx={{ bgcolor: t => t.palette.contentDark.main }} >
          {characterSheet?.sheet && "talents" in characterSheet?.sheet && <DropdownButton title={<strong><ColorText color={elementKey}>{sgt(`element.${elementKey}`)}</ColorText></strong>}>
            {Object.keys(characterSheet.sheet.talents).map(eleKey =>
              <MenuItem key={eleKey} selected={elementKey === eleKey} disabled={elementKey === eleKey} onClick={() => characterDispatch({ elementKey: eleKey })}>
                <strong><ColorText color={eleKey}>{sgt(`element.${eleKey}`)}</ColorText></strong></MenuItem>)}
          </DropdownButton>}
          <CustomNumberInputButtonGroupWrapper >
            <CustomNumberInput onChange={setLevel} value={level}
              startAdornment="Lv. "
              inputProps={{ min: 1, max: 90, sx: { textAlign: "center" } }}
              sx={{ width: "100%", height: "100%", pl: 2 }}
              disabled={!characterSheet} />
          </CustomNumberInputButtonGroupWrapper>
          <Button sx={{ pl: 1 }} disabled={!ambiguousLevel(level) || !characterSheet} onClick={setAscension}><strong>/ {ascensionMaxLevel[ascension]}</strong></Button>
          <DropdownButton title={t("selectLevel")} disabled={!characterSheet}>
            {milestoneLevels.map(([lv, as]) => {
              const sameLevel = lv === ascensionMaxLevel[as]
              const lvlstr = sameLevel ? `Lv. ${lv}` : `Lv. ${lv}/${ascensionMaxLevel[as]}`
              const selected = lv === level && as === ascension
              return <MenuItem key={`${lv}/${as}`} selected={selected} disabled={selected} onClick={() => characterDispatch({ level: lv, ascension: as })}>{lvlstr}</MenuItem>
            })}
          </DropdownButton>
        </ButtonGroup>
      </Grid>
    </Grid>
  </>
}
Example #9
Source File: UseEquipped.tsx    From genshin-optimizer with MIT License 5 votes vote down vote up
export default function UseEquipped({ useEquippedArts, buildSettingsDispatch, disabled }) {
  const { t } = useTranslation("page_character")
  const { character: { key: characterKey } } = useContext(DataContext)
  const { database } = useContext(DatabaseContext)
  const [show, onOpen, onClose] = useBoolState(false)
  const [{ equipmentPriority: tempEquipmentPriority }, setOptimizeDBState] = useOptimizeDBState()
  //Basic validate for the equipmentPrio list to remove dups and characters that doesnt exist.
  const equipmentPriority = useMemo(() => [...new Set(tempEquipmentPriority)].filter(ck => database._getChar(ck)), [database, tempEquipmentPriority])
  const setPrio = useCallback((equipmentPriority: CharacterKey[]) => setOptimizeDBState({ equipmentPriority }), [setOptimizeDBState])

  const setPrioRank = useCallback((fromIndex, toIndex) => {
    const arr = [...equipmentPriority]
    var element = arr[fromIndex];
    arr.splice(fromIndex, 1);
    arr.splice(toIndex, 0, element);
    setPrio(arr)
  }, [equipmentPriority, setPrio])
  const removePrio = useCallback((fromIndex) => {
    const arr = [...equipmentPriority]
    arr.splice(fromIndex, 1)
    setPrio(arr)
  }, [equipmentPriority, setPrio])
  const addPrio = useCallback((ck: CharacterKey) => setPrio([...equipmentPriority, ck]), [equipmentPriority, setPrio])
  const resetPrio = useCallback(() => setPrio([]), [setPrio])

  const numAbove = useMemo(() => {
    let numAbove = equipmentPriority.length
    const index = equipmentPriority.indexOf(characterKey)
    if (index >= 0) numAbove = index
    return numAbove
  }, [characterKey, equipmentPriority])
  const numUseEquippedChar = useMemo(() => {
    return database._getCharKeys().length - 1 - numAbove
  }, [numAbove, database])
  const numUnlisted = useMemo(() => {
    return database._getCharKeys().length - equipmentPriority.length
  }, [equipmentPriority, database])

  return <Box display="flex" gap={1}>
    <ModalWrapper open={show} onClose={onClose} containerProps={{ maxWidth: "sm" }}><CardDark>
      <CardContent>
        <Grid container spacing={1}>
          <Grid item flexGrow={1}>
            <Typography variant="h6"><Trans t={t} i18nKey="tabOptimize.useEquipped.modal.title">Character Priority for Equipped Artifacts</Trans></Typography>
          </Grid>
          <Grid item sx={{ mb: -1 }}>
            <CloseButton onClick={onClose} />
          </Grid>
        </Grid>
      </CardContent>
      <Divider />
      <CardContent>
        <CardLight sx={{ mb: 1 }}>
          <CardContent>
            <Typography gutterBottom><Trans t={t} i18nKey="tabOptimize.useEquipped.modal.desc1">When generating a build, the Optimizer will only consider equipped artifacts from characters below the current character or those not on the list.</Trans></Typography>
            <Typography gutterBottom><Trans t={t} i18nKey="tabOptimize.useEquipped.modal.desc2">If the current character is not on the list, the Optimizer will only consider equipped artifacts from others characters that are not on the list.</Trans></Typography>
          </CardContent>
        </CardLight>
        <Box display="flex" flexDirection="column" gap={2}>
          {equipmentPriority.map((ck, i) =>
            <SelectItem key={ck} characterKey={ck} rank={i + 1} maxRank={equipmentPriority.length} setRank={(num) => num && setPrioRank(i, num - 1)} onRemove={() => removePrio(i)} numAbove={numAbove} />)}
          <Box sx={{ display: "flex", gap: 1 }}>
            <NewItem onAdd={addPrio} list={equipmentPriority} />
            <Button color="error" onClick={resetPrio} startIcon={<Replay />}><Trans t={t} i18nKey="tabOptimize.useEquipped.modal.clearList">Clear List</Trans></Button>
          </Box>
          {!!numUseEquippedChar && <SqBadge color="success"><Typography><Trans t={t} i18nKey="tabOptimize.useEquipped.modal.usingNum" count={numUnlisted}>Using artifacts from <strong>{{ count: numUnlisted }}</strong> unlisted characters</Trans></Typography></SqBadge>}
        </Box>
      </CardContent>
    </CardDark ></ModalWrapper>
    <ButtonGroup sx={{ display: "flex", width: "100%" }}>
      <Button sx={{ flexGrow: 1 }} onClick={() => buildSettingsDispatch({ useEquippedArts: !useEquippedArts })} disabled={disabled} startIcon={useEquippedArts ? <CheckBox /> : <CheckBoxOutlineBlank />} color={useEquippedArts ? "success" : "secondary"}>
        <Box>
          <span><Trans t={t} i18nKey="tabOptimize.useEquipped.title">Use Equipped Artifacts</Trans></span>
          {useEquippedArts && <SqBadge><Trans t={t} i18nKey="tabOptimize.useEquipped.usingNum" count={numUseEquippedChar}>Using from <strong>{{ count: numUseEquippedChar }}</strong> characters</Trans></SqBadge>}
        </Box>
      </Button>
      {useEquippedArts && <Button sx={{ flexShrink: 1 }} color="info" onClick={onOpen}><Settings /></Button>}
    </ButtonGroup>
  </Box>
}
Example #10
Source File: ArtifactSetPicker.tsx    From genshin-optimizer with MIT License 5 votes vote down vote up
export default function ArtifactSetPicker({ index, setFilters, onChange, disabled = false }: PickerProps) {
  const { key: setKey, num: setNum } = setFilters[index]
  const { t } = useTranslation("page_character")
  const artifactSheets = usePromise(ArtifactSheet.getAll, [])
  const artifactSets = useMemo(() => {
    if (!artifactSheets) return undefined
    return allArtifactSets.filter(set => {
      const setsNumArr = set ? Object.keys(artifactSheets[set].setEffects) : []
      const artsAccountedOther = setFilters.reduce((accu, cur, ind) => (cur.key && ind !== index) ? accu + cur.num : accu, 0)
      if (setsNumArr.every((num: any) => parseInt(num) + artsAccountedOther > 5)) return false
      return true
    })
  }, [artifactSheets, setFilters, index])

  if (!artifactSets) return null

  const artsAccounted = setFilters.reduce((accu, cur) => cur.key ? accu + cur.num : accu, 0)

  return <CardLight>
    <ButtonGroup sx={{ width: "100%" }}>
      {/* Artifact set */}
      {artifactSheets && <ArtifactSetSingleAutocomplete
        flattenCorners
        showDefault
        size="small"
        artSetKey={setKey}
        setArtSetKey={setKey => onChange(index, setKey as ArtifactSetKey, parseInt(Object.keys(artifactSheets[setKey]?.setEffects ?? {})[0] as string) ?? 0)}
        allArtSetKeys={artifactSets}
        label={t("forceSet")}
        disabled={disabled}
        sx={{ flexGrow: 1 }}
        disable={(setKey) => setFilters.some(setFilter => setFilter.key === setKey)}
        defaultText={t("none")}
      />}
      {/* set number */}
      <DropdownButton title={`${setNum}-set`}
        disabled={disabled || !setKey || artsAccounted >= 5}
        sx={{ borderRadius: 0 }}
      >
        {Object.keys(artifactSheets?.[setKey]?.setEffects ?? {}).map((num: any) => {
          let artsAccountedOther = setFilters.reduce((accu, cur) => (cur.key && cur.key !== setKey) ? accu + cur.num : accu, 0)
          return (parseInt(num) + artsAccountedOther <= 5) &&
            (<MenuItem key={num} onClick={() => onChange(index, setFilters[index].key, parseInt(num) ?? 0)} >
              {`${num}-set`}
            </MenuItem>)
        })}
      </DropdownButton>
    </ButtonGroup>
    {!!setKey && <Divider />}
    {!!setKey && <CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
      {Object.keys(artifactSheets?.[setKey].setEffects ?? {}).map(setNKey => parseInt(setNKey as string) as SetNum).filter(setNkey => setNkey <= setNum).map(setNumKey =>
        <SetEffectDisplay key={setKey + setNumKey} setKey={setKey} setNumKey={setNumKey} />)}
    </CardContent>}
  </CardLight>
}
Example #11
Source File: StatInput.tsx    From genshin-optimizer with MIT License 5 votes vote down vote up
FlexButtonGroup = styled(ButtonGroup)({
  display: "flex"
})
Example #12
Source File: index.tsx    From genshin-optimizer with MIT License 4 votes vote down vote up
export default function TabBuild() {
  const { character, character: { key: characterKey } } = useContext(DataContext)
  const [{ tcMode }] = useDBState("GlobalSettings", initGlobalSettings)
  const { database } = useContext(DatabaseContext)

  const [generatingBuilds, setgeneratingBuilds] = useState(false)
  const [generationProgress, setgenerationProgress] = useState(0)
  const [generationDuration, setgenerationDuration] = useState(0)//in ms
  const [generationSkipped, setgenerationSkipped] = useState(0)

  const [chartData, setchartData] = useState(undefined as ChartData | undefined)

  const [artsDirty, setArtsDirty] = useForceUpdate()

  const [{ equipmentPriority, threads = defThreads }, setOptimizeDBState] = useOptimizeDBState()
  const maxWorkers = threads > defThreads ? defThreads : threads
  const setMaxWorkers = useCallback(threads => setOptimizeDBState({ threads }), [setOptimizeDBState],)

  const characterDispatch = useCharacterReducer(characterKey)
  const buildSettings = character?.buildSettings ?? initialBuildSettings()
  const { plotBase, setFilters, statFilters, mainStatKeys, optimizationTarget, mainStatAssumptionLevel, useExcludedArts, useEquippedArts, builds, buildDate, maxBuildsToShow, levelLow, levelHigh } = buildSettings
  const buildsArts = useMemo(() => builds.map(build => build.map(i => database._getArt(i)!)), [builds, database])
  const teamData = useTeamData(characterKey, mainStatAssumptionLevel)
  const { characterSheet, target: data } = teamData?.[characterKey as CharacterKey] ?? {}
  const compareData = character?.compareData ?? false

  const noArtifact = useMemo(() => !database._getArts().length, [database])

  const buildSettingsDispatch = useCallback((action) =>
    characterDispatch && characterDispatch({ buildSettings: buildSettingsReducer(buildSettings, action) })
    , [characterDispatch, buildSettings])

  const onChangeMainStatKey = useCallback((slotKey: SlotKey, mainStatKey?: MainStatKey) => {
    if (mainStatKey === undefined) buildSettingsDispatch({ type: "mainStatKeyReset", slotKey })
    else buildSettingsDispatch({ type: "mainStatKey", slotKey, mainStatKey })
  }, [buildSettingsDispatch])

  //register changes in artifact database
  useEffect(() =>
    database.followAnyArt(setArtsDirty),
    [setArtsDirty, database])

  const { split, setPerms, totBuildNumber } = useMemo(() => {
    if (!characterKey) // Make sure we have all slotKeys
      return { totBuildNumber: 0 }
    let cantTakeList: CharacterKey[] = []
    if (useEquippedArts) {
      const index = equipmentPriority.indexOf(characterKey)
      if (index < 0) cantTakeList = [...equipmentPriority]
      else cantTakeList = equipmentPriority.slice(0, index)
    }
    const arts = database._getArts().filter(art => {
      if (art.level < levelLow) return false
      if (art.level > levelHigh) return false
      const mainStats = mainStatKeys[art.slotKey]
      if (mainStats?.length && !mainStats.includes(art.mainStatKey)) return false

      // If its equipped on the selected character, bypass the check
      if (art.location === characterKey) return true

      if (art.exclude && !useExcludedArts) return false
      if (art.location && !useEquippedArts) return false
      if (art.location && useEquippedArts && cantTakeList.includes(art.location)) return false
      return true
    })
    const split = compactArtifacts(arts, mainStatAssumptionLevel)
    const setPerms = [...artSetPerm([setFilters.map(({ key, num }) => ({ key, min: num }))])]
    const totBuildNumber = [...setPerms].map(perm => countBuilds(filterArts(split, perm))).reduce((a, b) => a + b, 0)
    return artsDirty && { split, setPerms, totBuildNumber }
  }, [characterKey, useExcludedArts, useEquippedArts, equipmentPriority, mainStatKeys, setFilters, levelLow, levelHigh, artsDirty, database, mainStatAssumptionLevel])

  // Reset the Alert by setting progress to zero.
  useEffect(() => {
    setgenerationProgress(0)
  }, [totBuildNumber])

  // Provides a function to cancel the work
  const cancelToken = useRef(() => { })
  //terminate worker when component unmounts
  useEffect(() => () => cancelToken.current(), [])
  const generateBuilds = useCallback(async () => {
    if (!characterKey || !optimizationTarget || !split || !setPerms) return
    const teamData = await getTeamData(database, characterKey, mainStatAssumptionLevel, [])
    if (!teamData) return
    const workerData = uiDataForTeam(teamData.teamData, characterKey)[characterKey as CharacterKey]?.target.data![0]
    if (!workerData) return
    Object.assign(workerData, mergeData([workerData, dynamicData])) // Mark art fields as dynamic
    let optimizationTargetNode = objPathValue(workerData.display ?? {}, optimizationTarget) as NumNode | undefined
    if (!optimizationTargetNode) return
    const targetNode = optimizationTargetNode
    const valueFilter: { value: NumNode, minimum: number }[] = Object.entries(statFilters).map(([key, value]) => {
      if (key.endsWith("_")) value = value / 100 // TODO: Conversion
      return { value: input.total[key], minimum: value }
    }).filter(x => x.value && x.minimum > -Infinity)

    const t1 = performance.now()
    setgeneratingBuilds(true)
    setchartData(undefined)
    setgenerationDuration(0)
    setgenerationProgress(0)
    setgenerationSkipped(0)

    const cancelled = new Promise<void>(r => cancelToken.current = r)

    let nodes = [...valueFilter.map(x => x.value), optimizationTargetNode], arts = split!
    const origCount = totBuildNumber, minimum = [...valueFilter.map(x => x.minimum), -Infinity]
    if (plotBase) {
      nodes.push(input.total[plotBase])
      minimum.push(-Infinity)
    }

    nodes = optimize(nodes, workerData, ({ path: [p] }) => p !== "dyn");
    ({ nodes, arts } = pruneAll(nodes, minimum, arts, maxBuildsToShow,
      new Set(setFilters.map(x => x.key as ArtifactSetKey)), {
      reaffine: true, pruneArtRange: true, pruneNodeRange: true, pruneOrder: true
    }))

    const plotBaseNode = plotBase ? nodes.pop() : undefined
    optimizationTargetNode = nodes.pop()!

    let wrap = {
      buildCount: 0, failedCount: 0, skippedCount: origCount,
      buildValues: Array(maxBuildsToShow).fill(0).map(_ => -Infinity)
    }
    setPerms.forEach(filter => wrap.skippedCount -= countBuilds(filterArts(arts, filter)))

    const setPerm = splitFiltersBySet(arts, setPerms,
      maxWorkers === 1
        // Don't split for single worker
        ? Infinity
        // 8 perms / worker, up to 1M builds / perm
        : Math.min(origCount / maxWorkers / 4, 1_000_000))[Symbol.iterator]()

    function fetchWork(): Request | undefined {
      const { done, value } = setPerm.next()
      return done ? undefined : {
        command: "request",
        threshold: wrap.buildValues[maxBuildsToShow - 1], filter: value,
      }
    }

    const filters = nodes
      .map((value, i) => ({ value, min: minimum[i] }))
      .filter(x => x.min > -Infinity)

    const finalizedList: Promise<FinalizeResult>[] = []
    for (let i = 0; i < maxWorkers; i++) {
      const worker = new Worker()

      const setup: Setup = {
        command: "setup",
        id: `${i}`,
        arts,
        optimizationTarget: optimizationTargetNode,
        plotBase: plotBaseNode,
        maxBuilds: maxBuildsToShow,
        filters
      }
      worker.postMessage(setup, undefined)
      let finalize: (_: FinalizeResult) => void
      const finalized = new Promise<FinalizeResult>(r => finalize = r)
      worker.onmessage = async ({ data }: { data: WorkerResult }) => {
        switch (data.command) {
          case "interim":
            wrap.buildCount += data.buildCount
            wrap.failedCount += data.failedCount
            wrap.skippedCount += data.skippedCount
            if (data.buildValues) {
              wrap.buildValues.push(...data.buildValues)
              wrap.buildValues.sort((a, b) => b - a).splice(maxBuildsToShow)
            }
            break
          case "request":
            const work = fetchWork()
            if (work) {
              worker.postMessage(work)
            } else {
              const finalizeCommand: Finalize = { command: "finalize" }
              worker.postMessage(finalizeCommand)
            }
            break
          case "finalize":
            worker.terminate()
            finalize(data);
            break
          default: console.log("DEBUG", data)
        }
      }

      cancelled.then(() => worker.terminate())
      finalizedList.push(finalized)
    }

    const buildTimer = setInterval(() => {
      setgenerationProgress(wrap.buildCount)
      setgenerationSkipped(wrap.skippedCount)
      setgenerationDuration(performance.now() - t1)
    }, 100)
    const results = await Promise.any([Promise.all(finalizedList), cancelled])
    clearInterval(buildTimer)
    cancelToken.current = () => { }

    if (!results) {
      setgenerationDuration(0)
      setgenerationProgress(0)
      setgenerationSkipped(0)
    } else {
      if (plotBase) {
        const plotData = mergePlot(results.map(x => x.plotData!))
        const plotBaseNode = input.total[plotBase] as NumNode
        let data = Object.values(plotData)
        if (KeyMap.unit(targetNode.info?.key) === "%")
          data = data.map(({ value, plot }) => ({ value: value * 100, plot })) as Build[]
        if (KeyMap.unit(plotBaseNode!.info?.key) === "%")
          data = data.map(({ value, plot }) => ({ value, plot: (plot ?? 0) * 100 })) as Build[]
        setchartData({
          valueNode: targetNode,
          plotNode: plotBaseNode,
          data
        })
      }
      const builds = mergeBuilds(results.map(x => x.builds), maxBuildsToShow)
      if (process.env.NODE_ENV === "development") console.log("Build Result", builds)
      buildSettingsDispatch({ builds: builds.map(build => build.artifactIds), buildDate: Date.now() })
      const totalDuration = performance.now() - t1

      setgenerationProgress(wrap.buildCount)
      setgenerationSkipped(wrap.skippedCount)
      setgenerationDuration(totalDuration)
    }
    setgeneratingBuilds(false)
  }, [characterKey, database, totBuildNumber, mainStatAssumptionLevel, maxBuildsToShow, optimizationTarget, plotBase, setPerms, split, buildSettingsDispatch, setFilters, statFilters, maxWorkers])

  const characterName = characterSheet?.name ?? "Character Name"

  const setPlotBase = useCallback(plotBase => {
    buildSettingsDispatch({ plotBase })
    setchartData(undefined)
  }, [buildSettingsDispatch])
  const dataContext: dataContextObj | undefined = useMemo(() => {
    return data && characterSheet && character && teamData && {
      data,
      characterSheet,
      character,
      mainStatAssumptionLevel,
      teamData,
      characterDispatch
    }
  }, [data, characterSheet, character, teamData, characterDispatch, mainStatAssumptionLevel])

  return <Box display="flex" flexDirection="column" gap={1}>
    {noArtifact && <Alert severity="warning" variant="filled"> Opps! It looks like you haven't added any artifacts to GO yet! You should go to the <Link component={RouterLink} to="/artifact">Artifacts</Link> page and add some!</Alert>}
    {/* Build Generator Editor */}
    {dataContext && <DataContext.Provider value={dataContext}>

      <Grid container spacing={1} >
        {/* 1*/}
        <Grid item xs={12} sm={6} lg={3} display="flex" flexDirection="column" gap={1}>
          {/* character card */}
          <Box><CharacterCard characterKey={characterKey} /></Box>
        </Grid>

        {/* 2 */}
        <Grid item xs={12} sm={6} lg={3}>
          <CardLight>
            <CardContent  >
              <Typography gutterBottom>Main Stat</Typography>
              <BootstrapTooltip placement="top" title={<Typography><strong>Level Assumption</strong> changes mainstat value to be at least a specific level. Does not change substats.</Typography>}>
                <Box>
                  <AssumeFullLevelToggle mainStatAssumptionLevel={mainStatAssumptionLevel} setmainStatAssumptionLevel={mainStatAssumptionLevel => buildSettingsDispatch({ mainStatAssumptionLevel })} disabled={generatingBuilds} />
                </Box>
              </BootstrapTooltip>
            </CardContent>
            {/* main stat selector */}
            <MainStatSelectionCard
              mainStatKeys={mainStatKeys}
              onChangeMainStatKey={onChangeMainStatKey}
              disabled={generatingBuilds}
            />
          </CardLight>
        </Grid>

        {/* 3 */}
        <Grid item xs={12} sm={6} lg={3} display="flex" flexDirection="column" gap={1}>

          {/*Minimum Final Stat Filter */}
          <StatFilterCard statFilters={statFilters} setStatFilters={sFs => buildSettingsDispatch({ statFilters: sFs })} disabled={generatingBuilds} />

          <BonusStatsCard />

          {/* use excluded */}
          <UseExcluded disabled={generatingBuilds} useExcludedArts={useExcludedArts} buildSettingsDispatch={buildSettingsDispatch} artsDirty={artsDirty} />

          {/* use equipped */}
          <UseEquipped disabled={generatingBuilds} useEquippedArts={useEquippedArts} buildSettingsDispatch={buildSettingsDispatch} />

          { /* Level Filter */}
          <CardLight>
            <CardContent sx={{ py: 1 }}>
              Artifact Level Filter
            </CardContent>
            <ArtifactLevelSlider levelLow={levelLow} levelHigh={levelHigh}
              setLow={levelLow => buildSettingsDispatch({ levelLow })}
              setHigh={levelHigh => buildSettingsDispatch({ levelHigh })}
              setBoth={(levelLow, levelHigh) => buildSettingsDispatch({ levelLow, levelHigh })}
              disabled={generatingBuilds}
            />
          </CardLight>
        </Grid>

        {/* 4 */}
        <Grid item xs={12} sm={6} lg={3} display="flex" flexDirection="column" gap={1}>
          <ArtifactSetConditional disabled={generatingBuilds} />

          {/* Artifact set pickers */}
          {setFilters.map((setFilter, index) => (index <= setFilters.filter(s => s.key).length) && <ArtifactSetPicker key={index} index={index} setFilters={setFilters}
            disabled={generatingBuilds} onChange={(index, key, num) => buildSettingsDispatch({ type: 'setFilter', index, key, num })} />)}
        </Grid>

      </Grid>
      {/* Footer */}
      <Grid container spacing={1}>
        <Grid item flexGrow={1} >
          <ButtonGroup>
            <Button
              disabled={!characterKey || generatingBuilds || !optimizationTarget || !totBuildNumber || !objPathValue(data?.getDisplay(), optimizationTarget)}
              color={(characterKey && totBuildNumber <= warningBuildNumber) ? "success" : "warning"}
              onClick={generateBuilds}
              startIcon={<FontAwesomeIcon icon={faCalculator} />}
            >Generate Builds</Button>
            <DropdownButton disabled={generatingBuilds || !characterKey}
              title={<span><b>{maxBuildsToShow}</b> {maxBuildsToShow === 1 ? "Build" : "Builds"}</span>}>
              <MenuItem>
                <Typography variant="caption" color="info.main">
                  Decreasing the number of generated build will decrease build calculation time for large number of builds.
                </Typography>
              </MenuItem>
              <Divider />
              {maxBuildsToShowList.map(v => <MenuItem key={v}
                onClick={() => buildSettingsDispatch({ maxBuildsToShow: v })}>{v} {v === 1 ? "Build" : "Builds"}</MenuItem>)}
            </DropdownButton>
            <DropdownButton disabled={generatingBuilds || !characterKey}
              title={<span><b>{maxWorkers}</b> {maxWorkers === 1 ? "Thread" : "Threads"}</span>}>
              <MenuItem>
                <Typography variant="caption" color="info.main">
                  Increasing the number of threads will speed up build time, but will use more CPU power.
                </Typography>
              </MenuItem>
              <Divider />
              {range(1, defThreads).reverse().map(v => <MenuItem key={v}
                onClick={() => setMaxWorkers(v)}>{v} {v === 1 ? "Thread" : "Threads"}</MenuItem>)}
            </DropdownButton>
            <Button
              disabled={!generatingBuilds}
              color="error"
              onClick={() => cancelToken.current()}
              startIcon={<Close />}
            >Cancel</Button>
          </ButtonGroup>
        </Grid>
        <Grid item>
          <span>Optimization Target: </span>
          {<OptimizationTargetSelector
            optimizationTarget={optimizationTarget}
            setTarget={target => buildSettingsDispatch({ optimizationTarget: target })}
            disabled={!!generatingBuilds}
          />}
        </Grid>
      </Grid>

      {!!characterKey && <Box >
        <BuildAlert {...{ totBuildNumber, generatingBuilds, generationSkipped, generationProgress, generationDuration, characterName, maxBuildsToShow }} />
      </Box>}
      {tcMode && <Box >
        <ChartCard disabled={generatingBuilds} chartData={chartData} plotBase={plotBase} setPlotBase={setPlotBase} />
      </Box>}
      <CardLight>
        <CardContent>
          <Box display="flex" alignItems="center" gap={1} mb={1} >
            <Typography sx={{ flexGrow: 1 }}>
              {builds ? <span>Showing <strong>{builds.length}</strong> Builds generated for {characterName}. {!!buildDate && <span>Build generated on: <strong>{(new Date(buildDate)).toLocaleString()}</strong></span>}</span>
                : <span>Select a character to generate builds.</span>}
            </Typography>
            <Button disabled={!builds.length} color="error" onClick={() => buildSettingsDispatch({ builds: [], buildDate: 0 })} >Clear Builds</Button>
          </Box>
          <Grid container display="flex" spacing={1}>
            <Grid item><HitModeToggle size="small" /></Grid>
            <Grid item><ReactionToggle size="small" /></Grid>
            <Grid item flexGrow={1} />
            <Grid item><SolidToggleButtonGroup exclusive value={compareData} onChange={(e, v) => characterDispatch({ compareData: v })} size="small">
              <ToggleButton value={false} disabled={!compareData}>
                <small>Show New artifact Stats</small>
              </ToggleButton>
              <ToggleButton value={true} disabled={compareData}>
                <small>Compare against equipped artifacts</small>
              </ToggleButton>
            </SolidToggleButtonGroup></Grid>
          </Grid>
        </CardContent>
      </CardLight>
      <BuildList {...{ buildsArts, character, characterKey, characterSheet, data, compareData, mainStatAssumptionLevel, characterDispatch, disabled: !!generatingBuilds }} />
    </DataContext.Provider>}
  </Box>
}
Example #13
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>
}
Example #14
Source File: EXPCalc.tsx    From genshin-optimizer with MIT License 4 votes vote down vote up
export default function EXPCalc() {
  const [{ mora, level, curExp, goUnder, books, books: { advice, experience, wit } }, setState] = useDBState("ToolDisplayExpCalc", initExpCalc)

  let milestoneLvl = milestone.find(lvl => lvl > level)!
  let expReq = -curExp
  for (let i = level; i < Math.min(milestoneLvl, levelExp.length); i++)  expReq += levelExp[i]
  let bookResult = calculateBooks(wit, experience, advice, expReq, goUnder) || []
  let [numWit = 0, numExperience = 0, numAdvice = 0] = bookResult
  let bookResultObj = { advice: numAdvice, experience: numExperience, wit: numWit }
  let expFromBooks = numWit * 20000 + numExperience * 5000 + numAdvice * 1000
  let moraCost = expFromBooks / 5
  let expDiff = expReq - expFromBooks
  let finalMora = mora - moraCost
  let finalExp = expFromBooks + curExp
  let finalLvl = level
  for (; finalLvl < Math.min(milestoneLvl, levelExp.length); finalLvl++) {
    if (levelExp[finalLvl] <= finalExp) finalExp -= levelExp[finalLvl]
    else break;
  }
  if (finalLvl === milestoneLvl) finalExp = 0

  let invalidText: Displayable = ""

  if (finalMora < 0)
    invalidText = <span>You don't have enough <b>Mora</b> for this operation.</span>
  else if (bookResult.length === 0)
    invalidText = <span>You don't have enough <b>EXP. books</b> to level to the next milestone.</span>
  else if (level === 90)
    invalidText = "You are at the maximum level."
  return <CardDark>
    <Grid container sx={{ px: 2, py: 1 }} spacing={2} >
      <Grid item>
        <ImgIcon src={booksData.wit.img} sx={{ fontSize: "2em" }} />
      </Grid>
      <Grid item flexGrow={1}>
        <Typography variant="h6">Experience Calculator</Typography>
      </Grid>
      <Grid item>
        <ButtonGroup>
          <Button color="primary" disabled={!goUnder} onClick={() => setState({ goUnder: false })}>Full Level</Button>
          <Button color="primary" disabled={goUnder} onClick={() => setState({ goUnder: true })}>Don't fully level</Button>
        </ButtonGroup>
      </Grid>
    </Grid>
    <Divider />

    <CardContent>
      <Grid container spacing={1}>
        <Grid item>
          <Typography>
            <span>This calculator tries to calculate the amount of exp books required to get to the next milestone level. </span>
            {goUnder ? "It will try to get as close to the milestone level as possible, so you can grind the rest of the exp without any waste." :
              "It will try to calculate the amount of books needed to minimize as much exp loss as possible."}
          </Typography>
        </Grid>
        <Grid item xs={6} md={3} >
          <ButtonGroup sx={{ display: "flex" }}>
            <TextButton>Current Level</TextButton>
            <CustomNumberInputButtonGroupWrapper sx={{ flexBasis: 30, flexGrow: 1 }}>
              <CustomNumberInput
                value={level}
                onChange={(val) => setState({ level: clamp(val, 0, 90) })}
                sx={{ px: 2 }}
              />
            </CustomNumberInputButtonGroupWrapper >
          </ButtonGroup>
        </Grid>
        <Grid item xs={6} md={3} >
          <ButtonGroup sx={{ display: "flex" }}>
            <TextButton>Current EXP.</TextButton>
            <CustomNumberInputButtonGroupWrapper sx={{ flexBasis: 30, flexGrow: 1 }}>
              <CustomNumberInput
                value={curExp}
                onChange={(val) => setState({ curExp: clamp(val, 0, (levelExp[level] || 1) - 1) })}
                endAdornment={`/${levelExp[level] || 0}`}
                sx={{ px: 2 }}
              />
            </CustomNumberInputButtonGroupWrapper>
          </ButtonGroup>
        </Grid>
        <Grid item xs={6} md={3} ><CardLight>
          <Box sx={{ p: 1, display: "flex", justifyContent: "space-between" }}>
            <Typography>
              Next Milestone Level:
            </Typography>
            <Typography>
              <b>{milestoneLvl}</b>
            </Typography>
          </Box>
        </CardLight></Grid>
        <Grid item xs={6} md={3} ><CardLight>
          <Box sx={{ p: 1, display: "flex", justifyContent: "space-between" }}>
            <Typography>
              EXP. to milestone:
            </Typography>
            <Typography>
              <span><strong>{expFromBooks}</strong> / <strong>{expReq}</strong></span>
            </Typography>
          </Box>
        </CardLight></Grid>
        {Object.entries(books).map(([bookKey]) => {
          return <Grid item xs={12} md={4} key={bookKey}>
            <BookDisplay bookKey={bookKey} value={books[bookKey]} setValue={b => setState({ books: { ...books, [bookKey]: b } })} required={bookResultObj[bookKey]} />
          </Grid>
        })}
        <Grid item xs={12} md={4} >
          <ButtonGroup sx={{ display: "flex" }}>
            <TextButton>Current Mora</TextButton>
            <CustomNumberInputButtonGroupWrapper sx={{ flexBasis: 30, flexGrow: 1 }}>
              <CustomNumberInput
                value={mora}
                onChange={(val) => setState({ mora: Math.max(val ?? 0, 0) })}
                sx={{ px: 2 }}
              />
            </CustomNumberInputButtonGroupWrapper>
          </ButtonGroup>
        </Grid>
        <Grid item xs={12} md={4} ><CardLight>
          <Box sx={{ p: 1, display: "flex", justifyContent: "space-between" }}>
            <Typography>Mora Cost: </Typography>
            <Typography><b>{moraCost}</b></Typography>
          </Box>
        </CardLight></Grid>
        <Grid item xs={12} md={4} ><CardLight>
          <Box sx={{ p: 1, display: "flex", justifyContent: "space-between" }}>
            <Typography>EXP {!goUnder ? "Waste" : "Diff"}: </Typography>
            <Typography><b><ColorText color={expDiff < 0 ? `error` : `success`}>{expDiff}</ColorText></b></Typography>
          </Box>
        </CardLight></Grid>
        <Grid item xs={12} md={4} ><CardLight>
          <Box sx={{ p: 1, display: "flex", justifyContent: "space-between" }}>
            <Typography>Final Mora: </Typography>
            <Typography><b><ColorText color={finalMora < 0 ? `error` : `success`}>{finalMora}</ColorText></b></Typography>
          </Box>
        </CardLight></Grid>
        <Grid item xs={12} md={4} ><CardLight>
          <Box sx={{ p: 1, display: "flex", justifyContent: "space-between" }}>
            <Typography>Final Level: </Typography>
            <Typography><b><ColorText color="success">{finalLvl}</ColorText></b></Typography>
          </Box>
        </CardLight></Grid>
        <Grid item xs={12} md={4} ><CardLight>
          <Box sx={{ p: 1, display: "flex", justifyContent: "space-between" }}>
            <Typography>Final EXP: </Typography>
            <Typography><b><ColorText color={finalExp < 0 ? `error` : `success`}>{finalExp}</ColorText></b></Typography>
          </Box>
        </CardLight></Grid>
      </Grid>
    </CardContent>
    <Divider />
    <CardContent sx={{ py: 1 }}>
      <Grid container spacing={2}>
        <Grid item flexGrow={1}>
          {!!invalidText && <Alert variant="filled" severity="error" >{invalidText}</Alert>}
        </Grid>
        <Grid item xs="auto"><Button disabled={!!invalidText}
          onClick={() => setState({
            level: finalLvl,
            curExp: finalExp,
            books: objectMap(bookResultObj, (val, bookKey) => books[bookKey] - val) as any,
            mora: finalMora
          })}
          color="success"
          startIcon={<Check />}
          sx={{ height: "100%" }}
        >Apply</Button>
        </Grid>
      </Grid>
    </CardContent>
  </CardDark >
}
Example #15
Source File: SubstatInput.tsx    From genshin-optimizer with MIT License 4 votes vote down vote up
export default function SubstatInput({ index, artifact, setSubstat }: { index: number, artifact: ICachedArtifact | undefined, setSubstat: (index: number, substat: ISubstat) => void, }) {
  const { t } = useTranslation("artifact")
  const { mainStatKey = "", rarity = 5 } = artifact ?? {}
  const { key = "", value = 0, rolls = [], efficiency = 0 } = artifact?.substats[index] ?? {}

  const accurateValue = rolls.reduce((a, b) => a + b, 0)
  const unit = KeyMap.unit(key), rollNum = rolls.length

  let error: string = "", rollData: readonly number[] = [], allowedRolls = 0

  if (artifact) {
    // Account for the rolls it will need to fill all 4 substates, +1 for its base roll
    const rarity = artifact.rarity
    const { numUpgrades, high } = Artifact.rollInfo(rarity)
    const maxRollNum = numUpgrades + high - 3;
    allowedRolls = maxRollNum - rollNum
    rollData = key ? Artifact.getSubstatRollData(key, rarity) : []
  }
  const rollOffset = 7 - rollData.length

  if (!rollNum && key && value) error = error || t`editor.substat.error.noCalc`
  if (allowedRolls < 0) error = error || t("editor.substat.error.noOverRoll", { value: allowedRolls + rollNum })

  return <CardLight>
    <Box sx={{ display: "flex" }}>
      <ButtonGroup size="small" sx={{ width: "100%", display: "flex" }}>
        <DropdownButton
          startIcon={key ? StatIcon[key] : undefined}
          title={key ? KeyMap.getArtStr(key) : t('editor.substat.substatFormat', { value: index + 1 })}
          disabled={!artifact}
          color={key ? "success" : "primary"}
          sx={{ whiteSpace: "nowrap" }}>
          {key && <MenuItem onClick={() => setSubstat(index, { key: "", value: 0 })}>{t`editor.substat.noSubstat`}</MenuItem>}
          {allSubstatKeys.filter(key => mainStatKey !== key)
            .map(k => <MenuItem key={k} selected={key === k} disabled={key === k} onClick={() => setSubstat(index, { key: k, value: 0 })} >
              <ListItemIcon>{StatIcon[k]}</ListItemIcon>
              <ListItemText>{KeyMap.getArtStr(k)}</ListItemText>
            </MenuItem>)}
        </DropdownButton>
        <CustomNumberInputButtonGroupWrapper sx={{ flexBasis: 30, flexGrow: 1 }} >
          <CustomNumberInput
            float={unit === "%"}
            placeholder={t`editor.substat.selectSub`}
            value={key ? value : undefined}
            onChange={value => setSubstat(index, { key, value: value ?? 0 })}
            disabled={!key}
            error={!!error}
            sx={{
              px: 1,
            }}
            inputProps={{
              sx: { textAlign: "right" }
            }}
          />
        </CustomNumberInputButtonGroupWrapper>
        {!!rollData.length && <TextButton>{t`editor.substat.nextRolls`}</TextButton>}
        {rollData.map((v, i) => {
          let newValue = cacheValueString(accurateValue + v, unit)
          newValue = artifactSubstatRollCorrection[rarity]?.[key]?.[newValue] ?? newValue
          return <Button key={i} color={`roll${clamp(rollOffset + i, 1, 6)}` as any} disabled={(value && !rollNum) || allowedRolls <= 0} onClick={() => setSubstat(index, { key, value: parseFloat(newValue) })}>{newValue}</Button>
        })}
      </ButtonGroup>
    </Box>
    <Box sx={{ p: 1, }}>
      {error ? <SqBadge color="error">{t`ui:error`}</SqBadge> : <Grid container>
        <Grid item>
          <SqBadge color={rollNum === 0 ? "secondary" : `roll${clamp(rollNum, 1, 6)}`}>
            {rollNum ? t("editor.substat.RollCount", { count: rollNum }) : t`editor.substat.noRoll`}
          </SqBadge>
        </Grid>
        <Grid item flexGrow={1}>
          {!!rolls.length && [...rolls].sort().map((val, i) =>
            <Typography component="span" key={`${i}.${val}`} color={`roll${clamp(rollOffset + rollData.indexOf(val), 1, 6)}.main`} sx={{ ml: 1 }} >{cacheValueString(val, unit)}</Typography>)}
        </Grid>
        <Grid item xs="auto" flexShrink={1}>
          <Typography>
            <Trans t={t} i18nKey="editor.substat.eff" color="text.secondary">
              Efficiency: <PercentBadge valid={true} max={rollNum * 100} value={efficiency ? efficiency : t`editor.substat.noStat` as string} />
            </Trans>
          </Typography>
        </Grid>
      </Grid>}

    </Box>
  </CardLight >
}
Example #16
Source File: ArtifactCard.tsx    From genshin-optimizer with MIT License 4 votes vote down vote up
export default function ArtifactCard({ artifactId, artifactObj, onClick, onDelete, mainStatAssumptionLevel = 0, effFilter = allSubstatFilter, probabilityFilter, disableEditSetSlot = false, editor = false, canExclude = false, canEquip = false, extraButtons }: Data): JSX.Element | null {
  const { t } = useTranslation(["artifact", "ui"]);
  const { database } = useContext(DatabaseContext)
  const databaseArtifact = useArtifact(artifactId)
  const sheet = usePromise(ArtifactSheet.get((artifactObj ?? databaseArtifact)?.setKey), [artifactObj, databaseArtifact])
  const equipOnChar = (charKey: CharacterKey | "") => database.setArtLocation(artifactId!, charKey)
  const editable = !artifactObj
  const [showEditor, setshowEditor] = useState(false)
  const onHideEditor = useCallback(() => setshowEditor(false), [setshowEditor])
  const onShowEditor = useCallback(() => editable && setshowEditor(true), [editable, setshowEditor])

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

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

  const { id, lock, slotKey, rarity, level, mainStatKey, substats, exclude, location = "" } = art
  const mainStatLevel = Math.max(Math.min(mainStatAssumptionLevel, rarity * 4), level)
  const mainStatUnit = KeyMap.unit(mainStatKey)
  const levelVariant = "roll" + (Math.floor(Math.max(level, 0) / 4) + 1)
  const { currentEfficiency, maxEfficiency } = Artifact.getArtifactEfficiency(art, effFilter)
  const artifactValid = maxEfficiency !== 0
  const slotName = sheet?.getSlotName(slotKey) || "Unknown Piece Name"
  const slotDesc = sheet?.getSlotDesc(slotKey)
  const slotDescTooltip = slotDesc && <InfoTooltip title={<Box>
    <Typography variant='h6'>{slotName}</Typography>
    <Typography>{slotDesc}</Typography>
  </Box>} />
  const setEffects = sheet?.setEffects
  const setDescTooltip = sheet && setEffects && <InfoTooltip title={
    <span>
      {Object.keys(setEffects).map(setNumKey => <span key={setNumKey}>
        <Typography variant="h6"><SqBadge color="success">{t(`artifact:setEffectNum`, { setNum: setNumKey })}</SqBadge></Typography>
        <Typography>{sheet.setEffectDesc(setNumKey as any)}</Typography>
      </span>)}
    </span>
  } />
  return <Suspense fallback={<Skeleton variant="rectangular" sx={{ width: "100%", height: "100%", minHeight: 350 }} />}>
    {editor && <Suspense fallback={false}>
      <ArtifactEditor
        artifactIdToEdit={showEditor ? artifactId : ""}
        cancelEdit={onHideEditor}
        disableEditSetSlot={disableEditSetSlot}
      />
    </Suspense>}
    <CardLight sx={{ height: "100%", display: "flex", flexDirection: "column" }}>
      <ConditionalWrapper condition={!!onClick} wrapper={wrapperFunc} falseWrapper={falseWrapperFunc}>
        <Box className={`grad-${rarity}star`} sx={{ position: "relative", width: "100%" }}>
          {!onClick && <IconButton color="primary" disabled={!editable} onClick={() => database.updateArt({ lock: !lock }, id)} sx={{ position: "absolute", right: 0, bottom: 0, zIndex: 2 }}>
            {lock ? <Lock /> : <LockOpen />}
          </IconButton>}
          <Box sx={{ pt: 2, px: 2, position: "relative", zIndex: 1 }}>
            {/* header */}
            <Box component="div" sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}>
              <Chip size="small" label={<strong>{` +${level}`}</strong>} color={levelVariant as any} />
              <Typography component="span" noWrap sx={{ backgroundColor: "rgba(100,100,100,0.35)", borderRadius: "1em", px: 1 }}><strong>{slotName}</strong></Typography>
              <Box flexGrow={1} sx={{ textAlign: "right" }}>
                {slotDescTooltip}
              </Box>
            </Box>
            <Typography color="text.secondary" variant="body2">
              <SlotNameWithIcon slotKey={slotKey} />
            </Typography>
            <Typography variant="h6" color={`${KeyMap.getVariant(mainStatKey)}.main`}>
              <span>{StatIcon[mainStatKey]} {KeyMap.get(mainStatKey)}</span>
            </Typography>
            <Typography variant="h5">
              <strong>
                <ColorText color={mainStatLevel !== level ? "warning" : undefined}>{cacheValueString(Artifact.mainStatValue(mainStatKey, rarity, mainStatLevel) ?? 0, KeyMap.unit(mainStatKey))}{mainStatUnit}</ColorText>
              </strong>
            </Typography>
            <Stars stars={rarity} colored />
            {/* {process.env.NODE_ENV === "development" && <Typography color="common.black">{id || `""`} </Typography>} */}
          </Box>
          <Box sx={{ height: "100%", position: "absolute", right: 0, top: 0 }}>
            <Box
              component="img"
              src={sheet?.slotIcons[slotKey] ?? ""}
              width="auto"
              height="100%"
              sx={{ float: "right" }}
            />
          </Box>
        </Box>
        <CardContent sx={{ flexGrow: 1, display: "flex", flexDirection: "column", pt: 1, pb: 0, width: "100%" }}>
          {substats.map((stat: ICachedSubstat) => <SubstatDisplay key={stat.key} stat={stat} effFilter={effFilter} rarity={rarity} />)}
          <Box sx={{ display: "flex", my: 1 }}>
            <Typography color="text.secondary" component="span" variant="caption" sx={{ flexGrow: 1 }}>{t`artifact:editor.curSubEff`}</Typography>
            <PercentBadge value={currentEfficiency} max={900} valid={artifactValid} />
          </Box>
          {currentEfficiency !== maxEfficiency && <Box sx={{ display: "flex", mb: 1 }}>
            <Typography color="text.secondary" component="span" variant="caption" sx={{ flexGrow: 1 }}>{t`artifact:editor.maxSubEff`}</Typography>
            <PercentBadge value={maxEfficiency} max={900} valid={artifactValid} />
          </Box>}
          <Box flexGrow={1} />
          {probabilityFilter && <strong>Probability: {(probability(art, probabilityFilter) * 100).toFixed(2)}%</strong>}
          <Typography color="success.main">{sheet?.name ?? "Artifact Set"} {setDescTooltip}</Typography>
        </CardContent>
      </ConditionalWrapper>
      <Box sx={{ p: 1, display: "flex", gap: 1, justifyContent: "space-between", alignItems: "center" }}>
        {editable && canEquip
          ? <CharacterAutocomplete sx={{ flexGrow: 1 }} size="small" showDefault
            defaultIcon={<BusinessCenter />} defaultText={t("ui:inventory")}
            value={location} onChange={equipOnChar} />
          : <LocationName location={location} />}
        {editable && <ButtonGroup sx={{ height: "100%" }}>
          {editor && <Tooltip title={<Typography>{t`artifact:edit`}</Typography>} placement="top" arrow>
            <Button color="info" size="small" onClick={onShowEditor} >
              <FontAwesomeIcon icon={faEdit} className="fa-fw" />
            </Button>
          </Tooltip>}
          {canExclude && <Tooltip title={<Typography>{t`artifact:excludeArtifactTip`}</Typography>} placement="top" arrow>
            <Button onClick={() => database.updateArt({ exclude: !exclude }, id)} color={exclude ? "error" : "success"} size="small" >
              <FontAwesomeIcon icon={exclude ? faBan : faChartLine} className="fa-fw" />
            </Button>
          </Tooltip>}
          {!!onDelete && <Button color="error" size="small" onClick={() => onDelete(id)} disabled={lock}>
            <FontAwesomeIcon icon={faTrashAlt} className="fa-fw" />
          </Button>}
          {extraButtons}
        </ButtonGroup>}
      </Box>
    </CardLight >
  </Suspense>
}
Example #17
Source File: WeaponCard.tsx    From genshin-optimizer with MIT License 4 votes vote down vote up
export default function WeaponCard({ weaponId, onClick, onEdit, onDelete, canEquip = false, extraButtons }: WeaponCardProps) {
  const { t } = useTranslation(["page_weapon", "ui"]);
  const { database } = useContext(DatabaseContext)
  const databaseWeapon = useWeapon(weaponId)
  const weapon = databaseWeapon
  const weaponSheet = usePromise(weapon?.key ? WeaponSheet.get(weapon.key) : undefined, [weapon?.key])

  const filter = useCallback(
    (cs: CharacterSheet) => cs.weaponTypeKey === weaponSheet?.weaponType,
    [weaponSheet],
  )

  const wrapperFunc = useCallback(children => <CardActionArea onClick={() => onClick?.(weaponId)} >{children}</CardActionArea>, [onClick, weaponId],)
  const falseWrapperFunc = useCallback(children => <Box >{children}</Box>, [])

  const equipOnChar = useCallback((charKey: CharacterKey | "") => database.setWeaponLocation(weaponId, charKey), [database, weaponId],)

  const UIData = useMemo(() => weaponSheet && weapon && computeUIData([weaponSheet.data, dataObjForWeapon(weapon)]), [weaponSheet, weapon])

  if (!weapon || !weaponSheet || !UIData) return null;
  const { level, ascension, refinement, id, location = "", lock } = weapon
  const weaponTypeKey = UIData.get(input.weapon.type).value!
  const stats = [input.weapon.main, input.weapon.sub, input.weapon.sub2].map(x => UIData.get(x))
  const img = ascension < 2 ? weaponSheet?.img : weaponSheet?.imgAwaken

  return <Suspense fallback={<Skeleton variant="rectangular" sx={{ width: "100%", height: "100%", minHeight: 300 }} />}>
    <CardLight sx={{ height: "100%", display: "flex", flexDirection: "column", justifyContent: "space-between" }}>
      <ConditionalWrapper condition={!!onClick} wrapper={wrapperFunc} falseWrapper={falseWrapperFunc}>
        <Box className={`grad-${weaponSheet.rarity}star`} sx={{ position: "relative", pt: 2, px: 2, }}>
          {!onClick && <IconButton color="primary" onClick={() => database.updateWeapon({ lock: !lock }, id)} sx={{ position: "absolute", right: 0, bottom: 0, zIndex: 2 }}>
            {lock ? <Lock /> : <LockOpen />}
          </IconButton>}
          <Box sx={{ position: "relative", zIndex: 1 }}>
            <Box component="div" sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}>
              <ImgIcon sx={{ fontSize: "1.5em" }} src={Assets.weaponTypes?.[weaponTypeKey]} />
              <Typography noWrap sx={{ textAlign: "center", backgroundColor: "rgba(100,100,100,0.35)", borderRadius: "1em", px: 1 }}><strong>{weaponSheet.name}</strong></Typography>
            </Box>
            <Typography component="span" variant="h5">Lv. {level}</Typography>
            <Typography component="span" variant="h5" color="text.secondary">/{ascensionMaxLevel[ascension]}</Typography>
            <Typography variant="h6">Refinement <strong>{refinement}</strong></Typography>
            <Typography><Stars stars={weaponSheet.rarity} colored /></Typography>
          </Box>
          <Box sx={{ height: "100%", position: "absolute", right: 0, top: 0 }}>
            <Box
              component="img"
              src={img ?? ""}
              width="auto"
              height="100%"
              sx={{ float: "right" }}
            />
          </Box>
        </Box>
        <CardContent>
          {stats.map(node => {
            if (!node.info.key) return null
            const displayVal = valueString(node.value, node.unit, !node.unit ? 0 : undefined)
            return <Box key={node.info.key} sx={{ display: "flex" }}>
              <Typography flexGrow={1}>{StatIcon[node.info.key!]} {KeyMap.get(node.info.key)}</Typography>
              <Typography>{displayVal}</Typography>
            </Box>
          })}
        </CardContent>
      </ConditionalWrapper>
      <Box sx={{ p: 1, display: "flex", gap: 1, justifyContent: "space-between", alignItems: "center" }}>
        {canEquip
          ? <CharacterAutocomplete size="small" sx={{ flexGrow: 1 }}
            showDefault defaultIcon={<BusinessCenter />} defaultText={t("inventory")}
            value={location} onChange={equipOnChar} filter={filter} />
          : <LocationName location={location} />}
        <ButtonGroup>
          {!!onEdit && <Tooltip title={<Typography>{t`page_weapon:edit`}</Typography>} placement="top" arrow>
            <Button color="info" onClick={() => onEdit(id)} >
              <FontAwesomeIcon icon={faEdit} className="fa-fw" />
            </Button>
          </Tooltip>}
          {!!onDelete && <Button color="error" onClick={() => onDelete(id)} disabled={!!location || lock} >
            <FontAwesomeIcon icon={faTrashAlt} className="fa-fw" />
          </Button>}
          {extraButtons}
        </ButtonGroup>
      </Box>
    </CardLight>
  </Suspense>
}
Example #18
Source File: WeaponEditor.tsx    From genshin-optimizer with MIT License 4 votes vote down vote up
export default function WeaponEditor({
  weaponId: propWeaponId,
  footer = false,
  onClose,
  extraButtons
}: WeaponStatsEditorCardProps) {
  const { t } = useTranslation("ui")
  const { data } = useContext(DataContext)

  const { database } = useContext(DatabaseContext)
  const weapon = useWeapon(propWeaponId)
  const { key = "", level = 0, refinement = 0, ascension = 0, lock, location = "", id } = weapon ?? {}
  const weaponSheet = usePromise(WeaponSheet.get(key), [key])

  const weaponDispatch = useCallback((newWeapon: Partial<ICachedWeapon>) => {
    database.updateWeapon(newWeapon, propWeaponId)
  }, [propWeaponId, database])

  const setLevel = useCallback(level => {
    level = clamp(level, 1, 90)
    const ascension = ascensionMaxLevel.findIndex(ascenML => level <= ascenML)
    weaponDispatch({ level, ascension })
  }, [weaponDispatch])

  const setAscension = useCallback(() => {
    const lowerAscension = ascensionMaxLevel.findIndex(ascenML => level !== 90 && level === ascenML)
    if (ascension === lowerAscension) weaponDispatch({ ascension: ascension + 1 })
    else weaponDispatch({ ascension: lowerAscension })
  }, [weaponDispatch, ascension, level])

  const characterSheet = usePromise(location ? CharacterSheet.get(location) : undefined, [location])
  const weaponFilter = characterSheet ? (ws) => ws.weaponType === characterSheet.weaponTypeKey : undefined
  const initialWeaponFilter = characterSheet && characterSheet.weaponTypeKey

  const equipOnChar = useCallback((charKey: CharacterKey | "") => id && database.setWeaponLocation(id, charKey), [database, id])
  const filter = useCallback(
    (cs: CharacterSheet) => cs.weaponTypeKey === weaponSheet?.weaponType,
    [weaponSheet],
  )

  const [showModal, setshowModal] = useState(false)
  const img = ascension < 2 ? weaponSheet?.img : weaponSheet?.imgAwaken

  //check the levels when switching from a 5* to a 1*, for example.
  useEffect(() => {
    if (!weaponSheet || !weaponDispatch || weaponSheet.key !== weapon?.key) return
    if (weaponSheet.rarity <= 2 && (level > 70 || ascension > 4)) {
      const [level, ascension] = lowRarityMilestoneLevels[0]
      weaponDispatch({ level, ascension })
    }
  }, [weaponSheet, weapon, weaponDispatch, level, ascension])


  const weaponUIData = useMemo(() => weaponSheet && weapon && computeUIData([weaponSheet.data, dataObjForWeapon(weapon)]), [weaponSheet, weapon])
  return <ModalWrapper open={!!propWeaponId} onClose={onClose} containerProps={{ maxWidth: "md" }}><CardLight>
    <WeaponSelectionModal show={showModal} onHide={() => setshowModal(false)} onSelect={k => weaponDispatch({ key: k })} filter={weaponFilter} weaponFilter={initialWeaponFilter} />
    <CardContent >
      {weaponSheet && weaponUIData && <Grid container spacing={1.5}>
        <Grid item xs={12} sm={3}>
          <Grid container spacing={1.5}>
            <Grid item xs={6} sm={12}>
              <Box component="img" src={img} className={`grad-${weaponSheet.rarity}star`} sx={{ maxWidth: 256, width: "100%", height: "auto", borderRadius: 1 }} />
            </Grid>
            <Grid item xs={6} sm={12}>
              <Typography><small>{weaponSheet.description}</small></Typography>
            </Grid>
          </Grid>
        </Grid>
        <Grid item xs={12} sm={9} sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
          <Box display="flex" gap={1} flexWrap="wrap" justifyContent="space-between">
            <ButtonGroup>
              <Button onClick={() => setshowModal(true)} >{weaponSheet?.name ?? "Select a Weapon"}</Button>
              {weaponSheet?.hasRefinement && <DropdownButton title={`Refinement ${refinement}`}>
                <MenuItem>Select Weapon Refinement</MenuItem>
                <Divider />
                {[...Array(5).keys()].map(key =>
                  <MenuItem key={key} onClick={() => weaponDispatch({ refinement: key + 1 })} selected={refinement === (key + 1)} disabled={refinement === (key + 1)}>
                    {`Refinement ${key + 1}`}
                  </MenuItem>)}
              </DropdownButton>}
              {extraButtons}
            </ButtonGroup>
          </Box>
          <Box display="flex" gap={1} flexWrap="wrap" justifyContent="space-between">
            <ButtonGroup sx={{ bgcolor: t => t.palette.contentLight.main }} >
              <CustomNumberInputButtonGroupWrapper >
                <CustomNumberInput onChange={setLevel} value={level}
                  startAdornment="Lv. "
                  inputProps={{ min: 1, max: 90, sx: { textAlign: "center" } }}
                  sx={{ width: "100%", height: "100%", pl: 2 }}
                />
              </CustomNumberInputButtonGroupWrapper>
              {weaponSheet && <Button sx={{ pl: 1 }} disabled={!weaponSheet.ambiguousLevel(level)} onClick={setAscension}><strong>/ {ascensionMaxLevel[ascension]}</strong></Button>}
              {weaponSheet && <DropdownButton title={"Select Level"} >
                {weaponSheet.milestoneLevels.map(([lv, as]) => {
                  const sameLevel = lv === ascensionMaxLevel[as]
                  const lvlstr = sameLevel ? `Lv. ${lv}` : `Lv. ${lv}/${ascensionMaxLevel[as]}`
                  const selected = lv === level && as === ascension
                  return <MenuItem key={`${lv}/${as}`} selected={selected} disabled={selected} onClick={() => weaponDispatch({ level: lv, ascension: as })}>{lvlstr}</MenuItem>
                })}
              </DropdownButton>}
            </ButtonGroup>

            <Button color="error" onClick={() => id && database.updateWeapon({ lock: !lock }, id)} startIcon={lock ? <Lock /> : <LockOpen />}>
              {lock ? "Locked" : "Unlocked"}
            </Button>
          </Box>
          <Typography><Stars stars={weaponSheet.rarity} /></Typography>
          <Typography variant="subtitle1"><strong>{weaponSheet.passiveName}</strong></Typography>
          <Typography gutterBottom>{weaponSheet.passiveName && weaponSheet.passiveDescription(weaponUIData.get(input.weapon.refineIndex).value)}</Typography>
          <Box display="flex" flexDirection="column" gap={1}>
            <CardDark >
              <CardHeader title={"Main Stats"} titleTypographyProps={{ variant: "subtitle2" }} />
              <Divider />
              <FieldDisplayList>
                {[input.weapon.main, input.weapon.sub, input.weapon.sub2].map((node, i) => {
                  const n = weaponUIData.get(node)
                  if (n.isEmpty || !n.value) return null
                  return <NodeFieldDisplay key={n.info.key} node={n} component={ListItem} />
                })}
              </FieldDisplayList>
            </CardDark>
            {data && weaponSheet.document && <DocumentDisplay sections={weaponSheet.document} />}
          </Box>
        </Grid>
      </Grid>}
    </CardContent>
    {footer && id && <CardContent sx={{ py: 1 }}>
      <Grid container spacing={1}>
        <Grid item flexGrow={1}>
          <CharacterAutocomplete showDefault defaultIcon={<BusinessCenter />} defaultText={t("inventory")} value={location} onChange={equipOnChar} filter={filter} />
        </Grid>
        {!!onClose && <Grid item><CloseButton sx={{ height: "100%" }} large onClick={onClose} /></Grid>}
      </Grid>
    </CardContent>}
  </CardLight ></ModalWrapper>
}
Example #19
Source File: websiteCardNew.tsx    From Search-Next with GNU General Public License v3.0 4 votes vote down vote up
WebsiteCardNew: React.FC<WebsiteCardNewProps> = (props) => {
  const { datasource } = props;
  const { name, intro, color, url } = datasource;

  const onAdd = () => {
    const res = addSite({
      name,
      url: url.substring(0, url.lastIndexOf('/')),
    });
    if (res) toast.success('添加成功');
  };

  const onCopy = () => {
    if (navigator.clipboard) {
      navigator.clipboard.writeText(url);
      toast.success(`已复制 ${name} (${url})`);
    } else {
      const copy = new Clipboard(`.copy-button_${name}`);
      copy.on('success', (e) => {
        toast.success(`已复制 ${name} (${url})`);
      });
      copy.on('error', function (e) {
        toast.warning(
          `您的浏览器不支持复制功能,请点击跳转到该网站手动复制地址`,
        );
      });
    }
  };

  const onMore = () => {
    toast.warning('功能开发中...');
  };

  return (
    <div
      className={classNames(
        'cursor-pointer shadow-md rounded border-b-2',
        css`
          --tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
            0 1px 3px 0 ${hexToRgba(color ?? '#000', 0.45).rgba} !important;
          border-bottom-color: ${color};
        `,
      )}
    >
      <CardActionArea>
        <Tooltip title={intro || '暂无介绍'}>
          <div className="p-3 flex gap-3" onClick={() => window.open(url)}>
            <Avatar
              // style={{ backgroundColor: color }}
              src={getWebIconByUrl(url)}
            >
              {name.split('')[0].toUpperCase()}
            </Avatar>
            <div className="flex-grow overflow-hidden">
              <p className="font-bold text-base whitespace-nowrap overflow-x-hidden">
                {name}
              </p>
              <Overflow>{(intro as any) || ('暂无介绍' as any)}</Overflow>
            </div>
          </div>
        </Tooltip>
      </CardActionArea>
      <div>
        <ButtonGroup
          disableElevation
          variant="text"
          size="small"
          className={classNames(
            'w-full h-full flex',
            css`
              justify-content: flex-end;
              button {
                height: 100%;
                border-right: 0px !important;
              }
            `,
          )}
        >
          <Tooltip title="添加到首页">
            <Button onClick={onAdd}>
              <Add />
            </Button>
          </Tooltip>
          <Tooltip title="复制网站链接">
            <Button
              className={`copy-button_${name}`}
              data-clipboard-text={url}
              onClick={onCopy}
            >
              <CopyAll />
            </Button>
          </Tooltip>
          {false && (
            <Tooltip title="更多">
              <Button onClick={onMore}>
                <MoreHoriz />
              </Button>
            </Tooltip>
          )}
        </ButtonGroup>
      </div>
    </div>
  );
}