chore: moved to a new branch to save space
This commit is contained in:
191
directory/src/routes/Error.jsx
Normal file
191
directory/src/routes/Error.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
291
directory/src/routes/Root.jsx
Normal file
291
directory/src/routes/Root.jsx
Normal 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])
|
||||
}
|
||||
36
directory/src/routes/index.jsx
Normal file
36
directory/src/routes/index.jsx
Normal 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
|
||||
},
|
||||
]
|
||||
76
directory/src/routes/path/Changelogs.jsx
Normal file
76
directory/src/routes/path/Changelogs.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
201
directory/src/routes/path/Home.jsx
Normal file
201
directory/src/routes/path/Home.jsx
Normal 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,
|
||||
}
|
||||
554
directory/src/routes/path/Operator.jsx
Normal file
554
directory/src/routes/path/Operator.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user