feat(directory): finish voice, vcs, operator logo, subtitle

This commit is contained in:
Haoyu Xu
2023-03-02 16:33:19 -05:00
parent 959178161c
commit b06d78b77e
15 changed files with 613 additions and 247 deletions

View File

@@ -1 +1 @@
3.3.28 3.3.47

View File

@@ -1,6 +1,51 @@
showcase: showcase:
2023/03/01: 2023/02/26:
- Under Construction :) - Rename w_fugue to w_wonder
2023/02/14:
- Added pozemka_snowy_plains_in_words
2023/02/11:
- Added Insights
- Added Operator Voice
2023/02/05:
- Added ability to move Operator Logo around
- Added VCs
2023/01/17:
- Added ling, chongyue
2023/01/16:
- Refactor the project to use nodeJS
2022/12/01:
- Added mizuki_summer_feast
2022/10/31:
- Added texas, relight/neral, become anew/rosmon, dream/passager
2022/10/24:
- Added lee_trust_your_eyes
2022/08/11:
- Added gavial, surtr#9
2022/05/01:
- Added w_fugue, specter, and skadi_sublimation
2022/03/29:
- Moved to Spine 3.8.99
- Added phatom
2022/01/23:
- Added dusk, ling, everything is a miracle/dusk, unfettered freedom/nian
2021/11/01:
- Added Nearl, W, and Rosmontis
2021/08/21:
- Updated skadi
- Added nian, chen
2021/05/26:
- First commit
directory: directory:
2023/03/02:
- Added Voice, VCs, Operator Logo
2023/03/01: 2023/03/01:
- Under Construction :) - Added Operator page
- Added Changelogs page
2023/02/28:
- Init Operator page
2023/02/26:
- Added Home page
2023/02/25:
- Added portrait images
2023/02/21:
- Init Directory

View File

@@ -1,3 +1,5 @@
VITE_APP_TITLE=AKLive2D 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_BACKGROUND_FOLDER="background"

View File

@@ -1 +1 @@
0.5.24 0.5.28

View File

@@ -92,7 +92,7 @@
} }
.popup .return-button { .popup .return-button {
color: #666; color: var(--button-color);
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
} }

View File

@@ -91,6 +91,34 @@
"zh-CN": "中文方言", "zh-CN": "中文方言",
"en-US": "Chinese Topolect" "en-US": "Chinese Topolect"
}, },
"steam_workshop": {
"zh-CN": "壁纸引擎版",
"en-US": "Wallpaper Engine Version"
},
"external_links": {
"zh-CN": "外部链接",
"en-US": "External Links"
},
"web_version": {
"zh-CN": "全功能网页版",
"en-US": "Full Feature Web Version"
},
"idle": {
"zh-CN": "待机",
"en-US": "Idle"
},
"interact": {
"zh-CN": "交互",
"en-US": "Interact"
},
"special": {
"zh-CN": "特殊",
"en-US": "Special"
},
"subtitle": {
"zh-CN": "字幕",
"en-US": "Subtitle"
},
"zh-CN": { "zh-CN": {
"zh-CN": "简体中文", "zh-CN": "简体中文",
"en-US": "Chinese (Simplified)" "en-US": "Chinese (Simplified)"
@@ -98,6 +126,18 @@
"en-US": { "en-US": {
"zh-CN": "英语", "zh-CN": "英语",
"en-US": "English" "en-US": "English"
},
"zh-TW": {
"zh-CN": "繁体中文",
"en-US": "Chinese (Traditional)"
},
"ja-JP": {
"zh-CN": "日语",
"en-US": "Japanese"
},
"ko-KR": {
"zh-CN": "韩语",
"en-US": "Korean"
} }
} }
} }

View File

@@ -96,7 +96,7 @@
} }
.item-group .item .item-info .item-title-container { .item-group .item .item-info .item-title-container {
color: white; color: var(--text-color-full);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;

View File

@@ -75,7 +75,7 @@ export default function Home() {
const isShown = useCallback((type) => currentTab === 'all' || currentTab === type, [currentTab]) const isShown = useCallback((type) => currentTab === 'all' || currentTab === type, [currentTab])
const playVoice = useCallback((link) => { const playVoice = useCallback((link) => {
const audioUrl = `/${link}/assets/voice/${import.meta.env.VITE_APP_VOICE_URL}` const audioUrl = `/${link}/assets/${JSON.parse(import.meta.env.VITE_VOICE_FOLDERS).main}/${import.meta.env.VITE_APP_VOICE_URL}`
if (!voiceOn || (audioEl.src === (window.location.href.replace(/\/$/g, '') + audioUrl) && isPlaying)) return if (!voiceOn || (audioEl.src === (window.location.href.replace(/\/$/g, '') + audioUrl) && isPlaying)) return
audioEl.src = audioUrl audioEl.src = audioUrl
let startPlayPromise = audioEl.play() let startPlayPromise = audioEl.play()

View File

@@ -1,144 +1,242 @@
.operator .spine-player-wrapper { .operator .spine-player-wrapper {
padding: 3rem 0; padding: 3rem 0 2rem 0;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
}
.operator .spine-settings {
margin-right: 1.5rem;
}
.operator .spine-settings .text {
color: var(--text-color);
}
.operator .spine-settings .settings-title-wrapper {
font-size: 1.25rem;
border-left: 3px solid currentColor;
padding-left: 0.75rem;
margin-bottom: 0.81rem;
}
.operator .spine-settings .settings-content-wrapper {
margin-bottom: 0.81rem;
}
.operator .spine-settings .settings-content-wrapper .content {
padding: 0.81rem 0;
cursor: pointer;
}
.operator .spine-settings .settings-content-wrapper .content .content-text {
pointer-events: none;
position: relative;
transform: translate3d(0, 0, 1px);
font-size: 1rem;
padding: 0.44rem 2.25rem 0.44rem 0.63rem;
background-color: rgba(67, 67, 67, 0.3);
background-image: repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.1) 0, rgba(255, 255, 255, 0.1) 1px, transparent 1px, transparent 4px);
transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.operator .spine-settings .settings-content-wrapper .content:hover,
.operator .spine-settings .settings-content-wrapper .content.active {
transform: translate3d(6px, 0, 1px);
}
.operator .spine-settings .settings-content-wrapper .content .content-text .outline {
width: 100%;
height: 100%;
box-sizing: content-box;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 6px;
border: rgba(214, 214, 214, 0.3) 1px dashed;
}
.operator .spine-settings .settings-content-wrapper .content .content-text::before,
.operator .spine-settings .settings-content-wrapper .content .content-text .outline {
content: "";
display: block;
position: absolute;
z-index: -1;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.operator .spine-settings .settings-content-wrapper .content .content-text .outline::before,
.operator .spine-settings .settings-content-wrapper .content .content-text .outline::after {
content: "";
display: block;
position: absolute;
left: -2px;
height: 2px;
width: 100%;
border-left: var(--text-color) solid 2px;
border-right: var(--text-color) solid 2px;
}
.operator .spine-settings .settings-content-wrapper .content .content-text .outline::before {
top: -2px;
}
.operator .spine-settings .settings-content-wrapper .content .content-text .outline::after {
bottom: -2px;
}
.operator .spine-settings .settings-content-wrapper .content .content-text::before {
right: 0;
top: 0;
width: 60%;
height: 100%;
background-image: linear-gradient(90deg, transparent, currentColor);
}
.operator .spine-settings .settings-content-wrapper .content:hover .content-text::before,
.operator .spine-settings .settings-content-wrapper .content.active .content-text::before,
.operator .spine-settings .settings-content-wrapper .content:hover .content-text .outline,
.operator .spine-settings .settings-content-wrapper .content.active .content-text .outline,
.operator .spine-settings .settings-content-wrapper .content.active .content-text .tick-icon {
opacity: 1;
visibility: visible;
}
.operator .spine-settings .settings-content-wrapper .content .content-text .tick-icon {
display: inline-block;
position: absolute;
z-index: 0;
right: 0.31rem;
top: 50%;
width: 0.5rem;
height: 1rem;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
border-right: var(--text-color) solid 0.25rem;
border-bottom: var(--text-color) solid 0.25rem;
transform: translate(-50%, -70%) rotate(45deg);
}
.operator .backgrounds-dropdown {
padding: 0;
}
.operator .backgrounds-dropdown .content {
padding-right: 1.5rem;
}
.operator .backgrounds-dropdown .icon {
bottom: 0;
right: 0;
}
.operator .backgrounds-dropdown .menu {
left: 0;
right: unset;
} }
.operator .spine-container { .operator .spine-container {
max-height: 720px; 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%;
position: relative;
margin-bottom: 2rem;
}
.operator .spine-container:before {
content: "";
display: block;
padding-top: 100%;
}
.operator .spine-container .spine-player {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.operator .spine-settings,
.operator .steam-workshop-wrapper {
margin-right: 1.5rem;
}
.operator .text {
color: var(--text-color);
}
.operator .spine-settings .settings-title-wrapper {
font-size: 1.25rem;
border-left: 3px solid currentColor;
padding-left: 0.75rem;
margin-bottom: 0.8rem;
}
.operator .styled-selection {
margin-bottom: 0.8rem;
}
.operator .styled-selection .content {
padding: 0.8rem 0;
cursor: pointer;
transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.operator .styled-selection .content .content-text {
pointer-events: none;
position: relative;
transform: translate3d(0, 0, 1px);
font-size: 1rem;
padding: 0.44rem 2.25rem 0.44rem 0.63rem;
background-color: var(--home-item-hover-background-color);
background-image: repeating-linear-gradient(
90deg,
var(--home-item-background-linear-gradient-color) 0,
var(--home-item-background-linear-gradient-color) 1px,
transparent 1px,
transparent 4px
);
transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.operator .styled-selection .content:hover,
.operator .styled-selection .content.active {
transform: translate3d(6px, 0, 1px);
}
.operator .styled-selection .content .content-text .outline {
width: 100%;
height: 100%;
left: -6px;
top: -6px;
border: var(--home-item-outline-color) 1px dashed;
padding: 6px;
}
.operator .styled-selection .content .content-text::before,
.operator .styled-selection .content .content-text .outline {
content: "";
display: block;
position: absolute;
z-index: -1;
opacity: 0;
visibility: hidden;
transition: opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s,
visibility cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
}
.operator .styled-selection .content .content-text .outline::before,
.operator .styled-selection .content .content-text .outline::after {
content: "";
display: block;
position: absolute;
left: -2px;
height: 2px;
width: 100%;
border-left: var(--text-color) solid 2px;
border-right: var(--text-color) solid 2px;
}
.operator .styled-selection .content .content-text .outline::before {
top: -2px;
}
.operator .styled-selection .content .content-text .outline::after {
bottom: -2px;
}
.operator .styled-selection .content .content-text::before {
right: 0;
top: 0;
width: 60%;
height: 100%;
background-image: linear-gradient(90deg, transparent, currentColor);
}
.operator .styled-selection .content:hover .content-text::before,
.operator .styled-selection .content.active .content-text::before,
.operator .styled-selection .content:hover .content-text .outline,
.operator .styled-selection .content.active .content-text .outline,
.operator .styled-selection .content.active .content-text .tick-icon {
opacity: 1;
visibility: visible;
}
.operator .styled-selection .content .content-text .tick-icon {
display: inline-block;
position: absolute;
z-index: 0;
right: 0.31rem;
top: 50%;
width: 0.5rem;
height: 1rem;
opacity: 0;
visibility: hidden;
transition: opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s,
visibility cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
border-right: var(--text-color) solid 0.25rem;
border-bottom: var(--text-color) solid 0.25rem;
transform: translate(-50%, -70%) rotate(45deg);
}
.operator .voice-wrapper {
position: absolute;
left: 0;
bottom: 30%;
z-index: 1;
max-width: 480px;
width: 85%;
opacity: 0;
margin: 16px;
transition: all 0.5s cubic-bezier(0.65, 0.05, 0.36, 1);
visibility: hidden;
font-family: 'Noto Sans', sans-serif;
}
.operator .voice-wrapper.active {
opacity: 1;
visibility: visible;
}
.operator .voice-wrapper .voice-title {
background-color: #9e9e9e;
color: #000;
display: inline-block;
position: absolute;
top: -12px;
left: -8px;
padding: 2px 8px;
font-size: 14px;
max-width: 180px;
width: 65%;
box-shadow: 0 3px 6px #00000080;
z-index: 1;
}
.operator .voice-wrapper .voice-subtitle {
background-color: #000000a6;
color: #fff;
padding: 16px;
font-size: 18px;
box-shadow: 0 6px 12px #00000080;
position: relative;
word-break: break-word;
}
.operator .voice-wrapper .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;
}
.operator .operator-logo {
position: absolute;
top: 0;
left: 0;
width: 30%;
height: auto;
opacity: 0.3;
}
@media (max-width: 1024px) {
.operator .spine-container {
width: 100%;
position: relative;
margin-bottom: 2rem;
}
.operator .spine-container:before {
content: "";
display: block;
padding-top: 100%;
}
.operator .spine-container .spine-player {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.operator .spine-player-wrapper {
flex-direction: column-reverse;
}
.operator .voice-wrapper {
bottom: 0;
}
}

View File

@@ -2,10 +2,12 @@ import {
useState, useState,
useEffect, useEffect,
useRef, useRef,
useCallback
} from 'react' } from 'react'
import { import {
useParams, useParams,
useNavigate useNavigate,
Link
} from "react-router-dom"; } from "react-router-dom";
import { atom, useAtom } from 'jotai'; import { atom, useAtom } from 'jotai';
import './operator.css' import './operator.css'
@@ -20,11 +22,11 @@ import spine from '!/libs/spine-player'
import '!/libs/spine-player.css' import '!/libs/spine-player.css'
import MainBorder from '@/component/main_border'; import MainBorder from '@/component/main_border';
import { useI18n } from '@/state/language'; import { useI18n } from '@/state/language';
import Dropdown from '@/component/dropdown';
const getVoiceFoler = (lang) => { const getVoiceFoler = (lang) => {
const folderObject = JSON.parse(import.meta.env.VITE_VOICE_FOLDERS) const folderObject = JSON.parse(import.meta.env.VITE_VOICE_FOLDERS)
return `${folderObject.main}/${folderObject.sub.find(e => e.lang === lang).name}` const voiceFolder = folderObject.sub.find(e => e.lang === lang) || folderObject.sub.find(e => e.name === 'custom')
return `${folderObject.main}/${voiceFolder.name}`
} }
const configAtom = atom(null); const configAtom = atom(null);
const spinePlayerAtom = atom(null); const spinePlayerAtom = atom(null);
@@ -44,11 +46,20 @@ export default function Operator(props) {
} = useHeader() } = useHeader()
const [config, setConfig] = useAtom(configAtom) const [config, setConfig] = useAtom(configAtom)
const [spineData, setSpineData] = useState(null) const [spineData, setSpineData] = useState(null)
const _trackEvt = useUmami(`/operator/${key}`) const _trackEvt = useUmami(`/${key}`)
const spineRef = useRef(null) const spineRef = useRef(null)
const [, setSpinePlayer] = useAtom(spinePlayerAtom) const [spineAnimation, setSpineAnimation] = useAtom(spineAnimationAtom)
const { currentBackground } = useBackgrounds() const { i18n } = useI18n()
const [spineAnimation,] = useAtom(spineAnimationAtom) const [spinePlayer, setSpinePlayer] = useAtom(spinePlayerAtom)
const [voiceLang, setVoiceLang] = useState(null)
const { backgrounds, currentBackground, setCurrentBackground } = useBackgrounds()
const [voiceConfig, setVoiceConfig] = useState(null)
const [subtitleLang, setSubtitleLang] = useState(null)
const [subtitle, setSubtitle] = useState(null)
const [hideSubtitle, setHideSubtitle] = useState(true)
const [isVoicePlaying, setIsVoicePlaying] = useState(false)
const [lastVoiceId, setLastVoiceId] = useState(null)
const [currentVoiceId, setCurrentVoiceId] = useState(null)
useEffect(() => { useEffect(() => {
setAppbarExtraArea([]) setAppbarExtraArea([])
@@ -59,13 +70,16 @@ export default function Operator(props) {
const config = operators.find((item) => item.link === key) const config = operators.find((item) => item.link === key)
if (config) { if (config) {
setConfig(config) setConfig(config)
fetch(`/_assets/${config.filename.replace("#", "%23")}.json`).then(res => res.json()).then(data => { fetch(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/${config.filename.replace("#", "%23")}.json`).then(res => res.json()).then(data => {
setSpineData(data) setSpineData(data)
}) })
setHeaderIcon(config.type) setHeaderIcon(config.type)
if (spineRef.current?.children.length > 0) { if (spineRef.current?.children.length > 0) {
spineRef.current?.removeChild(spineRef.current?.children[0]) spineRef.current?.removeChild(spineRef.current?.children[0])
} }
fetch(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/voice_${config.link}.json`).then(res => res.json()).then(data => {
setVoiceConfig(data)
})
} }
}, [operators, key]) }, [operators, key])
@@ -137,41 +151,77 @@ export default function Operator(props) {
touch: false, touch: false,
fps: 60, fps: 60,
defaultMix: 0, defaultMix: 0,
success: (spinePlayer) => {
spinePlayer.setAnimation(spineAnimation, true)
}
})) }))
} }
}, [spineData]); }, [spineData]);
return ( useEffect(() => {
<section className="operator"> if (voiceConfig && voiceLang) {
<section className="spine-player-wrapper"> let subtitleObj = voiceConfig.subtitleLangs[subtitleLang || 'zh-CN']
<SpineSettingsElement /> let subtitleKey = 'default'
<section className="spine-container" ref={spineRef} style={{ if (subtitleObj[voiceLang]) {
backgroundImage: `url(/${key}/assets/background/${currentBackground})` subtitleKey = voiceLang
}} /> }
</section> subtitleObj = subtitleObj[subtitleKey]
<MainBorder /> const playVoice = () => {
</section> const voiceId = () => {
) const keys = Object.keys(subtitleObj)
} const id = keys[Math.floor((Math.random() * keys.length))]
return id === lastVoiceId ? voiceId() : id
}
const id = voiceId()
setLastVoiceId(id)
setCurrentVoiceId(id)
audioEl.src = `/${config.link}/assets/${getVoiceFoler(voiceLang)}/${id}.ogg`
let startPlayPromise = audioEl.play()
if (startPlayPromise !== undefined) {
startPlayPromise
.then(() => {
setIsVoicePlaying(true)
const audioEndedFunc = () => {
setIsVoicePlaying(false)
audioEl.removeEventListener('ended', audioEndedFunc)
if (currentVoiceId !== id) return
setSubtitle(null)
}
if (subtitleLang) showSubtitle(id)
audioEl.addEventListener('ended', audioEndedFunc)
})
.catch(() => {
return
})
}
}
const showSubtitle = (id) => {
setSubtitle(subtitleObj[id])
setHideSubtitle(false)
}
spineRef.current?.addEventListener('click', playVoice)
return () => {
spineRef.current?.removeEventListener('click', playVoice)
}
}
}, [voiceLang, spineRef, voiceConfig, subtitleLang, lastVoiceId])
function SpineSettingsElement() {
const [config,] = useAtom(configAtom)
const { i18n } = useI18n()
const [spineAnimation, setSpineAnimation] = useAtom(spineAnimationAtom)
const [spinePlayer,] = useAtom(spinePlayerAtom)
const [voiceLang, setVoiceLang] = useState(null)
const { backgrounds, currentBackground, setCurrentBackground } = useBackgrounds() useEffect(() => {
if (!isVoicePlaying && !hideSubtitle) {
const hideSubtitle = () => {
setHideSubtitle(true)
}
setTimeout(hideSubtitle, 5 * 1000)
return () => {
clearTimeout(hideSubtitle)
}
}
}, [isVoicePlaying, hideSubtitle])
const spineSettings = [ const spineSettings = [
{ {
name: 'animation', name: 'animation',
options: [ options: [
{ {
name: 'Idle', name: 'idle',
onClick: () => { onClick: () => {
spinePlayer.setAnimation("Idle", true) spinePlayer.setAnimation("Idle", true)
setSpineAnimation('Idle') setSpineAnimation('Idle')
@@ -180,7 +230,7 @@ function SpineSettingsElement() {
return spineAnimation === 'Idle' return spineAnimation === 'Idle'
} }
}, { }, {
name: 'Interact', name: 'interact',
onClick: () => { onClick: () => {
spinePlayer.setAnimation("Interact", true) spinePlayer.setAnimation("Interact", true)
setSpineAnimation('Interact') setSpineAnimation('Interact')
@@ -189,7 +239,7 @@ function SpineSettingsElement() {
return spineAnimation === 'Interact' return spineAnimation === 'Interact'
} }
}, { }, {
name: 'Special', name: 'special',
onClick: () => { onClick: () => {
spinePlayer.setAnimation("Special", true) spinePlayer.setAnimation("Special", true)
setSpineAnimation('Special') setSpineAnimation('Special')
@@ -201,7 +251,7 @@ function SpineSettingsElement() {
] ]
}, { }, {
name: 'voice', name: 'voice',
options: config?.voiceLangs.map((item) => { options: voiceConfig && Object.keys(voiceConfig?.voiceLangs["zh-CN"]).map((item) => {
return { return {
name: i18n(item), name: i18n(item),
onClick: () => { onClick: () => {
@@ -216,61 +266,140 @@ function SpineSettingsElement() {
} }
} }
}) || [] }) || []
}, {
name: 'subtitle',
options: voiceConfig && Object.keys(voiceConfig?.subtitleLangs).map((item) => {
return {
name: i18n(item),
onClick: () => {
if (subtitleLang !== item) {
setSubtitleLang(item)
} else {
setSubtitleLang(null)
}
},
activeRule: () => {
return subtitleLang === item
}
}
}) || []
}, {
name: 'backgrounds',
options: backgrounds.map((item) => {
return {
name: item,
onClick: () => {
setCurrentBackground(item)
},
activeRule: () => {
return currentBackground === item
}
}
}) || []
} }
] ]
return ( return (
<section className="spine-settings" style={{ <section className="operator">
color: config?.color <section className="spine-player-wrapper">
}}> <section className="spine-settings" style={{
{ color: config?.color
spineSettings.map((item) => { }}>
if (item.options.length === 0) return null {
return ( spineSettings.map((item) => {
<section key={item.name}> if (item.options.length === 0) return null
<section className='settings-title-wrapper'> return (
<section className='text'>{i18n(item.name)}</section> <section key={item.name}>
</section> <section className='settings-title-wrapper'>
<section className='settings-content-wrapper'> <section className='text'>{i18n(item.name)}</section>
{item.options.map((option) => { </section>
return ( <section className='settings-content-wrapper styled-selection'>
<section className={`content ${option.activeRule && option.activeRule() ? 'active' : ''}`} onClick={() => option.onClick()} key={option.name}> {item.options.map((option) => {
return (
<section className={`content ${option.activeRule && option.activeRule() ? 'active' : ''}`} onClick={(e) => option.onClick(e)} key={option.name}>
<section className='content-text'>
<section className="outline" />
<section className='text'>{i18n(option.name)}</section>
<section className='tick-icon' />
</section>
</section>
)
})}
</section>
</section>
)
})
}
<section>
<section className='settings-title-wrapper'>
<section className='text'>{i18n('external_links')}</section>
</section>
<section className='settings-content-wrapper styled-selection'>
<Link
reloadDocument
to={`./index.html?settings`}
target='_blank'
className='extra-links-item'
style={{
color: config?.color
}}
>
<section className='content'>
<section className='content-text'>
<section className="outline" />
<section className='text'>
{i18n('web_version')}
</section>
</section>
</section>
</Link>
{
config?.workshopId && (
<Link
reloadDocument
to={`https://steamcommunity.com/sharedfiles/filedetails/?id=${config.workshopId}`}
target='_blank'
className='extra-links-item'
style={{
color: config?.color
}}>
<section className='content'>
<section className='content-text'> <section className='content-text'>
<section className="outline" /> <section className="outline" />
<section className='text'>{option.name}</section> <section className='text'>
<section className='tick-icon' /> {i18n('steam_workshop')}
</section>
</section> </section>
</section> </section>
) </Link>
})} )
</section>
</section>
)
})
}
<section className='settings-title-wrapper'>
<section className='text'>
<Dropdown
text={i18n('backgrounds')}
menu={backgrounds.map((item) => {
return {
name: item,
value: item
} }
})} </section>
onClick={(item) => { </section>
setCurrentBackground(item.name) </section>
}} <section className="spine-container" style={{
className='backgrounds-dropdown' backgroundImage: `url(/${key}/assets/${import.meta.env.VITE_BACKGROUND_FOLDER}/${currentBackground})`
activeRule={(item) => { }} >
return item?.name === currentBackground {
}} config && (
activeColor={{ <img src={`/${config.link}/assets/${config.logo}.png`} alt={config?.codename[language]} className='operator-logo'/>
color: config?.color )
}} }
/> <section ref={spineRef} />
{
subtitle && (
<section className={`voice-wrapper${hideSubtitle ? '' : ' active'}`}>
<section className='voice-title'>{subtitle.title}</section>
<section className='voice-subtitle'>
<span>{subtitle.text}</span>
<span className='voice-triangle' />
</section>
</section>
)
}
</section> </section>
</section> </section>
<MainBorder />
</section> </section>
) )
} }

View File

@@ -162,9 +162,12 @@
.main .main-header .main-tab { .main .main-header .main-tab {
flex: auto; flex: auto;
text-align: right; white-space: pre;
white-space: nowrap;
user-select: none; user-select: none;
display: flex;
flex-direction: row;
justify-content: flex-end;
overflow: hidden;
} }
.main .main-header .main-tab .main-tab-item { .main .main-header .main-tab .main-tab-item {
@@ -178,11 +181,24 @@
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
} }
.main .main-header .main-tab .main-tab-item .main-tab-text-wrapper {
overflow: hidden;
text-overflow: ellipsis;
}
.main .main-header .main-tab .main-tab-item.active .main-tab-text-wrapper,
.main .main-header .main-tab .main-tab-item:hover .main-tab-text-wrapper,
.main .main-header .main-tab .main-tab-item.active .text, .main .main-header .main-tab .main-tab-item.active .text,
.main .main-header .main-tab .main-tab-item:hover .text { .main .main-header .main-tab .main-tab-item:hover .text {
color: currentColor; color: currentColor
}
.main .main-header .main-tab .main-tab-item.active {
border-bottom-color: currentColor;
} }
.main .main-header .main-tab .main-tab-item.active, .main .main-header .main-tab .main-tab-item.active,
@@ -190,13 +206,9 @@
color: var(--link-highlight-color); color: var(--link-highlight-color);
} }
.main .main-header .main-tab .main-tab-item.active { .main .main-header .main-tab .main-tab-item .main-tab-text-wrapper {
border-bottom-color: currentColor;
}
.main .main-header .main-tab .main-tab-item .text {
color: var(--text-color); color: var(--text-color);
transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
} }
.main .main-icon { .main .main-icon {
@@ -211,7 +223,7 @@
position: absolute; position: absolute;
right: -4rem; right: -4rem;
bottom: -24px; bottom: -24px;
color: #666; color: var(--button-color);
transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s;
} }

View File

@@ -1,7 +1,6 @@
import { import {
useState, useState,
useEffect, useEffect,
useContext,
useMemo, useMemo,
useCallback useCallback
} from 'react' } from 'react'
@@ -53,7 +52,10 @@ export default function Root(props) {
}} }}
style={item.style} style={item.style}
> >
<span className='text'>{i18n(item.key)}</span> <section className='main-tab-text-wrapper'>
<span className='text'>{i18n(item.key)}</span>
</section>
</section> </section>
) )
})) }))
@@ -147,7 +149,7 @@ export default function Root(props) {
<main className='main'> <main className='main'>
<section className='main-header'> <section className='main-header'>
<section className='main-title'> <section className='main-title'>
{headerIcon && ( {headerIcon && (
<section className='main-icon'> <section className='main-icon'>
<CharIcon <CharIcon
type={headerIcon} type={headerIcon}
@@ -156,8 +158,8 @@ export default function Root(props) {
} }
/> />
</section> </section>
)} )}
{title} {title}
</section> </section>
<section className='main-tab'> <section className='main-tab'>
{headerTabs} {headerTabs}

View File

@@ -11,7 +11,6 @@ export function useConfig() {
const [version, setVersion] = useAtom(versionAtom); const [version, setVersion] = useAtom(versionAtom);
const [operators, setOperators] = useAtom(operatorsAtom); const [operators, setOperators] = useAtom(operatorsAtom);
useEffect(() => { useEffect(() => {
fetcher('/_assets/directory.json').then(data => { fetcher('/_assets/directory.json').then(data => {
setConfig(data); setConfig(data);

View File

@@ -1,5 +1,5 @@
import path from 'path' import path from 'path'
import { writeSync, copy, rmdir } from './file.js' import { writeSync, copy, rmdir, readSync as readFile } from './file.js'
import { read } from './yaml.js'; import { read } from './yaml.js';
/** /**
@@ -22,17 +22,49 @@ export default function ({ backgrounds, charwordTable }) {
} else { } else {
acc[date] = [cur] acc[date] = [cur]
} }
cur.voiceLangs = []
const voiceInfo = Object.values(charwordTable.lookup(cur.link).operator.info.zh_CN) cur.workshopId = null
voiceInfo.forEach((item) => { try {
cur.voiceLangs = [...cur.voiceLangs, ...Object.keys(item)] cur.workshopId = JSON.parse(readFile(path.join(__projetRoot, __config.folder.operator, cur.link, 'project.json'))).workshopid
}) } catch (e) {
console.log(`No workshop id for ${cur.link}!`, e)
}
return acc return acc
}, {})) }, {}))
.sort((a, b) => Date.parse(b[0].date) - Date.parse(a[0].date)), .sort((a, b) => Date.parse(b[0].date) - Date.parse(a[0].date)),
} }
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 = []

View File

@@ -149,6 +149,13 @@ class ViteRunner {
}, { }, {
key: "voice_folders", key: "voice_folders",
value: JSON.stringify(this.#globalConfig.folder.voice) value: JSON.stringify(this.#globalConfig.folder.voice)
}, {
key: "directory_folder",
value: JSON.stringify(this.#globalConfig.folder.directory)
}
, {
key: "background_folder",
value: JSON.stringify(this.#globalConfig.folder.background)
} }
]), path.join(directoryDir, '.env')) ]), path.join(directoryDir, '.env'))
this.#mode = process.argv[3] this.#mode = process.argv[3]