feat(directory): add changelogs page

This commit is contained in:
Haoyu Xu
2023-03-01 16:22:35 -05:00
parent 3cc0f48648
commit 1c8356bbbb
29 changed files with 1077 additions and 397 deletions

View File

@@ -11,9 +11,6 @@ import ErrorPage from "@/routes/error-page";
import routes from "@/routes";
import '@/App.css';
import 'reset-css';
import { LanguageProvider } from '@/context/useLanguageContext';
import { ConfigProvider } from '@/context/useConfigContext';
import { HeaderProvider } from '@/context/useHeaderContext';
const router = createBrowserRouter(
createRoutesFromElements(
@@ -42,12 +39,6 @@ const router = createBrowserRouter(
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ConfigProvider>
<LanguageProvider>
<HeaderProvider>
<RouterProvider router={router} />
</HeaderProvider>
</LanguageProvider>
</ConfigProvider>
<RouterProvider router={router} />
</React.StrictMode>
)

View File

@@ -5,7 +5,7 @@ export default function CharIcon(props) {
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>
:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 94.563 67.437"><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>
<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>

View File

@@ -50,6 +50,11 @@
font-family: "Geometos", "Noto Sans SC", sans-serif;
}
.popup .text {
flex-grow: 1;
margin-right: 3rem;
}
.popup .content {
line-height: 1.3em;
padding: 1rem 1rem 0 1rem;

View File

@@ -1,6 +1,6 @@
import {
useState,
useEffect
useCallback
} from 'react'
import './popup.css'
import ReturnButton from '@/component/return_button';
@@ -9,16 +9,16 @@ import MainBorder from '@/component/main_border';
export default function Popup(props) {
const [hidden, setHidden] = useState(true)
const toggle = () => {
const toggle = useCallback(() => {
setHidden(!hidden)
}
}, [hidden])
return (
<>
<section className={`popup ${hidden ? '' : 'active'}`}>
<section className='wrapper'>
<section className='title'>
<span>{props.title}</span>
<section className="text">{props.title}</section>
<ReturnButton onClick={toggle} className="return-button"/>
</section>
<MainBorder/>

View File

@@ -1,92 +0,0 @@
import { createContext, useState, useEffect, useCallback } from "react"
import db, { invalidateCache } from "@/db"
const versionCompare = (v1, v2) => {
const v1Arr = v1.split(".")
const v2Arr = v2.split(".")
for (let i = 0; i < v1Arr.length; i++) {
if (v1Arr[i] > v2Arr[i]) {
return 1
} else if (v1Arr[i] < v2Arr[i]) {
return -1
}
}
return 0
}
const invalidateRules = (local, version) => {
if (local === undefined) {
// no local version
return true
}
if (version === undefined) {
// no remote version
return false
}
return versionCompare(local, version) < 0
}
export const ConfigContext = createContext()
export function ConfigProvider(props) {
const [config, setConfig] = useState([])
const [operators, setOperators] = useState([])
const [version, setVersion] = useState({})
const fetchConfig = (version) => {
fetch("/_assets/directory.json").then(res => res.json()).then(data => {
setConfig(data)
db.config.put({ key: "config", value: data })
resolveOperators(data)
db.config.put({ key: "version", value: version })
})
}
const resolveOperators = useCallback((data) => {
let operatorsList = []
data.operators.forEach((item) => {
operatorsList = [...operatorsList, ...item]
})
setOperators(operatorsList)
}, [])
useEffect(() => {
fetch("/_assets/version.json").then(res => res.json()).then(data => {
db.config.get({ key: "version" }).then((local) => {
if (local === undefined || invalidateRules(local.value.directory, data.directory) || invalidateRules(local.value.showcase, data.showcase)) {
invalidateCache()
fetchConfig(data)
} else {
db.config.get({ key: "config" }).then((local) => {
if (local) {
setConfig(local.value)
resolveOperators(local.value)
} else {
fetchConfig(data)
}
})
}
})
setVersion(data)
})
/*
local.directory | version.directory | action
--------------- | ----------------- | ------
1.0.0 | 1.0.0 | use cache
1.0.0 | 1.0.1 | invalidate cache
1.0.1 | 1.0.0 | impossible
local.showcase | version.showcase | action
-------------- | ---------------- | ------
1.0.0 | 1.0.0 | use cache
1.0.0 | 1.0.1 | invalidate cache
1.0.1 | 1.0.0 | use cache
*/
}, [])
return (
<ConfigContext.Provider value={{ config, operators, version }}>
{props.children}
</ConfigContext.Provider>
)
}

View File

@@ -1,38 +0,0 @@
import {
createContext,
useState,
useEffect,
useContext
} from "react"
import { LanguageContext } from "@/context/useLanguageContext"
export const HeaderContext = createContext()
export function HeaderProvider(props) {
const [key, setTitle] = useState('')
const [title, setRealTitle] = useState('')
const {
language,
i18n
} = useContext(LanguageContext)
const [tabs, setTabs] = useState([])
const [currentTab, setCurrentTab] = useState([])
const [appbarExtraArea, setAppbarExtraArea] = useState([])
useEffect(() => {
const newTitle = i18n(key)
document.title = `${newTitle} - ${import.meta.env.VITE_APP_TITLE}`;
setRealTitle(newTitle)
}, [key, language])
return (
<HeaderContext.Provider value={{
title, setTitle,
tabs, setTabs,
currentTab, setCurrentTab,
appbarExtraArea, setAppbarExtraArea
}}>
{props.children}
</HeaderContext.Provider>
)
}

View File

@@ -1,34 +0,0 @@
import { createContext, useState, useEffect, useCallback } from "react"
import i18nValues from '@/i18n'
export const LanguageContext = createContext()
export function LanguageProvider(props) {
const textDefaultLang = "en-US"
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-*
useEffect(() => {
setAlternateLang(language.startsWith("en") ? "zh-CN" : language)
}, [language])
const i18n = useCallback((key, preferredLanguage = language) => {
if (i18nValues.key[key]) {
return i18nValues.key[key][preferredLanguage]
}
return key
}, [language])
return (
<LanguageContext.Provider
value={{
language, setLanguage,
textDefaultLang,
alternateLang,
i18n, i18nValues
}}
>
{props.children}
</LanguageContext.Provider>
)
}

View File

@@ -1,16 +0,0 @@
import Dexie from 'dexie';
const db = new Dexie('aklive2dDatabase');
db.version(2).stores({
image: '++key, blob',
voice: '++key, blob',
config: '++key, value',
});
export function invalidateCache() {
db.image.clear();
db.voice.clear();
db.config.clear();
}
export default db;

View File

@@ -51,6 +51,14 @@
"zh-CN": "语音",
"en-US": "Voice"
},
"showcase": {
"zh-CN": "壁纸",
"en-US": "Wallpaper"
},
"directory": {
"zh-CN": "目录页",
"en-US": "Directory Page"
},
"zh-CN": {
"zh-CN": "简体中文",
"en-US": "Chinese (Simplified)"

View File

@@ -0,0 +1,30 @@
// taken from: https://shallowdepth.online/posts/2022/04/why-usenavigate-hook-in-react-router-v6-triggers-waste-re-renders-and-how-to-solve-it/
import { createContext, useContext, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
const StableNavigateContext = createContext(null)
const StableNavigateContextProvider = ({ children }) => {
const navigate = useNavigate()
const navigateRef = useRef(navigate)
return (
<StableNavigateContext.Provider value={navigateRef}>
{children}
</StableNavigateContext.Provider>
)
}
const useStableNavigate = () => {
const navigateRef = useContext(StableNavigateContext)
if (navigateRef.current === null)
throw new Error('StableNavigate context is not initialized')
return navigateRef.current
}
export {
StableNavigateContext,
StableNavigateContextProvider,
useStableNavigate,
}

View File

@@ -0,0 +1,17 @@
.changelogs .item-group {
flex-direction: column;
align-items: stretch;
}
.changelogs .item-info {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
padding-left: 1rem;
}
.changelogs .item-info-content {
font-size: 1.5rem;
display: list-item;
}

View File

@@ -1,32 +1,65 @@
import {
useState,
useEffect,
useContext
} from 'react'
import './changelogs.css'
import { HeaderContext } from '@/context/useHeaderContext';
import { LanguageContext } from '@/context/useLanguageContext';
import { useHeader } from '@/state/header';
import useUmami from '@parcellab/react-use-umami'
import MainBorder from '@/component/main_border';
export default function Changelogs(props) {
const _trackEvt = useUmami('/changelogs')
const {
setTitle,
setTabs,
currentTab, setCurrentTab
} = useContext(HeaderContext)
const { language, i18n } = useContext(LanguageContext)
currentTab,
setHeaderIcon,
} = useHeader()
const [changelogs, setChangelogs] = useState([])
useEffect(() => {
setTitle('changelogs')
setTabs([])
setHeaderIcon(null)
fetch('/_assets/changelogs.json').then(res => res.json()).then(data => {
setChangelogs(data)
})
}, [])
useEffect(() => {
setTabs(changelogs.map((item) => {
return {
key: item[0].key
}
}))
}, [changelogs])
return (
<section>
<section>
Under Construction :(
</section>
<section className="changelogs">
{
changelogs.map((v) => {
return (
v.map((item) => {
return (
<section className="item-group-wrapper" key={item.date} hidden={currentTab !== item.key}>
<section className="item-group">
<section className="item-info">
{item.content.map((entry, index) => {
return (
<section className="item-info-content" key={index}>
{entry}
</section>
)
})}
</section>
<section className='item-group-date'>{item.date}</section>
</section>
<MainBorder />
</section>
)
})
)
})
}
</section>
)
}

View File

@@ -1,11 +1,11 @@
.home .item-group {
.item-group {
padding: 1rem;
display: flex;
align-items: flex-end;
flex-wrap: wrap;
}
.home .item-group-date {
.item-group-date {
margin: 1.5rem;
font-family: "Bender";
font-weight: bold;
@@ -17,7 +17,7 @@
user-select: none;
}
.home .item-group .item {
.item-group .item {
position: relative;
cursor: pointer;
width: 180px;
@@ -25,7 +25,7 @@
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);
}
.home .item-group .item .item-background-filler {
.item-group .item .item-background-filler {
border-right: 1px solid var(--home-item-background-linear-gradient-color);
position: absolute;
top: 0;
@@ -34,7 +34,7 @@
bottom: 0;
}
.home .item-group .item .item-outline {
.item-group .item .item-outline {
display: block;
position: absolute;
opacity: 0;
@@ -42,7 +42,7 @@
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.home .item-group .item .item-outline {
.item-group .item .item-outline {
width: 100%;
height: 100%;
left: -6px;
@@ -51,8 +51,8 @@
padding: 6px;
}
.home .item-group .item .item-outline::before,
.home .item-group .item .item-outline::after {
.item-group .item .item-outline::before,
.item-group .item .item-outline::after {
content: "";
display: block;
position: absolute;
@@ -63,31 +63,31 @@
border-right: var(--text-color) solid 3px;
}
.home .item-group .item .item-outline::before {
.item-group .item .item-outline::before {
top: -3px;
}
.home .item-group .item .item-outline::after {
.item-group .item .item-outline::after {
bottom: -3px;
}
.home .item-group .item:hover .item-outline,
.home .item-group .item:hover .item-info .item-info-background {
.item-group .item:hover .item-outline,
.item-group .item:hover .item-info .item-info-background {
opacity: 1;
visibility: visible;
}
.home .item-group .item .item-img {
.item-group .item .item-img {
height: 360px;
width: 100%;
transition: background-color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.home .item-group .item:hover .item-img {
.item-group .item:hover .item-img {
background-color: var(--home-item-hover-background-color);
}
.home .item-group .item .item-info {
.item-group .item .item-info {
white-space: nowrap;
position: relative;
padding: 0.8rem 0.4rem;
@@ -95,7 +95,7 @@
height: 36px;
}
.home .item-group .item .item-info .item-title-container {
.item-group .item .item-info .item-title-container {
color: white;
display: flex;
justify-content: space-between;
@@ -104,18 +104,18 @@
font-weight: bold;
}
.home .item-group .item .item-info .item-title-container .item-title {
.item-group .item .item-info .item-title-container .item-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.home .item-group .item .item-info .item-title-container .item-title {
.item-group .item .item-info .item-title-container .item-title {
line-height: 1.3em;
height: auto;
}
.home .item-group .item .item-info .item-title-container .item-type {
.item-group .item .item-info .item-title-container .item-type {
display: flex;
flex-direction: row;
align-items: baseline;
@@ -124,13 +124,13 @@
fill: var(--text-color)
}
.home .item-group .item .item-info .item-text {
.item-group .item .item-info .item-text {
font-size: 0.75rem;
font-family: "Geometos";
margin-top: 1rem;
}
.home .item-group .item .item-info .item-info-background {
.item-group .item .item-info .item-info-background {
position: absolute;
top: 0;
left: 0;
@@ -142,7 +142,7 @@
background-image: linear-gradient(70deg, transparent 40%, currentColor 150%);
}
.home .item-group .item .item-info .item-text-wrapper {
.item-group .item .item-info .item-text-wrapper {
overflow: hidden;
text-overflow: ellipsis;
color: var(--secondary-text-color);

View File

@@ -1,23 +1,28 @@
import {
useState,
useEffect,
useContext,
useCallback
} from 'react'
import {
NavLink,
} from "react-router-dom";
import './home.css'
import { ConfigContext } from '@/context/useConfigContext';
import { LanguageContext } from '@/context/useLanguageContext';
import { HeaderContext } from '@/context/useHeaderContext';
import { useConfig } from '@/state/config';
import {
useLanguage,
useI18n
} from '@/state/language'
import { useHeader } from '@/state/header';
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils';
import CharIcon from '@/component/char_icon';
import MainBorder from '@/component/main_border';
import useUmami from '@parcellab/react-use-umami';
import Switch from '@/component/switch';
import db from '@/db';
const audioEl = new Audio()
let isPlaying = false
const voiceOnAtom = atomWithStorage('voiceOn', false)
export default function Home() {
const _trackEvt = useUmami('/')
@@ -25,30 +30,34 @@ export default function Home() {
setTitle,
setTabs,
currentTab,
setAppbarExtraArea
} = useContext(HeaderContext)
const { config } = useContext(ConfigContext)
const {
language,
textDefaultLang,
alternateLang,
i18n
} = useContext(LanguageContext)
setAppbarExtraArea,
setHeaderIcon
} = useHeader()
const { config } = useConfig()
const { textDefaultLang, language, alternateLang } = useLanguage()
const [content, setContent] = useState([])
const [voiceOn, setVoiceOn] = useState(false)
const [voiceOn, setVoiceOn] = useAtom(voiceOnAtom)
const { i18n } = useI18n()
useEffect(() => {
setTitle('dynamic_compile')
setTabs(['all', 'operator', 'skin'])
setTabs([{
key: 'all'
}, {
key: 'operator'
}, {
key: 'skin'
}])
setHeaderIcon(null)
}, [])
useEffect(() => {
setContent(config?.operators || [])
}, [config])
const toggleVoice = () => {
const toggleVoice = useCallback(() => {
setVoiceOn(!voiceOn)
}
}, [voiceOn])
useEffect(() => {
setAppbarExtraArea([
@@ -63,42 +72,29 @@ export default function Home() {
])
}, [voiceOn, language])
const isShown = (type) => currentTab === 'all' || currentTab === type
const isShown = useCallback((type) => currentTab === 'all' || currentTab === type, [currentTab])
const playVoice = useCallback((blob) => {
const audioUrl = URL.createObjectURL(blob)
const playVoice = useCallback((link) => {
const audioUrl = `/${link}/assets/voice/${import.meta.env.VITE_APP_VOICE_URL}`
if (!voiceOn || (audioEl.src === (window.location.href.replace(/\/$/g, '') + audioUrl) && isPlaying)) return
audioEl.src = audioUrl
let startPlayPromise = audioEl.play()
if (startPlayPromise !== undefined) {
startPlayPromise
.then(() => {
isPlaying = true
const audioEndedFunc = () => {
audioEl.removeEventListener('ended', audioEndedFunc)
URL.revokeObjectURL(audioUrl)
isPlaying = false
}
audioEl.addEventListener('ended', audioEndedFunc)
})
.catch(() => {
.catch((e) => {
console.log(e)
return
})
}
}, [])
const loadVoice = (link) => {
if (!voiceOn) return
db.voice.get({ key: link }).then((v) => {
if (v) {
playVoice(v.blob)
} else {
fetch(`/${link}/assets/voice/${import.meta.env.VITE_APP_VOICE_URL}`)
.then(res => res.blob())
.then(blob => {
db.voice.put({ key: link, blob: blob })
playVoice(blob)
})
}
})
}
}, [voiceOn])
return (
<section className="home">
@@ -115,7 +111,7 @@ export default function Home() {
className="item"
key={item.link}
hidden={!isShown(item.type)}
onMouseEnter={() => loadVoice(item.link)}
onMouseEnter={() => playVoice(item.link)}
>
<section className="item-background-filler" />
<section className="item-outline" />
@@ -158,22 +154,5 @@ export default function Home() {
}
function ImageElement({ item, language }) {
const [blobUrl, setBlobUrl] = useState(null)
useEffect(() => {
db.image.get({ key: item.link }).then((v) => {
if (v) {
setBlobUrl(URL.createObjectURL(v.blob))
} else {
fetch(`/${item.link}/assets/${item.fallback_name.replace("#", "%23")}_portrait.png`)
.then(res => res.blob())
.then(blob => {
db.image.put({ key: item.link, blob: blob })
setBlobUrl(URL.createObjectURL(blob))
})
}
})
}, [item.link])
return blobUrl ? <img src={blobUrl} alt={item.codename[language]} /> : null
return <img src={`/${item.link}/assets/${item.fallback_name.replace("#", "%23")}_portrait.png`} alt={item.codename[language]} />
}

View File

@@ -1,35 +1,38 @@
import {
useState,
useEffect,
useContext
useContext,
useCallback,
useMemo
} from 'react'
import {
useParams,
useNavigate
} from "react-router-dom";
import './operator.css'
import { ConfigContext } from '@/context/useConfigContext';
import { LanguageContext } from '@/context/useLanguageContext';
import { HeaderContext } from '@/context/useHeaderContext';
import { useConfig } from '@/state/config';
import {
useLanguage,
} from '@/state/language'
import { useHeader } from '@/state/header';
import useUmami from '@parcellab/react-use-umami'
export default function Operator(props) {
const _trackEvt = useUmami('/operator/:key')
const { operators } = useContext(ConfigContext)
const {
language,
i18n
} = useContext(LanguageContext)
const navigate = useNavigate()
const { operators } = useConfig()
const { language } = useLanguage()
const { key } = useParams();
const {
setTitle,
setTabs,
setAppbarExtraArea
} = useContext(HeaderContext)
setAppbarExtraArea,
setHeaderIcon
} = useHeader()
const [config, setConfig] = useState(null)
const [spineData, setSpineData] = useState({})
const _trackEvt = useUmami(`/operator/${key}`)
useEffect(() => {
setTabs([])
setAppbarExtraArea([])
}, [])
@@ -40,14 +43,54 @@ export default function Operator(props) {
fetch(`/_assets/${config.filename.replace("#", "%23")}.json`).then(res => res.json()).then(data => {
setSpineData(data)
})
setHeaderIcon(config.type)
}
}, [operators])
}, [operators, key])
const getTabName = (item) => {
if (item.type === 'operator') {
return 'operator'
} else {
return item.codename[language].replace(/^(.+)( )(·|\/)()(.+)$/, '$1')
}
}
const coverToTab = (item) => {
const key = getTabName(item)
return {
key: key,
style: {
color: item.color
},
onClick: (e, tab) => {
if (tab === key) return
navigate(`/operator/${item.link}`)
}
}
}
const getOtherEntries = () => {
return operators.filter((item) => item.id === config.id && item.link !== config.link).map((item) => {
return coverToTab(item)
})
}
useEffect(() => {
if (config) {
setTabs(
[
coverToTab(config),
...getOtherEntries()
]
)
}
}, [config, language, key])
useEffect(() => {
if (config) {
setTitle(config.codename[language])
}
}, [config, language])
}, [config, language, key])
return (
<section>

View File

@@ -147,26 +147,63 @@
line-height: 1.2em;
}
@media (max-width: 600px) {
.main .main-header .main-title {
font-size: 2.5rem;
}
}
@media (max-width: 480px) {
.main .main-header .main-title {
font-size: 2rem;
}
}
.main .main-header .main-tab {
display: flex;
flex-direction: row;
align-items: center;
flex: auto;
text-align: right;
white-space: nowrap;
user-select: none;
}
.main .main-header .main-tab .main-tab-item {
font-size: 1em;
font-size: 1.25rem;
line-height: 3em;
font-weight: 700;
padding: 0 1rem;
text-transform: uppercase;
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
cursor: pointer;
border-bottom: 0.3rem solid transparent;
display: inline-block;
cursor: pointer;
text-decoration: none;
}
.main .main-header .main-tab .main-tab-item.active .text,
.main .main-header .main-tab .main-tab-item:hover .text {
color: currentColor;
}
.main .main-header .main-tab .main-tab-item.active,
.main .main-header .main-tab .main-tab-item:hover {
color: var(--link-highlight-color);
}
.main .main-header .main-tab .main-tab-item.active {
color: var(--link-highlight-color);
border-bottom-color: var(--link-highlight-color);
border-bottom-color: currentColor;
}
.main .main-header .main-tab .main-tab-item .text {
color: var(--text-color);
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.main .main-icon {
width: 3.88rem;
margin-right: 1.88rem;
fill: var(--text-color);
display: inline-block;
vertical-align: middle;
}
.main .return-button {

View File

@@ -1,65 +1,67 @@
import {
useState,
useEffect,
useContext
useContext,
useMemo,
useCallback
} from 'react'
import {
Outlet,
Link,
NavLink,
useNavigate,
ScrollRestoration
} from "react-router-dom";
import './root.css'
import routes from '@/routes'
import { ConfigContext } from '@/context/useConfigContext';
import { HeaderContext } from '@/context/useHeaderContext';
import { LanguageContext } from '@/context/useLanguageContext';
import { useConfig } from '@/state/config';
import { useHeader } from '@/state/header';
import {
useI18n,
useLanguage,
} from '@/state/language'
import Dropdown from '@/component/dropdown';
import Popup from '@/component/popup';
import ReturnButton from '@/component/return_button';
import MainBorder from '@/component/main_border';
import CharIcon from '@/component/char_icon';
export default function Root(props) {
const navigate = useNavigate()
const [drawerHidden, setDrawerHidden] = useState(true)
const {
language, setLanguage,
textDefaultLang,
alternateLang,
i18n, i18nValues
} = useContext(LanguageContext)
const { textDefaultLang, language, alternateLang } = useLanguage()
const {
title,
tabs,
currentTab, setCurrentTab,
appbarExtraArea
} = useContext(HeaderContext)
const { version } = useContext(ConfigContext)
appbarExtraArea,
headerIcon
} = useHeader()
const { version } = useConfig()
const [drawerDestinations, setDrawerDestinations] = useState(null)
const currentYear = new Date().getFullYear()
const currentYear = useMemo(() => new Date().getFullYear(), [])
const [headerTabs, setHeaderTabs] = useState(null)
const { i18n } = useI18n()
const renderHeaderTabs = (tabs) => {
setHeaderTabs(tabs?.map((item) => {
return (
<section
key={item}
className={`main-tab-item ${currentTab === item ? 'active' : ''}`}
key={item.key}
className={`main-tab-item ${currentTab === item.key ? 'active' : ''}`}
onClick={(e) => {
setCurrentTab(item)
item.onClick && item.onClick(e, item)
setCurrentTab(item.key)
item.onClick && item.onClick(e, currentTab)
}}
style={item.style}
>
{i18n(item)}
<span className='text'>{i18n(item.key)}</span>
</section>
)
}))
}
const toggleDrawer = (value) => {
const toggleDrawer = useCallback((value) => {
setDrawerHidden(value || !drawerHidden)
}
}, [drawerHidden])
const renderDrawerDestinations = () => {
return routes.filter((item) => item.inDrawer).map((item) => {
@@ -110,7 +112,7 @@ export default function Root(props) {
useEffect(() => {
if (tabs.length > 0) {
setCurrentTab(tabs[0])
setCurrentTab(tabs[0].key)
} else {
setCurrentTab(null)
}
@@ -129,18 +131,7 @@ export default function Root(props) {
</section>
<section className='spacer' />
{appbarExtraArea}
<Dropdown
text={i18n(language)}
menu={i18nValues.available.map((item) => {
return {
name: i18n(item),
value: item
}
})}
onClick={(item) => {
setLanguage(item.value)
}}
/>
<LanguageDropdown />
</header>
<nav className={`drawer ${drawerHidden ? '' : 'active'}`}>
<section
@@ -155,19 +146,24 @@ export default function Root(props) {
</nav>
<main className='main'>
<section className='main-header'>
<section className='main-title'>{title}</section>
<section className='main-title'>
{headerIcon && (
<section className='main-icon'>
<CharIcon
type={headerIcon}
viewBox={
headerIcon === 'operator' ? '0 0 88.969 71.469' : '0 0 94.563 67.437'
}
/>
</section>
)}
{title}
</section>
<section className='main-tab'>
{headerTabs}
</section>
</section>
<MainBorder>
<ReturnButton
className='return-button'
onClick={() => {
navigate("/")
}}
/>
</MainBorder>
<HeaderReturnButton />
<Outlet />
</main>
<footer className='footer'>
@@ -187,7 +183,7 @@ export default function Root(props) {
<Link reloadDocument to="https://github.com/Halyul/aklive2d" target="_blank" className='link'>GitHub</Link>
</section>
<section className="item">
<Popup
<Popup
className='link'
title={i18n('contact_us')}
>
@@ -203,7 +199,49 @@ export default function Root(props) {
<span>Showcase @ {version.showcase}</span>
</section>
</footer>
<ScrollRestoration />
</>
)
}
function LanguageDropdown() {
const { language, setLanguage } = useLanguage()
const { i18n, i18nValues } = useI18n()
return (
<Dropdown
text={i18n(language)}
menu={i18nValues.available.map((item) => {
return {
name: i18n(item),
value: item
}
})}
onClick={(item) => {
setLanguage(item.value)
}}
/>
)
}
function HeaderReturnButton() {
const navigate = useNavigate()
const onClick = useCallback(() => {
navigate("/")
}, [])
const children = useMemo(() => {
return (
<ReturnButton
className='return-button'
onClick={onClick}
/>
)
}, [])
return (
<MainBorder>
{children}
</MainBorder>
)
}

View File

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

View File

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

View File

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