chore: moved to a new branch to save space

This commit is contained in:
Haoyu Xu
2023-03-16 21:49:29 -04:00
commit 6ef70824a1
116 changed files with 23521 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
import React, {
useState,
useEffect,
useMemo,
useRef,
useCallback
} from "react";
import {
useNavigate,
useRouteError
} from "react-router-dom";
import header from '@/scss/root/header.module.scss'
import classes from '@/scss/error/Error.module.scss'
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils';
import Switch from '@/component/switch';
import ReturnButton from "@/component/return_button";
import { Typewriter } from 'react-simple-typewriter'
import { useHeader } from '@/state/header';
import VoiceElement from '@/component/voice';
import spine from '!/libs/spine-player'
import '!/libs/spine-player.css'
import useUmami from '@parcellab/react-use-umami';
const voiceOnAtom = atomWithStorage('voiceOn', false)
const config = JSON.parse(import.meta.env.VITE_ERROR_FILES)
const obj = config.files[Math.floor((Math.random() * config.files.length))]
const filename = obj.key.replace("#", "%23")
const padding = obj.paddings
let lastVoiceState = 'ended'
export default function Error() {
// eslint-disable-next-line no-unused-vars
const _trackEvt = useUmami('/error')
const error = useRouteError();
const navigate = useNavigate();
const {
setTitle,
} = useHeader()
const [voiceOn, setVoiceOn] = useAtom(voiceOnAtom)
const [spineDone, _setSpineDone] = useState(false)
const spineRef = useRef(null)
const [spineData, setSpineData] = useState(null)
const spineDoneRef = useRef(spineDone)
const voiceOnRef = useRef(voiceOn)
const [voiceSrc, setVoiceSrc] = useState(null)
const [voiceReplay, setVoiceReplay] = useState(false)
const [spinePlayer, setSpinePlayer] = useState(null)
const setSpineDone = (data) => {
spineDoneRef.current = data
_setSpineDone(data)
}
const content = useMemo(() => ['エラー発生。', '发生错误。', 'Error occured.', '에러 발생.', '發生錯誤。'], [])
useEffect(() => {
console.log(error)
fetch(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/${filename}.json`).then(res => res.json()).then(data => {
setSpineData(data)
})
}, [error])
useEffect(() => {
setTitle(content[0])
}, [content, setTitle])
useEffect(() => {
if (!voiceOn) {
setVoiceSrc(null)
} else {
setVoiceSrc(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/error.ogg`)
if (spinePlayer) {
spinePlayer.animationState.setAnimation(0, "Interact", false, 0);
spinePlayer.animationState.addAnimation(0, "Relax", true, 0);
}
}
}, [voiceOn])
useEffect(() => {
voiceOnRef.current = voiceOn
}, [voiceOn])
const playVoice = useCallback(() => {
if (lastVoiceState === 'ended' && voiceSrc !== null) {
setVoiceReplay(true)
}
}, [voiceSrc])
const handleAduioStateChange = useCallback((e, state) => {
lastVoiceState = state
if (state === 'ended') {
setVoiceReplay(false)
}
}, [])
useEffect(() => {
if (spineRef.current?.children.length === 0 && spineData) {
setSpinePlayer(new spine.SpinePlayer(spineRef.current, {
skelUrl: `./assets/${filename}.skel`,
atlasUrl: `./assets/${filename}.atlas`,
rawDataURIs: spineData,
animation: 'Relax',
premultipliedAlpha: true,
alpha: true,
backgroundColor: "#00000000",
viewport: {
debugRender: false,
padLeft: `${padding.left}%`,
padRight: `${padding.right}%`,
padTop: `${padding.top}%`,
padBottom: `${padding.bottom}%`,
x: 0,
y: 0,
},
showControls: false,
touch: false,
fps: 60,
defaultMix: 0.3,
success: (player) => {
let isPlayingInteract = false
player.animationState.addListener({
end: (e) => {
if (e.animation.name == "Interact") {
isPlayingInteract = false;
}
}
});
setSpineDone(true)
const ani = () => {
if (isPlayingInteract) {
return;
}
isPlayingInteract = true;
player.animationState.setAnimation(0, "Interact", false, 0);
player.animationState.addAnimation(0, "Relax", true, 0);
if (voiceOnRef.current) playVoice()
}
ani()
player.canvas.onclick = () => {
ani()
}
player.canvas.onmouseenter = () => {
ani()
}
}
}))
}
}, [playVoice, spineData]);
return (
<section className={classes.error}>
<header className={`${header.header} ${classes.header}`}>
<ReturnButton
onClick={() => navigate(-1, { replace: true })}
/>
<Switch
key="voice"
text='voice'
on={voiceOn}
handleOnClick={() => setVoiceOn(!voiceOn)}
/>
</header>
<main className={classes.main}>
{
content.map((item, index) => {
return (
<section key={index} className={classes.content}>
<Typewriter
words={[item]}
cursor
cursorStyle='|'
typeSpeed={100}
/>
</section>
)
})
}
<section
className={`${classes.spine} ${spineDone ? classes.active : ''}`}
ref={spineRef}
/>
<VoiceElement
src={voiceSrc}
replay={voiceReplay}
handleAduioStateChange={handleAduioStateChange}
/>
</main>
</section>
);
}

View File

@@ -0,0 +1,291 @@
import React, {
useState,
useEffect,
useMemo,
useCallback
} from 'react'
import PropTypes from 'prop-types';
import {
Outlet,
Link,
NavLink,
useNavigate,
ScrollRestoration
} from "react-router-dom";
import classes from '@/scss/root/Root.module.scss'
import header from '@/scss/root/header.module.scss'
import footer from '@/scss/root/footer.module.scss'
import drawer from '@/scss/root/drawer.module.scss'
import routes from '@/routes'
import { useConfig } from '@/state/config';
import { useHeader } from '@/state/header';
import { useAppbar } from '@/state/appbar';
import {
useI18n,
useLanguage,
} from '@/state/language'
import { useBackgrounds } from '@/state/background';
import Dropdown from '@/component/dropdown';
import Popup from '@/component/popup';
import ReturnButton from '@/component/return_button';
import Border from '@/component/border';
import CharIcon from '@/component/char_icon';
const currentYear = new Date().getFullYear()
export default function Root() {
const [drawerHidden, setDrawerHidden] = useState(true)
const {
title,
tabs,
setCurrentTab,
headerIcon
} = useHeader()
const {
extraArea,
} = useAppbar()
const { fetchConfig, fetchVersion } = useConfig()
const { fetchBackgrounds } = useBackgrounds()
const headerTabs = useMemo(() => {
return (
tabs?.map((item) => {
return (
<HeaderTabsElement
key={item.key}
item={item}
/>
)
})
)
}, [tabs])
const toggleDrawer = useCallback((value) => {
setDrawerHidden(value || !drawerHidden)
}, [drawerHidden])
useEffect(() => {
if (tabs.length > 0) {
setCurrentTab(tabs[0].key)
} else {
setCurrentTab(null)
}
}, [setCurrentTab, tabs])
useEffect(() => {
fetchConfig()
fetchVersion()
fetchBackgrounds()
}, [fetchBackgrounds, fetchConfig, fetchVersion])
return (
<>
<header className={header.header}>
<section
className={`${header.navButton} ${drawerHidden ? '' : header.active}`}
onClick={() => toggleDrawer()}
>
<section className={header.bar} />
<section className={header.bar} />
<section className={header.bar} />
</section>
<section className={header.spacer} />
<section className={header['extra-area']}>
{extraArea}
<LanguageDropdown />
</section>
</header>
<nav className={`${drawer.drawer} ${drawerHidden ? '' : drawer.active}`}>
<section
className={drawer.links}
>
<DrawerDestinations
toggleDrawer={toggleDrawer}
/>
</section>
<section
className={`${drawer.overlay} ${drawerHidden ? '' : drawer.active}`}
onClick={() => toggleDrawer()}
/>
</nav>
<main className={classes.main}>
<section className={classes.header}>
<section className={classes.title}>
{headerIcon && (
<section className={classes.icon}>
<CharIcon
type={headerIcon}
viewBox={
headerIcon === 'operator' ? '0 0 88.969 71.469' : '0 0 94.563 67.437'
}
/>
</section>
)}
{title}
</section>
<section className={classes.tab}>
{headerTabs}
</section>
</section>
<HeaderReturnButton />
<Outlet />
<ScrollRestoration />
</main>
<FooterElement />
</>
)
}
function FooterElement() {
const { i18n } = useI18n()
const { version } = useConfig()
const navigate = useNavigate()
return useMemo(() => {
return (
<footer className={footer.footer}>
<section className={`${footer.links} ${footer.section}`}>
<section className={footer.item}>
<Popup
className={footer.link}
title={i18n('disclaimer')}
>
{i18n('disclaimer_content')}
</Popup>
</section>
<section className={footer.item}>
<Link reloadDocument to="https://privacy.halyul.dev" target="_blank" className={footer.link}>{i18n('privacy_policy')}</Link>
</section>
<section className={footer.item}>
<Link reloadDocument to="https://github.com/Halyul/aklive2d" target="_blank" className={footer.link}>GitHub</Link>
</section>
<section className={footer.item}>
<Popup
className={footer.link}
title={i18n('contact_us')}
>
ak#halyul.dev
</Popup>
</section>
</section>
<section className={`${footer.copyright} ${footer.section}`} onDoubleClick={() => {
navigate('/error')
}}>
<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, navigate, 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') {
return (
<Link reloadDocument
key={item.name}
to={item.path}
target="_blank"
className={drawer.link}
onClick={() => toggleDrawer(false)}
>
<section>
{i18n(item.name, textDefaultLang)}
</section>
<section>
{i18n(item.name, alternateLang)}
</section>
</Link>
)
} else {
return (
<NavLink
to={item.path}
key={item.name}
className={({ isActive, }) =>
`${drawer.link} ${isActive ? drawer.active : ''}`
}
onClick={() => toggleDrawer(false)}
>
<section>
{i18n(item.name, textDefaultLang)}
</section>
<section>
{i18n(item.name, alternateLang)}
</section>
</NavLink>
)
}
})
)
}
function LanguageDropdown() {
const { language, setLanguage } = useLanguage()
const { i18n, i18nValues } = useI18n()
return useMemo(() => {
return (
<Dropdown
text={i18n(language)}
menu={i18nValues.available.map((item) => {
return {
name: i18n(item),
value: item
}
})}
onClick={(item) => {
setLanguage(item.value)
}}
/>
)
}, [i18n, i18nValues.available, language, setLanguage])
}
function HeaderTabsElement({ item }) {
const {
currentTab, setCurrentTab,
} = useHeader()
const { i18n } = useI18n()
return (
<section
className={`${classes.item} ${currentTab === item.key ? classes.active : ''}`}
onClick={(e) => {
setCurrentTab(item.key)
item.onClick && item.onClick(e, currentTab)
}}
style={item.style}
>
<section className={classes['text-wrapper']}>
<span>{i18n(item.key)}</span>
</section>
</section>
)
}
HeaderTabsElement.propTypes = {
item: PropTypes.object.isRequired,
}
function HeaderReturnButton() {
const navigate = useNavigate()
return useMemo(() => {
return (
<Border>
<ReturnButton
className={classes['return-button']}
onClick={() => navigate("/")}
/>
</Border>
)
}, [navigate])
}

View File

@@ -0,0 +1,36 @@
import React from "react";
import Home from "@/routes/path/Home";
import Operator from "@/routes/path/Operator";
import Changelogs from "@/routes/path/Changelogs";
export default [
{
path: "/",
index: true,
name: "home",
element: <Home />,
inDrawer: true,
routeable: true
}, {
path: "changelogs",
index: false,
name: "changelogs",
element: <Changelogs />,
inDrawer: true,
routeable: true
}, {
path: "https://ak.hypergryph.com/archive/dynamicCompile/",
index: false,
name: "offical_page",
element: <a/>,
inDrawer: true,
routeable: false
}, {
path: ":key",
index: false,
name: "operator",
element: <Operator />,
inDrawer: false,
routeable: true
},
]

View File

@@ -0,0 +1,76 @@
import React, {
useState,
useEffect,
useMemo
} from 'react'
import classes from '@/scss/changelogs/Changelogs.module.scss'
import { useHeader } from '@/state/header';
import { useAppbar } from '@/state/appbar';
import useUmami from '@parcellab/react-use-umami'
import Border from '@/component/border';
export default function Changelogs() {
// eslint-disable-next-line no-unused-vars
const _trackEvt = useUmami('/changelogs')
const {
setTitle,
setTabs,
currentTab,
setHeaderIcon,
} = useHeader()
const {
setExtraArea,
} = useAppbar()
const [changelogs, setChangelogs] = useState([])
useEffect(() => {
setTitle('changelogs')
setExtraArea([])
setHeaderIcon(null)
fetch('/_assets/changelogs.json').then(res => res.json()).then(data => {
setChangelogs(data)
})
}, [setExtraArea, setHeaderIcon, setTitle])
useEffect(() => {
setTabs(changelogs.map((item) => {
return {
key: item[0].key
}
}))
}, [changelogs, setTabs])
const content = useMemo(() => {
return (
changelogs.map((v) => {
return (
v.map((item) => {
return (
<section className={classes.wrapper} key={item.date} hidden={currentTab !== item.key}>
<section className={classes.group}>
<section className={classes.info}>
{item.content.map((entry, index) => {
return (
<section className={classes.content} key={index}>
{entry}
</section>
)
})}
</section>
<section className={classes.date}>{item.date}</section>
</section>
<Border />
</section>
)
})
)
})
)
}, [changelogs, currentTab])
return (
<section>
{content}
</section>
)
}

View File

@@ -0,0 +1,201 @@
import React, {
useState,
useEffect,
useCallback,
useMemo
} from 'react'
import PropTypes from 'prop-types';
import {
NavLink,
} from "react-router-dom";
import classes from '@/scss/home/Home.module.scss'
import { useConfig } from '@/state/config';
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 '@parcellab/react-use-umami';
import Switch from '@/component/switch';
const voiceOnAtom = atomWithStorage('voiceOn', false)
let lastVoiceState = 'ended'
export default function Home() {
// eslint-disable-next-line no-unused-vars
const _trackEvt = useUmami('/')
const {
setTitle,
setTabs,
currentTab,
setHeaderIcon
} = useHeader()
const { config } = useConfig()
const [content, setContent] = useState([])
const [voiceOn] = useAtom(voiceOnAtom)
const [voiceSrc, setVoiceSrc] = useState(null)
const [voiceReplay, setVoiceReplay] = useState(false)
useEffect(() => {
setTitle('dynamic_compile')
setTabs([{
key: 'all'
}, {
key: 'operator'
}, {
key: 'skin'
}])
setHeaderIcon(null)
}, [setHeaderIcon, setTabs, setTitle])
useEffect(() => {
setContent(config?.operators || [])
}, [config])
const handleAduioStateChange = useCallback((e, state) => {
lastVoiceState = state
if (state === 'ended') {
setVoiceReplay(false)
}
}, [])
const isShown = useCallback((type) => currentTab === 'all' || currentTab === type, [currentTab])
const handleVoicePlay = useCallback((src) => {
if (!voiceOn) {
setVoiceSrc(null)
} else {
if (src === voiceSrc && lastVoiceState === 'ended') {
setVoiceReplay(true)
} else {
setVoiceSrc(src)
}
}
}, [voiceOn, voiceSrc])
return (
<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}>
{v.map(item => {
return (
<OperatorElement
key={item.link}
item={item}
hidden={!isShown(item.type)}
handleVoicePlay={handleVoicePlay}
/>
)
})}
<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()
return useMemo(() => {
return (
<NavLink
to={`/${item.link}`}
className={classes.item}
hidden={hidden}
>
<section
onMouseEnter={() => handleVoicePlay(`/${item.link}/assets/${JSON.parse(import.meta.env.VITE_VOICE_FOLDERS).main}/${import.meta.env.VITE_APP_VOICE_URL}`)}
>
<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, handleVoicePlay])
}
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,
}

View File

@@ -0,0 +1,554 @@
import React, {
useState,
useEffect,
useRef,
useCallback,
useMemo
} from 'react'
import {
useParams,
useNavigate,
Link
} from "react-router-dom";
import classes from '@/scss/operator/Operator.module.scss'
import { useConfig } from '@/state/config';
import {
useLanguage,
} from '@/state/language'
import { useHeader } from '@/state/header';
import { useAppbar } from '@/state/appbar';
import { useBackgrounds } from '@/state/background';
import VoiceElement from '@/component/voice';
import useUmami from '@parcellab/react-use-umami'
import spine from '!/libs/spine-player'
import '!/libs/spine-player.css'
import Border from '@/component/border';
import { useI18n } from '@/state/language';
import Switch from '@/component/switch';
import { atom, useAtom } from 'jotai'
const musicMapping = JSON.parse(import.meta.env.VITE_MUSIC_MAPPING)
const getVoiceFoler = (lang) => {
const folderObject = JSON.parse(import.meta.env.VITE_VOICE_FOLDERS)
const voiceFolder = folderObject.sub.find(e => e.lang === lang) || folderObject.sub.find(e => e.name === 'custom')
return `${folderObject.main}/${voiceFolder.name}`
}
const defaultSpineAnimation = 'Idle'
const backgroundAtom = atom(null)
const getTabName = (item, language) => {
if (item.type === 'operator') {
return 'operator'
} else {
return item.codename[language].replace(/^(.+)( )(·|\/)()(.+)$/, '$1')
}
}
export default function Operator() {
const navigate = useNavigate()
const { operators } = useConfig()
const { language } = useLanguage()
const { key } = useParams();
const {
setTitle,
setTabs,
setHeaderIcon
} = useHeader()
const { setExtraArea } = useAppbar()
const [config, setConfig] = useState(null)
const [spineData, setSpineData] = useState(null)
// eslint-disable-next-line no-unused-vars
const _trackEvt = useUmami(`/${key}`)
const spineRef = useRef(null)
const [spineAnimation, setSpineAnimation] = useState(defaultSpineAnimation)
const { i18n } = useI18n()
const [spinePlayer, setSpinePlayer] = useState(null)
const [voiceLang, _setVoiceLang] = useState(null)
const { backgrounds } = useBackgrounds()
const [currentBackground, setCurrentBackground] = useAtom(backgroundAtom)
const [voiceConfig, setVoiceConfig] = useState(null)
const [subtitleLang, setSubtitleLang] = useState(null)
const [hideSubtitle, setHideSubtitle] = useState(true)
const [subtitleObj, _setSubtitleObj] = useState(null)
const [currentVoiceId, setCurrentVoiceId] = useState(null)
const voiceLangRef = useRef(voiceLang)
const subtitleObjRef = useRef(subtitleObj)
const configRef = useRef(config)
const [voiceSrc, setVoiceSrc] = useState(null)
const [isVoicePlaying, _setIsVoicePlaying] = useState(false)
const isVoicePlayingRef = useRef(isVoicePlaying)
const setVoiceLang = (value) => {
voiceLangRef.current = value
_setVoiceLang(value)
}
const setSubtitleObj = (value) => {
subtitleObjRef.current = value
_setSubtitleObj(value)
}
const setIsVoicePlaying = (value) => {
isVoicePlayingRef.current = value
_setIsVoicePlaying(value)
}
useEffect(() => {
setExtraArea([])
}, [setExtraArea])
useEffect(() => {
if (backgrounds.length > 0) setCurrentBackground(backgrounds[0])
}, [backgrounds, setCurrentBackground])
useEffect(() => {
setSpineData(null)
const config = operators.find((item) => item.link === key)
if (config) {
setConfig(config)
configRef.current = config
fetch(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/${config.filename.replace("#", "%23")}.json`).then(res => res.json()).then(data => {
setSpineAnimation(defaultSpineAnimation)
setSpineData(data)
})
setHeaderIcon(config.type)
if (spineRef.current?.children.length > 0) {
spineRef.current?.removeChild(spineRef.current?.children[0])
}
fetch(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/voice_${config.link}.json`).then(res => res.json()).then(data => {
setVoiceConfig(data)
})
}
}, [key, operators, setHeaderIcon])
const coverToTab = useCallback((item, language) => {
const key = getTabName(item, language)
return {
key: key,
style: {
color: item.color
},
onClick: (e, tab) => {
if (tab === key) return
navigate(`/${item.link}`)
}
}
}, [navigate])
const otherEntries = useMemo(() => {
if (!config || !language) return null
return operators.filter((item) => item.id === config.id && item.link !== config.link).map((item) => {
return coverToTab(item, language)
})
}, [config, language, operators, coverToTab])
useEffect(() => {
if (config) {
setTabs(
[
coverToTab(config, language),
...otherEntries
]
)
}
}, [config, key, coverToTab, setTabs, otherEntries, language])
useEffect(() => {
if (config) {
setTitle(config.codename[language])
}
}, [config, language, key, setTitle])
useEffect(() => {
if (spineRef.current?.children.length === 0 && spineData && config) {
setSpinePlayer(new spine.SpinePlayer(spineRef.current, {
skelUrl: `./assets/${config.filename.replace('#', '%23')}.skel`,
atlasUrl: `./assets/${config.filename.replace('#', '%23')}.atlas`,
rawDataURIs: spineData,
animation: spineAnimation,
premultipliedAlpha: true,
alpha: true,
backgroundColor: "#00000000",
viewport: {
debugRender: false,
padLeft: `${config.viewport_left}%`,
padRight: `${config.viewport_right}%`,
padTop: `${config.viewport_top}%`,
padBottom: `${config.viewport_bottom}%`,
x: 0,
y: 0,
},
showControls: false,
touch: false,
fps: 60,
defaultMix: 0.3,
success: (player) => {
let lastVoiceId = null
let currentVoiceId = null
player.canvas.onclick = () => {
if (!voiceLangRef.current) return
const voiceId = () => {
const keys = Object.keys(subtitleObjRef.current)
const id = keys[Math.floor((Math.random() * keys.length))]
return id === lastVoiceId ? voiceId() : id
}
const id = voiceId()
currentVoiceId = id
setCurrentVoiceId(id)
setVoiceSrc(`/${configRef.current.link}/assets/${getVoiceFoler(voiceLangRef.current)}/${id}.ogg`)
lastVoiceId = currentVoiceId
}
}
}))
}
}, [config, spineData, spineAnimation]);
useEffect(() => {
if (voiceConfig && voiceLang) {
let subtitleObj = voiceConfig.subtitleLangs[subtitleLang || 'zh-CN']
let subtitleKey = 'default'
if (subtitleObj[voiceLang]) {
subtitleKey = voiceLang
}
setSubtitleObj(subtitleObj[subtitleKey])
}
}, [subtitleLang, voiceConfig, voiceLang])
const handleAduioStateChange = useCallback((e, state) => {
switch (state) {
case 'play':
setIsVoicePlaying(true)
break
default:
setIsVoicePlaying(false)
break
}
}, [])
useEffect(() => {
if (subtitleLang) {
if (isVoicePlaying) {
setHideSubtitle(false)
} else {
const autoHide = () => {
if (isVoicePlayingRef.current) return
setHideSubtitle(true)
}
setTimeout(autoHide, 5 * 1000)
return () => {
clearTimeout(autoHide)
}
}
} else {
setHideSubtitle(true)
}
}, [subtitleLang, isVoicePlaying])
useEffect(() => {
if (voiceLang && isVoicePlaying) {
const audioUrl = `/assets/${getVoiceFoler(voiceLang)}/${currentVoiceId}.ogg`
if (voiceSrc !== (window.location.href.replace(/\/$/g, '') + audioUrl)) {
setVoiceSrc(`/${config.link}${audioUrl}`)
}
}
}, [voiceLang, isVoicePlaying, currentVoiceId, config, voiceSrc])
const playAnimationVoice = useCallback((animation) => {
if (voiceLangRef.current) {
let id = null
if (animation === 'Idle') id = 'CN_011'
if (animation === 'Interact') id = 'CN_034'
if (animation === 'Special') id = 'CN_042'
if (id) {
setCurrentVoiceId(id)
setVoiceSrc(`/${key}/assets/${getVoiceFoler(voiceLangRef.current)}/${id}.ogg`)
}
}
}, [key])
useEffect(() => {
if (!voiceLang) {
setVoiceSrc(null)
}
}, [voiceLang])
const spineSettings = [
{
name: 'animation',
options: [
{
name: 'idle',
onClick: () => {
const animation = "Idle"
playAnimationVoice(animation)
spinePlayer.animationState.setAnimation(0, animation, true, 0)
setSpineAnimation(animation)
},
activeRule: () => {
return spineAnimation === 'Idle'
}
}, {
name: 'interact',
onClick: () => {
const animation = "Interact"
playAnimationVoice(animation)
spinePlayer.animationState.setAnimation(0, animation, true, 0)
setSpineAnimation(animation)
},
activeRule: () => {
return spineAnimation === 'Interact'
}
}, {
name: 'special',
onClick: () => {
const animation = "Special"
playAnimationVoice(animation)
spinePlayer.animationState.setAnimation(0, animation, true, 0)
setSpineAnimation(animation)
},
activeRule: () => {
return spineAnimation === 'Special'
}
}
]
}, {
name: 'voice',
options: voiceConfig && Object.keys(voiceConfig?.voiceLangs["zh-CN"]).map((item) => {
return {
name: i18n(item),
onClick: () => {
if (voiceLang !== item) {
setVoiceLang(item)
} else {
setVoiceLang(null)
}
if (!isVoicePlayingRef.current) {
playAnimationVoice(spineAnimation)
}
},
activeRule: () => {
return voiceLang === item
}
}
}) || []
}, {
name: 'subtitle',
options: voiceConfig && Object.keys(voiceConfig?.subtitleLangs).map((item) => {
return {
name: i18n(item),
onClick: () => {
if (subtitleLang !== item) {
setSubtitleLang(item)
} else {
setSubtitleLang(null)
}
},
activeRule: () => {
return subtitleLang === item
}
}
}) || []
}, {
name: 'music',
el: <MusicElement />
}, {
name: 'backgrounds',
options: backgrounds.map((item) => {
return {
name: item,
onClick: () => {
setCurrentBackground(item)
},
activeRule: () => {
return currentBackground === item
}
}
}) || []
}
]
if (!JSON.parse(import.meta.env.VITE_AVAILABLE_OPERATORS).includes(key)) {
throw new Error('Operator not found')
}
return (
<section className={classes.operator}>
<section className={classes.main}>
<section className={classes.settings} style={{
color: config?.color
}}>
{
spineSettings.map((item) => {
if (item.el) {
return (
<section key={item.name}>
{item.el}
</section>
)
}
if (item.options.length === 0) return null
return (
<section key={item.name}>
<section className={classes.title}>
<section className={classes.text}>{i18n(item.name)}</section>
</section>
<section className={classes['styled-selection']}>
{item.options.map((option) => {
return (
<section className={`${classes.content} ${option.activeRule && option.activeRule() ? classes.active : ''}`} onClick={(e) => option.onClick(e)} key={option.name}>
<section className={classes.option}>
<section className={classes.outline} />
<section className={classes.text}>{i18n(option.name)}</section>
<section className={classes['tick-icon']} />
</section>
</section>
)
})}
</section>
</section>
)
})
}
<section>
<section className={classes.title}>
<section className={classes.text}>{i18n('external_links')}</section>
</section>
<section className={classes['styled-selection']}>
<Link
reloadDocument
to={`./index.html?settings`}
target='_blank'
style={{
color: config?.color
}}
>
<section className={classes.content}>
<section className={classes.option}>
<section className={classes.outline} />
<section className={classes.text}>
{i18n('web_version')}
</section>
</section>
</section>
</Link>
{
config?.workshopId && (
<Link
reloadDocument
to={`https://steamcommunity.com/sharedfiles/filedetails/?id=${config.workshopId}`}
target='_blank'
style={{
color: config?.color
}}>
<section className={classes.content}>
<section className={classes.option}>
<section className={classes.outline} />
<section className={classes.text}>
{i18n('steam_workshop')}
</section>
</section>
</section>
</Link>
)
}
</section>
</section>
</section>
<section className={classes.container} style={currentBackground && {
backgroundImage: `url(/chen/assets/${import.meta.env.VITE_BACKGROUND_FOLDER}/${currentBackground})`
}} >
{
config && (
<img src={`/${config.link}/assets/${config.logo}.png`} alt={config?.codename[language]} className={classes.logo} />
)
}
<section ref={spineRef} className={classes.wrapper} />
{currentVoiceId && subtitleObj && (
<section className={`${classes.voice} ${hideSubtitle ? '' : classes.active}`}>
<section className={classes.type}>{subtitleObj[currentVoiceId]?.title}</section>
<section className={classes.subtitle}>
<span>{subtitleObj[currentVoiceId]?.text}</span>
<span className={classes.triangle} />
</section>
</section>)
}
</section>
</section>
<Border />
<VoiceElement
src={voiceSrc}
handleAduioStateChange={handleAduioStateChange}
/>
</section>
)
}
function MusicElement() {
const [enableMusic, setEnableMusic] = useState(false)
const { i18n } = useI18n()
const musicIntroRef = useRef(null)
const musicLoopRef = useRef(null)
const [background,] = useAtom(backgroundAtom)
useEffect(() => {
if (musicIntroRef.current && musicIntroRef.current) {
musicIntroRef.current.volume = 0.5
musicLoopRef.current.volume = 0.5
}
}, [musicIntroRef, musicLoopRef])
useEffect(() => {
if (!enableMusic || background) {
musicIntroRef.current.pause()
musicLoopRef.current.pause()
}
}, [enableMusic, background])
useEffect(() => {
if (background && enableMusic) {
const introOgg = musicMapping[background].intro
const intro = `./chen/assets/${import.meta.env.VITE_MUSIC_FOLDER}/${introOgg}`
const loop = `./chen/assets/${import.meta.env.VITE_MUSIC_FOLDER}/${musicMapping[background].loop}`
musicLoopRef.current.src = loop
if (introOgg) {
musicIntroRef.current.src = intro || loop
} else {
musicLoopRef.current.play()
}
}
}, [background, enableMusic])
const handleIntroTimeUpdate = useCallback(() => {
if (musicIntroRef.current.currentTime >= musicIntroRef.current.duration - 0.3) {
musicIntroRef.current.pause()
musicLoopRef.current.play()
}
}, [])
const handleLoopTimeUpdate = useCallback(() => {
if (musicLoopRef.current.currentTime >= musicLoopRef.current.duration - 0.3) {
musicLoopRef.current.currentTime = 0
musicLoopRef.current.play()
}
}, [])
return (
<section>
<section
className={classes.title}
onClick={() => setEnableMusic(!enableMusic)}
>
<section className={classes.text}>{i18n('music')}</section>
<section className={classes.switch}>
<Switch on={enableMusic} />
</section>
</section>
<audio ref={musicIntroRef} preload="auto" autoPlay onTimeUpdate={() => handleIntroTimeUpdate()}>
<source type="audio/ogg" />
</audio>
<audio ref={musicLoopRef} preload="auto" onTimeUpdate={() => handleLoopTimeUpdate()}>
<source type="audio/ogg"/>
</audio>
</section>
)
}