pages235/src/react/IndicatorEffects.tsx
2025-04-07 02:21:37 +03:00

129 lines
3.4 KiB
TypeScript

import { useMemo, useEffect, useRef } from 'react'
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
import './IndicatorEffects.css'
function formatTime (seconds: number): string {
if (seconds < 0) return ''
const minutes = Math.floor(seconds / 60)
const remainingSeconds = Math.floor(seconds % 60)
const formattedMinutes = String(minutes).padStart(2, '0')
const formattedSeconds = String(remainingSeconds)
return `${formattedMinutes}:${formattedSeconds}`
}
export type EffectType = {
image: string,
time: number,
level: number,
removeEffect: (image: string) => void,
reduceTime: (image: string) => void
}
const EffectBox = ({ image, time, level }: Pick<EffectType, 'image' | 'time' | 'level'>) => {
const formattedTime = useMemo(() => formatTime(time), [time])
return <div className='effect-box'>
<img className='effect-box__image' src={image} alt='' />
<div>
{formattedTime ? (
// if time is negative then effect is shown without time.
// Component should be removed manually with time = 0
<div className='effect-box__time'>{formattedTime}</div>
) : null}
{level > 0 && level < 256 ? (
<div className='effect-box__level'>{level + 1}</div>
) : null}
</div>
</div>
}
export const defaultIndicatorsState = {
chunksLoading: false,
readingFiles: false,
readonlyFiles: false,
writingFiles: false, // saving
appHasErrors: false,
connectionIssues: 0,
preventSleep: false,
}
const indicatorIcons: Record<keyof typeof defaultIndicatorsState, string> = {
chunksLoading: 'add-grid',
readingFiles: 'arrow-bar-down',
writingFiles: 'arrow-bar-up',
appHasErrors: 'alert',
readonlyFiles: 'file-off',
connectionIssues: pixelartIcons['cellular-signal-off'],
preventSleep: pixelartIcons.moon,
}
const colorOverrides = {
connectionIssues: {
0: false,
1: 'orange',
2: 'red'
}
}
export default ({ indicators, effects }: { indicators: typeof defaultIndicatorsState, effects: readonly EffectType[] }) => {
const effectsRef = useRef(effects)
useEffect(() => {
effectsRef.current = effects
}, [effects])
useEffect(() => {
// todo use more precise timer for each effect
const interval = setInterval(() => {
for (const [index, effect] of effectsRef.current.entries()) {
if (effect.time === 0) {
// effect.removeEffect(effect.image)
return
}
effect.reduceTime(effect.image)
}
}, 1000)
return () => {
clearInterval(interval)
}
}, [])
const indicatorsMapped = Object.entries(defaultIndicatorsState).map(([key]) => {
const state = indicators[key]
return {
icon: indicatorIcons[key],
// preserve order
state,
key
}
})
return <div className='effectsScreen-container'>
<div className='indicators-container'>
{
indicatorsMapped.map((indicator) => <div
key={indicator.icon}
style={{
opacity: indicator.state ? 1 : 0,
transition: 'opacity color 0.1s',
color: colorOverrides[indicator.key]?.[indicator.state]
}}
>
<PixelartIcon iconName={indicator.icon} />
</div>)
}
</div>
<div className='effects-container'>
{
effects.map((effect) => <EffectBox
key={`effectBox-${effect.image}`}
image={effect.image}
time={effect.time}
level={effect.level}
/>)
}
</div>
</div>
}