refactor(directory): use VoiceElement instead of useAudio
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
showcase:
|
||||
2023/03/15:
|
||||
- Added background music
|
||||
2023/03/03:
|
||||
- Performance optimization
|
||||
2023/02/26:
|
||||
@@ -38,10 +40,12 @@ showcase:
|
||||
2021/05/26:
|
||||
- First commit
|
||||
directory:
|
||||
2023/03/15:
|
||||
- Added background music
|
||||
2023/03/03:
|
||||
- Fixed Voice and VCs issues
|
||||
- Fixed Voice and VLs issues
|
||||
2023/03/02:
|
||||
- Added Voice, VCs, Operator Logo
|
||||
- Added Voice, VLs, Operator Logo
|
||||
2023/03/01:
|
||||
- Added Operator page
|
||||
- Added Changelogs page
|
||||
|
||||
@@ -34,6 +34,7 @@ localization:
|
||||
ui_privacy_do_not_track: Send usage data
|
||||
ui_music_title: <hr><h4>📝 Music</h4><hr>
|
||||
ui_music_notice: <span><b>Please adjust the 'Offset' value if you notice audio cutoff</b></span>
|
||||
ui_music_notice1: <span><b>'bg_rhodes_day.png' and 'operator_bg.png' use the same music, it is not a bug.</b></span>
|
||||
ui_music_selection: Music
|
||||
ui_music_volume: Volume
|
||||
ui_music_offset: Offset
|
||||
@@ -71,6 +72,7 @@ localization:
|
||||
ui_privacy_do_not_track: 发送使用数据
|
||||
ui_music_title: <hr><h4>🎵 音乐</h4><hr>
|
||||
ui_music_notice: <span><b>如若发现音频截止,请调节 '弥补' 数值</b></span>
|
||||
ui_music_notice1: <span><b>'bg_rhodes_day.png' 和 'operator_bg.png' 使用同样的音乐,并非Bug</b></span>
|
||||
ui_music_selection: 音乐
|
||||
ui_music_volume: 音量
|
||||
ui_music_offset: 弥补
|
||||
@@ -233,6 +235,10 @@ properties:
|
||||
value:
|
||||
text: ui_music_notice
|
||||
condition: music_title.value == true
|
||||
- key: music_notice1
|
||||
value:
|
||||
text: ui_music_notice1
|
||||
condition: music_title.value == true
|
||||
- key: music_selection
|
||||
value:
|
||||
text: ui_music_selection
|
||||
|
||||
@@ -5,3 +5,5 @@ VITE_DIRECTORY_FOLDER="_assets"
|
||||
VITE_BACKGROUND_FOLDER="background"
|
||||
VITE_AVAILABLE_OPERATORS=["chen","dusk","dusk_everything_is_a_miracle","ling","nearl","nian","nian_unfettered_freedom","phatom_focus","rosmontis","skadi","skadi_sublimation","w","w_wonder","specter","gavial","surtr_colorful_wonderland","lee_trust_your_eyes","texas_the_omertosa","nearl_relight","rosmontis_become_anew","passager_dream_in_a_moment","mizuki_summer_feast","chongyue","ling_it_does_wash_the_strings","pozemka_snowy_plains_in_words"]
|
||||
VITE_ERROR_FILES={"files":[{"key":"build_char_128_plosis_epoque%233","paddings":{"left":-120,"right":150,"top":10,"bottom":0}},{"key":"build_char_128_plosis","paddings":{"left":-90,"right":100,"top":10,"bottom":0}}],"voice":"CN_034.ogg"}
|
||||
VITE_MUSIC_FOLDER=music
|
||||
VITE_MUSIC_MAPPING={"operator_bg.png":{"intro":"m_sys_void_intro.ogg","loop":"m_sys_void_loop.ogg"},"bg_anniversary_1.png":{"intro":"m_dia_nightoflongmen_intro.ogg","loop":"m_dia_nightoflongmen_loop.ogg"},"bg_iberia_1.png":{"intro":"m_sys_act18d3d0_intro.ogg","loop":"m_sys_act18d3d0_loop.ogg"},"bg_kazimierz_1.png":{"intro":"m_dia_street_intro.ogg","loop":"m_dia_street_loop.ogg"},"bg_main_victoria_1.png":{"intro":"m_avg_ghosthunter_intro.ogg","loop":"m_avg_ghosthunter_loop.ogg"},"bg_rhodes_day.png":{"intro":"m_sys_void_intro.ogg","loop":"m_sys_void_loop.ogg"},"bg_rhodes_night.png":{"intro":"m_sys_tech_intro.ogg","loop":"m_sys_tech_loop.ogg"},"bg_rogue_1.png":{"intro":null,"loop":"m_avg_rglk1secretevent_loop.ogg"},"bg_siesta_1.png":{"intro":"m_sys_ddd_intro.ogg","loop":"m_sys_ddd_loop.ogg"},"bg_ursus_1.png":{"intro":"m_avg_loneliness_intro.ogg","loop":"m_avg_loneliness_loop.ogg"},"bg_yan_1.png":{"intro":null,"loop":"m_sys_bitw_loop.ogg"}}
|
||||
@@ -1 +1 @@
|
||||
1.0.18
|
||||
1.0.22
|
||||
@@ -16,7 +16,11 @@ export default function Switch(props) {
|
||||
return (
|
||||
<section
|
||||
className={`${classes.switch} ${on ? classes.active : ''}`}
|
||||
onClick={() => props.handleOnClick()}
|
||||
onClick={() => {
|
||||
if (props.handleOnClick) {
|
||||
props.handleOnClick(!on)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className={classes.text}>{i18n(props.text)}</span>
|
||||
<section className={classes.wrapper}>
|
||||
|
||||
53
directory/src/component/voice.jsx
Normal file
53
directory/src/component/voice.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react"
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function VoiceElement({
|
||||
src,
|
||||
replay,
|
||||
handleAduioStateChange,
|
||||
}) {
|
||||
const audioRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (src) {
|
||||
audioRef.current.src = src
|
||||
audioRef.current.play()
|
||||
} else {
|
||||
audioRef.current.pause()
|
||||
}
|
||||
}, [src])
|
||||
|
||||
useEffect(() => {
|
||||
if (replay) {
|
||||
audioRef.current.currentTime = 0
|
||||
audioRef.current.play()
|
||||
}
|
||||
}, [replay])
|
||||
|
||||
return (
|
||||
<audio
|
||||
ref={audioRef}
|
||||
preload="auto"
|
||||
autoPlay
|
||||
onEnded={(e) => {
|
||||
if (handleAduioStateChange) handleAduioStateChange(e, 'ended')
|
||||
}}
|
||||
onPlay={(e) => {
|
||||
if (handleAduioStateChange) handleAduioStateChange(e, 'play')
|
||||
}}
|
||||
onPause={(e) => {
|
||||
if (handleAduioStateChange) handleAduioStateChange(e, 'pause')
|
||||
}}
|
||||
>
|
||||
<source type="audio/ogg" />
|
||||
</audio>
|
||||
)
|
||||
}
|
||||
VoiceElement.propTypes = {
|
||||
src: PropTypes.string,
|
||||
handleAduioStateChange: PropTypes.func,
|
||||
replay: PropTypes.bool,
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react"
|
||||
import { useCallback } from "react"
|
||||
const audioEl = new Audio()
|
||||
let lastSrc = ''
|
||||
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 && link === lastSrc) return
|
||||
audioEl.src = link
|
||||
let startPlayPromise = audioEl.play()
|
||||
if (startPlayPromise !== undefined) {
|
||||
setIsPlaying(true)
|
||||
startPlayPromise
|
||||
.then(() => {
|
||||
lastSrc = link
|
||||
callback()
|
||||
return
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
return
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const stop = useCallback(() => {
|
||||
audioEl.pause()
|
||||
setIsPlaying(false)
|
||||
}, [])
|
||||
|
||||
const getSrc = useCallback(() => audioEl.src, [])
|
||||
const resetSrc = useCallback(() => {
|
||||
audioEl.src = ''
|
||||
}, [])
|
||||
|
||||
return {
|
||||
play,
|
||||
stop,
|
||||
getSrc,
|
||||
resetSrc,
|
||||
isPlaying,
|
||||
isPlayingRef,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import Switch from '@/component/switch';
|
||||
import ReturnButton from "@/component/return_button";
|
||||
import { Typewriter } from 'react-simple-typewriter'
|
||||
import { useHeader } from '@/state/header';
|
||||
import useAudio from '@/libs/voice';
|
||||
import VoiceElement from '@/component/voice';
|
||||
import spine from '!/libs/spine-player'
|
||||
import '!/libs/spine-player.css'
|
||||
import useUmami from '@parcellab/react-use-umami';
|
||||
@@ -27,6 +27,7 @@ const config = JSON.parse(import.meta.env.VITE_ERROR_FILES)
|
||||
const obj = config.files[Math.floor((Math.random() * config.files.length))]
|
||||
const filename = obj.key.replace("#", "%23")
|
||||
const padding = obj.paddings
|
||||
let lastVoiceState = 'ended'
|
||||
|
||||
export default function Error() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -40,9 +41,11 @@ export default function Error() {
|
||||
const [spineDone, _setSpineDone] = useState(false)
|
||||
const spineRef = useRef(null)
|
||||
const [spineData, setSpineData] = useState(null)
|
||||
const { play, stop } = useAudio()
|
||||
const spineDoneRef = useRef(spineDone)
|
||||
const voiceOnRef = useRef(voiceOn)
|
||||
const [voiceSrc, setVoiceSrc] = useState(null)
|
||||
const [voiceReplay, setVoiceReplay] = useState(false)
|
||||
const [spinePlayer, setSpinePlayer] = useState(null)
|
||||
|
||||
const setSpineDone = (data) => {
|
||||
spineDoneRef.current = data
|
||||
@@ -60,26 +63,40 @@ export default function Error() {
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(content[0])
|
||||
stop()
|
||||
}, [content, setTitle, stop])
|
||||
}, [content, setTitle])
|
||||
|
||||
useEffect(() => {
|
||||
if (!voiceOn) {
|
||||
stop()
|
||||
setVoiceSrc(null)
|
||||
} else {
|
||||
setVoiceSrc(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/error.ogg`)
|
||||
if (spinePlayer) {
|
||||
spinePlayer.animationState.setAnimation(0, "Interact", false, 0);
|
||||
spinePlayer.animationState.addAnimation(0, "Relax", true, 0);
|
||||
}
|
||||
}, [voiceOn, stop])
|
||||
}
|
||||
}, [voiceOn])
|
||||
|
||||
useEffect(() => {
|
||||
voiceOnRef.current = voiceOn
|
||||
}, [voiceOn])
|
||||
|
||||
const playVoice = useCallback(() => {
|
||||
play(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/error.ogg`, { overwrite: true })
|
||||
}, [play])
|
||||
if (lastVoiceState === 'ended' && voiceSrc !== null) {
|
||||
setVoiceReplay(true)
|
||||
}
|
||||
}, [voiceSrc])
|
||||
|
||||
const handleAduioStateChange = useCallback((e, state) => {
|
||||
lastVoiceState = state
|
||||
if (state === 'ended') {
|
||||
setVoiceReplay(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (spineRef.current?.children.length === 0 && spineData) {
|
||||
new spine.SpinePlayer(spineRef.current, {
|
||||
setSpinePlayer(new spine.SpinePlayer(spineRef.current, {
|
||||
skelUrl: `./assets/${filename}.skel`,
|
||||
atlasUrl: `./assets/${filename}.atlas`,
|
||||
rawDataURIs: spineData,
|
||||
@@ -99,7 +116,7 @@ export default function Error() {
|
||||
showControls: false,
|
||||
touch: false,
|
||||
fps: 60,
|
||||
defaultMix: 0,
|
||||
defaultMix: 0.3,
|
||||
success: (player) => {
|
||||
let isPlayingInteract = false
|
||||
player.animationState.addListener({
|
||||
@@ -127,7 +144,7 @@ export default function Error() {
|
||||
ani()
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
}, [playVoice, spineData]);
|
||||
|
||||
@@ -163,6 +180,11 @@ export default function Error() {
|
||||
className={`${classes.spine} ${spineDone ? classes.active : ''}`}
|
||||
ref={spineRef}
|
||||
/>
|
||||
<VoiceElement
|
||||
src={voiceSrc}
|
||||
replay={voiceReplay}
|
||||
handleAduioStateChange={handleAduioStateChange}
|
||||
/>
|
||||
</main>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '@/state/language'
|
||||
import { useHeader } from '@/state/header';
|
||||
import { useAppbar } from '@/state/appbar';
|
||||
import useAudio from '@/libs/voice';
|
||||
import VoiceElement from '@/component/voice';
|
||||
import { useAtom } from 'jotai'
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import CharIcon from '@/component/char_icon';
|
||||
@@ -24,6 +24,7 @@ import useUmami from '@parcellab/react-use-umami';
|
||||
import Switch from '@/component/switch';
|
||||
|
||||
const voiceOnAtom = atomWithStorage('voiceOn', false)
|
||||
let lastVoiceState = 'ended'
|
||||
|
||||
export default function Home() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -36,7 +37,9 @@ export default function Home() {
|
||||
} = useHeader()
|
||||
const { config } = useConfig()
|
||||
const [content, setContent] = useState([])
|
||||
const { stop } = useAudio()
|
||||
const [voiceOn] = useAtom(voiceOnAtom)
|
||||
const [voiceSrc, setVoiceSrc] = useState(null)
|
||||
const [voiceReplay, setVoiceReplay] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setTitle('dynamic_compile')
|
||||
@@ -48,15 +51,33 @@ export default function Home() {
|
||||
key: 'skin'
|
||||
}])
|
||||
setHeaderIcon(null)
|
||||
stop()
|
||||
}, [setHeaderIcon, setTabs, setTitle, stop])
|
||||
}, [setHeaderIcon, setTabs, setTitle])
|
||||
|
||||
useEffect(() => {
|
||||
setContent(config?.operators || [])
|
||||
}, [config])
|
||||
|
||||
const handleAduioStateChange = useCallback((e, state) => {
|
||||
lastVoiceState = state
|
||||
if (state === 'ended') {
|
||||
setVoiceReplay(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const isShown = useCallback((type) => currentTab === 'all' || currentTab === type, [currentTab])
|
||||
|
||||
const handleVoicePlay = useCallback((src) => {
|
||||
if (!voiceOn) {
|
||||
setVoiceSrc(null)
|
||||
} else {
|
||||
if (src === voiceSrc && lastVoiceState === 'ended') {
|
||||
setVoiceReplay(true)
|
||||
} else {
|
||||
setVoiceSrc(src)
|
||||
}
|
||||
}
|
||||
}, [voiceOn, voiceSrc])
|
||||
|
||||
return (
|
||||
<section>
|
||||
{
|
||||
@@ -71,6 +92,7 @@ export default function Home() {
|
||||
key={item.link}
|
||||
item={item}
|
||||
hidden={!isShown(item.type)}
|
||||
handleVoicePlay={handleVoicePlay}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -81,27 +103,17 @@ export default function Home() {
|
||||
)
|
||||
})
|
||||
}
|
||||
<VoiceSwitchElement />
|
||||
<VoiceSwitchElement
|
||||
src={voiceSrc}
|
||||
handleAduioStateChange={handleAduioStateChange}
|
||||
replay={voiceReplay}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function OperatorElement({ item, hidden }) {
|
||||
function OperatorElement({ item, hidden, handleVoicePlay }) {
|
||||
const { textDefaultLang, language, alternateLang } = useLanguage()
|
||||
const { play, stop, resetSrc } = 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}`)
|
||||
}, [voiceOn, play, item.link])
|
||||
|
||||
useEffect(() => {
|
||||
if (!voiceOn) {
|
||||
stop()
|
||||
resetSrc()
|
||||
}
|
||||
}, [voiceOn, stop, resetSrc])
|
||||
|
||||
return useMemo(() => {
|
||||
return (
|
||||
@@ -111,7 +123,7 @@ function OperatorElement({ item, hidden }) {
|
||||
hidden={hidden}
|
||||
>
|
||||
<section
|
||||
onMouseEnter={() => playVoice()}
|
||||
onMouseEnter={() => handleVoicePlay(`/${item.link}/assets/${JSON.parse(import.meta.env.VITE_VOICE_FOLDERS).main}/${import.meta.env.VITE_APP_VOICE_URL}`)}
|
||||
>
|
||||
<section className={classes['background-filler']} />
|
||||
<section className={classes.outline} />
|
||||
@@ -141,10 +153,10 @@ function OperatorElement({ item, hidden }) {
|
||||
</section>
|
||||
</NavLink>
|
||||
)
|
||||
}, [item, hidden, language, alternateLang, textDefaultLang, playVoice])
|
||||
}, [item, hidden, language, alternateLang, textDefaultLang, handleVoicePlay])
|
||||
}
|
||||
|
||||
function VoiceSwitchElement() {
|
||||
function VoiceSwitchElement({ src, replay, handleAduioStateChange }) {
|
||||
const [voiceOn, setVoiceOn] = useAtom(voiceOnAtom)
|
||||
const {
|
||||
setExtraArea,
|
||||
@@ -163,7 +175,19 @@ function VoiceSwitchElement() {
|
||||
])
|
||||
}, [voiceOn, setExtraArea, setVoiceOn])
|
||||
|
||||
return null
|
||||
return (
|
||||
<VoiceElement
|
||||
src={src}
|
||||
replay={replay}
|
||||
handleAduioStateChange={handleAduioStateChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
VoiceSwitchElement.propTypes = {
|
||||
src: PropTypes.string,
|
||||
replay: PropTypes.bool,
|
||||
handleAduioStateChange: PropTypes.func,
|
||||
}
|
||||
|
||||
function ImageElement({ item }) {
|
||||
|
||||
@@ -18,19 +18,23 @@ import {
|
||||
import { useHeader } from '@/state/header';
|
||||
import { useAppbar } from '@/state/appbar';
|
||||
import { useBackgrounds } from '@/state/background';
|
||||
import useAudio from '@/libs/voice';
|
||||
import VoiceElement from '@/component/voice';
|
||||
import useUmami from '@parcellab/react-use-umami'
|
||||
import spine from '!/libs/spine-player'
|
||||
import '!/libs/spine-player.css'
|
||||
import Border from '@/component/border';
|
||||
import { useI18n } from '@/state/language';
|
||||
import Switch from '@/component/switch';
|
||||
import { atom, useAtom } from 'jotai'
|
||||
|
||||
const musicMapping = JSON.parse(import.meta.env.VITE_MUSIC_MAPPING)
|
||||
const getVoiceFoler = (lang) => {
|
||||
const folderObject = JSON.parse(import.meta.env.VITE_VOICE_FOLDERS)
|
||||
const voiceFolder = folderObject.sub.find(e => e.lang === lang) || folderObject.sub.find(e => e.name === 'custom')
|
||||
return `${folderObject.main}/${voiceFolder.name}`
|
||||
}
|
||||
const defaultSpineAnimation = 'Idle'
|
||||
const backgroundAtom = atom(null)
|
||||
|
||||
const getTabName = (item, language) => {
|
||||
if (item.type === 'operator') {
|
||||
@@ -61,16 +65,18 @@ export default function Operator() {
|
||||
const [spinePlayer, setSpinePlayer] = useState(null)
|
||||
const [voiceLang, _setVoiceLang] = useState(null)
|
||||
const { backgrounds } = useBackgrounds()
|
||||
const [currentBackground, setCurrentBackground] = useState(null)
|
||||
const [currentBackground, setCurrentBackground] = useAtom(backgroundAtom)
|
||||
const [voiceConfig, setVoiceConfig] = useState(null)
|
||||
const [subtitleLang, setSubtitleLang] = useState(null)
|
||||
const [hideSubtitle, setHideSubtitle] = useState(true)
|
||||
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 [voiceSrc, setVoiceSrc] = useState(null)
|
||||
const [isVoicePlaying, _setIsVoicePlaying] = useState(false)
|
||||
const isVoicePlayingRef = useRef(isVoicePlaying)
|
||||
|
||||
const setVoiceLang = (value) => {
|
||||
voiceLangRef.current = value
|
||||
@@ -82,18 +88,18 @@ export default function Operator() {
|
||||
_setSubtitleObj(value)
|
||||
}
|
||||
|
||||
const setIsVoicePlaying = (value) => {
|
||||
isVoicePlayingRef.current = value
|
||||
_setIsVoicePlaying(value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setExtraArea([])
|
||||
stop()
|
||||
}, [setExtraArea, stop])
|
||||
}, [setExtraArea])
|
||||
|
||||
useEffect(() => {
|
||||
if (!voiceLang) stop()
|
||||
}, [stop, voiceLang])
|
||||
|
||||
useEffect(() => {
|
||||
if (backgrounds) setCurrentBackground(backgrounds[0])
|
||||
}, [backgrounds])
|
||||
if (backgrounds.length > 0) setCurrentBackground(backgrounds[0])
|
||||
}, [backgrounds, setCurrentBackground])
|
||||
|
||||
useEffect(() => {
|
||||
setSpineData(null)
|
||||
@@ -175,7 +181,7 @@ export default function Operator() {
|
||||
showControls: false,
|
||||
touch: false,
|
||||
fps: 60,
|
||||
defaultMix: 0,
|
||||
defaultMix: 0.3,
|
||||
success: (player) => {
|
||||
let lastVoiceId = null
|
||||
let currentVoiceId = null
|
||||
@@ -189,17 +195,13 @@ export default function Operator() {
|
||||
const id = voiceId()
|
||||
currentVoiceId = id
|
||||
setCurrentVoiceId(id)
|
||||
play(
|
||||
`/${configRef.current.link}/assets/${getVoiceFoler(voiceLangRef.current)}/${id}.ogg`,
|
||||
() => {
|
||||
setVoiceSrc(`/${configRef.current.link}/assets/${getVoiceFoler(voiceLangRef.current)}/${id}.ogg`)
|
||||
lastVoiceId = currentVoiceId
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}, [config, spineData, setSpinePlayer, spineAnimation, play]);
|
||||
}, [config, spineData, spineAnimation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (voiceConfig && voiceLang) {
|
||||
@@ -212,13 +214,24 @@ export default function Operator() {
|
||||
}
|
||||
}, [subtitleLang, voiceConfig, voiceLang])
|
||||
|
||||
const handleAduioStateChange = useCallback((e, state) => {
|
||||
switch (state) {
|
||||
case 'play':
|
||||
setIsVoicePlaying(true)
|
||||
break
|
||||
default:
|
||||
setIsVoicePlaying(false)
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (subtitleLang) {
|
||||
if (isPlaying) {
|
||||
if (isVoicePlaying) {
|
||||
setHideSubtitle(false)
|
||||
} else {
|
||||
const autoHide = () => {
|
||||
if (isPlayingRef.current) return
|
||||
if (isVoicePlayingRef.current) return
|
||||
setHideSubtitle(true)
|
||||
}
|
||||
setTimeout(autoHide, 5 * 1000)
|
||||
@@ -229,29 +242,35 @@ export default function Operator() {
|
||||
} else {
|
||||
setHideSubtitle(true)
|
||||
}
|
||||
}, [subtitleLang, isPlaying, isPlayingRef])
|
||||
}, [subtitleLang, isVoicePlaying])
|
||||
|
||||
useEffect(() => {
|
||||
if (voiceLang && isPlaying) {
|
||||
if (voiceLang && isVoicePlaying) {
|
||||
const audioUrl = `/assets/${getVoiceFoler(voiceLang)}/${currentVoiceId}.ogg`
|
||||
if (getSrc() !== (window.location.href.replace(/\/$/g, '') + audioUrl)) {
|
||||
play(`/${config.link}${audioUrl}`)
|
||||
if (voiceSrc !== (window.location.href.replace(/\/$/g, '') + audioUrl)) {
|
||||
setVoiceSrc(`/${config.link}${audioUrl}`)
|
||||
}
|
||||
}
|
||||
}, [voiceLang, isPlaying, currentVoiceId, config, getSrc, play])
|
||||
}, [voiceLang, isVoicePlaying, currentVoiceId, config, voiceSrc])
|
||||
|
||||
const playAnimationVoice = useCallback((animation) => {
|
||||
if (voiceLangRef.current) {
|
||||
let id = null
|
||||
if (animation === 'Idle') id = 'CN_011'
|
||||
if (animation === 'Interact') id = 'CN_034'
|
||||
if (animation === 'Special') id = 'CN_042'
|
||||
if (id) {
|
||||
setCurrentVoiceId(id)
|
||||
setVoiceSrc(`/${key}/assets/${getVoiceFoler(voiceLangRef.current)}/${id}.ogg`)
|
||||
}
|
||||
}
|
||||
}, [key])
|
||||
|
||||
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`
|
||||
)
|
||||
if (!voiceLang) {
|
||||
setVoiceSrc(null)
|
||||
}
|
||||
}, [voiceLang, config, spineAnimation, play])
|
||||
}, [voiceLang])
|
||||
|
||||
const spineSettings = [
|
||||
{
|
||||
@@ -260,8 +279,10 @@ export default function Operator() {
|
||||
{
|
||||
name: 'idle',
|
||||
onClick: () => {
|
||||
spinePlayer.animationState.setAnimation(0, "Idle", true, 0)
|
||||
setSpineAnimation('Idle')
|
||||
const animation = "Idle"
|
||||
playAnimationVoice(animation)
|
||||
spinePlayer.animationState.setAnimation(0, animation, true, 0)
|
||||
setSpineAnimation(animation)
|
||||
},
|
||||
activeRule: () => {
|
||||
return spineAnimation === 'Idle'
|
||||
@@ -269,8 +290,10 @@ export default function Operator() {
|
||||
}, {
|
||||
name: 'interact',
|
||||
onClick: () => {
|
||||
spinePlayer.animationState.setAnimation(0, "Interact", true, 0)
|
||||
setSpineAnimation('Interact')
|
||||
const animation = "Interact"
|
||||
playAnimationVoice(animation)
|
||||
spinePlayer.animationState.setAnimation(0, animation, true, 0)
|
||||
setSpineAnimation(animation)
|
||||
},
|
||||
activeRule: () => {
|
||||
return spineAnimation === 'Interact'
|
||||
@@ -278,8 +301,10 @@ export default function Operator() {
|
||||
}, {
|
||||
name: 'special',
|
||||
onClick: () => {
|
||||
spinePlayer.animationState.setAnimation(0, "Special", true, 0)
|
||||
setSpineAnimation('Special')
|
||||
const animation = "Special"
|
||||
playAnimationVoice(animation)
|
||||
spinePlayer.animationState.setAnimation(0, animation, true, 0)
|
||||
setSpineAnimation(animation)
|
||||
},
|
||||
activeRule: () => {
|
||||
return spineAnimation === 'Special'
|
||||
@@ -297,6 +322,9 @@ export default function Operator() {
|
||||
} else {
|
||||
setVoiceLang(null)
|
||||
}
|
||||
if (!isVoicePlayingRef.current) {
|
||||
playAnimationVoice(spineAnimation)
|
||||
}
|
||||
},
|
||||
activeRule: () => {
|
||||
return voiceLang === item
|
||||
@@ -320,6 +348,9 @@ export default function Operator() {
|
||||
}
|
||||
}
|
||||
}) || []
|
||||
}, {
|
||||
name: 'music',
|
||||
el: <MusicElement />
|
||||
}, {
|
||||
name: 'backgrounds',
|
||||
options: backgrounds.map((item) => {
|
||||
@@ -348,6 +379,13 @@ export default function Operator() {
|
||||
}}>
|
||||
{
|
||||
spineSettings.map((item) => {
|
||||
if (item.el) {
|
||||
return (
|
||||
<section key={item.name}>
|
||||
{item.el}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
if (item.options.length === 0) return null
|
||||
return (
|
||||
<section key={item.name}>
|
||||
@@ -417,7 +455,7 @@ export default function Operator() {
|
||||
</section>
|
||||
</section>
|
||||
<section className={classes.container} style={currentBackground && {
|
||||
backgroundImage: `url(/${key}/assets/${import.meta.env.VITE_BACKGROUND_FOLDER}/${currentBackground})`
|
||||
backgroundImage: `url(/chen/assets/${import.meta.env.VITE_BACKGROUND_FOLDER}/${currentBackground})`
|
||||
}} >
|
||||
{
|
||||
config && (
|
||||
@@ -437,6 +475,80 @@ export default function Operator() {
|
||||
</section>
|
||||
</section>
|
||||
<Border />
|
||||
<VoiceElement
|
||||
src={voiceSrc}
|
||||
handleAduioStateChange={handleAduioStateChange}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function MusicElement() {
|
||||
const [enableMusic, setEnableMusic] = useState(false)
|
||||
const { i18n } = useI18n()
|
||||
const musicIntroRef = useRef(null)
|
||||
const musicLoopRef = useRef(null)
|
||||
const [background,] = useAtom(backgroundAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (musicIntroRef.current && musicIntroRef.current) {
|
||||
musicIntroRef.current.volume = 0.5
|
||||
musicLoopRef.current.volume = 0.5
|
||||
}
|
||||
}, [musicIntroRef, musicLoopRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableMusic || background) {
|
||||
musicIntroRef.current.pause()
|
||||
musicLoopRef.current.pause()
|
||||
}
|
||||
}, [enableMusic, background])
|
||||
|
||||
useEffect(() => {
|
||||
if (background && enableMusic) {
|
||||
const introOgg = musicMapping[background].intro
|
||||
const intro = `./chen/assets/${import.meta.env.VITE_MUSIC_FOLDER}/${introOgg}`
|
||||
const loop = `./chen/assets/${import.meta.env.VITE_MUSIC_FOLDER}/${musicMapping[background].loop}`
|
||||
musicLoopRef.current.src = loop
|
||||
if (introOgg) {
|
||||
musicIntroRef.current.src = intro || loop
|
||||
} else {
|
||||
musicLoopRef.current.play()
|
||||
}
|
||||
}
|
||||
}, [background, enableMusic])
|
||||
|
||||
const handleIntroTimeUpdate = useCallback(() => {
|
||||
if (musicIntroRef.current.currentTime >= musicIntroRef.current.duration - 0.3) {
|
||||
musicIntroRef.current.pause()
|
||||
musicLoopRef.current.play()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLoopTimeUpdate = useCallback(() => {
|
||||
if (musicLoopRef.current.currentTime >= musicLoopRef.current.duration - 0.3) {
|
||||
musicLoopRef.current.currentTime = 0
|
||||
musicLoopRef.current.play()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section>
|
||||
<section
|
||||
className={classes.titleWithSwitch}
|
||||
onClick={() => setEnableMusic(!enableMusic)}
|
||||
>
|
||||
<section className={classes.text}>{i18n('music')}</section>
|
||||
<section className={classes.switch}>
|
||||
<Switch on={enableMusic} />
|
||||
</section>
|
||||
</section>
|
||||
<audio ref={musicIntroRef} preload="auto" autoPlay onTimeUpdate={() => handleIntroTimeUpdate()}>
|
||||
<source type="audio/ogg" />
|
||||
</audio>
|
||||
<audio ref={musicLoopRef} preload="auto" onTimeUpdate={() => handleLoopTimeUpdate()}>
|
||||
<source type="audio/ogg"/>
|
||||
</audio>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
padding-top: 10rem;
|
||||
font-size: 3rem;
|
||||
gap: 2rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.spine {
|
||||
@@ -38,6 +39,7 @@
|
||||
@media (max-width: 768px) {
|
||||
.main {
|
||||
padding-top: 6rem;
|
||||
max-height: calc(100vh - 6rem);
|
||||
}
|
||||
.content {
|
||||
font-size: 2rem;
|
||||
@@ -46,6 +48,7 @@
|
||||
@media (max-width: 480px) {
|
||||
.main {
|
||||
padding-top: 4rem;
|
||||
max-height: calc(100vh - 4rem);
|
||||
}
|
||||
.content {
|
||||
font-size: 1.5rem;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { read } from './yaml.js';
|
||||
import AssetsProcessor from './assets_processor.js'
|
||||
import EnvGenerator from './env_generator.js'
|
||||
|
||||
export default function ({ backgrounds }) {
|
||||
export default function ({ backgrounds, musicMapping }) {
|
||||
const extractedFolder = path.join(__projectRoot, __config.folder.operator, '_directory')
|
||||
const targetFolder = path.join(__projectRoot, __config.folder.release, __config.folder.directory);
|
||||
const sourceFolder = path.join(__projectRoot, __config.folder.operator);
|
||||
@@ -78,6 +78,12 @@ export default function ({ backgrounds }) {
|
||||
}, {
|
||||
key: "error_files",
|
||||
value: JSON.stringify(__config.directory.error).replace('#', '%23')
|
||||
}, {
|
||||
key: "music_folder",
|
||||
value: __config.folder.music
|
||||
}, {
|
||||
key: "music_mapping",
|
||||
value: JSON.stringify(musicMapping)
|
||||
}
|
||||
]), path.join(__projectRoot, 'directory', '.env'))
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export default class Music {
|
||||
}
|
||||
|
||||
success() {
|
||||
this.changeMusic(window.settings.currentBackground)
|
||||
if (this.#currentMusic === null) this.changeMusic(window.settings.currentBackground)
|
||||
}
|
||||
|
||||
changeMusic(name) {
|
||||
|
||||
@@ -93,10 +93,10 @@ window.wallpaperPropertyListener = {
|
||||
}
|
||||
if (properties.music_title) {
|
||||
window.music.useMusic = properties.music_title.value
|
||||
window.music.changeMusic(properties.music_selection.value)
|
||||
window.settings.functionInsights("useMusic", Object.keys(properties) !== 1)
|
||||
}
|
||||
if (properties.music_selection) {
|
||||
// TODO: not working
|
||||
window.music.changeMusic(properties.music_selection.value)
|
||||
window.settings.functionInsights("music_selection", Object.keys(properties) !== 1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user