feat(directory): add changelogs page
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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]} />
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user