diff --git a/changelogs.yaml b/changelogs.yaml
index 7e4bae7..9ee50b5 100644
--- a/changelogs.yaml
+++ b/changelogs.yaml
@@ -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:
diff --git a/directory/Version b/directory/Version
index dc2c2a7..aa0ea79 100644
--- a/directory/Version
+++ b/directory/Version
@@ -1 +1 @@
-0.5.28
\ No newline at end of file
+0.5.29
\ No newline at end of file
diff --git a/directory/src/libs/stable_navigate.jsx b/directory/src/libs/stable_navigate.jsx
deleted file mode 100644
index 2c92737..0000000
--- a/directory/src/libs/stable_navigate.jsx
+++ /dev/null
@@ -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 (
-
- {children}
-
- )
-}
-
-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,
-}
\ No newline at end of file
diff --git a/directory/src/libs/voice.jsx b/directory/src/libs/voice.jsx
new file mode 100644
index 0000000..dbe0152
--- /dev/null
+++ b/directory/src/libs/voice.jsx
@@ -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,
+ }
+
+}
\ No newline at end of file
diff --git a/directory/src/routes/path/home.jsx b/directory/src/routes/path/home.jsx
index d4c4bd1..42f2a67 100644
--- a/directory/src/routes/path/home.jsx
+++ b/directory/src/routes/path/home.jsx
@@ -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() {
)
@@ -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}
>
playVoice(item.link)}
+ onMouseEnter={() => playVoice()}
>
@@ -158,7 +134,7 @@ function OperatorElement({ item, hidden }) {
)
- }, [item, hidden, language, alternateLang, textDefaultLang])
+ }, [item, hidden, language, alternateLang, textDefaultLang, playVoice])
}
function VoiceSwitchElement() {
diff --git a/directory/src/routes/path/operator.jsx b/directory/src/routes/path/operator.jsx
index 278524a..4edb68b 100644
--- a/directory/src/routes/path/operator.jsx
+++ b/directory/src/routes/path/operator.jsx
@@ -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 && (
-
+
)
}
-
- {
- subtitle && (
-
-
-
+
+ {currentVoiceId && subtitleObj && (
+
+ {subtitleObj[currentVoiceId]?.title}
+
+ {subtitleObj[currentVoiceId]?.text}
+
- )
+ )
}
diff --git a/src/components/player.js b/src/components/player.js
index 6dba470..810cb6d 100644
--- a/src/components/player.js
+++ b/src/components/player.js
@@ -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);