From d6e7bc20d353d1e352605028e6b93223352e6066 Mon Sep 17 00:00:00 2001 From: Haoyu Xu Date: Sat, 22 Feb 2025 15:11:30 +0800 Subject: [PATCH] feat: migrate to turbo (#22) * feat: migrate top turbo * ci: ci test * fix: fix codeql issues * feat: ci test * chore: lint * chore: misc changes * feat: rename vite helpers * feat: use fetch to handle assets * feat: update directory * feat: fetch charword table * feat: migrate download game data and detect missing voice files * feat: symlink relative path * feat: finish wrangler upload * feat: migrate wrangler download * feat: finish * chore: auto update * ci: update ci * ci: update ci --------- Co-authored-by: Halyul --- .eslintrc.json | 15 - .github/workflows/cf-pages.yaml | 45 +- .github/workflows/update-charwords.yaml | 32 - .github/workflows/update-music.yaml | 32 - .../workflows/update-offical-dyn-info.yaml | 32 - .github/workflows/update.yaml | 32 + .gitignore | 5 +- .postcssrc.json | 6 - .stylelintrc.json | 3 - .vscode/launch.json | 63 +- README.md | 73 +- aklive2d.js | 309 ---- {directory => apps/directory}/.gitignore | 2 + apps/directory/.prettierignore | 3 + apps/directory/eslint.config.js | 30 + apps/directory/index.html | 86 + apps/directory/jsconfig.json | 9 + apps/directory/package.json | 43 + apps/directory/postcss.config.js | 5 + apps/directory/prettier.config.js | 11 + apps/directory/runner.js | 24 + apps/directory/src/App.jsx | 23 + apps/directory/src/App.scss | 103 ++ apps/directory/src/component/border.jsx | 9 + apps/directory/src/component/char_icon.jsx | 28 + apps/directory/src/component/dropdown.jsx | 99 ++ apps/directory/src/component/popup.jsx | 48 + .../directory/src/component/return_button.jsx | 38 + .../src/component/scss/border.module.scss | 30 + .../src/component/scss/dropdown.module.scss | 187 +++ .../src/component/scss/popup.module.scss | 96 ++ .../component/scss/return_button.module.scss | 55 + .../src/component/scss/search_box.module.scss | 78 + .../src/component/scss/switch.module.scss | 64 + .../component/scss/totop_button.module.scss | 43 + apps/directory/src/component/search_box.jsx | 50 + apps/directory/src/component/switch.jsx | 35 + apps/directory/src/component/totop_button.jsx | 64 + apps/directory/src/component/voice.jsx | 46 + apps/directory/src/i18n.json | 161 ++ apps/directory/src/routes/Error.jsx | 197 +++ apps/directory/src/routes/Root.jsx | 307 ++++ apps/directory/src/routes/index.jsx | 29 + apps/directory/src/routes/path/Home.jsx | 451 +++++ apps/directory/src/routes/path/Operator.jsx | 694 ++++++++ apps/directory/src/scss/_main_share.scss | 20 + apps/directory/src/scss/_page_base.scss | 369 +++++ .../scss/changelogs/Changelogs.module.scss | 30 + .../src/scss/error/Error.module.scss | 61 + apps/directory/src/scss/home/Home.module.scss | 23 + .../src/scss/operator/Operator.module.scss | 142 ++ apps/directory/src/scss/root/Root.module.scss | 95 ++ .../src/scss/root/drawer.module.scss | 55 + .../src/scss/root/footer.module.scss | 42 + .../src/scss/root/header.module.scss | 106 ++ apps/directory/src/state/appbar.js | 11 + apps/directory/src/state/config.js | 31 + apps/directory/src/state/header.js | 43 + apps/directory/src/state/insight.js | 20 + apps/directory/src/state/language.js | 38 + apps/directory/stylelint.config.js | 5 + apps/directory/vite.config.js | 45 + apps/module/.eslintignore | 1 + apps/module/.prettierignore | 1 + apps/module/index.js | 4 + .../src => apps/module}/libs/spine-player.css | 0 .../src => apps/module}/libs/spine-player.js | 78 +- apps/module/package.json | 7 + apps/showcase/.gitignore | 26 + apps/showcase/.prettierignore | 3 + apps/showcase/.stylelintignore | 1 + apps/showcase/eslint.config.js | 3 + apps/showcase/index.html | 22 + apps/showcase/index.js | 16 + apps/showcase/jsconfig.json | 9 + apps/showcase/package.json | 26 + apps/showcase/prettier.config.js | 11 + apps/showcase/runner.js | 38 + .../showcase}/src/components/aklive2d.css | 2 +- apps/showcase/src/components/aklive2d.js | 257 +++ apps/showcase/src/components/background.css | 23 + apps/showcase/src/components/background.js | 298 ++++ apps/showcase/src/components/events.js | 11 + .../showcase}/src/components/fallback.css | 4 +- apps/showcase/src/components/fallback.js | 40 + apps/showcase/src/components/helper.js | 101 ++ apps/showcase/src/components/insight.js | 46 + apps/showcase/src/components/logo.css | 6 + apps/showcase/src/components/logo.js | 411 +++++ apps/showcase/src/components/music.js | 365 +++++ .../showcase}/src/components/player.css | 2 +- apps/showcase/src/components/player.js | 588 +++++++ .../showcase}/src/components/voice.css | 7 +- apps/showcase/src/components/voice.js | 641 ++++++++ .../src/components/wallpaper_engine.js | 376 +++++ apps/showcase/src/index.css | 22 + apps/showcase/src/index.js | 6 + apps/showcase/stylelint.config.js | 5 + apps/showcase/vite.config.js | 57 + config.yaml | 99 -- config/_directory.yaml | 12 - config/_project_json.yaml | 369 ----- data/auto_update/charword_table.json | 1 - directory/index.html | 84 - directory/src/App.jsx | 24 - directory/src/App.scss | 100 -- directory/src/component/border.jsx | 14 - directory/src/component/char_icon.jsx | 21 - directory/src/component/dropdown.jsx | 94 -- directory/src/component/popup.jsx | 42 - directory/src/component/return_button.jsx | 39 - .../src/component/scss/border.module.scss | 27 - .../src/component/scss/dropdown.module.scss | 188 --- .../src/component/scss/popup.module.scss | 94 -- .../component/scss/return_button.module.scss | 46 - .../src/component/scss/search_box.module.scss | 77 - .../src/component/scss/switch.module.scss | 60 - .../component/scss/totop_button.module.scss | 43 - directory/src/component/search_box.jsx | 49 - directory/src/component/switch.jsx | 37 - directory/src/component/totop_button.jsx | 62 - directory/src/component/voice.jsx | 53 - directory/src/i18n.json | 163 -- directory/src/routes/Error.jsx | 196 --- directory/src/routes/Root.jsx | 316 ---- directory/src/routes/index.jsx | 28 - directory/src/routes/path/Home.jsx | 348 ---- directory/src/routes/path/Operator.jsx | 595 ------- directory/src/scss/_main_share.scss | 20 - directory/src/scss/_page_base.scss | 355 ---- .../scss/changelogs/Changelogs.module.scss | 29 - directory/src/scss/error/Error.module.scss | 57 - directory/src/scss/home/Home.module.scss | 21 - .../src/scss/operator/Operator.module.scss | 142 -- directory/src/scss/root/Root.module.scss | 91 - directory/src/scss/root/drawer.module.scss | 54 - directory/src/scss/root/footer.module.scss | 37 - directory/src/scss/root/header.module.scss | 103 -- directory/src/state/appbar.js | 10 - directory/src/state/config.js | 30 - directory/src/state/header.js | 37 - directory/src/state/insight.js | 17 - directory/src/state/language.js | 35 - download_packs.sh | 53 - eslint.config.js | 5 + game_data | 1 + jsconfig.json | 10 - libs/alpha_composite.js | 46 - libs/append.js | 10 - libs/assets_processor.js | 82 - libs/background.js | 64 - libs/cf_pages.js | 150 -- libs/charword_table.js | 134 -- libs/config.js | 30 - libs/content_processor.js | 64 - libs/directory.js | 86 - libs/downloader.js | 37 - libs/env_generator.js | 7 - libs/file.js | 83 - libs/initializer.js | 23 - libs/music.js | 90 - libs/offical_info.js | 93 -- libs/project_json.js | 90 - package.json | 66 +- packages/assets/.gitignore | 1 + packages/assets/.prettierignore | 3 + packages/assets/config.yaml | 8 + packages/assets/eslint.config.js | 3 + packages/assets/index.js | 12 + packages/assets/libs/build.js | 49 + packages/assets/libs/download.js | 57 + packages/assets/package.json | 21 + packages/assets/prettier.config.js | 11 + packages/assets/runner.js | 30 + packages/background/.gitignore | 1 + packages/background/.prettierignore | 3 + packages/background/eslint.config.js | 3 + packages/background/index.js | 84 + packages/background/package.json | 19 + packages/background/prettier.config.js | 11 + packages/background/runner.js | 22 + packages/charword-table/.prettierignore | 3 + .../charword_table_en_US_1739039571000.json | 0 .../charword_table_ja_JP_1739039571000.json | 0 .../charword_table_ko_KR_1739039571000.json | 0 .../charword_table_zh_CN_1739520152000.json | 1 + packages/charword-table/eslint.config.js | 3 + packages/charword-table/index.js | 267 +++ packages/charword-table/package.json | 20 + packages/charword-table/prettier.config.js | 11 + packages/charword-table/runner.js | 31 + packages/config/.prettierignore | 3 + packages/config/config.yaml | 96 ++ packages/config/eslint.config.js | 3 + packages/config/index.js | 4 + packages/config/package.json | 15 + packages/config/prettier.config.js | 11 + packages/downloader/.prettierignore | 3 + packages/downloader/eslint.config.js | 3 + packages/downloader/index.js | 112 ++ packages/downloader/package.json | 16 + packages/downloader/prettier.config.js | 11 + packages/eslint-config/index.js | 23 + packages/eslint-config/package.json | 17 + packages/libs/.prettierignore | 3 + packages/libs/eslint.config.js | 3 + packages/libs/index.js | 13 + packages/libs/libs/alpha_composite.js | 48 + packages/libs/libs/env.js | 7 + packages/libs/libs/env_parser.js | 43 + packages/libs/libs/error.js | 6 + packages/libs/libs/file.js | 166 ++ {libs => packages/libs/libs}/yaml.js | 8 +- packages/libs/package.json | 18 + packages/libs/prettier.config.js | 11 + packages/music/.gitignore | 1 + packages/music/.prettierignore | 3 + .../auto_update/audio_data_1740038433000.json | 1 + .../display_meta_table_1738742430000.json | 0 .../music}/auto_update/music_table.json | 0 packages/music/eslint.config.js | 3 + packages/music/index.js | 117 ++ packages/music/package.json | 18 + packages/music/prettier.config.js | 11 + packages/music/runner.js | 20 + packages/official-info/.prettierignore | 3 + .../auto_update/official_info.json | 60 - packages/official-info/eslint.config.js | 3 + packages/official-info/index.js | 101 ++ packages/official-info/package.json | 18 + packages/official-info/prettier.config.js | 11 + packages/official-info/runner.js | 20 + packages/operator/.gitignore | 1 + packages/operator/.prettierignore | 3 + packages/operator/config.yaml | 60 + .../operator/config}/_template.yaml | 8 +- .../config}/amiya_solo_around_the_world.yaml | 6 +- .../operator/config}/chen.yaml | 8 +- .../config}/chen_ten_thousand_mountains.yaml | 6 +- .../operator/config}/chongyue.yaml | 8 +- .../operator/config}/chongyue_alighting.yaml | 6 +- .../config}/chongyue_allround_actor.yaml | 6 +- .../degenbrecher_the_shadow_of_dark_moon.yaml | 6 +- .../operator/config}/dusk.yaml | 8 +- .../config}/dusk_everything_is_a_miracle.yaml | 8 +- ...xecutor_the_ex_foedere_allmind_as_one.yaml | 6 +- .../config}/eyjafjalla_the_hvit_aska.yaml | 6 +- ...hvit_aska_a_picnic_before_a_long_trip.yaml | 6 +- .../operator/config}/gavial.yaml | 8 +- .../gavial_the_invincible_holiday_hd26.yaml | 6 +- .../goldenglow_summer_flowers_fa394.yaml | 6 +- .../config}/ines_under_the_flaming_dome.yaml | 6 +- .../operator/config}/kaltsit_remnant.yaml | 6 +- .../config}/lappland_the_decadenza.yaml | 6 +- .../operator/config}/lee_trust_your_eyes.yaml | 8 +- .../operator/config}/lin_heavenly_mirage.yaml | 6 +- .../operator/config}/ling.yaml | 8 +- .../ling_it_does_wash_the_strings.yaml | 8 +- .../ling_towering_is_cliff_of_nostalgia.yaml | 6 +- .../operator/config}/mizuki_summer_feast.yaml | 8 +- .../operator/config}/muelsyse.yaml | 6 +- .../config}/muelsyse_young_branch.yaml | 6 +- .../operator/config}/mwynar_w_dali.yaml | 6 +- .../operator/config}/nearl.yaml | 8 +- .../operator/config}/nearl_relight.yaml | 8 +- .../operator/config}/nian.yaml | 8 +- .../config}/nian_thunderbolt_director.yaml | 6 +- .../config}/nian_unfettered_freedom.yaml | 8 +- .../config}/nightingale_iakhu_of_flows.yaml | 6 +- .../config}/passager_dream_in_a_moment.yaml | 8 +- .../operator/config}/pepe.yaml | 6 +- .../operator/config}/phatom_focus.yaml | 8 +- .../pozemka_snowy_plains_in_words.yaml | 8 +- .../reed_the_frame_shadow_curator.yaml | 6 +- .../reed_the_frame_shadow_summer_flower.yaml | 6 +- .../operator/config}/rosmontis.yaml | 8 +- .../config}/rosmontis_become_anew.yaml | 8 +- {config => packages/operator/config}/shu.yaml | 6 +- .../operator/config}/shu_spring_feast.yaml | 6 +- .../config}/silverash_never_melting_ice.yaml | 6 +- .../operator/config}/skadi.yaml | 8 +- .../operator/config}/skadi_sublimation.yaml | 8 +- ...adi_the_corrupting_heart_red_countess.yaml | 6 +- .../operator/config}/specter.yaml | 8 +- .../operator/config}/specter_born_as_one.yaml | 6 +- .../config}/surtr_colorful_wonderland.yaml | 8 +- .../operator/config}/texas_the_omertosa.yaml | 8 +- .../texas_the_omertosa_il_se_de_no.yaml | 6 +- .../texas_the_omertosa_wingbreaker.yaml | 6 +- .../operator/config}/virtuosa.yaml | 6 +- .../config}/virtuosa_diversity_oneness.yaml | 6 +- {config => packages/operator/config}/w.yaml | 8 +- .../operator/config}/w_wonder.yaml | 8 +- .../operator/config}/wisadel.yaml | 6 +- {config => packages/operator/config}/yu.yaml | 6 +- .../config}/zuole_youthful_journey.yaml | 6 +- packages/operator/eslint.config.js | 3 + packages/operator/index.js | 276 ++++ packages/operator/package.json | 20 + packages/operator/prettier.config.js | 11 + packages/operator/runner.js | 38 + packages/postcss-config/index.js | 6 + packages/postcss-config/package.json | 14 + packages/prettier-config/index.js | 14 + packages/prettier-config/package.json | 11 + packages/project-json/.prettierignore | 3 + packages/project-json/eslint.config.js | 3 + packages/project-json/index.js | 127 ++ .../project-json/libs/content_processor.js | 68 + packages/project-json/package.json | 21 + packages/project-json/prettier.config.js | 11 + packages/project-json/project.json | 25 + packages/project-json/project_json.yaml | 369 +++++ packages/project-json/runner.js | 26 + packages/stylelint-config/index.js | 3 + packages/stylelint-config/package.json | 15 + packages/vite-helpers/.prettierignore | 3 + packages/vite-helpers/eslint.config.js | 3 + packages/vite-helpers/index.js | 235 +++ packages/vite-helpers/package.json | 22 + packages/vite-helpers/prettier.config.js | 11 + packages/wrangler/.prettierignore | 3 + packages/wrangler/data/background | 1 + packages/wrangler/data/music | 1 + packages/wrangler/data/operator | 1 + packages/wrangler/eslint.config.js | 5 + packages/wrangler/index.js | 369 +++++ packages/wrangler/package.json | 26 + packages/wrangler/prettier.config.js | 11 + packages/wrangler/runner.js | 26 + pnpm-lock.yaml | 1460 +++++++++++++---- pnpm-workspace.yaml | 3 + prettier.config.js | 11 + showcase/index.html | 19 - showcase/src/components/aklive2d.js | 167 -- showcase/src/components/background.css | 23 - showcase/src/components/background.js | 259 --- showcase/src/components/events.js | 15 - showcase/src/components/fallback.js | 30 - showcase/src/components/helper.js | 90 - showcase/src/components/insight.js | 39 - showcase/src/components/logo.css | 6 - showcase/src/components/logo.js | 356 ---- showcase/src/components/music.js | 322 ---- showcase/src/components/player.js | 471 ------ showcase/src/components/voice.js | 584 ------- showcase/src/index.css | 22 - showcase/src/index.js | 7 - showcase/src/libs/wallpaper_engine.js | 155 -- stylelint.config.js | 5 + turbo.json | 110 ++ vite.config.js | 210 --- 352 files changed, 12911 insertions(+), 9411 deletions(-) delete mode 100644 .eslintrc.json delete mode 100644 .github/workflows/update-charwords.yaml delete mode 100644 .github/workflows/update-music.yaml delete mode 100644 .github/workflows/update-offical-dyn-info.yaml create mode 100644 .github/workflows/update.yaml delete mode 100644 .postcssrc.json delete mode 100644 .stylelintrc.json delete mode 100644 aklive2d.js rename {directory => apps/directory}/.gitignore (98%) create mode 100644 apps/directory/.prettierignore create mode 100644 apps/directory/eslint.config.js create mode 100644 apps/directory/index.html create mode 100644 apps/directory/jsconfig.json create mode 100644 apps/directory/package.json create mode 100644 apps/directory/postcss.config.js create mode 100644 apps/directory/prettier.config.js create mode 100644 apps/directory/runner.js create mode 100644 apps/directory/src/App.jsx create mode 100644 apps/directory/src/App.scss create mode 100644 apps/directory/src/component/border.jsx create mode 100644 apps/directory/src/component/char_icon.jsx create mode 100644 apps/directory/src/component/dropdown.jsx create mode 100644 apps/directory/src/component/popup.jsx create mode 100644 apps/directory/src/component/return_button.jsx create mode 100644 apps/directory/src/component/scss/border.module.scss create mode 100644 apps/directory/src/component/scss/dropdown.module.scss create mode 100644 apps/directory/src/component/scss/popup.module.scss create mode 100644 apps/directory/src/component/scss/return_button.module.scss create mode 100644 apps/directory/src/component/scss/search_box.module.scss create mode 100644 apps/directory/src/component/scss/switch.module.scss create mode 100644 apps/directory/src/component/scss/totop_button.module.scss create mode 100644 apps/directory/src/component/search_box.jsx create mode 100644 apps/directory/src/component/switch.jsx create mode 100644 apps/directory/src/component/totop_button.jsx create mode 100644 apps/directory/src/component/voice.jsx create mode 100644 apps/directory/src/i18n.json create mode 100644 apps/directory/src/routes/Error.jsx create mode 100644 apps/directory/src/routes/Root.jsx create mode 100644 apps/directory/src/routes/index.jsx create mode 100644 apps/directory/src/routes/path/Home.jsx create mode 100644 apps/directory/src/routes/path/Operator.jsx create mode 100644 apps/directory/src/scss/_main_share.scss create mode 100644 apps/directory/src/scss/_page_base.scss create mode 100644 apps/directory/src/scss/changelogs/Changelogs.module.scss create mode 100644 apps/directory/src/scss/error/Error.module.scss create mode 100644 apps/directory/src/scss/home/Home.module.scss create mode 100644 apps/directory/src/scss/operator/Operator.module.scss create mode 100644 apps/directory/src/scss/root/Root.module.scss create mode 100644 apps/directory/src/scss/root/drawer.module.scss create mode 100644 apps/directory/src/scss/root/footer.module.scss create mode 100644 apps/directory/src/scss/root/header.module.scss create mode 100644 apps/directory/src/state/appbar.js create mode 100644 apps/directory/src/state/config.js create mode 100644 apps/directory/src/state/header.js create mode 100644 apps/directory/src/state/insight.js create mode 100644 apps/directory/src/state/language.js create mode 100644 apps/directory/stylelint.config.js create mode 100644 apps/directory/vite.config.js create mode 100644 apps/module/.eslintignore create mode 100644 apps/module/.prettierignore create mode 100644 apps/module/index.js rename {showcase/src => apps/module}/libs/spine-player.css (100%) rename {showcase/src => apps/module}/libs/spine-player.js (99%) create mode 100644 apps/module/package.json create mode 100644 apps/showcase/.gitignore create mode 100644 apps/showcase/.prettierignore create mode 100644 apps/showcase/.stylelintignore create mode 100644 apps/showcase/eslint.config.js create mode 100644 apps/showcase/index.html create mode 100644 apps/showcase/index.js create mode 100644 apps/showcase/jsconfig.json create mode 100644 apps/showcase/package.json create mode 100644 apps/showcase/prettier.config.js create mode 100644 apps/showcase/runner.js rename {showcase => apps/showcase}/src/components/aklive2d.css (98%) create mode 100644 apps/showcase/src/components/aklive2d.js create mode 100644 apps/showcase/src/components/background.css create mode 100644 apps/showcase/src/components/background.js create mode 100644 apps/showcase/src/components/events.js rename {showcase => apps/showcase}/src/components/fallback.css (98%) create mode 100644 apps/showcase/src/components/fallback.js create mode 100644 apps/showcase/src/components/helper.js create mode 100644 apps/showcase/src/components/insight.js create mode 100644 apps/showcase/src/components/logo.css create mode 100644 apps/showcase/src/components/logo.js create mode 100644 apps/showcase/src/components/music.js rename {showcase => apps/showcase}/src/components/player.css (96%) create mode 100644 apps/showcase/src/components/player.js rename {showcase => apps/showcase}/src/components/voice.css (92%) create mode 100644 apps/showcase/src/components/voice.js create mode 100644 apps/showcase/src/components/wallpaper_engine.js create mode 100644 apps/showcase/src/index.css create mode 100644 apps/showcase/src/index.js create mode 100644 apps/showcase/stylelint.config.js create mode 100644 apps/showcase/vite.config.js delete mode 100644 config.yaml delete mode 100644 config/_directory.yaml delete mode 100644 config/_project_json.yaml delete mode 100644 data/auto_update/charword_table.json delete mode 100644 directory/index.html delete mode 100644 directory/src/App.jsx delete mode 100644 directory/src/App.scss delete mode 100644 directory/src/component/border.jsx delete mode 100644 directory/src/component/char_icon.jsx delete mode 100644 directory/src/component/dropdown.jsx delete mode 100644 directory/src/component/popup.jsx delete mode 100644 directory/src/component/return_button.jsx delete mode 100644 directory/src/component/scss/border.module.scss delete mode 100644 directory/src/component/scss/dropdown.module.scss delete mode 100644 directory/src/component/scss/popup.module.scss delete mode 100644 directory/src/component/scss/return_button.module.scss delete mode 100644 directory/src/component/scss/search_box.module.scss delete mode 100644 directory/src/component/scss/switch.module.scss delete mode 100644 directory/src/component/scss/totop_button.module.scss delete mode 100644 directory/src/component/search_box.jsx delete mode 100644 directory/src/component/switch.jsx delete mode 100644 directory/src/component/totop_button.jsx delete mode 100644 directory/src/component/voice.jsx delete mode 100644 directory/src/i18n.json delete mode 100644 directory/src/routes/Error.jsx delete mode 100644 directory/src/routes/Root.jsx delete mode 100644 directory/src/routes/index.jsx delete mode 100644 directory/src/routes/path/Home.jsx delete mode 100644 directory/src/routes/path/Operator.jsx delete mode 100644 directory/src/scss/_main_share.scss delete mode 100644 directory/src/scss/_page_base.scss delete mode 100644 directory/src/scss/changelogs/Changelogs.module.scss delete mode 100644 directory/src/scss/error/Error.module.scss delete mode 100644 directory/src/scss/home/Home.module.scss delete mode 100644 directory/src/scss/operator/Operator.module.scss delete mode 100644 directory/src/scss/root/Root.module.scss delete mode 100644 directory/src/scss/root/drawer.module.scss delete mode 100644 directory/src/scss/root/footer.module.scss delete mode 100644 directory/src/scss/root/header.module.scss delete mode 100644 directory/src/state/appbar.js delete mode 100644 directory/src/state/config.js delete mode 100644 directory/src/state/header.js delete mode 100644 directory/src/state/insight.js delete mode 100644 directory/src/state/language.js delete mode 100755 download_packs.sh create mode 100644 eslint.config.js create mode 120000 game_data delete mode 100644 jsconfig.json delete mode 100644 libs/alpha_composite.js delete mode 100644 libs/append.js delete mode 100644 libs/assets_processor.js delete mode 100644 libs/background.js delete mode 100644 libs/cf_pages.js delete mode 100644 libs/charword_table.js delete mode 100644 libs/config.js delete mode 100644 libs/content_processor.js delete mode 100644 libs/directory.js delete mode 100644 libs/downloader.js delete mode 100644 libs/env_generator.js delete mode 100644 libs/file.js delete mode 100644 libs/initializer.js delete mode 100644 libs/music.js delete mode 100644 libs/offical_info.js delete mode 100644 libs/project_json.js create mode 100644 packages/assets/.gitignore create mode 100644 packages/assets/.prettierignore create mode 100644 packages/assets/config.yaml create mode 100644 packages/assets/eslint.config.js create mode 100644 packages/assets/index.js create mode 100644 packages/assets/libs/build.js create mode 100644 packages/assets/libs/download.js create mode 100644 packages/assets/package.json create mode 100644 packages/assets/prettier.config.js create mode 100644 packages/assets/runner.js create mode 100644 packages/background/.gitignore create mode 100644 packages/background/.prettierignore create mode 100644 packages/background/eslint.config.js create mode 100644 packages/background/index.js create mode 100644 packages/background/package.json create mode 100644 packages/background/prettier.config.js create mode 100644 packages/background/runner.js create mode 100644 packages/charword-table/.prettierignore rename {data => packages/charword-table}/auto_update/charword_table_en_US_1739039571000.json (100%) rename {data => packages/charword-table}/auto_update/charword_table_ja_JP_1739039571000.json (100%) rename {data => packages/charword-table}/auto_update/charword_table_ko_KR_1739039571000.json (100%) create mode 100644 packages/charword-table/auto_update/charword_table_zh_CN_1739520152000.json create mode 100644 packages/charword-table/eslint.config.js create mode 100644 packages/charword-table/index.js create mode 100644 packages/charword-table/package.json create mode 100644 packages/charword-table/prettier.config.js create mode 100644 packages/charword-table/runner.js create mode 100644 packages/config/.prettierignore create mode 100644 packages/config/config.yaml create mode 100644 packages/config/eslint.config.js create mode 100644 packages/config/index.js create mode 100644 packages/config/package.json create mode 100644 packages/config/prettier.config.js create mode 100644 packages/downloader/.prettierignore create mode 100644 packages/downloader/eslint.config.js create mode 100644 packages/downloader/index.js create mode 100644 packages/downloader/package.json create mode 100644 packages/downloader/prettier.config.js create mode 100644 packages/eslint-config/index.js create mode 100644 packages/eslint-config/package.json create mode 100644 packages/libs/.prettierignore create mode 100644 packages/libs/eslint.config.js create mode 100644 packages/libs/index.js create mode 100644 packages/libs/libs/alpha_composite.js create mode 100644 packages/libs/libs/env.js create mode 100644 packages/libs/libs/env_parser.js create mode 100644 packages/libs/libs/error.js create mode 100644 packages/libs/libs/file.js rename {libs => packages/libs/libs}/yaml.js (86%) create mode 100644 packages/libs/package.json create mode 100644 packages/libs/prettier.config.js create mode 100644 packages/music/.gitignore create mode 100644 packages/music/.prettierignore create mode 100644 packages/music/auto_update/audio_data_1740038433000.json rename {data => packages/music}/auto_update/display_meta_table_1738742430000.json (100%) rename {data => packages/music}/auto_update/music_table.json (100%) create mode 100644 packages/music/eslint.config.js create mode 100644 packages/music/index.js create mode 100644 packages/music/package.json create mode 100644 packages/music/prettier.config.js create mode 100644 packages/music/runner.js create mode 100644 packages/official-info/.prettierignore rename offical_update.json => packages/official-info/auto_update/official_info.json (92%) create mode 100644 packages/official-info/eslint.config.js create mode 100644 packages/official-info/index.js create mode 100644 packages/official-info/package.json create mode 100644 packages/official-info/prettier.config.js create mode 100644 packages/official-info/runner.js create mode 100644 packages/operator/.gitignore create mode 100644 packages/operator/.prettierignore create mode 100644 packages/operator/config.yaml rename {config => packages/operator/config}/_template.yaml (67%) rename {config => packages/operator/config}/amiya_solo_around_the_world.yaml (69%) rename {config => packages/operator/config}/chen.yaml (67%) rename {config => packages/operator/config}/chen_ten_thousand_mountains.yaml (64%) rename {config => packages/operator/config}/chongyue.yaml (72%) rename {config => packages/operator/config}/chongyue_alighting.yaml (70%) rename {config => packages/operator/config}/chongyue_allround_actor.yaml (68%) rename {config => packages/operator/config}/degenbrecher_the_shadow_of_dark_moon.yaml (66%) rename {config => packages/operator/config}/dusk.yaml (74%) rename {config => packages/operator/config}/dusk_everything_is_a_miracle.yaml (66%) rename {config => packages/operator/config}/executor_the_ex_foedere_allmind_as_one.yaml (64%) rename {config => packages/operator/config}/eyjafjalla_the_hvit_aska.yaml (69%) rename {config => packages/operator/config}/eyjafjalla_the_hvit_aska_a_picnic_before_a_long_trip.yaml (61%) rename {config => packages/operator/config}/gavial.yaml (68%) rename {config => packages/operator/config}/gavial_the_invincible_holiday_hd26.yaml (65%) rename {config => packages/operator/config}/goldenglow_summer_flowers_fa394.yaml (68%) rename {config => packages/operator/config}/ines_under_the_flaming_dome.yaml (66%) rename {config => packages/operator/config}/kaltsit_remnant.yaml (72%) rename {config => packages/operator/config}/lappland_the_decadenza.yaml (69%) rename {config => packages/operator/config}/lee_trust_your_eyes.yaml (66%) rename {config => packages/operator/config}/lin_heavenly_mirage.yaml (71%) rename {config => packages/operator/config}/ling.yaml (74%) rename {config => packages/operator/config}/ling_it_does_wash_the_strings.yaml (66%) rename {config => packages/operator/config}/ling_towering_is_cliff_of_nostalgia.yaml (68%) rename {config => packages/operator/config}/mizuki_summer_feast.yaml (66%) rename {config => packages/operator/config}/muelsyse.yaml (74%) rename {config => packages/operator/config}/muelsyse_young_branch.yaml (69%) rename {config => packages/operator/config}/mwynar_w_dali.yaml (72%) rename {config => packages/operator/config}/nearl.yaml (67%) rename {config => packages/operator/config}/nearl_relight.yaml (67%) rename {config => packages/operator/config}/nian.yaml (74%) rename {config => packages/operator/config}/nian_thunderbolt_director.yaml (68%) rename {config => packages/operator/config}/nian_unfettered_freedom.yaml (67%) rename {config => packages/operator/config}/nightingale_iakhu_of_flows.yaml (71%) rename {config => packages/operator/config}/passager_dream_in_a_moment.yaml (64%) rename {config => packages/operator/config}/pepe.yaml (77%) rename {config => packages/operator/config}/phatom_focus.yaml (70%) rename {config => packages/operator/config}/pozemka_snowy_plains_in_words.yaml (63%) rename {config => packages/operator/config}/reed_the_frame_shadow_curator.yaml (68%) rename {config => packages/operator/config}/reed_the_frame_shadow_summer_flower.yaml (64%) rename {config => packages/operator/config}/rosmontis.yaml (72%) rename {config => packages/operator/config}/rosmontis_become_anew.yaml (66%) rename {config => packages/operator/config}/shu.yaml (78%) rename {config => packages/operator/config}/shu_spring_feast.yaml (71%) rename {config => packages/operator/config}/silverash_never_melting_ice.yaml (71%) rename {config => packages/operator/config}/skadi.yaml (66%) rename {config => packages/operator/config}/skadi_sublimation.yaml (62%) rename {config => packages/operator/config}/skadi_the_corrupting_heart_red_countess.yaml (65%) rename {config => packages/operator/config}/specter.yaml (67%) rename {config => packages/operator/config}/specter_born_as_one.yaml (65%) rename {config => packages/operator/config}/surtr_colorful_wonderland.yaml (63%) rename {config => packages/operator/config}/texas_the_omertosa.yaml (67%) rename {config => packages/operator/config}/texas_the_omertosa_il_se_de_no.yaml (64%) rename {config => packages/operator/config}/texas_the_omertosa_wingbreaker.yaml (67%) rename {config => packages/operator/config}/virtuosa.yaml (76%) rename {config => packages/operator/config}/virtuosa_diversity_oneness.yaml (68%) rename {config => packages/operator/config}/w.yaml (76%) rename {config => packages/operator/config}/w_wonder.yaml (72%) rename {config => packages/operator/config}/wisadel.yaml (75%) rename {config => packages/operator/config}/yu.yaml (78%) rename {config => packages/operator/config}/zuole_youthful_journey.yaml (69%) create mode 100644 packages/operator/eslint.config.js create mode 100644 packages/operator/index.js create mode 100644 packages/operator/package.json create mode 100644 packages/operator/prettier.config.js create mode 100644 packages/operator/runner.js create mode 100644 packages/postcss-config/index.js create mode 100644 packages/postcss-config/package.json create mode 100644 packages/prettier-config/index.js create mode 100644 packages/prettier-config/package.json create mode 100644 packages/project-json/.prettierignore create mode 100644 packages/project-json/eslint.config.js create mode 100644 packages/project-json/index.js create mode 100644 packages/project-json/libs/content_processor.js create mode 100644 packages/project-json/package.json create mode 100644 packages/project-json/prettier.config.js create mode 100644 packages/project-json/project.json create mode 100644 packages/project-json/project_json.yaml create mode 100644 packages/project-json/runner.js create mode 100644 packages/stylelint-config/index.js create mode 100644 packages/stylelint-config/package.json create mode 100644 packages/vite-helpers/.prettierignore create mode 100644 packages/vite-helpers/eslint.config.js create mode 100644 packages/vite-helpers/index.js create mode 100644 packages/vite-helpers/package.json create mode 100644 packages/vite-helpers/prettier.config.js create mode 100644 packages/wrangler/.prettierignore create mode 120000 packages/wrangler/data/background create mode 120000 packages/wrangler/data/music create mode 120000 packages/wrangler/data/operator create mode 100644 packages/wrangler/eslint.config.js create mode 100644 packages/wrangler/index.js create mode 100644 packages/wrangler/package.json create mode 100644 packages/wrangler/prettier.config.js create mode 100644 packages/wrangler/runner.js create mode 100644 pnpm-workspace.yaml create mode 100644 prettier.config.js delete mode 100644 showcase/index.html delete mode 100644 showcase/src/components/aklive2d.js delete mode 100644 showcase/src/components/background.css delete mode 100644 showcase/src/components/background.js delete mode 100644 showcase/src/components/events.js delete mode 100644 showcase/src/components/fallback.js delete mode 100644 showcase/src/components/helper.js delete mode 100644 showcase/src/components/insight.js delete mode 100644 showcase/src/components/logo.css delete mode 100644 showcase/src/components/logo.js delete mode 100644 showcase/src/components/music.js delete mode 100644 showcase/src/components/player.js delete mode 100644 showcase/src/components/voice.js delete mode 100644 showcase/src/index.css delete mode 100644 showcase/src/index.js delete mode 100644 showcase/src/libs/wallpaper_engine.js create mode 100644 stylelint.config.js create mode 100644 turbo.json delete mode 100644 vite.config.js diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index a799da1..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:react-hooks/recommended" - ], - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - } -} diff --git a/.github/workflows/cf-pages.yaml b/.github/workflows/cf-pages.yaml index c9d00fc..efbae7a 100644 --- a/.github/workflows/cf-pages.yaml +++ b/.github/workflows/cf-pages.yaml @@ -3,12 +3,12 @@ name: Build release and push to CF Pages on: push: branches: [ main ] + pull_request: + branches: + - main env: - CACHE_ZIP_FILENAME: cache.zip - ASSETS_FOLDER: data/operator - RELEASE_FOLDER: release - CACHE_BASE_KEY: akassets + DO_NOT_TRACK: 1 jobs: build: @@ -19,42 +19,15 @@ jobs: uses: pnpm/action-setup@v4 with: run_install: true - - name: Restore cached assets - id: cache-akassets-restore - uses: actions/cache@v4 - with: - path: | - ${{ env.CACHE_ZIP_FILENAME }} - key: ${{ env.CACHE_BASE_KEY }}-${{ hashFiles('offical_update.json') }} - restore-keys: | - ${{ env.CACHE_BASE_KEY }} - - name: Unzip assets - run: | - if test -f ${{ env.CACHE_ZIP_FILENAME }}; then - unzip -qq ${{ env.CACHE_ZIP_FILENAME }} -d . - fi - shell: bash - - name: Download Assets - run: pnpm run cf:download - - name: Build all - run: pnpm run operator:build-all - timeout-minutes: 10 - - name: Build directory - run: pnpm run directory:build + - name: Download Data + run: pnpm run download:data + - name: Build + run: pnpm run build - name: Publish to Cloudflare Pages uses: cloudflare/pages-action@v1 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} projectName: aklive2d - directory: ${{ env.RELEASE_FOLDER}} + directory: dist wranglerVersion: '3' - - name: Zip assets - run: zip -qq -9 -r ${{ env.CACHE_ZIP_FILENAME }} ${{ env.ASSETS_FOLDER }} - - name: Save assets - id: cache-akassets-save - uses: actions/cache/save@v4 - with: - path: | - ${{ env.CACHE_ZIP_FILENAME }} - key: ${{ env.CACHE_BASE_KEY }}-${{ hashFiles('offical_update.json') }} diff --git a/.github/workflows/update-charwords.yaml b/.github/workflows/update-charwords.yaml deleted file mode 100644 index e891ceb..0000000 --- a/.github/workflows/update-charwords.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: Update charwords - -on: - schedule: - - cron: '0 0 * * *' - -jobs: - build: - - runs-on: ubuntu-latest - - permissions: - contents: write - - steps: - - uses: actions/checkout@v3 - - name: Use PNPM - uses: pnpm/action-setup@v4 - with: - run_install: false - - name: Install Node.js - uses: actions/setup-node@v4 - with: - cache: 'pnpm' - - name: Install dependencies - run: pnpm i - - name: Update charwords - run: pnpm run charwords:update - - name: Commit changes if any - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: "chore(charwords): auto update" \ No newline at end of file diff --git a/.github/workflows/update-music.yaml b/.github/workflows/update-music.yaml deleted file mode 100644 index 2335f94..0000000 --- a/.github/workflows/update-music.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: Update music - -on: - schedule: - - cron: '0 0 * * *' - -jobs: - build: - - runs-on: ubuntu-latest - - permissions: - contents: write - - steps: - - uses: actions/checkout@v3 - - name: Use PNPM - uses: pnpm/action-setup@v4 - with: - run_install: false - - name: Install Node.js - uses: actions/setup-node@v4 - with: - cache: 'pnpm' - - name: Install dependencies - run: pnpm i - - name: Update music mapping - run: pnpm run music - - name: Commit changes if any - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: "chore(music): auto update mapping" \ No newline at end of file diff --git a/.github/workflows/update-offical-dyn-info.yaml b/.github/workflows/update-offical-dyn-info.yaml deleted file mode 100644 index f5f2d38..0000000 --- a/.github/workflows/update-offical-dyn-info.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: Update Offical Dyn Info - -on: - schedule: - - cron: '30 10 * * *' - -jobs: - build: - - runs-on: ubuntu-latest - - permissions: - contents: write - - steps: - - uses: actions/checkout@v3 - - name: Use PNPM - uses: pnpm/action-setup@v4 - with: - run_install: false - - name: Install Node.js - uses: actions/setup-node@v4 - with: - cache: 'pnpm' - - name: Install dependencies - run: pnpm i - - name: Update offical dyn info - run: pnpm run offical_update - - name: Commit changes if any - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: "chore(offical update): auto update" diff --git a/.github/workflows/update.yaml b/.github/workflows/update.yaml new file mode 100644 index 0000000..f52907e --- /dev/null +++ b/.github/workflows/update.yaml @@ -0,0 +1,32 @@ +name: Update + +env: + DO_NOT_TRACK: 1 + +on: + schedule: + - cron: '30 10 * * *' + pull_request: + branches: + - main + +jobs: + build: + + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v3 + - name: Use PNPM + uses: pnpm/action-setup@v4 + with: + run_install: true + - name: Update + run: pnpm run update + - name: Commit changes if any + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: "chore: auto update" diff --git a/.gitignore b/.gitignore index 3cb98b2..48de1b0 100644 --- a/.gitignore +++ b/.gitignore @@ -130,7 +130,6 @@ dist .pnp.* # custom -release/* spine-runtimes/* _*.json .DS_Store @@ -138,5 +137,5 @@ _*.json *_v2/* assets/* temp/* -operator/* -data/operator/* \ No newline at end of file +.turbo +data/* \ No newline at end of file diff --git a/.postcssrc.json b/.postcssrc.json deleted file mode 100644 index 0452220..0000000 --- a/.postcssrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "map": true, - "plugins": { - "autoprefixer": {} - } -} diff --git a/.stylelintrc.json b/.stylelintrc.json deleted file mode 100644 index eff2560..0000000 --- a/.stylelintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "stylelint-config-standard-scss" -} diff --git a/.vscode/launch.json b/.vscode/launch.json index 6baf0f9..f8af529 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,87 +9,100 @@ }, { "type": "node-terminal", - "name": "Run Script: charword", + "name": "Run Script: build chen", "request": "launch", - "command": "pnpm run charwords:update", + "env": { + "name": "chen" + }, + "command": "pnpm run build", "cwd": "${workspaceFolder}" }, { "type": "node-terminal", - "name": "Run Script: build mizuki_summer_feast", + "name": "Run Script: build all", "request": "launch", - "command": "pnpm run build mizuki_summer_feast", + "command": "pnpm run build", "cwd": "${workspaceFolder}" }, { "type": "node-terminal", - "name": "Run Script: build-all", + "name": "Run Script: update", "request": "launch", - "command": "pnpm run operator:build-all", + "command": "pnpm run update", "cwd": "${workspaceFolder}" }, { "type": "node-terminal", - "name": "Run Script: test", + "name": "Run Script: init", + "env": { + "name": "test", + "id": "202203231" + }, "request": "launch", - "command": "pnpm run test", + "command": "pnpm run init", "cwd": "${workspaceFolder}" }, { "type": "node-terminal", - "name": "Run Script: dev", + "name": "Run Script: lint", "request": "launch", - "command": "pnpm run dev kaltsit_remnant", + "command": "pnpm run lint", "cwd": "${workspaceFolder}" }, { "type": "node-terminal", - "name": "Run Script: Directory dev", + "name": "Run Script: dev:directory", "request": "launch", - "command": "pnpm run vite:directory:dev", + "command": "pnpm run dev:directory", "cwd": "${workspaceFolder}" }, { "type": "node-terminal", - "name": "Run Script: charwords:update", + "name": "Run Script: preview:directory", "request": "launch", - "command": "pnpm run charwords:update", + "command": "pnpm run preview:directory", "cwd": "${workspaceFolder}" }, { "type": "node-terminal", - "name": "Run Script: charwords:build", + "name": "Run Script: dev:showcase", + "env": { + "name": "chen" + }, "request": "launch", - "command": "pnpm run charwords:build", + "command": "pnpm run dev:showcase", "cwd": "${workspaceFolder}" }, { "type": "node-terminal", - "name": "Run Script: music", + "name": "Run Script: preview:showcase", + "env": { + "name": "chen" + }, "request": "launch", - "command": "pnpm run music", + "command": "pnpm run preview:showcase", "cwd": "${workspaceFolder}" }, { "type": "node-terminal", - "name": "Run Script: offical_update", + "name": "Run Script: download:game", "request": "launch", - "command": "pnpm run offical_update", + "command": "pnpm run download:game", "cwd": "${workspaceFolder}" }, { "type": "node-terminal", - "name": "Run Script: cf:upload", + "name": "Run Script: upload", "request": "launch", - "command": "pnpm run cf:upload", + "command": "pnpm run upload", "cwd": "${workspaceFolder}" }, { "type": "node-terminal", - "name": "Run Script: cf:download", + "name": "Run Script: download:data", "request": "launch", - "command": "pnpm run cf:download", + "command": "pnpm run download:data", "cwd": "${workspaceFolder}" - } + }, ] } diff --git a/README.md b/README.md index eb6af48..84e107b 100644 --- a/README.md +++ b/README.md @@ -14,36 +14,32 @@ A list of supported operators can be found at [Directory](https://gura.ch/aklive ### Command Line Tool ``` bash -$ npm run generate {operator_name} -To generate operator assets for showcase page +$ pnpm run update +Update data from official website and github repo ``` ``` bash -$ npm run dev {operator_name} -Live showcase page server for development +$ pnpm run lint +ESLint and StyleLint ``` ``` bash -$ npm run build {operator_name} +$ pnpm run build +Build showcase webpage for all operators and directory page +``` +``` bash +$ name= pnpm run build Build showcase webpage for an operator ``` ``` bash -$ npm run build-all -To generate all operator assets for showcase page -``` -``` bash -$ npm run init {operator_name} +$ name= id= pnpm run init To initialize folder and config file for an operator ``` ``` bash -$ npm run readme {operator_name} -To add operator info to README.md +$ name= pnpm run dev:showcase +Run dev server for showcase webpage for an operator ``` ``` bash -$ npm run directory -To generate directory.json -``` -``` bash -$ npm run charword -To generate the latest charword_table.json +$ name= pnpm run preview:showcase +Preview built showcase webpage for an operator ``` ### Webpage & JavaScript @@ -55,6 +51,7 @@ Using JS events to change settings is recommended. ## Config ### General Config +in `packages/config/config.yaml` ``` yaml folder: operator: ./operator/ # folder for operator assets @@ -83,20 +80,38 @@ operators: passager_dream_in_a_moment: !include config/passager_dream_in_a_moment.yaml mizuki_summer_feast: !include config/mizuki_summer_feast.yaml ``` -### Operator Config +### Operators Config +in `packages/operator/config.yaml` +```yaml +chen: !include config/chen.yaml +dusk: !include config/dusk.yaml +dusk_everything_is_a_miracle: !include config/dusk_everything_is_a_miracle.yaml +ling: !include config/ling.yaml +nearl: !include config/nearl.yaml +nian: !include config/nian.yaml +nian_unfettered_freedom: !include config/nian_unfettered_freedom.yaml +phatom_focus: !include config/phatom_focus.yaml +rosmontis: !include config/rosmontis.yaml +skadi: !include config/skadi.yaml +skadi_sublimation: !include config/skadi_sublimation.yaml +w: !include config/w.yaml +... +``` +### Operator Config +in `packages/operator/config/.yaml` ```yaml -link: chen # the link to access showcase page for this operator -type: operator # operator live2d or skin live2d -date: 2021/08 # release date -title: 'Arknights: Ch''en/Chen the Holungday - 明日方舟:假日威龙陈' # page title filename: dyn_illust_char_1013_chen2 # live2d assets name logo: logo_rhodes_override # operator logo fallback_name: char_1013_chen2_2 # fallback image name viewport_left: 0 # live2d view port settings viewport_right: 0 -viewport_top: 1 -viewport_bottom: 1 +viewport_top: 0 +viewport_bottom: 0 invert_filter: false # operator logo invert filter +codename: # operator name + zh-CN: 假日威龙陈 + en-US: Ch'en/Chen the Holungday +use_json: false # whether the spine skel is in json format ``` ## LICENSE @@ -104,12 +119,14 @@ The `LICENSE` file applies to all files unless listed specifically. `LICENSE_SPINE` file applies to following files including adapted code for this repo: -- `src/libs/spine-player.css` -- `src/libs/spine-player.js` +- `apps/module/libs/spine-player.css` +- `apps/module/libs/spine-player.js` `Copyright © 2017 - 2023 Arknights/Hypergryph Co., Ltd` applies to following files: -- all files under `operator` folder and its sub-folders +- all files under `packages/operator/data` folder and its sub-folders +- all files under `packages/music/data` folder and its sub-folders +- all files under `packages/background/data` folder and its sub-folders ## Instructions on Extracting In-Game Assets I'm still struggling to find a command-line tool to extract in-game assets. But [AssetRipper](https://github.com/AssetRipper/AssetRipper) seems to have a command-line interface, I'm too lazy to have a deeper inverstigation. diff --git a/aklive2d.js b/aklive2d.js deleted file mode 100644 index 4e941e1..0000000 --- a/aklive2d.js +++ /dev/null @@ -1,309 +0,0 @@ -/* eslint-disable no-fallthrough */ -/* eslint-disable no-undef */ -import assert from 'assert' -import path from 'path' -import { fileURLToPath } from 'url' -import { fork } from 'child_process'; -import getConfig from './libs/config.js' -import ProjectJson from './libs/project_json.js' -import EnvGenerator from './libs/env_generator.js' -import { write, rmdir, copy, writeSync, copyDir, readdirSync, exists, mkdir } from './libs/file.js' -import AssetsProcessor from './libs/assets_processor.js' -import init from './libs/initializer.js' -import directory from './libs/directory.js' -import Background from './libs/background.js' -import CharwordTable from './libs/charword_table.js'; -import Music from './libs/music.js'; -import OfficalInfo from './libs/offical_info.js'; -import CFPages from './libs/cf_pages.js'; - -async function main() { - global.__projectRoot = path.dirname(fileURLToPath(import.meta.url)) - const officalInfo = new OfficalInfo() - global.__config = getConfig(officalInfo) - global.__error = [] - - const OPERATOR_SOURCE_FOLDER = path.join(__projectRoot, __config.folder.operator) - const OPERATOR_SOURCE_DATA_FOLDER = path.join(__projectRoot, __config.folder.operator_data) - const OPERATOR_SHARE_FOLDER = path.join(OPERATOR_SOURCE_DATA_FOLDER, __config.folder.share) - - const op = process.argv[2] - let OPERATOR_NAMES = process.argv.slice(3); - - const charwordTable = new CharwordTable() - const musicTable = new Music() - - /** - * Skip all, no need for OPERATOR_NAME - * build-all: build all assets - * directory: build directory - */ - switch (op) { - case 'operator:build-all': - for (const [key,] of Object.entries(__config.operators)) { - OPERATOR_NAMES.push(key) - } - break - case 'operator:preview': - assert(OPERATOR_NAMES.length !== 0, 'Please set the operator name.') - fork(path.join(__projectRoot, 'vite.config.js'), [op, OPERATOR_NAMES]) - return - case 'charwords:update': - await charwordTable.process() - process.exit(0) - case 'music': - await musicTable.process() - process.exit(0) - case 'offical_update': - await officalInfo.update() - process.exit(0) - case 'cf:upload': - await (new CFPages()).upload() - process.exit(0) - case 'cf:download': - await (new CFPages()).download() - process.exit(0) - default: - break - } - - assert(OPERATOR_NAMES.length !== 0, 'Please set the operator name.') - - const background = new Background(OPERATOR_SHARE_FOLDER, OPERATOR_SOURCE_FOLDER) - await background.process() - const backgrounds = ['operator_bg.png', ...background.files] - const { musicToCopy, musicMapping } = musicTable.copy(OPERATOR_SHARE_FOLDER) - - for (const e of musicToCopy) { - const musicPath = path.join(e.source, e.filename) - if (!exists(musicPath)) { - __error.push(`Music file ${e.filename} is not found in music folder.`) - } - } - - for (const e of Object.keys(musicMapping)) { - if (!backgrounds.includes(e)) { - __error.push(`Background file ${e} is not found in background folder.`) - } - } - - for (const OPERATOR_NAME of OPERATOR_NAMES) { - const OPERATOR_RELEASE_FOLDER = path.join(__projectRoot, __config.folder.release, OPERATOR_NAME) - const SHOWCASE_PUBLIC_ASSSETS_FOLDER = path.join(OPERATOR_RELEASE_FOLDER, "assets") - const EXTRACTED_FOLDER = path.join(OPERATOR_SOURCE_DATA_FOLDER, OPERATOR_NAME, 'extracted') - const VOICE_FOLDERS = __config.folder.voice.sub.map((sub) => path.join(OPERATOR_SOURCE_DATA_FOLDER, OPERATOR_NAME, __config.folder.voice.main, sub.name)) - - /** - * Skip assets generation part - * init: init folder and config for an operator - * readme: append a new line to README.md - */ - switch (op) { - case 'init': - init(OPERATOR_NAME, [EXTRACTED_FOLDER, ...VOICE_FOLDERS], officalInfo) - process.exit(0) - default: - break - } - - rmdir(OPERATOR_RELEASE_FOLDER) - mkdir(path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME)) - - const charwordTableLookup = charwordTable.lookup(OPERATOR_NAME) - 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 } - }) - } - }) - - let voiceLangs = [], subtitleLangs = []; - try { - voiceLangs = Object.keys(voiceJson.voiceLangs["zh-CN"]) - subtitleLangs = Object.keys(voiceJson.subtitleLangs) - - writeSync(JSON.stringify(voiceJson), path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, 'charword_table.json')) - } catch (e) { - console.log(`charword_table is not available`) - } - - // check whether voice files has been added - const customVoiceName = voiceLangs.filter(i => !__config.folder.voice.sub.map(e => e.lang).includes(i))[0] - const voiceLangMapping = __config.folder.voice.sub.filter(e => { - return voiceLangs.includes(e.lang) || (e.lang === "CUSTOM" && typeof customVoiceName !== 'undefined') - }).map(e => { - return { - name: e.name, - lang: e.lang === "CUSTOM" ? customVoiceName : e.lang - } - }) - for (const voiceSubFolderMapping of voiceLangMapping) { - const voiceSubFolder = path.join(OPERATOR_SOURCE_DATA_FOLDER, OPERATOR_NAME, __config.folder.voice.main, voiceSubFolderMapping.name) - if (readdirSync(voiceSubFolder).length === 0) { - __error.push(`Voice folder ${voiceSubFolderMapping.name} for ${OPERATOR_NAME} is empty.`) - } - } - - const envPath = path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, '.env') - writeSync((new EnvGenerator()).generate([ - { - key: "insight_id", - value: __config.insight_id - }, - { - key: "link", - value: __config.operators[OPERATOR_NAME].link - }, { - key: "title", - value: __config.operators[OPERATOR_NAME].title - }, { - key: "filename", - value: __config.operators[OPERATOR_NAME].filename.replace('#', '%23') - }, { - key: "logo_filename", - value: __config.operators[OPERATOR_NAME].logo - }, { - key: "fallback_filename", - value: __config.operators[OPERATOR_NAME].fallback_name.replace('#', '%23') - }, { - key: "viewport_left", - value: __config.operators[OPERATOR_NAME].viewport_left - }, { - key: "viewport_right", - value: __config.operators[OPERATOR_NAME].viewport_right - }, { - key: "viewport_top", - value: __config.operators[OPERATOR_NAME].viewport_top - }, { - key: "viewport_bottom", - value: __config.operators[OPERATOR_NAME].viewport_bottom - }, { - key: "invert_filter", - value: __config.operators[OPERATOR_NAME].invert_filter - }, { - key: "image_width", - value: 2048 - }, { - key: "image_height", - value: 2048 - }, { - key: "background_files", - value: JSON.stringify(backgrounds) - }, { - key: "background_folder", - value: __config.folder.background - }, { - key: "voice_folders", - value: JSON.stringify(__config.folder.voice) - }, { - key: "music_folder", - value: __config.folder.music - }, { - key: "music_mapping", - value: JSON.stringify(musicMapping) - }, { - key: "use_json", - value: __config.operators[OPERATOR_NAME].use_json - } - ]), envPath) - - const projectJson = new ProjectJson(OPERATOR_NAME, OPERATOR_SHARE_FOLDER, { - backgrounds, - voiceLangs, - subtitleLangs, - music: Object.keys(musicMapping) - }) - projectJson.load().then((content) => { - write(JSON.stringify(content, null, 2), path.join(OPERATOR_RELEASE_FOLDER, 'project.json')) - }) - - const assetsProcessor = new AssetsProcessor(OPERATOR_NAME, OPERATOR_SHARE_FOLDER) - const assetContent = await assetsProcessor.process(EXTRACTED_FOLDER) - write(JSON.stringify(assetContent.assetsJson, null), path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, `assets.json`)) - - // copy remaining files - const filesToCopy = [ - ...background.getFilesToCopy(SHOWCASE_PUBLIC_ASSSETS_FOLDER), - ...musicToCopy.map(entry => { - return { - ...entry, - target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER, __config.folder.music) - } - }), - { - filename: 'preview.jpg', - source: path.join(OPERATOR_SOURCE_DATA_FOLDER, OPERATOR_NAME), - target: path.join(OPERATOR_RELEASE_FOLDER) - }, - { - filename: 'operator_bg.png', - source: path.join(OPERATOR_SHARE_FOLDER, __config.folder.background), - target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER, __config.folder.background) - }, - { - filename: `${__config.operators[OPERATOR_NAME].logo}.png`, - source: path.join(OPERATOR_SHARE_FOLDER, 'logo'), - target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER) - }, - { - filename: `${__config.operators[OPERATOR_NAME].fallback_name}.png`, - source: path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME), - target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER) - }, - { - filename: `${__config.operators[OPERATOR_NAME].fallback_name}_portrait.png`, - source: path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME), - target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER) - } - ] - filesToCopy.forEach((file) => { - copy(path.join(file.source, file.filename), path.join(file.target, file.filename)) - }) - - const foldersToCopy = [ - { - source: path.join(OPERATOR_SOURCE_DATA_FOLDER, OPERATOR_NAME, __config.folder.voice.main), - target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER, __config.folder.voice.main) - } - ] - foldersToCopy.forEach((folder) => { - copyDir(folder.source, folder.target) - }) - } - switch (op) { - case op.startsWith('directory'): - directory(OPERATOR_SOURCE_DATA_FOLDER, { backgrounds, musicMapping }) - default: - break - } - if (__error.length > 0) { - const str = `${__error.length} error${__error.length > 1 ? 's were' : ' was'} found:\n${__error.join('\n')}` - throw new Error(str) - } else { - for (const OPERATOR_NAME of OPERATOR_NAMES) { - fork(path.join(__projectRoot, 'vite.config.js'), [op, OPERATOR_NAME]) - } - } -} - -main(); \ No newline at end of file diff --git a/directory/.gitignore b/apps/directory/.gitignore similarity index 98% rename from directory/.gitignore rename to apps/directory/.gitignore index a547bf3..028fbac 100644 --- a/directory/.gitignore +++ b/apps/directory/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +data \ No newline at end of file diff --git a/apps/directory/.prettierignore b/apps/directory/.prettierignore new file mode 100644 index 0000000..cb88359 --- /dev/null +++ b/apps/directory/.prettierignore @@ -0,0 +1,3 @@ +dist +data +auto_update \ No newline at end of file diff --git a/apps/directory/eslint.config.js b/apps/directory/eslint.config.js new file mode 100644 index 0000000..8f5c34e --- /dev/null +++ b/apps/directory/eslint.config.js @@ -0,0 +1,30 @@ +import js from '@eslint/js' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import baseConfig from '@aklive2d/eslint-config' + +/** @type {import('eslint').Config} */ +export default [ + ...baseConfig, + { ignores: ['dist'] }, + { + settings: { react: { version: '18.3' } }, + plugins: { + react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + ...reactHooks.configs.recommended.rules, + 'react/jsx-no-target-blank': 'off', + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/apps/directory/index.html b/apps/directory/index.html new file mode 100644 index 0000000..75da59e --- /dev/null +++ b/apps/directory/index.html @@ -0,0 +1,86 @@ + + + + + + %VITE_APP_TITLE% + + + + + +
+
+ + +
+ + + diff --git a/apps/directory/jsconfig.json b/apps/directory/jsconfig.json new file mode 100644 index 0000000..30e9ad8 --- /dev/null +++ b/apps/directory/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "!/*": ["src/*"] + } + } +} diff --git a/apps/directory/package.json b/apps/directory/package.json new file mode 100644 index 0000000..036738a --- /dev/null +++ b/apps/directory/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aklive2d/directory", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev:directory": "vite --clearScreen false", + "build": "mode=build node runner.js", + "preview:directory": "vite preview", + "lint": "eslint \"src/**/*.js\" \"src/**/*.jsx\" && stylelint \"src/**/*.css\" \"src/**/*.scss\" && prettier --check ." + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "jotai": "^2.11.3", + "react-router-dom": "^7.1.5", + "react-simple-typewriter": "^5.0.1", + "reset-css": "^5.0.2", + "@aklive2d/eslint-config": "workspace:*", + "@aklive2d/stylelint-config": "workspace:*", + "@aklive2d/postcss-config": "workspace:*", + "@aklive2d/config": "workspace:*", + "@aklive2d/libs": "workspace:*", + "@aklive2d/assets": "workspace:*", + "@aklive2d/operator": "workspace:*", + "@aklive2d/vite-helpers": "workspace:*", + "@aklive2d/showcase": "workspace:*", + "@aklive2d/module": "workspace:*", + "@aklive2d/prettier-config": "workspace:*" + }, + "devDependencies": { + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react-swc": "^3.5.0", + "vite": "^6.1.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.18", + "prop-types": "^15.8.1", + "sass": "^1.84.0", + "autoprefixer": "^10.4.20" + } +} diff --git a/apps/directory/postcss.config.js b/apps/directory/postcss.config.js new file mode 100644 index 0000000..daf0049 --- /dev/null +++ b/apps/directory/postcss.config.js @@ -0,0 +1,5 @@ +import baseConfig from '@aklive2d/postcss-config' +/** @type {import('postcss').Config} */ +export default { + ...baseConfig, +} diff --git a/apps/directory/prettier.config.js b/apps/directory/prettier.config.js new file mode 100644 index 0000000..a58af8e --- /dev/null +++ b/apps/directory/prettier.config.js @@ -0,0 +1,11 @@ +import baseConfig from '@aklive2d/prettier-config' + +/** + * @type {import("prettier").Config} + */ +const config = { + ...baseConfig, + semi: false, +} + +export default config diff --git a/apps/directory/runner.js b/apps/directory/runner.js new file mode 100644 index 0000000..9dffe9b --- /dev/null +++ b/apps/directory/runner.js @@ -0,0 +1,24 @@ +import { build as viteBuild } from 'vite' +import { envParser } from '@aklive2d/libs' + +const build = async (namesToBuild) => { + if (!namesToBuild.length) { + // skip as directory can only build + // when all operators are built + await viteBuild() + } +} + +async function main() { + const { name } = envParser.parse({ + name: { + type: 'string', + short: 'n', + multiple: true, + default: [], + }, + }) + await build(name) +} + +main() diff --git a/apps/directory/src/App.jsx b/apps/directory/src/App.jsx new file mode 100644 index 0000000..c5dbe82 --- /dev/null +++ b/apps/directory/src/App.jsx @@ -0,0 +1,23 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { createBrowserRouter, RouterProvider } from 'react-router-dom' +import Root from '@/routes/Root' +import Error from '@/routes/Error' +import routes from '@/routes' +import '@/App.scss' +import 'reset-css' + +const router = createBrowserRouter([ + { + path: '/', + element: , + errorElement: , + children: routes.filter((item) => item.routeable), + }, +]) + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +) diff --git a/apps/directory/src/App.scss b/apps/directory/src/App.scss new file mode 100644 index 0000000..8eaae4d --- /dev/null +++ b/apps/directory/src/App.scss @@ -0,0 +1,103 @@ +@import 'https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&family=Noto+Sans+KR:wght@400;500;700&family=Noto+Sans+SC:wght@400;500;700&family=Noto+Sans:wght@400;500;700&display=swap'; +@import 'https://fonts.cdnfonts.com/css/bender'; +@import 'https://fonts.cdnfonts.com/css/geometos'; + +@mixin light-theme() { + --text-color: rgb(0 0 0 / 87%); + --text-color-full: #000; + --secondary-text-color: #97958d; + --date-color: rgb(0 0 0 / 20%); + --border-color: #8f8f8f; + --link-highlight-color: #33b5e5; + --drawer-background-color: rgb(255 255 255 / 88%); + --root-background-color: #ececec; + --home-item-hover-background-color: rgb(188 188 188 / 30%); + --home-item-background-linear-gradient-color: rgb(0 0 0 / 10%); + --home-item-outline-color: rgb(41 41 41 / 30%); + --button-color: #999; +} + +@mixin dark-theme() { + --text-color: rgb(255 255 255 / 87%); + --text-color-full: #fff; + --secondary-text-color: #686a72; + --date-color: rgb(255 255 255 / 20%); + --border-color: #707070; + --link-highlight-color: #33b5e5; + --drawer-background-color: rgb(0 0 0 / 88%); + --root-background-color: #131313; + --home-item-hover-background-color: rgb(67 67 67 / 30%); + --home-item-background-linear-gradient-color: rgb(255 255 255 / 10%); + --home-item-outline-color: rgb(214 214 214 / 30%); + --button-color: #666; +} + +@media (prefers-color-scheme: dark) { + :root { + @include dark-theme; + } +} + +@media (prefers-color-scheme: light) { + :root { + @include light-theme; + } +} + +:root { + font-family: + Geometos, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans', + sans-serif; + font-size: 16px; + line-height: 1.2em; + font-weight: 400; + color: var(--text-color); + background-color: var(--root-background-color); + min-height: 100vh; + + @media only screen and (width <= 430px) { + & { + font-size: 12px; + } + } + + @media only screen and (width <= 1600px) { + & { + font-size: 14px; + } + } +} + +#root { + display: flex; + flex-flow: column nowrap; + justify-content: flex-start; + align-items: stretch; + min-height: 100vh; +} + +:root::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +#root::-webkit-scrollbar-track { + background-color: transparent; +} + +:root::-webkit-scrollbar-thumb { + background-color: var(--border-color); + border: 5px solid transparent; + background-clip: padding-box; + transition: all 0.3s ease-in-out; +} + +:root::-webkit-scrollbar-thumb:hover { + border: 4px solid transparent; + background-color: var(--text-color); +} + +a { + color: var(--text-color); + text-decoration: none; +} diff --git a/apps/directory/src/component/border.jsx b/apps/directory/src/component/border.jsx new file mode 100644 index 0000000..d9751ba --- /dev/null +++ b/apps/directory/src/component/border.jsx @@ -0,0 +1,9 @@ +import PropTypes from 'prop-types' +import classes from './scss/border.module.scss' + +export default function Border(props) { + return
{props.children}
+} +Border.propTypes = { + children: PropTypes.node, +} diff --git a/apps/directory/src/component/char_icon.jsx b/apps/directory/src/component/char_icon.jsx new file mode 100644 index 0000000..3551201 --- /dev/null +++ b/apps/directory/src/component/char_icon.jsx @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types' + +export default function CharIcon(props) { + return ( + + {props.type === 'operator' ? ( + + + + + + + ) : ( + + )} + + ) +} + +CharIcon.propTypes = { + viewBox: PropTypes.string, + type: PropTypes.string, + style: PropTypes.object, +} diff --git a/apps/directory/src/component/dropdown.jsx b/apps/directory/src/component/dropdown.jsx new file mode 100644 index 0000000..fb6450c --- /dev/null +++ b/apps/directory/src/component/dropdown.jsx @@ -0,0 +1,99 @@ +import { useState } from 'react' +import PropTypes from 'prop-types' +import classes from './scss/dropdown.module.scss' + +export default function Dropdown(props) { + const [hidden, setHidden] = useState(true) + + const toggleDropdown = () => { + setHidden(!hidden) + } + + return ( + <> +
+
toggleDropdown()} + > + {props.text} + +
+ {props.altText} +
+
+
    + {props.menu.map((item) => { + switch (item.type) { + case 'date': { + return ( +
    +
    +
    + {item.name} +
    +
    + ) + } + case 'custom': { + return item.component + } + default: { + return ( +
  • { + props.onClick(item) + toggleDropdown() + }} + style={ + item.color + ? { color: item.color } + : {} + } + > + {item.icon ? ( +
    + {item.icon} +
    + ) : null} +
    + {item.name} +
    +
  • + ) + } + } + })} +
+ + + ) +} +Dropdown.propTypes = { + className: PropTypes.string, + text: PropTypes.string, + menu: PropTypes.array, + onClick: PropTypes.func, + activeColor: PropTypes.object, + activeRule: PropTypes.func, + altText: PropTypes.string, + iconStyle: PropTypes.object, + left: PropTypes.bool, +} diff --git a/apps/directory/src/component/popup.jsx b/apps/directory/src/component/popup.jsx new file mode 100644 index 0000000..4d38171 --- /dev/null +++ b/apps/directory/src/component/popup.jsx @@ -0,0 +1,48 @@ +import { useState } from 'react' +import classes from './scss/popup.module.scss' +import ReturnButton from '@/component/return_button' +import Border from '@/component/border' +import PropTypes from 'prop-types' + +export default function Popup(props) { + const [hidden, setHidden] = useState(true) + + const toggle = () => { + setHidden(!hidden) + } + + return ( + <> +
+
+
+
+ {props.title} +
+ +
+ +
+ {props.children} +
+
+
toggle()} + /> +
+ + {props.title} + + + ) +} +Popup.propTypes = { + title: PropTypes.string, + children: PropTypes.node, +} diff --git a/apps/directory/src/component/return_button.jsx b/apps/directory/src/component/return_button.jsx new file mode 100644 index 0000000..01a3755 --- /dev/null +++ b/apps/directory/src/component/return_button.jsx @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types' +import classes from './scss/return_button.module.scss' + +export default function ReturnButton(props) { + return ( + <> +
props.onClick()} + > +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + ) +} +ReturnButton.propTypes = { + onClick: PropTypes.func, + className: PropTypes.string, +} diff --git a/apps/directory/src/component/scss/border.module.scss b/apps/directory/src/component/scss/border.module.scss new file mode 100644 index 0000000..73c27b7 --- /dev/null +++ b/apps/directory/src/component/scss/border.module.scss @@ -0,0 +1,30 @@ +.border { + position: relative; + bottom: 1px; + border-bottom: 1px solid var(--text-color); + + @media only screen and (width <= 430px) { + & { + margin: 0 1rem; + } + } + + &::before, + &::after { + content: ''; + display: block; + position: absolute; + width: 5px; + height: 5px; + top: -2px; + background-color: var(--text-color); + } + + &::before { + right: 100%; + } + + &::after { + left: 100%; + } +} diff --git a/apps/directory/src/component/scss/dropdown.module.scss b/apps/directory/src/component/scss/dropdown.module.scss new file mode 100644 index 0000000..c6238c8 --- /dev/null +++ b/apps/directory/src/component/scss/dropdown.module.scss @@ -0,0 +1,187 @@ +.dropdown { + position: relative; + display: inline-block; + user-select: none; + z-index: 2; + padding: 0.5em; + cursor: pointer; + + .text { + display: flex; + flex-direction: row; + align-items: center; + color: var(--text-color); + height: 2em; + min-width: 2em; + + .popup { + opacity: 0; + position: absolute; + background-color: var(--root-background-color); + width: max-content; + height: max-content; + max-height: 61.8vh; + max-width: 61.8vw; + z-index: -1; + top: 2.5em; + right: 0; + cursor: auto; + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + overflow: auto; + padding: 0.5rem; + border: 1px solid var(--border-color); + visibility: hidden; + + .text { + font-size: 1rem; + } + } + + &:hover { + .popup { + visibility: unset; + opacity: 1; + } + } + } + + .content { + padding-right: 1.2em; + height: 1em; + } + + .icon { + position: absolute; + bottom: 0.5em; + right: 0.6em; + width: 0.5em; + height: 0.5em; + display: inline-block; + vertical-align: middle; + border-left: 0.15em solid var(--text-color); + border-bottom: 0.15em solid var(--text-color); + border-right: 0.15em solid var(--text-color); + border-top: 0.15em solid var(--text-color); + transform: translateY(-0.7em) rotate(-45deg); + } + + .menu { + scrollbar-gutter: stable; + opacity: 0; + position: absolute; + background-color: var(--root-background-color); + width: max-content; + max-height: 61.8vh; + max-width: 61.8vw; + z-index: -1; + top: 2.5em; + right: 0; + display: flex; + align-items: stretch; + flex-flow: column nowrap; + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + overflow: auto; + padding: 0.5rem; + border: 1px solid var(--border-color); + visibility: hidden; + color: var(--link-highlight-color); + cursor: auto; + + &.left { + left: 0; + right: unset; + } + + .date { + font-family: + Bender, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', + 'Noto Sans', sans-serif; + font-weight: bold; + font-size: 1.5rem; + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + width: 100%; + + .line { + height: 1px; + flex-grow: 1; + background-color: var(--text-color); + margin: 0.5rem; + } + } + + .item { + cursor: pointer; + padding: 0.5rem; + font-size: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + + .text { + flex: 1; + transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + margin-left: 1rem; + } + + .item-icon svg { + transition: fill cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + width: 1rem; + fill: var(--text-color); + } + + &:hover, + &:focus, + &.active { + .text { + color: currentcolor; + } + + .item-icon svg { + fill: currentcolor; + } + } + } + } + + &.left { + /* stylelint-disable no-descending-specificity */ + .popup, + .menu { + left: 0; + right: unset; + } + } + + .overlay { + z-index: -1; + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + cursor: auto; + } + + &.active, + &:hover { + .icon { + animation: icon-flash 2s cubic-bezier(0.65, 0.05, 0.36, 1) infinite; + } + } + + &.active { + .menu { + visibility: visible; + opacity: 1; + z-index: 2; + } + } +} + +@keyframes icon-flash { + 50% { + opacity: 0.2; + } +} diff --git a/apps/directory/src/component/scss/popup.module.scss b/apps/directory/src/component/scss/popup.module.scss new file mode 100644 index 0000000..fb1cb5d --- /dev/null +++ b/apps/directory/src/component/scss/popup.module.scss @@ -0,0 +1,96 @@ +.entry-text { + cursor: pointer; +} + +.popup { + position: fixed; + inset: 0; + overflow: hidden; + opacity: 0; + z-index: -1; + border: unset; + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 1.2rem; + + .wrapper { + display: flex; + flex-flow: column nowrap; + align-items: stretch; + max-width: 480px; + height: fit-content; + margin: 0 auto; + background-color: var(--root-background-color); + border: 1px solid var(--border-color); + padding: 2rem; + } + + .title { + font-size: 3rem; + font-weight: 700; + display: flex; + flex-direction: row; + place-content: center space-between; + align-items: center; + text-transform: uppercase; + font-family: + Geometos, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', + 'Noto Sans', sans-serif; + + .return-button { + color: var(--button-color); + transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + + &:hover { + color: var(--text-color); + } + } + } + + .text { + flex-grow: 1; + margin-right: 3rem; + } + + .content { + line-height: 1.3em; + padding: 1rem 1rem 0; + user-select: text; + } + + .overlay { + position: absolute; + inset: 0; + z-index: -1; + opacity: 0; + background-color: var(--root-background-color); + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + + &.active { + opacity: 0.5; + visibility: visible; + } + } + + &.active { + opacity: 1; + z-index: 10; + } + + @media (width <= 768px) { + .title { + font-size: 2rem; + } + + .content { + font-size: 1rem; + } + + .return-button { + transform: scale(0.8); + } + } +} diff --git a/apps/directory/src/component/scss/return_button.module.scss b/apps/directory/src/component/scss/return_button.module.scss new file mode 100644 index 0000000..36cd506 --- /dev/null +++ b/apps/directory/src/component/scss/return_button.module.scss @@ -0,0 +1,55 @@ +.return-button { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0.6rem 0; + width: 3rem; + cursor: pointer; + + %arrow-shared { + border-top: 0.24rem solid transparent; + border-bottom: 0.24rem solid transparent; + } + + .wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + + &:nth-child(1) { + transform: translateY(-0.1rem) rotate(45deg); + } + + &:nth-child(2) { + transform: translateY(-0.1rem) translate(90%, -100%) rotate(-45deg); + } + + &:nth-child(3) { + transform: translateY(-0.1rem) translateY(150%) rotate(315deg); + } + + &:nth-child(4) { + transform: translateY(-0.1rem) translate(90%, 50%) rotate(225deg); + } + } + + .bar { + width: 1rem; + height: 0.4rem; + background-color: currentcolor; + transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + } + + .arrow-left { + @extend %arrow-shared; + + border-right: 0.3rem solid currentcolor; + } + + .arrow-right { + @extend %arrow-shared; + + border-left: 0.3rem solid currentcolor; + } +} diff --git a/apps/directory/src/component/scss/search_box.module.scss b/apps/directory/src/component/scss/search_box.module.scss new file mode 100644 index 0000000..ef65b99 --- /dev/null +++ b/apps/directory/src/component/scss/search_box.module.scss @@ -0,0 +1,78 @@ +.search-box { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: stretch; + margin: 0.5rem; + + .icon { + position: absolute; + width: 0.8rem; + height: 0.8rem; + display: inline-block; + vertical-align: middle; + border-left: 0.15rem solid var(--text-color-full); + border-bottom: 0.15rem solid var(--text-color-full); + border-right: 0.15rem solid var(--text-color-full); + border-top: 0.15rem solid var(--text-color-full); + transform: translate(0.2rem, 0.3rem) rotate(-45deg); + } + + .icon-dot { + position: absolute; + background-color: var(--text-color-full); + width: 0.15rem; + height: 0.6rem; + transform: translate(1.2rem, 1.1rem) rotate(-45deg); + } + + .input { + flex-grow: 1; + font-size: 1.5rem; + width: 100%; + margin-left: 2rem; + padding-right: 2rem; + background-color: transparent; + border: unset; + border-bottom: 0.15em solid var(--home-item-outline-color); + color: var(--text-color); + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + } + + .input:focus, + .input:hover { + outline: none; + border-bottom: 0.15em solid var(--text-color); + } + + .icon-clear { + position: absolute; + right: 1rem; + width: 2rem; + height: 2rem; + cursor: pointer; + opacity: 0; + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + visibility: hidden; + + &.active { + opacity: 1; + visibility: unset; + } + + .line { + position: absolute; + width: 2rem; + height: 0.2rem; + background-color: var(--text-color); + + &:nth-child(1) { + transform: translate(0, 0.8rem) rotate(45deg); + } + + &:nth-child(2) { + transform: translate(0, 0.8rem) rotate(-45deg); + } + } + } +} diff --git a/apps/directory/src/component/scss/switch.module.scss b/apps/directory/src/component/scss/switch.module.scss new file mode 100644 index 0000000..03508c1 --- /dev/null +++ b/apps/directory/src/component/scss/switch.module.scss @@ -0,0 +1,64 @@ +.switch { + position: relative; + user-select: none; + z-index: 2; + padding: 8px 36px 8px 8px; + cursor: pointer; + display: flex; + flex-direction: row; + align-items: center; + color: var(--secondary-text-color); + transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + + .content { + padding-right: 8px; + } + + .wrapper { + color: var(--secondary-text-color); + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + + .icon { + position: absolute; + bottom: 0.65em; + right: 18px; + width: 0.5em; + height: 0.5em; + display: inline-block; + vertical-align: middle; + border-left: 0.15em solid currentcolor; + border-bottom: 0.15em solid currentcolor; + border-right: 0.15em solid currentcolor; + border-top: 0.15em solid currentcolor; + transform: translate(0, -0.15em) rotate(-45deg); + transition: + right cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s, + background-color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + } + + .line { + position: absolute; + bottom: 1.1em; + right: 6px; + width: 18px; + height: 0.15em; + display: inline-block; + vertical-align: middle; + background-color: currentcolor; + z-index: -1; + } + } + + &.active { + color: var(--text-color); + + .wrapper { + color: var(--text-color-full); + + .icon { + background-color: currentcolor; + right: 0; + } + } + } +} diff --git a/apps/directory/src/component/scss/totop_button.module.scss b/apps/directory/src/component/scss/totop_button.module.scss new file mode 100644 index 0000000..93dfd9e --- /dev/null +++ b/apps/directory/src/component/scss/totop_button.module.scss @@ -0,0 +1,43 @@ +.totop-button { + position: fixed; + user-select: none; + z-index: 2; + cursor: pointer; + right: 2rem; + bottom: 1rem; + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: flex-start; + width: 3rem; + height: 3rem; + opacity: 0; + visibility: hidden; + transition: opacity cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + + &.show { + opacity: 1; + visibility: unset; + } + + .bar { + position: absolute; + width: 100%; + height: 0.3rem; + background-color: var(--text-color-full); + will-change: auto; + } + + .bar:nth-child(1) { + transform: rotateZ(45deg) scaleX(0.5) translateX(45%); + } + + .bar:nth-child(2) { + transform: rotateZ(-45deg) scaleX(0.5) translateX(-45%); + } + + .bar:nth-child(3), + .bar:nth-child(4) { + transform: translateY(450%) rotateZ(90deg) scaleX(0.5); + } +} diff --git a/apps/directory/src/component/search_box.jsx b/apps/directory/src/component/search_box.jsx new file mode 100644 index 0000000..7b62d6c --- /dev/null +++ b/apps/directory/src/component/search_box.jsx @@ -0,0 +1,50 @@ +import { useState } from 'react' +import PropTypes from 'prop-types' +import classes from './scss/search_box.module.scss' +import { useI18n } from '@/state/language' + +export default function SearchBox(props) { + const { i18n } = useI18n() + const [searchField, setSearchField] = useState('') + + const filterBySearch = (event) => { + const query = event.target.value + props.handleOnChange(query) + setSearchField(query) + } + + return ( + <> +
+
+
+ +
{ + setSearchField('') + props.handleOnChange('') + }} + > +
+
+
+
+ + ) +} +SearchBox.propTypes = { + className: PropTypes.string, + text: PropTypes.string, + altText: PropTypes.string, + handleOnChange: PropTypes.func, + searchField: PropTypes.string, +} diff --git a/apps/directory/src/component/switch.jsx b/apps/directory/src/component/switch.jsx new file mode 100644 index 0000000..6661292 --- /dev/null +++ b/apps/directory/src/component/switch.jsx @@ -0,0 +1,35 @@ +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import classes from './scss/switch.module.scss' +import { useI18n } from '@/state/language' + +export default function Switch(props) { + const [on, setOn] = useState(props.on) + const { i18n } = useI18n() + + useEffect(() => { + setOn(props.on) + }, [props.on]) + + return ( +
{ + if (props.handleOnClick) { + props.handleOnClick(!on) + } + }} + > + {i18n(props.text)} +
+ + +
+
+ ) +} +Switch.propTypes = { + on: PropTypes.bool, + text: PropTypes.string, + handleOnClick: PropTypes.func, +} diff --git a/apps/directory/src/component/totop_button.jsx b/apps/directory/src/component/totop_button.jsx new file mode 100644 index 0000000..d1b8e10 --- /dev/null +++ b/apps/directory/src/component/totop_button.jsx @@ -0,0 +1,64 @@ +import { useEffect, useState, useCallback } from 'react' +import PropTypes from 'prop-types' +import classes from './scss/totop_button.module.scss' + +export default function ToTopButton(props) { + const [hidden, setHidden] = useState(true) + + useEffect(() => { + const handleButton = () => { + const scrollBarPos = window.scrollY || 0 + setHidden(!(scrollBarPos > 100)) + } + window.addEventListener('scroll', handleButton) + return () => { + window.removeEventListener('scroll', handleButton) + } + }, []) + + const smoothScroll = useCallback((target) => { + const targetElement = document.querySelector(target) + const targetPosition = + targetElement.getBoundingClientRect().top + window.scrollY + const startPosition = window.scrollY + const distance = targetPosition - startPosition + const duration = 1000 + let start = null + window.requestAnimationFrame(step) + function step(timestamp) { + if (!start) start = timestamp + const progress = timestamp - start + window.scrollTo( + 0, + easeInOutCubic(progress, startPosition, distance, duration) + ) + if (progress < duration) window.requestAnimationFrame(step) + } + function easeInOutCubic(t, b, c, d) { + t /= d / 2 + if (t < 1) return (c / 2) * t * t * t + b + t -= 2 + return (c / 2) * (t * t * t + 2) + b + } + }, []) + + return ( + <> +
{ + smoothScroll('#root') + }} + > +
+
+
+
+
+ + ) +} +ToTopButton.propTypes = { + onClick: PropTypes.func, + className: PropTypes.string, +} diff --git a/apps/directory/src/component/voice.jsx b/apps/directory/src/component/voice.jsx new file mode 100644 index 0000000..3ee7241 --- /dev/null +++ b/apps/directory/src/component/voice.jsx @@ -0,0 +1,46 @@ +import { useEffect, useRef } from 'react' +import PropTypes from 'prop-types' + +export default function VoiceElement({ src, replay, handleAduioStateChange }) { + const audioRef = useRef(null) + + useEffect(() => { + if (src) { + audioRef.current.src = src + audioRef.current.play() + } else { + audioRef.current.pause() + } + }, [src]) + + useEffect(() => { + if (replay) { + audioRef.current.currentTime = 0 + audioRef.current.play() + } + }, [replay]) + + return ( + + ) +} +VoiceElement.propTypes = { + src: PropTypes.string, + handleAduioStateChange: PropTypes.func, + replay: PropTypes.bool, +} diff --git a/apps/directory/src/i18n.json b/apps/directory/src/i18n.json new file mode 100644 index 0000000..0741553 --- /dev/null +++ b/apps/directory/src/i18n.json @@ -0,0 +1,161 @@ +{ + "available": ["zh-CN", "en-US"], + "key": { + "dynamic_compile": { + "zh-CN": "动态集录", + "en-US": "Dynamic Compile" + }, + "home": { + "zh-CN": "首页", + "en-US": "Home" + }, + "official_page": { + "zh-CN": "官方页面", + "en-US": "Official Page" + }, + "disclaimer": { + "zh-CN": "免责声明", + "en-US": "Disclaimer" + }, + "disclaimer_content": { + "zh-CN": "本网站由 Halyul 设立并为明日方舟社区服务,Halyul 声明本网站完全独立运营,与 上海鹰角网络科技有限公司, Esoteric Software LLC 或其任何关联实体并无任何联系。", + "en-US": "This website is set up and operated by Halyul for the benefit of the Arknights Community. Halyul hereby states that this website is dedicated, but not related to Hypergryph Co., Ltd, Esoteric Software LLC or any of its affiliated entity." + }, + "privacy_policy": { + "zh-CN": "隐私政策", + "en-US": "Privacy Policy" + }, + "contact_us": { + "zh-CN": "联系我们", + "en-US": "Contact Us" + }, + "all": { + "zh-CN": "综合", + "en-US": "All" + }, + "operator": { + "zh-CN": "精英2", + "en-US": "Elite 2" + }, + "skin": { + "zh-CN": "时装", + "en-US": "Skin" + }, + "voice": { + "zh-CN": "语音", + "en-US": "Voice" + }, + "music": { + "zh-CN": "音乐", + "en-US": "Music" + }, + "showcase": { + "zh-CN": "壁纸", + "en-US": "Wallpaper" + }, + "directory": { + "zh-CN": "目录页", + "en-US": "Directory Page" + }, + "animation": { + "zh-CN": "动画", + "en-US": "Animation" + }, + "backgrounds": { + "zh-CN": "背景", + "en-US": "Backgrounds" + }, + "CN_MANDARIN": { + "zh-CN": "普通话", + "en-US": "Mandarin" + }, + "JP": { + "zh-CN": "日语", + "en-US": "Japanese" + }, + "KR": { + "zh-CN": "韩语", + "en-US": "Korean" + }, + "EN": { + "zh-CN": "英语", + "en-US": "English" + }, + "ITA": { + "zh-CN": "意大利语", + "en-US": "Italian" + }, + "CN_TOPOLECT": { + "zh-CN": "中文方言", + "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": "简体中文", + "en-US": "Chinese (Simplified)" + }, + "en-US": { + "zh-CN": "英语", + "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" + }, + "switch_language": { + "zh-CN": "🌐 切换语言", + "en-US": "🌐 Switch Language" + }, + "fast_navigation": { + "zh-CN": "🧭 快速导航", + "en-US": "🧭 Fast Navigation" + }, + "return": { + "zh-CN": "↩️ 返回", + "en-US": "↩️ Return" + }, + "search_by_name": { + "zh-CN": "名字搜索", + "en-US": "Search by Name" + }, + "new_op_wait_to_update": { + "zh-CN": "个新干员等待更新", + "en-US": "New Operator(s) Waiting to Update" + } + } +} diff --git a/apps/directory/src/routes/Error.jsx b/apps/directory/src/routes/Error.jsx new file mode 100644 index 0000000..232f413 --- /dev/null +++ b/apps/directory/src/routes/Error.jsx @@ -0,0 +1,197 @@ +import { useState, useEffect, useMemo, useRef, useCallback } from 'react' +import { useNavigate, useRouteError } from 'react-router-dom' +import header from '@/scss/root/header.module.scss' +import classes from '@/scss/error/Error.module.scss' +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 VoiceElement from '@/component/voice' +import { spine } from '@aklive2d/module' +import useInsight from '@/state/insight' +import buildConfig from '!/config.json' + +const voiceOnAtom = atomWithStorage('voiceOn', false) +const config = buildConfig.error_files +const obj = config.files[Math.floor(Math.random() * config.files.length)] +const filename = obj.key.replace(/#/g, '%23') +const padding = obj.paddings +let lastVoiceState = 'ended' + +export default function Error() { + // eslint-disable-next-line no-unused-vars + const _trackEvt = useInsight() + const error = useRouteError() + const navigate = useNavigate() + const { setTitle } = useHeader() + const [voiceOn, setVoiceOn] = useAtom(voiceOnAtom) + const [spineDone, _setSpineDone] = useState(false) + const spineRef = useRef(null) + const spineDoneRef = useRef(spineDone) + const voiceOnRef = useRef(voiceOn) + const [voiceSrc, setVoiceSrc] = useState(null) + const [voiceReplay, setVoiceReplay] = useState(false) + const [spinePlayer, setSpinePlayer] = useState(null) + + const setSpineDone = (data) => { + spineDoneRef.current = data + _setSpineDone(data) + } + + const content = useMemo( + () => [ + 'エラー発生。', + '发生错误。', + 'Error occured.', + '에러 발생.', + '發生錯誤。', + ], + [] + ) + + useEffect(() => { + console.log(error) + }, [error]) + + useEffect(() => { + setTitle(content[0]) + }, [content, setTitle]) + + useEffect(() => { + if (!voiceOn) { + setVoiceSrc(null) + } else { + setVoiceSrc(`/${buildConfig.directory_folder}/error.ogg`) + if (spinePlayer) { + spinePlayer.animationState.setAnimation(0, 'Interact', false, 0) + spinePlayer.animationState.addAnimation(0, 'Relax', true, 0) + } + } + }, [spinePlayer, voiceOn]) + + useEffect(() => { + voiceOnRef.current = voiceOn + }, [voiceOn]) + + const playVoice = useCallback(() => { + if (lastVoiceState === 'ended' && voiceSrc !== null) { + setVoiceReplay(true) + } + }, [voiceSrc]) + + const handleAduioStateChange = useCallback((e, state) => { + lastVoiceState = state + if (state === 'ended') { + setVoiceReplay(false) + } + }, []) + + useEffect(() => { + if (spineRef.current?.children.length === 0) { + setSpinePlayer( + new spine.SpinePlayer(spineRef.current, { + skelUrl: `./_assets/${filename}.skel`, + atlasUrl: `./_assets/${filename}.atlas`, + 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.3, + 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() + } + }, + }) + ) + } + return () => { + if (spinePlayer) { + spinePlayer.dispose() + } + } + }, [playVoice, spinePlayer]) + + return ( +
+
+ navigate(-1, { replace: true })} /> + setVoiceOn(!voiceOn)} + /> +
+
+ {content.map((item, index) => { + return ( +
+ +
+ ) + })} +
+ +
+
+ ) +} diff --git a/apps/directory/src/routes/Root.jsx b/apps/directory/src/routes/Root.jsx new file mode 100644 index 0000000..60f0729 --- /dev/null +++ b/apps/directory/src/routes/Root.jsx @@ -0,0 +1,307 @@ +import { useState, useEffect, useMemo, useCallback } from 'react' +import PropTypes from 'prop-types' +import { + Outlet, + Link, + NavLink, + useNavigate, + ScrollRestoration, +} from 'react-router-dom' +import classes from '@/scss/root/Root.module.scss' +import header from '@/scss/root/header.module.scss' +import footer from '@/scss/root/footer.module.scss' +import drawer from '@/scss/root/drawer.module.scss' +import routes from '@/routes' +import { useConfig } from '@/state/config' +import { useHeader } from '@/state/header' +import { useAppbar } from '@/state/appbar' +import { useI18n, useLanguage } from '@/state/language' +import Dropdown from '@/component/dropdown' +import Popup from '@/component/popup' +import Border from '@/component/border' +import CharIcon from '@/component/char_icon' +import ToTopButton from '@/component/totop_button' + +const currentYear = new Date().getFullYear() + +export default function Root() { + const [drawerHidden, setDrawerHidden] = useState(true) + const { title, tabs, setCurrentTab, headerIcon } = useHeader() + const { extraArea } = useAppbar() + const { fetchOfficialUpdate } = useConfig() + + const headerTabs = useMemo(() => { + return tabs?.map((item) => { + return + }) + }, [tabs]) + + const toggleDrawer = useCallback( + (value) => { + setDrawerHidden(value || !drawerHidden) + }, + [drawerHidden] + ) + + useEffect(() => { + if (tabs.length > 0) { + setCurrentTab(tabs[0].key) + } else { + setCurrentTab(null) + } + }, [setCurrentTab, tabs]) + + useEffect(() => { + fetchOfficialUpdate() + }, [fetchOfficialUpdate]) + + useEffect(() => { + document.querySelector('.loader').classList.add('loaded') + setTimeout(() => { + document.querySelector('.loader').style.display = 'none' + }, 500) + }, []) + + return ( + <> +
+
toggleDrawer()} + > +
+
+
+
+ +
+
+ {extraArea} + +
+
+ +
+
+
+ {headerIcon && ( +
+ +
+ )} + {title} +
+
{headerTabs}
+
+ + + + +
+ + + ) +} + +function FooterElement() { + const { i18n } = useI18n() + const navigate = useNavigate() + + return useMemo(() => { + return ( +
+
+
+ + {i18n('disclaimer_content')} + +
+
+ + {i18n('privacy_policy')} + +
+
+ + GitHub + +
+
+ + ak#halyul.dev + +
+
+
{ + navigate('/error') + }} + > + + Spine Runtimes © 2013 - 2019 Esoteric Software LLC + + + Assets © 2017 - {currentYear} Arknights/Hypergryph Co., + Ltd + + Source Code © 2021 - {currentYear} Halyul +
+
+ ) + }, [i18n, navigate]) +} + +function DrawerDestinations({ toggleDrawer }) { + const { i18n } = useI18n() + const { textDefaultLang, alternateLang } = useLanguage() + + return routes + .filter((item) => item.inDrawer) + .map((item) => { + if (typeof item.element.type === 'string') { + return ( + toggleDrawer(false)} + > +
{i18n(item.name, textDefaultLang)}
+
{i18n(item.name, alternateLang)}
+ + ) + } else { + return ( + + `${drawer.link} ${isActive ? drawer.active : ''}` + } + onClick={() => toggleDrawer(false)} + > +
{i18n(item.name, textDefaultLang)}
+
{i18n(item.name, alternateLang)}
+
+ ) + } + }) +} + +function LanguageDropdown() { + const { language, setLanguage } = useLanguage() + const { i18n, i18nValues } = useI18n() + + return useMemo(() => { + return ( + { + return { + name: i18n(item), + value: item, + } + })} + onClick={(item) => { + setLanguage(item.value) + }} + /> + ) + }, [i18n, i18nValues.available, language, setLanguage]) +} + +function HeaderTabsElement({ item }) { + const { currentTab, setCurrentTab } = useHeader() + const { i18n } = useI18n() + + return ( +
{ + setCurrentTab(item.key) + item.onClick && item.onClick(e, currentTab) + }} + style={item.style} + > +
+ {i18n(item.key)} +
+
+ ) +} +HeaderTabsElement.propTypes = { + item: PropTypes.object.isRequired, +} + +function HeaderButton() { + const navigate = useNavigate() + const { i18n } = useI18n() + const { fastNavigation } = useHeader() + + if (fastNavigation.length > 0) { + return ( + { + navigate(item.value) + }} + className={header['fast-navigate']} + iconStyle={{ + borderWidth: '0.15em', + width: '1em', + transform: + 'translateY(-0.4rem) translateX(-0.7rem) rotate(-45deg)', + height: '1em', + }} + left={true} + /> + ) + } else { + return ( +
+ +
+
+ +
+ ) + } +} diff --git a/apps/directory/src/routes/index.jsx b/apps/directory/src/routes/index.jsx new file mode 100644 index 0000000..9edf7f1 --- /dev/null +++ b/apps/directory/src/routes/index.jsx @@ -0,0 +1,29 @@ +import Home from '@/routes/path/Home' +import Operator from '@/routes/path/Operator' + +export default [ + { + path: '/', + index: true, + name: 'home', + element: , + inDrawer: true, + routeable: true, + }, + { + path: 'https://gura.ch/dynamicCompile', + index: false, + name: 'official_page', + element: , + inDrawer: true, + routeable: false, + }, + { + path: ':key', + index: false, + name: 'operator', + element: , + inDrawer: false, + routeable: true, + }, +] diff --git a/apps/directory/src/routes/path/Home.jsx b/apps/directory/src/routes/path/Home.jsx new file mode 100644 index 0000000..978e5ac --- /dev/null +++ b/apps/directory/src/routes/path/Home.jsx @@ -0,0 +1,451 @@ +import { useState, useEffect, useCallback, useMemo } from 'react' +import PropTypes from 'prop-types' +import { NavLink, Link } from 'react-router-dom' +import classes from '@/scss/home/Home.module.scss' +import { useConfig } from '@/state/config' +import { useI18n } from '@/state/language' +import { useLanguage } from '@/state/language' +import { useHeader } from '@/state/header' +import { useAppbar } from '@/state/appbar' +import VoiceElement from '@/component/voice' +import { useAtom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' +import CharIcon from '@/component/char_icon' +import Border from '@/component/border' +import useInsight from '@/state/insight' +import Switch from '@/component/switch' +import SearchBox from '@/component/search_box' +import buildConfig from '!/config.json' + +const voiceOnAtom = atomWithStorage('voiceOn', false) +let lastVoiceState = 'ended' + +export default function Home() { + // eslint-disable-next-line no-unused-vars + const _trackEvt = useInsight() + const { setTitle, setTabs, currentTab, setHeaderIcon, setFastNavigation } = + useHeader() + const { config, operators, officialUpdate } = useConfig() + const { i18n } = useI18n() + const [content, setContent] = useState([]) + const [voiceOn] = useAtom(voiceOnAtom) + const [voiceSrc, setVoiceSrc] = useState(null) + const [voiceReplay, setVoiceReplay] = useState(false) + const { language } = useLanguage() + const [navigationList, setNavigationList] = useState([]) + const [searchField, setSearchField] = useState('') + const [updatedList, setUpdatedList] = useState([]) + + useEffect(() => { + setTitle('dynamic_compile') + setTabs([ + { + key: 'all', + }, + { + key: 'operator', + }, + { + key: 'skin', + }, + ]) + setHeaderIcon(null) + }, [setHeaderIcon, setTabs, setTitle]) + + useEffect(() => { + setContent(config?.operators || []) + }, [config]) + + const handleAduioStateChange = useCallback((e, state) => { + lastVoiceState = state + if (state === 'ended') { + setVoiceReplay(false) + } + }, []) + + const isShown = useCallback( + (type) => currentTab === 'all' || currentTab === type, + [currentTab] + ) + + const fastNavigateDict = useMemo(() => { + const dict = {} + operators.forEach((item) => { + if (!(item.date in dict)) { + dict[item.date] = [] + } + dict[item.date].push({ + codename: item.codename, + link: item.link, + type: item.type, + color: item.color, + }) + }) + return dict + }, [operators]) + + useEffect(() => { + const list = [] + for (const [key, value] of Object.entries(fastNavigateDict)) { + const newValue = value.filter((item) => isShown(item.type)) + if (newValue.length > 0) { + list.push({ + name: key, + value: null, + type: 'date', + }) + newValue.forEach((item) => { + list.push({ + name: item.codename[language], + value: item.link, + type: 'item', + color: item.color, + icon: ( + + ), + }) + }) + } + } + setNavigationList(list) + setUpdatedList(list) + }, [fastNavigateDict, isShown, language]) + + useEffect(() => { + const list = navigationList.filter((item) => { + return ( + item.name.toLowerCase().indexOf(searchField.toLowerCase()) !== + -1 || item.type === 'date' + ) + }) + const newList = [] + for (let i = 0; i < list.length - 1; i++) { + const firstType = list[i].type + const secondType = list[i + 1].type + if (firstType === 'date' && secondType === 'date') { + continue + } + newList.push(list[i]) + } + if (list.length > 0 && list[list.length - 1].type !== 'date') { + newList.push(list[list.length - 1]) + } + setUpdatedList(newList) + }, [navigationList, searchField]) + + useEffect(() => { + setFastNavigation([ + { + type: 'custom', + component: ( + { + setSearchField(e) + }} + searchField={searchField} + /> + ), + }, + ...updatedList, + ]) + }, [searchField, setFastNavigation, updatedList]) + + const handleVoicePlay = useCallback( + (src) => { + if (!voiceOn) { + setVoiceSrc(null) + } else { + if (src === voiceSrc && lastVoiceState === 'ended') { + setVoiceReplay(true) + } else { + setVoiceSrc(src) + } + } + }, + [voiceOn, voiceSrc] + ) + + return ( +
+ {officialUpdate.length > operators.length && ( +
+
+
+
+
+ {officialUpdate.length - operators.length}{' '} + {i18n('new_op_wait_to_update')} +
+
+ {officialUpdate.dates + .reduce((acc, cur) => { + const op = officialUpdate[cur] + return [...acc, ...op] + }, []) + .slice( + 0, + officialUpdate.length - + operators.length + ) + .map((entry, index) => { + return ( + +
+
+
+
+
+ +
+
+ { + entry + .codename[ + language + ] + } +
+
+
+
+
+
+
+
+
+
+ + ) + })} +
+
+
+
+ {officialUpdate.latest} +
+
+ +
+ )} + {content.map((v) => { + const length = v.filter((v) => isShown(v.type)).length + return ( + + ) + })} + +
+ ) +} + +function OperatorElement({ item, hidden, handleVoicePlay }) { + const { textDefaultLang, language, alternateLang } = useLanguage() + + return useMemo(() => { + return ( +
+ handleVoicePlay( + `/${item.link}/assets/${buildConfig.voice_folders.main}/${buildConfig.app_voice_url}` + ) + } + > +
+
+
+ +
+
+
+
+ {item.codename[language]} +
+
+ +
+
+
+ + { + item.codename[ + language.startsWith('en') + ? alternateLang + : textDefaultLang + ] + } + +
+
+
+
+ + ) + }, [ + item, + hidden, + language, + alternateLang, + textDefaultLang, + handleVoicePlay, + ]) +} + +function VoiceSwitchElement({ src, replay, handleAduioStateChange }) { + const [voiceOn, setVoiceOn] = useAtom(voiceOnAtom) + const { setExtraArea } = useAppbar() + + useEffect(() => { + setExtraArea([ + setVoiceOn(!voiceOn)} + />, + ]) + }, [voiceOn, setExtraArea, setVoiceOn]) + + return ( + + ) +} + +VoiceSwitchElement.propTypes = { + src: PropTypes.string, + replay: PropTypes.bool, + handleAduioStateChange: PropTypes.func, +} + +function ImageElement({ item }) { + const { language } = useLanguage() + return ( + {item.codename[language]} + ) +} +ImageElement.propTypes = { + item: PropTypes.object.isRequired, + fallback_name: PropTypes.string, + codename: PropTypes.object, +} diff --git a/apps/directory/src/routes/path/Operator.jsx b/apps/directory/src/routes/path/Operator.jsx new file mode 100644 index 0000000..577be01 --- /dev/null +++ b/apps/directory/src/routes/path/Operator.jsx @@ -0,0 +1,694 @@ +import { useState, useEffect, useRef, useCallback, useMemo } from 'react' +import { useParams, useNavigate, Link } from 'react-router-dom' +import classes from '@/scss/operator/Operator.module.scss' +import { useConfig } from '@/state/config' +import { useLanguage } from '@/state/language' +import { useHeader } from '@/state/header' +import { useAppbar } from '@/state/appbar' +import VoiceElement from '@/component/voice' +import useInsight from '@/state/insight' +import { spine } from '@aklive2d/module' +import Border from '@/component/border' +import { useI18n } from '@/state/language' +import Switch from '@/component/switch' +import { atom, useAtom } from 'jotai' +import buildConfig from '!/config.json' + +const musicMapping = buildConfig.music_mapping +const getVoiceFoler = (lang) => { + const folderObject = buildConfig.voice_folders + const voiceFolder = + folderObject.sub.find((e) => e.lang === lang) || + folderObject.sub.find((e) => e.name === 'custom') + return `${folderObject.main}/${voiceFolder.name}` +} +const defaultSpineAnimationName = 'Idle' +const backgroundAtom = atom(buildConfig.default_background) + +const getPartialName = (type, input) => { + let part + switch (type) { + case 'name': + part = 5 + break + case 'skin': + part = 1 + break + default: + return input + } + return input.replace(/^(.+)( )(·|\/)( )(.+)$/, `$${part}`) +} + +const getTabName = (item, language) => { + if (item.type === 'operator') { + return 'operator' + } else { + return getPartialName('skin', item.codename[language]) + } +} + +export default function Operator() { + const navigate = useNavigate() + const { operators } = useConfig() + const { language } = useLanguage() + const { key } = useParams() + const { setTitle, setTabs, setHeaderIcon, setFastNavigation } = useHeader() + const { setExtraArea } = useAppbar() + const [config, setConfig] = useState(null) + // eslint-disable-next-line no-unused-vars + const _trackEvt = useInsight(`/${key}`) + const spineRef = useRef(null) + const [spineAnimationName, setSpineAnimationName] = useState( + defaultSpineAnimationName + ) + const { i18n } = useI18n() + const [spinePlayer, setSpinePlayer] = useState(null) + const [voiceLang, _setVoiceLang] = useState(null) + const [currentBackground, setCurrentBackground] = useAtom(backgroundAtom) + const [voiceConfig, setVoiceConfig] = useState(null) + const [subtitleLang, setSubtitleLang] = useState(null) + const [hideSubtitle, setHideSubtitle] = useState(true) + const [subtitleObj, _setSubtitleObj] = useState(null) + const [currentVoiceId, setCurrentVoiceId] = useState(null) + const voiceLangRef = useRef(voiceLang) + const subtitleObjRef = useRef(subtitleObj) + const configRef = useRef(config) + const [voiceSrc, setVoiceSrc] = useState(null) + const [isVoicePlaying, _setIsVoicePlaying] = useState(false) + const isVoicePlayingRef = useRef(isVoicePlaying) + + const setVoiceLang = (value) => { + voiceLangRef.current = value + _setVoiceLang(value) + } + + const setSubtitleObj = (value) => { + subtitleObjRef.current = value + _setSubtitleObj(value) + } + + const setIsVoicePlaying = (value) => { + isVoicePlayingRef.current = value + _setIsVoicePlaying(value) + } + + useEffect(() => { + setExtraArea([]) + setFastNavigation([]) + }, [setExtraArea, setFastNavigation]) + + useEffect(() => { + const config = operators.find((item) => item.link === key) + if (config) { + setConfig(config) + configRef.current = config + setSpineAnimationName(defaultSpineAnimationName) + setHeaderIcon(config.type) + if (spineRef.current?.children.length > 0) { + spineRef.current?.removeChild(spineRef.current?.children[0]) + } + fetch(`/${key}/assets/charword_table.json`) + .then((res) => res.json()) + .then((data) => { + setVoiceConfig(data) + }) + } + }, [key, operators, setHeaderIcon]) + + const coverToTab = useCallback( + (item, language) => { + const key = getTabName(item, language) + return { + key: key, + style: { + color: item.color, + }, + onClick: (e, tab) => { + if (tab === key) return + navigate(`/${item.link}`) + }, + } + }, + [navigate] + ) + + const otherEntries = useMemo(() => { + if (!config || !language) return null + return operators + .filter( + (item) => item.id === config.id && item.link !== config.link + ) + .map((item) => { + return coverToTab(item, language) + }) + }, [config, language, operators, coverToTab]) + + useEffect(() => { + if (config) { + setTabs([coverToTab(config, language), ...otherEntries]) + } + }, [config, key, coverToTab, setTabs, otherEntries, language]) + + useEffect(() => { + if (config) { + setTitle(getPartialName('name', config.codename[language])) + } + }, [config, language, key, setTitle]) + + useEffect(() => { + if (spineRef.current?.children.length === 0 && configRef.current) { + const playerConfig = { + atlasUrl: `./${key}/assets/${configRef.current.filename.replace(/#/g, '%23')}.atlas`, + animation: spineAnimationName, + premultipliedAlpha: true, + alpha: true, + backgroundColor: '#00000000', + viewport: { + debugRender: false, + padLeft: `${configRef.current.viewport_left}%`, + padRight: `${configRef.current.viewport_right}%`, + padTop: `${configRef.current.viewport_top}%`, + padBottom: `${configRef.current.viewport_bottom}%`, + x: 0, + y: 0, + }, + showControls: false, + touch: false, + fps: 60, + defaultMix: 0.3, + success: (player) => { + if ( + player.skeleton.data.animations + .map((e) => e.name) + .includes('Start') + ) { + player.animationState.setAnimation(0, 'Start', false, 0) + player.animationState.addAnimation(0, 'Idle', true, 0) + } + let lastVoiceId = null + let currentVoiceId = null + player.canvas.onclick = () => { + if (!voiceLangRef.current) return + const voiceId = () => { + const keys = Object.keys(subtitleObjRef.current) + const id = + keys[Math.floor(Math.random() * keys.length)] + return id === lastVoiceId ? voiceId() : id + } + const id = voiceId() + currentVoiceId = id + setCurrentVoiceId(id) + setVoiceSrc( + `/${configRef.current.link}/assets/${getVoiceFoler(voiceLangRef.current)}/${id}.ogg` + ) + lastVoiceId = currentVoiceId + } + }, + } + + if (configRef.current.use_json) { + playerConfig.jsonUrl = `./${key}/assets/${configRef.current.filename.replace(/#/g, '%23')}.json` + } else { + playerConfig.skelUrl = `./${key}/assets/${configRef.current.filename.replace(/#/g, '%23')}.skel` + } + setSpinePlayer( + new spine.SpinePlayer(spineRef.current, playerConfig) + ) + } + }, [spineAnimationName, setSpinePlayer, spinePlayer, key]) + + useEffect(() => { + return () => { + if (spinePlayer) { + spinePlayer.dispose() + } + } + }, [spinePlayer]) + + useEffect(() => { + if (voiceConfig && voiceLang) { + let subtitleObj = voiceConfig.subtitleLangs[subtitleLang || 'zh-CN'] + let subtitleKey = 'default' + if (subtitleObj[voiceLang]) { + subtitleKey = voiceLang + } + setSubtitleObj(subtitleObj[subtitleKey]) + } + }, [subtitleLang, voiceConfig, voiceLang]) + + const handleAduioStateChange = useCallback((e, state) => { + switch (state) { + case 'play': + setIsVoicePlaying(true) + break + default: + setIsVoicePlaying(false) + break + } + }, []) + + useEffect(() => { + if (subtitleLang) { + if (isVoicePlaying) { + setHideSubtitle(false) + } else { + const autoHide = () => { + if (isVoicePlayingRef.current) return + setHideSubtitle(true) + } + setTimeout(autoHide, 5 * 1000) + return () => { + clearTimeout(autoHide) + } + } + } else { + setHideSubtitle(true) + } + }, [subtitleLang, isVoicePlaying]) + + useEffect(() => { + if (voiceLang && isVoicePlaying) { + const audioUrl = `/assets/${getVoiceFoler(voiceLang)}/${currentVoiceId}.ogg` + if ( + voiceSrc !== + window.location.href.replace(/\/$/g, '') + audioUrl + ) { + setVoiceSrc(`/${config.link}${audioUrl}`) + } + } + }, [voiceLang, isVoicePlaying, currentVoiceId, config, voiceSrc]) + + const playAnimationVoice = useCallback( + (animation) => { + if (voiceLangRef.current) { + let id = null + if (animation === 'Idle') id = 'CN_011' + if (animation === 'Interact') id = 'CN_034' + if (animation === 'Special') id = 'CN_042' + if (id) { + setCurrentVoiceId(id) + setVoiceSrc( + `/${key}/assets/${getVoiceFoler(voiceLangRef.current)}/${id}.ogg` + ) + } + } + }, + [key] + ) + + useEffect(() => { + if (!voiceLang) { + setVoiceSrc(null) + } + }, [voiceLang]) + + const setSpineAnimation = useCallback( + (animation) => { + playAnimationVoice(animation) + const entry = spinePlayer.animationState.setAnimation( + 0, + animation, + true + ) + entry.mixDuration = 0.3 + setSpineAnimationName(animation) + }, + [playAnimationVoice, spinePlayer] + ) + + const spineSettings = [ + { + name: 'animation', + options: [ + { + name: 'idle', + onClick: () => setSpineAnimation('Idle'), + activeRule: () => { + return spineAnimationName === 'Idle' + }, + }, + { + name: 'interact', + onClick: () => setSpineAnimation('Interact'), + activeRule: () => { + return spineAnimationName === 'Interact' + }, + }, + { + name: 'special', + onClick: () => setSpineAnimation('Special'), + activeRule: () => { + return spineAnimationName === 'Special' + }, + }, + ], + }, + { + name: 'voice', + options: + (voiceConfig && + Object.keys(voiceConfig?.voiceLangs['zh-CN']).map( + (item) => { + return { + name: i18n(item), + onClick: () => { + if (voiceLang !== item) { + setVoiceLang(item) + } else { + setVoiceLang(null) + } + if (!isVoicePlayingRef.current) { + playAnimationVoice(spineAnimationName) + } + }, + activeRule: () => { + return voiceLang === item + }, + } + } + )) || + [], + }, + { + 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: 'music', + el: , + }, + { + name: 'backgrounds', + options: + buildConfig.background_files.map((item) => { + return { + name: item, + onClick: () => { + setCurrentBackground(item) + }, + activeRule: () => { + return currentBackground === item + }, + } + }) || [], + }, + ] + + if (!buildConfig.available_operators.includes(key)) { + throw new Error('Operator not found') + } + + return ( +
+
+
+ {spineSettings.map((item) => { + if (item.el) { + return
{item.el}
+ } + if (item.options.length === 0) return null + return ( +
+
+
+ {i18n(item.name)} +
+
+
+ {item.options.map((option) => { + return ( +
+ option.onClick(e) + } + key={option.name} + > +
+
+
+ {i18n(option.name)} +
+
+
+
+ ) + })} +
+
+ ) + })} +
+
+
+ {i18n('external_links')} +
+
+
+ +
+
+
+
+ {i18n('web_version')} +
+
+
+
+
+
+
+
+
+ + {config?.workshopId && ( + +
+
+
+
+ {i18n('steam_workshop')} +
+
+
+
+
+
+
+
+
+ + )} +
+
+
+
+ {config && ( + {config?.codename[language]} + )} +
+ {currentVoiceId && subtitleObj && ( +
+
+ {subtitleObj[currentVoiceId]?.title} +
+
+ {subtitleObj[currentVoiceId]?.text} + +
+
+ )} +
+
+ + +
+ ) +} + +function MusicElement() { + const [enableMusic, setEnableMusic] = useState(false) + const { i18n } = useI18n() + const musicIntroRef = useRef(null) + const musicLoopRef = useRef(null) + const [background] = useAtom(backgroundAtom) + + useEffect(() => { + if (musicIntroRef.current && musicIntroRef.current) { + musicIntroRef.current.volume = 0.5 + musicLoopRef.current.volume = 0.5 + } + }, [musicIntroRef, musicLoopRef]) + + useEffect(() => { + if (!enableMusic || background) { + musicIntroRef.current.pause() + musicLoopRef.current.pause() + } + }, [enableMusic, background]) + + useEffect(() => { + if (background && enableMusic) { + const introOgg = musicMapping[background].intro + const intro = `./chen/assets/${buildConfig.music_folder}/${introOgg}` + const loop = `./chen/assets/${buildConfig.music_folder}/${musicMapping[background].loop}` + musicLoopRef.current.src = loop + if (introOgg) { + musicIntroRef.current.src = intro || loop + } else { + musicLoopRef.current.play() + } + } + }, [background, enableMusic]) + + const handleIntroTimeUpdate = useCallback(() => { + if ( + musicIntroRef.current.currentTime >= + musicIntroRef.current.duration - 0.3 + ) { + musicIntroRef.current.pause() + musicLoopRef.current.play() + } + }, []) + + const handleLoopTimeUpdate = useCallback(() => { + if ( + musicLoopRef.current.currentTime >= + musicLoopRef.current.duration - 0.3 + ) { + musicLoopRef.current.currentTime = 0 + musicLoopRef.current.play() + } + }, []) + + return ( +
+
setEnableMusic(!enableMusic)} + > +
{i18n('music')}
+
+ +
+
+ + +
+ ) +} diff --git a/apps/directory/src/scss/_main_share.scss b/apps/directory/src/scss/_main_share.scss new file mode 100644 index 0000000..0222976 --- /dev/null +++ b/apps/directory/src/scss/_main_share.scss @@ -0,0 +1,20 @@ +.main { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: flex-start; + padding-bottom: 3rem; + margin: 0 auto; + width: 70%; + max-width: 100rem; + padding-top: 5rem; + min-height: calc(100vh - 5rem - 3rem); + + @media only screen and (width <= 430px) { + & { + width: 100vw; + margin-left: 0; + } + } +} diff --git a/apps/directory/src/scss/_page_base.scss b/apps/directory/src/scss/_page_base.scss new file mode 100644 index 0000000..36d49cf --- /dev/null +++ b/apps/directory/src/scss/_page_base.scss @@ -0,0 +1,369 @@ +.date { + margin: 1.5rem; + font-family: + Bender, 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans', + sans-serif; + font-weight: bold; + text-align: right; + color: var(--date-color); + font-size: 1.5rem; + letter-spacing: 0.1rem; + flex: auto; + user-select: none; + + @media only screen and (width <= 430px) { + & { + margin: 0; + } + } +} + +.container { + color: var(--text-color-full); + display: flex; + justify-content: space-between; + align-items: baseline; + font-size: 1.25rem; + font-weight: bold; + + .title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.3em; + height: auto; + } + + .type { + display: flex; + flex-direction: row; + align-items: center; + + svg { + width: 1.5rem; + fill: var(--text-color); + } + } +} + +.group { + padding: 1rem; + display: flex; + align-items: flex-end; + flex-wrap: wrap; + user-select: none; + + .operator-group { + display: flex; + flex-flow: row wrap; + + @media only screen and (width <= 430px) { + & { + width: 100%; + padding-bottom: 1rem; + overflow-y: hidden; + flex-wrap: nowrap; + } + } + } + + .item { + position: relative; + flex-shrink: 0; + cursor: pointer; + width: 12rem; + margin: 1.25rem; + 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 5px + ); + + .background-filler { + border-right: 1px solid + var(--home-item-background-linear-gradient-color); + position: absolute; + inset: 0 -1px 0 0; + } + + .outline { + display: block; + position: absolute; + opacity: 0; + visibility: hidden; + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + width: 100%; + height: 100%; + left: -6px; + top: -6px; + border: var(--home-item-outline-color) 1px dashed; + padding: 6px; + + &::before, + &::after { + content: ''; + display: block; + position: absolute; + left: -3px; + height: 3px; + width: 100%; + border-left: var(--text-color) solid 3px; + border-right: var(--text-color) solid 3px; + } + + &::before { + top: -3px; + } + + &::after { + bottom: -3px; + } + } + + .img { + transition: background-color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + + & img { + height: auto; + width: 100%; + display: block; + } + } + + .info { + white-space: nowrap; + position: relative; + padding: 0.8rem 0.4rem; + line-height: 1.2em; + height: 36px; + + .wrapper { + overflow: hidden; + text-overflow: ellipsis; + color: var(--secondary-text-color); + + .text { + font-size: 0.75rem; + font-family: + Geometos, 'Noto Sans SC', 'Noto Sans JP', + 'Noto Sans KR', 'Noto Sans', sans-serif; + margin-top: 1rem; + } + } + + .background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + visibility: hidden; + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + background-image: linear-gradient( + 70deg, + transparent 40%, + currentcolor 150% + ); + } + } + + &:hover { + .img { + background-color: var(--home-item-hover-background-color); + } + + .outline, + .info .background { + opacity: 1; + visibility: visible; + } + } + + @media only screen and (width <= 430px) { + & { + width: 8.08rem; + margin: 1.08rem; + + .outline, + .info .background { + opacity: 1; + visibility: visible; + } + } + } + } + + @media only screen and (width <= 430px) { + & { + align-items: flex-start; + flex-direction: column-reverse; + padding-bottom: 0; + } + } +} + +.styled-selection { + margin-bottom: 0.8rem; + + .content { + padding: 0.8rem 0; + cursor: pointer; + transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + + .option { + white-space: nowrap; + pointer-events: none; + position: relative; + transform: translate3d(0, 0, 1px); + font-size: 1rem; + padding: 0.44rem 3.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 1px, + transparent 1px 4px + ); + transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + + .outline { + width: 100%; + height: 100%; + left: -6px; + top: -6px; + border: var(--home-item-outline-color) 1px dashed; + padding: 6px; + + &::before, + &::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; + } + + &::before { + top: -2px; + } + + &::after { + bottom: -2px; + } + } + + &::before, + .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; + } + + &::before { + right: 0; + top: 0; + width: 60%; + height: 100%; + background-image: linear-gradient( + 90deg, + transparent, + currentcolor + ); + } + + .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); + } + + .arrow-icon { + display: inline-block; + position: absolute; + z-index: 0; + right: -0.1em; + top: 0.55em; + width: 2em; + height: 1em; + transform: rotate(90deg); + 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; + opacity: 0; + visibility: hidden; + + .bar { + position: absolute; + width: 100%; + height: 0.25em; + background-color: var(--text-color-full); + will-change: auto; + } + + .bar:nth-child(1) { + transform: rotateZ(45deg) scaleX(0.5) translateX(0.8em); + } + + .bar:nth-child(2) { + transform: rotateZ(-45deg) scaleX(0.5) translateX(-0.8em); + } + + .bar:nth-child(3), + .bar:nth-child(4) { + transform: translateY(1em) rotateZ(90deg) scaleX(0.5); + } + } + } + + &:hover, + &.active { + transform: translate3d(6px, 0, 1px); + + .option::before, + .option .outline { + opacity: 1; + visibility: visible; + } + + .arrow-icon { + opacity: 1; + visibility: visible; + } + } + + &.active { + .option .tick-icon { + opacity: 1; + visibility: visible; + } + } + } +} + +.no-overflow { + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/apps/directory/src/scss/changelogs/Changelogs.module.scss b/apps/directory/src/scss/changelogs/Changelogs.module.scss new file mode 100644 index 0000000..c126d8c --- /dev/null +++ b/apps/directory/src/scss/changelogs/Changelogs.module.scss @@ -0,0 +1,30 @@ +@use '@/scss/page_base'; + +.group { + flex-direction: column; + align-items: stretch; + + .info { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + padding-left: 1rem; + word-break: break-word; + + .content { + font-size: 1.5rem; + display: list-item; + } + } + + .date { + margin-bottom: 1rem; + } + + @media only screen and (width <= 430px) { + flex-direction: column-reverse; + align-items: flex-start; + padding-bottom: 1rem; + } +} diff --git a/apps/directory/src/scss/error/Error.module.scss b/apps/directory/src/scss/error/Error.module.scss new file mode 100644 index 0000000..d5d042c --- /dev/null +++ b/apps/directory/src/scss/error/Error.module.scss @@ -0,0 +1,61 @@ +@use '@/scss/main_share'; + +.error { + min-height: 100vh; + display: flex; + flex-direction: column; + font-size: 16px; + user-select: none; + + .header { + padding: 1rem; + justify-content: space-between; + pointer-events: auto; + } + + .main { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding-top: 6rem; + font-size: 3rem; + gap: 2rem; + padding-bottom: 0; + } + + .spine { + max-width: 600px; + flex: 1; + visibility: hidden; + opacity: 0; + + &.active { + visibility: visible; + opacity: 1; + } + } + + @media (width <= 768px) { + .main { + padding-top: 6rem; + max-height: calc(100vh - 6rem); + } + + .content { + font-size: 2rem; + } + } + + @media (width <= 480px) { + .main { + padding-top: 4rem; + max-height: calc(100vh - 4rem); + } + + .content { + font-size: 1.5rem; + } + } +} diff --git a/apps/directory/src/scss/home/Home.module.scss b/apps/directory/src/scss/home/Home.module.scss new file mode 100644 index 0000000..73545a8 --- /dev/null +++ b/apps/directory/src/scss/home/Home.module.scss @@ -0,0 +1,23 @@ +@use '@/scss/page_base'; + +.official-update { + .info { + display: flex; + flex-direction: column; + align-items: flex-start; + padding-left: 1rem; + word-break: break-word; + margin-top: 1.25rem; + + .content { + font-size: 1.5rem; + max-width: 100%; + } + + .container { + .title { + margin-left: 0.5rem; + } + } + } +} diff --git a/apps/directory/src/scss/operator/Operator.module.scss b/apps/directory/src/scss/operator/Operator.module.scss new file mode 100644 index 0000000..e706c72 --- /dev/null +++ b/apps/directory/src/scss/operator/Operator.module.scss @@ -0,0 +1,142 @@ +@use '@/scss/page_base'; + +.main { + padding: 3rem 0 2rem; + display: flex; + justify-content: space-between; + align-items: flex-start; + + .container { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + width: 100%; + position: relative; + margin-bottom: 2rem; + user-select: none; + + &::before { + content: ''; + display: block; + padding-top: 100%; + } + + .wrapper { + position: absolute; + inset: 0; + } + + @media (width <= 1280px) { + width: 100%; + position: relative; + margin-bottom: 2rem; + + &::before { + content: ''; + display: block; + padding-top: 100%; + } + } + } + + .settings { + margin-right: 1.5rem; + user-select: none; + margin-left: 1rem; + + .title { + font-size: 1.25rem; + border-left: 3px solid currentcolor; + padding-left: 0.75rem; + margin-bottom: 0.8rem; + display: flex; + justify-content: space-between; + cursor: pointer; + + .switch { + bottom: -10px; + position: relative; + } + } + } + + .text { + color: var(--text-color); + line-height: 1.2; + } + + .logo { + position: absolute; + top: 0; + left: 0; + width: 30%; + height: auto; + opacity: 0.3; + } + + .voice { + position: absolute; + left: 0; + bottom: 20%; + 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 SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans', + sans-serif; + + .type { + 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; + } + + .subtitle { + background-color: #000000a6; + color: #fff; + padding: 16px; + font-size: 18px; + box-shadow: 0 6px 12px #00000080; + position: relative; + word-break: break-word; + } + + .triangle { + position: absolute; + bottom: 0; + right: 8px; + width: 0; + height: 0; + border-style: solid; + border-width: 8px; + border-color: white transparent transparent; + } + + &.active { + opacity: 1; + visibility: visible; + } + + @media (width <= 1280px) { + bottom: 0; + } + } + + @media (width <= 1280px) { + flex-direction: column-reverse; + align-items: stretch; + } +} diff --git a/apps/directory/src/scss/root/Root.module.scss b/apps/directory/src/scss/root/Root.module.scss new file mode 100644 index 0000000..88dcc28 --- /dev/null +++ b/apps/directory/src/scss/root/Root.module.scss @@ -0,0 +1,95 @@ +@use '@/scss/main_share'; + +.main { + .header { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + align-items: flex-end; + position: relative; + + @media only screen and (width <= 430px) { + & { + margin: 0 1rem; + } + } + + .title { + font-size: 3rem; + font-weight: 700; + text-transform: uppercase; + line-height: 1.2em; + + .icon { + margin-right: 1.5rem; + display: inline-block; + vertical-align: middle; + + svg { + fill: var(--text-color); + height: 2.5rem; + } + } + + @media (width <= 600px) { + font-size: 2.5rem; + } + + @media (width <= 480px) { + font-size: 2rem; + } + } + + .tab { + flex: auto; + white-space: pre; + user-select: none; + display: flex; + flex-direction: row; + justify-content: flex-end; + overflow: hidden; + z-index: 1; + + .item { + font-size: 1.25rem; + line-height: 3em; + font-weight: 700; + padding: 0 1rem; + text-transform: uppercase; + border-bottom: 0.3rem solid transparent; + display: inline-block; + cursor: pointer; + text-decoration: none; + + .text-wrapper { + color: var(--text-color); + transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + } + + &.active, + &:hover { + color: var(--link-highlight-color); + + .text-wrapper, + .text { + color: currentcolor; + } + } + + &.active { + border-bottom-color: currentcolor; + } + + @media only screen and (width <= 430px) { + line-height: 2em; + } + } + } + + @media only screen and (width <= 430px) { + & { + padding-right: 0; + } + } + } +} diff --git a/apps/directory/src/scss/root/drawer.module.scss b/apps/directory/src/scss/root/drawer.module.scss new file mode 100644 index 0000000..d528605 --- /dev/null +++ b/apps/directory/src/scss/root/drawer.module.scss @@ -0,0 +1,55 @@ +.drawer { + position: fixed; + top: 0; + left: -15rem; + width: 15rem; + height: 100%; + z-index: 3; + pointer-events: none; + transition: left cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + text-align: center; + display: flex; + flex-direction: row; + align-items: flex-start; + user-select: none; + + .links { + padding: 8rem 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + background-color: var(--drawer-background-color); + height: 100%; + width: 15rem; + + .link { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.2rem; + color: var(--text-color); + font-size: 0.8rem; + font-weight: 500; + transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + text-transform: uppercase; + + &:hover, + &.active { + color: var(--link-highlight-color); + } + } + } + + .overlay { + height: 100%; + flex-grow: 1; + z-index: 3; + } + + &.active { + pointer-events: all; + left: 0; + width: 100vw; + } +} diff --git a/apps/directory/src/scss/root/footer.module.scss b/apps/directory/src/scss/root/footer.module.scss new file mode 100644 index 0000000..7e19f82 --- /dev/null +++ b/apps/directory/src/scss/root/footer.module.scss @@ -0,0 +1,42 @@ +.footer { + user-select: none; + + .section { + border-top: 1px solid var(--border-color); + padding: 1rem 0; + display: flex; + justify-content: center; + align-items: center; + flex-wrap: nowrap; + font-family: + 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans', + sans-serif; + } + + .links { + flex-direction: row; + height: 2rem; + overflow-y: hidden; + + .item { + text-align: center; + padding: 0 1rem; + border-left: 2px solid var(--border-color); + height: inherit; + display: flex; + flex-flow: column wrap; + justify-content: center; + align-items: center; + + &:first-of-type { + border-left: none; + } + } + } + + .copyright { + flex-direction: column; + gap: 0.5rem; + font-size: 12px; + } +} diff --git a/apps/directory/src/scss/root/header.module.scss b/apps/directory/src/scss/root/header.module.scss new file mode 100644 index 0000000..4a6ded1 --- /dev/null +++ b/apps/directory/src/scss/root/header.module.scss @@ -0,0 +1,106 @@ +.header { + width: auto; + position: fixed; + left: 0; + top: 0; + right: 0; + padding: 1rem; + z-index: 4; + height: 3rem; + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-start; + pointer-events: none; + + .fast-navigate { + pointer-events: auto; + margin-left: 0.6rem; + height: 2rem; + width: 2rem; + } + + .dropdown { + margin-left: auto; + } + + .extra-area { + display: flex; + flex-direction: row; + align-items: center; + pointer-events: auto; + } +} + +.nav-button { + padding: 0.5rem; + font-size: 2rem; + width: 2rem; + height: 2rem; + cursor: pointer; + display: flex; + justify-content: center; + align-items: flex-start; + flex-direction: column; + z-index: 2; + pointer-events: auto; + + .bar { + width: 2rem; + height: 0.2rem; + background-color: var(--text-color); + transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + + &:nth-child(1) { + transform: translate(0, -200%); + } + + &:nth-child(3) { + transform: translate(0, 200%); + } + } + + &.active { + .bar { + &:nth-child(1) { + transform: translate(0, 100%) rotateZ(45deg) scaleX(0.5) + translate(-50%); + } + + &:nth-child(2) { + transform: rotateZ(-45deg); + } + + &:nth-child(3) { + transform: translate(0, -100%) rotateZ(45deg) scaleX(0.5) + translate(50%); + } + } + } +} + +.spacer { + flex-grow: 1; +} + +.back-arrow { + padding-left: 1rem; + height: 2rem; + width: 2rem; + pointer-events: auto; + + .arrow1 { + height: 1rem; + width: 1rem; + border-left: 0.15rem solid; + border-bottom: 0.15rem solid; + transform: translateY(0.38rem) rotate(45deg); + } + + .arrow2 { + width: 1.2rem; + height: 0.15rem; + background-color: currentcolor; + transform: translate(0.5rem, -0.25rem); + } +} diff --git a/apps/directory/src/state/appbar.js b/apps/directory/src/state/appbar.js new file mode 100644 index 0000000..3f6861e --- /dev/null +++ b/apps/directory/src/state/appbar.js @@ -0,0 +1,11 @@ +import { atom, useAtom } from 'jotai' + +const extraAreaAtom = atom([]) + +export function useAppbar() { + const [extraArea, setExtraArea] = useAtom(extraAreaAtom) + return { + extraArea, + setExtraArea, + } +} diff --git a/apps/directory/src/state/config.js b/apps/directory/src/state/config.js new file mode 100644 index 0000000..55ff1dd --- /dev/null +++ b/apps/directory/src/state/config.js @@ -0,0 +1,31 @@ +import CONFIG from '!/config.json' +import { useCallback } from 'react' +import { atom, useAtom } from 'jotai' + +const officialUpdateAtom = atom({}) +let operators = [] +CONFIG.operators.forEach((item) => { + operators = [...operators, ...item] +}) +const OPERATORS = operators + +export function useConfig() { + const config = CONFIG + const operators = OPERATORS + const [officialUpdate, setOfficialUpdate] = useAtom(officialUpdateAtom) + + const fetchOfficialUpdate = useCallback(async () => { + const res = await fetch( + 'https://raw.githubusercontent.com/Halyul/aklive2d/main/official_update.json' + ) + const data = await res.json().catch((e) => { + console.error(e) + return { + length: 0, + } + }) + setOfficialUpdate(data) + }, [setOfficialUpdate]) + + return { config, operators, officialUpdate, fetchOfficialUpdate } +} diff --git a/apps/directory/src/state/header.js b/apps/directory/src/state/header.js new file mode 100644 index 0000000..195f880 --- /dev/null +++ b/apps/directory/src/state/header.js @@ -0,0 +1,43 @@ +import { useEffect } from 'react' +import { atom, useAtom } from 'jotai' +import { useI18n } from '@/state/language' + +const keyAtom = atom('') +const titleAtom = atom('') +const tabsAtom = atom([]) +const currentTabAtom = atom(null) +const appbarExtraAreaAtom = atom([]) +const headerIconAtom = atom(null) +const fastNaviationAtom = atom([]) + +export function useHeader() { + const [key, setTitle] = useAtom(keyAtom) + const [title, setRealTitle] = useAtom(titleAtom) + const [tabs, setTabs] = useAtom(tabsAtom) + const [currentTab, setCurrentTab] = useAtom(currentTabAtom) + const [appbarExtraArea, setAppbarExtraArea] = useAtom(appbarExtraAreaAtom) + const [headerIcon, setHeaderIcon] = useAtom(headerIconAtom) + const [fastNavigation, setFastNavigation] = useAtom(fastNaviationAtom) + const { i18n } = useI18n() + + useEffect(() => { + const newTitle = i18n(key) + document.title = `${newTitle} - ${import.meta.env.VITE_APP_TITLE}` + setRealTitle(newTitle) + }, [i18n, key, setRealTitle]) + + return { + title, + setTitle, + tabs, + setTabs, + currentTab, + setCurrentTab, + appbarExtraArea, + setAppbarExtraArea, + headerIcon, + setHeaderIcon, + fastNavigation, + setFastNavigation, + } +} diff --git a/apps/directory/src/state/insight.js b/apps/directory/src/state/insight.js new file mode 100644 index 0000000..28b159a --- /dev/null +++ b/apps/directory/src/state/insight.js @@ -0,0 +1,20 @@ +import React from 'react' +import buildConfig from '!/config.json' + +export default (path = null, skipPageView = false) => { + React.useEffect(() => { + if (!skipPageView && import.meta.env.MODE !== 'development') { + try { + window.counterscale = { + q: [ + ['set', 'siteId', buildConfig.insight_id], + ['trackPageview', { path }], + ], + } + window.counterscaleOnDemandTrack() + } catch (err) { + console.warn && console.warn(err.message) + } + } + }, [path, skipPageView]) +} diff --git a/apps/directory/src/state/language.js b/apps/directory/src/state/language.js new file mode 100644 index 0000000..25a4f1b --- /dev/null +++ b/apps/directory/src/state/language.js @@ -0,0 +1,38 @@ +import { atom, useAtom, useAtomValue } from 'jotai' +import { atomWithStorage } from 'jotai/utils' +import i18nObject from '@/i18n' + +const language = i18nObject.available.includes(navigator.language) + ? navigator.language + : 'en-US' + +const textDefaultLang = 'en-US' +const languageAtom = atomWithStorage('language', language) +const alternateLangAtom = atom((get) => { + const language = get(languageAtom) + return language.startsWith('en') ? 'zh-CN' : language +}) + +export function useI18n() { + const language = useAtomValue(languageAtom) + return { + i18n: (key, preferredLanguage = language) => { + if (i18nObject.key[key]) { + return i18nObject.key[key][preferredLanguage] + } + return key + }, + i18nValues: i18nObject, + } +} + +export function useLanguage() { + const [language, setLanguage] = useAtom(languageAtom) + const alternateLang = useAtomValue(alternateLangAtom) + return { + textDefaultLang, + language, + setLanguage, + alternateLang, + } +} diff --git a/apps/directory/stylelint.config.js b/apps/directory/stylelint.config.js new file mode 100644 index 0000000..d3c74a2 --- /dev/null +++ b/apps/directory/stylelint.config.js @@ -0,0 +1,5 @@ +import baseConfig from '@aklive2d/stylelint-config' +/** @type {import('stylelint').Config} */ +export default { + ...baseConfig, +} diff --git a/apps/directory/vite.config.js b/apps/directory/vite.config.js new file mode 100644 index 0000000..1bccace --- /dev/null +++ b/apps/directory/vite.config.js @@ -0,0 +1,45 @@ +import path from 'node:path' +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' +import config from '@aklive2d/config' +import * as showcaseDirs from '@aklive2d/showcase' +import { copyDirectoryData } from '@aklive2d/vite-helpers' + +// https://vite.dev/config/ +export default defineConfig(async () => { + const dataDir = path.resolve(import.meta.dirname, config.dir_name.data) + const publicDir = path.resolve(showcaseDirs.DIST_DIR) + await copyDirectoryData({ dataDir, publicDir }) + return { + envDir: dataDir, + plugins: [react()], + publicDir, + resolve: { + alias: { + '@': path.resolve('./src'), + '!': dataDir, + }, + }, + build: { + emptyOutDir: false, + outDir: publicDir, + rollupOptions: { + output: { + entryFileNames: `${config.directory.assets_dir}/[name]-[hash:8].js`, + chunkFileNames: `${config.directory.assets_dir}/[name]-[hash:8].js`, + assetFileNames: `${config.directory.assets_dir}/[name]-[hash:8].[ext]`, + manualChunks: (id) => { + if (id.includes('node_modules')) { + return 'vendor' // all other package goes here + } else if ( + id.includes('data') && + id.includes('.json') + ) { + return 'assets' + } + }, + }, + }, + }, + } +}) diff --git a/apps/module/.eslintignore b/apps/module/.eslintignore new file mode 100644 index 0000000..4dc6caa --- /dev/null +++ b/apps/module/.eslintignore @@ -0,0 +1 @@ +libs \ No newline at end of file diff --git a/apps/module/.prettierignore b/apps/module/.prettierignore new file mode 100644 index 0000000..4dc6caa --- /dev/null +++ b/apps/module/.prettierignore @@ -0,0 +1 @@ +libs \ No newline at end of file diff --git a/apps/module/index.js b/apps/module/index.js new file mode 100644 index 0000000..8ddd7d9 --- /dev/null +++ b/apps/module/index.js @@ -0,0 +1,4 @@ +import './libs/spine-player.css' +import spine from './libs/spine-player' + +export { spine } \ No newline at end of file diff --git a/showcase/src/libs/spine-player.css b/apps/module/libs/spine-player.css similarity index 100% rename from showcase/src/libs/spine-player.css rename to apps/module/libs/spine-player.css diff --git a/showcase/src/libs/spine-player.js b/apps/module/libs/spine-player.js similarity index 99% rename from showcase/src/libs/spine-player.js rename to apps/module/libs/spine-player.js index af1d409..33b7379 100644 --- a/showcase/src/libs/spine-player.js +++ b/apps/module/libs/spine-player.js @@ -2188,42 +2188,64 @@ var spine; this.pathPrefix = pathPrefix; } AssetManager.prototype.downloadText = function (url, success, error) { - var request = new XMLHttpRequest(); - request.overrideMimeType("text/html"); if (this.rawDataUris[url]) url = this.rawDataUris[url]; - request.open("GET", url, true); - request.onload = function () { - if (request.status == 200) { - success(request.responseText); + fetch(url).then(function (response) { + if (!response.ok) { + error(response.status, response.statusText); } - else { - error(request.status, request.responseText); - } - }; - request.onerror = function () { - error(request.status, request.responseText); - }; - request.send(); + return response.text(); + }).then(function (text) { + success(text); + }); + + // var request = new XMLHttpRequest(); + // request.overrideMimeType("text/html"); + + // request.open("GET", url, true); + // request.onload = function () { + // if (request.status == 200) { + // success(request.responseText); + // } + // else { + // error(request.status, request.responseText); + // } + // }; + // request.onerror = function () { + // error(request.status, request.responseText); + // }; + // request.send(); }; AssetManager.prototype.downloadBinary = function (url, success, error) { - var request = new XMLHttpRequest(); if (this.rawDataUris[url]) url = this.rawDataUris[url]; - request.open("GET", url, true); - request.responseType = "arraybuffer"; - request.onload = function () { - if (request.status == 200) { - success(new Uint8Array(request.response)); + fetch(url).then(function (response) { + if (!response.ok) { + error(response.status, response.statusText); } - else { - error(request.status, request.responseText); - } - }; - request.onerror = function () { - error(request.status, request.responseText); - }; - request.send(); + return response.arrayBuffer(); + }).then(function (arrayBuffer) { + success(new Uint8Array(arrayBuffer)); + }); + + + // var request = new XMLHttpRequest(); + // if (this.rawDataUris[url]) + // url = this.rawDataUris[url]; + // request.open("GET", url, true); + // request.responseType = "arraybuffer"; + // request.onload = function () { + // if (request.status == 200) { + // success(new Uint8Array(request.response)); + // } + // else { + // error(request.status, request.responseText); + // } + // }; + // request.onerror = function () { + // error(request.status, request.responseText); + // }; + // request.send(); }; AssetManager.prototype.setRawDataURI = function (path, data) { this.rawDataUris[this.pathPrefix + path] = data; diff --git a/apps/module/package.json b/apps/module/package.json new file mode 100644 index 0000000..34d6c3b --- /dev/null +++ b/apps/module/package.json @@ -0,0 +1,7 @@ +{ + "name": "@aklive2d/module", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "index.js" +} diff --git a/apps/showcase/.gitignore b/apps/showcase/.gitignore new file mode 100644 index 0000000..028fbac --- /dev/null +++ b/apps/showcase/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +data \ No newline at end of file diff --git a/apps/showcase/.prettierignore b/apps/showcase/.prettierignore new file mode 100644 index 0000000..cb88359 --- /dev/null +++ b/apps/showcase/.prettierignore @@ -0,0 +1,3 @@ +dist +data +auto_update \ No newline at end of file diff --git a/apps/showcase/.stylelintignore b/apps/showcase/.stylelintignore new file mode 100644 index 0000000..0f8b224 --- /dev/null +++ b/apps/showcase/.stylelintignore @@ -0,0 +1 @@ +spine-player.css \ No newline at end of file diff --git a/apps/showcase/eslint.config.js b/apps/showcase/eslint.config.js new file mode 100644 index 0000000..6713eef --- /dev/null +++ b/apps/showcase/eslint.config.js @@ -0,0 +1,3 @@ +import baseConfig from '@aklive2d/eslint-config' +/** @type {import('eslint').Config} */ +export default [...baseConfig, { ignores: ['src/libs/*'] }] diff --git a/apps/showcase/index.html b/apps/showcase/index.html new file mode 100644 index 0000000..7ff69fd --- /dev/null +++ b/apps/showcase/index.html @@ -0,0 +1,22 @@ + + + + + + + + %VITE_APP_TITLE% + + + +
+ + + diff --git a/apps/showcase/index.js b/apps/showcase/index.js new file mode 100644 index 0000000..ada7157 --- /dev/null +++ b/apps/showcase/index.js @@ -0,0 +1,16 @@ +import path from 'node:path' +import config from '@aklive2d/config' + +export const DATA_DIR = path.resolve(import.meta.dirname, config.dir_name.data) +export const PUBLIC_DIR = path.resolve(DATA_DIR, config.dir_name.public) +export const PUBLIC_ASSETS_DIR = path.resolve( + PUBLIC_DIR, + config.dir_name.assets +) +export const OUT_DIR = path.resolve(import.meta.dirname, config.dir_name.dist) +export const DIST_DIR = path.resolve( + import.meta.dirname, + '..', + '..', + config.dir_name.dist +) diff --git a/apps/showcase/jsconfig.json b/apps/showcase/jsconfig.json new file mode 100644 index 0000000..30e9ad8 --- /dev/null +++ b/apps/showcase/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "!/*": ["src/*"] + } + } +} diff --git a/apps/showcase/package.json b/apps/showcase/package.json new file mode 100644 index 0000000..74fd199 --- /dev/null +++ b/apps/showcase/package.json @@ -0,0 +1,26 @@ +{ + "name": "@aklive2d/showcase", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "index.js", + "scripts": { + "dev:showcase": "vite --clearScreen false", + "build": "mode=build node runner.js", + "preview:showcase": "vite preview", + "lint": "eslint \"src/**/*.js\" && stylelint \"**/*.css\" && prettier --check ." + }, + "devDependencies": { + "vite": "^6.1.0", + "@aklive2d/eslint-config": "workspace:*", + "@aklive2d/postcss-config": "workspace:*", + "@aklive2d/stylelint-config": "workspace:*", + "@aklive2d/config": "workspace:*", + "@aklive2d/libs": "workspace:*", + "@aklive2d/assets": "workspace:*", + "@aklive2d/operator": "workspace:*", + "@aklive2d/vite-helpers": "workspace:*", + "@aklive2d/module": "workspace:*", + "@aklive2d/prettier-config": "workspace:*" + } +} diff --git a/apps/showcase/prettier.config.js b/apps/showcase/prettier.config.js new file mode 100644 index 0000000..a58af8e --- /dev/null +++ b/apps/showcase/prettier.config.js @@ -0,0 +1,11 @@ +import baseConfig from '@aklive2d/prettier-config' + +/** + * @type {import("prettier").Config} + */ +const config = { + ...baseConfig, + semi: false, +} + +export default config diff --git a/apps/showcase/runner.js b/apps/showcase/runner.js new file mode 100644 index 0000000..f050467 --- /dev/null +++ b/apps/showcase/runner.js @@ -0,0 +1,38 @@ +import path from 'node:path' +import { build as viteBuild } from 'vite' +import operators from '@aklive2d/operator' +import { envParser, file } from '@aklive2d/libs' +import { copyShowcaseData, copyProjectJSON } from '@aklive2d/vite-helpers' +import * as dirs from './index.js' + +const build = async (namesToBuild) => { + const names = !namesToBuild.length ? Object.keys(operators) : namesToBuild + console.log('Generating assets for', names.length, 'operators') + for (const name of names) { + copyShowcaseData(name, { + dataDir: dirs.DATA_DIR, + publicAssetsDir: dirs.PUBLIC_ASSETS_DIR, + }) + await viteBuild() + const releaseDir = path.join(dirs.DIST_DIR, name) + file.mv(dirs.OUT_DIR, releaseDir) + file.rm(dirs.DATA_DIR) + copyProjectJSON(name, { + releaseDir, + }) + } +} + +async function main() { + const { name } = envParser.parse({ + name: { + type: 'string', + short: 'n', + multiple: true, + default: [], + }, + }) + await build(name) +} + +main() diff --git a/showcase/src/components/aklive2d.css b/apps/showcase/src/components/aklive2d.css similarity index 98% rename from showcase/src/components/aklive2d.css rename to apps/showcase/src/components/aklive2d.css index 16623d2..b1da59d 100644 --- a/showcase/src/components/aklive2d.css +++ b/apps/showcase/src/components/aklive2d.css @@ -5,4 +5,4 @@ background-color: white; user-select: auto; z-index: 999; -} \ No newline at end of file +} diff --git a/apps/showcase/src/components/aklive2d.js b/apps/showcase/src/components/aklive2d.js new file mode 100644 index 0000000..e01bb9c --- /dev/null +++ b/apps/showcase/src/components/aklive2d.js @@ -0,0 +1,257 @@ +import Voice from '@/components/voice' +import Fallback from '@/components/fallback' +import Music from '@/components/music' +import Player from '@/components/player' +import Background from '@/components/background' +import Logo from '@/components/logo' +import Insight from '@/components/insight' +import Events from '@/components/events' +import { + isWebGLSupported, + insertHTMLChild, + addEventListeners, + updateElementPosition, +} from '@/components/helper' +import '@/components/aklive2d.css' + +export default class AKLive2D { + #el = document.createElement('div') + #appEl + #queries = new URLSearchParams(window.location.search) + #voice + #music + #player + #background + #logo + #configQ = [] + #isInited = false + #isSelfInited = false + #isAllInited = false + #insight = new Insight() + + constructor(appEl) { + console.log( + 'All resources are extracted from Arknights. Github: https://gura.ch/aklive2d-gh' + ) + + window.addEventListener('contextmenu', (e) => e.preventDefault()) + document.addEventListener('gesturestart', (e) => e.preventDefault()) + + this.#appEl = appEl + this.#logo = new Logo(this.#appEl) + this.#background = new Background(this.#appEl) + this.#voice = new Voice(this.#appEl) + this.#music = new Music(this.#appEl) + if (isWebGLSupported()) { + this.#player = new Player(this.#appEl) + } else { + new Fallback(this.#appEl) + } + addEventListeners([ + { + event: Events.Player.Ready.name, + handler: () => this.#selfInited(), + }, + { + event: Events.RegisterConfig.name, + handler: (e) => this.#registerConfig(e), + }, + ]) + + Promise.all( + [ + this.#logo, + this.#background, + this.#voice, + this.#music, + this.#player, + ].map(async (e) => e && (await e.init())) + ).then(() => this.#allInited()) + } + + #registerConfig(e) { + if (!this.#isInited) { + this.#configQ.push(e.detail) + } else { + this.#applyConfig(e.detail) + } + } + + #applyConfig(config = null) { + if (config) { + let targetObj + const target = config.target + switch (target) { + case 'player': + targetObj = this.#player + break + case 'background': + targetObj = this.#background + break + case 'logo': + targetObj = this.#logo + break + case 'music': + targetObj = this.#music + break + case 'voice': + targetObj = this.#voice + break + default: + return + } + targetObj.applyConfig(config.key, config.value) + } else { + this.#configQ.map((e) => this.#applyConfig(e)) + } + return + } + + get voice() { + return this.#voice + } + + get music() { + return this.#music + } + + get player() { + return this.#player + } + + get background() { + return this.#background + } + + get logo() { + return this.#logo + } + + get events() { + return Events + } + + get config() { + return { + player: this.#player.config, + background: this.#background.config, + logo: this.#logo.config, + music: this.#music.config, + voice: this.#voice.config, + } + } + + get configStr() { + return JSON.stringify(this.config, null) + } + + open() { + this.#el.hidden = false + } + + close() { + this.#el.hidden = true + } + + reset() { + this.#player.reset() + this.#background.reset() + this.#logo.reset() + this.#voice.reset() + this.#music.reset() + } + + #allInited() { + this.#isAllInited = true + if (this.#isSelfInited) { + this.#success() + } + } + + #selfInited() { + this.#isSelfInited = true + if (this.#isAllInited) { + this.#success() + } + } + + #success() { + this.#isInited = true + this.#el.id = 'settings-box' + this.#el.hidden = true + this.#el.innerHTML = ` +
+ ${this.#logo.HTML} + ${this.#background.HTML} + ${this.#player.HTML} + ${this.#music.HTML} + ${this.#voice.HTML} +
+ + + +
+
+ ` + insertHTMLChild(this.#appEl, this.#el) + addEventListeners([ + { + id: 'settings-reset', + event: 'click', + handler: () => this.reset(), + }, + { + id: 'settings-close', + event: 'click', + handler: () => this.close(), + }, + { + id: 'settings-to-directory', + event: 'click', + handler: () => { + window.location.href = '/' + }, + }, + ...this.#logo.listeners, + ...this.#background.listeners, + ...this.#player.listeners, + ...this.#voice.listeners, + ...this.#music.listeners, + ...this.#insight.listeners, + ]) + + this.#music.link(this.#background) + this.#background.link(this.#music) + this.#voice.link(this.#player) + this.#player.success() + this.#voice.success() + this.#music.success() + this.#insight.success() + + this.#applyConfig() + + if ( + this.#queries.has('aklive2d') || + import.meta.env.MODE === 'development' + ) { + this.open() + } + this.#registerBackCompatibilityFns() + } + + #registerBackCompatibilityFns() { + const _this = this + window.voice = _this.#voice + window.music = _this.#music + window.settings = { + elementPosition: updateElementPosition, + open: _this.open, + close: _this.close, + reset: _this.reset, + ..._this.#player.backCompatibilityFns, + ..._this.#logo.backCompatibilityFns, + ..._this.#music.backCompatibilityFns, + ..._this.#background.backCompatibilityFns, + } + } +} diff --git a/apps/showcase/src/components/background.css b/apps/showcase/src/components/background.css new file mode 100644 index 0000000..13d141e --- /dev/null +++ b/apps/showcase/src/components/background.css @@ -0,0 +1,23 @@ +#background-box { + position: absolute; + top: 0; + bottom: 0; + width: 100%; + height: 100%; + overflow: hidden; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + z-index: -2; +} + +#background-box #video-src { + min-width: 100%; + min-height: 100%; + width: auto; + height: auto; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} diff --git a/apps/showcase/src/components/background.js b/apps/showcase/src/components/background.js new file mode 100644 index 0000000..d938bec --- /dev/null +++ b/apps/showcase/src/components/background.js @@ -0,0 +1,298 @@ +import { + readFile, + updateHTMLOptions, + showRelatedHTML, + syncHTMLValue, + insertHTMLChild, +} from '@/components/helper' +import '@/components/background.css' +import buildConfig from '!/config.json' + +export default class Background { + #el = document.createElement('div') + #parentEl + #videoEl + #default = { + location: `${import.meta.env.BASE_URL}assets/${buildConfig.background_folder}/`, + image: buildConfig.default_background, + } + #config = { + video: { + name: null, + volume: 100, + }, + useVideo: false, + name: null, + } + #musicObj + + constructor(el) { + this.#parentEl = el + this.#el.id = 'background-box' + this.image = this.#default.location + this.#default.image + this.#el.innerHTML = ` +