refactor(directory): add eslint

This commit is contained in:
Haoyu Xu
2023-03-02 23:53:46 -05:00
parent 1cbf3357d2
commit 5fc6f60b16
18 changed files with 1387 additions and 294 deletions

15
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,15 @@
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
'eslint:recommended',
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
}

View File

@@ -1,3 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
export default function CharIcon(props) { export default function CharIcon(props) {
return ( return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox={props.viewBox}> <svg xmlns="http://www.w3.org/2000/svg" viewBox={props.viewBox}>
@@ -8,6 +11,10 @@ export default function CharIcon(props) {
<path d="M90.4 50.6l-39.8-23.5v-4c0-4.5-5-6.5-5-6.5a5.4 5.4 0 012.2-10.1c2.7 0 5.3 1.5 5.5 4.8.4 5.3 6.4 3.9 6.4-.3a11.7 11.7 0 00-12-11c-9 0-11.6 8.8-11.6 11.6a11.5 11.5 0 001.6 6.2c2.2 3.8 6.6 4.3 6.6 6.8v2.5L4.2 50.7c-4 2.3-4.7 7.3-3.8 10.3a9.1 9.1 0 009.1 6.4h75.2c5.9 0 8.6-3.4 9.5-6.3C95 58.1 95 53.4 90.4 50.6Zm-5.6 10.3h-75.2c-2.4.1-4-3.3-1.5-4.8l39.2-22.9 39 22.8A2.7 2.7 0 0184.7 60.8Z" /> <path d="M90.4 50.6l-39.8-23.5v-4c0-4.5-5-6.5-5-6.5a5.4 5.4 0 012.2-10.1c2.7 0 5.3 1.5 5.5 4.8.4 5.3 6.4 3.9 6.4-.3a11.7 11.7 0 00-12-11c-9 0-11.6 8.8-11.6 11.6a11.5 11.5 0 001.6 6.2c2.2 3.8 6.6 4.3 6.6 6.8v2.5L4.2 50.7c-4 2.3-4.7 7.3-3.8 10.3a9.1 9.1 0 009.1 6.4h75.2c5.9 0 8.6-3.4 9.5-6.3C95 58.1 95 53.4 90.4 50.6Zm-5.6 10.3h-75.2c-2.4.1-4-3.3-1.5-4.8l39.2-22.9 39 22.8A2.7 2.7 0 0184.7 60.8Z" />
} }
</svg> </svg>
) )
} }
CharIcon.propTypes = {
viewBox: PropTypes.string,
type: PropTypes.string,
};

View File

@@ -1,6 +1,7 @@
import { import React, {
useState useState
} from 'react' } from 'react'
import PropTypes from 'prop-types';
import './dropdown.css' import './dropdown.css'
export default function Dropdown(props) { export default function Dropdown(props) {
@@ -47,3 +48,11 @@ export default function Dropdown(props) {
</> </>
) )
} }
Dropdown.propTypes = {
className: PropTypes.string,
text: PropTypes.string,
menu: PropTypes.array,
onClick: PropTypes.func,
activeColor: PropTypes.object,
activeRule: PropTypes.func,
};

View File

@@ -1,3 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import './main_border.css'; import './main_border.css';
export default function MainBorder(props) { export default function MainBorder(props) {
@@ -7,3 +9,6 @@ export default function MainBorder(props) {
</section> </section>
) )
} }
MainBorder.propTypes = {
children: PropTypes.node,
};

View File

@@ -1,27 +1,26 @@
import { import React, {
useState, useState,
useCallback
} from 'react' } from 'react'
import './popup.css' import './popup.css'
import ReturnButton from '@/component/return_button'; import ReturnButton from '@/component/return_button';
import MainBorder from '@/component/main_border'; import MainBorder from '@/component/main_border';
import PropTypes from 'prop-types';
export default function Popup(props) { export default function Popup(props) {
const [hidden, setHidden] = useState(true) const [hidden, setHidden] = useState(true)
const toggle = useCallback(() => { const toggle = () => {
setHidden(!hidden) setHidden(!hidden)
}, [hidden]) }
return ( return (<>
<>
<section className={`popup ${hidden ? '' : 'active'}`}> <section className={`popup ${hidden ? '' : 'active'}`}>
<section className='wrapper'> <section className='wrapper'>
<section className='title'> <section className='title'>
<section className="text">{props.title}</section> <section className="text">{props.title}</section>
<ReturnButton onClick={toggle} className="return-button"/> <ReturnButton onClick={toggle} className="return-button" />
</section> </section>
<MainBorder/> <MainBorder />
<section className='content'> <section className='content'>
{props.children} {props.children}
</section> </section>
@@ -35,6 +34,9 @@ export default function Popup(props) {
> >
{props.title} {props.title}
</span> </span>
</> </>)
)
} }
Popup.propTypes = {
title: PropTypes.string,
children: PropTypes.node,
};

View File

@@ -1,3 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import './return_button.css' import './return_button.css'
export default function ReturnButton(props) { export default function ReturnButton(props) {
@@ -31,3 +33,6 @@ export default function ReturnButton(props) {
</> </>
) )
} }
ReturnButton.propTypes = {
onClick: PropTypes.func,
};

View File

@@ -1,8 +1,13 @@
import { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './switch.css'; import './switch.css';
import {
useI18n
} from '@/state/language'
export default function Switch(props) { export default function Switch(props) {
const [on, setOn] = useState(props.on) const [on, setOn] = useState(props.on)
const { i18n } = useI18n()
useEffect(() => { useEffect(() => {
setOn(props.on) setOn(props.on)
@@ -13,7 +18,7 @@ export default function Switch(props) {
className={`switch ${on ? 'active' : ''}`} className={`switch ${on ? 'active' : ''}`}
onClick={() => props.handleOnClick()} onClick={() => props.handleOnClick()}
> >
<span className='text'>{props.text}</span> <span className='text'>{i18n(props.text)}</span>
<section className='icon-wrapper'> <section className='icon-wrapper'>
<span className='icon-line'></span> <span className='icon-line'></span>
<span className='icon'></span> <span className='icon'></span>
@@ -21,3 +26,8 @@ export default function Switch(props) {
</section> </section>
) )
} }
Switch.propTypes = {
on: PropTypes.bool,
text: PropTypes.string,
handleOnClick: PropTypes.func,
};

View File

@@ -1,3 +1,4 @@
import React from "react";
import { import {
useNavigate, useNavigate,
useRouteError useRouteError

View File

@@ -1,3 +1,4 @@
import React from "react";
import Home from "@/routes/path/home"; import Home from "@/routes/path/home";
import Operator from "@/routes/path/operator"; import Operator from "@/routes/path/operator";
import Changelogs from "@/routes/path/changelogs"; import Changelogs from "@/routes/path/changelogs";

View File

@@ -1,4 +1,4 @@
import { import React, {
useState, useState,
useEffect, useEffect,
useMemo useMemo
@@ -9,7 +9,8 @@ import { useAppbar } from '@/state/appbar';
import useUmami from '@parcellab/react-use-umami' import useUmami from '@parcellab/react-use-umami'
import MainBorder from '@/component/main_border'; import MainBorder from '@/component/main_border';
export default function Changelogs(props) { export default function Changelogs() {
// eslint-disable-next-line no-unused-vars
const _trackEvt = useUmami('/changelogs') const _trackEvt = useUmami('/changelogs')
const { const {
setTitle, setTitle,
@@ -29,7 +30,7 @@ export default function Changelogs(props) {
fetch('/_assets/changelogs.json').then(res => res.json()).then(data => { fetch('/_assets/changelogs.json').then(res => res.json()).then(data => {
setChangelogs(data) setChangelogs(data)
}) })
}, []) }, [setExtraArea, setHeaderIcon, setTitle])
useEffect(() => { useEffect(() => {
setTabs(changelogs.map((item) => { setTabs(changelogs.map((item) => {
@@ -37,7 +38,7 @@ export default function Changelogs(props) {
key: item[0].key key: item[0].key
} }
})) }))
}, [changelogs]) }, [changelogs, setTabs])
const content = useMemo(() => { const content = useMemo(() => {
return ( return (

View File

@@ -1,17 +1,17 @@
import { import React, {
useState, useState,
useEffect, useEffect,
useCallback, useCallback,
useMemo useMemo
} from 'react' } from 'react'
import PropTypes from 'prop-types';
import { import {
NavLink, NavLink,
} from "react-router-dom"; } from "react-router-dom";
import './home.css' import './home.css'
import { useConfig } from '@/state/config'; import { useConfig } from '@/state/config';
import { import {
useLanguage, useLanguage
useI18n
} from '@/state/language' } from '@/state/language'
import { useHeader } from '@/state/header'; import { useHeader } from '@/state/header';
import { useAppbar } from '@/state/appbar'; import { useAppbar } from '@/state/appbar';
@@ -58,6 +58,7 @@ const playVoice = (link) => {
} }
export default function Home() { export default function Home() {
// eslint-disable-next-line no-unused-vars
const _trackEvt = useUmami('/') const _trackEvt = useUmami('/')
const { const {
setTitle, setTitle,
@@ -78,7 +79,7 @@ export default function Home() {
key: 'skin' key: 'skin'
}]) }])
setHeaderIcon(null) setHeaderIcon(null)
}, []) }, [setHeaderIcon, setTabs, setTitle])
useEffect(() => { useEffect(() => {
setContent(config?.operators || []) setContent(config?.operators || [])
@@ -157,7 +158,7 @@ function OperatorElement({ item, hidden }) {
</section> </section>
</NavLink> </NavLink>
) )
}, [language, alternateLang, hidden]) }, [item, hidden, language, alternateLang, textDefaultLang])
} }
function VoiceSwitchElement() { function VoiceSwitchElement() {
@@ -165,28 +166,19 @@ function VoiceSwitchElement() {
const { const {
setExtraArea, setExtraArea,
} = useAppbar() } = useAppbar()
const { i18n } = useI18n()
const toggleVoice = useCallback(() => { useEffect(() => {
setVoiceOn(!voiceOn) setExtraArea([
}, [voiceOn])
const appbarSwitch = useMemo(() => {
return [
( (
<Switch <Switch
key="voice" key="voice"
text={i18n('voice')} text='voice'
on={voiceOn} on={voiceOn}
handleOnClick={() => toggleVoice()} handleOnClick={() => setVoiceOn(!voiceOn)}
/> />
) )
] ])
}, [voiceOn]) }, [voiceOn, setExtraArea, setVoiceOn])
useEffect(() => {
setExtraArea(appbarSwitch)
}, [voiceOn])
return null return null
} }
@@ -195,3 +187,8 @@ function ImageElement({ item }) {
const { language } = useLanguage() const { language } = useLanguage()
return <img src={`/${item.link}/assets/${item.fallback_name.replace("#", "%23")}_portrait.png`} alt={item.codename[language]} /> 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,
}

View File

@@ -1,8 +1,9 @@
import { import React, {
useState, useState,
useEffect, useEffect,
useRef, useRef,
useCallback useCallback,
useMemo
} from 'react' } from 'react'
import { import {
useParams, useParams,
@@ -33,7 +34,17 @@ const spinePlayerAtom = atom(null);
const spineAnimationAtom = atom("Idle"); const spineAnimationAtom = atom("Idle");
const audioEl = new Audio() const audioEl = new Audio()
export default function Operator(props) { const getTabName = (item, language) => {
if (item.type === 'operator') {
return 'operator'
} else {
return item.codename[language].replace(/^(.+)( )(·|\/)()(.+)$/, '$1')
}
}
// TODO: fix subtitle show/hide, fix voice play/pause when change route
export default function Operator() {
const navigate = useNavigate() const navigate = useNavigate()
const { operators } = useConfig() const { operators } = useConfig()
const { language } = useLanguage() const { language } = useLanguage()
@@ -46,13 +57,15 @@ export default function Operator(props) {
} = useHeader() } = useHeader()
const [config, setConfig] = useAtom(configAtom) const [config, setConfig] = useAtom(configAtom)
const [spineData, setSpineData] = useState(null) const [spineData, setSpineData] = useState(null)
// eslint-disable-next-line no-unused-vars
const _trackEvt = useUmami(`/${key}`) const _trackEvt = useUmami(`/${key}`)
const spineRef = useRef(null) const spineRef = useRef(null)
const [spineAnimation, setSpineAnimation] = useAtom(spineAnimationAtom) const [spineAnimation, setSpineAnimation] = useAtom(spineAnimationAtom)
const { i18n } = useI18n() const { i18n } = useI18n()
const [spinePlayer, setSpinePlayer] = useAtom(spinePlayerAtom) const [spinePlayer, setSpinePlayer] = useAtom(spinePlayerAtom)
const [voiceLang, setVoiceLang] = useState(null) const [voiceLang, setVoiceLang] = useState(null)
const { backgrounds, currentBackground, setCurrentBackground } = useBackgrounds() const { backgrounds } = useBackgrounds()
const [currentBackground, setCurrentBackground] = useState(null)
const [voiceConfig, setVoiceConfig] = useState(null) const [voiceConfig, setVoiceConfig] = useState(null)
const [subtitleLang, setSubtitleLang] = useState(null) const [subtitleLang, setSubtitleLang] = useState(null)
const [subtitle, setSubtitle] = useState(null) const [subtitle, setSubtitle] = useState(null)
@@ -63,7 +76,11 @@ export default function Operator(props) {
useEffect(() => { useEffect(() => {
setAppbarExtraArea([]) setAppbarExtraArea([])
}, []) }, [setAppbarExtraArea])
useEffect(() => {
if (backgrounds) setCurrentBackground(backgrounds[0])
}, [backgrounds])
useEffect(() => { useEffect(() => {
setSpineData(null) setSpineData(null)
@@ -81,18 +98,10 @@ export default function Operator(props) {
setVoiceConfig(data) setVoiceConfig(data)
}) })
} }
}, [operators, key]) }, [operators, key, setHeaderIcon, setConfig])
const getTabName = (item) => { const coverToTab = useCallback((item, language) => {
if (item.type === 'operator') { const key = getTabName(item, language)
return 'operator'
} else {
return item.codename[language].replace(/^(.+)( )(·|\/)()(.+)$/, '$1')
}
}
const coverToTab = (item) => {
const key = getTabName(item)
return { return {
key: key, key: key,
style: { style: {
@@ -103,30 +112,31 @@ export default function Operator(props) {
navigate(`/${item.link}`) navigate(`/${item.link}`)
} }
} }
} }, [navigate])
const getOtherEntries = () => { const otherEntries = useMemo(() => {
if (!config || !language) return null
return operators.filter((item) => item.id === config.id && item.link !== config.link).map((item) => { return operators.filter((item) => item.id === config.id && item.link !== config.link).map((item) => {
return coverToTab(item) return coverToTab(item, language)
}) })
} }, [config, language, operators, coverToTab])
useEffect(() => { useEffect(() => {
if (config) { if (config) {
setTabs( setTabs(
[ [
coverToTab(config), coverToTab(config, language),
...getOtherEntries() ...otherEntries
] ]
) )
} }
}, [config, language, key]) }, [config, key, coverToTab, setTabs, otherEntries, language])
useEffect(() => { useEffect(() => {
if (config) { if (config) {
setTitle(config.codename[language]) setTitle(config.codename[language])
} }
}, [config, language, key]) }, [config, language, key, setTitle])
useEffect(() => { useEffect(() => {
if (spineRef.current?.children.length === 0 && spineData && config) { if (spineRef.current?.children.length === 0 && spineData && config) {
@@ -153,68 +163,68 @@ export default function Operator(props) {
defaultMix: 0, defaultMix: 0,
})) }))
} }
}, [spineData]); }, [spineData, setSpinePlayer, spineAnimation, config]);
useEffect(() => { const subtitleObj = useMemo(() => {
if (voiceConfig && voiceLang) { if (voiceConfig && voiceLang) {
let subtitleObj = voiceConfig.subtitleLangs[subtitleLang || 'zh-CN'] let subtitleObj = voiceConfig.subtitleLangs[subtitleLang || 'zh-CN']
let subtitleKey = 'default' let subtitleKey = 'default'
if (subtitleObj[voiceLang]) { if (subtitleObj[voiceLang]) {
subtitleKey = voiceLang subtitleKey = voiceLang
} }
subtitleObj = subtitleObj[subtitleKey] return subtitleObj[subtitleKey]
const playVoice = () => { }
}, [subtitleLang, voiceConfig, voiceLang])
const handleClickPlay = useCallback(() => {
if (!voiceLang) return
const voiceId = () => { const voiceId = () => {
const keys = Object.keys(subtitleObj) const keys = Object.keys(subtitleObj)
const id = keys[Math.floor((Math.random() * keys.length))] const id = keys[Math.floor((Math.random() * keys.length))]
return id === lastVoiceId ? voiceId() : id return id === lastVoiceId ? voiceId() : id
} }
const id = voiceId() const id = voiceId()
setLastVoiceId(id) setLastVoiceId(currentVoiceId)
setCurrentVoiceId(id) setCurrentVoiceId(id)
audioEl.src = `/${config.link}/assets/${getVoiceFoler(voiceLang)}/${id}.ogg` audioEl.src = `/${config.link}/assets/${getVoiceFoler(voiceLang)}/${id}.ogg`
let startPlayPromise = audioEl.play() let startPlayPromise = audioEl.play()
setIsVoicePlaying(true)
if (startPlayPromise !== undefined) { if (startPlayPromise !== undefined) {
startPlayPromise startPlayPromise
.then(() => { .then(() => {
setIsVoicePlaying(true)
const audioEndedFunc = () => { const audioEndedFunc = () => {
setIsVoicePlaying(false)
audioEl.removeEventListener('ended', audioEndedFunc) audioEl.removeEventListener('ended', audioEndedFunc)
if (currentVoiceId !== id) return if (currentVoiceId !== id) return
setSubtitle(null) setIsVoicePlaying(false)
} }
if (subtitleLang) showSubtitle(id)
audioEl.addEventListener('ended', audioEndedFunc) audioEl.addEventListener('ended', audioEndedFunc)
}) })
.catch(() => { .catch(() => {
return return
}) })
} }
} }, [voiceLang, lastVoiceId, config, currentVoiceId, subtitleObj])
const showSubtitle = (id) => {
setSubtitle(subtitleObj[id])
setHideSubtitle(false)
}
spineRef.current?.addEventListener('click', playVoice)
return () => {
spineRef.current?.removeEventListener('click', playVoice)
}
}
}, [voiceLang, spineRef, voiceConfig, subtitleLang, lastVoiceId])
useEffect(() => { useEffect(() => {
if (!isVoicePlaying && !hideSubtitle) { if (subtitleLang) {
const hideSubtitle = () => { if (isVoicePlaying) {
setHideSubtitle(false)
setSubtitle(subtitleObj[currentVoiceId])
} else {
const autoHide = () => {
if (isVoicePlaying) return
setHideSubtitle(true) setHideSubtitle(true)
} }
setTimeout(hideSubtitle, 5 * 1000) setTimeout(autoHide, 5 * 1000)
return () => { return () => {
clearTimeout(hideSubtitle) clearTimeout(autoHide)
} }
// setHideSubtitle(true)
} }
}, [isVoicePlaying, hideSubtitle]) } else {
setHideSubtitle(true)
}
}, [subtitleLang, currentVoiceId, isVoicePlaying, subtitleObj])
const spineSettings = [ const spineSettings = [
{ {
@@ -377,7 +387,7 @@ export default function Operator(props) {
</section> </section>
</section> </section>
</section> </section>
<section className="spine-container" style={{ <section className="spine-container" style={currentBackground && {
backgroundImage: `url(/${key}/assets/${import.meta.env.VITE_BACKGROUND_FOLDER}/${currentBackground})` backgroundImage: `url(/${key}/assets/${import.meta.env.VITE_BACKGROUND_FOLDER}/${currentBackground})`
}} > }} >
{ {
@@ -385,7 +395,7 @@ export default function Operator(props) {
<img src={`/${config.link}/assets/${config.logo}.png`} alt={config?.codename[language]} className='operator-logo'/> <img src={`/${config.link}/assets/${config.logo}.png`} alt={config?.codename[language]} className='operator-logo'/>
) )
} }
<section ref={spineRef} /> <section ref={spineRef} onClick={handleClickPlay} />
{ {
subtitle && ( subtitle && (
<section className={`voice-wrapper${hideSubtitle ? '' : ' active'}`}> <section className={`voice-wrapper${hideSubtitle ? '' : ' active'}`}>

View File

@@ -1,9 +1,10 @@
import { import React, {
useState, useState,
useEffect, useEffect,
useMemo, useMemo,
useCallback useCallback
} from 'react' } from 'react'
import PropTypes from 'prop-types';
import { import {
Outlet, Outlet,
Link, Link,
@@ -26,22 +27,20 @@ import ReturnButton from '@/component/return_button';
import MainBorder from '@/component/main_border'; import MainBorder from '@/component/main_border';
import CharIcon from '@/component/char_icon'; import CharIcon from '@/component/char_icon';
export default function Root(props) { const currentYear = new Date().getFullYear()
export default function Root() {
const [drawerHidden, setDrawerHidden] = useState(true) const [drawerHidden, setDrawerHidden] = useState(true)
const { textDefaultLang, language, alternateLang } = useLanguage()
const { const {
title, title,
tabs, tabs,
currentTab, setCurrentTab, setCurrentTab,
headerIcon headerIcon
} = useHeader() } = useHeader()
const { const {
extraArea, extraArea,
} = useAppbar() } = useAppbar()
const { version, fetchConfig, fetchVersion } = useConfig() const { fetchConfig, fetchVersion } = useConfig()
const [drawerDestinations, setDrawerDestinations] = useState(null)
const currentYear = useMemo(() => new Date().getFullYear(), [])
const { i18n } = useI18n()
const { fetchBackgrounds } = useBackgrounds() const { fetchBackgrounds } = useBackgrounds()
const headerTabs = useMemo(() => { const headerTabs = useMemo(() => {
@@ -61,8 +60,124 @@ export default function Root(props) {
setDrawerHidden(value || !drawerHidden) setDrawerHidden(value || !drawerHidden)
}, [drawerHidden]) }, [drawerHidden])
const renderDrawerDestinations = () => { useEffect(() => {
return routes.filter((item) => item.inDrawer).map((item) => { if (tabs.length > 0) {
setCurrentTab(tabs[0].key)
} else {
setCurrentTab(null)
}
}, [setCurrentTab, tabs])
useEffect(() => {
fetchConfig()
fetchVersion()
fetchBackgrounds()
}, [fetchBackgrounds, fetchConfig, fetchVersion])
return (
<>
<header className='header'>
<section
className={`navButton ${drawerHidden ? '' : 'active'}`}
onClick={() => toggleDrawer()}
>
<section className='bar'></section>
<section className='bar'></section>
<section className='bar'></section>
</section>
<section className='spacer' />
{extraArea}
<LanguageDropdown />
</header>
<nav className={`drawer ${drawerHidden ? '' : 'active'}`}>
<section
className='links'
>
<DrawerDestinations
toggleDrawer={toggleDrawer}
/>
</section>
<section
className={`overlay ${drawerHidden ? '' : 'active'}`}
onClick={() => toggleDrawer()}
/>
</nav>
<main className='main'>
<section className='main-header'>
<section className='main-title'>
{headerIcon && (
<section className='main-icon'>
<CharIcon
type={headerIcon}
viewBox={
headerIcon === 'operator' ? '0 0 88.969 71.469' : '0 0 94.563 67.437'
}
/>
</section>
)}
{title}
</section>
<section className='main-tab'>
{headerTabs}
</section>
</section>
<HeaderReturnButton />
<Outlet />
</main>
<FooterElement />
</>
)
}
function FooterElement() {
const { i18n } = useI18n()
const { version } = useConfig()
return useMemo(() => {
return (
<footer className='footer'>
<section className='links section'>
<section className="item">
<Popup
className='link'
title={i18n('disclaimer')}
>
{i18n('disclaimer_content')}
</Popup>
</section>
<section className="item">
<Link reloadDocument to="https://privacy.halyul.dev" target="_blank" className='link'>{i18n('privacy_policy')}</Link>
</section>
<section className="item">
<Link reloadDocument to="https://github.com/Halyul/aklive2d" target="_blank" className='link'>GitHub</Link>
</section>
<section className="item">
<Popup
className='link'
title={i18n('contact_us')}
>
ak#halyul.dev
</Popup>
</section>
</section>
<section className='copyright section'>
<span>Spine Runtimes © 2013 - 2019 Esoteric Software LLC</span>
<span>Assets © 2017 - {currentYear} Arknights/Hypergryph Co., Ltd</span>
<span>Source Code © 2021 - {currentYear} Halyul</span>
<span>Directory @ {version.directory}</span>
<span>Showcase @ {version.showcase}</span>
</section>
</footer>
)
}, [i18n, version.directory, version.showcase])
}
function DrawerDestinations({ toggleDrawer }) {
const { i18n } = useI18n()
const { textDefaultLang, alternateLang } = useLanguage()
return (
routes.filter((item) => item.inDrawer).map((item) => {
if (typeof item.element.type === 'string') { if (typeof item.element.type === 'string') {
return ( return (
<Link reloadDocument <Link reloadDocument
@@ -98,108 +213,6 @@ export default function Root(props) {
) )
} }
}) })
}
useEffect(() => {
setDrawerDestinations(renderDrawerDestinations())
}, [alternateLang])
useEffect(() => {
if (tabs.length > 0) {
setCurrentTab(tabs[0].key)
} else {
setCurrentTab(null)
}
}, [tabs])
useEffect(() => {
fetchConfig()
fetchVersion()
fetchBackgrounds()
}, [])
return (
<>
<header className='header'>
<section
className={`navButton ${drawerHidden ? '' : 'active'}`}
onClick={() => toggleDrawer()}
>
<section className='bar'></section>
<section className='bar'></section>
<section className='bar'></section>
</section>
<section className='spacer' />
{extraArea}
<LanguageDropdown />
</header>
<nav className={`drawer ${drawerHidden ? '' : 'active'}`}>
<section
className='links'
>
{drawerDestinations}
</section>
<section
className={`overlay ${drawerHidden ? '' : 'active'}`}
onClick={() => toggleDrawer()}
/>
</nav>
<main className='main'>
<section className='main-header'>
<section className='main-title'>
{headerIcon && (
<section className='main-icon'>
<CharIcon
type={headerIcon}
viewBox={
headerIcon === 'operator' ? '0 0 88.969 71.469' : '0 0 94.563 67.437'
}
/>
</section>
)}
{title}
</section>
<section className='main-tab'>
{headerTabs}
</section>
</section>
<HeaderReturnButton />
<Outlet />
</main>
<footer className='footer'>
<section className='links section'>
<section className="item">
<Popup
className='link'
title={i18n('disclaimer')}
>
{i18n('disclaimer_content')}
</Popup>
</section>
<section className="item">
<Link reloadDocument to="https://privacy.halyul.dev" target="_blank" className='link'>{i18n('privacy_policy')}</Link>
</section>
<section className="item">
<Link reloadDocument to="https://github.com/Halyul/aklive2d" target="_blank" className='link'>GitHub</Link>
</section>
<section className="item">
<Popup
className='link'
title={i18n('contact_us')}
>
ak#halyul.dev
</Popup>
</section>
</section>
<section className='copyright section'>
<span>Spine Runtimes © 2013 - 2019 Esoteric Software LLC</span>
<span>Assets © 2017 - {currentYear} Arknights/Hypergryph Co., Ltd</span>
<span>Source Code © 2021 - {currentYear} Halyul</span>
<span>Directory @ {version.directory}</span>
<span>Showcase @ {version.showcase}</span>
</section>
</footer>
</>
) )
} }
@@ -207,6 +220,7 @@ function LanguageDropdown() {
const { language, setLanguage } = useLanguage() const { language, setLanguage } = useLanguage()
const { i18n, i18nValues } = useI18n() const { i18n, i18nValues } = useI18n()
return useMemo(() => {
return ( return (
<Dropdown <Dropdown
text={i18n(language)} text={i18n(language)}
@@ -221,6 +235,7 @@ function LanguageDropdown() {
}} }}
/> />
) )
}, [i18n, i18nValues.available, language, setLanguage])
} }
function HeaderTabsElement({ item }) { function HeaderTabsElement({ item }) {
@@ -241,30 +256,24 @@ function HeaderTabsElement({ item }) {
<section className='main-tab-text-wrapper'> <section className='main-tab-text-wrapper'>
<span className='text'>{i18n(item.key)}</span> <span className='text'>{i18n(item.key)}</span>
</section> </section>
</section> </section>
) )
} }
HeaderTabsElement.propTypes = {
item: PropTypes.object.isRequired,
}
function HeaderReturnButton() { function HeaderReturnButton() {
const navigate = useNavigate() const navigate = useNavigate()
const onClick = useCallback(() => { return useMemo(() => {
navigate("/")
}, [])
const children = useMemo(() => {
return (
<ReturnButton
className='return-button'
onClick={onClick}
/>
)
}, [])
return ( return (
<MainBorder> <MainBorder>
{children} <ReturnButton
className='return-button'
onClick={() => navigate("/")}
/>
</MainBorder> </MainBorder>
) )
}, [navigate])
} }

View File

@@ -1,25 +1,19 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { atom, useAtom } from 'jotai'; import { atom, useAtom } from 'jotai';
const fetcher = (...args) => fetch(...args).then(res => res.json())
const backgroundsAtom = atom([]); const backgroundsAtom = atom([]);
const currentBackgroundAtom = atom(null);
export function useBackgrounds() { export function useBackgrounds() {
const [backgrounds, setBackgrounds] = useAtom(backgroundsAtom); const [backgrounds, setBackgrounds] = useAtom(backgroundsAtom);
const [currentBackground, setCurrentBackground] = useAtom(currentBackgroundAtom)
const fetchBackgrounds = useCallback(async () => { const fetchBackgrounds = useCallback(async () => {
const res = await fetch('/_assets/backgrounds.json') const res = await fetch('/_assets/backgrounds.json')
const data = await res.json() const data = await res.json()
setBackgrounds(data) setBackgrounds(data)
setCurrentBackground(data[0]) }, [setBackgrounds])
}, [])
return { return {
backgrounds, backgrounds,
currentBackground,
setCurrentBackground,
fetchBackgrounds fetchBackgrounds
}; };
} }

View File

@@ -19,13 +19,13 @@ export function useConfig() {
operatorsList = [...operatorsList, ...item] operatorsList = [...operatorsList, ...item]
}) })
setOperators(operatorsList) setOperators(operatorsList)
}, []) }, [setConfig, setOperators])
const fetchVersion = useCallback(async () => { const fetchVersion = useCallback(async () => {
const res = await fetch('/_assets/version.json') const res = await fetch('/_assets/version.json')
const data = await res.json() const data = await res.json()
setVersion(data); setVersion(data);
}, []) }, [setVersion])
return { config, version, operators, fetchConfig, fetchVersion }; return { config, version, operators, fetchConfig, fetchVersion };
} }

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { atom, useAtom } from 'jotai'; import { atom, useAtom } from 'jotai';
import { useLanguage, useI18n } from "@/state/language" import { useI18n } from "@/state/language"
const keyAtom = atom(''); const keyAtom = atom('');
const titleAtom = atom(''); const titleAtom = atom('');
@@ -17,13 +17,12 @@ export function useHeader() {
const [appbarExtraArea, setAppbarExtraArea] = useAtom(appbarExtraAreaAtom); const [appbarExtraArea, setAppbarExtraArea] = useAtom(appbarExtraAreaAtom);
const [headerIcon, setHeaderIcon] = useAtom(headerIconAtom); const [headerIcon, setHeaderIcon] = useAtom(headerIconAtom);
const { i18n } = useI18n() const { i18n } = useI18n()
const { language } = useLanguage()
useEffect(() => { useEffect(() => {
const newTitle = i18n(key) const newTitle = i18n(key)
document.title = `${newTitle} - ${import.meta.env.VITE_APP_TITLE}`; document.title = `${newTitle} - ${import.meta.env.VITE_APP_TITLE}`;
setRealTitle(newTitle) setRealTitle(newTitle)
}, [key, language]) }, [i18n, key, setRealTitle])
return { return {
title, setTitle, title, setTitle,

View File

@@ -25,6 +25,10 @@
"@types/react": "^18.0.28", "@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@vitejs/plugin-react-swc": "^3.2.0", "@vitejs/plugin-react-swc": "^3.2.0",
"eslint": "^8.35.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prop-types": "^15.8.1",
"rollup": "^3.17.3", "rollup": "^3.17.3",
"vite": "^4.1.4" "vite": "^4.1.4"
}, },

1032
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff