fix(directory): fixed issues related to voice and vcs

This commit is contained in:
Haoyu Xu
2023-03-03 16:20:36 -05:00
parent 5fc6f60b16
commit 18eb1371c6
7 changed files with 168 additions and 133 deletions

View File

@@ -36,6 +36,8 @@ showcase:
2021/05/26:
- First commit
directory:
2023/03/03:
- Fixed Voice and VCs issues
2023/03/02:
- Added Voice, VCs, Operator Logo
2023/03/01:

View File

@@ -1 +1 @@
0.5.28
0.5.29

View File

@@ -1,30 +0,0 @@
// 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,63 @@
import {
useState,
useEffect,
useRef,
} from "react"
import { useCallback } from "react"
const audioEl = new Audio()
export default function useAudio() {
const [isPlaying, _setIsPlaying] = useState(false)
const isPlayingRef = useRef(isPlaying)
const setIsPlaying = (data) => {
isPlayingRef.current = data
_setIsPlaying(data)
}
useEffect(() => {
audioEl.addEventListener('ended', () => setIsPlaying(false))
return () => {
audioEl.removeEventListener('ended', () => setIsPlaying(false))
}
}, [])
const play = useCallback(
(
link,
options = {
overwrite: false
},
callback = () => { }
) => {
if (!options.overwrite && audioEl.src === (window.location.href.replace(/\/$/g, '') + link)) return
audioEl.src = link
let startPlayPromise = audioEl.play()
if (startPlayPromise !== undefined) {
setIsPlaying(true)
startPlayPromise
.then(() => {
callback()
return
})
.catch(() => {
return
})
}
}, [])
const stop = useCallback(() => {
audioEl.pause()
setIsPlaying(false)
}, [])
const getSrc = useCallback(() => audioEl.src, [])
return {
play,
stop,
getSrc,
isPlaying,
isPlayingRef,
}
}

View File

@@ -15,47 +15,15 @@ import {
} from '@/state/language'
import { useHeader } from '@/state/header';
import { useAppbar } from '@/state/appbar';
import useAudio from '@/libs/voice';
import { useAtom } from 'jotai'
import { atom } 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';
const audioEl = new Audio()
let isPlaying = false
let voiceOnState = false
const voiceOnStateAtom = atom(voiceOnState)
const voiceOnAtom = atom(
(get) => get(voiceOnStateAtom),
(get, set, newState) => {
voiceOnState = newState
set(voiceOnStateAtom, voiceOnState)
// you can set as many atoms as you want at the same time
}
)
const playVoice = (link) => {
const audioUrl = `/${link}/assets/${JSON.parse(import.meta.env.VITE_VOICE_FOLDERS).main}/${import.meta.env.VITE_APP_VOICE_URL}`
if (!voiceOnState || (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)
isPlaying = false
}
audioEl.addEventListener('ended', audioEndedFunc)
})
.catch((e) => {
console.log(e)
return
})
}
}
const voiceOnAtom = atomWithStorage('voiceOn', false)
export default function Home() {
// eslint-disable-next-line no-unused-vars
@@ -68,6 +36,7 @@ export default function Home() {
} = useHeader()
const { config } = useConfig()
const [content, setContent] = useState([])
const { stop } = useAudio()
useEffect(() => {
setTitle('dynamic_compile')
@@ -79,7 +48,8 @@ export default function Home() {
key: 'skin'
}])
setHeaderIcon(null)
}, [setHeaderIcon, setTabs, setTitle])
stop()
}, [setHeaderIcon, setTabs, setTitle, stop])
useEffect(() => {
setContent(config?.operators || [])
@@ -100,7 +70,6 @@ export default function Home() {
<OperatorElement
key={item.link}
item={item}
handleOnMouseEnter={playVoice}
hidden={!isShown(item.type)}
/>
)
@@ -119,6 +88,13 @@ export default function Home() {
function OperatorElement({ item, hidden }) {
const { textDefaultLang, language, alternateLang } = useLanguage()
const { play } = useAudio()
const [voiceOn] = useAtom(voiceOnAtom)
const playVoice = useCallback(() => {
if (!voiceOn) return
play(`/${item.link}/assets/${JSON.parse(import.meta.env.VITE_VOICE_FOLDERS).main}/${import.meta.env.VITE_APP_VOICE_URL}`)
}, [play, item.link, voiceOn])
return useMemo(() => {
return (
@@ -128,7 +104,7 @@ function OperatorElement({ item, hidden }) {
hidden={hidden}
>
<section
onMouseEnter={() => playVoice(item.link)}
onMouseEnter={() => playVoice()}
>
<section className="item-background-filler" />
<section className="item-outline" />
@@ -158,7 +134,7 @@ function OperatorElement({ item, hidden }) {
</section>
</NavLink>
)
}, [item, hidden, language, alternateLang, textDefaultLang])
}, [item, hidden, language, alternateLang, textDefaultLang, playVoice])
}
function VoiceSwitchElement() {

View File

@@ -17,7 +17,9 @@ import {
useLanguage,
} from '@/state/language'
import { useHeader } from '@/state/header';
import { useAppbar } from '@/state/appbar';
import { useBackgrounds } from '@/state/background';
import useAudio from '@/libs/voice';
import useUmami from '@parcellab/react-use-umami'
import spine from '!/libs/spine-player'
import '!/libs/spine-player.css'
@@ -29,10 +31,10 @@ const getVoiceFoler = (lang) => {
const voiceFolder = folderObject.sub.find(e => e.lang === lang) || folderObject.sub.find(e => e.name === 'custom')
return `${folderObject.main}/${voiceFolder.name}`
}
const configAtom = atom(null);
const spinePlayerAtom = atom(null);
const spineAnimationAtom = atom("Idle");
const audioEl = new Audio()
const voiceLangAtom = atom(null);
const subtitleLangAtom = atom(null);
const getTabName = (item, language) => {
if (item.type === 'operator') {
@@ -42,8 +44,6 @@ const getTabName = (item, language) => {
}
}
// TODO: fix subtitle show/hide, fix voice play/pause when change route
export default function Operator() {
const navigate = useNavigate()
const { operators } = useConfig()
@@ -52,10 +52,10 @@ export default function Operator() {
const {
setTitle,
setTabs,
setAppbarExtraArea,
setHeaderIcon
} = useHeader()
const [config, setConfig] = useAtom(configAtom)
const { setExtraArea } = useAppbar()
const [config, setConfig] = useState(null)
const [spineData, setSpineData] = useState(null)
// eslint-disable-next-line no-unused-vars
const _trackEvt = useUmami(`/${key}`)
@@ -63,20 +63,33 @@ export default function Operator() {
const [spineAnimation, setSpineAnimation] = useAtom(spineAnimationAtom)
const { i18n } = useI18n()
const [spinePlayer, setSpinePlayer] = useAtom(spinePlayerAtom)
const [voiceLang, setVoiceLang] = useState(null)
const [voiceLang, _setVoiceLang] = useAtom(voiceLangAtom)
const { backgrounds } = useBackgrounds()
const [currentBackground, setCurrentBackground] = useState(null)
const [voiceConfig, setVoiceConfig] = useState(null)
const [subtitleLang, setSubtitleLang] = useState(null)
const [subtitle, setSubtitle] = useState(null)
const [subtitleLang, setSubtitleLang] = useAtom(subtitleLangAtom)
const [hideSubtitle, setHideSubtitle] = useState(true)
const [isVoicePlaying, setIsVoicePlaying] = useState(false)
const [lastVoiceId, setLastVoiceId] = useState(null)
const { play, stop, getSrc, isPlaying, isPlayingRef } = useAudio()
const [subtitleObj, _setSubtitleObj] = useState(null)
const [currentVoiceId, setCurrentVoiceId] = useState(null)
const voiceLangRef = useRef(voiceLang)
const subtitleObjRef = useRef(subtitleObj)
const configRef = useRef(config)
const setVoiceLang = (value) => {
voiceLangRef.current = value
_setVoiceLang(value)
}
const setSubtitleObj = (value) => {
subtitleObjRef.current = value
_setSubtitleObj(value)
}
useEffect(() => {
setAppbarExtraArea([])
}, [setAppbarExtraArea])
setExtraArea([])
stop()
}, [setExtraArea, stop])
useEffect(() => {
if (backgrounds) setCurrentBackground(backgrounds[0])
@@ -87,6 +100,7 @@ export default function Operator() {
const config = operators.find((item) => item.link === key)
if (config) {
setConfig(config)
configRef.current = config
fetch(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/${config.filename.replace("#", "%23")}.json`).then(res => res.json()).then(data => {
setSpineData(data)
})
@@ -98,7 +112,7 @@ export default function Operator() {
setVoiceConfig(data)
})
}
}, [operators, key, setHeaderIcon, setConfig])
}, [key, operators, setHeaderIcon])
const coverToTab = useCallback((item, language) => {
const key = getTabName(item, language)
@@ -161,70 +175,82 @@ export default function Operator() {
touch: false,
fps: 60,
defaultMix: 0,
success: (player) => {
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)
play(
`/${configRef.current.link}/assets/${getVoiceFoler(voiceLangRef.current)}/${id}.ogg`,
() => {
lastVoiceId = currentVoiceId
}
)
}
}
}))
}
}, [spineData, setSpinePlayer, spineAnimation, config]);
}, [config, spineData, setSpinePlayer, spineAnimation, play]);
const subtitleObj = useMemo(() => {
useEffect(() => {
if (voiceConfig && voiceLang) {
let subtitleObj = voiceConfig.subtitleLangs[subtitleLang || 'zh-CN']
let subtitleKey = 'default'
if (subtitleObj[voiceLang]) {
subtitleKey = voiceLang
}
return subtitleObj[subtitleKey]
setSubtitleObj(subtitleObj[subtitleKey])
}
}, [subtitleLang, voiceConfig, voiceLang])
const handleClickPlay = useCallback(() => {
if (!voiceLang) return
const voiceId = () => {
const keys = Object.keys(subtitleObj)
const id = keys[Math.floor((Math.random() * keys.length))]
return id === lastVoiceId ? voiceId() : id
}
const id = voiceId()
setLastVoiceId(currentVoiceId)
setCurrentVoiceId(id)
audioEl.src = `/${config.link}/assets/${getVoiceFoler(voiceLang)}/${id}.ogg`
let startPlayPromise = audioEl.play()
setIsVoicePlaying(true)
if (startPlayPromise !== undefined) {
startPlayPromise
.then(() => {
const audioEndedFunc = () => {
audioEl.removeEventListener('ended', audioEndedFunc)
if (currentVoiceId !== id) return
setIsVoicePlaying(false)
}
audioEl.addEventListener('ended', audioEndedFunc)
})
.catch(() => {
return
})
}
}, [voiceLang, lastVoiceId, config, currentVoiceId, subtitleObj])
useEffect(() => {
if (subtitleLang) {
if (isVoicePlaying) {
if (isPlaying) {
setHideSubtitle(false)
setSubtitle(subtitleObj[currentVoiceId])
} else {
const autoHide = () => {
if (isVoicePlaying) return
if (isPlayingRef.current) return
setHideSubtitle(true)
}
setTimeout(autoHide, 5 * 1000)
return () => {
clearTimeout(autoHide)
}
// setHideSubtitle(true)
}
} else {
setHideSubtitle(true)
}
}, [subtitleLang, currentVoiceId, isVoicePlaying, subtitleObj])
}, [subtitleLang, isPlaying, isPlayingRef])
useEffect(() => {
if (voiceLang && isPlaying) {
const audioUrl = `/assets/${getVoiceFoler(voiceLang)}/${currentVoiceId}.ogg`
if (getSrc() !== (window.location.href.replace(/\/$/g, '') + audioUrl)) {
play(`/${config.link}${audioUrl}`)
}
}
}, [voiceLang, isPlaying, currentVoiceId, config, getSrc, play])
useEffect(() => {
if (voiceLang && config) {
let id = ''
if (spineAnimation === 'Idle') id = 'CN_011'
if (spineAnimation === 'Interact') id = 'CN_034'
if (spineAnimation === 'Special') id = 'CN_042'
setCurrentVoiceId(id)
play(
`/${config.link}/assets/${getVoiceFoler(voiceLang)}/${id}.ogg`
)
}
}, [voiceLang, config, spineAnimation, play])
const spineSettings = [
{
@@ -392,20 +418,18 @@ export default function Operator() {
}} >
{
config && (
<img src={`/${config.link}/assets/${config.logo}.png`} alt={config?.codename[language]} className='operator-logo'/>
<img src={`/${config.link}/assets/${config.logo}.png`} alt={config?.codename[language]} className='operator-logo' />
)
}
<section ref={spineRef} onClick={handleClickPlay} />
{
subtitle && (
<section className={`voice-wrapper${hideSubtitle ? '' : ' active'}`}>
<section className='voice-title'>{subtitle.title}</section>
<section className='voice-subtitle'>
<span>{subtitle.text}</span>
<span className='voice-triangle' />
</section>
<section ref={spineRef} />
{currentVoiceId && subtitleObj && (
<section className={`voice-wrapper${hideSubtitle ? '' : ' active'}`}>
<section className='voice-title'>{subtitleObj[currentVoiceId]?.title}</section>
<section className='voice-subtitle'>
<span>{subtitleObj[currentVoiceId]?.text}</span>
<span className='voice-triangle' />
</section>
)
</section>)
}
</section>
</section>

View File

@@ -37,7 +37,7 @@ export default function spinePlayer(el) {
isPlayingInteract = false;
}
},
complete: (e) => {
complete: () => {
if (window.performance.now() - resetTime >= 8 * 1000 && Math.random() < 0.3) {
resetTime = window.performance.now();
let entry = widget.animationState.setAnimation(0, "Special", false, 0);