@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 |
// 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
FlexButtonGroup = styled(ButtonGroup)({
display: "flex"
})
Example #12
Source File: index.tsx From genshin-optimizer with MIT License | 4 votes |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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>
);
}