feat(directory): init operator page

This commit is contained in:
Haoyu Xu
2023-02-28 14:20:28 -05:00
parent 7edc231e89
commit e4648945d1
18 changed files with 172 additions and 83 deletions

View File

@@ -1 +1 @@
3.3.11 3.3.13

View File

@@ -37,7 +37,7 @@ async function main() {
for (const [key, _] of Object.entries(__config.operators)) { for (const [key, _] of Object.entries(__config.operators)) {
OPERATOR_NAMES.push(key) OPERATOR_NAMES.push(key)
} }
increase(__projetRoot) __config.version.showcase = increase(__projetRoot)
break break
case 'preview': case 'preview':
assert(OPERATOR_NAMES.length !== 0, 'Please set the operator name.') assert(OPERATOR_NAMES.length !== 0, 'Please set the operator name.')

View File

@@ -1,3 +1,4 @@
VITE_APP_TITLE=AKLive2D VITE_APP_TITLE=AKLive2D
VITE_VERSION=0.5.7 VITE_VERSION=0.5.9
VITE_APP_VOICE_URL=jp/CN_037.ogg VITE_APP_VOICE_URL=jp/CN_037.ogg
VITE_SHOWCASE_VERSION=3.3.13

View File

@@ -1 +1 @@
0.5.8 0.5.9

View File

@@ -32,6 +32,7 @@ const router = createBrowserRouter(
index={route.index} index={route.index}
path={route.path} path={route.path}
element={route.element} element={route.element}
loader={route.loader}
/> />
) )
})} })}

View File

@@ -25,7 +25,7 @@
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
flex-wrap: nowrap; flex-wrap: nowrap;
width: 480px; max-width: 480px;
height: fit-content; height: fit-content;
margin: 0 auto; margin: 0 auto;
background-color: var(--root-background-color); background-color: var(--root-background-color);
@@ -55,6 +55,20 @@
padding: 1rem 1rem 0 1rem; padding: 1rem 1rem 0 1rem;
} }
@media (max-width: 768px) {
.popup .title {
font-size: 2rem;
}
.popup .content {
font-size: 1rem;
}
.popup .return-button {
transform: scale(0.8);
}
}
.popup .overlay { .popup .overlay {
position: absolute; position: absolute;
left: 0; left: 0;

View File

@@ -1,18 +1,29 @@
import { createContext, useState, useEffect } from "react" import { createContext, useState, useEffect } from "react"
import db from "@/db"
export const ConfigContext = createContext() export const ConfigContext = createContext()
export function ConfigProvider(props) { export function ConfigProvider(props) {
const [config, setConfig] = useState([]) const [config, setConfig] = useState([])
const [operators, setOperators] = useState([])
useEffect(() => { useEffect(() => {
fetch("/_assets/directory.json").then(res => res.json()).then(data => { fetch("/_assets/directory.json").then(res => res.json()).then(data => {
setConfig(data) setConfig(data)
db.config.put({ key: "config", value: data })
let operatorsList = []
data.operators.forEach((item) => {
operatorsList = [...operatorsList, ...item]
})
setOperators(operatorsList)
})
fetch("/_assets/version.json").then(res => res.json()).then(data => {
db.config.put({ key: "version", value: data })
}) })
}, []) }, [])
return ( return (
<ConfigContext.Provider value={{ config }}> <ConfigContext.Provider value={{ config, operators }}>
{props.children} {props.children}
</ConfigContext.Provider> </ConfigContext.Provider>
) )

View File

@@ -15,15 +15,12 @@ export function HeaderProvider(props) {
language, language,
i18n i18n
} = useContext(LanguageContext) } = useContext(LanguageContext)
const [tabs, setTabs] = useState(null) const [tabs, setTabs] = useState([])
const [currentTab, setCurrentTab] = useState([]) const [currentTab, setCurrentTab] = useState([])
const [appbarExtraArea, setAppbarExtraArea] = useState([]) const [appbarExtraArea, setAppbarExtraArea] = useState([])
useEffect(() => { useEffect(() => {
let newTitle = key const newTitle = i18n(key)
if (i18n.key[newTitle]) {
newTitle = i18n.key[newTitle][language]
}
document.title = `${newTitle} - ${import.meta.env.VITE_APP_TITLE}`; document.title = `${newTitle} - ${import.meta.env.VITE_APP_TITLE}`;
setRealTitle(newTitle) setRealTitle(newTitle)
}, [key, language]) }, [key, language])

View File

@@ -1,24 +1,31 @@
import { createContext, useState, useEffect } from "react" import { createContext, useState, useEffect, useCallback } from "react"
import i18n from '@/i18n' import i18nValues from '@/i18n'
export const LanguageContext = createContext() export const LanguageContext = createContext()
export function LanguageProvider(props) { export function LanguageProvider(props) {
const textDefaultLang = "en-US" const textDefaultLang = "en-US"
const [language, setLanguage] = useState(i18n.available.includes(navigator.language) ? navigator.language : "en-US") // language will be default to en-US if not available const [language, setLanguage] = useState(i18nValues.available.includes(navigator.language) ? navigator.language : "en-US") // language will be default to en-US if not available
const [alternateLang, setAlternateLang] = useState(null) // drawerAlternateLang will be default to zh-CN if language is en-* const [alternateLang, setAlternateLang] = useState(null) // drawerAlternateLang will be default to zh-CN if language is en-*
useEffect(() => { useEffect(() => {
setAlternateLang(language.startsWith("en") ? "zh-CN" : language) setAlternateLang(language.startsWith("en") ? "zh-CN" : language)
}, [language]) }, [language])
const i18n = useCallback((key, preferredLanguage = language) => {
if (i18nValues.key[key]) {
return i18nValues.key[key][preferredLanguage]
}
return key
}, [language])
return ( return (
<LanguageContext.Provider <LanguageContext.Provider
value={{ value={{
language, setLanguage, language, setLanguage,
textDefaultLang, textDefaultLang,
alternateLang, alternateLang,
i18n i18n, i18nValues
}} }}
> >
{props.children} {props.children}

View File

@@ -4,6 +4,7 @@ const db = new Dexie('aklive2dDatabase');
db.version(2).stores({ db.version(2).stores({
image: '++key, blob', image: '++key, blob',
voice: '++key, blob', voice: '++key, blob',
config: '++key, value',
}); });
export default db; export default db;

View File

@@ -39,12 +39,12 @@
"zh-CN": "综合", "zh-CN": "综合",
"en-US": "All" "en-US": "All"
}, },
"elite2": { "operator": {
"zh-CN": "干员晋升", "zh-CN": "精英2",
"en-US": "Elite 2" "en-US": "Elite 2"
}, },
"skin": { "skin": {
"zh-CN": "干员时装", "zh-CN": "时装",
"en-US": "Skin" "en-US": "Skin"
}, },
"voice": { "voice": {

View File

@@ -5,7 +5,7 @@ import {
useCallback useCallback
} from 'react' } from 'react'
import { import {
Link, NavLink,
} from "react-router-dom"; } from "react-router-dom";
import './home.css' import './home.css'
import { ConfigContext } from '@/context/useConfigContext'; import { ConfigContext } from '@/context/useConfigContext';
@@ -24,7 +24,7 @@ export default function Home() {
const { const {
setTitle, setTitle,
setTabs, setTabs,
currentTab, setCurrentTab, currentTab,
setAppbarExtraArea setAppbarExtraArea
} = useContext(HeaderContext) } = useContext(HeaderContext)
const { config } = useContext(ConfigContext) const { config } = useContext(ConfigContext)
@@ -39,19 +39,7 @@ export default function Home() {
useEffect(() => { useEffect(() => {
setTitle('dynamic_compile') setTitle('dynamic_compile')
setTabs([ setTabs(['all', 'operator', 'skin'])
{
key: 'all',
text: i18n.key.all
}, {
key: 'operator',
text: i18n.key.elite2
}, {
key: 'skin',
text: i18n.key.skin
}
])
setCurrentTab('all')
}, []) }, [])
useEffect(() => { useEffect(() => {
@@ -67,7 +55,7 @@ export default function Home() {
( (
<Switch <Switch
key="voice" key="voice"
text={i18n.key.voice[language]} text={i18n('voice')}
on={voiceOn} on={voiceOn}
handleOnClick={() => toggleVoice()} handleOnClick={() => toggleVoice()}
/> />
@@ -122,9 +110,8 @@ export default function Home() {
<section className="item-group"> <section className="item-group">
{v.map(item => { {v.map(item => {
return ( return (
<Link <NavLink
reloadDocument to={`/operator/${item.link}`}
to={`/${item.link}`}
className="item" className="item"
key={item.link} key={item.link}
hidden={!isShown(item.type)} hidden={!isShown(item.type)}
@@ -156,7 +143,7 @@ export default function Home() {
color: item.color color: item.color
}} /> }} />
</section> </section>
</Link> </NavLink>
) )
})} })}
<section className='item-group-date'>{v[0].date}</section> <section className='item-group-date'>{v[0].date}</section>

View File

@@ -3,18 +3,52 @@ import {
useEffect, useEffect,
useContext useContext
} from 'react' } from 'react'
import {
useParams,
} from "react-router-dom";
import './operator.css' import './operator.css'
import { ConfigContext } from '@/context/useConfigContext';
import { LanguageContext } from '@/context/useLanguageContext';
import { HeaderContext } from '@/context/useHeaderContext'; import { HeaderContext } from '@/context/useHeaderContext';
import useUmami from '@parcellab/react-use-umami' import useUmami from '@parcellab/react-use-umami'
export default function Operator(props) { export default function Operator(props) {
const _trackEvt = useUmami('/operator/:key') const _trackEvt = useUmami('/operator/:key')
const { setTitle } = useContext(HeaderContext) const { operators } = useContext(ConfigContext)
const {
language,
i18n
} = useContext(LanguageContext)
const { key } = useParams();
const {
setTitle,
setTabs,
setAppbarExtraArea
} = useContext(HeaderContext)
const [config, setConfig] = useState(null)
const [spineData, setSpineData] = useState({})
useEffect(() => { useEffect(() => {
setTitle('chen') setTabs([])
setAppbarExtraArea([])
}, []) }, [])
useEffect(() => {
const config = operators.find((item) => item.link === key)
if (config) {
setConfig(config)
fetch(`/_assets/${config.filename.replace("#", "%23")}.json`).then(res => res.json()).then(data => {
setSpineData(data)
})
}
}, [operators])
useEffect(() => {
if (config) {
setTitle(config.codename[language])
}
}, [config, language])
return ( return (
<section> <section>
<section> <section>

View File

@@ -191,7 +191,6 @@
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
padding: 1rem 0; padding: 1rem 0;
display: flex; display: flex;
align-content: center;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-wrap: nowrap; flex-wrap: nowrap;
@@ -200,12 +199,22 @@
.footer .links { .footer .links {
flex-direction: row; flex-direction: row;
gap: 1rem; height: 2rem;
} }
.footer .links .separator { .footer .links .item {
height: 1rem; padding: 0 1rem;
border-left: 2px solid var(--border-color); border-left: 2px solid var(--border-color);
height: inherit;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.footer .links .item:first-of-type {
border-left: none;
} }
.footer .copyright { .footer .copyright {

View File

@@ -8,6 +8,7 @@ import {
Link, Link,
NavLink, NavLink,
useNavigate, useNavigate,
ScrollRestoration
} from "react-router-dom"; } from "react-router-dom";
import './root.css' import './root.css'
import routes from '@/routes' import routes from '@/routes'
@@ -25,7 +26,7 @@ export default function Root(props) {
language, setLanguage, language, setLanguage,
textDefaultLang, textDefaultLang,
alternateLang, alternateLang,
i18n i18n, i18nValues
} = useContext(LanguageContext) } = useContext(LanguageContext)
const { const {
title, title,
@@ -41,21 +42,21 @@ export default function Root(props) {
setHeaderTabs(tabs?.map((item) => { setHeaderTabs(tabs?.map((item) => {
return ( return (
<section <section
key={item.key} key={item}
className={`main-tab-item ${currentTab === item.key ? 'active' : ''}`} className={`main-tab-item ${currentTab === item ? 'active' : ''}`}
onClick={(e) => { onClick={(e) => {
setCurrentTab(item.key) setCurrentTab(item)
item.onClick && item.onClick(e, item.key) item.onClick && item.onClick(e, item)
}} }}
> >
{item.text[language]} {i18n(item)}
</section> </section>
) )
})) }))
} }
const toggleDrawer = () => { const toggleDrawer = (value) => {
setDrawerHidden(!drawerHidden) setDrawerHidden(value || !drawerHidden)
} }
const renderDrawerDestinations = () => { const renderDrawerDestinations = () => {
@@ -67,13 +68,13 @@ export default function Root(props) {
to={item.path} to={item.path}
target="_blank" target="_blank"
className="link" className="link"
onClick={() => toggleDrawer()} onClick={() => toggleDrawer(false)}
> >
<section> <section>
{i18n.key[item.name][textDefaultLang]} {i18n(item.name, textDefaultLang)}
</section> </section>
<section> <section>
{i18n.key[item.name][alternateLang]} {i18n(item.name, alternateLang)}
</section> </section>
</Link> </Link>
) )
@@ -83,13 +84,13 @@ export default function Root(props) {
to={item.path} to={item.path}
key={item.name} key={item.name}
className="link" className="link"
onClick={() => toggleDrawer()} onClick={() => toggleDrawer(false)}
> >
<section> <section>
{i18n.key[item.name][textDefaultLang]} {i18n(item.name, textDefaultLang)}
</section> </section>
<section> <section>
{i18n.key[item.name][alternateLang]} {i18n(item.name, alternateLang)}
</section> </section>
</NavLink> </NavLink>
) )
@@ -105,6 +106,14 @@ export default function Root(props) {
renderHeaderTabs(tabs) renderHeaderTabs(tabs)
}, [tabs, currentTab, language]) }, [tabs, currentTab, language])
useEffect(() => {
if (tabs.length > 0) {
setCurrentTab(tabs[0])
} else {
setCurrentTab(null)
}
}, [tabs])
return ( return (
<> <>
<header className='header'> <header className='header'>
@@ -119,10 +128,10 @@ export default function Root(props) {
<section className='spacer' /> <section className='spacer' />
{appbarExtraArea} {appbarExtraArea}
<Dropdown <Dropdown
text={i18n.key[language][language]} text={i18n(language)}
menu={i18n.available.map((item) => { menu={i18nValues.available.map((item) => {
return { return {
name: i18n.key[item][language], name: i18n(item),
value: item value: item
} }
})} })}
@@ -161,24 +170,29 @@ export default function Root(props) {
</main> </main>
<footer className='footer'> <footer className='footer'>
<section className='links section'> <section className='links section'>
<section className="item">
<Popup <Popup
className='item' className='link'
title={i18n.key.disclaimer[language]} title={i18n('disclaimer')}
> >
{i18n.key.disclaimer_content[language]} {i18n('disclaimer_content')}
</Popup> </Popup>
<span className='separator' /> </section>
<Link reloadDocument to="https://privacy.halyul.dev" target="_blank" className='item'>{i18n.key.privacy_policy[language]}</Link> <section className="item">
<span className='separator' /> <Link reloadDocument to="https://privacy.halyul.dev" target="_blank" className='link'>{i18n('privacy_policy')}</Link>
<Link reloadDocument to="https://github.com/Halyul/aklive2d" target="_blank" className='item'>Github</Link> </section>
<span className='separator'/> <section className="item">
<Link reloadDocument to="https://github.com/Halyul/aklive2d" target="_blank" className='link'>GitHub</Link>
</section>
<section className="item">
<Popup <Popup
className='item' className='link'
title={i18n.key.contact_us[language]} title={i18n('contact_us')}
> >
ak#halyul.dev ak#halyul.dev
</Popup> </Popup>
</section> </section>
</section>
<section className='copyright section'> <section className='copyright section'>
<span>Spine Runtimes © 2013 - 2019 Esoteric Software LLC</span> <span>Spine Runtimes © 2013 - 2019 Esoteric Software LLC</span>
<span>Assets © 2017 - {currentYear} Arknights/Hypergryph Co., Ltd</span> <span>Assets © 2017 - {currentYear} Arknights/Hypergryph Co., Ltd</span>
@@ -186,6 +200,12 @@ export default function Root(props) {
<span>Version: {import.meta.env.VITE_VERSION}</span> <span>Version: {import.meta.env.VITE_VERSION}</span>
</section> </section>
</footer> </footer>
<ScrollRestoration
getKey={(location, matches) => {
// default behavior
return location.pathname;
}}
/>
</> </>
) )
} }

View File

@@ -25,7 +25,9 @@ export default function () {
}, {})) }, {}))
.sort((a, b) => Date.parse(b[0].date) - Date.parse(a[0].date)), .sort((a, b) => Date.parse(b[0].date) - Date.parse(a[0].date)),
} }
const versionJson = __config.version
writeSync(JSON.stringify(directoryJson, null), path.join(targetFolder, "directory.json")) writeSync(JSON.stringify(directoryJson, null), path.join(targetFolder, "directory.json"))
writeSync(JSON.stringify(versionJson, null), path.join(targetFolder, "version.json"))
filesToCopy.forEach((key) => { filesToCopy.forEach((key) => {
copy(path.join(sourceFolder, key, 'assets.json'), path.join(targetFolder, `${__config.operators[key].filename}.json`)) copy(path.join(sourceFolder, key, 'assets.json'), path.join(targetFolder, `${__config.operators[key].filename}.json`))
}) })

View File

@@ -9,5 +9,7 @@ export function increase(dir) {
// release version will be lagged by 0.0.1 // release version will be lagged by 0.0.1
const version = read(dir) const version = read(dir)
const [major, minor, patch] = version.split('.') const [major, minor, patch] = version.split('.')
writeSync(`${major}.${minor}.${+patch + 1}`, path.join(dir, 'Version')) const newVersion = `${major}.${minor}.${+patch + 1}`
writeSync(newVersion, path.join(dir, 'Version'))
return newVersion
} }

View File

@@ -63,7 +63,7 @@ class ViteRunner {
this.#dev(viteConfig) this.#dev(viteConfig)
break break
case 'build': case 'build':
increase(configObj.versionDir) this.#globalConfig.version.showcase = increase(configObj.versionDir)
case 'build-all': case 'build-all':
this.#build(viteConfig) this.#build(viteConfig)
break break
@@ -133,7 +133,7 @@ class ViteRunner {
get #directoryConfig() { get #directoryConfig() {
if (process.env.npm_lifecycle_event === 'vite:directory:build') { if (process.env.npm_lifecycle_event === 'vite:directory:build') {
increase(path.join(__projetRoot, "directory")) this.#globalConfig.version.directory = increase(path.join(__projetRoot, "directory"))
} }
const directoryDir = path.resolve(__projetRoot, 'directory') const directoryDir = path.resolve(__projetRoot, 'directory')
writeSync((new EnvGenerator()).generate([ writeSync((new EnvGenerator()).generate([
@@ -146,6 +146,9 @@ class ViteRunner {
}, { }, {
key: "app_voice_url", key: "app_voice_url",
value: this.#globalConfig.directory.voice value: this.#globalConfig.directory.voice
}, {
key: "showcase_version",
value: this.#globalConfig.version.showcase
} }
]), path.join(directoryDir, '.env')) ]), path.join(directoryDir, '.env'))
this.#mode = process.argv[3] this.#mode = process.argv[3]