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'
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
1
apps/module/.eslintignore
Normal file
1
apps/module/.eslintignore
Normal file
@@ -0,0 +1 @@
|
||||
libs
|
||||
1
apps/module/.prettierignore
Normal file
1
apps/module/.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
libs
|
||||
4
apps/module/index.js
Normal file
4
apps/module/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import './libs/spine-player.css'
|
||||
import spine from './libs/spine-player'
|
||||
|
||||
export { spine }
|
||||
408
apps/module/libs/spine-player.css
Normal file
408
apps/module/libs/spine-player.css
Normal file
@@ -0,0 +1,408 @@
|
||||
/** Player **/
|
||||
.spine-player {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spine-player * {
|
||||
box-sizing: border-box;
|
||||
font-family: "PT Sans",Arial,"Helvetica Neue",Helvetica,Tahoma,sans-serif;
|
||||
color: #dddddd;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.spine-player-error {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: black;
|
||||
z-index: 10;
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.spine-player-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/** Slider **/
|
||||
.spine-player-slider {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spine-player-slider-value {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
background: rgba(98, 176, 238, 0.6);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spine-player-slider:hover .spine-player-slider-value {
|
||||
height: 4px;
|
||||
background: rgba(98, 176, 238, 1);
|
||||
transition: height 0.2s;
|
||||
}
|
||||
|
||||
.spine-player-slider-value.hovering {
|
||||
height: 4px;
|
||||
background: rgba(98, 176, 238, 1);
|
||||
transition: height 0.2s;
|
||||
}
|
||||
|
||||
.spine-player-slider.big {
|
||||
height: 12px;
|
||||
background: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.spine-player-slider.big .spine-player-slider-value {
|
||||
height: 12px;
|
||||
background: rgba(98, 176, 238, 1);
|
||||
}
|
||||
|
||||
/** Column and row layout elements **/
|
||||
.spine-player-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spine-player-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/** List **/
|
||||
.spine-player-list {
|
||||
list-style: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.spine-player-list li {
|
||||
cursor: pointer;
|
||||
margin: 8px 8px;
|
||||
}
|
||||
|
||||
.spine-player-list .selectable {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0 !important;
|
||||
padding: 2px 20px 2px 0 !important;
|
||||
}
|
||||
|
||||
.spine-player-list li.selectable:first-child {
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
.spine-player-list li.selectable:last-child {
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.spine-player-list li.selectable:hover {
|
||||
background: #6e6e6e;
|
||||
}
|
||||
|
||||
.spine-player-list li.selectable .selectable-circle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 6px;
|
||||
min-width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
align-self: center;
|
||||
opacity: 0;
|
||||
margin: 5px 10px;
|
||||
}
|
||||
|
||||
.spine-player-list li.selectable.selected .selectable-circle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.spine-player-list li.selectable .selectable-text {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.spine-player-list li.selectable.selected .selectable-text, .spine-player-list li.selectable:hover .selectable-text {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
/** Switch **/
|
||||
.spine-player-switch {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 2px 10px;
|
||||
}
|
||||
|
||||
.spine-player-switch-text {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.spine-player-switch-knob-area {
|
||||
width: 30px; /* width of the switch */
|
||||
height: 10px;
|
||||
display: block;
|
||||
border-radius: 5px; /* must be half of height */
|
||||
background: #6e6e6e;
|
||||
position: relative;
|
||||
align-self: center;
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
.spine-player-switch.active .spine-player-switch-knob-area {
|
||||
background: #5EAFF1;
|
||||
}
|
||||
|
||||
.spine-player-switch-knob {
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #9e9e9e;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: -2px;
|
||||
filter: drop-shadow(0 0 1px #333);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.spine-player-switch.active .spine-player-switch-knob {
|
||||
background: #fff;
|
||||
transform: translateX(18px);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
/** Popup **/
|
||||
.spine-player-popup-parent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spine-player-popup {
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
z-index: 1;
|
||||
right: 2px;
|
||||
bottom: 40px;
|
||||
border-radius: 4px;
|
||||
max-height: 400%;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
.spine-player-popup-title {
|
||||
margin: 4px 15px 2px 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spine-player-popup hr {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #cccccc70;
|
||||
}
|
||||
|
||||
/** Canvas **/
|
||||
.spine-player canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/** Player controls **/
|
||||
.spine-player-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
||||
.spine-player-controls-hidden {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
||||
/** Player buttons **/
|
||||
.spine-player-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
padding: 2px 8px 3px;
|
||||
}
|
||||
|
||||
.spine-player-button {
|
||||
background: none;
|
||||
outline: 0;
|
||||
border: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-size: 20px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
cursor: pointer;
|
||||
margin-right: 3px;
|
||||
padding-bottom: 3px;
|
||||
filter: drop-shadow(0 0 1px #333);
|
||||
}
|
||||
|
||||
.spine-player-button-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.spine-player-button-icon-play {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eplay%3C%2Ftitle%3E%3Cg%20id%3D%22play%22%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2243%2023.3%204%2047%204%201%2043%2023.3%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-play:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eplay%3C%2Ftitle%3E%3Cg%20id%3D%22play%22%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2243%2023.3%204%2047%204%201%2043%2023.3%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-play-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eplay%3C%2Ftitle%3E%3Cg%20id%3D%22play%22%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2243%2023.3%204%2047%204%201%2043%2023.3%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-pause {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Epause%3C%2Ftitle%3E%3Cg%20id%3D%22pause%22%3E%3Crect%20class%3D%22cls-1%22%20x%3D%226%22%20y%3D%221%22%20width%3D%2213%22%20height%3D%2246%22%2F%3E%3Crect%20class%3D%22cls-1%22%20x%3D%2228%22%20y%3D%221%22%20width%3D%2213%22%20height%3D%2246%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-pause:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Epause%3C%2Ftitle%3E%3Cg%20id%3D%22pause%22%3E%3Crect%20class%3D%22cls-1%22%20x%3D%226%22%20y%3D%221%22%20width%3D%2213%22%20height%3D%2246%22%2F%3E%3Crect%20class%3D%22cls-1%22%20x%3D%2228%22%20y%3D%221%22%20width%3D%2213%22%20height%3D%2246%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-pause-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Epause%3C%2Ftitle%3E%3Cg%20id%3D%22pause%22%3E%3Crect%20class%3D%22cls-1%22%20x%3D%226%22%20y%3D%221%22%20width%3D%2213%22%20height%3D%2246%22%2F%3E%3Crect%20class%3D%22cls-1%22%20x%3D%2228%22%20y%3D%221%22%20width%3D%2213%22%20height%3D%2246%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-speed {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20id%3D%22playback%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eplayback%3C%2Ftitle%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M48%2C28V20l-4.7-1.18a20.16%2C20.16%2C0%2C0%2C0-2-4.81l2.49-4.15L38.14%2C4.2%2C34%2C6.69a20.16%2C20.16%2C0%2C0%2C0-4.81-2L28%2C0H20L18.82%2C4.7A20.16%2C20.16%2C0%2C0%2C0%2C14%2C6.7L9.86%2C4.2%2C4.2%2C9.86%2C6.69%2C14a20.16%2C20.16%2C0%2C0%2C0-2%2C4.81L0%2C20v8l4.7%2C1.18A20.16%2C20.16%2C0%2C0%2C0%2C6.7%2C34L4.2%2C38.14%2C9.86%2C43.8%2C14%2C41.31a20.16%2C20.16%2C0%2C0%2C0%2C4.81%2C2L20%2C48h8l1.18-4.7a20.16%2C20.16%2C0%2C0%2C0%2C4.81-2l4.15%2C2.49%2C5.66-5.66L41.31%2C34a20.16%2C20.16%2C0%2C0%2C0%2C2-4.81ZM24%2C38A14%2C14%2C0%2C1%2C1%2C38%2C24%2C14%2C14%2C0%2C0%2C1%2C24%2C38Z%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2234%2024%2018%2033%2018%2015%2034%2024%2034%2024%22%2F%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-speed:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20id%3D%22playback%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eplayback%3C%2Ftitle%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M48%2C28V20l-4.7-1.18a20.16%2C20.16%2C0%2C0%2C0-2-4.81l2.49-4.15L38.14%2C4.2%2C34%2C6.69a20.16%2C20.16%2C0%2C0%2C0-4.81-2L28%2C0H20L18.82%2C4.7A20.16%2C20.16%2C0%2C0%2C0%2C14%2C6.7L9.86%2C4.2%2C4.2%2C9.86%2C6.69%2C14a20.16%2C20.16%2C0%2C0%2C0-2%2C4.81L0%2C20v8l4.7%2C1.18A20.16%2C20.16%2C0%2C0%2C0%2C6.7%2C34L4.2%2C38.14%2C9.86%2C43.8%2C14%2C41.31a20.16%2C20.16%2C0%2C0%2C0%2C4.81%2C2L20%2C48h8l1.18-4.7a20.16%2C20.16%2C0%2C0%2C0%2C4.81-2l4.15%2C2.49%2C5.66-5.66L41.31%2C34a20.16%2C20.16%2C0%2C0%2C0%2C2-4.81ZM24%2C38A14%2C14%2C0%2C1%2C1%2C38%2C24%2C14%2C14%2C0%2C0%2C1%2C24%2C38Z%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2234%2024%2018%2033%2018%2015%2034%2024%2034%2024%22%2F%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-speed-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20id%3D%22playback%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eplayback%3C%2Ftitle%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M48%2C28V20l-4.7-1.18a20.16%2C20.16%2C0%2C0%2C0-2-4.81l2.49-4.15L38.14%2C4.2%2C34%2C6.69a20.16%2C20.16%2C0%2C0%2C0-4.81-2L28%2C0H20L18.82%2C4.7A20.16%2C20.16%2C0%2C0%2C0%2C14%2C6.7L9.86%2C4.2%2C4.2%2C9.86%2C6.69%2C14a20.16%2C20.16%2C0%2C0%2C0-2%2C4.81L0%2C20v8l4.7%2C1.18A20.16%2C20.16%2C0%2C0%2C0%2C6.7%2C34L4.2%2C38.14%2C9.86%2C43.8%2C14%2C41.31a20.16%2C20.16%2C0%2C0%2C0%2C4.81%2C2L20%2C48h8l1.18-4.7a20.16%2C20.16%2C0%2C0%2C0%2C4.81-2l4.15%2C2.49%2C5.66-5.66L41.31%2C34a20.16%2C20.16%2C0%2C0%2C0%2C2-4.81ZM24%2C38A14%2C14%2C0%2C1%2C1%2C38%2C24%2C14%2C14%2C0%2C0%2C1%2C24%2C38Z%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2234%2024%2018%2033%2018%2015%2034%2024%2034%2024%22%2F%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-animations {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eanimations%3C%2Ftitle%3E%3Cg%20id%3D%22animations%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M12%2C45V43.22a6.39%2C6.39%2C0%2C0%2C0%2C.63-.81%2C27.83%2C27.83%2C0%2C0%2C1%2C3.79-4.16c.93-.84%2C2.06-1.88%2C2.86-2.71a13.83%2C13.83%2C0%2C0%2C0%2C1.53-1.9l3.9-5.24c1-1.17.95-1.1%2C2.11%2C0l3%2C2.24a4%2C4%2C0%2C0%2C0-2.29%2C2.38c-1.37%2C3-2.39%2C4-2.68%2C4.22l-.23.18c-.54.39-1.81%2C1-1.7%2C1.54l.8%2C1.49a4.5%2C4.5%2C0%2C0%2C1%2C.39%2C1l.57%2C2.15a.69.69%2C0%2C0%2C0%2C.58.48c.47.08%2C1%2C.5%2C1.33.53%2C1.29.1%2C1.79%2C0%2C1.42-.54L26.7%2C42.72a.86.86%2C0%2C0%2C1-.2-.24%2C3.64%2C3.64%2C0%2C0%2C1-.42-2.2A5.39%2C5.39%2C0%2C0%2C1%2C26.61%2C39c1.84-2%2C6.74-6.36%2C6.74-6.36%2C1.71-1.81%2C1.4-2.52.81-3.84a27.38%2C27.38%2C0%2C0%2C0-2-3c-.41-.61-2.08-2.38-2.85-3.28-.43-.5.38-2.08.87-2.82.18-.12-.41.05%2C1.72.07a23.32%2C23.32%2C0%2C0%2C0%2C3.56-.19l1.63.61c.28%2C0%2C1.18-.09%2C1.31-.35l.12-.78c.18-.39.31-1.56-.05-1.75l-.6-.52a2.28%2C2.28%2C0%2C0%2C0-1.61.07l-.2.44c-.14.15-.52.37-.71.29l-2.24%2C0c-.5.12-1.18-.42-1.81-.73L32.05%2C15a8%2C8%2C0%2C0%2C0%2C.8-3.92%2C1.22%2C1.22%2C0%2C0%2C0-.28-.82%2C7.87%2C7.87%2C0%2C0%2C0-1.15-1.06l.11-.73c-.12-.49%2C1-.82%2C1.52-.82l.76-.33c.32%2C0%2C.68-.89.78-1.21L34.94%2C4a11.26%2C11.26%2C0%2C0%2C0%2C0-1.61C34.57.08%2C30.06-1.42%2C28.78%2C2c-.14.38-.62.77.34%2C3.21a1.55%2C1.55%2C0%2C0%2C1-.3%2C1.2L28.4%2C7a4%2C4%2C0%2C0%2C1-1.19.49c-.79%2C0-1.59-.75-4%2C.54C21%2C9.16%2C18.59%2C13%2C17.7%2C14.22a3.21%2C3.21%2C0%2C0%2C0-.61%2C1.58c-.05%2C1.16.7%2C3.74.87%2C5.75.13%2C1.53.21%2C2.52.72%2C3.06%2C1.07%2C1.14%2C2.1-.18%2C2.61-1a2.74%2C2.74%2C0%2C0%2C0-.14-1.86l-.74-.1c-.15-.15-.4-.42-.39-.64-.05-3.48-.22-3.14-.18-5.39%2C1.74-1.46%2C2.4-2.45%2C2.3-2-.2%2C1.15.28%2C2.83.09%2C4.35a6.46%2C6.46%2C0%2C0%2C1-.7%2C2.58s-2.11%2C4.22-2.14%2C4.27l-1.26%2C5.6-.7%2C1.44s-.71.54-1.59%2C1.21a9.67%2C9.67%2C0%2C0%2C0-2.27%2C3.18%2C20.16%2C20.16%2C0%2C0%2C1-1.42%2C2.83l-.87%2C1.31a1.72%2C1.72%2C0%2C0%2C1-.6.61l-1.83%2C1.1a1.39%2C1.39%2C0%2C0%2C0-.16.93l.68%2C1.71a4.07%2C4.07%2C0%2C0%2C1%2C.27%2C1.07l.17%2C1.56a.75.75%2C0%2C0%2C0%2C.71.59%2C18.13%2C18.13%2C0%2C0%2C0%2C3.26-.5c.27-.09-.29-.78-.53-1s-.45-.36-.45-.36A12.78%2C12.78%2C0%2C0%2C1%2C12%2C45Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
|
||||
}
|
||||
|
||||
.spine-player-button-icon-animations:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eanimations%3C%2Ftitle%3E%3Cg%20id%3D%22animations%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M12%2C45V43.22a6.39%2C6.39%2C0%2C0%2C0%2C.63-.81%2C27.83%2C27.83%2C0%2C0%2C1%2C3.79-4.16c.93-.84%2C2.06-1.88%2C2.86-2.71a13.83%2C13.83%2C0%2C0%2C0%2C1.53-1.9l3.9-5.24c1-1.17.95-1.1%2C2.11%2C0l3%2C2.24a4%2C4%2C0%2C0%2C0-2.29%2C2.38c-1.37%2C3-2.39%2C4-2.68%2C4.22l-.23.18c-.54.39-1.81%2C1-1.7%2C1.54l.8%2C1.49a4.5%2C4.5%2C0%2C0%2C1%2C.39%2C1l.57%2C2.15a.69.69%2C0%2C0%2C0%2C.58.48c.47.08%2C1%2C.5%2C1.33.53%2C1.29.1%2C1.79%2C0%2C1.42-.54L26.7%2C42.72a.86.86%2C0%2C0%2C1-.2-.24%2C3.64%2C3.64%2C0%2C0%2C1-.42-2.2A5.39%2C5.39%2C0%2C0%2C1%2C26.61%2C39c1.84-2%2C6.74-6.36%2C6.74-6.36%2C1.71-1.81%2C1.4-2.52.81-3.84a27.38%2C27.38%2C0%2C0%2C0-2-3c-.41-.61-2.08-2.38-2.85-3.28-.43-.5.38-2.08.87-2.82.18-.12-.41.05%2C1.72.07a23.32%2C23.32%2C0%2C0%2C0%2C3.56-.19l1.63.61c.28%2C0%2C1.18-.09%2C1.31-.35l.12-.78c.18-.39.31-1.56-.05-1.75l-.6-.52a2.28%2C2.28%2C0%2C0%2C0-1.61.07l-.2.44c-.14.15-.52.37-.71.29l-2.24%2C0c-.5.12-1.18-.42-1.81-.73L32.05%2C15a8%2C8%2C0%2C0%2C0%2C.8-3.92%2C1.22%2C1.22%2C0%2C0%2C0-.28-.82%2C7.87%2C7.87%2C0%2C0%2C0-1.15-1.06l.11-.73c-.12-.49%2C1-.82%2C1.52-.82l.76-.33c.32%2C0%2C.68-.89.78-1.21L34.94%2C4a11.26%2C11.26%2C0%2C0%2C0%2C0-1.61C34.57.08%2C30.06-1.42%2C28.78%2C2c-.14.38-.62.77.34%2C3.21a1.55%2C1.55%2C0%2C0%2C1-.3%2C1.2L28.4%2C7a4%2C4%2C0%2C0%2C1-1.19.49c-.79%2C0-1.59-.75-4%2C.54C21%2C9.16%2C18.59%2C13%2C17.7%2C14.22a3.21%2C3.21%2C0%2C0%2C0-.61%2C1.58c-.05%2C1.16.7%2C3.74.87%2C5.75.13%2C1.53.21%2C2.52.72%2C3.06%2C1.07%2C1.14%2C2.1-.18%2C2.61-1a2.74%2C2.74%2C0%2C0%2C0-.14-1.86l-.74-.1c-.15-.15-.4-.42-.39-.64-.05-3.48-.22-3.14-.18-5.39%2C1.74-1.46%2C2.4-2.45%2C2.3-2-.2%2C1.15.28%2C2.83.09%2C4.35a6.46%2C6.46%2C0%2C0%2C1-.7%2C2.58s-2.11%2C4.22-2.14%2C4.27l-1.26%2C5.6-.7%2C1.44s-.71.54-1.59%2C1.21a9.67%2C9.67%2C0%2C0%2C0-2.27%2C3.18%2C20.16%2C20.16%2C0%2C0%2C1-1.42%2C2.83l-.87%2C1.31a1.72%2C1.72%2C0%2C0%2C1-.6.61l-1.83%2C1.1a1.39%2C1.39%2C0%2C0%2C0-.16.93l.68%2C1.71a4.07%2C4.07%2C0%2C0%2C1%2C.27%2C1.07l.17%2C1.56a.75.75%2C0%2C0%2C0%2C.71.59%2C18.13%2C18.13%2C0%2C0%2C0%2C3.26-.5c.27-.09-.29-.78-.53-1s-.45-.36-.45-.36A12.78%2C12.78%2C0%2C0%2C1%2C12%2C45Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
|
||||
}
|
||||
|
||||
.spine-player-button-icon-animations-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eanimations%3C%2Ftitle%3E%3Cg%20id%3D%22animations%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M12%2C45V43.22a6.39%2C6.39%2C0%2C0%2C0%2C.63-.81%2C27.83%2C27.83%2C0%2C0%2C1%2C3.79-4.16c.93-.84%2C2.06-1.88%2C2.86-2.71a13.83%2C13.83%2C0%2C0%2C0%2C1.53-1.9l3.9-5.24c1-1.17.95-1.1%2C2.11%2C0l3%2C2.24a4%2C4%2C0%2C0%2C0-2.29%2C2.38c-1.37%2C3-2.39%2C4-2.68%2C4.22l-.23.18c-.54.39-1.81%2C1-1.7%2C1.54l.8%2C1.49a4.5%2C4.5%2C0%2C0%2C1%2C.39%2C1l.57%2C2.15a.69.69%2C0%2C0%2C0%2C.58.48c.47.08%2C1%2C.5%2C1.33.53%2C1.29.1%2C1.79%2C0%2C1.42-.54L26.7%2C42.72a.86.86%2C0%2C0%2C1-.2-.24%2C3.64%2C3.64%2C0%2C0%2C1-.42-2.2A5.39%2C5.39%2C0%2C0%2C1%2C26.61%2C39c1.84-2%2C6.74-6.36%2C6.74-6.36%2C1.71-1.81%2C1.4-2.52.81-3.84a27.38%2C27.38%2C0%2C0%2C0-2-3c-.41-.61-2.08-2.38-2.85-3.28-.43-.5.38-2.08.87-2.82.18-.12-.41.05%2C1.72.07a23.32%2C23.32%2C0%2C0%2C0%2C3.56-.19l1.63.61c.28%2C0%2C1.18-.09%2C1.31-.35l.12-.78c.18-.39.31-1.56-.05-1.75l-.6-.52a2.28%2C2.28%2C0%2C0%2C0-1.61.07l-.2.44c-.14.15-.52.37-.71.29l-2.24%2C0c-.5.12-1.18-.42-1.81-.73L32.05%2C15a8%2C8%2C0%2C0%2C0%2C.8-3.92%2C1.22%2C1.22%2C0%2C0%2C0-.28-.82%2C7.87%2C7.87%2C0%2C0%2C0-1.15-1.06l.11-.73c-.12-.49%2C1-.82%2C1.52-.82l.76-.33c.32%2C0%2C.68-.89.78-1.21L34.94%2C4a11.26%2C11.26%2C0%2C0%2C0%2C0-1.61C34.57.08%2C30.06-1.42%2C28.78%2C2c-.14.38-.62.77.34%2C3.21a1.55%2C1.55%2C0%2C0%2C1-.3%2C1.2L28.4%2C7a4%2C4%2C0%2C0%2C1-1.19.49c-.79%2C0-1.59-.75-4%2C.54C21%2C9.16%2C18.59%2C13%2C17.7%2C14.22a3.21%2C3.21%2C0%2C0%2C0-.61%2C1.58c-.05%2C1.16.7%2C3.74.87%2C5.75.13%2C1.53.21%2C2.52.72%2C3.06%2C1.07%2C1.14%2C2.1-.18%2C2.61-1a2.74%2C2.74%2C0%2C0%2C0-.14-1.86l-.74-.1c-.15-.15-.4-.42-.39-.64-.05-3.48-.22-3.14-.18-5.39%2C1.74-1.46%2C2.4-2.45%2C2.3-2-.2%2C1.15.28%2C2.83.09%2C4.35a6.46%2C6.46%2C0%2C0%2C1-.7%2C2.58s-2.11%2C4.22-2.14%2C4.27l-1.26%2C5.6-.7%2C1.44s-.71.54-1.59%2C1.21a9.67%2C9.67%2C0%2C0%2C0-2.27%2C3.18%2C20.16%2C20.16%2C0%2C0%2C1-1.42%2C2.83l-.87%2C1.31a1.72%2C1.72%2C0%2C0%2C1-.6.61l-1.83%2C1.1a1.39%2C1.39%2C0%2C0%2C0-.16.93l.68%2C1.71a4.07%2C4.07%2C0%2C0%2C1%2C.27%2C1.07l.17%2C1.56a.75.75%2C0%2C0%2C0%2C.71.59%2C18.13%2C18.13%2C0%2C0%2C0%2C3.26-.5c.27-.09-.29-.78-.53-1s-.45-.36-.45-.36A12.78%2C12.78%2C0%2C0%2C1%2C12%2C45Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
|
||||
}
|
||||
|
||||
.spine-player-button-icon-skins {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eskins%3C%2Ftitle%3E%3Cg%20id%3D%22skins%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M36%2C12.54l-6.92%2C1-.79%2C1.2c-1%2C.25-2-.62-3-.55V12.33a1.35%2C1.35%2C0%2C0%2C1%2C.55-1.07c3-2.24%2C3.28-3.75%2C3.28-5.34A5.06%2C5.06%2C0%2C0%2C0%2C24%2C.76c-2.54%2C0-4.38.71-5.49%2C2.13a5.74%2C5.74%2C0%2C0%2C0-.9%2C4.57l2.48-.61a3.17%2C3.17%2C0%2C0%2C1%2C.45-2.4c.6-.75%2C1.75-1.13%2C3.42-1.13%2C2.56%2C0%2C2.56%2C1.24%2C2.56%2C2.56%2C0%2C.92%2C0%2C1.65-2.26%2C3.34a3.92%2C3.92%2C0%2C0%2C0-1.58%2C3.12v1.86c-1-.07-2%2C.8-3%2C.55l-.79-1.2-6.92-1c-2.25%2C0-4.35%2C2.09-5.64%2C3.93L1%2C24c3.83%2C5.11%2C10.22%2C5.11%2C10.22%2C5.11V41.93c0%2C2.34%2C2.68%2C3.88%2C5.59%2C4.86a22.59%2C22.59%2C0%2C0%2C0%2C14.37%2C0c2.91-1%2C5.59-2.52%2C5.59-4.86V29.15S43.17%2C29.15%2C47%2C24l-5.33-7.57C40.38%2C14.63%2C38.27%2C12.54%2C36%2C12.54ZM23.32%2C20.09%2C21%2C17l1.8-.6a3.79%2C3.79%2C0%2C0%2C1%2C2.4%2C0L27%2C17l-2.32%2C3.09A.85.85%2C0%2C0%2C1%2C23.32%2C20.09Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.spine-player-button-icon-skins:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eskins%3C%2Ftitle%3E%3Cg%20id%3D%22skins%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M36%2C12.54l-6.92%2C1-.79%2C1.2c-1%2C.25-2-.62-3-.55V12.33a1.35%2C1.35%2C0%2C0%2C1%2C.55-1.07c3-2.24%2C3.28-3.75%2C3.28-5.34A5.06%2C5.06%2C0%2C0%2C0%2C24%2C.76c-2.54%2C0-4.38.71-5.49%2C2.13a5.74%2C5.74%2C0%2C0%2C0-.9%2C4.57l2.48-.61a3.17%2C3.17%2C0%2C0%2C1%2C.45-2.4c.6-.75%2C1.75-1.13%2C3.42-1.13%2C2.56%2C0%2C2.56%2C1.24%2C2.56%2C2.56%2C0%2C.92%2C0%2C1.65-2.26%2C3.34a3.92%2C3.92%2C0%2C0%2C0-1.58%2C3.12v1.86c-1-.07-2%2C.8-3%2C.55l-.79-1.2-6.92-1c-2.25%2C0-4.35%2C2.09-5.64%2C3.93L1%2C24c3.83%2C5.11%2C10.22%2C5.11%2C10.22%2C5.11V41.93c0%2C2.34%2C2.68%2C3.88%2C5.59%2C4.86a22.59%2C22.59%2C0%2C0%2C0%2C14.37%2C0c2.91-1%2C5.59-2.52%2C5.59-4.86V29.15S43.17%2C29.15%2C47%2C24l-5.33-7.57C40.38%2C14.63%2C38.27%2C12.54%2C36%2C12.54ZM23.32%2C20.09%2C21%2C17l1.8-.6a3.79%2C3.79%2C0%2C0%2C1%2C2.4%2C0L27%2C17l-2.32%2C3.09A.85.85%2C0%2C0%2C1%2C23.32%2C20.09Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-skins-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eskins%3C%2Ftitle%3E%3Cg%20id%3D%22skins%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M36%2C12.54l-6.92%2C1-.79%2C1.2c-1%2C.25-2-.62-3-.55V12.33a1.35%2C1.35%2C0%2C0%2C1%2C.55-1.07c3-2.24%2C3.28-3.75%2C3.28-5.34A5.06%2C5.06%2C0%2C0%2C0%2C24%2C.76c-2.54%2C0-4.38.71-5.49%2C2.13a5.74%2C5.74%2C0%2C0%2C0-.9%2C4.57l2.48-.61a3.17%2C3.17%2C0%2C0%2C1%2C.45-2.4c.6-.75%2C1.75-1.13%2C3.42-1.13%2C2.56%2C0%2C2.56%2C1.24%2C2.56%2C2.56%2C0%2C.92%2C0%2C1.65-2.26%2C3.34a3.92%2C3.92%2C0%2C0%2C0-1.58%2C3.12v1.86c-1-.07-2%2C.8-3%2C.55l-.79-1.2-6.92-1c-2.25%2C0-4.35%2C2.09-5.64%2C3.93L1%2C24c3.83%2C5.11%2C10.22%2C5.11%2C10.22%2C5.11V41.93c0%2C2.34%2C2.68%2C3.88%2C5.59%2C4.86a22.59%2C22.59%2C0%2C0%2C0%2C14.37%2C0c2.91-1%2C5.59-2.52%2C5.59-4.86V29.15S43.17%2C29.15%2C47%2C24l-5.33-7.57C40.38%2C14.63%2C38.27%2C12.54%2C36%2C12.54ZM23.32%2C20.09%2C21%2C17l1.8-.6a3.79%2C3.79%2C0%2C0%2C1%2C2.4%2C0L27%2C17l-2.32%2C3.09A.85.85%2C0%2C0%2C1%2C23.32%2C20.09Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-settings {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Esettings%3C%2Ftitle%3E%3Cg%20id%3D%22settings%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M40%2C3H8A5%2C5%2C0%2C0%2C0%2C3%2C8V40a5%2C5%2C0%2C0%2C0%2C5%2C5H40a5%2C5%2C0%2C0%2C0%2C5-5V8A5%2C5%2C0%2C0%2C0%2C40%2C3ZM16%2C40H9V33h7Zm0-12H9V21h7Zm0-12H9V9h7ZM39%2C38H20V35H39Zm0-12H20V23H39Zm0-12H20V11H39Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.spine-player-button-icon-settings:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Esettings%3C%2Ftitle%3E%3Cg%20id%3D%22settings%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M40%2C3H8A5%2C5%2C0%2C0%2C0%2C3%2C8V40a5%2C5%2C0%2C0%2C0%2C5%2C5H40a5%2C5%2C0%2C0%2C0%2C5-5V8A5%2C5%2C0%2C0%2C0%2C40%2C3ZM16%2C40H9V33h7Zm0-12H9V21h7Zm0-12H9V9h7ZM39%2C38H20V35H39Zm0-12H20V23H39Zm0-12H20V11H39Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-settings-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Esettings%3C%2Ftitle%3E%3Cg%20id%3D%22settings%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M40%2C3H8A5%2C5%2C0%2C0%2C0%2C3%2C8V40a5%2C5%2C0%2C0%2C0%2C5%2C5H40a5%2C5%2C0%2C0%2C0%2C5-5V8A5%2C5%2C0%2C0%2C0%2C40%2C3ZM16%2C40H9V33h7Zm0-12H9V21h7Zm0-12H9V9h7ZM39%2C38H20V35H39Zm0-12H20V23H39Zm0-12H20V11H39Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-fullscreen {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eexpand%3C%2Ftitle%3E%3Cg%20id%3D%22settings%22%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2230.14%208%2040%208%2040%2017.86%2044.5%2017.86%2044.5%203.5%2030.14%203.5%2030.14%208%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%228%2017.86%208%208%2017.86%208%2017.86%203.5%203.5%203.5%203.5%2017.86%208%2017.86%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2240%2030.14%2040%2040%2030.14%2040%2030.14%2044.5%2044.5%2044.5%2044.5%2030.14%2040%2030.14%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2217.86%2040%208%2040%208%2030.14%203.5%2030.14%203.5%2044.5%2017.86%2044.5%2017.86%2040%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.spine-player-button-icon-fullscreen:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eexpand%3C%2Ftitle%3E%3Cg%20id%3D%22settings%22%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2230.14%208%2040%208%2040%2017.86%2044.5%2017.86%2044.5%203.5%2030.14%203.5%2030.14%208%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%228%2017.86%208%208%2017.86%208%2017.86%203.5%203.5%203.5%203.5%2017.86%208%2017.86%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2240%2030.14%2040%2040%2030.14%2040%2030.14%2044.5%2044.5%2044.5%2044.5%2030.14%2040%2030.14%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2217.86%2040%208%2040%208%2030.14%203.5%2030.14%203.5%2044.5%2017.86%2044.5%2017.86%2040%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-fullscreen-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eexpand%3C%2Ftitle%3E%3Cg%20id%3D%22settings%22%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2230.14%208%2040%208%2040%2017.86%2044.5%2017.86%2044.5%203.5%2030.14%203.5%2030.14%208%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%228%2017.86%208%208%2017.86%208%2017.86%203.5%203.5%203.5%203.5%2017.86%208%2017.86%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2240%2030.14%2040%2040%2030.14%2040%2030.14%2044.5%2044.5%2044.5%2044.5%2030.14%2040%2030.14%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2217.86%2040%208%2040%208%2030.14%203.5%2030.14%203.5%2044.5%2017.86%2044.5%2017.86%2040%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-spine-logo {
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin: 0 8px !important;
|
||||
align-self: center;
|
||||
border: none !important;
|
||||
width: auto !important;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
box-shadow: none !important;
|
||||
filter: drop-shadow(0 0 1px #333);
|
||||
}
|
||||
|
||||
.spine-player-button-icon-spine-logo:hover {
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
/** Speed slider **/
|
||||
.spine-player-speed-slider {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
/** Player editor **/
|
||||
.spine-player-editor-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spine-player-editor-code {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.spine-player-editor-player {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: black;
|
||||
}
|
||||
12001
apps/module/libs/spine-player.js
Normal file
12001
apps/module/libs/spine-player.js
Normal file
File diff suppressed because one or more lines are too long
7
apps/module/package.json
Normal file
7
apps/module/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@aklive2d/module",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "index.js"
|
||||
}
|
||||
26
apps/showcase/.gitignore
vendored
Normal file
26
apps/showcase/.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/showcase/.prettierignore
Normal file
3
apps/showcase/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
dist
|
||||
data
|
||||
auto_update
|
||||
1
apps/showcase/.stylelintignore
Normal file
1
apps/showcase/.stylelintignore
Normal file
@@ -0,0 +1 @@
|
||||
spine-player.css
|
||||
3
apps/showcase/eslint.config.js
Normal file
3
apps/showcase/eslint.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import baseConfig from '@aklive2d/eslint-config'
|
||||
/** @type {import('eslint').Config} */
|
||||
export default [...baseConfig, { ignores: ['src/libs/*'] }]
|
||||
22
apps/showcase/index.html
Normal file
22
apps/showcase/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,minimum-scale=1,initial-scale=1,maximum-scale=1,user-scalable=no"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="renderer" content="webkit" />
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
<script
|
||||
id="counterscale-script"
|
||||
src="%VITE_INSIGHT_URL%"
|
||||
defer
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
16
apps/showcase/index.js
Normal file
16
apps/showcase/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import path from 'node:path'
|
||||
import config from '@aklive2d/config'
|
||||
|
||||
export const DATA_DIR = path.resolve(import.meta.dirname, config.dir_name.data)
|
||||
export const PUBLIC_DIR = path.resolve(DATA_DIR, config.dir_name.public)
|
||||
export const PUBLIC_ASSETS_DIR = path.resolve(
|
||||
PUBLIC_DIR,
|
||||
config.dir_name.assets
|
||||
)
|
||||
export const OUT_DIR = path.resolve(import.meta.dirname, config.dir_name.dist)
|
||||
export const DIST_DIR = path.resolve(
|
||||
import.meta.dirname,
|
||||
'..',
|
||||
'..',
|
||||
config.dir_name.dist
|
||||
)
|
||||
9
apps/showcase/jsconfig.json
Normal file
9
apps/showcase/jsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"!/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
26
apps/showcase/package.json
Normal file
26
apps/showcase/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@aklive2d/showcase",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev:showcase": "vite --clearScreen false",
|
||||
"build": "mode=build node runner.js",
|
||||
"preview:showcase": "vite preview",
|
||||
"lint": "eslint \"src/**/*.js\" && stylelint \"**/*.css\" && prettier --check ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.1.0",
|
||||
"@aklive2d/eslint-config": "workspace:*",
|
||||
"@aklive2d/postcss-config": "workspace:*",
|
||||
"@aklive2d/stylelint-config": "workspace:*",
|
||||
"@aklive2d/config": "workspace:*",
|
||||
"@aklive2d/libs": "workspace:*",
|
||||
"@aklive2d/assets": "workspace:*",
|
||||
"@aklive2d/operator": "workspace:*",
|
||||
"@aklive2d/vite-helpers": "workspace:*",
|
||||
"@aklive2d/module": "workspace:*",
|
||||
"@aklive2d/prettier-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
11
apps/showcase/prettier.config.js
Normal file
11
apps/showcase/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
|
||||
38
apps/showcase/runner.js
Normal file
38
apps/showcase/runner.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import path from 'node:path'
|
||||
import { build as viteBuild } from 'vite'
|
||||
import operators from '@aklive2d/operator'
|
||||
import { envParser, file } from '@aklive2d/libs'
|
||||
import { copyShowcaseData, copyProjectJSON } from '@aklive2d/vite-helpers'
|
||||
import * as dirs from './index.js'
|
||||
|
||||
const build = async (namesToBuild) => {
|
||||
const names = !namesToBuild.length ? Object.keys(operators) : namesToBuild
|
||||
console.log('Generating assets for', names.length, 'operators')
|
||||
for (const name of names) {
|
||||
copyShowcaseData(name, {
|
||||
dataDir: dirs.DATA_DIR,
|
||||
publicAssetsDir: dirs.PUBLIC_ASSETS_DIR,
|
||||
})
|
||||
await viteBuild()
|
||||
const releaseDir = path.join(dirs.DIST_DIR, name)
|
||||
file.mv(dirs.OUT_DIR, releaseDir)
|
||||
file.rm(dirs.DATA_DIR)
|
||||
copyProjectJSON(name, {
|
||||
releaseDir,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { name } = envParser.parse({
|
||||
name: {
|
||||
type: 'string',
|
||||
short: 'n',
|
||||
multiple: true,
|
||||
default: [],
|
||||
},
|
||||
})
|
||||
await build(name)
|
||||
}
|
||||
|
||||
main()
|
||||
8
apps/showcase/src/components/aklive2d.css
Normal file
8
apps/showcase/src/components/aklive2d.css
Normal file
@@ -0,0 +1,8 @@
|
||||
#settings-box {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: white;
|
||||
user-select: auto;
|
||||
z-index: 999;
|
||||
}
|
||||
257
apps/showcase/src/components/aklive2d.js
Normal file
257
apps/showcase/src/components/aklive2d.js
Normal file
@@ -0,0 +1,257 @@
|
||||
import Voice from '@/components/voice'
|
||||
import Fallback from '@/components/fallback'
|
||||
import Music from '@/components/music'
|
||||
import Player from '@/components/player'
|
||||
import Background from '@/components/background'
|
||||
import Logo from '@/components/logo'
|
||||
import Insight from '@/components/insight'
|
||||
import Events from '@/components/events'
|
||||
import {
|
||||
isWebGLSupported,
|
||||
insertHTMLChild,
|
||||
addEventListeners,
|
||||
updateElementPosition,
|
||||
} from '@/components/helper'
|
||||
import '@/components/aklive2d.css'
|
||||
|
||||
export default class AKLive2D {
|
||||
#el = document.createElement('div')
|
||||
#appEl
|
||||
#queries = new URLSearchParams(window.location.search)
|
||||
#voice
|
||||
#music
|
||||
#player
|
||||
#background
|
||||
#logo
|
||||
#configQ = []
|
||||
#isInited = false
|
||||
#isSelfInited = false
|
||||
#isAllInited = false
|
||||
#insight = new Insight()
|
||||
|
||||
constructor(appEl) {
|
||||
console.log(
|
||||
'All resources are extracted from Arknights. Github: https://gura.ch/aklive2d-gh'
|
||||
)
|
||||
|
||||
window.addEventListener('contextmenu', (e) => e.preventDefault())
|
||||
document.addEventListener('gesturestart', (e) => e.preventDefault())
|
||||
|
||||
this.#appEl = appEl
|
||||
this.#logo = new Logo(this.#appEl)
|
||||
this.#background = new Background(this.#appEl)
|
||||
this.#voice = new Voice(this.#appEl)
|
||||
this.#music = new Music(this.#appEl)
|
||||
if (isWebGLSupported()) {
|
||||
this.#player = new Player(this.#appEl)
|
||||
} else {
|
||||
new Fallback(this.#appEl)
|
||||
}
|
||||
addEventListeners([
|
||||
{
|
||||
event: Events.Player.Ready.name,
|
||||
handler: () => this.#selfInited(),
|
||||
},
|
||||
{
|
||||
event: Events.RegisterConfig.name,
|
||||
handler: (e) => this.#registerConfig(e),
|
||||
},
|
||||
])
|
||||
|
||||
Promise.all(
|
||||
[
|
||||
this.#logo,
|
||||
this.#background,
|
||||
this.#voice,
|
||||
this.#music,
|
||||
this.#player,
|
||||
].map(async (e) => e && (await e.init()))
|
||||
).then(() => this.#allInited())
|
||||
}
|
||||
|
||||
#registerConfig(e) {
|
||||
if (!this.#isInited) {
|
||||
this.#configQ.push(e.detail)
|
||||
} else {
|
||||
this.#applyConfig(e.detail)
|
||||
}
|
||||
}
|
||||
|
||||
#applyConfig(config = null) {
|
||||
if (config) {
|
||||
let targetObj
|
||||
const target = config.target
|
||||
switch (target) {
|
||||
case 'player':
|
||||
targetObj = this.#player
|
||||
break
|
||||
case 'background':
|
||||
targetObj = this.#background
|
||||
break
|
||||
case 'logo':
|
||||
targetObj = this.#logo
|
||||
break
|
||||
case 'music':
|
||||
targetObj = this.#music
|
||||
break
|
||||
case 'voice':
|
||||
targetObj = this.#voice
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
targetObj.applyConfig(config.key, config.value)
|
||||
} else {
|
||||
this.#configQ.map((e) => this.#applyConfig(e))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
get voice() {
|
||||
return this.#voice
|
||||
}
|
||||
|
||||
get music() {
|
||||
return this.#music
|
||||
}
|
||||
|
||||
get player() {
|
||||
return this.#player
|
||||
}
|
||||
|
||||
get background() {
|
||||
return this.#background
|
||||
}
|
||||
|
||||
get logo() {
|
||||
return this.#logo
|
||||
}
|
||||
|
||||
get events() {
|
||||
return Events
|
||||
}
|
||||
|
||||
get config() {
|
||||
return {
|
||||
player: this.#player.config,
|
||||
background: this.#background.config,
|
||||
logo: this.#logo.config,
|
||||
music: this.#music.config,
|
||||
voice: this.#voice.config,
|
||||
}
|
||||
}
|
||||
|
||||
get configStr() {
|
||||
return JSON.stringify(this.config, null)
|
||||
}
|
||||
|
||||
open() {
|
||||
this.#el.hidden = false
|
||||
}
|
||||
|
||||
close() {
|
||||
this.#el.hidden = true
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#player.reset()
|
||||
this.#background.reset()
|
||||
this.#logo.reset()
|
||||
this.#voice.reset()
|
||||
this.#music.reset()
|
||||
}
|
||||
|
||||
#allInited() {
|
||||
this.#isAllInited = true
|
||||
if (this.#isSelfInited) {
|
||||
this.#success()
|
||||
}
|
||||
}
|
||||
|
||||
#selfInited() {
|
||||
this.#isSelfInited = true
|
||||
if (this.#isAllInited) {
|
||||
this.#success()
|
||||
}
|
||||
}
|
||||
|
||||
#success() {
|
||||
this.#isInited = true
|
||||
this.#el.id = 'settings-box'
|
||||
this.#el.hidden = true
|
||||
this.#el.innerHTML = `
|
||||
<div>
|
||||
${this.#logo.HTML}
|
||||
${this.#background.HTML}
|
||||
${this.#player.HTML}
|
||||
${this.#music.HTML}
|
||||
${this.#voice.HTML}
|
||||
<div>
|
||||
<button type="button" id="settings-reset">Reset</button>
|
||||
<button type="button" id="settings-close">Close</button>
|
||||
<button type="button" id="settings-to-directory">Back to Directory</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
insertHTMLChild(this.#appEl, this.#el)
|
||||
addEventListeners([
|
||||
{
|
||||
id: 'settings-reset',
|
||||
event: 'click',
|
||||
handler: () => this.reset(),
|
||||
},
|
||||
{
|
||||
id: 'settings-close',
|
||||
event: 'click',
|
||||
handler: () => this.close(),
|
||||
},
|
||||
{
|
||||
id: 'settings-to-directory',
|
||||
event: 'click',
|
||||
handler: () => {
|
||||
window.location.href = '/'
|
||||
},
|
||||
},
|
||||
...this.#logo.listeners,
|
||||
...this.#background.listeners,
|
||||
...this.#player.listeners,
|
||||
...this.#voice.listeners,
|
||||
...this.#music.listeners,
|
||||
...this.#insight.listeners,
|
||||
])
|
||||
|
||||
this.#music.link(this.#background)
|
||||
this.#background.link(this.#music)
|
||||
this.#voice.link(this.#player)
|
||||
this.#player.success()
|
||||
this.#voice.success()
|
||||
this.#music.success()
|
||||
this.#insight.success()
|
||||
|
||||
this.#applyConfig()
|
||||
|
||||
if (
|
||||
this.#queries.has('aklive2d') ||
|
||||
import.meta.env.MODE === 'development'
|
||||
) {
|
||||
this.open()
|
||||
}
|
||||
this.#registerBackCompatibilityFns()
|
||||
}
|
||||
|
||||
#registerBackCompatibilityFns() {
|
||||
const _this = this
|
||||
window.voice = _this.#voice
|
||||
window.music = _this.#music
|
||||
window.settings = {
|
||||
elementPosition: updateElementPosition,
|
||||
open: _this.open,
|
||||
close: _this.close,
|
||||
reset: _this.reset,
|
||||
..._this.#player.backCompatibilityFns,
|
||||
..._this.#logo.backCompatibilityFns,
|
||||
..._this.#music.backCompatibilityFns,
|
||||
..._this.#background.backCompatibilityFns,
|
||||
}
|
||||
}
|
||||
}
|
||||
23
apps/showcase/src/components/background.css
Normal file
23
apps/showcase/src/components/background.css
Normal file
@@ -0,0 +1,23 @@
|
||||
#background-box {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
#background-box #video-src {
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
298
apps/showcase/src/components/background.js
Normal file
298
apps/showcase/src/components/background.js
Normal file
@@ -0,0 +1,298 @@
|
||||
import {
|
||||
readFile,
|
||||
updateHTMLOptions,
|
||||
showRelatedHTML,
|
||||
syncHTMLValue,
|
||||
insertHTMLChild,
|
||||
} from '@/components/helper'
|
||||
import '@/components/background.css'
|
||||
import buildConfig from '!/config.json'
|
||||
|
||||
export default class Background {
|
||||
#el = document.createElement('div')
|
||||
#parentEl
|
||||
#videoEl
|
||||
#default = {
|
||||
location: `${import.meta.env.BASE_URL}assets/${buildConfig.background_folder}/`,
|
||||
image: buildConfig.default_background,
|
||||
}
|
||||
#config = {
|
||||
video: {
|
||||
name: null,
|
||||
volume: 100,
|
||||
},
|
||||
useVideo: false,
|
||||
name: null,
|
||||
}
|
||||
#musicObj
|
||||
|
||||
constructor(el) {
|
||||
this.#parentEl = el
|
||||
this.#el.id = 'background-box'
|
||||
this.image = this.#default.location + this.#default.image
|
||||
this.#el.innerHTML = `
|
||||
<video autoplay loop disablepictureinpicture id="video-src" />
|
||||
`
|
||||
insertHTMLChild(this.#parentEl, this.#el)
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.#videoEl = document.getElementById('video-src')
|
||||
}
|
||||
|
||||
resetImage() {
|
||||
document.getElementById('custom-background').value = ''
|
||||
document.getElementById('custom-background-clear').disabled = true
|
||||
this.#config.name = null
|
||||
this.image = this.#default.location + this.#default.image
|
||||
}
|
||||
|
||||
resetVideo() {
|
||||
this.#config.video.name = null
|
||||
this.#videoEl.src = ''
|
||||
document.getElementById('custom-video-background').value = ''
|
||||
document.getElementById('custom-video-background-clear').disabled = true
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.resetImage()
|
||||
this.resetVideo()
|
||||
}
|
||||
|
||||
link(musicObj) {
|
||||
this.#musicObj = musicObj
|
||||
}
|
||||
|
||||
get useVideo() {
|
||||
return this.#config.useVideo
|
||||
}
|
||||
|
||||
set useVideo(v) {
|
||||
this.#config.useVideo = v
|
||||
}
|
||||
|
||||
set image(v) {
|
||||
this.#el.style.backgroundImage = `url("${v}")`
|
||||
}
|
||||
|
||||
set video(v) {
|
||||
if (!v) {
|
||||
this.resetVideo()
|
||||
return
|
||||
}
|
||||
const update = (url, v = null) => {
|
||||
this.#config.video.name = {
|
||||
isLocalFile: v !== null,
|
||||
value: v ? v.name : url,
|
||||
}
|
||||
this.#videoEl.src = url
|
||||
this.#videoEl.load()
|
||||
document.getElementById('custom-video-background-clear').disabled =
|
||||
false
|
||||
}
|
||||
if (typeof v === 'object') {
|
||||
readFile(v, (blobURL) => update(blobURL, v))
|
||||
} else {
|
||||
update(v)
|
||||
}
|
||||
}
|
||||
|
||||
get volume() {
|
||||
return this.#config.video.volume
|
||||
}
|
||||
|
||||
set volume(v) {
|
||||
v = parseInt(v)
|
||||
this.#config.video.volume = v
|
||||
this.#videoEl.volume = v / 100
|
||||
}
|
||||
|
||||
get current() {
|
||||
return this.#config.name || this.#default.image
|
||||
}
|
||||
|
||||
set default(v) {
|
||||
this.#default.image = v
|
||||
this.#musicObj.music = v
|
||||
this.image = this.#default.location + this.#default.image
|
||||
}
|
||||
|
||||
set custom(v) {
|
||||
if (!v) {
|
||||
this.resetImage()
|
||||
return
|
||||
}
|
||||
const update = (url, v = null) => {
|
||||
this.#config.name = {
|
||||
isLocalFile: v !== null,
|
||||
value: v ? v.name : url,
|
||||
}
|
||||
this.image = url
|
||||
document.getElementById('custom-background-clear').disabled = false
|
||||
}
|
||||
if (typeof v === 'object') {
|
||||
readFile(v, (blobURL) => update(blobURL, v))
|
||||
} else {
|
||||
update(v)
|
||||
}
|
||||
}
|
||||
|
||||
get config() {
|
||||
return {
|
||||
default: this.#default.image,
|
||||
...this.#config,
|
||||
}
|
||||
}
|
||||
|
||||
get backCompatibilityFns() {
|
||||
const _this = this
|
||||
return {
|
||||
currentBackground: _this.current,
|
||||
setBackgoundImage: (v) => (_this.image = v),
|
||||
setDefaultBackground: (v) => (_this.default = v),
|
||||
setBackground: (v) => (_this.custom = v),
|
||||
resetBackground: _this.resetImage,
|
||||
setVideo: (e) => (_this.video = e.target.files[0]),
|
||||
getVideoVolume: () => _this.volume,
|
||||
setVideoVolume: (v) => (_this.volume = v),
|
||||
setVideoFromWE: (url) => (_this.video = url),
|
||||
resetVideo: _this.resetVideo,
|
||||
}
|
||||
}
|
||||
|
||||
get HTML() {
|
||||
return `
|
||||
<div>
|
||||
<div>
|
||||
<label for="default-background-select">Choose a default background:</label>
|
||||
<select name="default-backgrounds" id="default-background-select">
|
||||
${updateHTMLOptions(buildConfig.background_files, null, this.#default.image)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom-background">Custom Background (Store Locally)</label>
|
||||
<input type="file" id="custom-background" accept="image/*"/>
|
||||
<button type="button" id="custom-background-clear" ${this.#config.name ? (this.#config.name.isLocalFile ? '' : 'disabled') : 'disabled'}>Clear</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom-background-url">Custom Background URL:</label>
|
||||
<input type="text" id="custom-background-url" name="custom-background-url" value="${this.#config.name ? this.#config.name.value : ''}">
|
||||
<button type="button" id="custom-background-url-apply">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="video">Video</label>
|
||||
<input type="checkbox" id="video" name="video" ${this.useVideo ? 'checked' : ''}/>
|
||||
<div id="video-realted" ${this.useVideo ? '' : 'hidden'}>
|
||||
<div>
|
||||
<label for="custom-video-background">Custom Video Background (Store Locally)</label>
|
||||
<input type="file" id="custom-video-background" accept="video/*"/>
|
||||
<button type="button" id="custom-video-background-clear" ${this.#config.video.name ? (this.#config.video.name.isLocalFile ? '' : 'disabled') : 'disabled'}>Clear</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom-video-background-url">Custom Video Background URL:</label>
|
||||
<input type="text" id="custom-video-background-url" name="custom-video-background-url" value="${this.#config.video.name ? this.#config.video.name.value : ''}">
|
||||
<button type="button" id="custom-video-background-url-apply">Apply</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="video-volume">Video Volume</label>
|
||||
<input type="range" min="0" max="100" step="1" id="video-volume-slider" value="${this.volume}" />
|
||||
<input type="number" id="video-volume-input" min="0" max="100" step="1" name="video-volume" value="${this.volume}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
get listeners() {
|
||||
return [
|
||||
{
|
||||
id: 'default-background-select',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
this.default = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'custom-background',
|
||||
event: 'change',
|
||||
handler: (e) => (this.custom = e.target.files[0]),
|
||||
},
|
||||
{
|
||||
id: 'custom-background-clear',
|
||||
event: 'click',
|
||||
handler: () => this.resetImage(),
|
||||
},
|
||||
{
|
||||
id: 'custom-background-url-apply',
|
||||
event: 'click',
|
||||
handler: () =>
|
||||
(this.custom = document.getElementById(
|
||||
'custom-background-url'
|
||||
).value),
|
||||
},
|
||||
{
|
||||
id: 'video',
|
||||
event: 'click',
|
||||
handler: (e) => {
|
||||
showRelatedHTML(e.currentTarget, 'video-realted')
|
||||
this.useVideo = e.currentTarget.checked
|
||||
if (!e.currentTarget.checked) this.resetVideo()
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'custom-video-background',
|
||||
event: 'change',
|
||||
handler: (e) => (this.video = e.target.files[0]),
|
||||
},
|
||||
{
|
||||
id: 'custom-video-background-clear',
|
||||
event: 'click',
|
||||
handler: () => this.resetVideo(),
|
||||
},
|
||||
{
|
||||
id: 'custom-video-background-url-apply',
|
||||
event: 'click',
|
||||
handler: () =>
|
||||
(this.video = document.getElementById(
|
||||
'custom-video-background-url'
|
||||
).value),
|
||||
},
|
||||
{
|
||||
id: 'video-volume-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'video-volume-input')
|
||||
this.volume = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'video-volume-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'video-volume-slider')
|
||||
this.volume = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
applyConfig(key, value) {
|
||||
switch (key) {
|
||||
case 'default':
|
||||
this.default = value
|
||||
break
|
||||
case 'custom':
|
||||
this.custom = value
|
||||
break
|
||||
case 'video':
|
||||
this.video = value
|
||||
break
|
||||
case 'volume':
|
||||
this.volume = value
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
11
apps/showcase/src/components/events.js
Normal file
11
apps/showcase/src/components/events.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createCustomEvent } from '@/components/helper'
|
||||
import { Events as Insight } from '@/components/insight'
|
||||
import { Events as Player } from '@/components/player'
|
||||
|
||||
const RegisterConfig = createCustomEvent('register-config', true)
|
||||
|
||||
export default {
|
||||
Insight,
|
||||
Player,
|
||||
RegisterConfig,
|
||||
}
|
||||
16
apps/showcase/src/components/fallback.css
Normal file
16
apps/showcase/src/components/fallback.css
Normal file
@@ -0,0 +1,16 @@
|
||||
#fallback-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#fallback {
|
||||
margin: auto;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
40
apps/showcase/src/components/fallback.js
Normal file
40
apps/showcase/src/components/fallback.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { insertHTMLChild } from '@/components/helper'
|
||||
import '@/components/fallback.css'
|
||||
import buildConfig from '!/config.json'
|
||||
|
||||
export default class Fallback {
|
||||
#el = document.createElement('div')
|
||||
|
||||
constructor(parentEl) {
|
||||
alert('WebGL is unavailable. Fallback image will be used.')
|
||||
const calculateScale = (width, height) => {
|
||||
return {
|
||||
x: window.innerWidth / width,
|
||||
y: window.innerHeight / height,
|
||||
}
|
||||
}
|
||||
const fallback = () => {
|
||||
const el = document.getElementById('fallback-container')
|
||||
const scale = calculateScale(
|
||||
buildConfig.image_width,
|
||||
buildConfig.image_height
|
||||
)
|
||||
el.style.width = '100%'
|
||||
el.style.height =
|
||||
buildConfig.image_height *
|
||||
(scale.x > scale.y ? scale.y : scale.x) +
|
||||
'px'
|
||||
}
|
||||
window.addEventListener('resize', fallback, true)
|
||||
this.#el.id = 'fallback-box'
|
||||
this.#el.innerHTML = `
|
||||
<div id="fallback-container">
|
||||
<div id="fallback"
|
||||
style="background-image: url(./assets/${buildConfig.fallback_name}.png)"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
insertHTMLChild(parentEl, this.#el)
|
||||
fallback()
|
||||
}
|
||||
}
|
||||
101
apps/showcase/src/components/helper.js
Normal file
101
apps/showcase/src/components/helper.js
Normal file
@@ -0,0 +1,101 @@
|
||||
export const isWebGLSupported = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx =
|
||||
canvas.getContext('webgl') ||
|
||||
canvas.getContext('experimental-webgl')
|
||||
return ctx != null
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const insertHTMLChild = (parent, child) => {
|
||||
parent.appendChild(child)
|
||||
}
|
||||
|
||||
export const insertHTMLNodeBefore = (parent, sibling, child) => {
|
||||
parent.insertBefore(child, sibling)
|
||||
}
|
||||
|
||||
const getIntPx = (value) => parseInt(value.replace('px', ''))
|
||||
export const updateElementPosition = (el, position) => {
|
||||
const computedStyle = getComputedStyle(el)
|
||||
const elWidth = getIntPx(computedStyle.width)
|
||||
const elHeight = getIntPx(computedStyle.height)
|
||||
const elMarginLeft = getIntPx(computedStyle.marginLeft)
|
||||
const elMarginRight = getIntPx(computedStyle.marginRight)
|
||||
const windowWidth = window.innerWidth
|
||||
const windowHeight = window.innerHeight
|
||||
const xRange = windowWidth - (elWidth + elMarginLeft + elMarginRight)
|
||||
const yRange = windowHeight - elHeight
|
||||
const xpx = (position.x * xRange) / 100
|
||||
const ypx = (position.y * yRange) / 100
|
||||
el.style.transform = `translate(${xpx}px, ${ypx}px)`
|
||||
}
|
||||
|
||||
export const updateHTMLOptions = (array, id = null, selected = null) => {
|
||||
const value = array.map(
|
||||
(item) =>
|
||||
`<option value="${item}" ${item === selected ? 'selected' : ''}>${item}</option>`
|
||||
)
|
||||
if (id) {
|
||||
document.getElementById(id).innerHTML = value.join('')
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export const addEventListeners = (listeners) => {
|
||||
listeners.forEach((listener) => {
|
||||
if (listener.id) {
|
||||
document
|
||||
.getElementById(listener.id)
|
||||
.addEventListener(listener.event, (e) => listener.handler(e))
|
||||
} else {
|
||||
document.addEventListener(listener.event, (e) =>
|
||||
listener.handler(e)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const showRelatedHTML = (e, relatedSettingsID, revert = false) => {
|
||||
const eRelatedSettings = document.getElementById(relatedSettingsID)
|
||||
const checked = revert ? !e.checked : e.checked
|
||||
if (checked) {
|
||||
eRelatedSettings.hidden = false
|
||||
} else {
|
||||
eRelatedSettings.hidden = true
|
||||
}
|
||||
}
|
||||
|
||||
export const getCurrentHTMLOptions = (id, value) => {
|
||||
const e = document.getElementById(id)
|
||||
const options = [...e]
|
||||
const toSelecteIndex = options.findIndex(
|
||||
(i) => options.find((o) => o.value === value) === i
|
||||
)
|
||||
e.selectedIndex = toSelecteIndex
|
||||
}
|
||||
|
||||
export const syncHTMLValue = (source, targetID) => {
|
||||
if (typeof source === 'string') source = document.getElementById(source)
|
||||
document.getElementById(targetID).value = source.value
|
||||
}
|
||||
|
||||
export const readFile = (file, callback = () => {}) => {
|
||||
if (!file) return
|
||||
callback(URL.createObjectURL(file.slice()), file.type)
|
||||
}
|
||||
|
||||
export const createCustomEvent = (name, withArg = false) => {
|
||||
const ret = {
|
||||
name,
|
||||
}
|
||||
if (withArg) {
|
||||
ret.handler = (detail) => new CustomEvent(name, { detail })
|
||||
} else {
|
||||
ret.handler = () => new Event(name)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
46
apps/showcase/src/components/insight.js
Normal file
46
apps/showcase/src/components/insight.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { createCustomEvent } from '@/components/helper'
|
||||
import buildConfig from '!/config.json'
|
||||
|
||||
export default class Insight {
|
||||
#isInsightInited = false
|
||||
|
||||
success() {
|
||||
this.insight(false)
|
||||
}
|
||||
|
||||
insight(doNotTrack, isFromWallpaperEngine = false) {
|
||||
if (this.#isInsightInited || import.meta.env.MODE === 'development')
|
||||
return
|
||||
this.#isInsightInited = true
|
||||
if (doNotTrack) return
|
||||
try {
|
||||
const config = {
|
||||
path: `/${buildConfig.link}`,
|
||||
}
|
||||
if (isFromWallpaperEngine)
|
||||
config.hostname = 'file://wallpaperengine.local'
|
||||
window.counterscale = {
|
||||
q: [
|
||||
['set', 'siteId', buildConfig.insight_id],
|
||||
['trackPageview', config],
|
||||
],
|
||||
}
|
||||
window.counterscaleOnDemandTrack()
|
||||
} catch (e) {
|
||||
console.warn && console.warn(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
get listeners() {
|
||||
return [
|
||||
{
|
||||
event: Events.Register.name,
|
||||
handler: (e) => this.insight(e.detail, true),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export const Events = {
|
||||
Register: createCustomEvent('insight-register', true),
|
||||
}
|
||||
6
apps/showcase/src/components/logo.css
Normal file
6
apps/showcase/src/components/logo.css
Normal file
@@ -0,0 +1,6 @@
|
||||
#logo-box {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
411
apps/showcase/src/components/logo.js
Normal file
411
apps/showcase/src/components/logo.js
Normal file
@@ -0,0 +1,411 @@
|
||||
import {
|
||||
insertHTMLChild,
|
||||
updateElementPosition,
|
||||
readFile,
|
||||
showRelatedHTML,
|
||||
syncHTMLValue,
|
||||
} from '@/components/helper'
|
||||
import '@/components/logo.css'
|
||||
import buildConfig from '!/config.json'
|
||||
|
||||
export default class Logo {
|
||||
#el = document.createElement('div')
|
||||
#imageEl
|
||||
#parentEl
|
||||
#default = {
|
||||
location: `${import.meta.env.BASE_URL}assets/`,
|
||||
image: `${buildConfig.logo_filename}.png`,
|
||||
useInvertFilter: buildConfig.invert_filter === 'true',
|
||||
ratio: 61.8,
|
||||
opacity: 30,
|
||||
hidden: false,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
}
|
||||
#config = {
|
||||
hidden: this.#default.hidden,
|
||||
ratio: this.#default.ratio,
|
||||
opacity: this.#default.opacity,
|
||||
position: { ...this.#default.position },
|
||||
name: null,
|
||||
}
|
||||
|
||||
constructor(el) {
|
||||
this.#parentEl = el
|
||||
this.#el.id = 'logo-box'
|
||||
this.#el.innerHTML = `
|
||||
<img src="${this.#default.location + this.#default.image}" id="logo" alt="operator logo" />
|
||||
`
|
||||
insertHTMLChild(this.#parentEl, this.#el)
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.#imageEl = document.getElementById('logo')
|
||||
this.#setInvertFilter(this.#default.useInvertFilter)
|
||||
this.opacity = this.#default.opacity
|
||||
this.#updateSizeOnWindowResize()
|
||||
}
|
||||
|
||||
setImage(src, invertFilter = false) {
|
||||
this.#imageEl.src = src
|
||||
this.#resize()
|
||||
this.#setInvertFilter(invertFilter)
|
||||
}
|
||||
|
||||
resetPosition() {
|
||||
this.position = { ...this.#default.position }
|
||||
document.getElementById('logo-position-x-slider').value =
|
||||
this.#default.position.x
|
||||
document.getElementById('logo-position-x-input').value =
|
||||
this.#default.position.x
|
||||
document.getElementById('logo-position-y-slider').value =
|
||||
this.#default.position.y
|
||||
document.getElementById('logo-position-y-input').value =
|
||||
this.#default.position.y
|
||||
}
|
||||
|
||||
resetImage() {
|
||||
this.#config.name = null
|
||||
this.setImage(
|
||||
this.#default.location + this.#default.image,
|
||||
this.#default.useInvertFilter
|
||||
)
|
||||
document.getElementById('logo-image-clear').disabled = true
|
||||
}
|
||||
|
||||
resetOpacity() {
|
||||
this.opacity = this.#default.opacity
|
||||
document.getElementById('logo-opacity-slider').value =
|
||||
this.#default.opacity
|
||||
document.getElementById('logo-opacity-input').value =
|
||||
this.#default.opacity
|
||||
}
|
||||
|
||||
resetHidden() {
|
||||
this.hidden = this.#default.hidden
|
||||
}
|
||||
|
||||
resetRatio() {
|
||||
this.ratio = this.#default.ratio
|
||||
document.getElementById('logo-ratio-slider').value = this.#default.ratio
|
||||
document.getElementById('logo-ratio-input').value = this.#default.ratio
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.resetPosition()
|
||||
this.resetImage()
|
||||
this.resetRatio()
|
||||
this.resetOpacity()
|
||||
this.resetHidden()
|
||||
}
|
||||
|
||||
#resize(_this, value) {
|
||||
_this = _this || this
|
||||
_this.#imageEl.width =
|
||||
((window.innerWidth / 2) * (value || _this.ratio)) / 100
|
||||
updateElementPosition(_this.#imageEl, _this.#config.position)
|
||||
}
|
||||
|
||||
#setInvertFilter(v) {
|
||||
if (!v) {
|
||||
this.#imageEl.style.filter = 'invert(0)'
|
||||
} else {
|
||||
this.#imageEl.style.filter = 'invert(1)'
|
||||
}
|
||||
}
|
||||
|
||||
#updateLogoPosition() {
|
||||
updateElementPosition(this.#imageEl, this.#config.position)
|
||||
}
|
||||
|
||||
#updateSizeOnWindowResize() {
|
||||
const _this = this
|
||||
const resize = () => {
|
||||
_this.#resize(_this)
|
||||
}
|
||||
window.addEventListener('resize', resize, true)
|
||||
resize()
|
||||
}
|
||||
|
||||
set image(v) {
|
||||
if (!v) {
|
||||
this.resetImage()
|
||||
return
|
||||
}
|
||||
const update = (url, v = null) => {
|
||||
this.#config.name = {
|
||||
isLocalFile: v !== null,
|
||||
value: v ? v.name : url,
|
||||
}
|
||||
this.setImage(url, false)
|
||||
document.getElementById('logo-image-clear').disabled = false
|
||||
}
|
||||
if (typeof v === 'object') {
|
||||
readFile(v, (blobURL) => update(blobURL, v))
|
||||
} else {
|
||||
update(v)
|
||||
}
|
||||
}
|
||||
|
||||
get hidden() {
|
||||
return this.#config.hidden
|
||||
}
|
||||
|
||||
set hidden(v) {
|
||||
this.#config.hidden = v
|
||||
this.#imageEl.hidden = v
|
||||
}
|
||||
|
||||
get ratio() {
|
||||
return this.#config.ratio
|
||||
}
|
||||
|
||||
set ratio(v) {
|
||||
v = parseInt(v)
|
||||
this.#config.ratio = v
|
||||
this.#resize(this, v)
|
||||
}
|
||||
|
||||
get opacity() {
|
||||
return this.#config.opacity
|
||||
}
|
||||
|
||||
set opacity(v) {
|
||||
v = parseInt(v)
|
||||
this.#imageEl.style.opacity = v / 100
|
||||
this.#config.opacity = v
|
||||
}
|
||||
|
||||
get x() {
|
||||
return this.position.x
|
||||
}
|
||||
|
||||
set x(v) {
|
||||
this.position = {
|
||||
x: v,
|
||||
}
|
||||
}
|
||||
|
||||
get y() {
|
||||
return this.position.y
|
||||
}
|
||||
|
||||
set y(v) {
|
||||
this.position = {
|
||||
y: v,
|
||||
}
|
||||
}
|
||||
|
||||
get position() {
|
||||
return this.#config.position
|
||||
}
|
||||
|
||||
set position(v) {
|
||||
if (typeof v !== 'object') return
|
||||
if (v.x) v.x = parseInt(v.x)
|
||||
if (v.y) v.y = parseInt(v.y)
|
||||
this.#config.position = { ...this.#config.position, ...v }
|
||||
this.#updateLogoPosition()
|
||||
}
|
||||
|
||||
get backCompatibilityFns() {
|
||||
const _this = this
|
||||
return {
|
||||
setLogoDisplay: (v) => (_this.hidden = v),
|
||||
setLogo: _this.setImage,
|
||||
setLogoImage: (e) => (_this.image = e.target.files[0]),
|
||||
resetLogoImage: _this.resetImage,
|
||||
setLogoRatio: (v) => (_this.ratio = v),
|
||||
setLogoOpacity: (v) => (_this.opacity = v),
|
||||
logoPadding: (key, value) => {
|
||||
switch (key) {
|
||||
case 'x':
|
||||
this.position = {
|
||||
x: value,
|
||||
}
|
||||
break
|
||||
case 'y':
|
||||
this.position = {
|
||||
y: value,
|
||||
}
|
||||
break
|
||||
default:
|
||||
this.position = value
|
||||
break
|
||||
}
|
||||
},
|
||||
logoReset: _this.resetPosition,
|
||||
}
|
||||
}
|
||||
|
||||
get config() {
|
||||
return {
|
||||
...this.#config,
|
||||
}
|
||||
}
|
||||
|
||||
get HTML() {
|
||||
return `
|
||||
<label for="operator-logo">Operator Logo</label>
|
||||
<input type="checkbox" id="operator-logo" name="operator-logo" ${this.hidden ? '' : 'checked'}/>
|
||||
<div id="operator-logo-realted" ${this.hidden ? 'hidden' : ''}>
|
||||
<div>
|
||||
<label for="logo-image">Logo Image (Store Locally)</label>
|
||||
<input type="file" id="logo-image" accept="image/*"/>
|
||||
<button type="button" id="logo-image-clear" ${this.#config.name ? (this.#config.name.isLocalFile ? '' : 'disabled') : 'disabled'}>Clear</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="logo-image-url">Logo Image URL:</label>
|
||||
<input type="text" id="logo-image-url" name="logo-image-url" value="${this.#config.name ? this.#config.name.value : ''}">
|
||||
<button type="button" id="logo-image-url-apply">Apply</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="logo-ratio">Logo Ratio</label>
|
||||
<input type="range" min="0" max="100" step="0.1" id="logo-ratio-slider" value="${this.ratio}" />
|
||||
<input type="number" id="logo-ratio-input" name="logo-ratio" value="${this.ratio}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="logo-opacity">Logo Opacity</label>
|
||||
<input type="range" min="0" max="100" data-css-class="logo" step="1" id="logo-opacity-slider" value="${this.opacity}" />
|
||||
<input type="number" id="logo-opacity-input" name="logo-opacity" value="${this.opacity}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="logo-position-x">Logo X Position</label>
|
||||
<input type="range" min="0" max="100" id="logo-position-x-slider" value="${this.position.x}" />
|
||||
<input type="number" id="logo-position-x-input" name="logo-position-x" value="${this.position.x}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="logo-position-y">Logo Y Position</label>
|
||||
<input type="range" min="0" max="100" id="logo-position-y-slider" value="${this.position.y}" />
|
||||
<input type="number" id="logo-position-y-input" name="logo-position-y" value="${this.position.y}" />
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
get listeners() {
|
||||
return [
|
||||
{
|
||||
id: 'operator-logo',
|
||||
event: 'click',
|
||||
handler: (e) => {
|
||||
showRelatedHTML(e.currentTarget, 'operator-logo-realted')
|
||||
this.hidden = !e.currentTarget.checked
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'logo-image',
|
||||
event: 'change',
|
||||
handler: (e) => (this.image = e.target.files[0]),
|
||||
},
|
||||
{
|
||||
id: 'logo-image-clear',
|
||||
event: 'click',
|
||||
handler: () => this.resetImage(),
|
||||
},
|
||||
{
|
||||
id: 'logo-image-url-apply',
|
||||
event: 'click',
|
||||
handler: () =>
|
||||
(this.image =
|
||||
document.getElementById('logo-image-url').value),
|
||||
},
|
||||
{
|
||||
id: 'logo-ratio-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'logo-ratio-input')
|
||||
this.ratio = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'logo-ratio-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'logo-ratio-slider')
|
||||
this.ratio = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'logo-opacity-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'logo-opacity-input')
|
||||
this.opacity = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'logo-opacity-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'logo-opacity-slider')
|
||||
this.opacity = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'logo-position-x-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'logo-position-x-input')
|
||||
this.position = {
|
||||
x: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'logo-position-x-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'logo-position-x-slider')
|
||||
this.position = {
|
||||
x: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'logo-position-y-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'logo-position-y-input')
|
||||
this.position = {
|
||||
y: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'logo-position-y-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'logo-position-y-slider')
|
||||
this.position = {
|
||||
y: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
applyConfig(key, value) {
|
||||
switch (key) {
|
||||
case 'hidden':
|
||||
this.hidden = value
|
||||
break
|
||||
case 'ratio':
|
||||
this.ratio = value
|
||||
break
|
||||
case 'opacity':
|
||||
this.opacity = value
|
||||
break
|
||||
case 'image':
|
||||
this.image = value
|
||||
break
|
||||
case 'position':
|
||||
this.position = value
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
365
apps/showcase/src/components/music.js
Normal file
365
apps/showcase/src/components/music.js
Normal file
@@ -0,0 +1,365 @@
|
||||
import {
|
||||
insertHTMLChild,
|
||||
updateHTMLOptions,
|
||||
showRelatedHTML,
|
||||
syncHTMLValue,
|
||||
readFile,
|
||||
getCurrentHTMLOptions,
|
||||
} from '@/components/helper'
|
||||
import buildConfig from '!/config.json'
|
||||
|
||||
export default class Music {
|
||||
#el = document.createElement('div')
|
||||
#parentEl
|
||||
#audio = {
|
||||
intro: {
|
||||
id: 'music-intro',
|
||||
el: null,
|
||||
},
|
||||
loop: {
|
||||
id: 'music-loop',
|
||||
el: null,
|
||||
},
|
||||
}
|
||||
#music = {
|
||||
mapping: buildConfig.music_mapping,
|
||||
location: buildConfig.music_folder,
|
||||
current: null,
|
||||
isUsingCustom: false,
|
||||
list: [],
|
||||
}
|
||||
#config = {
|
||||
useMusic: false,
|
||||
timeOffset: 0.3,
|
||||
volume: 50,
|
||||
name: null,
|
||||
}
|
||||
#backgroundObj
|
||||
|
||||
constructor(el) {
|
||||
this.#parentEl = el
|
||||
this.#el.id = 'music-box'
|
||||
this.#el.innerHTML = `
|
||||
<audio id="${this.#audio.intro.id}" preload="auto">
|
||||
<source type="audio/ogg" />
|
||||
</audio>
|
||||
<audio id="${this.#audio.loop.id}" preload="auto">
|
||||
<source type="audio/ogg" />
|
||||
</audio>
|
||||
`
|
||||
insertHTMLChild(this.#parentEl, this.#el)
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.#music.list = Object.keys(this.#music.mapping)
|
||||
this.#audio.intro.el = document.getElementById(this.#audio.intro.id)
|
||||
this.#audio.loop.el = document.getElementById(this.#audio.loop.id)
|
||||
this.#audio.intro.el.volume = this.#volume
|
||||
this.#audio.loop.el.volume = this.#volume
|
||||
this.#audio.intro.el.ontimeupdate = () => {
|
||||
if (
|
||||
this.#audio.intro.el.currentTime >=
|
||||
this.#audio.intro.el.duration - this.#config.timeOffset
|
||||
) {
|
||||
this.#audio.intro.el.pause()
|
||||
this.#audio.loop.el.currentTime = 0
|
||||
this.#audio.loop.el.volume = this.#volume
|
||||
}
|
||||
}
|
||||
this.#audio.loop.el.ontimeupdate = () => {
|
||||
if (
|
||||
this.#audio.loop.el.currentTime >=
|
||||
this.#audio.loop.el.duration - this.#config.timeOffset
|
||||
) {
|
||||
this.#audio.loop.el.currentTime = 0
|
||||
this.#audio.loop.el.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
success() {
|
||||
if (this.#music.current === null)
|
||||
this.music = this.#backgroundObj.current
|
||||
}
|
||||
|
||||
link(backgroundObj) {
|
||||
this.#backgroundObj = backgroundObj
|
||||
}
|
||||
|
||||
reset() {
|
||||
document.getElementById('custom-music').value = ''
|
||||
document.getElementById('custom-music-clear').disabled = true
|
||||
this.#music.isUsingCustom = false
|
||||
this.#config.name = null
|
||||
if (this.#config.useMusic) {
|
||||
this.#playMusic()
|
||||
}
|
||||
}
|
||||
|
||||
#setMusic(data, type) {
|
||||
this.#audio.loop.el.src = data
|
||||
this.#audio.loop.el.querySelector('source').type = type
|
||||
this.#music.isUsingCustom = true
|
||||
this.#playMusic()
|
||||
}
|
||||
|
||||
#playMusic() {
|
||||
if (!this.#music.isUsingCustom) {
|
||||
const introOgg = this.#music.mapping[this.#music.current].intro
|
||||
const intro = `./assets/${this.#music.location}/${introOgg}`
|
||||
const loop = `./assets/${this.#music.location}/${this.#music.mapping[this.#music.current].loop}`
|
||||
this.#audio.loop.el.src = loop
|
||||
this.#audio.loop.el.querySelector('source').type = 'audio/ogg'
|
||||
if (introOgg) {
|
||||
this.#audio.intro.el.src = intro || loop
|
||||
this.#audio.intro.el.querySelector('source').type = 'audio/ogg'
|
||||
this.#audio.intro.el.play()
|
||||
this.#audio.loop.el.volume = 0
|
||||
this.#audio.loop.el.play()
|
||||
} else {
|
||||
this.#audio.loop.el.volume = this.#volume
|
||||
this.#audio.loop.el.play()
|
||||
}
|
||||
} else {
|
||||
this.#audio.intro.el.pause()
|
||||
this.#audio.loop.el.volume = this.#volume
|
||||
this.#audio.loop.el.play()
|
||||
}
|
||||
}
|
||||
|
||||
#stopMusic() {
|
||||
this.#audio.intro.el.pause()
|
||||
this.#audio.loop.el.pause()
|
||||
}
|
||||
|
||||
get timeOffset() {
|
||||
return this.#config.timeOffset
|
||||
}
|
||||
|
||||
set timeOffset(value) {
|
||||
value = value < 0 ? 0 : parseFloat(value)
|
||||
this.#config.timeOffset = value
|
||||
}
|
||||
|
||||
get volume() {
|
||||
return this.#config.volume
|
||||
}
|
||||
|
||||
get #volume() {
|
||||
return this.#config.volume / 100
|
||||
}
|
||||
|
||||
set volume(value) {
|
||||
value = value < 0 ? 0 : value > 100 ? 100 : parseInt(value)
|
||||
this.#config.volume = value
|
||||
this.#audio.intro.el.volume = this.#volume
|
||||
if (this.#audio.intro.el.paused)
|
||||
this.#audio.loop.el.volume = this.#volume
|
||||
}
|
||||
|
||||
get musics() {
|
||||
return this.#music.list
|
||||
}
|
||||
|
||||
get useMusic() {
|
||||
return this.#config.useMusic
|
||||
}
|
||||
|
||||
set useMusic(value) {
|
||||
this.#config.useMusic = value
|
||||
if (value) {
|
||||
this.#playMusic()
|
||||
} else {
|
||||
this.#stopMusic()
|
||||
}
|
||||
}
|
||||
|
||||
get music() {
|
||||
return this.#music.current
|
||||
}
|
||||
|
||||
get isUsingCustom() {
|
||||
return this.#music.isUsingCustom
|
||||
}
|
||||
|
||||
set music(name) {
|
||||
if (name !== null && name !== this.#music.current) {
|
||||
this.#music.current = name
|
||||
if (this.#config.useMusic && !this.#music.isUsingCustom) {
|
||||
this.#audio.loop.el.pause()
|
||||
this.#audio.intro.el.pause()
|
||||
this.#playMusic()
|
||||
}
|
||||
getCurrentHTMLOptions('music-select', name)
|
||||
}
|
||||
}
|
||||
|
||||
set custom(url) {
|
||||
if (!url) {
|
||||
this.reset()
|
||||
return
|
||||
}
|
||||
const update = (url, type, v = null) => {
|
||||
this.#config.name = {
|
||||
isLocalFile: v !== null,
|
||||
value: v ? v.name : url,
|
||||
}
|
||||
this.#setMusic(url, type)
|
||||
document.getElementById('custom-music-clear').disabled = false
|
||||
}
|
||||
if (typeof url === 'object') {
|
||||
readFile(url, (blobURL, type) => update(blobURL, type, url))
|
||||
} else {
|
||||
update(url, url.split('.').pop())
|
||||
}
|
||||
}
|
||||
|
||||
get currentMusic() {
|
||||
// Note: Back Compatibility
|
||||
return this.music
|
||||
}
|
||||
|
||||
changeMusic(name) {
|
||||
// Note: Back Compatibility
|
||||
this.music = name
|
||||
}
|
||||
|
||||
get backCompatibilityFns() {
|
||||
const _this = this
|
||||
return {
|
||||
setMusicFromWE: (url) => (_this.custom = url),
|
||||
setMusic: (e) => (_this.custom = e.target.files[0]),
|
||||
resetMusic: _this.reset,
|
||||
}
|
||||
}
|
||||
|
||||
get config() {
|
||||
return {
|
||||
default: this.#music.current,
|
||||
...this.#config,
|
||||
}
|
||||
}
|
||||
|
||||
get HTML() {
|
||||
return `
|
||||
<div>
|
||||
<label for="music">Music</label>
|
||||
<input type="checkbox" id="music" name="music" ${this.useMusic ? 'checked' : ''}/>
|
||||
<div id="music-realted" ${this.useMusic ? '' : 'hidden'}>
|
||||
<div>
|
||||
<label for="music-select">Choose theme music:</label>
|
||||
<select name="music-select" id="music-select">
|
||||
${updateHTMLOptions(this.musics)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom-music">Custom Music (Store Locally)</label>
|
||||
<input type="file" id="custom-music" accept="audio/*"/>
|
||||
<button type="button" id="custom-music-clear" ${this.#config.name ? (this.#config.name.isLocalFile ? '' : 'disabled') : 'disabled'}>Clear</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom-music-url">Custom Music URL:</label>
|
||||
<input type="text" id="custom-music-url" name="custom-music-url" value="${this.#config.name ? this.#config.name.value : ''}">
|
||||
<button type="button" id="custom-music-url-apply">Apply</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="music-volume">Music Volume</label>
|
||||
<input type="range" min="0" max="100" step="1" id="music-volume-slider" value="${this.volume}" />
|
||||
<input type="number" id="music-volume-input" min="0" max="100" step="1" name="music-volume" value="${this.volume}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="music-switch-offset">Music Swtich Offset</label>
|
||||
<input type="range" min="0" max="1" step="0.01" id="music-switch-offset-slider" value="${this.timeOffset}" />
|
||||
<input type="number" id="music-switch-offset-input" min="0" max="1" step="0.01" name="music-switch-offset" value="${this.timeOffset}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
get listeners() {
|
||||
return [
|
||||
{
|
||||
id: 'music',
|
||||
event: 'click',
|
||||
handler: (e) => {
|
||||
showRelatedHTML(e.currentTarget, 'music-realted')
|
||||
this.useMusic = e.currentTarget.checked
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'music-select',
|
||||
event: 'change',
|
||||
handler: (e) => (this.music = e.currentTarget.value),
|
||||
},
|
||||
{
|
||||
id: 'custom-music',
|
||||
event: 'change',
|
||||
handler: (e) => (this.custom = e.target.files[0]),
|
||||
},
|
||||
{
|
||||
id: 'custom-music-clear',
|
||||
event: 'click',
|
||||
handler: () => this.reset(),
|
||||
},
|
||||
{
|
||||
id: 'custom-music-url-apply',
|
||||
event: 'click',
|
||||
handler: () =>
|
||||
(this.custom =
|
||||
document.getElementById('custom-music-url').value),
|
||||
},
|
||||
{
|
||||
id: 'music-volume-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'music-volume-input')
|
||||
this.volume = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'music-volume-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'music-volume-slider')
|
||||
this.volume = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'music-switch-offset-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'music-switch-offset-input')
|
||||
this.timeOffset = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'music-switch-offset-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'music-switch-offset-slider')
|
||||
this.timeOffset = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
applyConfig(key, value) {
|
||||
switch (key) {
|
||||
case 'music':
|
||||
this.music = value
|
||||
break
|
||||
case 'use-music':
|
||||
this.useMusic = value
|
||||
break
|
||||
case 'volume':
|
||||
this.volume = value
|
||||
break
|
||||
case 'custom':
|
||||
this.custom = value
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
4
apps/showcase/src/components/player.css
Normal file
4
apps/showcase/src/components/player.css
Normal file
@@ -0,0 +1,4 @@
|
||||
#player-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
588
apps/showcase/src/components/player.js
Normal file
588
apps/showcase/src/components/player.js
Normal file
@@ -0,0 +1,588 @@
|
||||
import {
|
||||
insertHTMLChild,
|
||||
updateHTMLOptions,
|
||||
showRelatedHTML,
|
||||
syncHTMLValue,
|
||||
createCustomEvent,
|
||||
} from '@/components/helper'
|
||||
import { spine } from '@aklive2d/module'
|
||||
import '@/components/player.css'
|
||||
import buildConfig from '!/config.json'
|
||||
|
||||
export default class Player {
|
||||
#el = document.createElement('div')
|
||||
#parentEl
|
||||
#showControls = new URLSearchParams(window.location.search).has('controls')
|
||||
#resetTime = window.performance.now()
|
||||
#isPlayingInteract = false
|
||||
#spine
|
||||
#default = {
|
||||
fps: 60,
|
||||
padding: {
|
||||
left: parseInt(buildConfig.viewport_left),
|
||||
right: parseInt(buildConfig.viewport_right),
|
||||
top: parseInt(buildConfig.viewport_top),
|
||||
bottom: parseInt(buildConfig.viewport_bottom),
|
||||
},
|
||||
scale: 1,
|
||||
}
|
||||
#config = {
|
||||
fps: this.#default.fps,
|
||||
useStartAnimation: true,
|
||||
usePadding: false,
|
||||
padding: {
|
||||
...this.#default.padding,
|
||||
},
|
||||
scale: this.#default.scale,
|
||||
}
|
||||
|
||||
constructor(el) {
|
||||
this.#parentEl = el
|
||||
this.#el.id = 'player-box'
|
||||
insertHTMLChild(this.#parentEl, this.#el)
|
||||
}
|
||||
|
||||
async init() {
|
||||
const _this = this
|
||||
const playerConfig = {
|
||||
atlasUrl: `./assets/${buildConfig.filename}.atlas`,
|
||||
premultipliedAlpha: true,
|
||||
alpha: true,
|
||||
backgroundColor: '#00000000',
|
||||
viewport: {
|
||||
debugRender: false,
|
||||
padLeft: `${buildConfig.viewport_left}%`,
|
||||
padRight: `${buildConfig.viewport_right}%`,
|
||||
padTop: `${buildConfig.viewport_top}%`,
|
||||
padBottom: `${buildConfig.viewport_bottom}%`,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
showControls: _this.#showControls,
|
||||
touch: _this.#showControls,
|
||||
fps: 60,
|
||||
defaultMix: 0,
|
||||
success: function (widget) {
|
||||
if (
|
||||
widget.skeleton.data.animations
|
||||
.map((e) => e.name)
|
||||
.includes('Start') &&
|
||||
_this.useStartAnimation
|
||||
) {
|
||||
widget.animationState.setAnimation(0, 'Start', false)
|
||||
}
|
||||
widget.animationState.addAnimation(0, 'Idle', true, 0)
|
||||
widget.animationState.addListener({
|
||||
end: (e) => {
|
||||
if (e.animation.name == 'Interact') {
|
||||
_this.#isPlayingInteract = false
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
if (
|
||||
window.performance.now() - _this.#resetTime >=
|
||||
8 * 1000 &&
|
||||
Math.random() < 0.3
|
||||
) {
|
||||
_this.#resetTime = window.performance.now()
|
||||
let entry = widget.animationState.setAnimation(
|
||||
0,
|
||||
'Special',
|
||||
false
|
||||
)
|
||||
entry.mixDuration = 0.3
|
||||
widget.animationState.addAnimation(
|
||||
0,
|
||||
'Idle',
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
widget.canvas.onclick = function () {
|
||||
if (_this.#isPlayingInteract) {
|
||||
return
|
||||
}
|
||||
_this.#isPlayingInteract = true
|
||||
let entry = widget.animationState.setAnimation(
|
||||
0,
|
||||
'Interact',
|
||||
false
|
||||
)
|
||||
entry.mixDuration = 0.3
|
||||
widget.animationState.addAnimation(0, 'Idle', true, 0)
|
||||
}
|
||||
document.dispatchEvent(Events.Ready.handler())
|
||||
},
|
||||
}
|
||||
if (buildConfig.use_json === 'true') {
|
||||
playerConfig.jsonUrl = `./assets/${buildConfig.filename}.json`
|
||||
} else {
|
||||
playerConfig.skelUrl = `./assets/${buildConfig.filename}.skel`
|
||||
}
|
||||
this.#spine = new spine.SpinePlayer(this.#el, playerConfig)
|
||||
}
|
||||
|
||||
success() {
|
||||
this.#loadViewport()
|
||||
updateHTMLOptions(
|
||||
this.#spine.skeleton.data.animations.map((e) => e.name),
|
||||
'animation-selection'
|
||||
)
|
||||
}
|
||||
|
||||
resetPadding() {
|
||||
this.padding = { ...this.#default.padding }
|
||||
document.getElementById('position-padding-left-slider').value =
|
||||
this.#default.padding.left
|
||||
document.getElementById('position-padding-left-input').value =
|
||||
this.#default.padding.left
|
||||
document.getElementById('position-padding-right-slider').value =
|
||||
this.#default.padding.right
|
||||
document.getElementById('position-padding-right-input').value =
|
||||
this.#default.padding.right
|
||||
document.getElementById('position-padding-top-slider').value =
|
||||
this.#default.padding.top
|
||||
document.getElementById('position-padding-top-input').value =
|
||||
this.#default.padding.top
|
||||
document.getElementById('position-padding-bottom-slider').value =
|
||||
this.#default.padding.bottom
|
||||
document.getElementById('position-padding-bottom-input').value =
|
||||
this.#default.padding.bottom
|
||||
}
|
||||
|
||||
resetScale() {
|
||||
this.scale = this.#default.scale
|
||||
}
|
||||
|
||||
resetFPS() {
|
||||
this.fps = this.#default.fps
|
||||
document.getElementById('fps-slider').value = this.#default.fps
|
||||
document.getElementById('fps-input').value = this.#default.fps
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.resetFPS()
|
||||
this.resetPadding()
|
||||
this.resetScale()
|
||||
this.#spine.play()
|
||||
}
|
||||
|
||||
#loadViewport() {
|
||||
this.#spine.updateViewport({
|
||||
padLeft: `${this.#config.padding.left}%`,
|
||||
padRight: `${this.#config.padding.right}%`,
|
||||
padTop: `${this.#config.padding.top}%`,
|
||||
padBottom: `${this.#config.padding.bottom}%`,
|
||||
})
|
||||
}
|
||||
|
||||
get usePadding() {
|
||||
return this.#config.usePadding
|
||||
}
|
||||
|
||||
set usePadding(v) {
|
||||
this.#config.usePadding = v
|
||||
}
|
||||
|
||||
set useStartAnimation(v) {
|
||||
this.#config.useStartAnimation = v
|
||||
}
|
||||
|
||||
get useStartAnimation() {
|
||||
return this.#config.useStartAnimation
|
||||
}
|
||||
|
||||
get spine() {
|
||||
return this.#spine
|
||||
}
|
||||
|
||||
set fps(v) {
|
||||
v = parseInt(v)
|
||||
this.#config.fps = v
|
||||
this.#spine.setFps(v)
|
||||
}
|
||||
|
||||
get fps() {
|
||||
return this.#config.fps
|
||||
}
|
||||
|
||||
set scale(v) {
|
||||
v = parseInt(v)
|
||||
this.#config.scale = 1 / v
|
||||
this.#spine.setOperatorScale(1 / v)
|
||||
}
|
||||
|
||||
get scale() {
|
||||
return this.#config.scale
|
||||
}
|
||||
|
||||
get node() {
|
||||
return this.#el
|
||||
}
|
||||
|
||||
get padLeft() {
|
||||
return this.padding.left
|
||||
}
|
||||
|
||||
set padLeft(v) {
|
||||
this.padding = {
|
||||
left: v,
|
||||
}
|
||||
}
|
||||
|
||||
get padRight() {
|
||||
return this.padding.right
|
||||
}
|
||||
|
||||
set padRight(v) {
|
||||
this.padding = {
|
||||
right: v,
|
||||
}
|
||||
}
|
||||
|
||||
get padTop() {
|
||||
return this.padding.top
|
||||
}
|
||||
|
||||
set padTop(v) {
|
||||
this.padding = {
|
||||
top: v,
|
||||
}
|
||||
}
|
||||
|
||||
get padBottom() {
|
||||
return this.padding.bottom
|
||||
}
|
||||
|
||||
set padBottom(v) {
|
||||
this.padding = {
|
||||
bottom: v,
|
||||
}
|
||||
}
|
||||
|
||||
get padding() {
|
||||
return this.#config.padding
|
||||
}
|
||||
|
||||
set padding(v) {
|
||||
if (!v) {
|
||||
this.resetPadding()
|
||||
return
|
||||
}
|
||||
if (typeof v !== 'object') return
|
||||
if (v.left) v.left = parseInt(v.left)
|
||||
if (v.right) v.right = parseInt(v.right)
|
||||
if (v.top) v.top = parseInt(v.top)
|
||||
if (v.bottom) v.bottom = parseInt(v.bottom)
|
||||
this.#config.padding = { ...this.#config.padding, ...v }
|
||||
this.#loadViewport()
|
||||
}
|
||||
|
||||
get backCompatibilityFns() {
|
||||
const _this = this
|
||||
return {
|
||||
spinePlayer: _this.#spine,
|
||||
setFPS: (fps) => (_this.fps = fps),
|
||||
loadViewport: _this.#loadViewport,
|
||||
setScale: (v) => (this.scale = v),
|
||||
scale: _this.scale,
|
||||
positionPadding: (key, value) => {
|
||||
switch (key) {
|
||||
case 'left':
|
||||
this.padding = {
|
||||
left: value,
|
||||
}
|
||||
break
|
||||
case 'right':
|
||||
this.padding = {
|
||||
right: value,
|
||||
}
|
||||
break
|
||||
case 'top':
|
||||
this.padding = {
|
||||
top: value,
|
||||
}
|
||||
break
|
||||
case 'bottom':
|
||||
this.padding = {
|
||||
bottom: value,
|
||||
}
|
||||
break
|
||||
default:
|
||||
this.#config.padding = value
|
||||
break
|
||||
}
|
||||
},
|
||||
positionReset: _this.resetPadding,
|
||||
scaleReset: _this.resetScale,
|
||||
useStartAnimation: _this.useStartAnimation,
|
||||
}
|
||||
}
|
||||
|
||||
get config() {
|
||||
return { ...this.#config }
|
||||
}
|
||||
|
||||
get HTML() {
|
||||
return `
|
||||
<div>
|
||||
<div>
|
||||
<label for="fps">FPS</label>
|
||||
<input type="range" min="1" max="60" value="${this.fps}" step="1" id="fps-slider"/>
|
||||
<input type="number" id="fps-input" min="1" max="60" name="fps" value="${this.fps}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="animation-select">Animation:</label>
|
||||
<select name="animation-select" id="animation-selection"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="use-start-animation">Use Start Animation</label>
|
||||
<input type="checkbox" id="use-start-animation" name="use-start-animation" checked/>
|
||||
</div>
|
||||
<button type="button" id="player-play" disabled>Play</button>
|
||||
<button type="button" id="player-pause">Pause</button>
|
||||
<div>
|
||||
<label for="scale">Scale</label>
|
||||
<input type="range" min="0.1" max="10" step="0.1" id="scale-slider" value="${this.scale}" />
|
||||
<input type="number" id="scale-input" name="scale" value="${this.scale}" step="0.1"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="position">Position</label>
|
||||
<input type="checkbox" id="position" name="position" ${this.usePadding ? 'checked' : ''}/>
|
||||
<div id="position-realted" ${this.usePadding ? '' : 'hidden'}>
|
||||
<div>
|
||||
<label for="position-padding-left">Padding Left</label>
|
||||
<input type="range" min="-100" max="100" id="position-padding-left-slider" value="${this.padding.left}" />
|
||||
<input type="number" id="position-padding-left-input" name="position-padding-left" value="${this.padding.left}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="position-padding-right">Padding Right</label>
|
||||
<input type="range" min="-100" max="100" id="position-padding-right-slider" value="${this.padding.right}" />
|
||||
<input type="number" id="position-padding-right-input" name="position-padding-right" value="${this.padding.right}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="position-padding-top">Padding Top</label>
|
||||
<input type="range" min="-100" max="100" id="position-padding-top-slider" value="${this.padding.top}" />
|
||||
<input type="number" id="position-padding-top-input" name="position-padding-top" value="${this.padding.top}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="position-padding-bottom">Padding Bottom</label>
|
||||
<input type="range" min="-100" max="100" id="position-padding-bottom-slider" value="${this.padding.bottom}" />
|
||||
<input type="number" id="position-padding-bottom-input" name="position-padding-bottom" value="${this.padding.bottom}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
get listeners() {
|
||||
return [
|
||||
{
|
||||
id: 'fps-slider',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'fps-input')
|
||||
this.fps = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'fps-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'fps-slider')
|
||||
this.fps = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'animation-selection',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
this.spine.animationState.setAnimation(
|
||||
0,
|
||||
e.currentTarget.value,
|
||||
false,
|
||||
0
|
||||
)
|
||||
this.spine.animationState.addAnimation(0, 'Idle', true, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'use-start-animation',
|
||||
event: 'click',
|
||||
handler: (e) => {
|
||||
this.useStartAnimation = e.currentTarget.checked
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'player-play',
|
||||
event: 'click',
|
||||
handler: (e) => {
|
||||
this.spine.play()
|
||||
e.currentTarget.disabled = true
|
||||
document.getElementById('player-pause').disabled = false
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'player-pause',
|
||||
event: 'click',
|
||||
handler: (e) => {
|
||||
this.spine.pause()
|
||||
e.currentTarget.disabled = true
|
||||
document.getElementById('player-play').disabled = false
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'scale-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'scale-input')
|
||||
this.scale = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'scale-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'scale-slider')
|
||||
this.scale = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'position',
|
||||
event: 'click',
|
||||
handler: (e) => {
|
||||
showRelatedHTML(e.currentTarget, 'position-realted')
|
||||
this.usePadding = e.currentTarget.checked
|
||||
if (!e.currentTarget.checked) this.resetPadding()
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'position-padding-left-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(
|
||||
e.currentTarget,
|
||||
'position-padding-left-input'
|
||||
)
|
||||
this.padding = {
|
||||
left: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'position-padding-left-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(
|
||||
e.currentTarget,
|
||||
'position-padding-left-slider'
|
||||
)
|
||||
this.padding = {
|
||||
left: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'position-padding-right-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(
|
||||
e.currentTarget,
|
||||
'position-padding-right-input'
|
||||
)
|
||||
this.padding = {
|
||||
right: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'position-padding-right-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(
|
||||
e.currentTarget,
|
||||
'position-padding-right-slider'
|
||||
)
|
||||
this.padding = {
|
||||
right: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'position-padding-top-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'position-padding-top-input')
|
||||
this.padding = {
|
||||
top: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'position-padding-top-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(
|
||||
e.currentTarget,
|
||||
'position-padding-top-slider'
|
||||
)
|
||||
this.padding = {
|
||||
top: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'position-padding-bottom-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(
|
||||
e.currentTarget,
|
||||
'position-padding-bottom-input'
|
||||
)
|
||||
this.padding = {
|
||||
bottom: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'position-padding-bottom-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(
|
||||
e.currentTarget,
|
||||
'position-padding-bottom-slider'
|
||||
)
|
||||
this.padding = {
|
||||
bottom: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
applyConfig(key, value) {
|
||||
switch (key) {
|
||||
case 'fps':
|
||||
this.fps = value
|
||||
break
|
||||
case 'scale':
|
||||
this.scale = value
|
||||
break
|
||||
case 'position':
|
||||
this.padding = value
|
||||
break
|
||||
case 'use-start-animation':
|
||||
this.useStartAnimation = value
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const Events = {
|
||||
Ready: createCustomEvent('player-ready'),
|
||||
}
|
||||
75
apps/showcase/src/components/voice.css
Normal file
75
apps/showcase/src/components/voice.css
Normal file
@@ -0,0 +1,75 @@
|
||||
@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';
|
||||
|
||||
#voice-box {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
width: 480px;
|
||||
opacity: 0;
|
||||
margin: 16px;
|
||||
font-family:
|
||||
'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans', sans-serif;
|
||||
transition: opacity 0.5s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
}
|
||||
|
||||
.voice-title {
|
||||
background-color: #9e9e9e;
|
||||
color: black;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: -8px;
|
||||
padding: 2px 8px;
|
||||
font-size: 14px;
|
||||
min-width: 120px;
|
||||
box-shadow: 0 3px 6px rgb(0 0 0 / 50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.voice-subtitle {
|
||||
background-color: rgb(0 0 0 / 65%);
|
||||
color: white;
|
||||
padding: 16px;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 6px 12px rgb(0 0 0 / 50%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.voice-triangle {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 8px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 8px;
|
||||
border-color: white transparent transparent;
|
||||
}
|
||||
|
||||
.voice-actor {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.voice-actor-icon {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
background-color: rgb(0 0 0);
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAXCAYAAAAGAx/kAAACGElEQVQ4jY2UO4sUQRSFv+6e3hkf6yqi62LiD1DRwGRFZDUTRTAVMTHzHxhosBgr+IhMzIXFRzKJYiQoCLqroYGCgiDK6K6z8+hj0Kd2yrZn8EJRza06X91761Ynkphgh4ETwBB4BrwZu1NS3WhJui1pTSNblXTLa/9oxoEWNd4W/xc0K6kzAfRd0lxV16jJdg6YBtaAB8AvoPA4D+wEZoEvsSgGJYAiXwf4EEFyYOC1qYqmFhQ2bQEO+MYKILMv1tWCikqKm4CPwKoF00DTa6pqkqiP9gIHgR3AWaDvdJIoip4jewh8BVaAz1XQOeAKcNcp9O0fAqlTLgzsApeBa8BSNbUUaAFvgVdMtnlHmsXiEHrHpw+iaOps4BRlDUASQDnw3uA9QHsCqA3sMmjFmhRJmaSmO/SmpCeSjktarunqd5IWJLUl3bCmKSlLJGWuVQbMAHcoi3oPWAAOOerXwFPgErAVuEjZGkNgkEhKDco9bwOuG/Ac+OS67ANOGngV+OYDewEUqt+gvLWmxxHgFHDapz52fV4C64Z3PW+AUsPyCJYCP4FHPuQMsNlRrHv0DRo2XP3Ccw/4Tdl8U2GTb6trYYimYNQ6Gw0pYDuwn1E7hJR3W3TMvp5hKbAM/IC/n8hR4L6hqSPJGT3QjPIfFX4nDeAC8CIGhTrBqNsz16plWN/R9KJShGzUiCBBnFS+FYmCP41gBaA/cImMsTl24NIAAAAASUVORK5CYII=');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.voice-actor-name {
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
background-color: rgb(0 0 0 / 30%);
|
||||
color: white;
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
}
|
||||
641
apps/showcase/src/components/voice.js
Normal file
641
apps/showcase/src/components/voice.js
Normal file
@@ -0,0 +1,641 @@
|
||||
import {
|
||||
insertHTMLChild,
|
||||
updateElementPosition,
|
||||
updateHTMLOptions,
|
||||
showRelatedHTML,
|
||||
syncHTMLValue,
|
||||
} from '@/components/helper'
|
||||
import '@/components/voice.css'
|
||||
import buildConfig from '!/config.json'
|
||||
|
||||
export default class Voice {
|
||||
#el = document.createElement('div')
|
||||
#parentEl
|
||||
#charwordTable
|
||||
#default = {
|
||||
region: buildConfig.voice_default_region,
|
||||
duration: {
|
||||
idle: 10 * 60 * 1000,
|
||||
next: 3 * 60 * 1000,
|
||||
},
|
||||
language: {
|
||||
voice: null,
|
||||
},
|
||||
subtitle: {
|
||||
x: 0,
|
||||
y: 100,
|
||||
},
|
||||
}
|
||||
#voice = {
|
||||
id: {
|
||||
current: null,
|
||||
last: null,
|
||||
},
|
||||
listener: {
|
||||
idle: -1,
|
||||
next: -1,
|
||||
},
|
||||
lastClickToNext: false,
|
||||
locations: null,
|
||||
list: null,
|
||||
}
|
||||
#audio = {
|
||||
id: 'voice-audio',
|
||||
el: new Audio(),
|
||||
isPlaying: false,
|
||||
}
|
||||
#config = {
|
||||
useSubtitle: false,
|
||||
useVoice: false,
|
||||
useVoiceActor: false,
|
||||
language: null,
|
||||
subtitle: {
|
||||
language: this.#default.region,
|
||||
...this.#default.subtitle,
|
||||
},
|
||||
duration: { ...this.#default.duration },
|
||||
}
|
||||
#playerObj
|
||||
|
||||
constructor(el) {
|
||||
this.#parentEl = el
|
||||
this.#el.id = 'voice-box'
|
||||
this.#el.hidden = true
|
||||
this.#el.innerHTML = `
|
||||
<audio id="${this.#audio.id}" autoplay>
|
||||
<source type="audio/ogg" />
|
||||
</audio>
|
||||
<div class="voice-wrapper" id="voice-wrapper">
|
||||
<div class="voice-title" id="voice-title"></div>
|
||||
<div class="voice-subtitle">
|
||||
<div id="voice-subtitle"></div>
|
||||
<div class="voice-triangle"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="voice-actor-box" hidden>
|
||||
<div class="voice-actor">
|
||||
<span class="voice-actor-icon"></span>
|
||||
<span id="voice-actor-name" class="voice-actor-name"></span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
insertHTMLChild(this.#parentEl, this.#el)
|
||||
}
|
||||
|
||||
async init() {
|
||||
const res = await fetch('./assets/charword_table.json')
|
||||
this.#charwordTable = await res.json()
|
||||
this.#voice.languages = Object.keys(
|
||||
this.#charwordTable.voiceLangs[this.#default.region]
|
||||
)
|
||||
this.#default.language.voice = this.#voice.languages[0]
|
||||
this.#config.language = this.#default.language.voice
|
||||
this.#voice.locations = this.#getVoiceLocations()
|
||||
this.#voice.list = Object.keys(this.#getVoices())
|
||||
}
|
||||
|
||||
success() {
|
||||
const audioEndedFunc = () => {
|
||||
this.#audio.isPlaying = false
|
||||
this.#setCurrentSubtitle(null)
|
||||
this.#audio.lastClickToNext = false
|
||||
}
|
||||
this.#audio.el.addEventListener('ended', audioEndedFunc)
|
||||
this.#playEntryVoice()
|
||||
this.#initNextVoiceTimer()
|
||||
this.#playerObj.node.addEventListener('click', () => {
|
||||
this.#audio.lastClickToNext = true
|
||||
this.#nextVoice()
|
||||
})
|
||||
document.addEventListener('mousemove', () => {
|
||||
if (this.#voice.listener.idle === -1) {
|
||||
this.#initIdleVoiceTimer()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
link(playerObj) {
|
||||
this.#playerObj = playerObj
|
||||
}
|
||||
|
||||
resetPosition() {
|
||||
this.position = { ...this.#default.subtitle }
|
||||
document.getElementById('subtitle-padding-x-slider').value =
|
||||
this.#default.subtitle.x
|
||||
document.getElementById('subtitle-padding-x-input').value =
|
||||
this.#default.subtitle.x
|
||||
document.getElementById('subtitle-padding-y-slider').value =
|
||||
this.#default.subtitle.y
|
||||
document.getElementById('subtitle-padding-y-input').value =
|
||||
this.#default.subtitle.y
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.resetPosition()
|
||||
}
|
||||
|
||||
#getVoiceLocations() {
|
||||
const folders = buildConfig.voice_folders
|
||||
const customVoiceName = this.#voice.languages.filter(
|
||||
(i) => !folders.sub.map((e) => e.lang).includes(i)
|
||||
)[0]
|
||||
folders.sub = folders.sub.map((e) => {
|
||||
return {
|
||||
name: e.name,
|
||||
lang: e.lang === 'CUSTOM' ? customVoiceName : e.lang,
|
||||
}
|
||||
})
|
||||
return folders
|
||||
}
|
||||
|
||||
#getVoices() {
|
||||
return this.#charwordTable.subtitleLangs[this.#config.subtitle.language]
|
||||
.default
|
||||
}
|
||||
|
||||
#playEntryVoice() {
|
||||
this.#playSpecialVoice('问候')
|
||||
}
|
||||
|
||||
#playSpecialVoice(matcher) {
|
||||
const voices = this.#getVoices()
|
||||
const voiceId = Object.keys(voices).find(
|
||||
(e) => voices[e].title === matcher
|
||||
)
|
||||
this.#playVoice(voiceId)
|
||||
}
|
||||
|
||||
#playVoice(id) {
|
||||
if (!this.useVoice) return
|
||||
this.#voice.id.last = this.#voice.id.current
|
||||
this.#voice.id.current = id
|
||||
this.#audio.el.src = `./assets/${this.#getVoiceLocation()}/${id}.ogg`
|
||||
let startPlayPromise = this.#audio.el.play()
|
||||
if (startPlayPromise !== undefined) {
|
||||
startPlayPromise
|
||||
.then(() => {
|
||||
this.#audio.isPlaying = true
|
||||
this.#setCurrentSubtitle(id)
|
||||
})
|
||||
.catch(() => {
|
||||
return
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#getVoiceLocation() {
|
||||
const locations = this.#voice.locations
|
||||
return `${locations.main}/${locations.sub.find((e) => e.lang === this.#config.language).name}`
|
||||
}
|
||||
|
||||
#setCurrentSubtitle(id) {
|
||||
if (id === null) {
|
||||
setTimeout(() => {
|
||||
if (this.#audio.isPlaying) return
|
||||
this.#toggleSubtitle(0)
|
||||
}, 5 * 1000)
|
||||
return
|
||||
}
|
||||
const subtitle = this.#getSubtitleById(id)
|
||||
const title = subtitle.title
|
||||
const content = subtitle.text
|
||||
const cvInfo =
|
||||
this.#charwordTable.voiceLangs[this.subtitleLanguage][
|
||||
this.#config.language
|
||||
]
|
||||
document.getElementById('voice-title').innerText = title
|
||||
document.getElementById('voice-subtitle').innerText = content
|
||||
document.getElementById('voice-actor-name').innerText = cvInfo.join('')
|
||||
if (this.#audio.isPlaying) {
|
||||
this.#toggleSubtitle(1)
|
||||
}
|
||||
}
|
||||
|
||||
#toggleSubtitle(v) {
|
||||
this.#el.style.opacity = v ? 1 : 0
|
||||
}
|
||||
|
||||
#getSubtitleById(id) {
|
||||
const obj =
|
||||
this.#charwordTable.subtitleLangs[this.#config.subtitle.language]
|
||||
let key = 'default'
|
||||
if (obj[this.#config.language]) {
|
||||
key = this.#config.language
|
||||
}
|
||||
return obj[key][id]
|
||||
}
|
||||
|
||||
#getSubtitleLanguages() {
|
||||
return Object.keys(this.#charwordTable.subtitleLangs)
|
||||
}
|
||||
|
||||
#updateSubtitlePosition() {
|
||||
updateElementPosition(this.#el, {
|
||||
x: this.position.x,
|
||||
y: this.position.y - 100,
|
||||
})
|
||||
}
|
||||
|
||||
#initNextVoiceTimer() {
|
||||
this.#voice.listener.next = setInterval(() => {
|
||||
if (!this.#voice.lastClickToNext) {
|
||||
this.#nextVoice()
|
||||
}
|
||||
}, this.#config.duration.next)
|
||||
}
|
||||
|
||||
#nextVoice() {
|
||||
const getVoiceId = () => {
|
||||
const id =
|
||||
this.#voice.list[
|
||||
Math.floor(Math.random() * this.#voice.list.length)
|
||||
]
|
||||
return id === this.#voice.id.last ? getVoiceId() : id
|
||||
}
|
||||
this.#playVoice(getVoiceId())
|
||||
}
|
||||
|
||||
#initIdleVoiceTimer() {
|
||||
this.#voice.listener.idle = setInterval(() => {
|
||||
this.#playSpecialVoice('闲置')
|
||||
clearInterval(this.#voice.listener.idle)
|
||||
this.#voice.listener.idle = -1
|
||||
}, this.#config.duration.idle)
|
||||
}
|
||||
|
||||
set useSubtitle(show) {
|
||||
this.#config.useSubtitle = show
|
||||
this.#el.hidden = !show
|
||||
}
|
||||
|
||||
get useSubtitle() {
|
||||
return this.#config.useSubtitle
|
||||
}
|
||||
|
||||
set useVoice(show) {
|
||||
this.#config.useVoice = show
|
||||
this.#playEntryVoice()
|
||||
if (!show && this.#audio.isPlaying) {
|
||||
this.#audio.el.pause()
|
||||
}
|
||||
this.#toggleSubtitle(0)
|
||||
}
|
||||
|
||||
get useVoice() {
|
||||
return this.#config.useVoice
|
||||
}
|
||||
|
||||
set useVoiceActor(show) {
|
||||
this.#config.useVoiceActor = show
|
||||
document.getElementById('voice-actor-box').hidden = !show
|
||||
}
|
||||
|
||||
get useVoiceActor() {
|
||||
return this.#config.useVoiceActor
|
||||
}
|
||||
|
||||
set subtitleLanguage(lang) {
|
||||
if (this.#getSubtitleLanguages().includes(lang)) {
|
||||
this.#config.subtitle.language = lang
|
||||
} else {
|
||||
this.#config.subtitle.language = this.#default.region
|
||||
}
|
||||
this.#setCurrentSubtitle(this.#voice.id.current)
|
||||
}
|
||||
|
||||
get subtitleLanguage() {
|
||||
return this.#config.subtitle.language
|
||||
}
|
||||
|
||||
get subtitleLanguages() {
|
||||
return this.#getSubtitleLanguages()
|
||||
}
|
||||
|
||||
get x() {
|
||||
return this.position.x
|
||||
}
|
||||
|
||||
set x(v) {
|
||||
this.position = {
|
||||
x: v,
|
||||
}
|
||||
}
|
||||
|
||||
get y() {
|
||||
return this.position.y
|
||||
}
|
||||
|
||||
set y(v) {
|
||||
this.position = {
|
||||
y: v,
|
||||
}
|
||||
}
|
||||
|
||||
get position() {
|
||||
return {
|
||||
x: this.#config.subtitle.x,
|
||||
y: this.#config.subtitle.y,
|
||||
}
|
||||
}
|
||||
|
||||
set position(v) {
|
||||
if (typeof v !== 'object') return
|
||||
if (v.x) v.x = parseInt(v.x)
|
||||
if (v.y) v.y = parseInt(v.y)
|
||||
this.#config.subtitle = { ...this.#config.subtitle, ...v }
|
||||
console.log(v)
|
||||
this.#updateSubtitlePosition()
|
||||
}
|
||||
|
||||
set language(lang) {
|
||||
if (this.#voice.languages.includes(lang)) {
|
||||
this.#config.language = lang
|
||||
} else {
|
||||
this.#config.language = this.#default.language.voice
|
||||
}
|
||||
const availableSubtitleLang = this.#getSubtitleLanguages()
|
||||
if (!availableSubtitleLang.includes(this.#config.subtitle.language)) {
|
||||
this.#config.subtitle.language = availableSubtitleLang[0]
|
||||
}
|
||||
}
|
||||
|
||||
get language() {
|
||||
return this.#config.language
|
||||
}
|
||||
|
||||
get languages() {
|
||||
return this.#voice.languages
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return {
|
||||
idle: this.#config.duration.idle / 60 / 1000,
|
||||
next: this.#config.duration.next / 60 / 1000,
|
||||
}
|
||||
}
|
||||
|
||||
set duration(v) {
|
||||
if (typeof v !== 'object') return
|
||||
if (v.idle) {
|
||||
clearInterval(this.#voice.listener.idle)
|
||||
if (v.idle !== 0) {
|
||||
this.#config.duration.idle = parseInt(v.idle) * 60 * 1000
|
||||
this.#initIdleVoiceTimer()
|
||||
}
|
||||
}
|
||||
if (v.next) {
|
||||
clearInterval(this.#voice.listener.next)
|
||||
if (v.next !== 0) {
|
||||
this.#config.duration.next = parseInt(v.next) * 60 * 1000
|
||||
this.#initNextVoiceTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get durationIdle() {
|
||||
return this.duration.idle
|
||||
}
|
||||
|
||||
set durationIdle(duration) {
|
||||
this.duration = {
|
||||
idle: duration,
|
||||
}
|
||||
}
|
||||
|
||||
set durationNext(duration) {
|
||||
this.duration = {
|
||||
next: duration,
|
||||
}
|
||||
}
|
||||
|
||||
get durationNext() {
|
||||
return this.duration.next
|
||||
}
|
||||
|
||||
set subtitleX(x) {
|
||||
// Note: Back Compatibility
|
||||
this.position = {
|
||||
x,
|
||||
}
|
||||
}
|
||||
|
||||
get subtitleX() {
|
||||
// Note: Back Compatibility
|
||||
return this.position.x
|
||||
}
|
||||
|
||||
set subtitleY(y) {
|
||||
// Note: Back Compatibility
|
||||
this.position = {
|
||||
y,
|
||||
}
|
||||
}
|
||||
|
||||
get subtitleY() {
|
||||
// Note: Back Compatibility
|
||||
return this.position.y
|
||||
}
|
||||
|
||||
set idleDuration(duration) {
|
||||
// Note: Back Compatibility
|
||||
this.duration = {
|
||||
idle: duration,
|
||||
}
|
||||
}
|
||||
|
||||
get idleDuration() {
|
||||
// Note: Back Compatibility
|
||||
return this.duration.idle
|
||||
}
|
||||
|
||||
set nextDuration(duration) {
|
||||
// Note: Back Compatibility
|
||||
this.duration.next = duration
|
||||
}
|
||||
|
||||
get nextDuration() {
|
||||
// Note: Back Compatibility
|
||||
return this.duration.next
|
||||
}
|
||||
|
||||
get config() {
|
||||
return { ...this.#config }
|
||||
}
|
||||
|
||||
get HTML() {
|
||||
return `
|
||||
<div>
|
||||
<label for="voice">Voice</label>
|
||||
<input type="checkbox" id="voice" name="voice" ${this.useVoice ? 'checked' : ''}/>
|
||||
<div id="voice-realted" ${this.useVoice ? '' : 'hidden'}>
|
||||
<div>
|
||||
<label for="voice-lang-select">Choose the language of voice:</label>
|
||||
<select name="voice-lang" id="voice-lang-select">
|
||||
${updateHTMLOptions(this.languages)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="voice-idle-duration">Idle Duration (min)</label>
|
||||
<input type="number" id="voice-idle-duration-input" min="0" name="voice-idle-duration" value="${this.duration.idle}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="voice-next-duration">Next Duration (min)</label>
|
||||
<input type="number" id="voice-next-duration-input" name="voice-next-duration" value="${this.duration.next}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="subtitle">Subtitle</label>
|
||||
<input type="checkbox" id="subtitle" name="subtitle" ${this.useSubtitle ? 'checked' : ''}/>
|
||||
<div id="subtitle-realted" ${this.useSubtitle ? '' : 'hidden'}>
|
||||
<div>
|
||||
<label for="subtitle-lang-select">Choose the language of subtitle:</label>
|
||||
<select name="subtitle-lang" id="subtitle-lang-select">
|
||||
${updateHTMLOptions(this.subtitleLanguages)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="subtitle-padding-x">Subtitle X Position</label>
|
||||
<input type="range" min="0" max="100" id="subtitle-padding-x-slider" value="${this.position.x}" />
|
||||
<input type="number" id="subtitle-padding-x-input" name="subtitle-padding-x" value="${this.position.x}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="subtitle-padding-y">Subtitle Y Position</label>
|
||||
<input type="range" min="0" max="100" id="subtitle-padding-y-slider" value="${this.position.y}" />
|
||||
<input type="number" id="subtitle-padding-y-input" name="subtitle-padding-y" value="${this.position.y}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="voice-actor">Voice Actor</label>
|
||||
<input type="checkbox" id="voice-actor" name="voice-actor" ${this.useVoiceActor ? 'checked' : ''}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
get listeners() {
|
||||
return [
|
||||
{
|
||||
id: 'voice',
|
||||
event: 'click',
|
||||
handler: (e) => {
|
||||
showRelatedHTML(e.currentTarget, 'voice-realted')
|
||||
this.useVoice = e.currentTarget.checked
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'voice-lang-select',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
this.language = e.currentTarget.value
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'voice-idle-duration-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
this.duration = {
|
||||
idle: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'voice-next-duration-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
this.duration = {
|
||||
next: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'subtitle',
|
||||
event: 'click',
|
||||
handler: (e) => {
|
||||
showRelatedHTML(e.currentTarget, 'subtitle-realted')
|
||||
this.useSubtitle = e.currentTarget.checked
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'subtitle-lang-select',
|
||||
event: 'change',
|
||||
handler: (e) => (this.subtitleLanguage = e.currentTarget.value),
|
||||
},
|
||||
{
|
||||
id: 'subtitle-padding-x-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'subtitle-padding-x-input')
|
||||
this.position = {
|
||||
x: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'subtitle-padding-x-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'subtitle-padding-x-slider')
|
||||
this.position = {
|
||||
x: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'subtitle-padding-y-slider',
|
||||
event: 'input',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'subtitle-padding-y-input')
|
||||
this.position = {
|
||||
y: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'subtitle-padding-y-input',
|
||||
event: 'change',
|
||||
handler: (e) => {
|
||||
syncHTMLValue(e.currentTarget, 'subtitle-padding-y-slider')
|
||||
this.position = {
|
||||
y: e.currentTarget.value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'voice-actor',
|
||||
event: 'click',
|
||||
handler: (e) => {
|
||||
this.useVoiceActor = e.currentTarget.checked
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
applyConfig(key, value) {
|
||||
switch (key) {
|
||||
case 'use-voice':
|
||||
this.useVoice = value
|
||||
break
|
||||
case 'language':
|
||||
this.language = value
|
||||
break
|
||||
case 'duration':
|
||||
this.duration = value
|
||||
break
|
||||
case 'use-subtitle':
|
||||
this.useSubtitle = value
|
||||
break
|
||||
case 'subtitle-language':
|
||||
this.subtitleLanguage = value
|
||||
break
|
||||
case 'subtitle-position':
|
||||
this.position = value
|
||||
break
|
||||
case 'use-voice-actor':
|
||||
this.useVoiceActor = value
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
376
apps/showcase/src/components/wallpaper_engine.js
Normal file
376
apps/showcase/src/components/wallpaper_engine.js
Normal file
@@ -0,0 +1,376 @@
|
||||
import Events from '@/components/events'
|
||||
|
||||
window.wallpaperPropertyListener = {
|
||||
applyGeneralProperties: function (properties) {
|
||||
if (properties.fps) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'player',
|
||||
key: 'fps',
|
||||
value: properties.fps,
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
applyUserProperties: function (properties) {
|
||||
console.log(properties)
|
||||
if (properties.privacydonottrack) {
|
||||
document.dispatchEvent(
|
||||
Events.Insight.Register.handler(
|
||||
!properties.privacydonottrack.value
|
||||
)
|
||||
)
|
||||
}
|
||||
if (properties.logo) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'logo',
|
||||
key: 'hidden',
|
||||
value: !properties.logo.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.logoratio) {
|
||||
if (properties.logoratio.value) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'logo',
|
||||
key: 'ratio',
|
||||
value: properties.logoratio.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
if (properties.logoopacity) {
|
||||
if (properties.logoopacity.value) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'logo',
|
||||
key: 'opacity',
|
||||
value: properties.logoopacity.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
if (properties.logoimage) {
|
||||
if (properties.logoimage.value) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'logo',
|
||||
key: 'image',
|
||||
value: 'file:///' + properties.logoimage.value,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'logo',
|
||||
key: 'image',
|
||||
value: null,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
if (properties.logox) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'logo',
|
||||
key: 'position',
|
||||
value: {
|
||||
x: properties.logox.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.logoy) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'logo',
|
||||
key: 'position',
|
||||
value: {
|
||||
y: properties.logoy.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.defaultbackground) {
|
||||
if (properties.defaultbackground.value) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'background',
|
||||
key: 'default',
|
||||
value: properties.defaultbackground.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
if (properties.background) {
|
||||
if (properties.background.value) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'background',
|
||||
key: 'custom',
|
||||
value: `file:///${properties.background.value}`,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'background',
|
||||
key: 'custom',
|
||||
value: null,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
if (properties.voicetitle) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'voice',
|
||||
key: 'use-voice',
|
||||
value: properties.voicetitle.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.voicelanguage) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'voice',
|
||||
key: 'language',
|
||||
value: properties.voicelanguage.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.voiceidle) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'voice',
|
||||
key: 'duration',
|
||||
value: {
|
||||
idle: properties.voiceidle.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.voicenext) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'voice',
|
||||
key: 'duration',
|
||||
value: {
|
||||
next: properties.voicenext.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.voicesubtitle) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'voice',
|
||||
key: 'use-subtitle',
|
||||
value: properties.voicesubtitle.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.voicesubtitlelanguage) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'voice',
|
||||
key: 'subtitle-language',
|
||||
value: properties.voicesubtitlelanguage.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.voicesubtitlex) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'voice',
|
||||
key: 'subtitle-position',
|
||||
value: {
|
||||
x: properties.voicesubtitlex.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.voicesubtitley) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'voice',
|
||||
key: 'subtitle-position',
|
||||
value: {
|
||||
y: properties.voicesubtitley.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.voiceactor) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'voice',
|
||||
key: 'use-voice-actor',
|
||||
value: properties.voiceactor.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.music_selection) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'music',
|
||||
key: 'music',
|
||||
value: properties.music_selection.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.music_title) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'music',
|
||||
key: 'use-music',
|
||||
value: properties.music_title.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.music_volume) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'music',
|
||||
key: 'volume',
|
||||
value: properties.music_volume.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.custom_music) {
|
||||
if (properties.custom_music.value) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'music',
|
||||
key: 'custom',
|
||||
value: `file:///${properties.custom_music.value}`,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'music',
|
||||
key: 'custom',
|
||||
value: null,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
if (properties.custom_video) {
|
||||
if (properties.custom_video.value) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'background',
|
||||
key: 'video',
|
||||
value: `file:///${properties.custom_video.value}`,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'background',
|
||||
key: 'video',
|
||||
value: null,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
if (properties.video_volume) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'background',
|
||||
key: 'volume',
|
||||
value: properties.video_volume.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.scale) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'player',
|
||||
key: 'scale',
|
||||
value: properties.scale.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.position) {
|
||||
if (!properties.position.value) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'player',
|
||||
key: 'position',
|
||||
value: null,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'player',
|
||||
key: 'position',
|
||||
value: {
|
||||
left: properties.paddingleft.value,
|
||||
right: properties.paddingright.value,
|
||||
top: properties.paddingtop.value,
|
||||
bottom: properties.paddingbottom.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
if (properties.paddingleft) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'player',
|
||||
key: 'position',
|
||||
value: {
|
||||
left: properties.paddingleft.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.paddingright) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'player',
|
||||
key: 'position',
|
||||
value: {
|
||||
right: properties.paddingright.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.paddingtop) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'player',
|
||||
key: 'position',
|
||||
value: {
|
||||
top: properties.paddingtop.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.paddingbottom) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'player',
|
||||
key: 'position',
|
||||
value: {
|
||||
bottom: properties.paddingbottom.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
if (properties.useStartAnimation) {
|
||||
document.dispatchEvent(
|
||||
Events.RegisterConfig.handler({
|
||||
target: 'player',
|
||||
key: 'use-start-animation',
|
||||
value: properties.useStartAnimation.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
22
apps/showcase/src/index.css
Normal file
22
apps/showcase/src/index.css
Normal file
@@ -0,0 +1,22 @@
|
||||
html {
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body,
|
||||
#app {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
#widget-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
6
apps/showcase/src/index.js
Normal file
6
apps/showcase/src/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import '@/index.css'
|
||||
import '@/components/wallpaper_engine'
|
||||
import AKLive2D from '@/components/aklive2d'
|
||||
;(() => {
|
||||
window.aklive2d = new AKLive2D(document.getElementById('app'))
|
||||
})()
|
||||
5
apps/showcase/stylelint.config.js
Normal file
5
apps/showcase/stylelint.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import baseConfig from '@aklive2d/stylelint-config'
|
||||
/** @type {import('stylelint').Config} */
|
||||
export default {
|
||||
...baseConfig,
|
||||
}
|
||||
57
apps/showcase/vite.config.js
Normal file
57
apps/showcase/vite.config.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import path from 'node:path'
|
||||
import { has } from '@aklive2d/operator'
|
||||
import { envParser, file } from '@aklive2d/libs'
|
||||
import { copyShowcaseData } from '@aklive2d/vite-helpers'
|
||||
import * as dirs from './index.js'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(({ command, isPreview }) => {
|
||||
let newOutDir = dirs.OUT_DIR
|
||||
if (command === 'serve') {
|
||||
const { name } = envParser.parse({
|
||||
name: {
|
||||
type: 'string',
|
||||
short: 'n',
|
||||
},
|
||||
})
|
||||
if (!name) {
|
||||
throw new Error('Please set the operator name.')
|
||||
}
|
||||
if (!has(name)) {
|
||||
throw new Error(`Invalid operator name: ${name}`)
|
||||
}
|
||||
if (isPreview) {
|
||||
newOutDir = path.join(dirs.DIST_DIR, name)
|
||||
} else {
|
||||
file.rm(dirs.DATA_DIR)
|
||||
copyShowcaseData(name, {
|
||||
dataDir: dirs.DATA_DIR,
|
||||
publicAssetsDir: dirs.PUBLIC_ASSETS_DIR,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
base: '',
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve('./src'),
|
||||
'!': dirs.DATA_DIR,
|
||||
},
|
||||
},
|
||||
envDir: dirs.DATA_DIR,
|
||||
publicDir: dirs.PUBLIC_DIR,
|
||||
build: {
|
||||
chunkSizeWarningLimit: 10000,
|
||||
outDir: newOutDir,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `assets/[name].js`,
|
||||
chunkFileNames: `assets/[name].js`,
|
||||
assetFileNames: `assets/[name].[ext]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user