355 lines
11 KiB
JavaScript
355 lines
11 KiB
JavaScript
import React, {
|
|
useState,
|
|
useEffect,
|
|
useCallback,
|
|
useMemo
|
|
} from 'react'
|
|
import PropTypes from 'prop-types';
|
|
import {
|
|
NavLink,
|
|
Link,
|
|
} from "react-router-dom";
|
|
import classes from '@/scss/home/Home.module.scss'
|
|
import { useConfig } from '@/state/config';
|
|
import { useI18n } from '@/state/language';
|
|
import {
|
|
useLanguage
|
|
} from '@/state/language'
|
|
import { useHeader } from '@/state/header';
|
|
import { useAppbar } from '@/state/appbar';
|
|
import VoiceElement from '@/component/voice';
|
|
import { useAtom } from 'jotai'
|
|
import { atomWithStorage } from 'jotai/utils';
|
|
import CharIcon from '@/component/char_icon';
|
|
import Border from '@/component/border';
|
|
import useUmami from '@/state/insights';
|
|
import Switch from '@/component/switch';
|
|
import SearchBox from '@/component/search_box';
|
|
|
|
const voiceOnAtom = atomWithStorage('voiceOn', false)
|
|
let lastVoiceState = 'ended'
|
|
|
|
export default function Home() {
|
|
// eslint-disable-next-line no-unused-vars
|
|
const _trackEvt = useUmami('/', "Dynamic Compile")
|
|
const {
|
|
setTitle,
|
|
setTabs,
|
|
currentTab,
|
|
setHeaderIcon,
|
|
setFastNavigation
|
|
} = useHeader()
|
|
const { config, operators, officalUpdate } = useConfig()
|
|
const { i18n } = useI18n()
|
|
const [content, setContent] = useState([])
|
|
const [voiceOn] = useAtom(voiceOnAtom)
|
|
const [voiceSrc, setVoiceSrc] = useState(null)
|
|
const [voiceReplay, setVoiceReplay] = useState(false)
|
|
const { language } = useLanguage()
|
|
const [navigationList, setNavigationList] = useState([])
|
|
const [searchField, setSearchField] = useState('');
|
|
const [updatedList, setUpdatedList] = useState([])
|
|
|
|
useEffect(() => {
|
|
setTitle('dynamic_compile')
|
|
setTabs([{
|
|
key: 'all'
|
|
}, {
|
|
key: 'operator'
|
|
}, {
|
|
key: 'skin'
|
|
}])
|
|
setHeaderIcon(null)
|
|
}, [setHeaderIcon, setTabs, setTitle])
|
|
|
|
useEffect(() => {
|
|
setContent(config?.operators || [])
|
|
document.documentElement.style.setProperty('--cursor-color', 'var(--text-color)');
|
|
}, [config])
|
|
|
|
const handleAduioStateChange = useCallback((e, state) => {
|
|
lastVoiceState = state
|
|
if (state === 'ended') {
|
|
setVoiceReplay(false)
|
|
}
|
|
}, [])
|
|
|
|
const isShown = useCallback((type) => currentTab === 'all' || currentTab === type, [currentTab])
|
|
|
|
const fastNavigateDict = useMemo(() => {
|
|
const dict = {}
|
|
operators.forEach((item) => {
|
|
if (!(item.date in dict)) {
|
|
dict[item.date] = []
|
|
}
|
|
dict[item.date].push({
|
|
codename: item.codename,
|
|
link: item.link,
|
|
type: item.type,
|
|
color: item.color
|
|
})
|
|
})
|
|
return dict
|
|
}, [operators])
|
|
|
|
useEffect(() => {
|
|
const list = []
|
|
for (const [key, value] of Object.entries(fastNavigateDict)) {
|
|
const newValue = value.filter((item) => isShown(item.type))
|
|
if (newValue.length > 0) {
|
|
list.push({
|
|
name: key,
|
|
value: null,
|
|
type: "date",
|
|
})
|
|
newValue.forEach((item) => {
|
|
list.push({
|
|
name: item.codename[language],
|
|
value: item.link,
|
|
type: "item",
|
|
color: item.color,
|
|
icon: <CharIcon
|
|
type={item.type}
|
|
viewBox={
|
|
item.type === 'operator' ? '0 0 88.969 71.469' : '0 0 94.563 67.437'
|
|
} />
|
|
})
|
|
})
|
|
}
|
|
}
|
|
setNavigationList(list)
|
|
setUpdatedList(list)
|
|
}, [fastNavigateDict, isShown, language])
|
|
|
|
useEffect(() => {
|
|
const list = navigationList.filter((item) => { return (item.name.toLowerCase().indexOf(searchField.toLowerCase()) !== -1) || (item.type === 'date'); })
|
|
const newList = []
|
|
for (let i = 0; i < list.length - 1; i++) {
|
|
const firstType = list[i].type
|
|
const secondType = list[i + 1].type
|
|
if (firstType === 'date' && secondType === 'date') {
|
|
continue
|
|
}
|
|
newList.push(list[i])
|
|
}
|
|
if (list.length > 0 && list[list.length - 1].type !== 'date') {
|
|
newList.push(list[list.length - 1])
|
|
}
|
|
setUpdatedList(newList)
|
|
}, [navigationList, searchField])
|
|
|
|
useEffect(() => {
|
|
setFastNavigation([
|
|
{
|
|
type: "custom",
|
|
component: <SearchBox
|
|
key="search-box"
|
|
altText={"search_by_name"}
|
|
handleOnChange={(e) => { setSearchField(e) }}
|
|
searchField={searchField}
|
|
/>
|
|
},
|
|
...updatedList
|
|
])
|
|
}, [searchField, setFastNavigation, updatedList])
|
|
|
|
const handleVoicePlay = useCallback((src) => {
|
|
if (!voiceOn) {
|
|
setVoiceSrc(null)
|
|
} else {
|
|
if (src === voiceSrc && lastVoiceState === 'ended') {
|
|
setVoiceReplay(true)
|
|
} else {
|
|
setVoiceSrc(src)
|
|
}
|
|
}
|
|
}, [voiceOn, voiceSrc])
|
|
|
|
return (
|
|
<section>
|
|
{
|
|
officalUpdate.length > operators.length && (
|
|
<section>
|
|
<section className={classes['offical-update']}>
|
|
<section className={classes.info}>
|
|
<section className={classes.content}>
|
|
<section className={classes.text}>{officalUpdate.length - operators.length} {i18n("new_op_wait_to_update")}</section>
|
|
<section className={`${classes['styled-selection']}`}>
|
|
{officalUpdate[officalUpdate.latest].map((entry, index) => {
|
|
return (
|
|
<Link
|
|
reloadDocument
|
|
to={entry.link}
|
|
target='_blank'
|
|
style={{
|
|
color: entry.color
|
|
}}
|
|
key={index}
|
|
>
|
|
<section className={classes.content}>
|
|
<section className={classes.option}>
|
|
<section className={classes.outline} />
|
|
<section className={`${classes.text} ${classes.container}`}>
|
|
<section className={classes.type}>
|
|
<CharIcon
|
|
type={entry.type}
|
|
viewBox={
|
|
entry.type === 'operator' ? '0 0 88.969 71.469' : '0 0 94.563 67.437'
|
|
} />
|
|
</section>
|
|
<section className={classes.title}>
|
|
{entry.codename[language]}
|
|
</section>
|
|
<seection className={classes['arrow-icon']}>
|
|
<section className={classes.bar}></section>
|
|
<section className={classes.bar}></section>
|
|
<section className={classes.bar}></section>
|
|
<section className={classes.bar}></section>
|
|
</seection>
|
|
</section>
|
|
</section>
|
|
</section>
|
|
</Link>
|
|
)
|
|
})}
|
|
</section>
|
|
</section>
|
|
|
|
</section>
|
|
<section className={classes.date}>{officalUpdate.latest}</section>
|
|
</section>
|
|
<Border />
|
|
</section>
|
|
)
|
|
}
|
|
{
|
|
content.map((v) => {
|
|
const length = v.filter((v) => isShown(v.type)).length
|
|
return (
|
|
<section key={v[0].date} hidden={length === 0}>
|
|
<section className={classes.group}>
|
|
<section className={classes['operator-group']}>
|
|
{v.map(item => {
|
|
return (
|
|
<OperatorElement
|
|
key={item.link}
|
|
item={item}
|
|
hidden={!isShown(item.type)}
|
|
handleVoicePlay={handleVoicePlay}
|
|
/>
|
|
)
|
|
})}
|
|
</section>
|
|
<section className={classes.date}>{v[0].date}</section>
|
|
</section>
|
|
<Border />
|
|
</section>
|
|
)
|
|
})
|
|
}
|
|
<VoiceSwitchElement
|
|
src={voiceSrc}
|
|
handleAduioStateChange={handleAduioStateChange}
|
|
replay={voiceReplay}
|
|
/>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function OperatorElement({ item, hidden, handleVoicePlay }) {
|
|
const { textDefaultLang, language, alternateLang } = useLanguage()
|
|
|
|
const onMouseEnter = useCallback(() => {
|
|
handleVoicePlay(`/${item.link}/assets/${JSON.parse(import.meta.env.VITE_VOICE_FOLDERS).main}/${import.meta.env.VITE_APP_VOICE_URL}`)
|
|
document.documentElement.style.setProperty('--cursor-color', item.color);
|
|
}, [handleVoicePlay, item.color, item.link])
|
|
|
|
const onMouseLeave = useCallback(() => {
|
|
document.documentElement.style.setProperty('--cursor-color', 'var(--text-color)');
|
|
}, [])
|
|
|
|
return useMemo(() => {
|
|
return (
|
|
<NavLink
|
|
to={`/${item.link}`}
|
|
className={classes.item}
|
|
hidden={hidden}
|
|
>
|
|
<section
|
|
onMouseEnter={() => onMouseEnter()}
|
|
onMouseLeave={() => onMouseLeave()}
|
|
>
|
|
<section className={classes['background-filler']} />
|
|
<section className={classes.outline} />
|
|
<section className={classes.img}>
|
|
<ImageElement
|
|
item={item}
|
|
/>
|
|
</section>
|
|
<section className={classes.info}>
|
|
<section className={classes.container}>
|
|
<section className={classes.title}>{item.codename[language]}</section>
|
|
<section className={classes.type}>
|
|
<CharIcon
|
|
type={item.type}
|
|
viewBox={
|
|
item.type === 'operator' ? '0 0 88.969 71.469' : '0 0 94.563 67.437'
|
|
} />
|
|
</section>
|
|
</section>
|
|
<section className={classes.wrapper}>
|
|
<span className={classes.text}>{item.codename[language.startsWith("en") ? alternateLang : textDefaultLang]}</span>
|
|
</section>
|
|
<section className={classes.background} style={{
|
|
color: item.color
|
|
}} />
|
|
</section>
|
|
</section>
|
|
</NavLink>
|
|
)
|
|
}, [item, hidden, language, alternateLang, textDefaultLang, onMouseEnter, onMouseLeave])
|
|
}
|
|
|
|
function VoiceSwitchElement({ src, replay, handleAduioStateChange }) {
|
|
const [voiceOn, setVoiceOn] = useAtom(voiceOnAtom)
|
|
const {
|
|
setExtraArea,
|
|
} = useAppbar()
|
|
|
|
useEffect(() => {
|
|
setExtraArea([
|
|
(
|
|
<Switch
|
|
key="voice"
|
|
text='voice'
|
|
on={voiceOn}
|
|
handleOnClick={() => setVoiceOn(!voiceOn)}
|
|
/>
|
|
)
|
|
])
|
|
}, [voiceOn, setExtraArea, setVoiceOn])
|
|
|
|
return (
|
|
<VoiceElement
|
|
src={src}
|
|
replay={replay}
|
|
handleAduioStateChange={handleAduioStateChange}
|
|
/>
|
|
)
|
|
}
|
|
|
|
VoiceSwitchElement.propTypes = {
|
|
src: PropTypes.string,
|
|
replay: PropTypes.bool,
|
|
handleAduioStateChange: PropTypes.func,
|
|
}
|
|
|
|
function ImageElement({ item }) {
|
|
const { language } = useLanguage()
|
|
return <img src={`/${item.link}/assets/${item.fallback_name.replace("#", "%23")}_portrait.png`} alt={item.codename[language]} />
|
|
}
|
|
ImageElement.propTypes = {
|
|
item: PropTypes.object.isRequired,
|
|
fallback_name: PropTypes.string,
|
|
codename: PropTypes.object,
|
|
} |