fix(directory): fixed issues related to voice and vcs
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.5.28
|
||||
0.5.29
|
||||
@@ -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,
|
||||
}
|
||||
63
directory/src/libs/voice.jsx
Normal file
63
directory/src/libs/voice.jsx
Normal 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,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user