From 9b3237f46f58aeb178f1a5a3975ba1be89b01932 Mon Sep 17 00:00:00 2001 From: Haoyu Xu Date: Thu, 16 Mar 2023 19:05:31 -0400 Subject: [PATCH] refactor(directory): use VoiceElement instead of useAudio --- Version | 2 +- changelogs.yaml | 8 +- config/_project_json.yaml | 6 + directory/.env | 4 +- directory/Version | 2 +- directory/src/component/switch.jsx | 6 +- directory/src/component/voice.jsx | 53 ++++++ directory/src/libs/voice.jsx | 69 ------- directory/src/routes/Error.jsx | 44 +++-- directory/src/routes/path/Home.jsx | 72 +++++--- directory/src/routes/path/Operator.jsx | 200 ++++++++++++++++----- directory/src/scss/error/Error.module.scss | 3 + libs/directory.js | 8 +- src/components/music.js | 2 +- src/libs/wallpaper_engine.js | 2 +- 15 files changed, 324 insertions(+), 157 deletions(-) create mode 100644 directory/src/component/voice.jsx delete mode 100644 directory/src/libs/voice.jsx diff --git a/Version b/Version index 8cf6caf..3f4e15a 100644 --- a/Version +++ b/Version @@ -1 +1 @@ -3.4.1 \ No newline at end of file +3.4.20 \ No newline at end of file diff --git a/changelogs.yaml b/changelogs.yaml index a75e4e2..2edccdc 100644 --- a/changelogs.yaml +++ b/changelogs.yaml @@ -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 diff --git a/config/_project_json.yaml b/config/_project_json.yaml index 66aaf81..1951376 100644 --- a/config/_project_json.yaml +++ b/config/_project_json.yaml @@ -34,6 +34,7 @@ localization: ui_privacy_do_not_track: Send usage data ui_music_title:

📝 Music


ui_music_notice: Please adjust the 'Offset' value if you notice audio cutoff + ui_music_notice1: 'bg_rhodes_day.png' and 'operator_bg.png' use the same music, it is not a bug. 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:

🎵 音乐


ui_music_notice: 如若发现音频截止,请调节 '弥补' 数值 + ui_music_notice1: 'bg_rhodes_day.png' 和 'operator_bg.png' 使用同样的音乐,并非Bug 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 diff --git a/directory/.env b/directory/.env index ede81d2..2ae0018 100644 --- a/directory/.env +++ b/directory/.env @@ -4,4 +4,6 @@ VITE_VOICE_FOLDERS={"main":"voice","sub":[{"name":"jp","lang":"JP"},{"name":"cn" 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"} \ No newline at end of file +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"}} \ No newline at end of file diff --git a/directory/Version b/directory/Version index c678b02..2fa3901 100644 --- a/directory/Version +++ b/directory/Version @@ -1 +1 @@ -1.0.18 \ No newline at end of file +1.0.22 \ No newline at end of file diff --git a/directory/src/component/switch.jsx b/directory/src/component/switch.jsx index c02a531..e3998e5 100644 --- a/directory/src/component/switch.jsx +++ b/directory/src/component/switch.jsx @@ -16,7 +16,11 @@ export default function Switch(props) { return (
props.handleOnClick()} + onClick={() => { + if (props.handleOnClick) { + props.handleOnClick(!on) + } + }} > {i18n(props.text)}
diff --git a/directory/src/component/voice.jsx b/directory/src/component/voice.jsx new file mode 100644 index 0000000..c5e86ba --- /dev/null +++ b/directory/src/component/voice.jsx @@ -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 ( + + ) +} +VoiceElement.propTypes = { + src: PropTypes.string, + handleAduioStateChange: PropTypes.func, + replay: PropTypes.bool, +} \ No newline at end of file diff --git a/directory/src/libs/voice.jsx b/directory/src/libs/voice.jsx deleted file mode 100644 index a7e2a5b..0000000 --- a/directory/src/libs/voice.jsx +++ /dev/null @@ -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, - } - -} \ No newline at end of file diff --git a/directory/src/routes/Error.jsx b/directory/src/routes/Error.jsx index 33c46e9..66f153d 100644 --- a/directory/src/routes/Error.jsx +++ b/directory/src/routes/Error.jsx @@ -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} /> +
); diff --git a/directory/src/routes/path/Home.jsx b/directory/src/routes/path/Home.jsx index 4795f85..fef6515 100644 --- a/directory/src/routes/path/Home.jsx +++ b/directory/src/routes/path/Home.jsx @@ -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 (
{ @@ -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() { ) }) } - +
) } -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} >
playVoice()} + onMouseEnter={() => handleVoicePlay(`/${item.link}/assets/${JSON.parse(import.meta.env.VITE_VOICE_FOLDERS).main}/${import.meta.env.VITE_APP_VOICE_URL}`)} >
@@ -141,10 +153,10 @@ function OperatorElement({ item, hidden }) {
) - }, [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 ( + + ) +} + +VoiceSwitchElement.propTypes = { + src: PropTypes.string, + replay: PropTypes.bool, + handleAduioStateChange: PropTypes.func, } function ImageElement({ item }) { diff --git a/directory/src/routes/path/Operator.jsx b/directory/src/routes/path/Operator.jsx index c98fc25..b6ae9ba 100644 --- a/directory/src/routes/path/Operator.jsx +++ b/directory/src/routes/path/Operator.jsx @@ -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`, - () => { - lastVoiceId = currentVoiceId - } - ) + 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: }, { name: 'backgrounds', options: backgrounds.map((item) => { @@ -348,6 +379,13 @@ export default function Operator() { }}> { spineSettings.map((item) => { + if (item.el) { + return ( +
+ {item.el} +
+ ) + } if (item.options.length === 0) return null return (
@@ -417,7 +455,7 @@ export default function Operator() {
{ config && ( @@ -426,7 +464,7 @@ export default function Operator() { }
{currentVoiceId && subtitleObj && ( -
+
{subtitleObj[currentVoiceId]?.title}
{subtitleObj[currentVoiceId]?.text} @@ -437,6 +475,80 @@ export default function Operator() {
+ +
+ ) +} + +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 ( +
+
setEnableMusic(!enableMusic)} + > +
{i18n('music')}
+
+ +
+
+ +
) } \ No newline at end of file diff --git a/directory/src/scss/error/Error.module.scss b/directory/src/scss/error/Error.module.scss index 044f340..9fe0eb3 100644 --- a/directory/src/scss/error/Error.module.scss +++ b/directory/src/scss/error/Error.module.scss @@ -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; diff --git a/libs/directory.js b/libs/directory.js index b9e06fe..67944f9 100644 --- a/libs/directory.js +++ b/libs/directory.js @@ -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')) diff --git a/src/components/music.js b/src/components/music.js index 5ab7809..2cbd513 100644 --- a/src/components/music.js +++ b/src/components/music.js @@ -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) { diff --git a/src/libs/wallpaper_engine.js b/src/libs/wallpaper_engine.js index 5567b6e..6e8d358 100644 --- a/src/libs/wallpaper_engine.js +++ b/src/libs/wallpaper_engine.js @@ -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) }