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 59bffecc0f
116 changed files with 23521 additions and 0 deletions

9
directory/.env Normal file
View File

@@ -0,0 +1,9 @@
VITE_APP_TITLE=AKLive2D
VITE_APP_VOICE_URL=jp/CN_037.ogg
VITE_VOICE_FOLDERS={"main":"voice","sub":[{"name":"jp","lang":"JP"},{"name":"cn","lang":"CN_MANDARIN"},{"name":"en","lang":"EN"},{"name":"kr","lang":"KR"},{"name":"custom","lang":"CUSTOM"}]}
VITE_DIRECTORY_FOLDER="_assets"
VITE_BACKGROUND_FOLDER="background"
VITE_AVAILABLE_OPERATORS=["chen","dusk","dusk_everything_is_a_miracle","ling","nearl","nian","nian_unfettered_freedom","phatom_focus","rosmontis","skadi","skadi_sublimation","w","w_wonder","specter","gavial","surtr_colorful_wonderland","lee_trust_your_eyes","texas_the_omertosa","nearl_relight","rosmontis_become_anew","passager_dream_in_a_moment","mizuki_summer_feast","chongyue","ling_it_does_wash_the_strings","pozemka_snowy_plains_in_words"]
VITE_ERROR_FILES={"files":[{"key":"build_char_128_plosis_epoque%233","paddings":{"left":-120,"right":150,"top":10,"bottom":0}},{"key":"build_char_128_plosis","paddings":{"left":-90,"right":100,"top":10,"bottom":0}}],"voice":"CN_034.ogg"}
VITE_MUSIC_FOLDER=music
VITE_MUSIC_MAPPING={"operator_bg.png":{"intro":"m_sys_void_intro.ogg","loop":"m_sys_void_loop.ogg"},"bg_anniversary_1.png":{"intro":"m_dia_nightoflongmen_intro.ogg","loop":"m_dia_nightoflongmen_loop.ogg"},"bg_iberia_1.png":{"intro":"m_sys_act18d3d0_intro.ogg","loop":"m_sys_act18d3d0_loop.ogg"},"bg_kazimierz_1.png":{"intro":"m_dia_street_intro.ogg","loop":"m_dia_street_loop.ogg"},"bg_main_victoria_1.png":{"intro":"m_avg_ghosthunter_intro.ogg","loop":"m_avg_ghosthunter_loop.ogg"},"bg_rhodes_day.png":{"intro":"m_sys_void_intro.ogg","loop":"m_sys_void_loop.ogg"},"bg_rhodes_night.png":{"intro":"m_sys_tech_intro.ogg","loop":"m_sys_tech_loop.ogg"},"bg_rogue_1.png":{"intro":null,"loop":"m_avg_rglk1secretevent_loop.ogg"},"bg_siesta_1.png":{"intro":"m_sys_ddd_intro.ogg","loop":"m_sys_ddd_loop.ogg"},"bg_ursus_1.png":{"intro":"m_avg_loneliness_intro.ogg","loop":"m_avg_loneliness_loop.ogg"},"bg_yan_1.png":{"intro":null,"loop":"m_sys_bitw_loop.ogg"}}

24
directory/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

1
directory/Version Normal file
View File

@@ -0,0 +1 @@
1.2.3

14
directory/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>aklive2d directory</title>
<script async defer data-website-id="05ee2d3d-66e0-429b-89ba-db112743a8c2" src="https://insights.halyul.dev/insights.js"
data-do-not-track="true" data-auto-track="false"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/App.jsx"></script>
</body>
</html>

24
directory/src/App.jsx Normal file
View File

@@ -0,0 +1,24 @@
import React from 'react';
import ReactDOM from 'react-dom/client'
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import Root from "@/routes/Root";
import Error from "@/routes/Error";
import routes from "@/routes";
import '@/App.scss';
import 'reset-css';
const router = createBrowserRouter([{
path: "/",
element: <Root />,
errorElement: <Error />,
children: routes.filter((item) => item.routeable),
},]);
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
)

39
directory/src/App.scss Normal file
View File

@@ -0,0 +1,39 @@
@import 'https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap';
@import 'https://fonts.cdnfonts.com/css/bender';
@import 'https://fonts.cdnfonts.com/css/geometos';
:root {
--text-color: rgba(255, 255, 255, 87%);
--text-color-full: #fff;
--secondary-text-color: #686a72;
--date-color: rgba(255, 255, 255, 20%);
--border-color: #707070;
--link-highlight-color: #33b5e5;
--drawer-background-color: rgba(0, 0, 0, 88%);
--root-background-color: #131313;
--home-item-hover-background-color: rgba(67, 67, 67, 30%);
--home-item-background-linear-gradient-color: rgba(255, 255, 255, 10%);
--home-item-outline-color: rgba(214, 214, 214, 30%);
--button-color: #666;
font-family: Geometos, "Noto Sans SC", sans-serif;
font-size: 16px;
line-height: 1.2em;
font-weight: 400;
color: var(--text-color);
background-color: var(--root-background-color);
min-height: 100vh;
}
#root {
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
align-items: stretch;
min-height: 100vh;
}
a {
color: var(--text-color);
text-decoration: none;
}

View File

@@ -0,0 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import classes from './scss/border.module.scss';
export default function Border(props) {
return (
<section className={classes.border}>
{props.children}
</section>
)
}
Border.propTypes = {
children: PropTypes.node,
};

View File

@@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
export default function CharIcon(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox={props.viewBox}>
{
props.type === 'operator' ?
<g><path d="M89 17.5 30.4 57 24.3 71.4 82.9 32.6Z"></path><path d="M0 17.5 58.6 57 64.7 71.4 6.1 32.7Z"> </path><path d="M89 0 30.4 39.5 24.3 53.9 82.9 15.1Z"> </path><path d="M0 0 58.6 39.5 64.7 53.9 6.1 15.2Z"> </path></g>
:
<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>
)
}
CharIcon.propTypes = {
viewBox: PropTypes.string,
type: PropTypes.string,
};

View File

@@ -0,0 +1,58 @@
import React, {
useState
} from 'react'
import PropTypes from 'prop-types';
import classes from './scss/dropdown.module.scss'
export default function Dropdown(props) {
const [hidden, setHidden] = useState(true)
const toggleDropdown = () => {
setHidden(!hidden)
}
return (
<>
<section className={`${classes.dropdown} ${hidden ? '' : classes.active} ${props.className ? props.className : ''}`} >
<section
className={classes.text}
onClick={() => toggleDropdown()}
>
<span className={classes.content}>{props.text}</span>
<span className={classes.icon}></span>
</section>
<ul className={classes.menu} style={props.activeColor}>
{
props.menu.map((item) => {
return (
<li
key={item.name}
className={`${classes.item} ${item.name === props.text || (props.activeRule && props.activeRule(item)) ? classes.active : ''}`}
onClick={() => {
props.onClick(item)
toggleDropdown()
}}
>
<section className={classes.text}>{item.name}</section>
</li>
)
})
}
</ul>
<section
className={classes.overlay}
hidden={hidden}
onClick={() => toggleDropdown()}
/>
</section>
</>
)
}
Dropdown.propTypes = {
className: PropTypes.string,
text: PropTypes.string,
menu: PropTypes.array,
onClick: PropTypes.func,
activeColor: PropTypes.object,
activeRule: PropTypes.func,
};

View File

@@ -0,0 +1,42 @@
import React, {
useState,
} from 'react'
import classes from './scss/popup.module.scss';
import ReturnButton from '@/component/return_button';
import Border from '@/component/border';
import PropTypes from 'prop-types';
export default function Popup(props) {
const [hidden, setHidden] = useState(true)
const toggle = () => {
setHidden(!hidden)
}
return (<>
<section className={`${classes.popup} ${hidden ? '' : classes.active}`}>
<section className={classes.wrapper}>
<section className={classes.title}>
<section className={classes.text}>{props.title}</section>
<ReturnButton onClick={toggle} className={classes["return-button"]} />
</section>
<Border />
<section className={classes.content}>
{props.children}
</section>
</section>
<section className={`${classes.overlay} ${hidden ? '' : classes.active}`}
onClick={() => toggle()} />
</section>
<span
className={classes['entry-text']}
onClick={toggle}
>
{props.title}
</span>
</>)
}
Popup.propTypes = {
title: PropTypes.string,
children: PropTypes.node,
};

View File

@@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import classes from './scss/return_button.module.scss'
export default function ReturnButton(props) {
return (
<>
<section className={`${classes['return-button']} ${props.className ? props.className : ''}`}
onClick={() => props.onClick()}
>
<section className={classes.wrapper}>
<section className={classes["arrow-left"]}></section>
<section className={classes.bar}></section>
<section className={classes["arrow-right"]}></section>
</section>
<section className={classes.wrapper}>
<section className={classes["arrow-left"]}></section>
<section className={classes.bar}></section>
<section className={classes["arrow-right"]}></section>
</section>
<section className={classes.wrapper}>
<section className={classes["arrow-left"]}></section>
<section className={classes.bar}></section>
<section className={classes["arrow-right"]}></section>
</section>
<section className={classes.wrapper}>
<section className={classes["arrow-left"]}></section>
<section className={classes.bar}></section>
<section className={classes["arrow-right"]}></section>
</section>
</section>
</>
)
}
ReturnButton.propTypes = {
onClick: PropTypes.func,
className: PropTypes.string,
};

View File

@@ -0,0 +1,22 @@
.border {
position: relative;
bottom: 1px;
border-bottom: 1px solid var(--text-color);
&:before,
&:after {
content: "";
display: block;
position: absolute;
width: 5px;
height: 5px;
top: -2px;
background-color: var(--text-color);
}
&:before {
right: 100%;
}
&:after {
left: 100%;
}
}

View File

@@ -0,0 +1,111 @@
.dropdown {
position: relative;
display: inline-block;
user-select: none;
z-index: 2;
padding: 0.5rem;
cursor: pointer;
.text {
display: flex;
flex-direction: row;
align-items: center;
color: var(--text-color);
}
.content {
padding-right: 0.5rem;
}
.icon {
position: absolute;
bottom: 0.5rem;
right: -0.1rem;
width: 0.5em;
height: 0.5em;
display: inline-block;
vertical-align: middle;
border-left: 0.15em solid var(--text-color);
border-bottom: 0.15em solid var(--text-color);
border-right: 0.15em solid var(--text-color);
border-top: 0.15em solid var(--text-color);
transform: translate(0, -0.15em) rotate(-45deg);
}
.menu {
opacity: 0;
position: absolute;
background-color: var(--root-background-color);
width: max-content;
z-index: -1;
top: 2rem;
right: 0;
gap: 0.5rem;
display: flex;
align-items: center;
flex-direction: column;
flex-wrap: nowrap;
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
overflow: hidden;
padding: 0.5rem;
border: 1px solid var(--border-color);
visibility: hidden;
color: var(--link-highlight-color);
cursor: auto;
.item {
cursor: pointer;
padding: 0.5rem;
font-size: 1rem;
width: max-content;
height: max-content;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
flex-wrap: nowrap;
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
&:hover,
&:focus,
&.active {
.text {
color: currentColor;
}
}
}
}
.overlay {
z-index: -1;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
cursor: auto;
}
&.active,
&:hover {
.icon {
animation: icon-flash 2s cubic-bezier(0.65, 0.05, 0.36, 1) infinite;
}
}
&.active {
.menu {
visibility: visible;
opacity: 1;
z-index: 2;
}
}
}
@keyframes icon-flash {
50% {
opacity: 0.2;
}
}

View File

@@ -0,0 +1,94 @@
.entry-text {
cursor: pointer;
}
.popup {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
opacity: 0;
z-index: -1;
border: unset;
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 1.2rem;
.wrapper {
display: flex;
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
max-width: 480px;
height: fit-content;
margin: 0 auto;
background-color: var(--root-background-color);
border: 1px solid var(--border-color);
padding: 2rem;
}
.title {
font-size: 3rem;
font-weight: 700;
display: flex;
flex-direction: row;
justify-content: space-between;
align-content: center;
align-items: center;
text-transform: uppercase;
font-family: "Geometos", "Noto Sans SC", sans-serif;
.return-button {
color: var(--button-color);
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
&:hover {
color: var(--text-color);
}
}
}
.text {
flex-grow: 1;
margin-right: 3rem;
}
.content {
line-height: 1.3em;
padding: 1rem 1rem 0 1rem;
user-select: text;
}
.overlay {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
opacity: 0;
background-color: var(--root-background-color);
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
&.active {
opacity: 0.5;
visibility: visible;
}
}
&.active {
opacity: 1;
z-index: 10;
}
@media (max-width: 768px) {
.title {
font-size: 2rem;
}
.content {
font-size: 1rem;
}
.return-button {
transform: scale(0.8);
}
}
}

View File

@@ -0,0 +1,46 @@
.return-button {
%arrow-shared {
border-top: 0.24rem solid transparent;
border-bottom: 0.24rem solid transparent;
}
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0.6rem 0;
width: 3rem;
cursor: pointer;
.wrapper {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
&:nth-child(1) {
transform: translateY(-0.1rem) rotate(45deg);
}
&:nth-child(2) {
transform: translateY(-0.1rem) translate(90%, -100%) rotate(-45deg);
}
&:nth-child(3) {
transform: translateY(-0.1rem) translateY(150%) rotate(315deg);
}
&:nth-child(4) {
transform: translateY(-0.1rem) translate(90%, 50%) rotate(225deg);
}
}
.bar {
width: 1rem;
height: 0.4rem;
background-color: currentColor;
transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.arrow-left {
@extend %arrow-shared;
border-right: 0.3rem solid currentColor;
}
.arrow-right {
@extend %arrow-shared;
border-left: 0.3rem solid currentColor;
}
}

View File

@@ -0,0 +1,60 @@
.switch {
position: relative;
user-select: none;
z-index: 2;
padding: 8px 36px 8px 8px;
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
color: var(--secondary-text-color);
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
.content {
padding-right: 8px;
}
.wrapper {
color: var(--secondary-text-color);
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
.icon {
position: absolute;
bottom: 8px;
right: 18px;
width: 8px;
height: 8px;
display: inline-block;
vertical-align: middle;
border-left: 2px solid currentColor;
border-bottom: 2px solid currentColor;
border-right: 2px solid currentColor;
border-top: 2px solid currentColor;
transform: translate(0, -2px) rotate(-45deg);
transition: right cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s,
background-color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.line {
position: absolute;
bottom: 15px;
right: 6px;
width: 18px;
height: 2px;
display: inline-block;
vertical-align: middle;
background-color: currentColor;
z-index: -1;
}
}
&.active {
color: var(--text-color);
.wrapper {
color: var(--text-color-full);
.icon {
background-color: currentColor;
right: 0;
}
}
}
}

View File

@@ -0,0 +1,37 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import classes from './scss/switch.module.scss';
import {
useI18n
} from '@/state/language'
export default function Switch(props) {
const [on, setOn] = useState(props.on)
const { i18n } = useI18n()
useEffect(() => {
setOn(props.on)
}, [props.on])
return (
<section
className={`${classes.switch} ${on ? classes.active : ''}`}
onClick={() => {
if (props.handleOnClick) {
props.handleOnClick(!on)
}
}}
>
<span className={classes.text}>{i18n(props.text)}</span>
<section className={classes.wrapper}>
<span className={classes.line}></span>
<span className={classes.icon}></span>
</section>
</section>
)
}
Switch.propTypes = {
on: PropTypes.bool,
text: PropTypes.string,
handleOnClick: PropTypes.func,
};

View File

@@ -0,0 +1,53 @@
import React, {
useEffect,
useRef,
} from "react"
import PropTypes from 'prop-types';
export default function VoiceElement({
src,
replay,
handleAduioStateChange,
}) {
const audioRef = useRef(null)
useEffect(() => {
if (src) {
audioRef.current.src = src
audioRef.current.play()
} else {
audioRef.current.pause()
}
}, [src])
useEffect(() => {
if (replay) {
audioRef.current.currentTime = 0
audioRef.current.play()
}
}, [replay])
return (
<audio
ref={audioRef}
preload="auto"
autoPlay
onEnded={(e) => {
if (handleAduioStateChange) handleAduioStateChange(e, 'ended')
}}
onPlay={(e) => {
if (handleAduioStateChange) handleAduioStateChange(e, 'play')
}}
onPause={(e) => {
if (handleAduioStateChange) handleAduioStateChange(e, 'pause')
}}
>
<source type="audio/ogg" />
</audio>
)
}
VoiceElement.propTypes = {
src: PropTypes.string,
handleAduioStateChange: PropTypes.func,
replay: PropTypes.bool,
}

143
directory/src/i18n.json Normal file
View File

@@ -0,0 +1,143 @@
{
"available": [
"zh-CN", "en-US"
],
"key": {
"dynamic_compile": {
"zh-CN": "动态集录",
"en-US": "Dynamic Compile"
},
"home": {
"zh-CN": "首页",
"en-US": "Home"
},
"changelogs": {
"zh-CN": "更新日志",
"en-US": "Changelogs"
},
"offical_page": {
"zh-CN": "官方页面",
"en-US": "Offical Page"
},
"disclaimer": {
"zh-CN": "免责声明",
"en-US": "Disclaimer"
},
"disclaimer_content": {
"zh-CN": "本网站由 Halyul 设立并为明日方舟社区服务Halyul 声明本网站完全独立运营,与 上海鹰角网络科技有限公司, Esoteric Software LLC 或其任何关联实体并无任何联系。",
"en-US": "This website is set up and operated by Halyul for the benefit of the Arknights Community. Halyul hereby states that this website is dedicated, but not related to Hypergryph Co., Ltd, Esoteric Software LLC or any of its affiliated entity."
},
"privacy_policy": {
"zh-CN": "隐私政策",
"en-US": "Privacy Policy"
},
"contact_us": {
"zh-CN": "联系我们",
"en-US": "Contact Us"
},
"all": {
"zh-CN": "综合",
"en-US": "All"
},
"operator": {
"zh-CN": "精英2",
"en-US": "Elite 2"
},
"skin": {
"zh-CN": "时装",
"en-US": "Skin"
},
"voice": {
"zh-CN": "语音",
"en-US": "Voice"
},
"showcase": {
"zh-CN": "壁纸",
"en-US": "Wallpaper"
},
"directory": {
"zh-CN": "目录页",
"en-US": "Directory Page"
},
"animation": {
"zh-CN": "动画",
"en-US": "Animation"
},
"backgrounds": {
"zh-CN": "背景",
"en-US": "Backgrounds"
},
"CN_MANDARIN": {
"zh-CN": "普通话",
"en-US": "Mandarin"
},
"JP": {
"zh-CN": "日语",
"en-US": "Japanese"
},
"KR": {
"zh-CN": "韩语",
"en-US": "Korean"
},
"EN": {
"zh-CN": "英语",
"en-US": "English"
},
"ITA": {
"zh-CN": "意大利语",
"en-US": "Italian"
},
"CN_TOPOLECT": {
"zh-CN": "中文方言",
"en-US": "Chinese Topolect"
},
"steam_workshop": {
"zh-CN": "壁纸引擎版",
"en-US": "Wallpaper Engine Version"
},
"external_links": {
"zh-CN": "外部链接",
"en-US": "External Links"
},
"web_version": {
"zh-CN": "全功能网页版",
"en-US": "Full Feature Web Version"
},
"idle": {
"zh-CN": "待机",
"en-US": "Idle"
},
"interact": {
"zh-CN": "交互",
"en-US": "Interact"
},
"special": {
"zh-CN": "特殊",
"en-US": "Special"
},
"subtitle": {
"zh-CN": "字幕",
"en-US": "Subtitle"
},
"zh-CN": {
"zh-CN": "简体中文",
"en-US": "Chinese (Simplified)"
},
"en-US": {
"zh-CN": "英语",
"en-US": "English"
},
"zh-TW": {
"zh-CN": "繁体中文",
"en-US": "Chinese (Traditional)"
},
"ja-JP": {
"zh-CN": "日语",
"en-US": "Japanese"
},
"ko-KR": {
"zh-CN": "韩语",
"en-US": "Korean"
}
}
}

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>
)
}

View File

@@ -0,0 +1,13 @@
.main {
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
padding-bottom: 3rem;
margin: 0 auto;
width: 70%;
max-width: 100rem;
padding-top: 5rem;
min-height: calc(100vh - 5rem - 3rem);
}

View File

@@ -0,0 +1,150 @@
.group {
padding: 1rem;
display: flex;
align-items: flex-end;
flex-wrap: wrap;
user-select: none;
.date {
margin: 1.5rem;
font-family: "Bender";
font-weight: bold;
text-align: right;
color: var(--date-color);
font-size: 1.5rem;
letter-spacing: 0.1rem;
flex: auto;
user-select: none;
}
.item {
position: relative;
cursor: pointer;
width: 180px;
margin: 1.25rem;
background-image: repeating-linear-gradient(
90deg,
var(--home-item-background-linear-gradient-color) 0,
var(--home-item-background-linear-gradient-color) 1px,
transparent 1px,
transparent 5px
);
.background-filler {
border-right: 1px solid var(--home-item-background-linear-gradient-color);
position: absolute;
top: 0;
left: 0;
right: -1px;
bottom: 0;
}
.outline {
display: block;
position: absolute;
opacity: 0;
visibility: hidden;
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
width: 100%;
height: 100%;
left: -6px;
top: -6px;
border: var(--home-item-outline-color) 1px dashed;
padding: 6px;
&:before,
&:after {
content: "";
display: block;
position: absolute;
left: -3px;
height: 3px;
width: 100%;
border-left: var(--text-color) solid 3px;
border-right: var(--text-color) solid 3px;
}
&:before {
top: -3px;
}
&:after {
bottom: -3px;
}
}
.img {
height: 360px;
width: 100%;
transition: background-color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.info {
white-space: nowrap;
position: relative;
padding: 0.8rem 0.4rem;
line-height: 1.2em;
height: 36px;
.container {
color: var(--text-color-full);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1.25rem;
font-weight: bold;
.title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3em;
height: auto;
}
.type {
display: flex;
flex-direction: row;
align-items: baseline;
text-align: center;
width: 1.5rem;
fill: var(--text-color);
}
}
.wrapper {
overflow: hidden;
text-overflow: ellipsis;
color: var(--secondary-text-color);
.text {
font-size: 0.75rem;
font-family: "Geometos";
margin-top: 1rem;
}
}
.background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
visibility: hidden;
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
background-image: linear-gradient(
70deg,
transparent 40%,
currentColor 150%
);
}
}
&:hover {
.img {
background-color: var(--home-item-hover-background-color);
}
.outline,
.info .background {
opacity: 1;
visibility: visible;
}
}
}
}

View File

@@ -0,0 +1,19 @@
@use '@/scss/_page_base.scss';
.group {
flex-direction: column;
align-items: stretch;
.info {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
padding-left: 1rem;
word-break: break-word;
.content {
font-size: 1.5rem;
display: list-item;
}
}
}

View File

@@ -0,0 +1,57 @@
@use '@/scss/_main_share.scss';
.error {
min-height: 100vh;
display: flex;
flex-direction: column;
font-size: 16px;
user-select: none;
.header {
padding: 1rem;
justify-content: space-between;
pointer-events: auto;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
padding-top: 10rem;
font-size: 3rem;
gap: 2rem;
padding-bottom: 0;
}
.spine {
max-width: 600px;
flex: 1;
visibility: hidden;
opacity: 0;
&.active {
visibility: visible;
opacity: 1;
}
}
@media (max-width: 768px) {
.main {
padding-top: 6rem;
max-height: calc(100vh - 6rem);
}
.content {
font-size: 2rem;
}
}
@media (max-width: 480px) {
.main {
padding-top: 4rem;
max-height: calc(100vh - 4rem);
}
.content {
font-size: 1.5rem;
}
}
}

View File

@@ -0,0 +1 @@
@use '@/scss/_page_base.scss';

View File

@@ -0,0 +1,95 @@
@use '@/scss/_main_share.scss';
.main {
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
flex-wrap: wrap;
position: relative;
padding-right: 2rem;
.title {
font-size: 3rem;
font-weight: 700;
text-transform: uppercase;
line-height: 1.2em;
.icon {
width: 3.88rem;
margin-right: 1.88rem;
fill: var(--text-color);
display: inline-block;
vertical-align: middle;
}
@media (max-width: 600px) {
font-size: 2.5rem;
}
@media (max-width: 480px) {
font-size: 2rem;
}
}
.tab {
flex: auto;
white-space: pre;
user-select: none;
display: flex;
flex-direction: row;
justify-content: flex-end;
overflow: hidden;
z-index: 1;
.item {
font-size: 1.25rem;
line-height: 3em;
font-weight: 700;
padding: 0 1rem;
text-transform: uppercase;
cursor: pointer;
border-bottom: 0.3rem solid transparent;
display: inline-block;
cursor: pointer;
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
.text-wrapper {
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-color);
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
&.active,
&:hover {
color: var(--link-highlight-color);
.text-wrapper,
.text {
color: currentColor;
}
}
&.active {
border-bottom-color: currentColor;
}
}
}
}
.return-button {
position: absolute;
right: -4rem;
bottom: -24px;
color: var(--button-color);
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
&:hover {
color: var(--text-color);
}
@media (max-width: 768px) {
right: -3.4rem;
}
}
}

View File

@@ -0,0 +1,54 @@
.drawer {
position: fixed;
top: 0;
left: -15rem;
width: 15rem;
height: 100%;
z-index: 2;
pointer-events: none;
transition: left cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
text-align: center;
display: flex;
flex-direction: row;
align-items: flex-start;
user-select: none;
.links {
padding: 8rem 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
background-color: var(--drawer-background-color);
height: 100%;
width: 15rem;
.link {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
color: var(--text-color);
font-size: 0.8rem;
font-weight: 500;
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
text-transform: uppercase;
&:hover,
&.active {
color: var(--link-highlight-color);
}
}
}
.overlay {
height: 100%;
flex-grow: 1;
z-index: 2;
}
&.active {
pointer-events: all;
left: 0;
width: 100vw;
}
}

View File

@@ -0,0 +1,35 @@
.footer {
user-select: none;
.section {
border-top: 1px solid var(--border-color);
padding: 1rem 0;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
font-family: "Noto Sans SC", sans-serif;
}
.links {
flex-direction: row;
height: 2rem;
.item {
padding: 0 1rem;
border-left: 2px solid var(--border-color);
height: inherit;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-wrap: wrap;
&:first-of-type {
border-left: none;
}
}
}
.copyright {
flex-direction: column;
gap: 0.5rem;
font-size: 12px;
}
}

View File

@@ -0,0 +1,74 @@
.header {
width: auto;
position: fixed;
left: 0;
top: 0;
right: 0;
padding: 1rem;
z-index: 3;
height: 3rem;
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
justify-content: flex-start;
pointer-events: none;
.spacer {
flex-grow: 1;
}
.dropdown {
margin-left: auto;
}
.extra-area {
display: flex;
flex-direction: row;
align-items: center;
pointer-events: auto;
}
}
.navButton {
padding: 0.5rem;
font-size: 2rem;
width: 2rem;
height: 2rem;
cursor: pointer;
display: flex;
justify-content: center;
align-items: flex-start;
flex-direction: column;
z-index: 2;
pointer-events: auto;
.bar {
width: 2rem;
height: 0.2rem;
background-color: var(--text-color);
transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
&:nth-child(1) {
transform: translate(0, -200%);
}
&:nth-child(3) {
transform: translate(0, 200%);
}
}
&.active {
.bar {
&:nth-child(1) {
transform: translate(0, 100%) rotateZ(45deg) scaleX(0.5) translate(-50%);
}
&:nth-child(2) {
transform: rotateZ(-45deg);
}
&:nth-child(3) {
transform: translate(0, -100%) rotateZ(45deg) scaleX(0.5) translate(50%);
}
}
}
}

View File

@@ -0,0 +1,10 @@
import { atom, useAtom } from 'jotai';
const extraAreaAtom = atom([]);
export function useAppbar() {
const [extraArea, setExtraArea] = useAtom(extraAreaAtom);
return {
extraArea, setExtraArea
}
}

View File

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

View File

@@ -0,0 +1,31 @@
import { useCallback } from 'react';
import { atom, useAtom } from 'jotai';
const configAtom = atom([]);
const operatorsAtom = atom([]);
const versionAtom = atom({});
export function useConfig() {
const [config, setConfig] = useAtom(configAtom);
const [version, setVersion] = useAtom(versionAtom);
const [operators, setOperators] = useAtom(operatorsAtom);
const fetchConfig = useCallback(async () => {
const res = await fetch('/_assets/directory.json')
const data = await res.json()
setConfig(data);
let operatorsList = []
data.operators.forEach((item) => {
operatorsList = [...operatorsList, ...item]
})
setOperators(operatorsList)
}, [setConfig, setOperators])
const fetchVersion = useCallback(async () => {
const res = await fetch('/_assets/version.json')
const data = await res.json()
setVersion(data);
}, [setVersion])
return { config, version, operators, fetchConfig, fetchVersion };
}

View File

@@ -0,0 +1,34 @@
import { useEffect } from 'react';
import { atom, useAtom } from 'jotai';
import { useI18n } from "@/state/language"
const keyAtom = atom('');
const titleAtom = atom('');
const tabsAtom = atom([]);
const currentTabAtom = atom(null);
const appbarExtraAreaAtom = atom([]);
const headerIconAtom = atom(null);
export function useHeader() {
const [key, setTitle] = useAtom(keyAtom);
const [title, setRealTitle] = useAtom(titleAtom);
const [tabs, setTabs] = useAtom(tabsAtom);
const [currentTab, setCurrentTab] = useAtom(currentTabAtom);
const [appbarExtraArea, setAppbarExtraArea] = useAtom(appbarExtraAreaAtom);
const [headerIcon, setHeaderIcon] = useAtom(headerIconAtom);
const { i18n } = useI18n()
useEffect(() => {
const newTitle = i18n(key)
document.title = `${newTitle} - ${import.meta.env.VITE_APP_TITLE}`;
setRealTitle(newTitle)
}, [i18n, key, setRealTitle])
return {
title, setTitle,
tabs, setTabs,
currentTab, setCurrentTab,
appbarExtraArea, setAppbarExtraArea,
headerIcon, setHeaderIcon
}
}

View File

@@ -0,0 +1,35 @@
import { atom, useAtom, useAtomValue } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import i18nObject from '@/i18n'
const language = i18nObject.available.includes(navigator.language) ? navigator.language : "en-US"
const textDefaultLang = "en-US"
const languageAtom = atomWithStorage('language', language)
const alternateLangAtom = atom((get) => {
const language = get(languageAtom)
return language.startsWith("en") ? "zh-CN" : language
})
export function useI18n() {
const language = useAtomValue(languageAtom)
return {
i18n: (key, preferredLanguage = language) => {
if (i18nObject.key[key]) {
return i18nObject.key[key][preferredLanguage]
}
return key
},
i18nValues: i18nObject,
}
}
export function useLanguage() {
const [language, setLanguage] = useAtom(languageAtom)
const alternateLang = useAtomValue(alternateLangAtom)
return {
textDefaultLang,
language, setLanguage,
alternateLang,
}
}