feat: migrate to turbo (#22)
* feat: migrate top turbo * ci: ci test * fix: fix codeql issues * feat: ci test * chore: lint * chore: misc changes * feat: rename vite helpers * feat: use fetch to handle assets * feat: update directory * feat: fetch charword table * feat: migrate download game data and detect missing voice files * feat: symlink relative path * feat: finish wrangler upload * feat: migrate wrangler download * feat: finish * chore: auto update * ci: update ci * ci: update ci --------- Co-authored-by: Halyul <Halyul@users.noreply.github.com>
This commit is contained in:
26
apps/directory/.gitignore
vendored
Normal file
26
apps/directory/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# 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?
|
||||
|
||||
data
|
||||
3
apps/directory/.prettierignore
Normal file
3
apps/directory/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
dist
|
||||
data
|
||||
auto_update
|
||||
30
apps/directory/eslint.config.js
Normal file
30
apps/directory/eslint.config.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import js from '@eslint/js'
|
||||
import react from 'eslint-plugin-react'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import baseConfig from '@aklive2d/eslint-config'
|
||||
|
||||
/** @type {import('eslint').Config} */
|
||||
export default [
|
||||
...baseConfig,
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
react,
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
86
apps/directory/index.html
Normal file
86
apps/directory/index.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
<script
|
||||
id="counterscale-script"
|
||||
src="%VITE_INSIGHT_URL%"
|
||||
defer
|
||||
></script>
|
||||
<style>
|
||||
.loader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.loader.loaded {
|
||||
opacity: 0;
|
||||
transition: opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
}
|
||||
|
||||
.loader .icon,
|
||||
.loader .flasher {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-left: 0.3rem solid;
|
||||
border-bottom: 0.3rem solid;
|
||||
border-right: 0.3rem solid;
|
||||
border-top: 0.3rem solid;
|
||||
transform: rotate(-45deg);
|
||||
box-shadow:
|
||||
inset 0 0 16px 2px rgb(200 14 0),
|
||||
0 0 18px 5px rgb(200 14 0);
|
||||
border-color: rgba(54, 0, 0, 87%);
|
||||
}
|
||||
|
||||
.loader .flasher {
|
||||
position: absolute;
|
||||
box-shadow: unset;
|
||||
animation: icon-flash 1.2s cubic-bezier(0.2, 0.6, 0.2, 1)
|
||||
infinite;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.loader {
|
||||
background-color: #131313;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.loader {
|
||||
background-color: #ececec;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes icon-flash {
|
||||
100% {
|
||||
opacity: 0;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-color: rgb(0, 0, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div class="loader">
|
||||
<span class="icon"></span>
|
||||
<span class="flasher"></span>
|
||||
</div>
|
||||
<script type="module" src="/src/App.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
9
apps/directory/jsconfig.json
Normal file
9
apps/directory/jsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"!/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
43
apps/directory/package.json
Normal file
43
apps/directory/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@aklive2d/directory",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:directory": "vite --clearScreen false",
|
||||
"build": "mode=build node runner.js",
|
||||
"preview:directory": "vite preview",
|
||||
"lint": "eslint \"src/**/*.js\" \"src/**/*.jsx\" && stylelint \"src/**/*.css\" \"src/**/*.scss\" && prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"jotai": "^2.11.3",
|
||||
"react-router-dom": "^7.1.5",
|
||||
"react-simple-typewriter": "^5.0.1",
|
||||
"reset-css": "^5.0.2",
|
||||
"@aklive2d/eslint-config": "workspace:*",
|
||||
"@aklive2d/stylelint-config": "workspace:*",
|
||||
"@aklive2d/postcss-config": "workspace:*",
|
||||
"@aklive2d/config": "workspace:*",
|
||||
"@aklive2d/libs": "workspace:*",
|
||||
"@aklive2d/assets": "workspace:*",
|
||||
"@aklive2d/operator": "workspace:*",
|
||||
"@aklive2d/vite-helpers": "workspace:*",
|
||||
"@aklive2d/showcase": "workspace:*",
|
||||
"@aklive2d/module": "workspace:*",
|
||||
"@aklive2d/prettier-config": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"vite": "^6.1.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.18",
|
||||
"prop-types": "^15.8.1",
|
||||
"sass": "^1.84.0",
|
||||
"autoprefixer": "^10.4.20"
|
||||
}
|
||||
}
|
||||
5
apps/directory/postcss.config.js
Normal file
5
apps/directory/postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import baseConfig from '@aklive2d/postcss-config'
|
||||
/** @type {import('postcss').Config} */
|
||||
export default {
|
||||
...baseConfig,
|
||||
}
|
||||
11
apps/directory/prettier.config.js
Normal file
11
apps/directory/prettier.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import baseConfig from '@aklive2d/prettier-config'
|
||||
|
||||
/**
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
...baseConfig,
|
||||
semi: false,
|
||||
}
|
||||
|
||||
export default config
|
||||
24
apps/directory/runner.js
Normal file
24
apps/directory/runner.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { build as viteBuild } from 'vite'
|
||||
import { envParser } from '@aklive2d/libs'
|
||||
|
||||
const build = async (namesToBuild) => {
|
||||
if (!namesToBuild.length) {
|
||||
// skip as directory can only build
|
||||
// when all operators are built
|
||||
await viteBuild()
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { name } = envParser.parse({
|
||||
name: {
|
||||
type: 'string',
|
||||
short: 'n',
|
||||
multiple: true,
|
||||
default: [],
|
||||
},
|
||||
})
|
||||
await build(name)
|
||||
}
|
||||
|
||||
main()
|
||||
23
apps/directory/src/App.jsx
Normal file
23
apps/directory/src/App.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
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>
|
||||
)
|
||||
103
apps/directory/src/App.scss
Normal file
103
apps/directory/src/App.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
@import 'https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&family=Noto+Sans+KR:wght@400;500;700&family=Noto+Sans+SC:wght@400;500;700&family=Noto+Sans:wght@400;500;700&display=swap';
|
||||
@import 'https://fonts.cdnfonts.com/css/bender';
|
||||
@import 'https://fonts.cdnfonts.com/css/geometos';
|
||||
|
||||
@mixin light-theme() {
|
||||
--text-color: rgb(0 0 0 / 87%);
|
||||
--text-color-full: #000;
|
||||
--secondary-text-color: #97958d;
|
||||
--date-color: rgb(0 0 0 / 20%);
|
||||
--border-color: #8f8f8f;
|
||||
--link-highlight-color: #33b5e5;
|
||||
--drawer-background-color: rgb(255 255 255 / 88%);
|
||||
--root-background-color: #ececec;
|
||||
--home-item-hover-background-color: rgb(188 188 188 / 30%);
|
||||
--home-item-background-linear-gradient-color: rgb(0 0 0 / 10%);
|
||||
--home-item-outline-color: rgb(41 41 41 / 30%);
|
||||
--button-color: #999;
|
||||
}
|
||||
|
||||
@mixin dark-theme() {
|
||||
--text-color: rgb(255 255 255 / 87%);
|
||||
--text-color-full: #fff;
|
||||
--secondary-text-color: #686a72;
|
||||
--date-color: rgb(255 255 255 / 20%);
|
||||
--border-color: #707070;
|
||||
--link-highlight-color: #33b5e5;
|
||||
--drawer-background-color: rgb(0 0 0 / 88%);
|
||||
--root-background-color: #131313;
|
||||
--home-item-hover-background-color: rgb(67 67 67 / 30%);
|
||||
--home-item-background-linear-gradient-color: rgb(255 255 255 / 10%);
|
||||
--home-item-outline-color: rgb(214 214 214 / 30%);
|
||||
--button-color: #666;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
@include dark-theme;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
@include light-theme;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family:
|
||||
Geometos, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans',
|
||||
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;
|
||||
|
||||
@media only screen and (width <= 430px) {
|
||||
& {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 1600px) {
|
||||
& {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
:root::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
#root::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:root::-webkit-scrollbar-thumb {
|
||||
background-color: var(--border-color);
|
||||
border: 5px solid transparent;
|
||||
background-clip: padding-box;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
:root::-webkit-scrollbar-thumb:hover {
|
||||
border: 4px solid transparent;
|
||||
background-color: var(--text-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
9
apps/directory/src/component/border.jsx
Normal file
9
apps/directory/src/component/border.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
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,
|
||||
}
|
||||
28
apps/directory/src/component/char_icon.jsx
Normal file
28
apps/directory/src/component/char_icon.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default function CharIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox={props.viewBox}
|
||||
style={props.style}
|
||||
>
|
||||
{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,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
99
apps/directory/src/component/dropdown.jsx
Normal file
99
apps/directory/src/component/dropdown.jsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { 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 : ''} ${props.left ? classes.left : ''}`}
|
||||
>
|
||||
<section
|
||||
className={classes.text}
|
||||
onClick={() => toggleDropdown()}
|
||||
>
|
||||
<span className={classes.content}>{props.text}</span>
|
||||
<span
|
||||
className={classes.icon}
|
||||
style={props.iconStyle}
|
||||
></span>
|
||||
<section className={classes.popup}>
|
||||
<span className={classes.text}>{props.altText}</span>
|
||||
</section>
|
||||
</section>
|
||||
<ul className={classes.menu} style={props.activeColor}>
|
||||
{props.menu.map((item) => {
|
||||
switch (item.type) {
|
||||
case 'date': {
|
||||
return (
|
||||
<section
|
||||
key={item.name}
|
||||
className={classes.date}
|
||||
>
|
||||
<section className={classes.line} />
|
||||
<section className={classes.text}>
|
||||
{item.name}
|
||||
</section>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
case 'custom': {
|
||||
return item.component
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<li
|
||||
key={item.name}
|
||||
className={`${classes.item} ${item.name === props.text || (props.activeRule && props.activeRule(item)) ? classes.active : ''}`}
|
||||
onClick={() => {
|
||||
props.onClick(item)
|
||||
toggleDropdown()
|
||||
}}
|
||||
style={
|
||||
item.color
|
||||
? { color: item.color }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{item.icon ? (
|
||||
<section
|
||||
className={classes['item-icon']}
|
||||
>
|
||||
{item.icon}
|
||||
</section>
|
||||
) : null}
|
||||
<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,
|
||||
altText: PropTypes.string,
|
||||
iconStyle: PropTypes.object,
|
||||
left: PropTypes.bool,
|
||||
}
|
||||
48
apps/directory/src/component/popup.jsx
Normal file
48
apps/directory/src/component/popup.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { 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,
|
||||
}
|
||||
38
apps/directory/src/component/return_button.jsx
Normal file
38
apps/directory/src/component/return_button.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
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,
|
||||
}
|
||||
30
apps/directory/src/component/scss/border.module.scss
Normal file
30
apps/directory/src/component/scss/border.module.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
.border {
|
||||
position: relative;
|
||||
bottom: 1px;
|
||||
border-bottom: 1px solid var(--text-color);
|
||||
|
||||
@media only screen and (width <= 430px) {
|
||||
& {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
top: -2px;
|
||||
background-color: var(--text-color);
|
||||
}
|
||||
|
||||
&::before {
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
187
apps/directory/src/component/scss/dropdown.module.scss
Normal file
187
apps/directory/src/component/scss/dropdown.module.scss
Normal file
@@ -0,0 +1,187 @@
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
user-select: none;
|
||||
z-index: 2;
|
||||
padding: 0.5em;
|
||||
cursor: pointer;
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
height: 2em;
|
||||
min-width: 2em;
|
||||
|
||||
.popup {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
background-color: var(--root-background-color);
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
max-height: 61.8vh;
|
||||
max-width: 61.8vw;
|
||||
z-index: -1;
|
||||
top: 2.5em;
|
||||
right: 0;
|
||||
cursor: auto;
|
||||
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
overflow: auto;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
visibility: hidden;
|
||||
|
||||
.text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.popup {
|
||||
visibility: unset;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-right: 1.2em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
bottom: 0.5em;
|
||||
right: 0.6em;
|
||||
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: translateY(-0.7em) rotate(-45deg);
|
||||
}
|
||||
|
||||
.menu {
|
||||
scrollbar-gutter: stable;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
background-color: var(--root-background-color);
|
||||
width: max-content;
|
||||
max-height: 61.8vh;
|
||||
max-width: 61.8vw;
|
||||
z-index: -1;
|
||||
top: 2.5em;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-flow: column nowrap;
|
||||
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
overflow: auto;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
visibility: hidden;
|
||||
color: var(--link-highlight-color);
|
||||
cursor: auto;
|
||||
|
||||
&.left {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-family:
|
||||
Bender, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR',
|
||||
'Noto Sans', sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.line {
|
||||
height: 1px;
|
||||
flex-grow: 1;
|
||||
background-color: var(--text-color);
|
||||
margin: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
flex: 1;
|
||||
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.item-icon svg {
|
||||
transition: fill cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
width: 1rem;
|
||||
fill: var(--text-color);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.active {
|
||||
.text {
|
||||
color: currentcolor;
|
||||
}
|
||||
|
||||
.item-icon svg {
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
.popup,
|
||||
.menu {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
z-index: -1;
|
||||
position: fixed;
|
||||
inset: 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;
|
||||
}
|
||||
}
|
||||
96
apps/directory/src/component/scss/popup.module.scss
Normal file
96
apps/directory/src/component/scss/popup.module.scss
Normal file
@@ -0,0 +1,96 @@
|
||||
.entry-text {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: fixed;
|
||||
inset: 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-flow: column nowrap;
|
||||
align-items: stretch;
|
||||
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;
|
||||
place-content: center space-between;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
font-family:
|
||||
Geometos, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR',
|
||||
'Noto Sans', 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;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
inset: 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 (width <= 768px) {
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.return-button {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
apps/directory/src/component/scss/return_button.module.scss
Normal file
55
apps/directory/src/component/scss/return_button.module.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
.return-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 0.6rem 0;
|
||||
width: 3rem;
|
||||
cursor: pointer;
|
||||
|
||||
%arrow-shared {
|
||||
border-top: 0.24rem solid transparent;
|
||||
border-bottom: 0.24rem solid transparent;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
78
apps/directory/src/component/scss/search_box.module.scss
Normal file
78
apps/directory/src/component/scss/search_box.module.scss
Normal file
@@ -0,0 +1,78 @@
|
||||
.search-box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
margin: 0.5rem;
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
border-left: 0.15rem solid var(--text-color-full);
|
||||
border-bottom: 0.15rem solid var(--text-color-full);
|
||||
border-right: 0.15rem solid var(--text-color-full);
|
||||
border-top: 0.15rem solid var(--text-color-full);
|
||||
transform: translate(0.2rem, 0.3rem) rotate(-45deg);
|
||||
}
|
||||
|
||||
.icon-dot {
|
||||
position: absolute;
|
||||
background-color: var(--text-color-full);
|
||||
width: 0.15rem;
|
||||
height: 0.6rem;
|
||||
transform: translate(1.2rem, 1.1rem) rotate(-45deg);
|
||||
}
|
||||
|
||||
.input {
|
||||
flex-grow: 1;
|
||||
font-size: 1.5rem;
|
||||
width: 100%;
|
||||
margin-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
background-color: transparent;
|
||||
border: unset;
|
||||
border-bottom: 0.15em solid var(--home-item-outline-color);
|
||||
color: var(--text-color);
|
||||
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:hover {
|
||||
outline: none;
|
||||
border-bottom: 0.15em solid var(--text-color);
|
||||
}
|
||||
|
||||
.icon-clear {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
visibility: hidden;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
visibility: unset;
|
||||
}
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
height: 0.2rem;
|
||||
background-color: var(--text-color);
|
||||
|
||||
&:nth-child(1) {
|
||||
transform: translate(0, 0.8rem) rotate(45deg);
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
transform: translate(0, 0.8rem) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
apps/directory/src/component/scss/switch.module.scss
Normal file
64
apps/directory/src/component/scss/switch.module.scss
Normal file
@@ -0,0 +1,64 @@
|
||||
.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: 0.65em;
|
||||
right: 18px;
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
border-left: 0.15em solid currentcolor;
|
||||
border-bottom: 0.15em solid currentcolor;
|
||||
border-right: 0.15em solid currentcolor;
|
||||
border-top: 0.15em solid currentcolor;
|
||||
transform: translate(0, -0.15em) 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: 1.1em;
|
||||
right: 6px;
|
||||
width: 18px;
|
||||
height: 0.15em;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
apps/directory/src/component/scss/totop_button.module.scss
Normal file
43
apps/directory/src/component/scss/totop_button.module.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
.totop-button {
|
||||
position: fixed;
|
||||
user-select: none;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
right: 2rem;
|
||||
bottom: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: unset;
|
||||
}
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0.3rem;
|
||||
background-color: var(--text-color-full);
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
.bar:nth-child(1) {
|
||||
transform: rotateZ(45deg) scaleX(0.5) translateX(45%);
|
||||
}
|
||||
|
||||
.bar:nth-child(2) {
|
||||
transform: rotateZ(-45deg) scaleX(0.5) translateX(-45%);
|
||||
}
|
||||
|
||||
.bar:nth-child(3),
|
||||
.bar:nth-child(4) {
|
||||
transform: translateY(450%) rotateZ(90deg) scaleX(0.5);
|
||||
}
|
||||
}
|
||||
50
apps/directory/src/component/search_box.jsx
Normal file
50
apps/directory/src/component/search_box.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classes from './scss/search_box.module.scss'
|
||||
import { useI18n } from '@/state/language'
|
||||
|
||||
export default function SearchBox(props) {
|
||||
const { i18n } = useI18n()
|
||||
const [searchField, setSearchField] = useState('')
|
||||
|
||||
const filterBySearch = (event) => {
|
||||
const query = event.target.value
|
||||
props.handleOnChange(query)
|
||||
setSearchField(query)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section
|
||||
className={`${classes['search-box']} ${props.className ? props.className : ''}`}
|
||||
>
|
||||
<section className={classes.icon} />
|
||||
<section className={classes['icon-dot']} />
|
||||
<input
|
||||
type="text"
|
||||
className={classes.input}
|
||||
placeholder={i18n(props.altText)}
|
||||
onChange={filterBySearch}
|
||||
value={searchField}
|
||||
/>
|
||||
<section
|
||||
className={`${classes['icon-clear']} ${searchField === '' ? '' : classes.active}`}
|
||||
onClick={() => {
|
||||
setSearchField('')
|
||||
props.handleOnChange('')
|
||||
}}
|
||||
>
|
||||
<section className={classes.line} />
|
||||
<section className={classes.line} />
|
||||
</section>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
SearchBox.propTypes = {
|
||||
className: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
altText: PropTypes.string,
|
||||
handleOnChange: PropTypes.func,
|
||||
searchField: PropTypes.string,
|
||||
}
|
||||
35
apps/directory/src/component/switch.jsx
Normal file
35
apps/directory/src/component/switch.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { 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,
|
||||
}
|
||||
64
apps/directory/src/component/totop_button.jsx
Normal file
64
apps/directory/src/component/totop_button.jsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classes from './scss/totop_button.module.scss'
|
||||
|
||||
export default function ToTopButton(props) {
|
||||
const [hidden, setHidden] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const handleButton = () => {
|
||||
const scrollBarPos = window.scrollY || 0
|
||||
setHidden(!(scrollBarPos > 100))
|
||||
}
|
||||
window.addEventListener('scroll', handleButton)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleButton)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const smoothScroll = useCallback((target) => {
|
||||
const targetElement = document.querySelector(target)
|
||||
const targetPosition =
|
||||
targetElement.getBoundingClientRect().top + window.scrollY
|
||||
const startPosition = window.scrollY
|
||||
const distance = targetPosition - startPosition
|
||||
const duration = 1000
|
||||
let start = null
|
||||
window.requestAnimationFrame(step)
|
||||
function step(timestamp) {
|
||||
if (!start) start = timestamp
|
||||
const progress = timestamp - start
|
||||
window.scrollTo(
|
||||
0,
|
||||
easeInOutCubic(progress, startPosition, distance, duration)
|
||||
)
|
||||
if (progress < duration) window.requestAnimationFrame(step)
|
||||
}
|
||||
function easeInOutCubic(t, b, c, d) {
|
||||
t /= d / 2
|
||||
if (t < 1) return (c / 2) * t * t * t + b
|
||||
t -= 2
|
||||
return (c / 2) * (t * t * t + 2) + b
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<section
|
||||
className={`${classes['totop-button']} ${hidden ? '' : classes.show} ${props.className ? props.className : ''}`}
|
||||
onClick={() => {
|
||||
smoothScroll('#root')
|
||||
}}
|
||||
>
|
||||
<section className={classes.bar}></section>
|
||||
<section className={classes.bar}></section>
|
||||
<section className={classes.bar}></section>
|
||||
<section className={classes.bar}></section>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
ToTopButton.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
46
apps/directory/src/component/voice.jsx
Normal file
46
apps/directory/src/component/voice.jsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { 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,
|
||||
}
|
||||
161
apps/directory/src/i18n.json
Normal file
161
apps/directory/src/i18n.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"available": ["zh-CN", "en-US"],
|
||||
"key": {
|
||||
"dynamic_compile": {
|
||||
"zh-CN": "动态集录",
|
||||
"en-US": "Dynamic Compile"
|
||||
},
|
||||
"home": {
|
||||
"zh-CN": "首页",
|
||||
"en-US": "Home"
|
||||
},
|
||||
"official_page": {
|
||||
"zh-CN": "官方页面",
|
||||
"en-US": "Official 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"
|
||||
},
|
||||
"music": {
|
||||
"zh-CN": "音乐",
|
||||
"en-US": "Music"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"switch_language": {
|
||||
"zh-CN": "🌐 切换语言",
|
||||
"en-US": "🌐 Switch Language"
|
||||
},
|
||||
"fast_navigation": {
|
||||
"zh-CN": "🧭 快速导航",
|
||||
"en-US": "🧭 Fast Navigation"
|
||||
},
|
||||
"return": {
|
||||
"zh-CN": "↩️ 返回",
|
||||
"en-US": "↩️ Return"
|
||||
},
|
||||
"search_by_name": {
|
||||
"zh-CN": "名字搜索",
|
||||
"en-US": "Search by Name"
|
||||
},
|
||||
"new_op_wait_to_update": {
|
||||
"zh-CN": "个新干员等待更新",
|
||||
"en-US": "New Operator(s) Waiting to Update"
|
||||
}
|
||||
}
|
||||
}
|
||||
197
apps/directory/src/routes/Error.jsx
Normal file
197
apps/directory/src/routes/Error.jsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { 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 '@aklive2d/module'
|
||||
import useInsight from '@/state/insight'
|
||||
import buildConfig from '!/config.json'
|
||||
|
||||
const voiceOnAtom = atomWithStorage('voiceOn', false)
|
||||
const config = buildConfig.error_files
|
||||
const obj = config.files[Math.floor(Math.random() * config.files.length)]
|
||||
const filename = obj.key.replace(/#/g, '%23')
|
||||
const padding = obj.paddings
|
||||
let lastVoiceState = 'ended'
|
||||
|
||||
export default function Error() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const _trackEvt = useInsight()
|
||||
const error = useRouteError()
|
||||
const navigate = useNavigate()
|
||||
const { setTitle } = useHeader()
|
||||
const [voiceOn, setVoiceOn] = useAtom(voiceOnAtom)
|
||||
const [spineDone, _setSpineDone] = useState(false)
|
||||
const spineRef = useRef(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)
|
||||
}, [error])
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(content[0])
|
||||
}, [content, setTitle])
|
||||
|
||||
useEffect(() => {
|
||||
if (!voiceOn) {
|
||||
setVoiceSrc(null)
|
||||
} else {
|
||||
setVoiceSrc(`/${buildConfig.directory_folder}/error.ogg`)
|
||||
if (spinePlayer) {
|
||||
spinePlayer.animationState.setAnimation(0, 'Interact', false, 0)
|
||||
spinePlayer.animationState.addAnimation(0, 'Relax', true, 0)
|
||||
}
|
||||
}
|
||||
}, [spinePlayer, 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) {
|
||||
setSpinePlayer(
|
||||
new spine.SpinePlayer(spineRef.current, {
|
||||
skelUrl: `./_assets/${filename}.skel`,
|
||||
atlasUrl: `./_assets/${filename}.atlas`,
|
||||
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()
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
return () => {
|
||||
if (spinePlayer) {
|
||||
spinePlayer.dispose()
|
||||
}
|
||||
}
|
||||
}, [playVoice, spinePlayer])
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
307
apps/directory/src/routes/Root.jsx
Normal file
307
apps/directory/src/routes/Root.jsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import { 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 Dropdown from '@/component/dropdown'
|
||||
import Popup from '@/component/popup'
|
||||
import Border from '@/component/border'
|
||||
import CharIcon from '@/component/char_icon'
|
||||
import ToTopButton from '@/component/totop_button'
|
||||
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
export default function Root() {
|
||||
const [drawerHidden, setDrawerHidden] = useState(true)
|
||||
const { title, tabs, setCurrentTab, headerIcon } = useHeader()
|
||||
const { extraArea } = useAppbar()
|
||||
const { fetchOfficialUpdate } = useConfig()
|
||||
|
||||
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(() => {
|
||||
fetchOfficialUpdate()
|
||||
}, [fetchOfficialUpdate])
|
||||
|
||||
useEffect(() => {
|
||||
document.querySelector('.loader').classList.add('loaded')
|
||||
setTimeout(() => {
|
||||
document.querySelector('.loader').style.display = 'none'
|
||||
}, 500)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className={header.header}>
|
||||
<section
|
||||
className={`${header['nav-button']} ${drawerHidden ? '' : header.active}`}
|
||||
onClick={() => toggleDrawer()}
|
||||
>
|
||||
<section className={header.bar} />
|
||||
<section className={header.bar} />
|
||||
<section className={header.bar} />
|
||||
</section>
|
||||
<HeaderButton />
|
||||
<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>
|
||||
<Border />
|
||||
<ToTopButton />
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
</main>
|
||||
<FooterElement />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function FooterElement() {
|
||||
const { i18n } = useI18n()
|
||||
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://gura.ch/pp"
|
||||
target="_blank"
|
||||
className={footer.link}
|
||||
>
|
||||
{i18n('privacy_policy')}
|
||||
</Link>
|
||||
</section>
|
||||
<section className={footer.item}>
|
||||
<Link
|
||||
reloadDocument
|
||||
to="https://gura.ch/aklive2d-gh"
|
||||
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>
|
||||
</section>
|
||||
</footer>
|
||||
)
|
||||
}, [i18n, navigate])
|
||||
}
|
||||
|
||||
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)}
|
||||
altText={i18n('switch_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 HeaderButton() {
|
||||
const navigate = useNavigate()
|
||||
const { i18n } = useI18n()
|
||||
const { fastNavigation } = useHeader()
|
||||
|
||||
if (fastNavigation.length > 0) {
|
||||
return (
|
||||
<Dropdown
|
||||
menu={fastNavigation}
|
||||
altText={i18n('fast_navigation')}
|
||||
onClick={(item) => {
|
||||
navigate(item.value)
|
||||
}}
|
||||
className={header['fast-navigate']}
|
||||
iconStyle={{
|
||||
borderWidth: '0.15em',
|
||||
width: '1em',
|
||||
transform:
|
||||
'translateY(-0.4rem) translateX(-0.7rem) rotate(-45deg)',
|
||||
height: '1em',
|
||||
}}
|
||||
left={true}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<section className={header['back-arrow']}>
|
||||
<Link to="/" className={header.link}>
|
||||
<section className={header.arrow1} />
|
||||
<section className={header.arrow2} />
|
||||
</Link>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
29
apps/directory/src/routes/index.jsx
Normal file
29
apps/directory/src/routes/index.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import Home from '@/routes/path/Home'
|
||||
import Operator from '@/routes/path/Operator'
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/',
|
||||
index: true,
|
||||
name: 'home',
|
||||
element: <Home />,
|
||||
inDrawer: true,
|
||||
routeable: true,
|
||||
},
|
||||
{
|
||||
path: 'https://gura.ch/dynamicCompile',
|
||||
index: false,
|
||||
name: 'official_page',
|
||||
element: <a />,
|
||||
inDrawer: true,
|
||||
routeable: false,
|
||||
},
|
||||
{
|
||||
path: ':key',
|
||||
index: false,
|
||||
name: 'operator',
|
||||
element: <Operator />,
|
||||
inDrawer: false,
|
||||
routeable: true,
|
||||
},
|
||||
]
|
||||
451
apps/directory/src/routes/path/Home.jsx
Normal file
451
apps/directory/src/routes/path/Home.jsx
Normal file
@@ -0,0 +1,451 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { NavLink, Link } from 'react-router-dom'
|
||||
import classes from '@/scss/home/Home.module.scss'
|
||||
import { useConfig } from '@/state/config'
|
||||
import { useI18n } from '@/state/language'
|
||||
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 useInsight from '@/state/insight'
|
||||
import Switch from '@/component/switch'
|
||||
import SearchBox from '@/component/search_box'
|
||||
import buildConfig from '!/config.json'
|
||||
|
||||
const voiceOnAtom = atomWithStorage('voiceOn', false)
|
||||
let lastVoiceState = 'ended'
|
||||
|
||||
export default function Home() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const _trackEvt = useInsight()
|
||||
const { setTitle, setTabs, currentTab, setHeaderIcon, setFastNavigation } =
|
||||
useHeader()
|
||||
const { config, operators, officialUpdate } = useConfig()
|
||||
const { i18n } = useI18n()
|
||||
const [content, setContent] = useState([])
|
||||
const [voiceOn] = useAtom(voiceOnAtom)
|
||||
const [voiceSrc, setVoiceSrc] = useState(null)
|
||||
const [voiceReplay, setVoiceReplay] = useState(false)
|
||||
const { language } = useLanguage()
|
||||
const [navigationList, setNavigationList] = useState([])
|
||||
const [searchField, setSearchField] = useState('')
|
||||
const [updatedList, setUpdatedList] = useState([])
|
||||
|
||||
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 fastNavigateDict = useMemo(() => {
|
||||
const dict = {}
|
||||
operators.forEach((item) => {
|
||||
if (!(item.date in dict)) {
|
||||
dict[item.date] = []
|
||||
}
|
||||
dict[item.date].push({
|
||||
codename: item.codename,
|
||||
link: item.link,
|
||||
type: item.type,
|
||||
color: item.color,
|
||||
})
|
||||
})
|
||||
return dict
|
||||
}, [operators])
|
||||
|
||||
useEffect(() => {
|
||||
const list = []
|
||||
for (const [key, value] of Object.entries(fastNavigateDict)) {
|
||||
const newValue = value.filter((item) => isShown(item.type))
|
||||
if (newValue.length > 0) {
|
||||
list.push({
|
||||
name: key,
|
||||
value: null,
|
||||
type: 'date',
|
||||
})
|
||||
newValue.forEach((item) => {
|
||||
list.push({
|
||||
name: item.codename[language],
|
||||
value: item.link,
|
||||
type: 'item',
|
||||
color: item.color,
|
||||
icon: (
|
||||
<CharIcon
|
||||
type={item.type}
|
||||
viewBox={
|
||||
item.type === 'operator'
|
||||
? '0 0 88.969 71.469'
|
||||
: '0 0 94.563 67.437'
|
||||
}
|
||||
/>
|
||||
),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
setNavigationList(list)
|
||||
setUpdatedList(list)
|
||||
}, [fastNavigateDict, isShown, language])
|
||||
|
||||
useEffect(() => {
|
||||
const list = navigationList.filter((item) => {
|
||||
return (
|
||||
item.name.toLowerCase().indexOf(searchField.toLowerCase()) !==
|
||||
-1 || item.type === 'date'
|
||||
)
|
||||
})
|
||||
const newList = []
|
||||
for (let i = 0; i < list.length - 1; i++) {
|
||||
const firstType = list[i].type
|
||||
const secondType = list[i + 1].type
|
||||
if (firstType === 'date' && secondType === 'date') {
|
||||
continue
|
||||
}
|
||||
newList.push(list[i])
|
||||
}
|
||||
if (list.length > 0 && list[list.length - 1].type !== 'date') {
|
||||
newList.push(list[list.length - 1])
|
||||
}
|
||||
setUpdatedList(newList)
|
||||
}, [navigationList, searchField])
|
||||
|
||||
useEffect(() => {
|
||||
setFastNavigation([
|
||||
{
|
||||
type: 'custom',
|
||||
component: (
|
||||
<SearchBox
|
||||
key="search-box"
|
||||
altText={'search_by_name'}
|
||||
handleOnChange={(e) => {
|
||||
setSearchField(e)
|
||||
}}
|
||||
searchField={searchField}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...updatedList,
|
||||
])
|
||||
}, [searchField, setFastNavigation, updatedList])
|
||||
|
||||
const handleVoicePlay = useCallback(
|
||||
(src) => {
|
||||
if (!voiceOn) {
|
||||
setVoiceSrc(null)
|
||||
} else {
|
||||
if (src === voiceSrc && lastVoiceState === 'ended') {
|
||||
setVoiceReplay(true)
|
||||
} else {
|
||||
setVoiceSrc(src)
|
||||
}
|
||||
}
|
||||
},
|
||||
[voiceOn, voiceSrc]
|
||||
)
|
||||
|
||||
return (
|
||||
<section>
|
||||
{officialUpdate.length > operators.length && (
|
||||
<section>
|
||||
<section
|
||||
className={`${classes['official-update']} ${classes.group}`}
|
||||
>
|
||||
<section className={classes.info}>
|
||||
<section className={classes.content}>
|
||||
<section className={classes.text}>
|
||||
{officialUpdate.length - operators.length}{' '}
|
||||
{i18n('new_op_wait_to_update')}
|
||||
</section>
|
||||
<section
|
||||
className={`${classes['styled-selection']}`}
|
||||
>
|
||||
{officialUpdate.dates
|
||||
.reduce((acc, cur) => {
|
||||
const op = officialUpdate[cur]
|
||||
return [...acc, ...op]
|
||||
}, [])
|
||||
.slice(
|
||||
0,
|
||||
officialUpdate.length -
|
||||
operators.length
|
||||
)
|
||||
.map((entry, index) => {
|
||||
return (
|
||||
<Link
|
||||
reloadDocument
|
||||
to={entry.link}
|
||||
target="_blank"
|
||||
style={{
|
||||
color: entry.color,
|
||||
}}
|
||||
key={index}
|
||||
>
|
||||
<section
|
||||
className={
|
||||
classes.content
|
||||
}
|
||||
>
|
||||
<section
|
||||
className={
|
||||
classes.option
|
||||
}
|
||||
>
|
||||
<section
|
||||
className={
|
||||
classes.outline
|
||||
}
|
||||
/>
|
||||
<section
|
||||
className={`${classes.text} ${classes.container}`}
|
||||
>
|
||||
<section
|
||||
className={
|
||||
classes.type
|
||||
}
|
||||
>
|
||||
<CharIcon
|
||||
type={
|
||||
entry.type
|
||||
}
|
||||
viewBox={
|
||||
entry.type ===
|
||||
'operator'
|
||||
? '0 0 88.969 71.469'
|
||||
: '0 0 94.563 67.437'
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
<section
|
||||
className={
|
||||
classes.title
|
||||
}
|
||||
>
|
||||
{
|
||||
entry
|
||||
.codename[
|
||||
language
|
||||
]
|
||||
}
|
||||
</section>
|
||||
<section
|
||||
className={
|
||||
classes[
|
||||
'arrow-icon'
|
||||
]
|
||||
}
|
||||
>
|
||||
<section
|
||||
className={
|
||||
classes.bar
|
||||
}
|
||||
></section>
|
||||
<section
|
||||
className={
|
||||
classes.bar
|
||||
}
|
||||
></section>
|
||||
<section
|
||||
className={
|
||||
classes.bar
|
||||
}
|
||||
></section>
|
||||
<section
|
||||
className={
|
||||
classes.bar
|
||||
}
|
||||
></section>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
<section className={classes.date}>
|
||||
{officialUpdate.latest}
|
||||
</section>
|
||||
</section>
|
||||
<Border />
|
||||
</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}>
|
||||
<section className={classes['operator-group']}>
|
||||
{v.map((item) => {
|
||||
return (
|
||||
<OperatorElement
|
||||
key={item.link}
|
||||
item={item}
|
||||
hidden={!isShown(item.type)}
|
||||
handleVoicePlay={handleVoicePlay}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
<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/${buildConfig.voice_folders.main}/${buildConfig.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={`/${buildConfig.directory_folder}/${item.fallback_name.replace(/#/g, '%23')}_portrait.png`}
|
||||
alt={item.codename[language]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
ImageElement.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
fallback_name: PropTypes.string,
|
||||
codename: PropTypes.object,
|
||||
}
|
||||
694
apps/directory/src/routes/path/Operator.jsx
Normal file
694
apps/directory/src/routes/path/Operator.jsx
Normal file
@@ -0,0 +1,694 @@
|
||||
import { 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 VoiceElement from '@/component/voice'
|
||||
import useInsight from '@/state/insight'
|
||||
import { spine } from '@aklive2d/module'
|
||||
import Border from '@/component/border'
|
||||
import { useI18n } from '@/state/language'
|
||||
import Switch from '@/component/switch'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import buildConfig from '!/config.json'
|
||||
|
||||
const musicMapping = buildConfig.music_mapping
|
||||
const getVoiceFoler = (lang) => {
|
||||
const folderObject = buildConfig.voice_folders
|
||||
const voiceFolder =
|
||||
folderObject.sub.find((e) => e.lang === lang) ||
|
||||
folderObject.sub.find((e) => e.name === 'custom')
|
||||
return `${folderObject.main}/${voiceFolder.name}`
|
||||
}
|
||||
const defaultSpineAnimationName = 'Idle'
|
||||
const backgroundAtom = atom(buildConfig.default_background)
|
||||
|
||||
const getPartialName = (type, input) => {
|
||||
let part
|
||||
switch (type) {
|
||||
case 'name':
|
||||
part = 5
|
||||
break
|
||||
case 'skin':
|
||||
part = 1
|
||||
break
|
||||
default:
|
||||
return input
|
||||
}
|
||||
return input.replace(/^(.+)( )(·|\/)( )(.+)$/, `$${part}`)
|
||||
}
|
||||
|
||||
const getTabName = (item, language) => {
|
||||
if (item.type === 'operator') {
|
||||
return 'operator'
|
||||
} else {
|
||||
return getPartialName('skin', item.codename[language])
|
||||
}
|
||||
}
|
||||
|
||||
export default function Operator() {
|
||||
const navigate = useNavigate()
|
||||
const { operators } = useConfig()
|
||||
const { language } = useLanguage()
|
||||
const { key } = useParams()
|
||||
const { setTitle, setTabs, setHeaderIcon, setFastNavigation } = useHeader()
|
||||
const { setExtraArea } = useAppbar()
|
||||
const [config, setConfig] = useState(null)
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const _trackEvt = useInsight(`/${key}`)
|
||||
const spineRef = useRef(null)
|
||||
const [spineAnimationName, setSpineAnimationName] = useState(
|
||||
defaultSpineAnimationName
|
||||
)
|
||||
const { i18n } = useI18n()
|
||||
const [spinePlayer, setSpinePlayer] = useState(null)
|
||||
const [voiceLang, _setVoiceLang] = useState(null)
|
||||
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([])
|
||||
setFastNavigation([])
|
||||
}, [setExtraArea, setFastNavigation])
|
||||
|
||||
useEffect(() => {
|
||||
const config = operators.find((item) => item.link === key)
|
||||
if (config) {
|
||||
setConfig(config)
|
||||
configRef.current = config
|
||||
setSpineAnimationName(defaultSpineAnimationName)
|
||||
setHeaderIcon(config.type)
|
||||
if (spineRef.current?.children.length > 0) {
|
||||
spineRef.current?.removeChild(spineRef.current?.children[0])
|
||||
}
|
||||
fetch(`/${key}/assets/charword_table.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(getPartialName('name', config.codename[language]))
|
||||
}
|
||||
}, [config, language, key, setTitle])
|
||||
|
||||
useEffect(() => {
|
||||
if (spineRef.current?.children.length === 0 && configRef.current) {
|
||||
const playerConfig = {
|
||||
atlasUrl: `./${key}/assets/${configRef.current.filename.replace(/#/g, '%23')}.atlas`,
|
||||
animation: spineAnimationName,
|
||||
premultipliedAlpha: true,
|
||||
alpha: true,
|
||||
backgroundColor: '#00000000',
|
||||
viewport: {
|
||||
debugRender: false,
|
||||
padLeft: `${configRef.current.viewport_left}%`,
|
||||
padRight: `${configRef.current.viewport_right}%`,
|
||||
padTop: `${configRef.current.viewport_top}%`,
|
||||
padBottom: `${configRef.current.viewport_bottom}%`,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
showControls: false,
|
||||
touch: false,
|
||||
fps: 60,
|
||||
defaultMix: 0.3,
|
||||
success: (player) => {
|
||||
if (
|
||||
player.skeleton.data.animations
|
||||
.map((e) => e.name)
|
||||
.includes('Start')
|
||||
) {
|
||||
player.animationState.setAnimation(0, 'Start', false, 0)
|
||||
player.animationState.addAnimation(0, 'Idle', true, 0)
|
||||
}
|
||||
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
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if (configRef.current.use_json) {
|
||||
playerConfig.jsonUrl = `./${key}/assets/${configRef.current.filename.replace(/#/g, '%23')}.json`
|
||||
} else {
|
||||
playerConfig.skelUrl = `./${key}/assets/${configRef.current.filename.replace(/#/g, '%23')}.skel`
|
||||
}
|
||||
setSpinePlayer(
|
||||
new spine.SpinePlayer(spineRef.current, playerConfig)
|
||||
)
|
||||
}
|
||||
}, [spineAnimationName, setSpinePlayer, spinePlayer, key])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (spinePlayer) {
|
||||
spinePlayer.dispose()
|
||||
}
|
||||
}
|
||||
}, [spinePlayer])
|
||||
|
||||
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 setSpineAnimation = useCallback(
|
||||
(animation) => {
|
||||
playAnimationVoice(animation)
|
||||
const entry = spinePlayer.animationState.setAnimation(
|
||||
0,
|
||||
animation,
|
||||
true
|
||||
)
|
||||
entry.mixDuration = 0.3
|
||||
setSpineAnimationName(animation)
|
||||
},
|
||||
[playAnimationVoice, spinePlayer]
|
||||
)
|
||||
|
||||
const spineSettings = [
|
||||
{
|
||||
name: 'animation',
|
||||
options: [
|
||||
{
|
||||
name: 'idle',
|
||||
onClick: () => setSpineAnimation('Idle'),
|
||||
activeRule: () => {
|
||||
return spineAnimationName === 'Idle'
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'interact',
|
||||
onClick: () => setSpineAnimation('Interact'),
|
||||
activeRule: () => {
|
||||
return spineAnimationName === 'Interact'
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'special',
|
||||
onClick: () => setSpineAnimation('Special'),
|
||||
activeRule: () => {
|
||||
return spineAnimationName === '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(spineAnimationName)
|
||||
}
|
||||
},
|
||||
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:
|
||||
buildConfig.background_files.map((item) => {
|
||||
return {
|
||||
name: item,
|
||||
onClick: () => {
|
||||
setCurrentBackground(item)
|
||||
},
|
||||
activeRule: () => {
|
||||
return currentBackground === item
|
||||
},
|
||||
}
|
||||
}) || [],
|
||||
},
|
||||
]
|
||||
|
||||
if (!buildConfig.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} ${classes['no-overflow']}`}
|
||||
>
|
||||
{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?aklive2d`}
|
||||
target="_blank"
|
||||
style={{
|
||||
color: config?.color,
|
||||
}}
|
||||
>
|
||||
<section className={classes.content}>
|
||||
<section className={classes.option}>
|
||||
<section className={classes.outline} />
|
||||
<section
|
||||
className={`${classes.text} ${classes['no-overflow']}`}
|
||||
>
|
||||
{i18n('web_version')}
|
||||
</section>
|
||||
<section
|
||||
className={classes['arrow-icon']}
|
||||
>
|
||||
<section
|
||||
className={classes.bar}
|
||||
></section>
|
||||
<section
|
||||
className={classes.bar}
|
||||
></section>
|
||||
<section
|
||||
className={classes.bar}
|
||||
></section>
|
||||
<section
|
||||
className={classes.bar}
|
||||
></section>
|
||||
</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} ${classes['no-overflow']}`}
|
||||
>
|
||||
{i18n('steam_workshop')}
|
||||
</section>
|
||||
<section
|
||||
className={
|
||||
classes['arrow-icon']
|
||||
}
|
||||
>
|
||||
<section
|
||||
className={classes.bar}
|
||||
></section>
|
||||
<section
|
||||
className={classes.bar}
|
||||
></section>
|
||||
<section
|
||||
className={classes.bar}
|
||||
></section>
|
||||
<section
|
||||
className={classes.bar}
|
||||
></section>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
<section
|
||||
className={classes.container}
|
||||
style={
|
||||
currentBackground && {
|
||||
backgroundImage: `url(/chen/assets/${buildConfig.background_folder}/${currentBackground})`,
|
||||
}
|
||||
}
|
||||
>
|
||||
{config && (
|
||||
<img
|
||||
src={`/${config.link}/assets/${config.logo}.png`}
|
||||
alt={config?.codename[language]}
|
||||
className={classes.logo}
|
||||
style={
|
||||
config.invert_filter
|
||||
? {
|
||||
filter: 'invert(1)',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<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/${buildConfig.music_folder}/${introOgg}`
|
||||
const loop = `./chen/assets/${buildConfig.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>
|
||||
)
|
||||
}
|
||||
20
apps/directory/src/scss/_main_share.scss
Normal file
20
apps/directory/src/scss/_main_share.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.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);
|
||||
|
||||
@media only screen and (width <= 430px) {
|
||||
& {
|
||||
width: 100vw;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
369
apps/directory/src/scss/_page_base.scss
Normal file
369
apps/directory/src/scss/_page_base.scss
Normal file
@@ -0,0 +1,369 @@
|
||||
.date {
|
||||
margin: 1.5rem;
|
||||
font-family:
|
||||
Bender, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans',
|
||||
sans-serif;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
color: var(--date-color);
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: 0.1rem;
|
||||
flex: auto;
|
||||
user-select: none;
|
||||
|
||||
@media only screen and (width <= 430px) {
|
||||
& {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
color: var(--text-color-full);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
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: center;
|
||||
|
||||
svg {
|
||||
width: 1.5rem;
|
||||
fill: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
user-select: none;
|
||||
|
||||
.operator-group {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
@media only screen and (width <= 430px) {
|
||||
& {
|
||||
width: 100%;
|
||||
padding-bottom: 1rem;
|
||||
overflow-y: hidden;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
width: 12rem;
|
||||
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;
|
||||
inset: 0 -1px 0 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 {
|
||||
transition: background-color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
|
||||
& img {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
padding: 0.8rem 0.4rem;
|
||||
line-height: 1.2em;
|
||||
height: 36px;
|
||||
|
||||
.wrapper {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
.text {
|
||||
font-size: 0.75rem;
|
||||
font-family:
|
||||
Geometos, 'Noto Sans SC', 'Noto Sans JP',
|
||||
'Noto Sans KR', 'Noto Sans', sans-serif;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 430px) {
|
||||
& {
|
||||
width: 8.08rem;
|
||||
margin: 1.08rem;
|
||||
|
||||
.outline,
|
||||
.info .background {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 430px) {
|
||||
& {
|
||||
align-items: flex-start;
|
||||
flex-direction: column-reverse;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.styled-selection {
|
||||
margin-bottom: 0.8rem;
|
||||
|
||||
.content {
|
||||
padding: 0.8rem 0;
|
||||
cursor: pointer;
|
||||
transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
|
||||
.option {
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
transform: translate3d(0, 0, 1px);
|
||||
font-size: 1rem;
|
||||
padding: 0.44rem 3.25rem 0.44rem 0.63rem;
|
||||
background-color: var(--home-item-hover-background-color);
|
||||
background-image: repeating-linear-gradient(
|
||||
90deg,
|
||||
var(--home-item-background-linear-gradient-color) 0 1px,
|
||||
transparent 1px 4px
|
||||
);
|
||||
transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
|
||||
.outline {
|
||||
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: -2px;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
border-left: var(--text-color) solid 2px;
|
||||
border-right: var(--text-color) solid 2px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
&::before,
|
||||
.outline {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s,
|
||||
visibility cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
}
|
||||
|
||||
&::before {
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
currentcolor
|
||||
);
|
||||
}
|
||||
|
||||
.tick-icon {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
right: 0.31rem;
|
||||
top: 50%;
|
||||
width: 0.5rem;
|
||||
height: 1rem;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s,
|
||||
visibility cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
border-right: var(--text-color) solid 0.25rem;
|
||||
border-bottom: var(--text-color) solid 0.25rem;
|
||||
transform: translate(-50%, -70%) rotate(45deg);
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
right: -0.1em;
|
||||
top: 0.55em;
|
||||
width: 2em;
|
||||
height: 1em;
|
||||
transform: rotate(90deg);
|
||||
transition:
|
||||
opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s,
|
||||
visibility cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0.25em;
|
||||
background-color: var(--text-color-full);
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
.bar:nth-child(1) {
|
||||
transform: rotateZ(45deg) scaleX(0.5) translateX(0.8em);
|
||||
}
|
||||
|
||||
.bar:nth-child(2) {
|
||||
transform: rotateZ(-45deg) scaleX(0.5) translateX(-0.8em);
|
||||
}
|
||||
|
||||
.bar:nth-child(3),
|
||||
.bar:nth-child(4) {
|
||||
transform: translateY(1em) rotateZ(90deg) scaleX(0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
transform: translate3d(6px, 0, 1px);
|
||||
|
||||
.option::before,
|
||||
.option .outline {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
.option .tick-icon {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-overflow {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
30
apps/directory/src/scss/changelogs/Changelogs.module.scss
Normal file
30
apps/directory/src/scss/changelogs/Changelogs.module.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
@use '@/scss/page_base';
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media only screen and (width <= 430px) {
|
||||
flex-direction: column-reverse;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
61
apps/directory/src/scss/error/Error.module.scss
Normal file
61
apps/directory/src/scss/error/Error.module.scss
Normal file
@@ -0,0 +1,61 @@
|
||||
@use '@/scss/main_share';
|
||||
|
||||
.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: 6rem;
|
||||
font-size: 3rem;
|
||||
gap: 2rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.spine {
|
||||
max-width: 600px;
|
||||
flex: 1;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
|
||||
&.active {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.main {
|
||||
padding-top: 6rem;
|
||||
max-height: calc(100vh - 6rem);
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 480px) {
|
||||
.main {
|
||||
padding-top: 4rem;
|
||||
max-height: calc(100vh - 4rem);
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
apps/directory/src/scss/home/Home.module.scss
Normal file
23
apps/directory/src/scss/home/Home.module.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
@use '@/scss/page_base';
|
||||
|
||||
.official-update {
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding-left: 1rem;
|
||||
word-break: break-word;
|
||||
margin-top: 1.25rem;
|
||||
|
||||
.content {
|
||||
font-size: 1.5rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
.title {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
142
apps/directory/src/scss/operator/Operator.module.scss
Normal file
142
apps/directory/src/scss/operator/Operator.module.scss
Normal file
@@ -0,0 +1,142 @@
|
||||
@use '@/scss/page_base';
|
||||
|
||||
.main {
|
||||
padding: 3rem 0 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
.container {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-bottom: 2rem;
|
||||
user-select: none;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
@media (width <= 1280px) {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
padding-top: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings {
|
||||
margin-right: 1.5rem;
|
||||
user-select: none;
|
||||
margin-left: 1rem;
|
||||
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
border-left: 3px solid currentcolor;
|
||||
padding-left: 0.75rem;
|
||||
margin-bottom: 0.8rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
|
||||
.switch {
|
||||
bottom: -10px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--text-color);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 30%;
|
||||
height: auto;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.voice {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 20%;
|
||||
z-index: 1;
|
||||
max-width: 480px;
|
||||
width: 85%;
|
||||
opacity: 0;
|
||||
margin: 16px;
|
||||
transition: all 0.5s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
visibility: hidden;
|
||||
font-family:
|
||||
'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans',
|
||||
sans-serif;
|
||||
|
||||
.type {
|
||||
background-color: #9e9e9e;
|
||||
color: #000;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: -8px;
|
||||
padding: 2px 8px;
|
||||
font-size: 14px;
|
||||
max-width: 180px;
|
||||
width: 65%;
|
||||
box-shadow: 0 3px 6px #00000080;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
background-color: #000000a6;
|
||||
color: #fff;
|
||||
padding: 16px;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 6px 12px #00000080;
|
||||
position: relative;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.triangle {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 8px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 8px;
|
||||
border-color: white transparent transparent;
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@media (width <= 1280px) {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 1280px) {
|
||||
flex-direction: column-reverse;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
95
apps/directory/src/scss/root/Root.module.scss
Normal file
95
apps/directory/src/scss/root/Root.module.scss
Normal file
@@ -0,0 +1,95 @@
|
||||
@use '@/scss/main_share';
|
||||
|
||||
.main {
|
||||
.header {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
|
||||
@media only screen and (width <= 430px) {
|
||||
& {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.2em;
|
||||
|
||||
.icon {
|
||||
margin-right: 1.5rem;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
svg {
|
||||
fill: var(--text-color);
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
@media (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;
|
||||
border-bottom: 0.3rem solid transparent;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
.text-wrapper {
|
||||
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;
|
||||
}
|
||||
|
||||
@media only screen and (width <= 430px) {
|
||||
line-height: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 430px) {
|
||||
& {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
apps/directory/src/scss/root/drawer.module.scss
Normal file
55
apps/directory/src/scss/root/drawer.module.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
.drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: -15rem;
|
||||
width: 15rem;
|
||||
height: 100%;
|
||||
z-index: 3;
|
||||
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: 3;
|
||||
}
|
||||
|
||||
&.active {
|
||||
pointer-events: all;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
42
apps/directory/src/scss/root/footer.module.scss
Normal file
42
apps/directory/src/scss/root/footer.module.scss
Normal file
@@ -0,0 +1,42 @@
|
||||
.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', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans',
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.links {
|
||||
flex-direction: row;
|
||||
height: 2rem;
|
||||
overflow-y: hidden;
|
||||
|
||||
.item {
|
||||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
border-left: 2px solid var(--border-color);
|
||||
height: inherit;
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:first-of-type {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
106
apps/directory/src/scss/root/header.module.scss
Normal file
106
apps/directory/src/scss/root/header.module.scss
Normal file
@@ -0,0 +1,106 @@
|
||||
.header {
|
||||
width: auto;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 1rem;
|
||||
z-index: 4;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
pointer-events: none;
|
||||
|
||||
.fast-navigate {
|
||||
pointer-events: auto;
|
||||
margin-left: 0.6rem;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.extra-area {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
padding-left: 1rem;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
pointer-events: auto;
|
||||
|
||||
.arrow1 {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-left: 0.15rem solid;
|
||||
border-bottom: 0.15rem solid;
|
||||
transform: translateY(0.38rem) rotate(45deg);
|
||||
}
|
||||
|
||||
.arrow2 {
|
||||
width: 1.2rem;
|
||||
height: 0.15rem;
|
||||
background-color: currentcolor;
|
||||
transform: translate(0.5rem, -0.25rem);
|
||||
}
|
||||
}
|
||||
11
apps/directory/src/state/appbar.js
Normal file
11
apps/directory/src/state/appbar.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { atom, useAtom } from 'jotai'
|
||||
|
||||
const extraAreaAtom = atom([])
|
||||
|
||||
export function useAppbar() {
|
||||
const [extraArea, setExtraArea] = useAtom(extraAreaAtom)
|
||||
return {
|
||||
extraArea,
|
||||
setExtraArea,
|
||||
}
|
||||
}
|
||||
31
apps/directory/src/state/config.js
Normal file
31
apps/directory/src/state/config.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import CONFIG from '!/config.json'
|
||||
import { useCallback } from 'react'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
|
||||
const officialUpdateAtom = atom({})
|
||||
let operators = []
|
||||
CONFIG.operators.forEach((item) => {
|
||||
operators = [...operators, ...item]
|
||||
})
|
||||
const OPERATORS = operators
|
||||
|
||||
export function useConfig() {
|
||||
const config = CONFIG
|
||||
const operators = OPERATORS
|
||||
const [officialUpdate, setOfficialUpdate] = useAtom(officialUpdateAtom)
|
||||
|
||||
const fetchOfficialUpdate = useCallback(async () => {
|
||||
const res = await fetch(
|
||||
'https://raw.githubusercontent.com/Halyul/aklive2d/main/official_update.json'
|
||||
)
|
||||
const data = await res.json().catch((e) => {
|
||||
console.error(e)
|
||||
return {
|
||||
length: 0,
|
||||
}
|
||||
})
|
||||
setOfficialUpdate(data)
|
||||
}, [setOfficialUpdate])
|
||||
|
||||
return { config, operators, officialUpdate, fetchOfficialUpdate }
|
||||
}
|
||||
43
apps/directory/src/state/header.js
Normal file
43
apps/directory/src/state/header.js
Normal file
@@ -0,0 +1,43 @@
|
||||
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)
|
||||
const fastNaviationAtom = atom([])
|
||||
|
||||
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 [fastNavigation, setFastNavigation] = useAtom(fastNaviationAtom)
|
||||
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,
|
||||
fastNavigation,
|
||||
setFastNavigation,
|
||||
}
|
||||
}
|
||||
20
apps/directory/src/state/insight.js
Normal file
20
apps/directory/src/state/insight.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import buildConfig from '!/config.json'
|
||||
|
||||
export default (path = null, skipPageView = false) => {
|
||||
React.useEffect(() => {
|
||||
if (!skipPageView && import.meta.env.MODE !== 'development') {
|
||||
try {
|
||||
window.counterscale = {
|
||||
q: [
|
||||
['set', 'siteId', buildConfig.insight_id],
|
||||
['trackPageview', { path }],
|
||||
],
|
||||
}
|
||||
window.counterscaleOnDemandTrack()
|
||||
} catch (err) {
|
||||
console.warn && console.warn(err.message)
|
||||
}
|
||||
}
|
||||
}, [path, skipPageView])
|
||||
}
|
||||
38
apps/directory/src/state/language.js
Normal file
38
apps/directory/src/state/language.js
Normal file
@@ -0,0 +1,38 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
5
apps/directory/stylelint.config.js
Normal file
5
apps/directory/stylelint.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import baseConfig from '@aklive2d/stylelint-config'
|
||||
/** @type {import('stylelint').Config} */
|
||||
export default {
|
||||
...baseConfig,
|
||||
}
|
||||
45
apps/directory/vite.config.js
Normal file
45
apps/directory/vite.config.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import path from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import config from '@aklive2d/config'
|
||||
import * as showcaseDirs from '@aklive2d/showcase'
|
||||
import { copyDirectoryData } from '@aklive2d/vite-helpers'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(async () => {
|
||||
const dataDir = path.resolve(import.meta.dirname, config.dir_name.data)
|
||||
const publicDir = path.resolve(showcaseDirs.DIST_DIR)
|
||||
await copyDirectoryData({ dataDir, publicDir })
|
||||
return {
|
||||
envDir: dataDir,
|
||||
plugins: [react()],
|
||||
publicDir,
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve('./src'),
|
||||
'!': dataDir,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
emptyOutDir: false,
|
||||
outDir: publicDir,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `${config.directory.assets_dir}/[name]-[hash:8].js`,
|
||||
chunkFileNames: `${config.directory.assets_dir}/[name]-[hash:8].js`,
|
||||
assetFileNames: `${config.directory.assets_dir}/[name]-[hash:8].[ext]`,
|
||||
manualChunks: (id) => {
|
||||
if (id.includes('node_modules')) {
|
||||
return 'vendor' // all other package goes here
|
||||
} else if (
|
||||
id.includes('data') &&
|
||||
id.includes('.json')
|
||||
) {
|
||||
return 'assets'
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user