feat(aklive2d): performance optimization for showcase and error page for directory
This commit is contained in:
53
aklive2d.js
53
aklive2d.js
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable no-fallthrough */
|
||||
/* eslint-disable no-undef */
|
||||
import assert from 'assert'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
@@ -56,8 +58,6 @@ async function main() {
|
||||
await background.process()
|
||||
const backgrounds = ['operator_bg.png', ...background.files]
|
||||
|
||||
directory({backgrounds, charwordTable})
|
||||
|
||||
for (const OPERATOR_NAME of OPERATOR_NAMES) {
|
||||
const OPERATOR_SOURCE_FOLDER = path.join(__projetRoot, __config.folder.operator)
|
||||
const OPERATOR_RELEASE_FOLDER = path.join(__projetRoot, __config.folder.release, OPERATOR_NAME)
|
||||
@@ -85,29 +85,36 @@ async function main() {
|
||||
rmdir(OPERATOR_RELEASE_FOLDER)
|
||||
|
||||
const charwordTableLookup = charwordTable.lookup(OPERATOR_NAME)
|
||||
const voiceLangs = (() => {
|
||||
const infoArray = Object.values(charwordTableLookup.operator.info[charwordTableLookup.config.default_region])
|
||||
// combine the infoArray
|
||||
let output = {}
|
||||
for (const info of infoArray) {
|
||||
output = {
|
||||
...output,
|
||||
...info
|
||||
const voiceJson = {}
|
||||
voiceJson.config = {
|
||||
default_region: charwordTableLookup.config.default_region.replace("_", "-"),
|
||||
regions: charwordTableLookup.config.regions.map((item) => item.replace("_", "-")),
|
||||
}
|
||||
voiceJson.voiceLangs = {}
|
||||
voiceJson.subtitleLangs = {}
|
||||
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 subtitleLangs = (() => {
|
||||
const output = []
|
||||
for (const [key, value] of Object.entries(charwordTableLookup.operator.info)) {
|
||||
if (Object.keys(value).length !== 0) {
|
||||
output.push(key)
|
||||
}
|
||||
}
|
||||
return output
|
||||
})()
|
||||
})
|
||||
const voiceLangs = Object.keys(voiceJson.voiceLangs["zh-CN"])
|
||||
const subtitleLangs = Object.keys(voiceJson.subtitleLangs)
|
||||
|
||||
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, {
|
||||
backgrounds,
|
||||
@@ -219,6 +226,8 @@ async function main() {
|
||||
]), envPath)
|
||||
fork(path.join(__projetRoot, 'vite.config.js'), [op, OPERATOR_NAME])
|
||||
}
|
||||
|
||||
directory({ backgrounds, charwordTable })
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,4 +1,6 @@
|
||||
showcase:
|
||||
2023/03/03:
|
||||
- Performance optimization
|
||||
2023/02/26:
|
||||
- Rename w_fugue to w_wonder
|
||||
2023/02/14:
|
||||
|
||||
@@ -24,6 +24,9 @@ share:
|
||||
directory:
|
||||
title: AKLive2D
|
||||
voice: jp/CN_037.ogg
|
||||
error:
|
||||
files: !include config/_directory.yaml
|
||||
voice: CN_034.ogg
|
||||
operators:
|
||||
chen: !include config/chen.yaml
|
||||
dusk: !include config/dusk.yaml
|
||||
|
||||
12
config/_directory.yaml
Normal file
12
config/_directory.yaml
Normal 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
|
||||
@@ -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:
|
||||
en-us:
|
||||
ui_notice_title: <hr><h4>📝 Notes</h4><hr>
|
||||
|
||||
@@ -3,3 +3,5 @@ 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_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"}
|
||||
@@ -1 +1 @@
|
||||
0.5.30
|
||||
1.0.4
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} 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)
|
||||
@@ -29,17 +29,19 @@ export default function useAudio() {
|
||||
},
|
||||
callback = () => { }
|
||||
) => {
|
||||
if (!options.overwrite && audioEl.src === (window.location.href.replace(/\/$/g, '') + link)) return
|
||||
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(() => {
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
53
directory/src/routes/error-page.css
Normal file
53
directory/src/routes/error-page.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,170 @@
|
||||
import React from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useCallback
|
||||
} from "react";
|
||||
import {
|
||||
useNavigate,
|
||||
useRouteError
|
||||
} 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() {
|
||||
const error = useRouteError();
|
||||
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 (
|
||||
<section>
|
||||
error
|
||||
<button onClick={() => navigate(-1, { replace: true })}>Go Home</button>
|
||||
<section className='error-page'>
|
||||
<header className='header'>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.changelogs .item-info-content {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.item-group-date {
|
||||
|
||||
@@ -8,13 +8,12 @@
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.operator .spine-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-bottom: 2rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.operator .spine-container:before {
|
||||
content: "";
|
||||
display: block;
|
||||
@@ -28,9 +27,9 @@
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.operator .spine-settings,
|
||||
.operator .steam-workshop-wrapper {
|
||||
.operator .spine-settings {
|
||||
margin-right: 1.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.operator .text {
|
||||
|
||||
@@ -33,8 +33,6 @@ const getVoiceFoler = (lang) => {
|
||||
}
|
||||
const spinePlayerAtom = atom(null);
|
||||
const spineAnimationAtom = atom("Idle");
|
||||
const voiceLangAtom = atom(null);
|
||||
const subtitleLangAtom = atom(null);
|
||||
|
||||
const getTabName = (item, language) => {
|
||||
if (item.type === 'operator') {
|
||||
@@ -63,11 +61,11 @@ export default function Operator() {
|
||||
const [spineAnimation, setSpineAnimation] = useAtom(spineAnimationAtom)
|
||||
const { i18n } = useI18n()
|
||||
const [spinePlayer, setSpinePlayer] = useAtom(spinePlayerAtom)
|
||||
const [voiceLang, _setVoiceLang] = useAtom(voiceLangAtom)
|
||||
const [voiceLang, _setVoiceLang] = useState(null)
|
||||
const { backgrounds } = useBackgrounds()
|
||||
const [currentBackground, setCurrentBackground] = useState(null)
|
||||
const [voiceConfig, setVoiceConfig] = useState(null)
|
||||
const [subtitleLang, setSubtitleLang] = useAtom(subtitleLangAtom)
|
||||
const [subtitleLang, setSubtitleLang] = useState(null)
|
||||
const [hideSubtitle, setHideSubtitle] = useState(true)
|
||||
const { play, stop, getSrc, isPlaying, isPlayingRef } = useAudio()
|
||||
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 (
|
||||
<section className="operator">
|
||||
<section className="spine-player-wrapper">
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.drawer .links {
|
||||
@@ -237,6 +238,10 @@
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.footer {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.footer .section {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 1rem 0;
|
||||
|
||||
@@ -132,6 +132,7 @@ export default function Root() {
|
||||
function FooterElement() {
|
||||
const { i18n } = useI18n()
|
||||
const { version } = useConfig()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return useMemo(() => {
|
||||
return (
|
||||
@@ -160,7 +161,9 @@ function FooterElement() {
|
||||
</Popup>
|
||||
</section>
|
||||
</section>
|
||||
<section className='copyright section'>
|
||||
<section className='copyright section' onDoubleClick={() => {
|
||||
navigate('/error')
|
||||
}}>
|
||||
<span>Spine Runtimes © 2013 - 2019 Esoteric Software LLC</span>
|
||||
<span>Assets © 2017 - {currentYear} Arknights/Hypergryph Co., Ltd</span>
|
||||
<span>Source Code © 2021 - {currentYear} Halyul</span>
|
||||
@@ -169,7 +172,7 @@ function FooterElement() {
|
||||
</section>
|
||||
</footer>
|
||||
)
|
||||
}, [i18n, version.directory, version.showcase])
|
||||
}, [i18n, navigate, version.directory, version.showcase])
|
||||
}
|
||||
|
||||
function DrawerDestinations({ toggleDrawer }) {
|
||||
|
||||
@@ -36,10 +36,10 @@ export default class AssetsProcessor {
|
||||
const croppedBuffer = await this.#alphaCompositer.crop(portraitBuffer, rect)
|
||||
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_PNG_PREFIX = 'data:image/png;base64,'
|
||||
const assetsJson = {}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-undef */
|
||||
import fetch from "node-fetch"
|
||||
import path from "path"
|
||||
import dotenv from "dotenv"
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
/* eslint-disable no-undef */
|
||||
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';
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* 1. add voice config -> look up charword table
|
||||
*/
|
||||
import AssetsProcessor from './assets_processor.js'
|
||||
|
||||
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 sourceFolder = path.join(__projetRoot, __config.folder.operator);
|
||||
rmdir(targetFolder);
|
||||
const filesToCopy = Object.keys(__config.operators)
|
||||
const directoryJson = {
|
||||
operators: Object.values(
|
||||
@@ -36,35 +33,6 @@ export default function ({ backgrounds, charwordTable }) {
|
||||
}
|
||||
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 changelogsArray = Object.keys(changelogs).reduce((acc, cur) => {
|
||||
const array = []
|
||||
@@ -79,11 +47,20 @@ export default function ({ backgrounds, charwordTable }) {
|
||||
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(versionJson, null), path.join(targetFolder, "version.json"))
|
||||
writeSync(JSON.stringify(changelogsArray, null), path.join(targetFolder, "changelogs.json"))
|
||||
writeSync(JSON.stringify(backgrounds, null), path.join(targetFolder, "backgrounds.json"))
|
||||
filesToCopy.forEach((key) => {
|
||||
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`))
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-simple-typewriter": "^5.0.1",
|
||||
"reset-css": "^5.0.1",
|
||||
"sharp": "^0.31.3",
|
||||
"yaml": "^2.2.1"
|
||||
|
||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@@ -17,6 +17,7 @@ specifiers:
|
||||
react-dom: ^18.2.0
|
||||
react-refresh: ^0.14.0
|
||||
react-router-dom: ^6.8.1
|
||||
react-simple-typewriter: ^5.0.1
|
||||
reset-css: ^5.0.1
|
||||
rollup: ^3.17.3
|
||||
sharp: ^0.31.3
|
||||
@@ -32,6 +33,7 @@ dependencies:
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
react-refresh: 0.14.0
|
||||
react-router-dom: 6.8.1_biqbaboplfbrettd7655fr4n2y
|
||||
react-simple-typewriter: 5.0.1_biqbaboplfbrettd7655fr4n2y
|
||||
reset-css: 5.0.1
|
||||
sharp: 0.31.3
|
||||
yaml: 2.2.1
|
||||
@@ -1996,6 +1998,17 @@ packages:
|
||||
react: 18.2.0
|
||||
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:
|
||||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
@@ -9,7 +9,7 @@ export default class Voice {
|
||||
#defaultRegion = charword_table.config.default_region
|
||||
#defaultIdleDuration = 10 * 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]
|
||||
#voiceLang = this.#defaultVoiceLang
|
||||
#subtitleLang = this.#defaultRegion
|
||||
@@ -42,6 +42,12 @@ export default class Voice {
|
||||
}
|
||||
|
||||
success() {
|
||||
const audioEndedFunc = () => {
|
||||
this.#isPlaying = false
|
||||
this.#setCurrentSubtitle(null)
|
||||
this.#lastClickToNext = false
|
||||
}
|
||||
this.#audioEl.addEventListener('ended', audioEndedFunc)
|
||||
this.#playEntryVoice()
|
||||
this.#initNextVoiceTimer()
|
||||
this.#widgetEl.addEventListener('click', e => {
|
||||
@@ -242,7 +248,7 @@ export default class Voice {
|
||||
const subtitle = this.#getSubtitleById(id)
|
||||
const title = subtitle.title
|
||||
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_subtitle').innerText = content
|
||||
this.#el.style.opacity = 1
|
||||
@@ -260,15 +266,6 @@ export default class Voice {
|
||||
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)
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -278,7 +275,8 @@ export default class Voice {
|
||||
}
|
||||
|
||||
#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)
|
||||
}
|
||||
|
||||
@@ -287,18 +285,17 @@ export default class Voice {
|
||||
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]]
|
||||
return charword_table.subtitleLangs[this.#subtitleLang].default
|
||||
}
|
||||
|
||||
#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() {
|
||||
@@ -314,51 +311,8 @@ export default class Voice {
|
||||
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])
|
||||
return Object.keys(charword_table.subtitleLangs)
|
||||
}
|
||||
|
||||
#insertHTML() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-undef */
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { defineConfig } from 'vite'
|
||||
@@ -156,12 +157,20 @@ class ViteRunner {
|
||||
, {
|
||||
key: "background_folder",
|
||||
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'))
|
||||
this.#mode = process.argv[3]
|
||||
const publicDir = path.resolve(__projetRoot, this.#globalConfig.folder.release)
|
||||
return {
|
||||
...this.#baseViteConfig,
|
||||
envDir: directoryDir,
|
||||
base: "/",
|
||||
plugins: [
|
||||
react(),
|
||||
// PerfseePlugin({
|
||||
@@ -178,6 +187,7 @@ class ViteRunner {
|
||||
alias: {
|
||||
'@': path.resolve(directoryDir, './src'),
|
||||
'!': path.resolve(__projetRoot, './src'),
|
||||
'#': path.resolve(publicDir, this.#globalConfig.folder.directory),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
@@ -187,6 +197,7 @@ class ViteRunner {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'react': ['react', 'react-dom', 'react-router-dom'],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user