feat(showcase): add operator voice

This commit is contained in:
Haoyu Xu
2023-02-10 17:54:14 -05:00
parent 36afd6d786
commit 59f113e741
10 changed files with 123 additions and 34 deletions

View File

@@ -2,9 +2,21 @@ folder:
operator: ./operator/
release: ./release/
background: background
voice: voice
directory: _assets
share: _share
voice:
main: voice
sub:
- name: jp
lang: JP
- name: cn
lang: CN_MANDARIN
- name: en
lang: EN
- name: kr
lang: KR
- name: custom
lang: CUSTOM
operators:
chen: !include config/chen.yaml
dusk: !include config/dusk.yaml

View File

@@ -23,6 +23,7 @@ export default class EnvGenerator {
`VITE_IMAGE_HEIGHT=2048`,
`VITE_BACKGROUND_FILES=${JSON.stringify(this.#assets.backgrounds)}`,
`VITE_BACKGROUND_FOLDER=${__config.folder.background}`,
`VITE_VOICE_FOLDERS=${JSON.stringify(__config.folder.voice)}`,
].join('\n')
}

View File

@@ -53,6 +53,15 @@ export async function copy(sourcePath, targetPath) {
return await fsP.copyFile(sourcePath, targetPath)
}
export async function copyDir(sourcePath, targetPath) {
if (!exists(sourcePath)) {
console.warn(`Source file ${sourcePath} does not exist.`)
return
}
mkdir(targetPath)
return await fsP.cp(sourcePath, targetPath, { recursive: true })
}
export function appendSync(content, filePath) {
return fs.appendFileSync(filePath, content, 'utf8');
}

View File

@@ -5,7 +5,9 @@ import { mkdir, writeSync } from './file.js'
import { appendMainConfig } from './append.js'
export default function init(operatorName, extractedDir) {
mkdir(extractedDir)
extractedDir.forEach((dir) => {
mkdir(dir)
})
const date = new Date()
const template = readYAML(path.join(__dirname, 'config', '_template.yaml'))
template.link = operatorName

View File

@@ -5,7 +5,7 @@ import { fork } from 'child_process';
import getConfig from './libs/config.js'
import ProjectJson from './libs/project_json.js'
import EnvGenerator from './libs/env_generator.js'
import { write, rmdir, copy, writeSync } from './libs/file.js'
import { write, rmdir, copy, writeSync, copyDir } from './libs/file.js'
import AssetsProcessor from './libs/assets_processor.js'
import init from './libs/initializer.js'
import directory from './libs/directory.js'
@@ -54,6 +54,7 @@ async function main() {
const OPERATOR_RELEASE_FOLDER = path.join(__dirname, __config.folder.release, OPERATOR_NAME)
const SHOWCASE_PUBLIC_ASSSETS_FOLDER = path.join(OPERATOR_RELEASE_FOLDER, "assets")
const EXTRACTED_FOLDER = path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, 'extracted')
const VOICE_FOLDERS = __config.folder.voice.sub.map((sub) => path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, __config.folder.voice.main, sub.name))
const OPERATOR_SHARE_FOLDER = path.join(OPERATOR_SOURCE_FOLDER, __config.folder.share)
/**
@@ -63,7 +64,7 @@ async function main() {
*/
switch (op) {
case 'init':
init(OPERATOR_NAME, EXTRACTED_FOLDER)
init(OPERATOR_NAME, [EXTRACTED_FOLDER, ...VOICE_FOLDERS])
process.exit(0)
case 'readme':
appendReadme(OPERATOR_NAME)
@@ -115,6 +116,16 @@ async function main() {
copy(path.join(file.source, file.filename), path.join(file.target, file.filename))
})
const foldersToCopy = [
{
source: path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, __config.folder.voice.main),
target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER, __config.folder.voice.main)
}
]
foldersToCopy.forEach((folder) => {
copyDir(folder.source, folder.target)
})
const envPath = path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, '.env')
writeSync((new EnvGenerator(OPERATOR_NAME, {
backgrounds

View File

@@ -55,6 +55,7 @@ export default function spinePlayer(el) {
entry.mixDuration = 0.3;
widget.animationState.addAnimation(0, "Idle", true, 0);
}
window.voice.success()
window.settings.success()
},
})

View File

@@ -38,6 +38,8 @@ export default class Settings {
#logoX = this.#defaultLogoX
#logoY = this.#defaultLogoY
#isInsightsInited = false
#doNotTrack = false
#lastFunctionInsights = null
constructor(el, logoEl) {
this.#el = el
@@ -65,22 +67,31 @@ export default class Settings {
success() {
this.loadViewport()
this.insights(false)
this.insights(false, false)
}
insights(flag) {
if (this.#isInsightsInited) return
window.umami.trackView(`/${import.meta.env.VITE_LINK}${flag ? "?steam" : ""}`);
insights(isWallpaperEngine, doNotTrack) {
this.#isInsightsInited = true
if (this.#isInsightsInited || doNotTrack || import.meta.env.MODE === 'development') return
this.#doNotTrack = doNotTrack
window.umami?.trackView(`/${import.meta.env.VITE_LINK}${isWallpaperEngine ? "?steam" : ""}`);
}
functionInsights(functionName) {
if (!this.#isInsightsInited || this.#doNotTrack || import.meta.env.MODE === 'development' || functionName === this.#lastFunctionInsights) return
this.#lastFunctionInsights = functionName
window.umami?.trackEvent(`${functionName}`);
}
setFPS(value) {
this.#fps = value
this.spinePlayer.setFps(value)
this.functionInsights("setFPS")
}
setLogoDisplay(flag) {
this.#logoEl.hidden = flag;
this.functionInsights("setLogoDisplay")
}
#resize(_this, value) {
@@ -101,6 +112,7 @@ export default class Settings {
this.#logoEl.src = src
this.#resize()
this.#setLogoInvertFilter(invert_filter)
this.functionInsights("setLogo")
}
#readFile(e, onload, callback) {
@@ -131,23 +143,27 @@ export default class Settings {
setLogoRatio(value) {
this.#ratio = value
this.#resize(this, value)
this.functionInsights("setLogoRatio")
}
setLogoOpacity(value) {
this.#logoEl.style.opacity = value / 100
this.#opacity = value
this.functionInsights("setLogoOpacity")
}
setBackgoundImage(v) {
setBackgoundImage(v, skipInsights = false) {
document.body.style.backgroundImage = v
if (!skipInsights) this.functionInsights("setBackgoundImage");
}
setDefaultBackground(e) {
const backgroundURL = `url("${import.meta.env.BASE_URL}assets/${import.meta.env.VITE_BACKGROUND_FOLDER}/${e}")`
if (document.getElementById("custom_background_clear").disabled && !document.body.style.backgroundImage.startsWith("url(\"file:")) {
this.setBackgoundImage(backgroundURL)
this.setBackgoundImage(backgroundURL, true)
}
this.#defaultBackgroundImage = backgroundURL
this.functionInsights("setDefaultBackground")
}
setBackground(e) {
@@ -198,6 +214,7 @@ export default class Settings {
break;
}
this.loadViewport()
this.functionInsights("positionPadding")
}
positionReset() {
@@ -242,6 +259,7 @@ export default class Settings {
break;
}
this.elementPosition(this.#logoEl, this.#logoX, this.#logoY)
this.functionInsights("logoPadding")
}
logoReset() {

View File

@@ -3,11 +3,15 @@ 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
@@ -19,19 +23,22 @@ export default class Voice {
#nextListener = -1
#nextDuration = this.#defaultNextDuration
#lastClickToNext = false
#voiceFolderObject = this.#getVoiceFolderObject()
constructor(el) {
constructor(el, widgetEl) {
this.#el = el
this.#widgetEl = widgetEl
}
init() {
this.#playTitleVoice()
this.#insertHTML()
this.#audioEl = document.getElementById(this.#audioElId)
}
success() {
this.#playEntryVoice()
this.#initNextVoiceTimer()
document.addEventListener('click', e => {
this.#widgetEl.addEventListener('click', e => {
this.#lastClickToNext = true
this.#nextVoice()
})
@@ -86,7 +93,7 @@ export default class Voice {
* @param {string} lang
*/
set language(lang) {
if (this.#getVoiceLanguages().includes(lang)) {
if (this.#voiceLanguages.includes(lang)) {
this.#voiceLang = lang
} else {
this.#voiceLang = this.#defaultVoiceLang
@@ -102,7 +109,7 @@ export default class Voice {
}
get languages() {
return this.#getVoiceLanguages()
return this.#voiceLanguages
}
/**
@@ -135,10 +142,6 @@ export default class Voice {
return this.#nextDuration / 60 / 1000
}
#playTitleVoice() {
this.#playSpecialVoice("标题")
}
#initIdleVoiceTimer() {
this.#idleListener = setInterval(() => {
this.#playSpecialVoice("闲置")
@@ -169,16 +172,26 @@ export default class Voice {
#playVoice(id) {
this.#currentVoiceId = id
const audio = new Audio()
// audio.src = `https://cdn.jsdelivr.net/gh/Arondight/Adachi-BOT@master/src/assets/voice/${id}.mp3`
audio.play()
audio.addEventListener('ended', () => {
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) {
@@ -186,6 +199,11 @@ export default class Voice {
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)
@@ -200,8 +218,17 @@ export default class Voice {
return charword_table.operator.voice[this.#subtitleLang][this.#getWordKeyByVoiceLang()[this.#voiceLang]][id]
}
#getVoiceLanguages() {
return Object.keys(this.#getCVInfo(this.#defaultRegion))
#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
}
/**
@@ -251,4 +278,12 @@ export default class Voice {
return Object.keys(this.#getCVInfoByVoiceLang()[this.#voiceLang])
}
#insertHTML() {
this.#el.innerHTML = `
<audio id="${this.#audioElId}" autoplay>
<source type="audio/wav">
Your browser does not support the audio element.
</audio>
`
}
}

View File

@@ -7,16 +7,16 @@ import Voice from '@/components/voice'
document.querySelector('#app').innerHTML = `
<img src="./assets/${import.meta.env.VITE_LOGO_FILENAME}.png" class="logo invert-filter" id="logo" alt="operator logo" />
<div id="settings"></div>
<div id="voice" hidden></div>
<div id="widget-wrapper">
<div id="fallback"
style="background-image: url(./assets/${import.meta.env.VITE_FALLBACK_FILENAME}.png)"
hidden
></div>
<div id="player" hidden></div>
<div id="voice" hidden></div>
</div>
`
window.voice = new Voice(document.querySelector('#voice'))
window.voice = new Voice(document.querySelector('#voice'), document.querySelector('#widget-wrapper'))
window.voice.init()
window.settings = new Settings(document.querySelector('#settings'), document.querySelector('#logo'))
document.title = import.meta.env.VITE_TITLE

View File

@@ -5,7 +5,7 @@ window.wallpaperPropertyListener = {
}
},
applyUserProperties: function (properties) {
window.settings.insights(true)
window.settings.insights(true, false)
if (properties.logo) {
window.settings.setLogoDisplay(!properties.logo.value)
}