feat(aklive2d): performance optimization for showcase and error page for directory

This commit is contained in:
Haoyu Xu
2023-03-04 01:05:36 -05:00
parent 1c0f425c9a
commit 2fde26d96e
24 changed files with 352 additions and 148 deletions

View File

@@ -1 +1 @@
3.3.47 3.3.59

View File

@@ -1,3 +1,5 @@
/* eslint-disable no-fallthrough */
/* eslint-disable no-undef */
import assert from 'assert' import assert from 'assert'
import path from 'path' import path from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
@@ -56,8 +58,6 @@ async function main() {
await background.process() await background.process()
const backgrounds = ['operator_bg.png', ...background.files] const backgrounds = ['operator_bg.png', ...background.files]
directory({backgrounds, charwordTable})
for (const OPERATOR_NAME of OPERATOR_NAMES) { for (const OPERATOR_NAME of OPERATOR_NAMES) {
const OPERATOR_SOURCE_FOLDER = path.join(__projetRoot, __config.folder.operator) const OPERATOR_SOURCE_FOLDER = path.join(__projetRoot, __config.folder.operator)
const OPERATOR_RELEASE_FOLDER = path.join(__projetRoot, __config.folder.release, OPERATOR_NAME) const OPERATOR_RELEASE_FOLDER = path.join(__projetRoot, __config.folder.release, OPERATOR_NAME)
@@ -85,29 +85,36 @@ async function main() {
rmdir(OPERATOR_RELEASE_FOLDER) rmdir(OPERATOR_RELEASE_FOLDER)
const charwordTableLookup = charwordTable.lookup(OPERATOR_NAME) const charwordTableLookup = charwordTable.lookup(OPERATOR_NAME)
const voiceLangs = (() => { const voiceJson = {}
const infoArray = Object.values(charwordTableLookup.operator.info[charwordTableLookup.config.default_region]) voiceJson.config = {
// combine the infoArray default_region: charwordTableLookup.config.default_region.replace("_", "-"),
let output = {} regions: charwordTableLookup.config.regions.map((item) => item.replace("_", "-")),
for (const info of infoArray) { }
output = { voiceJson.voiceLangs = {}
...output, voiceJson.subtitleLangs = {}
...info const subtitleInfo = Object.keys(charwordTableLookup.operator.info)
subtitleInfo.forEach((item) => {
if (Object.keys(charwordTableLookup.operator.info[item]).length > 0) {
const key = item.replace("_", "-")
voiceJson.subtitleLangs[key] = {}
for (const [id, subtitles] of Object.entries(charwordTableLookup.operator.voice[item])) {
const match = id.replace(/(.+?)([A-Z]\w+)/, '$2')
if (match === id) {
voiceJson.subtitleLangs[key].default = subtitles
} else {
voiceJson.subtitleLangs[key][match] = subtitles
}
} }
voiceJson.voiceLangs[key] = {}
Object.values(charwordTableLookup.operator.info[item]).forEach((item) => {
voiceJson.voiceLangs[key] = { ...voiceJson.voiceLangs[key], ...item }
})
} }
return Object.keys(output) })
})() const voiceLangs = Object.keys(voiceJson.voiceLangs["zh-CN"])
const subtitleLangs = (() => { const subtitleLangs = Object.keys(voiceJson.subtitleLangs)
const output = []
for (const [key, value] of Object.entries(charwordTableLookup.operator.info)) {
if (Object.keys(value).length !== 0) {
output.push(key)
}
}
return output
})()
writeSync(JSON.stringify(charwordTableLookup), path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, 'charword_table.json')) writeSync(JSON.stringify(voiceJson), path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, 'charword_table.json'))
const projectJson = new ProjectJson(OPERATOR_NAME, OPERATOR_SHARE_FOLDER, { const projectJson = new ProjectJson(OPERATOR_NAME, OPERATOR_SHARE_FOLDER, {
backgrounds, backgrounds,
@@ -219,6 +226,8 @@ async function main() {
]), envPath) ]), envPath)
fork(path.join(__projetRoot, 'vite.config.js'), [op, OPERATOR_NAME]) fork(path.join(__projetRoot, 'vite.config.js'), [op, OPERATOR_NAME])
} }
directory({ backgrounds, charwordTable })
} }
main(); main();

View File

@@ -1,4 +1,6 @@
showcase: showcase:
2023/03/03:
- Performance optimization
2023/02/26: 2023/02/26:
- Rename w_fugue to w_wonder - Rename w_fugue to w_wonder
2023/02/14: 2023/02/14:

View File

@@ -24,6 +24,9 @@ share:
directory: directory:
title: AKLive2D title: AKLive2D
voice: jp/CN_037.ogg voice: jp/CN_037.ogg
error:
files: !include config/_directory.yaml
voice: CN_034.ogg
operators: operators:
chen: !include config/chen.yaml chen: !include config/chen.yaml
dusk: !include config/dusk.yaml dusk: !include config/dusk.yaml
@@ -49,4 +52,4 @@ operators:
mizuki_summer_feast: !include config/mizuki_summer_feast.yaml mizuki_summer_feast: !include config/mizuki_summer_feast.yaml
chongyue: !include config/chongyue.yaml chongyue: !include config/chongyue.yaml
ling_it_does_wash_the_strings: !include config/ling_it_does_wash_the_strings.yaml ling_it_does_wash_the_strings: !include config/ling_it_does_wash_the_strings.yaml
pozemka_snowy_plains_in_words: !include config/pozemka_snowy_plains_in_words.yaml pozemka_snowy_plains_in_words: !include config/pozemka_snowy_plains_in_words.yaml

12
config/_directory.yaml Normal file
View File

@@ -0,0 +1,12 @@
- key: build_char_128_plosis_epoque#3
paddings:
left: -120
right: 150
top: 10
bottom: 0
- key: build_char_128_plosis
paddings:
left: -90
right: 100
top: 10
bottom: 0

View File

@@ -1,4 +1,4 @@
description: !match "~{split('config', 'title' ,' - ')[0]} Live 2D\n~{split('config', 'title' ,' - ')[1]} Live 2D\nThe model is extracted from game with Spine support.\n模型来自游戏内提取支持Spine\nPlease set your FPS target in Wallpaper Engine > Settings > Performance > FPS\n\nLive preview on: https://arknights.halyul.dev/~{var('config', 'link')}?settings\nGithub: https://github.com/Halyul/aklive2d\nCheck out our privacy policy at https://privacy.halyul.dev" description: !match "~{split('config', 'title' ,' - ')[0]} Live 2D\n~{split('config', 'title' ,' - ')[1]} Live 2D\nThe model is extracted from game with Spine support.\n模型来自游戏内提取支持Spine\nPlease set your FPS target in Wallpaper Engine > Settings > Performance > FPS\n\nLive preview on: https://arknights.halyul.dev/~{var('config', 'link')}\nGithub: https://github.com/Halyul/aklive2d\nCheck out our privacy policy at https://privacy.halyul.dev"
localization: localization:
en-us: en-us:
ui_notice_title: <hr><h4>📝 Notes</h4><hr> ui_notice_title: <hr><h4>📝 Notes</h4><hr>

View File

@@ -2,4 +2,6 @@ VITE_APP_TITLE=AKLive2D
VITE_APP_VOICE_URL=jp/CN_037.ogg VITE_APP_VOICE_URL=jp/CN_037.ogg
VITE_VOICE_FOLDERS={"main":"voice","sub":[{"name":"jp","lang":"JP"},{"name":"cn","lang":"CN_MANDARIN"},{"name":"en","lang":"EN"},{"name":"kr","lang":"KR"},{"name":"custom","lang":"CUSTOM"}]} VITE_VOICE_FOLDERS={"main":"voice","sub":[{"name":"jp","lang":"JP"},{"name":"cn","lang":"CN_MANDARIN"},{"name":"en","lang":"EN"},{"name":"kr","lang":"KR"},{"name":"custom","lang":"CUSTOM"}]}
VITE_DIRECTORY_FOLDER="_assets" VITE_DIRECTORY_FOLDER="_assets"
VITE_BACKGROUND_FOLDER="background" 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"}

View File

@@ -1 +1 @@
0.5.30 1.0.4

View File

@@ -5,7 +5,7 @@ import {
} from "react" } from "react"
import { useCallback } from "react" import { useCallback } from "react"
const audioEl = new Audio() const audioEl = new Audio()
let lastSrc = ''
export default function useAudio() { export default function useAudio() {
const [isPlaying, _setIsPlaying] = useState(false) const [isPlaying, _setIsPlaying] = useState(false)
const isPlayingRef = useRef(isPlaying) const isPlayingRef = useRef(isPlaying)
@@ -29,17 +29,19 @@ export default function useAudio() {
}, },
callback = () => { } callback = () => { }
) => { ) => {
if (!options.overwrite && audioEl.src === (window.location.href.replace(/\/$/g, '') + link)) return if (!options.overwrite && link === lastSrc) return
audioEl.src = link audioEl.src = link
let startPlayPromise = audioEl.play() let startPlayPromise = audioEl.play()
if (startPlayPromise !== undefined) { if (startPlayPromise !== undefined) {
setIsPlaying(true) setIsPlaying(true)
startPlayPromise startPlayPromise
.then(() => { .then(() => {
lastSrc = link
callback() callback()
return return
}) })
.catch(() => { .catch((e) => {
console.log(e)
return return
}) })
} }

View File

@@ -0,0 +1,53 @@
.error-page {
min-height: 100vh;
display: flex;
flex-direction: column;
font-size: 16px;
user-select: none;
}
.error-page .header {
padding: 1rem;
justify-content: space-between;
}
.error-page .main {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
padding-top: 10rem;
font-size: 3rem;
gap: 2rem;
}
.error-page .spine {
max-width: 600px;
flex: 1;
visibility: hidden;
opacity: 0;
}
.error-page .spine.active {
visibility: visible;
opacity: 1;
}
@media (max-width: 768px) {
.error-page .main {
padding-top: 6rem;
}
.error-page .content {
font-size: 2rem;
}
}
@media (max-width: 480px) {
.error-page .main {
padding-top: 4rem;
}
.error-page .content {
font-size: 1.5rem;
}
}

View File

@@ -1,17 +1,170 @@
import React from "react"; import React, {
useState,
useEffect,
useMemo,
useRef,
useCallback
} from "react";
import { import {
useNavigate, useNavigate,
useRouteError useRouteError
} from "react-router-dom"; } from "react-router-dom";
import './error-page.css'
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils';
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 spine from '!/libs/spine-player'
import '!/libs/spine-player.css'
const voiceOnAtom = atomWithStorage('voiceOn', false)
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
export default function ErrorPage() { export default function ErrorPage() {
const error = useRouteError(); const error = useRouteError();
const navigate = useNavigate(); const navigate = useNavigate();
console.log(error) const {
setTitle,
} = useHeader()
const [voiceOn, setVoiceOn] = useAtom(voiceOnAtom)
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 setSpineDone = (data) => {
spineDoneRef.current = data
_setSpineDone(data)
}
const content = useMemo(() => ['エラー発生。', '发生错误。', 'Error occured.', '에러 발생.', '發生錯誤。'], [])
useEffect(() => {
console.log(error)
fetch(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/${filename}.json`).then(res => res.json()).then(data => {
setSpineData(data)
})
}, [error])
useEffect(() => {
setTitle(content[0])
stop()
}, [content, setTitle, stop])
useEffect(() => {
if (!voiceOn) {
stop()
}
}, [voiceOn, stop])
useEffect(() => {
voiceOnRef.current = voiceOn
}, [voiceOn])
const playVoice = useCallback(() => {
play(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/error.ogg`, { overwrite: true })
}, [play])
useEffect(() => {
if (voiceOn) playVoice()
}, [playVoice, voiceOn])
useEffect(() => {
if (spineRef.current?.children.length === 0 && spineData) {
new spine.SpinePlayer(spineRef.current, {
skelUrl: `./assets/${filename}.skel`,
atlasUrl: `./assets/${filename}.atlas`,
rawDataURIs: spineData,
animation: 'Relax',
premultipliedAlpha: true,
alpha: true,
backgroundColor: "#00000000",
viewport: {
debugRender: false,
padLeft: `${padding.left}%`,
padRight: `${padding.right}%`,
padTop: `${padding.top}%`,
padBottom: `${padding.bottom}%`,
x: 0,
y: 0,
},
showControls: false,
touch: false,
fps: 60,
defaultMix: 0,
success: (player) => {
let isPlayingInteract = false
player.animationState.addListener({
end: (e) => {
if (e.animation.name == "Interact") {
isPlayingInteract = false;
}
}
});
setSpineDone(true)
const ani = () => {
if (isPlayingInteract) {
return;
}
isPlayingInteract = true;
player.animationState.setAnimation(0, "Interact", false, 0);
player.animationState.addAnimation(0, "Relax", true, 0);
if (voiceOnRef.current) playVoice()
}
ani()
player.canvas.onclick = () => {
ani()
}
player.canvas.onmouseenter = () => {
ani()
}
}
})
}
}, [playVoice, spineData]);
return ( return (
<section> <section className='error-page'>
error <header className='header'>
<button onClick={() => navigate(-1, { replace: true })}>Go Home</button> <ReturnButton
className='return-button'
onClick={() => navigate(-1, { replace: true })}
/>
<Switch
key="voice"
text='voice'
on={voiceOn}
handleOnClick={() => setVoiceOn(!voiceOn)}
/>
</header>
<main className='main'>
{
content.map((item, index) => {
return (
<section key={index} className='content'>
<Typewriter
words={[item]}
cursor
cursorStyle='|'
typeSpeed={100}
/>
</section>
)
})
}
<section
className={`spine ${spineDone ? 'active' : ''}`}
ref={spineRef}
/>
</main>
</section> </section>
); );
} }

View File

@@ -9,6 +9,7 @@
align-items: flex-start; align-items: flex-start;
gap: 0.5rem; gap: 0.5rem;
padding-left: 1rem; padding-left: 1rem;
word-break: break-word;
} }
.changelogs .item-info-content { .changelogs .item-info-content {

View File

@@ -3,6 +3,7 @@
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
flex-wrap: wrap; flex-wrap: wrap;
user-select: none;
} }
.item-group-date { .item-group-date {

View File

@@ -8,13 +8,12 @@
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
}
.operator .spine-container {
width: 100%; width: 100%;
position: relative; position: relative;
margin-bottom: 2rem; margin-bottom: 2rem;
user-select: none;
} }
.operator .spine-container:before { .operator .spine-container:before {
content: ""; content: "";
display: block; display: block;
@@ -28,9 +27,9 @@
right: 0; right: 0;
} }
.operator .spine-settings, .operator .spine-settings {
.operator .steam-workshop-wrapper {
margin-right: 1.5rem; margin-right: 1.5rem;
user-select: none;
} }
.operator .text { .operator .text {

View File

@@ -33,8 +33,6 @@ const getVoiceFoler = (lang) => {
} }
const spinePlayerAtom = atom(null); const spinePlayerAtom = atom(null);
const spineAnimationAtom = atom("Idle"); const spineAnimationAtom = atom("Idle");
const voiceLangAtom = atom(null);
const subtitleLangAtom = atom(null);
const getTabName = (item, language) => { const getTabName = (item, language) => {
if (item.type === 'operator') { if (item.type === 'operator') {
@@ -63,11 +61,11 @@ export default function Operator() {
const [spineAnimation, setSpineAnimation] = useAtom(spineAnimationAtom) const [spineAnimation, setSpineAnimation] = useAtom(spineAnimationAtom)
const { i18n } = useI18n() const { i18n } = useI18n()
const [spinePlayer, setSpinePlayer] = useAtom(spinePlayerAtom) const [spinePlayer, setSpinePlayer] = useAtom(spinePlayerAtom)
const [voiceLang, _setVoiceLang] = useAtom(voiceLangAtom) const [voiceLang, _setVoiceLang] = useState(null)
const { backgrounds } = useBackgrounds() const { backgrounds } = useBackgrounds()
const [currentBackground, setCurrentBackground] = useState(null) const [currentBackground, setCurrentBackground] = useState(null)
const [voiceConfig, setVoiceConfig] = useState(null) const [voiceConfig, setVoiceConfig] = useState(null)
const [subtitleLang, setSubtitleLang] = useAtom(subtitleLangAtom) const [subtitleLang, setSubtitleLang] = useState(null)
const [hideSubtitle, setHideSubtitle] = useState(true) const [hideSubtitle, setHideSubtitle] = useState(true)
const { play, stop, getSrc, isPlaying, isPlayingRef } = useAudio() const { play, stop, getSrc, isPlaying, isPlayingRef } = useAudio()
const [subtitleObj, _setSubtitleObj] = useState(null) const [subtitleObj, _setSubtitleObj] = useState(null)
@@ -339,6 +337,10 @@ export default function Operator() {
} }
] ]
if (!JSON.parse(import.meta.env.VITE_AVAILABLE_OPERATORS).includes(key)) {
throw new Error('Operator not found')
}
return ( return (
<section className="operator"> <section className="operator">
<section className="spine-player-wrapper"> <section className="spine-player-wrapper">

View File

@@ -86,6 +86,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: flex-start;
user-select: none;
} }
.drawer .links { .drawer .links {
@@ -237,6 +238,10 @@
color: var(--text-color); color: var(--text-color);
} }
.footer {
user-select: none;
}
.footer .section { .footer .section {
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
padding: 1rem 0; padding: 1rem 0;

View File

@@ -132,6 +132,7 @@ export default function Root() {
function FooterElement() { function FooterElement() {
const { i18n } = useI18n() const { i18n } = useI18n()
const { version } = useConfig() const { version } = useConfig()
const navigate = useNavigate()
return useMemo(() => { return useMemo(() => {
return ( return (
@@ -160,7 +161,9 @@ function FooterElement() {
</Popup> </Popup>
</section> </section>
</section> </section>
<section className='copyright section'> <section className='copyright section' onDoubleClick={() => {
navigate('/error')
}}>
<span>Spine Runtimes © 2013 - 2019 Esoteric Software LLC</span> <span>Spine Runtimes © 2013 - 2019 Esoteric Software LLC</span>
<span>Assets © 2017 - {currentYear} Arknights/Hypergryph Co., Ltd</span> <span>Assets © 2017 - {currentYear} Arknights/Hypergryph Co., Ltd</span>
<span>Source Code © 2021 - {currentYear} Halyul</span> <span>Source Code © 2021 - {currentYear} Halyul</span>
@@ -169,7 +172,7 @@ function FooterElement() {
</section> </section>
</footer> </footer>
) )
}, [i18n, version.directory, version.showcase]) }, [i18n, navigate, version.directory, version.showcase])
} }
function DrawerDestinations({ toggleDrawer }) { function DrawerDestinations({ toggleDrawer }) {

View File

@@ -36,10 +36,10 @@ export default class AssetsProcessor {
const croppedBuffer = await this.#alphaCompositer.crop(portraitBuffer, rect) const croppedBuffer = await this.#alphaCompositer.crop(portraitBuffer, rect)
await write(croppedBuffer, path.join(this.#operatorSourceFolder, this.#operatorName, `${fallback_name}_portrait.png`)) await write(croppedBuffer, path.join(this.#operatorSourceFolder, this.#operatorName, `${fallback_name}_portrait.png`))
return await this.#generateAssets(__config.operators[this.#operatorName].filename, extractedDir) return await this.generateAssets(__config.operators[this.#operatorName].filename, extractedDir)
} }
async #generateAssets(filename, extractedDir) { async generateAssets(filename, extractedDir) {
const BASE64_BINARY_PREFIX = 'data:application/octet-stream;base64,' const BASE64_BINARY_PREFIX = 'data:application/octet-stream;base64,'
const BASE64_PNG_PREFIX = 'data:image/png;base64,' const BASE64_PNG_PREFIX = 'data:image/png;base64,'
const assetsJson = {} const assetsJson = {}

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-undef */
import fetch from "node-fetch" import fetch from "node-fetch"
import path from "path" import path from "path"
import dotenv from "dotenv" import dotenv from "dotenv"

View File

@@ -1,16 +1,13 @@
/* eslint-disable no-undef */
import path from 'path' import path from 'path'
import { writeSync, copy, rmdir, readSync as readFile } from './file.js' import { writeSync, copy, readSync as readFile } from './file.js'
import { read } from './yaml.js'; import { read } from './yaml.js';
import AssetsProcessor from './assets_processor.js'
/**
* TODO:
* 1. add voice config -> look up charword table
*/
export default function ({ backgrounds, charwordTable }) { export default function ({ backgrounds, charwordTable }) {
const extractedFolder = path.join(__projetRoot, __config.folder.operator, '_directory')
const targetFolder = path.join(__projetRoot, __config.folder.release, __config.folder.directory); const targetFolder = path.join(__projetRoot, __config.folder.release, __config.folder.directory);
const sourceFolder = path.join(__projetRoot, __config.folder.operator); const sourceFolder = path.join(__projetRoot, __config.folder.operator);
rmdir(targetFolder);
const filesToCopy = Object.keys(__config.operators) const filesToCopy = Object.keys(__config.operators)
const directoryJson = { const directoryJson = {
operators: Object.values( operators: Object.values(
@@ -36,35 +33,6 @@ export default function ({ backgrounds, charwordTable }) {
} }
const versionJson = __config.version const versionJson = __config.version
filesToCopy.forEach((operator) => {
const voiceJson = {}
voiceJson.voiceLangs = {}
voiceJson.subtitleLangs = {}
const charwordTableObj = charwordTable.lookup(operator)
const subtitleInfo = Object.keys(charwordTableObj.operator.info)
subtitleInfo.forEach((item) => {
if (Object.keys(charwordTableObj.operator.info[item]).length > 0) {
const key = item.replace("_", "-")
voiceJson.subtitleLangs[key] = {}
for (const [id, subtitles] of Object.entries(charwordTableObj.operator.voice[item])) {
const match = id.replace(/(.+?)([A-Z]\w+)/, '$2')
if (match === id) {
voiceJson.subtitleLangs[key].default = subtitles
} else {
voiceJson.subtitleLangs[key][match] = subtitles
}
}
voiceJson.voiceLangs[key] = {}
Object.values(charwordTableObj.operator.info[item]).forEach((item) => {
voiceJson.voiceLangs[key] = { ...voiceJson.voiceLangs[key], ...item }
})
}
})
writeSync(JSON.stringify(voiceJson, null), path.join(targetFolder, `voice_${operator}.json`))
})
const changelogs = read(path.join(__projetRoot, 'changelogs.yaml')) const changelogs = read(path.join(__projetRoot, 'changelogs.yaml'))
const changelogsArray = Object.keys(changelogs).reduce((acc, cur) => { const changelogsArray = Object.keys(changelogs).reduce((acc, cur) => {
const array = [] const array = []
@@ -79,11 +47,20 @@ export default function ({ backgrounds, charwordTable }) {
return acc return acc
}, []) }, [])
__config.directory.error.files.forEach((key) => {
const assetsProcessor = new AssetsProcessor()
assetsProcessor.generateAssets(key.key, extractedFolder).then((content) => {
writeSync(JSON.stringify(content.assetsJson, null), path.join(targetFolder, `${key.key}.json`))
})
})
writeSync(JSON.stringify(directoryJson, null), path.join(targetFolder, "directory.json")) writeSync(JSON.stringify(directoryJson, null), path.join(targetFolder, "directory.json"))
writeSync(JSON.stringify(versionJson, null), path.join(targetFolder, "version.json")) writeSync(JSON.stringify(versionJson, null), path.join(targetFolder, "version.json"))
writeSync(JSON.stringify(changelogsArray, null), path.join(targetFolder, "changelogs.json")) writeSync(JSON.stringify(changelogsArray, null), path.join(targetFolder, "changelogs.json"))
writeSync(JSON.stringify(backgrounds, null), path.join(targetFolder, "backgrounds.json")) writeSync(JSON.stringify(backgrounds, null), path.join(targetFolder, "backgrounds.json"))
filesToCopy.forEach((key) => { filesToCopy.forEach((key) => {
copy(path.join(sourceFolder, key, 'assets.json'), path.join(targetFolder, `${__config.operators[key].filename}.json`)) copy(path.join(sourceFolder, key, 'assets.json'), path.join(targetFolder, `${__config.operators[key].filename}.json`))
copy(path.join(sourceFolder, key, 'charword_table.json'), path.join(targetFolder, `voice_${key}.json`))
}) })
copy(path.join(extractedFolder, __config.directory.error.voice), path.join(targetFolder, `error.ogg`))
} }

View File

@@ -41,6 +41,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-refresh": "^0.14.0", "react-refresh": "^0.14.0",
"react-router-dom": "^6.8.1", "react-router-dom": "^6.8.1",
"react-simple-typewriter": "^5.0.1",
"reset-css": "^5.0.1", "reset-css": "^5.0.1",
"sharp": "^0.31.3", "sharp": "^0.31.3",
"yaml": "^2.2.1" "yaml": "^2.2.1"

13
pnpm-lock.yaml generated
View File

@@ -17,6 +17,7 @@ specifiers:
react-dom: ^18.2.0 react-dom: ^18.2.0
react-refresh: ^0.14.0 react-refresh: ^0.14.0
react-router-dom: ^6.8.1 react-router-dom: ^6.8.1
react-simple-typewriter: ^5.0.1
reset-css: ^5.0.1 reset-css: ^5.0.1
rollup: ^3.17.3 rollup: ^3.17.3
sharp: ^0.31.3 sharp: ^0.31.3
@@ -32,6 +33,7 @@ dependencies:
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
react-refresh: 0.14.0 react-refresh: 0.14.0
react-router-dom: 6.8.1_biqbaboplfbrettd7655fr4n2y react-router-dom: 6.8.1_biqbaboplfbrettd7655fr4n2y
react-simple-typewriter: 5.0.1_biqbaboplfbrettd7655fr4n2y
reset-css: 5.0.1 reset-css: 5.0.1
sharp: 0.31.3 sharp: 0.31.3
yaml: 2.2.1 yaml: 2.2.1
@@ -1996,6 +1998,17 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/react-simple-typewriter/5.0.1_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-vA5HkABwJKL/DJ4RshSlY/igdr+FiVY4MLsSQYJX6FZG/f1/VwN4y1i3mPXRyfaswrvI8xii1kOVe1dYtO2Row==}
engines: {node: '>=14'}
peerDependencies:
react: '>=18.0.0'
react-dom: '>=18.0.0'
dependencies:
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/react/18.2.0: /react/18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}

View File

@@ -9,7 +9,7 @@ export default class Voice {
#defaultRegion = charword_table.config.default_region #defaultRegion = charword_table.config.default_region
#defaultIdleDuration = 10 * 60 * 1000 #defaultIdleDuration = 10 * 60 * 1000
#defaultNextDuration = 3 * 60 * 1000 #defaultNextDuration = 3 * 60 * 1000
#voiceLanguages = Object.keys(this.#getCVInfo(this.#defaultRegion)) #voiceLanguages = Object.keys(charword_table.voiceLangs["zh-CN"])
#defaultVoiceLang = this.#voiceLanguages[0] #defaultVoiceLang = this.#voiceLanguages[0]
#voiceLang = this.#defaultVoiceLang #voiceLang = this.#defaultVoiceLang
#subtitleLang = this.#defaultRegion #subtitleLang = this.#defaultRegion
@@ -42,6 +42,12 @@ export default class Voice {
} }
success() { success() {
const audioEndedFunc = () => {
this.#isPlaying = false
this.#setCurrentSubtitle(null)
this.#lastClickToNext = false
}
this.#audioEl.addEventListener('ended', audioEndedFunc)
this.#playEntryVoice() this.#playEntryVoice()
this.#initNextVoiceTimer() this.#initNextVoiceTimer()
this.#widgetEl.addEventListener('click', e => { this.#widgetEl.addEventListener('click', e => {
@@ -242,7 +248,7 @@ export default class Voice {
const subtitle = this.#getSubtitleById(id) const subtitle = this.#getSubtitleById(id)
const title = subtitle.title const title = subtitle.title
const content = subtitle.text const content = subtitle.text
const cvInfo = this.#getCVInfoByVoiceLang()[this.#voiceLang][this.subtitleLanguage] const cvInfo = charword_table.voiceLangs[this.subtitleLanguage][this.#voiceLang]
document.getElementById('voice_title').innerText = title document.getElementById('voice_title').innerText = title
document.getElementById('voice_subtitle').innerText = content document.getElementById('voice_subtitle').innerText = content
this.#el.style.opacity = 1 this.#el.style.opacity = 1
@@ -260,15 +266,6 @@ export default class Voice {
startPlayPromise startPlayPromise
.then(() => { .then(() => {
this.#isPlaying = true 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) this.#setCurrentSubtitle(id)
}) })
.catch(() => { .catch(() => {
@@ -278,7 +275,8 @@ export default class Voice {
} }
#playSpecialVoice(matcher) { #playSpecialVoice(matcher) {
const voiceId = this.#getSpecialVoiceId(matcher) const voices = this.#getVoices()
const voiceId = Object.keys(voices).find(e => voices[e].title === matcher)
this.#playVoice(voiceId) this.#playVoice(voiceId)
} }
@@ -287,18 +285,17 @@ export default class Voice {
return `${folderObject.main}/${folderObject.sub.find(e => e.lang === this.#voiceLang).name}` 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() { #getVoices() {
return charword_table.operator.voice[this.#defaultRegion][this.#getWordKeyByVoiceLang()[this.#defaultVoiceLang]] return charword_table.subtitleLangs[this.#subtitleLang].default
} }
#getSubtitleById(id) { #getSubtitleById(id) {
return charword_table.operator.voice[this.#subtitleLang][this.#getWordKeyByVoiceLang()[this.#voiceLang]][id] const obj = charword_table.subtitleLangs[this.#subtitleLang]
let key = 'default'
if (obj[this.#voiceLang]) {
key = this.#voiceLang
}
return obj[key][id]
} }
#getVoiceFolderObject() { #getVoiceFolderObject() {
@@ -314,51 +311,8 @@ export default class Voice {
return folderObject 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() { #getSubtitleLanguages() {
return Object.keys(this.#getCVInfoByVoiceLang()[this.#voiceLang]) return Object.keys(charword_table.subtitleLangs)
} }
#insertHTML() { #insertHTML() {

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-undef */
import path from 'path' import path from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
@@ -156,12 +157,20 @@ class ViteRunner {
, { , {
key: "background_folder", key: "background_folder",
value: JSON.stringify(this.#globalConfig.folder.background) value: JSON.stringify(this.#globalConfig.folder.background)
}, {
key: "available_operators",
value: JSON.stringify(Object.keys(this.#globalConfig.operators))
}, {
key: "error_files",
value: JSON.stringify(this.#globalConfig.directory.error).replace('#', '%23')
} }
]), path.join(directoryDir, '.env')) ]), path.join(directoryDir, '.env'))
this.#mode = process.argv[3] this.#mode = process.argv[3]
const publicDir = path.resolve(__projetRoot, this.#globalConfig.folder.release)
return { return {
...this.#baseViteConfig, ...this.#baseViteConfig,
envDir: directoryDir, envDir: directoryDir,
base: "/",
plugins: [ plugins: [
react(), react(),
// PerfseePlugin({ // PerfseePlugin({
@@ -178,6 +187,7 @@ class ViteRunner {
alias: { alias: {
'@': path.resolve(directoryDir, './src'), '@': path.resolve(directoryDir, './src'),
'!': path.resolve(__projetRoot, './src'), '!': path.resolve(__projetRoot, './src'),
'#': path.resolve(publicDir, this.#globalConfig.folder.directory),
}, },
}, },
build: { build: {
@@ -187,6 +197,7 @@ class ViteRunner {
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks: { manualChunks: {
'react': ['react', 'react-dom', 'react-router-dom'],
}, },
} }
} }