chore: moved to a new branch to save space
This commit is contained in:
9
directory/.env
Normal file
9
directory/.env
Normal 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
24
directory/.gitignore
vendored
Normal 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
1
directory/Version
Normal file
@@ -0,0 +1 @@
|
||||
1.2.3
|
||||
14
directory/index.html
Normal file
14
directory/index.html
Normal 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
24
directory/src/App.jsx
Normal 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
39
directory/src/App.scss
Normal 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;
|
||||
}
|
||||
14
directory/src/component/border.jsx
Normal file
14
directory/src/component/border.jsx
Normal 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,
|
||||
};
|
||||
20
directory/src/component/char_icon.jsx
Normal file
20
directory/src/component/char_icon.jsx
Normal 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,
|
||||
};
|
||||
58
directory/src/component/dropdown.jsx
Normal file
58
directory/src/component/dropdown.jsx
Normal 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,
|
||||
};
|
||||
42
directory/src/component/popup.jsx
Normal file
42
directory/src/component/popup.jsx
Normal 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,
|
||||
};
|
||||
39
directory/src/component/return_button.jsx
Normal file
39
directory/src/component/return_button.jsx
Normal 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,
|
||||
};
|
||||
22
directory/src/component/scss/border.module.scss
Normal file
22
directory/src/component/scss/border.module.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
111
directory/src/component/scss/dropdown.module.scss
Normal file
111
directory/src/component/scss/dropdown.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
94
directory/src/component/scss/popup.module.scss
Normal file
94
directory/src/component/scss/popup.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
directory/src/component/scss/return_button.module.scss
Normal file
46
directory/src/component/scss/return_button.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
60
directory/src/component/scss/switch.module.scss
Normal file
60
directory/src/component/scss/switch.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
directory/src/component/switch.jsx
Normal file
37
directory/src/component/switch.jsx
Normal 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,
|
||||
};
|
||||
53
directory/src/component/voice.jsx
Normal file
53
directory/src/component/voice.jsx
Normal 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
143
directory/src/i18n.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
13
directory/src/scss/_main_share.scss
Normal file
13
directory/src/scss/_main_share.scss
Normal 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);
|
||||
}
|
||||
150
directory/src/scss/_page_base.scss
Normal file
150
directory/src/scss/_page_base.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
directory/src/scss/changelogs/Changelogs.module.scss
Normal file
19
directory/src/scss/changelogs/Changelogs.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
57
directory/src/scss/error/Error.module.scss
Normal file
57
directory/src/scss/error/Error.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
directory/src/scss/home/Home.module.scss
Normal file
1
directory/src/scss/home/Home.module.scss
Normal file
@@ -0,0 +1 @@
|
||||
@use '@/scss/_page_base.scss';
|
||||
95
directory/src/scss/root/Root.module.scss
Normal file
95
directory/src/scss/root/Root.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
directory/src/scss/root/drawer.module.scss
Normal file
54
directory/src/scss/root/drawer.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
35
directory/src/scss/root/footer.module.scss
Normal file
35
directory/src/scss/root/footer.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
74
directory/src/scss/root/header.module.scss
Normal file
74
directory/src/scss/root/header.module.scss
Normal 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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
directory/src/state/appbar.js
Normal file
10
directory/src/state/appbar.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { atom, useAtom } from 'jotai';
|
||||
|
||||
const extraAreaAtom = atom([]);
|
||||
|
||||
export function useAppbar() {
|
||||
const [extraArea, setExtraArea] = useAtom(extraAreaAtom);
|
||||
return {
|
||||
extraArea, setExtraArea
|
||||
}
|
||||
}
|
||||
19
directory/src/state/background.js
Normal file
19
directory/src/state/background.js
Normal 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
|
||||
};
|
||||
}
|
||||
31
directory/src/state/config.js
Normal file
31
directory/src/state/config.js
Normal 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 };
|
||||
}
|
||||
34
directory/src/state/header.js
Normal file
34
directory/src/state/header.js
Normal 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
|
||||
}
|
||||
}
|
||||
35
directory/src/state/language.js
Normal file
35
directory/src/state/language.js
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user