feat: allow to specify default and locked settings in config.json feat: allow to specify default app params in config.json feat: rework how loading progress is reported in app on connect feat: add setting to wait for chunks to load before starting rendering (which is highly recommended to enable), however not enabled by default because functionality is top priority of the client out of the box, not pleasent ux, so pls enable yourself
211 lines
6.6 KiB
TypeScript
211 lines
6.6 KiB
TypeScript
import { useSnapshot } from 'valtio'
|
|
import { noCase } from 'change-case'
|
|
import { titleCase } from 'title-case'
|
|
import { useMemo } from 'react'
|
|
import { disabledSettings, options, qsOptions } from '../optionsStorage'
|
|
import { hideAllModals, miscUiState } from '../globalState'
|
|
import Button from './Button'
|
|
import Slider from './Slider'
|
|
import Screen from './Screen'
|
|
import { showOptionsModal } from './SelectOption'
|
|
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
|
|
|
|
type GeneralItem<T extends string | number | boolean> = {
|
|
id?: string
|
|
text?: string,
|
|
disabledReason?: string,
|
|
disabledDuringGame?: boolean
|
|
tooltip?: string
|
|
// description?: string
|
|
enableWarning?: string
|
|
willHaveNoEffect?: boolean
|
|
values?: Array<T | [T, string]>
|
|
disableIf?: [option: keyof typeof options, value: any]
|
|
}
|
|
|
|
export type OptionMeta<T = any> = GeneralItem<T & string> & ({
|
|
type: 'toggle',
|
|
} | {
|
|
type: 'slider'
|
|
min?: number,
|
|
max?: number,
|
|
valueText?: (value: number) => string,
|
|
unit?: string,
|
|
delayApply?: boolean,
|
|
} | {
|
|
type: 'element'
|
|
render: () => React.ReactNode,
|
|
})
|
|
|
|
// todo not reactive
|
|
const isLocked = (item: GeneralItem<any>) => {
|
|
return disabledSettings.value.has(item.id!)
|
|
}
|
|
|
|
const useCommonComponentsProps = (item: OptionMeta) => {
|
|
let disabledBecauseOfSetting = false
|
|
|
|
if (item.disableIf) {
|
|
// okay to use hook conditionally as disableIf must be a constant
|
|
const disableIfSetting = useSnapshot(options)[item.disableIf[0]]
|
|
disabledBecauseOfSetting = disableIfSetting === item.disableIf[1]
|
|
}
|
|
|
|
return {
|
|
disabledBecauseOfSetting
|
|
}
|
|
}
|
|
|
|
export const OptionButton = ({ item }: { item: Extract<OptionMeta, { type: 'toggle' }> }) => {
|
|
const { disabledBecauseOfSetting } = useCommonComponentsProps(item)
|
|
|
|
const optionValue = useSnapshot(options)[item.id!]
|
|
|
|
const valuesTitlesMap = useMemo(() => {
|
|
if (!item.values) {
|
|
return {
|
|
// true: <span style={{ color: 'lime' }}>ON</span>,
|
|
// false: <span style={{ color: 'red' }}>OFF</span>,
|
|
true: 'ON',
|
|
false: 'OFF',
|
|
}
|
|
}
|
|
return Object.fromEntries(item.values.map((value) => {
|
|
if (typeof value === 'string') {
|
|
return [value, titleCase(noCase(value))]
|
|
} else {
|
|
return [value[0], value[1]]
|
|
}
|
|
}))
|
|
}, [item.values])
|
|
|
|
let { disabledReason } = item
|
|
if (disabledBecauseOfSetting) disabledReason = `Disabled because ${item.disableIf![0]} is ${item.disableIf![1]}`
|
|
|
|
return <Button
|
|
data-setting={item.id}
|
|
label={`${item.text}: ${valuesTitlesMap[optionValue]}`}
|
|
// label={`${item.text}:`}
|
|
// postLabel={valuesTitlesMap[optionValue]}
|
|
onClick={async (event) => {
|
|
if (disabledReason) {
|
|
await showOptionsModal(`The option is unavailable. ${disabledReason}`, [])
|
|
return
|
|
}
|
|
if (item.enableWarning && !options[item.id!]) {
|
|
const result = await showOptionsModal(item.enableWarning, ['Enable'])
|
|
if (!result) return
|
|
}
|
|
const { values } = item
|
|
if (values) {
|
|
const getOptionValue = (arrItem) => {
|
|
if (typeof arrItem === 'string') {
|
|
return arrItem
|
|
} else {
|
|
return arrItem[0]
|
|
}
|
|
}
|
|
const currentIndex = values.findIndex((value) => {
|
|
return getOptionValue(value) === optionValue
|
|
})
|
|
if (currentIndex === -1) {
|
|
options[item.id!] = getOptionValue(values[0])
|
|
} else {
|
|
const nextIndex = event.shiftKey
|
|
? (currentIndex - 1 + values.length) % values.length
|
|
: (currentIndex + 1) % values.length
|
|
options[item.id!] = getOptionValue(values[nextIndex])
|
|
}
|
|
} else {
|
|
options[item.id!] = !options[item.id!]
|
|
}
|
|
}}
|
|
title={disabledReason ? `${disabledReason} | ${item.tooltip}` : item.tooltip}
|
|
disabled={disabledBecauseOfSetting || !!item.disabledReason || isLocked(item)}
|
|
style={{
|
|
width: 150,
|
|
}}
|
|
/>
|
|
}
|
|
|
|
export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slider' }> }) => {
|
|
const { disabledBecauseOfSetting } = useCommonComponentsProps(item)
|
|
|
|
const optionValue = useSnapshot(options)[item.id!]
|
|
|
|
const valueDisplay = useMemo(() => {
|
|
if (item.valueText) return item.valueText(optionValue)
|
|
return undefined // default display
|
|
}, [optionValue])
|
|
|
|
return (
|
|
<Slider
|
|
label={item.text!}
|
|
value={options[item.id!]}
|
|
data-setting={item.id}
|
|
disabledReason={isLocked(item) ? 'qs' : disabledBecauseOfSetting ? `Disabled because ${item.disableIf![0]} is ${item.disableIf![1]}` : item.disabledReason}
|
|
min={item.min}
|
|
max={item.max}
|
|
unit={item.unit}
|
|
valueDisplay={valueDisplay}
|
|
updateOnDragEnd={item.delayApply}
|
|
updateValue={(value) => {
|
|
options[item.id!] = value
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const OptionElement = ({ item }: { item: Extract<OptionMeta, { type: 'element' }> }) => {
|
|
return item.render()
|
|
}
|
|
|
|
const RenderOption = ({ item }: { item: OptionMeta }) => {
|
|
const { gameLoaded } = useSnapshot(miscUiState)
|
|
if (item.id) {
|
|
item.text ??= titleCase(noCase(item.id))
|
|
}
|
|
if (item.disabledDuringGame && gameLoaded) {
|
|
item.disabledReason = 'Cannot be changed during game'
|
|
}
|
|
|
|
let baseElement = null as React.ReactNode | null
|
|
if (item.type === 'toggle') baseElement = <OptionButton item={item} />
|
|
if (item.type === 'slider') baseElement = <OptionSlider item={item} />
|
|
if (item.type === 'element') baseElement = <OptionElement item={item} />
|
|
return baseElement
|
|
// if (!item.description && item.type === 'element') return baseElement
|
|
|
|
// return <div>
|
|
// {baseElement}
|
|
// {item.description && <div style={{ fontSize: 9, color: 'gray' }}>{item.description}</div>}
|
|
// </div>
|
|
}
|
|
|
|
interface Props {
|
|
readonly items: OptionMeta[]
|
|
title: string
|
|
backButtonAction?: () => void
|
|
}
|
|
|
|
export default ({ items, title, backButtonAction }: Props) => {
|
|
const { currentTouch } = useSnapshot(miscUiState)
|
|
|
|
return <Screen
|
|
title={title}
|
|
>
|
|
<div className='screen-items'>
|
|
{currentTouch && (
|
|
<div style={{ position: 'fixed', marginLeft: '-30px', display: 'flex', flexDirection: 'column', gap: 1, }}>
|
|
<Button icon={pixelartIcons['close']} onClick={hideAllModals} style={{ color: '#ff5d5d', }} />
|
|
<Button icon={pixelartIcons['chevron-left']} onClick={backButtonAction} style={{ color: 'yellow', }} />
|
|
</div>
|
|
)}
|
|
{items.map((element, i) => {
|
|
// make sure its unique!
|
|
return <RenderOption key={element.id ?? `${title}-${i}`} item={element} />
|
|
})}
|
|
</div>
|
|
{backButtonAction && <Button onClick={() => backButtonAction()}>Back</Button>}
|
|
</Screen>
|
|
}
|