diff --git a/src/components/settings.js b/src/components/settings.js index a9503fa..5a40610 100644 --- a/src/components/settings.js +++ b/src/components/settings.js @@ -234,8 +234,9 @@ export default class Settings { } elementPosition(el, x, y) { - const elWidth = getComputedStyle(el).width - const elHeight = getComputedStyle(el).height + const computedStyle = getComputedStyle(el) + const elWidth = computedStyle.width + const elHeight = computedStyle.height const windowWidth = window.innerWidth const windowHeight = window.innerHeight const xRange = windowWidth - parseInt(elWidth) @@ -352,8 +353,8 @@ export default class Settings {
- -
+ + -
-
+
@@ -380,11 +379,23 @@ export default class Settings { ${this.#updateOptions("subtitle_lang_select", window.voice.subtitleLanguages)}
+
+ + + +
+
+ + + +
- + +
+
@@ -445,6 +456,13 @@ export default class Settings { return value } + #getCurrentOptions(id, value) { + const e = document.getElementById(id); + const options = [...e] + const toSelecteIndex = options.findIndex(i => options.find(o => o.value === value) === i) + e.selectedIndex = toSelecteIndex; + } + #addEventListeners() { const _this = this; const listeners = [ @@ -526,6 +544,7 @@ export default class Settings { id: "voice_lang_select", event: "change", handler: e => { window.voice.language = e.currentTarget.value _this.#updateOptions("subtitle_lang_select", window.voice.subtitleLanguages) + _this.#getCurrentOptions("subtitle_lang_select", window.voice.subtitleLanguage) } }, { id: "voice_idle_duration_input", event: "change", handler: e => { @@ -542,6 +561,26 @@ export default class Settings { } }, { id: "subtitle_lang_select", event: "change", handler: e => window.voice.subtitleLanguage = e.currentTarget.value + }, { + id: "subtitle_padding_x_slider", event: "input", handler: e => { + _this.#sync(e.currentTarget, "subtitle_padding_x_input"); + window.voice.subtitleX = e.currentTarget.value + } + }, { + id: "subtitle_padding_x_input", event: "change", handler: e => { + _this.#sync(e.currentTarget, "subtitle_padding_x_slider"); + window.voice.subtitleX = e.currentTarget.value + } + }, { + id: "subtitle_padding_y_slider", event: "input", handler: e => { + _this.#sync(e.currentTarget, "subtitle_padding_y_input"); + window.voice.subtitleY = e.currentTarget.value + } + }, { + id: "subtitle_padding_y_input", event: "change", handler: e => { + _this.#sync(e.currentTarget, "subtitle_padding_y_slider"); + window.voice.subtitleY = e.currentTarget.value + } }, { id: "voice_actor", event: "click", handler: e => { window.voice.useVoiceActor = e.currentTarget.checked; diff --git a/src/components/voice.css b/src/components/voice.css index e69de29..4a5cec4 100644 --- a/src/components/voice.css +++ b/src/components/voice.css @@ -0,0 +1,73 @@ +#voice_box { + position: fixed; + left: 0; + bottom: 0; + z-index: 1; + width: 480px; + opacity: 0; + margin: 16px; + font-family: 'Roboto', sans-serif; + transition: opacity 0.5s cubic-bezier(0.65, 0.05, 0.36, 1); +} + +.voice-title { + background-color: #9e9e9e; + color: black; + display: inline-block; + position: absolute; + top: -12px; + left: -8px; + padding: 2px 8px; + font-size: 14px; + min-width: 120px; + box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.5); + z-index: 1; +} + +.voice-subtitle { + background-color: rgba(0, 0, 0, 0.65); + color: white; + padding: 16px; + font-size: 18px; + box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.5); + position: relative; +} + +.voice-triangle { + position: absolute; + bottom: 0px; + right: 8px; + width: 0; + height: 0; + border-style: solid; + border-width: 8px 8px 8px 8px; + border-color: white transparent transparent transparent; +} + +.voice-actor { + margin-top: 10px; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; +} + +.voice-actor-icon { + height: 32px; + width: 32px; + background-color: rgba(0, 0, 0, 1); + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAXCAYAAAAGAx/kAAACGElEQVQ4jY2UO4sUQRSFv+6e3hkf6yqi62LiD1DRwGRFZDUTRTAVMTHzHxhosBgr+IhMzIXFRzKJYiQoCLqroYGCgiDK6K6z8+hj0Kd2yrZn8EJRza06X91761Ynkphgh4ETwBB4BrwZu1NS3WhJui1pTSNblXTLa/9oxoEWNd4W/xc0K6kzAfRd0lxV16jJdg6YBtaAB8AvoPA4D+wEZoEvsSgGJYAiXwf4EEFyYOC1qYqmFhQ2bQEO+MYKILMv1tWCikqKm4CPwKoF00DTa6pqkqiP9gIHgR3AWaDvdJIoip4jewh8BVaAz1XQOeAKcNcp9O0fAqlTLgzsApeBa8BSNbUUaAFvgVdMtnlHmsXiEHrHpw+iaOps4BRlDUASQDnw3uA9QHsCqA3sMmjFmhRJmaSmO/SmpCeSjktarunqd5IWJLUl3bCmKSlLJGWuVQbMAHcoi3oPWAAOOerXwFPgErAVuEjZGkNgkEhKDco9bwOuG/Ac+OS67ANOGngV+OYDewEUqt+gvLWmxxHgFHDapz52fV4C64Z3PW+AUsPyCJYCP4FHPuQMsNlRrHv0DRo2XP3Ccw/4Tdl8U2GTb6trYYimYNQ6Gw0pYDuwn1E7hJR3W3TMvp5hKbAM/IC/n8hR4L6hqSPJGT3QjPIfFX4nDeAC8CIGhTrBqNsz16plWN/R9KJShGzUiCBBnFS+FYmCP41gBaA/cImMsTl24NIAAAAASUVORK5CYII='); + background-position: center; + background-repeat: no-repeat; +} + +.voice-actor-name { + height: 16px; + font-size: 16px; + line-height: 16px; + background-color: rgba(0, 0, 0, 0.3); + color: white; + display: inline-block; + padding: 8px; +} \ No newline at end of file diff --git a/src/components/voice.js b/src/components/voice.js index 6bd8861..e13714c 100644 --- a/src/components/voice.js +++ b/src/components/voice.js @@ -2,288 +2,373 @@ import charword_table from '!/charword_table.json' import '@/components/voice.css' export default class Voice { - #el - #widgetEl - #audioEl = new Audio() - #audioElId = 'voice-audio' - #defaultVoiceLang = "JP" - #defaultRegion = charword_table.config.default_region - #defaultIdleDuration = 10 * 60 * 1000 - #defaultNextDuration = 3 * 60 * 1000 - #voiceLang = this.#defaultVoiceLang - #voiceLanguages = Object.keys(this.#getCVInfo(this.#defaultRegion)) - #subtitleLang = this.#defaultRegion - #useSubtitle = false - #useVoice = false - #useVoiceActor = false - #currentVoiceId = null - #lastVoiceId = null - #idleListener = -1 - #idleDuration = this.#defaultIdleDuration - #nextListener = -1 - #nextDuration = this.#defaultNextDuration - #lastClickToNext = false - #voiceFolderObject = this.#getVoiceFolderObject() - - constructor(el, widgetEl) { - this.#el = el - this.#widgetEl = widgetEl - } + #el + #widgetEl + #audioEl = new Audio() + #audioElId = 'voice-audio' + #defaultVoiceLang = "CN_MANDARIN" + #defaultRegion = charword_table.config.default_region + #defaultIdleDuration = 10 * 60 * 1000 + #defaultNextDuration = 3 * 60 * 1000 + #voiceLang = this.#defaultVoiceLang + #voiceLanguages = Object.keys(this.#getCVInfo(this.#defaultRegion)) + #subtitleLang = this.#defaultRegion + #useSubtitle = true + #useVoice = false + #useVoiceActor = false + #isPlaying = false + #currentVoiceId = null + #lastVoiceId = null + #idleListener = -1 + #idleDuration = this.#defaultIdleDuration + #nextListener = -1 + #nextDuration = this.#defaultNextDuration + #lastClickToNext = false + #voiceFolderObject = this.#getVoiceFolderObject() + #voiceList = Object.keys(this.#getVoices()) + #defaultSubtitleX = 0 + #defaultSubtitleY = 0 + #subtitleX = this.#defaultSubtitleX + #subtitleY = this.#defaultSubtitleY - init() { - this.#insertHTML() - this.#audioEl = document.getElementById(this.#audioElId) - } + constructor(el, widgetEl) { + this.#el = el + this.#widgetEl = widgetEl + } - success() { - this.#playEntryVoice() - this.#initNextVoiceTimer() - this.#widgetEl.addEventListener('click', e => { - this.#lastClickToNext = true - this.#nextVoice() + init() { + this.#insertHTML() + this.#audioEl = document.getElementById(this.#audioElId) + } + + success() { + this.#playEntryVoice() + this.#initNextVoiceTimer() + this.#widgetEl.addEventListener('click', e => { + this.#lastClickToNext = true + this.#nextVoice() + }) + document.addEventListener('mousemove', e => { + if (this.#idleListener === -1) { + this.#initIdleVoiceTimer() + } + }) + } + + /** + * @param {boolean} show + */ + set useSubtitle(show) { + this.#useSubtitle = show + this.#el.hidden = !show + } + + get useSubtitle() { + return this.#useSubtitle + } + + /** + * @param {boolean} show + */ + set useVoice(show) { + this.#useVoice = show + this.#el.hidden = !show + this.#playEntryVoice() + if (!show && this.#isPlaying) { + this.#audioEl.pause() + } + } + + get useVoice() { + return this.#useVoice + } + + /** + * @param {boolean} show + */ + set useVoiceActor(show) { + this.#useVoiceActor = show + document.getElementById('voice_actor_box').hidden = !show + } + + get useVoiceActor() { + return this.#useVoiceActor + } + + /** + * @param {string} lang + */ + set subtitleLanguage(lang) { + if (this.#getSubtitleLanguages().includes(lang)) { + this.#subtitleLang = lang + } else { + this.#subtitleLang = this.#defaultRegion + } + } + + get subtitleLanguage() { + return this.#subtitleLang + } + + get subtitleLanguages() { + return this.#getSubtitleLanguages() + } + + /** + * @param {int} x + */ + set subtitleX(x) { + this.#subtitleX = x + this.#updateSubtitlePosition() + } + + get subtitleX() { + return this.#subtitleX + } + + /** + * @param {int} y + */ + set subtitleY(y) { + this.#subtitleY = y - 100 + this.#updateSubtitlePosition() + } + + get subtitleY() { + return this.#subtitleY + 100 + } + + #updateSubtitlePosition() { + window.settings.elementPosition(this.#el, this.#subtitleX, this.#subtitleY) + } + + /** + * @param {string} lang + */ + set language(lang) { + if (this.#voiceLanguages.includes(lang)) { + this.#voiceLang = lang + } else { + this.#voiceLang = this.#defaultVoiceLang + } + const availableSubtitleLang = this.#getSubtitleLanguages() + if (!availableSubtitleLang.includes(this.#subtitleLang)) { + this.#subtitleLang = availableSubtitleLang[0] + } + } + + get language() { + return this.#voiceLang + } + + get languages() { + return this.#voiceLanguages + } + + /** + * @param {int} duration + */ + set idleDuration(duration) { + clearInterval(this.#idleListener) + if (duration !== 0) { + this.#idleDuration = duration * 60 * 1000 + this.#initIdleVoiceTimer() + } + } + + get idleDuration() { + return this.#idleDuration / 60 / 1000 + } + + /** + * @param {int} duration + */ + set nextDuration(duration) { + clearInterval(this.#nextListener) + if (duration !== 0) { + this.#nextDuration = duration * 60 * 1000 + this.#initNextVoiceTimer() + } + } + + get nextDuration() { + return this.#nextDuration / 60 / 1000 + } + + #initIdleVoiceTimer() { + this.#idleListener = setInterval(() => { + this.#playSpecialVoice("闲置") + clearInterval(this.#idleListener) + this.#idleListener = -1 + }, this.#idleDuration) + } + + #initNextVoiceTimer() { + this.#nextListener = setInterval(() => { + if (!this.#lastClickToNext) { + this.#nextVoice() + } + }, this.#nextDuration) + } + + #nextVoice() { + const voiceId = () => { + const id = this.#voiceList[Math.floor((Math.random() * this.#voiceList.length))] + return id === this.#lastVoiceId ? voiceId() : id + } + this.#playVoice(voiceId()) + } + + #playEntryVoice() { + this.#playSpecialVoice("问候") + } + + #setCurrentSubtitle(id) { + if (id === null) { + setTimeout(() => { + if (this.#isPlaying) return + this.#el.style.opacity = 1 + }, 5 * 1000); + return + } + const subtitle = this.#getSubtitleById(id) + const title = subtitle.title + const content = subtitle.text + const cvInfo = this.#getCVInfoByVoiceLang()[this.#voiceLang][this.subtitleLanguage] + document.getElementById('voice_title').innerText = title + document.getElementById('voice_subtitle').innerText = content + this.#el.style.opacity = 1 + document.getElementById('voice_actor_name').innerText = cvInfo.join('') + } + + #playVoice(id) { + if (!this.useVoice) return + this.#lastVoiceId = this.#currentVoiceId + this.#currentVoiceId = id + this.#audioEl.src = `./assets/${this.#getVoiceFoler() + }/${id}.wav` + let startPlayPromise = this.#audioEl.play() + if (startPlayPromise !== undefined) { + startPlayPromise + .then(() => { + this.#isPlaying = true + const audioEndedFunc = () => { + this.#isPlaying = false + this.#audioEl.removeEventListener('ended', audioEndedFunc) + if (this.#currentVoiceId !== id) return + this.#setCurrentSubtitle(null) + this.#lastClickToNext = false + } + + this.#audioEl.addEventListener('ended', audioEndedFunc) + this.#setCurrentSubtitle(id) }) - document.addEventListener('mousemove', e => { - if (this.#idleListener === -1) { - this.#initIdleVoiceTimer() - } + .catch(() => { + return }) } + } - /** - * @param {boolean} show - */ - set useSubtitle(show) { - this.#useSubtitle = show + #playSpecialVoice(matcher) { + const voiceId = this.#getSpecialVoiceId(matcher) + this.#playVoice(voiceId) + } + + #getVoiceFoler() { + const folderObject = this.#voiceFolderObject + return `${folderObject.main}/${folderObject.sub.find(e => e.lang === this.#voiceLang).name}` + } + + #getSpecialVoiceId(matcher) { + const voices = this.#getVoices() + const voiceId = Object.keys(voices).find(e => voices[e].title === matcher) + return voiceId + } + + #getVoices() { + return charword_table.operator.voice[this.#defaultRegion][this.#getWordKeyByVoiceLang()[this.#defaultVoiceLang]] + } + + #getSubtitleById(id) { + return charword_table.operator.voice[this.#subtitleLang][this.#getWordKeyByVoiceLang()[this.#voiceLang]][id] + } + + #getVoiceFolderObject() { + const folderObject = JSON.parse(import.meta.env.VITE_VOICE_FOLDERS) + const languagesCopy = this.#voiceLanguages.slice() + const customVoiceName = languagesCopy.filter(i => !folderObject.sub.map(e => e.lang).includes(i))[0] + folderObject.sub = folderObject.sub.map(e => { + return { + name: e.name, + lang: e.lang === "CUSTOM" ? customVoiceName : e.lang + } + }) + return folderObject + } + + /** + * @returns the cvInfo in the region's language + */ + #getCVInfo(region) { + const infoArray = Object.values(charword_table.operator.info[region]) + // combine the infoArray + let output = {} + for (const info of infoArray) { + output = { + ...output, + ...info + } } + return output + } - /** - * @param {boolean} show - */ - set useVoice(show) { - this.#useVoice = show - } - - /** - * @param {boolean} show - */ - set useVoiceActor(show) { - this.#useVoiceActor = show - } - - /** - * @param {string} lang - */ - set subtitleLanguage(lang) { - if (this.#getSubtitleLanguages().includes(lang)) { - this.#subtitleLang = lang - } else { - this.#subtitleLang = this.#defaultRegion + /** + * @returns the cvInfo corresponsing to the voice language + */ + #getCVInfoByVoiceLang() { + const languages = {} + for (const lang of Object.keys(charword_table.operator.info)) { + const cvInfo = this.#getCVInfo(lang) + for (const [voiceLanguage, cvArray] of Object.entries(cvInfo)) { + if (languages[voiceLanguage] === undefined) { + languages[voiceLanguage] = {} } + languages[voiceLanguage][lang] = cvArray + } } + return languages + } - get subtitleLanguage() { - return this.#subtitleLang + #getWordKeyByVoiceLang() { + const output = {} + for (const [wordKey, wordKeyDict] of Object.entries(charword_table.operator.info[this.#defaultRegion])) { + for (const lang of Object.keys(wordKeyDict)) { + output[lang] = wordKey + } } + return output + } - get subtitleLanguages() { - return this.#getSubtitleLanguages() - } + #getSubtitleLanguages() { + return Object.keys(this.#getCVInfoByVoiceLang()[this.#voiceLang]) + } - /** - * @param {string} lang - */ - set language(lang) { - if (this.#voiceLanguages.includes(lang)) { - this.#voiceLang = lang - } else { - this.#voiceLang = this.#defaultVoiceLang - } - const availableSubtitleLang = this.#getSubtitleLanguages() - if (!availableSubtitleLang.includes(this.#subtitleLang)) { - this.#subtitleLang = availableSubtitleLang[0] - } - } - - get language() { - return this.#voiceLang - } - - get languages() { - return this.#voiceLanguages - } - - /** - * @param {int} duration - */ - set idleDuration(duration) { - clearInterval(this.#idleListener) - if (duration !== 0) { - this.#idleDuration = duration * 60 * 1000 - this.#initIdleVoiceTimer() - } - } - - get idleDuration() { - return this.#idleDuration / 60 / 1000 - } - - /** - * @param {int} duration - */ - set nextDuration(duration) { - clearInterval(this.#nextListener) - if (duration !== 0) { - this.#nextDuration = duration * 1000 - this.#initNextVoiceTimer() - } - } - - get nextDuration() { - return this.#nextDuration / 60 / 1000 - } - - #initIdleVoiceTimer() { - this.#idleListener = setInterval(() => { - this.#playSpecialVoice("闲置") - clearInterval(this.#idleListener) - this.#idleListener = -1 - }, this.#idleDuration) - } - - #initNextVoiceTimer() { - this.#nextListener = setInterval(() => { - if (!this.#lastClickToNext) { - this.#nextVoice() - } - }, this.#nextDuration) - } - - #nextVoice() { - this.#playVoice("CN_001") - } - - #playEntryVoice() { - this.#playSpecialVoice("问候") - } - - #setCurrentSubtitle(id) { - console.log(id, this.#getSubtitleById(id)) - } - - #playVoice(id) { - this.#currentVoiceId = id - this.#audioEl.src = `./assets/${this.#getVoiceFoler() -}/${id}.wav` - let startPlayPromise = this.#audioEl.play() - if (startPlayPromise !== undefined) { - startPlayPromise - .then(() => { - const audioEndedFunc = () => { - this.#lastVoiceId = this.#currentVoiceId - this.#currentVoiceId = null - this.#setCurrentSubtitle(null) - this.#lastClickToNext = false - this.#audioEl.removeEventListener('ended', audioEndedFunc) - } - this.#audioEl.addEventListener('ended', audioEndedFunc) - this.#setCurrentSubtitle(id) - }) - .catch(() => { - return - }) - } - } - - #playSpecialVoice(matcher) { - const voiceId = this.#getSpecialVoiceId(matcher) - this.#playVoice(voiceId) - } - - #getVoiceFoler() { - const folderObject = this.#voiceFolderObject - return `${folderObject.main}/${folderObject.sub.find(e => e.lang === this.#voiceLang).name}` - } - - #getSpecialVoiceId(matcher) { - const voices = this.#getVoices() - const voiceId = Object.keys(voices).find(e => voices[e].title === matcher) - return voiceId - } - - #getVoices() { - return charword_table.operator.voice[this.#defaultRegion][this.#getWordKeyByVoiceLang()[this.#defaultVoiceLang]] - } - - #getSubtitleById(id) { - return charword_table.operator.voice[this.#subtitleLang][this.#getWordKeyByVoiceLang()[this.#voiceLang]][id] - } - - #getVoiceFolderObject() { - const folderObject = JSON.parse(import.meta.env.VITE_VOICE_FOLDERS) - const languagesCopy = this.#voiceLanguages.slice() - const customVoiceName = languagesCopy.filter(i => !folderObject.sub.map(e => e.lang).includes(i))[0] - folderObject.sub = folderObject.sub.map(e => { - return { - name: e.name, - lang: e.lang === "CUSTOM" ? customVoiceName : e.lang - } - }) - return folderObject - } - - /** - * @returns the cvInfo in the region's language - */ - #getCVInfo(region) { - const infoArray = Object.values(charword_table.operator.info[region]) - // combine the infoArray - let output = {} - for (const info of infoArray) { - output = { - ...output, - ...info - } - } - return output - } - - /** - * @returns the cvInfo corresponsing to the voice language - */ - #getCVInfoByVoiceLang() { - const languages = {} - for (const lang of Object.keys(charword_table.operator.info)) { - const cvInfo = this.#getCVInfo(lang) - for (const [voiceLanguage, cvArray] of Object.entries(cvInfo)) { - if (languages[voiceLanguage] === undefined) { - languages[voiceLanguage] = {} - } - languages[voiceLanguage][lang] = cvArray - } - } - return languages - } - - #getWordKeyByVoiceLang() { - const output = {} - for (const [wordKey, wordKeyDict] of Object.entries(charword_table.operator.info[this.#defaultRegion])) { - for (const lang of Object.keys(wordKeyDict)) { - output[lang] = wordKey - } - } - return output - } - - #getSubtitleLanguages() { - return Object.keys(this.#getCVInfoByVoiceLang()[this.#voiceLang]) - } - - #insertHTML() { - this.#el.innerHTML = ` - - ` - } + #insertHTML() { + this.#el.innerHTML = ` + +
+
+
+
+
+
+
+ + ` + } } \ No newline at end of file diff --git a/src/index.js b/src/index.js index e15e92b..1de4077 100644 --- a/src/index.js +++ b/src/index.js @@ -7,7 +7,7 @@ import Voice from '@/components/voice' document.querySelector('#app').innerHTML = `
- +
` -window.voice = new Voice(document.querySelector('#voice'), document.querySelector('#widget-wrapper')) +window.voice = new Voice(document.querySelector('#voice_box'), document.querySelector('#widget-wrapper')) window.voice.init() window.settings = new Settings(document.querySelector('#settings'), document.querySelector('#logo')) document.title = import.meta.env.VITE_TITLE