From 255d6333107c6ed8e53e4c64a73738f04959e186 Mon Sep 17 00:00:00 2001 From: daydreamer-json Date: Tue, 24 Mar 2026 05:38:45 +0900 Subject: [PATCH] feat(pages): add pages-v2 react variant --- pages-v2/.editorconfig | 8 + pages-v2/.gitignore | 24 + pages-v2/README_vite.md | 73 +++ pages-v2/biome.json | 38 ++ pages-v2/bun.lock | 476 ++++++++++++++++++ pages-v2/eslint.config.js | 26 + pages-v2/index.html | 25 + pages-v2/package.json | 38 ++ pages-v2/public/favicon.svg | 1 + pages-v2/public/icons.svg | 24 + pages-v2/src/App.tsx | 271 ++++++++++ pages-v2/src/assets/css/about.css | 8 + pages-v2/src/assets/css/essentials.css | 68 +++ pages-v2/src/assets/css/index.css | 58 +++ .../harmonyos_sans_condensed_black.ttf | Bin .../harmonyos_sans_condensed_black.woff2 | Bin .../harmonyos_sans_condensed_bold.ttf | Bin .../harmonyos_sans_condensed_bold.woff2 | Bin .../harmonyos_sans_condensed_light.ttf | Bin .../harmonyos_sans_condensed_light.woff2 | Bin .../harmonyos_sans_condensed_medium.ttf | Bin .../harmonyos_sans_condensed_medium.woff2 | Bin .../harmonyos_sans_condensed_regular.ttf | Bin .../harmonyos_sans_condensed_regular.woff2 | Bin .../harmonyos_sans_condensed_semibold.ttf | Bin .../harmonyos_sans_condensed_semibold.woff2 | Bin .../harmonyos_sans_condensed_thin.ttf | Bin .../harmonyos_sans_condensed_thin.woff2 | Bin .../harmonyos_sans_condensed_italic_black.ttf | Bin ...armonyos_sans_condensed_italic_black.woff2 | Bin .../harmonyos_sans_condensed_italic_bold.ttf | Bin ...harmonyos_sans_condensed_italic_bold.woff2 | Bin .../harmonyos_sans_condensed_italic_light.ttf | Bin ...armonyos_sans_condensed_italic_light.woff2 | Bin ...harmonyos_sans_condensed_italic_medium.ttf | Bin ...rmonyos_sans_condensed_italic_medium.woff2 | Bin ...armonyos_sans_condensed_italic_regular.ttf | Bin ...monyos_sans_condensed_italic_regular.woff2 | Bin ...rmonyos_sans_condensed_italic_semibold.ttf | Bin ...onyos_sans_condensed_italic_semibold.woff2 | Bin .../harmonyos_sans_condensed_italic_thin.ttf | Bin ...harmonyos_sans_condensed_italic_thin.woff2 | Bin .../fonts/harmonyos_sans/harmonyos_sans.css | 0 .../italic/harmonyos_sans_italic_black.ttf | Bin .../italic/harmonyos_sans_italic_black.woff2 | Bin .../italic/harmonyos_sans_italic_bold.ttf | Bin .../italic/harmonyos_sans_italic_bold.woff2 | Bin .../italic/harmonyos_sans_italic_light.ttf | Bin .../italic/harmonyos_sans_italic_light.woff2 | Bin .../italic/harmonyos_sans_italic_medium.ttf | Bin .../italic/harmonyos_sans_italic_medium.woff2 | Bin .../italic/harmonyos_sans_italic_regular.ttf | Bin .../harmonyos_sans_italic_regular.woff2 | Bin .../italic/harmonyos_sans_italic_semibold.ttf | Bin .../harmonyos_sans_italic_semibold.woff2 | Bin .../italic/harmonyos_sans_italic_thin.ttf | Bin .../italic/harmonyos_sans_italic_thin.woff2 | Bin .../normal/harmonyos_sans_black.ttf | Bin .../normal/harmonyos_sans_black.woff2 | Bin .../normal/harmonyos_sans_bold.ttf | Bin .../normal/harmonyos_sans_bold.woff2 | Bin .../normal/harmonyos_sans_light.ttf | Bin .../normal/harmonyos_sans_light.woff2 | Bin .../normal/harmonyos_sans_medium.ttf | Bin .../normal/harmonyos_sans_medium.woff2 | Bin .../normal/harmonyos_sans_regular.ttf | Bin .../normal/harmonyos_sans_regular.woff2 | Bin .../normal/harmonyos_sans_semibold.ttf | Bin .../normal/harmonyos_sans_semibold.woff2 | Bin .../normal/harmonyos_sans_thin.ttf | Bin .../normal/harmonyos_sans_thin.woff2 | Bin .../src/assets/fonts/iosevka/Iosevka.css | 0 .../fonts/iosevka/woff2/Iosevka-Bold.woff2 | Bin .../iosevka/woff2/Iosevka-BoldItalic.woff2 | Bin .../iosevka/woff2/Iosevka-BoldOblique.woff2 | Bin .../iosevka/woff2/Iosevka-Extended.woff2 | Bin .../iosevka/woff2/Iosevka-ExtendedBold.woff2 | Bin .../woff2/Iosevka-ExtendedBoldItalic.woff2 | Bin .../woff2/Iosevka-ExtendedBoldOblique.woff2 | Bin .../woff2/Iosevka-ExtendedExtraBold.woff2 | Bin .../Iosevka-ExtendedExtraBoldItalic.woff2 | Bin .../Iosevka-ExtendedExtraBoldOblique.woff2 | Bin .../woff2/Iosevka-ExtendedExtraLight.woff2 | Bin .../Iosevka-ExtendedExtraLightItalic.woff2 | Bin .../Iosevka-ExtendedExtraLightOblique.woff2 | Bin .../iosevka/woff2/Iosevka-ExtendedHeavy.woff2 | Bin .../woff2/Iosevka-ExtendedHeavyItalic.woff2 | Bin .../woff2/Iosevka-ExtendedHeavyOblique.woff2 | Bin .../woff2/Iosevka-ExtendedItalic.woff2 | Bin .../iosevka/woff2/Iosevka-ExtendedLight.woff2 | Bin .../woff2/Iosevka-ExtendedLightItalic.woff2 | Bin .../woff2/Iosevka-ExtendedLightOblique.woff2 | Bin .../woff2/Iosevka-ExtendedMedium.woff2 | Bin .../woff2/Iosevka-ExtendedMediumItalic.woff2 | Bin .../woff2/Iosevka-ExtendedMediumOblique.woff2 | Bin .../woff2/Iosevka-ExtendedOblique.woff2 | Bin .../woff2/Iosevka-ExtendedSemiBold.woff2 | Bin .../Iosevka-ExtendedSemiBoldItalic.woff2 | Bin .../Iosevka-ExtendedSemiBoldOblique.woff2 | Bin .../iosevka/woff2/Iosevka-ExtendedThin.woff2 | Bin .../woff2/Iosevka-ExtendedThinItalic.woff2 | Bin .../woff2/Iosevka-ExtendedThinOblique.woff2 | Bin .../iosevka/woff2/Iosevka-ExtraBold.woff2 | Bin .../woff2/Iosevka-ExtraBoldItalic.woff2 | Bin .../woff2/Iosevka-ExtraBoldOblique.woff2 | Bin .../iosevka/woff2/Iosevka-ExtraLight.woff2 | Bin .../woff2/Iosevka-ExtraLightItalic.woff2 | Bin .../woff2/Iosevka-ExtraLightOblique.woff2 | Bin .../fonts/iosevka/woff2/Iosevka-Heavy.woff2 | Bin .../iosevka/woff2/Iosevka-HeavyItalic.woff2 | Bin .../iosevka/woff2/Iosevka-HeavyOblique.woff2 | Bin .../fonts/iosevka/woff2/Iosevka-Italic.woff2 | Bin .../fonts/iosevka/woff2/Iosevka-Light.woff2 | Bin .../iosevka/woff2/Iosevka-LightItalic.woff2 | Bin .../iosevka/woff2/Iosevka-LightOblique.woff2 | Bin .../fonts/iosevka/woff2/Iosevka-Medium.woff2 | Bin .../iosevka/woff2/Iosevka-MediumItalic.woff2 | Bin .../iosevka/woff2/Iosevka-MediumOblique.woff2 | Bin .../fonts/iosevka/woff2/Iosevka-Oblique.woff2 | Bin .../fonts/iosevka/woff2/Iosevka-Regular.woff2 | Bin .../iosevka/woff2/Iosevka-SemiBold.woff2 | Bin .../woff2/Iosevka-SemiBoldItalic.woff2 | Bin .../woff2/Iosevka-SemiBoldOblique.woff2 | Bin .../fonts/iosevka/woff2/Iosevka-Thin.woff2 | Bin .../iosevka/woff2/Iosevka-ThinItalic.woff2 | Bin .../iosevka/woff2/Iosevka-ThinOblique.woff2 | Bin .../novecento_sans_condensed_bold.otf | Bin .../novecento_sans_condensed_bold.woff2 | Bin .../novecento_sans_condensed_book.otf | Bin .../novecento_sans_condensed_book.woff2 | Bin .../novecento_sans_condensed_demibold.otf | Bin .../novecento_sans_condensed_demibold.woff2 | Bin .../novecento_sans_condensed_light.otf | Bin .../novecento_sans_condensed_light.woff2 | Bin .../novecento_sans_condensed_medium.otf | Bin .../novecento_sans_condensed_medium.woff2 | Bin .../novecento_sans_condensed_normal.otf | Bin .../novecento_sans_condensed_normal.woff2 | Bin .../novecento_sans_condensed_ultrabold.otf | Bin .../novecento_sans_condensed_ultrabold.woff2 | Bin .../novecento_sans_condensed_ultralight.otf | Bin .../novecento_sans_condensed_ultralight.woff2 | Bin .../narrow/novecento_sans_narrow_bold.otf | Bin .../narrow/novecento_sans_narrow_bold.woff2 | Bin .../narrow/novecento_sans_narrow_book.otf | Bin .../narrow/novecento_sans_narrow_book.woff2 | Bin .../narrow/novecento_sans_narrow_demibold.otf | Bin .../novecento_sans_narrow_demibold.woff2 | Bin .../narrow/novecento_sans_narrow_light.otf | Bin .../narrow/novecento_sans_narrow_light.woff2 | Bin .../narrow/novecento_sans_narrow_medium.otf | Bin .../narrow/novecento_sans_narrow_medium.woff2 | Bin .../narrow/novecento_sans_narrow_normal.otf | Bin .../narrow/novecento_sans_narrow_normal.woff2 | Bin .../novecento_sans_narrow_ultrabold.otf | Bin .../novecento_sans_narrow_ultrabold.woff2 | Bin .../novecento_sans_narrow_ultralight.otf | Bin .../novecento_sans_narrow_ultralight.woff2 | Bin .../normal/novecento_sans_bold.otf | Bin .../normal/novecento_sans_bold.woff2 | Bin .../normal/novecento_sans_book.otf | Bin .../normal/novecento_sans_book.woff2 | Bin .../normal/novecento_sans_demibold.otf | Bin .../normal/novecento_sans_demibold.woff2 | Bin .../normal/novecento_sans_light.otf | Bin .../normal/novecento_sans_light.woff2 | Bin .../normal/novecento_sans_medium.otf | Bin .../normal/novecento_sans_medium.woff2 | Bin .../normal/novecento_sans_normal.otf | Bin .../normal/novecento_sans_normal.woff2 | Bin .../normal/novecento_sans_ultrabold.otf | Bin .../normal/novecento_sans_ultrabold.woff2 | Bin .../normal/novecento_sans_ultralight.otf | Bin .../normal/novecento_sans_ultralight.woff2 | Bin .../fonts/novecento_sans/novecento_sans.css | 0 .../novecento_sans_normal_number_only.css | 0 .../novecento_sans_wide_number_only.css | 0 .../wide/novecento_sans_wide_bold.otf | Bin .../wide/novecento_sans_wide_bold.woff2 | Bin .../wide/novecento_sans_wide_book.otf | Bin .../wide/novecento_sans_wide_book.woff2 | Bin .../wide/novecento_sans_wide_demibold.otf | Bin .../wide/novecento_sans_wide_demibold.woff2 | Bin .../wide/novecento_sans_wide_light.otf | Bin .../wide/novecento_sans_wide_light.woff2 | Bin .../wide/novecento_sans_wide_medium.otf | Bin .../wide/novecento_sans_wide_medium.woff2 | Bin .../wide/novecento_sans_wide_normal.otf | Bin .../wide/novecento_sans_wide_normal.woff2 | Bin .../wide/novecento_sans_wide_ultrabold.otf | Bin .../wide/novecento_sans_wide_ultrabold.woff2 | Bin .../wide/novecento_sans_wide_ultralight.otf | Bin .../wide/novecento_sans_wide_ultralight.woff2 | Bin .../src/assets/img/endmin_thumbsup.png | Bin .../src/assets/img/endmin_thumbsup.webp | Bin .../src/assets/img/endmin_thumbsup_640px.webp | Bin pages-v2/src/components/tabs/AboutTab.tsx | 45 ++ .../src/components/tabs/GamePackagesTab.tsx | 187 +++++++ pages-v2/src/components/tabs/LauncherTab.tsx | 227 +++++++++ pages-v2/src/components/tabs/OverviewTab.tsx | 307 +++++++++++ pages-v2/src/components/tabs/PatchesTab.tsx | 209 ++++++++ pages-v2/src/components/tabs/ResourcesTab.tsx | 284 +++++++++++ pages-v2/src/components/tabs/WebTab.tsx | 64 +++ .../tabs/web/AnnouncementSection.tsx | 132 +++++ .../src/components/tabs/web/BannerSection.tsx | 132 +++++ .../tabs/web/MainBgImageSection.tsx | 131 +++++ .../components/tabs/web/SidebarSection.tsx | 150 ++++++ .../components/tabs/web/SingleEntSection.tsx | 165 ++++++ pages-v2/src/legacy/api.ts | 51 ++ pages-v2/src/legacy/essentials.ts | 16 + pages-v2/src/legacy/gamePackages.ts | 78 +++ pages-v2/src/legacy/index.ts | 57 +++ pages-v2/src/legacy/launchers.ts | 137 +++++ pages-v2/src/legacy/overview.ts | 255 ++++++++++ pages-v2/src/legacy/patches.ts | 89 ++++ pages-v2/src/legacy/renderers/gamePackages.ts | 78 +++ pages-v2/src/legacy/renderers/launchers.ts | 137 +++++ pages-v2/src/legacy/renderers/overview.ts | 255 ++++++++++ pages-v2/src/legacy/renderers/patches.ts | 89 ++++ pages-v2/src/legacy/renderers/resources.ts | 119 +++++ pages-v2/src/legacy/renderers/web.ts | 157 ++++++ pages-v2/src/legacy/renderers/webPretty.ts | 14 + .../renderers/webPretty/announcement.ts | 179 +++++++ .../src/legacy/renderers/webPretty/banner.ts | 175 +++++++ .../legacy/renderers/webPretty/mainBgImage.ts | 174 +++++++ .../src/legacy/renderers/webPretty/sidebar.ts | 195 +++++++ .../legacy/renderers/webPretty/singleEnt.ts | 205 ++++++++ pages-v2/src/legacy/resources.ts | 119 +++++ pages-v2/src/legacy/types.ts | 72 +++ pages-v2/src/legacy/utils/constants.ts | 44 ++ pages-v2/src/legacy/utils/logger.ts | 12 + pages-v2/src/legacy/utils/math.ts | 163 ++++++ pages-v2/src/legacy/utils/ui.ts | 16 + pages-v2/src/legacy/web.ts | 157 ++++++ pages-v2/src/legacy/webPretty.ts | 14 + pages-v2/src/legacy/webPretty/announcement.ts | 179 +++++++ pages-v2/src/legacy/webPretty/banner.ts | 175 +++++++ pages-v2/src/legacy/webPretty/mainBgImage.ts | 174 +++++++ pages-v2/src/legacy/webPretty/sidebar.ts | 195 +++++++ pages-v2/src/legacy/webPretty/singleEnt.ts | 205 ++++++++ pages-v2/src/main.tsx | 14 + pages-v2/src/types.ts | 72 +++ pages-v2/src/utils/api.ts | 51 ++ pages-v2/src/utils/constants.ts | 44 ++ pages-v2/src/utils/logger.ts | 12 + pages-v2/src/utils/math.ts | 162 ++++++ pages-v2/src/utils/ui.ts | 16 + pages-v2/tsconfig.app.json | 29 ++ pages-v2/tsconfig.json | 7 + pages-v2/tsconfig.node.json | 26 + pages-v2/vite.config.ts | 7 + 251 files changed, 7596 insertions(+) create mode 100644 pages-v2/.editorconfig create mode 100644 pages-v2/.gitignore create mode 100644 pages-v2/README_vite.md create mode 100644 pages-v2/biome.json create mode 100644 pages-v2/bun.lock create mode 100644 pages-v2/eslint.config.js create mode 100644 pages-v2/index.html create mode 100644 pages-v2/package.json create mode 100644 pages-v2/public/favicon.svg create mode 100644 pages-v2/public/icons.svg create mode 100644 pages-v2/src/App.tsx create mode 100644 pages-v2/src/assets/css/about.css create mode 100644 pages-v2/src/assets/css/essentials.css create mode 100644 pages-v2/src/assets/css/index.css rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_black.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_black.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_bold.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_bold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_light.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_light.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_medium.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_medium.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_regular.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_regular.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_semibold.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_semibold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_thin.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_thin.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_black.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_black.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_bold.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_bold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_light.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_light.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_medium.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_medium.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_regular.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_regular.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_semibold.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_semibold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_thin.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_thin.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/harmonyos_sans.css (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_black.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_black.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_bold.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_bold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_light.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_light.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_medium.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_medium.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_regular.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_regular.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_semibold.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_semibold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_thin.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_thin.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_black.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_black.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_bold.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_bold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_light.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_light.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_medium.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_medium.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_regular.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_regular.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_semibold.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_semibold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_thin.ttf (100%) rename {pages => pages-v2}/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_thin.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/Iosevka.css (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-Bold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-BoldItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-BoldOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-Extended.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBoldItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBoldOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBoldItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBoldOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLight.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLightItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLightOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavy.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavyItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavyOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLight.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLightItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLightOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMedium.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMediumItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMediumOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBoldItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBoldOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThin.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThinItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThinOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBoldItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBoldOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLight.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLightItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLightOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-Heavy.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-HeavyItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-HeavyOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-Italic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-Light.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-LightItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-LightOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-Medium.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-MediumItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-MediumOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-Oblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-Regular.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-SemiBold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-SemiBoldItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-SemiBoldOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-Thin.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ThinItalic.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/iosevka/woff2/Iosevka-ThinOblique.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_bold.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_bold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_book.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_book.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_demibold.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_demibold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_light.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_light.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_medium.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_medium.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_normal.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_normal.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultrabold.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultrabold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultralight.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultralight.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_bold.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_bold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_book.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_book.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_demibold.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_demibold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_light.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_light.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_medium.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_medium.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_normal.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_normal.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultrabold.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultrabold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultralight.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultralight.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_bold.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_bold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_book.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_book.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_demibold.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_demibold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_light.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_light.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_medium.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_medium.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_normal.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_normal.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_ultrabold.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_ultrabold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_ultralight.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/normal/novecento_sans_ultralight.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/novecento_sans.css (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/novecento_sans_normal_number_only.css (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/novecento_sans_wide_number_only.css (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_bold.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_bold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_book.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_book.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_demibold.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_demibold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_light.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_light.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_medium.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_medium.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_normal.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_normal.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultrabold.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultrabold.woff2 (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultralight.otf (100%) rename {pages => pages-v2}/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultralight.woff2 (100%) rename {pages => pages-v2}/src/assets/img/endmin_thumbsup.png (100%) rename {pages => pages-v2}/src/assets/img/endmin_thumbsup.webp (100%) rename {pages => pages-v2}/src/assets/img/endmin_thumbsup_640px.webp (100%) create mode 100644 pages-v2/src/components/tabs/AboutTab.tsx create mode 100644 pages-v2/src/components/tabs/GamePackagesTab.tsx create mode 100644 pages-v2/src/components/tabs/LauncherTab.tsx create mode 100644 pages-v2/src/components/tabs/OverviewTab.tsx create mode 100644 pages-v2/src/components/tabs/PatchesTab.tsx create mode 100644 pages-v2/src/components/tabs/ResourcesTab.tsx create mode 100644 pages-v2/src/components/tabs/WebTab.tsx create mode 100644 pages-v2/src/components/tabs/web/AnnouncementSection.tsx create mode 100644 pages-v2/src/components/tabs/web/BannerSection.tsx create mode 100644 pages-v2/src/components/tabs/web/MainBgImageSection.tsx create mode 100644 pages-v2/src/components/tabs/web/SidebarSection.tsx create mode 100644 pages-v2/src/components/tabs/web/SingleEntSection.tsx create mode 100644 pages-v2/src/legacy/api.ts create mode 100644 pages-v2/src/legacy/essentials.ts create mode 100644 pages-v2/src/legacy/gamePackages.ts create mode 100644 pages-v2/src/legacy/index.ts create mode 100644 pages-v2/src/legacy/launchers.ts create mode 100644 pages-v2/src/legacy/overview.ts create mode 100644 pages-v2/src/legacy/patches.ts create mode 100644 pages-v2/src/legacy/renderers/gamePackages.ts create mode 100644 pages-v2/src/legacy/renderers/launchers.ts create mode 100644 pages-v2/src/legacy/renderers/overview.ts create mode 100644 pages-v2/src/legacy/renderers/patches.ts create mode 100644 pages-v2/src/legacy/renderers/resources.ts create mode 100644 pages-v2/src/legacy/renderers/web.ts create mode 100644 pages-v2/src/legacy/renderers/webPretty.ts create mode 100644 pages-v2/src/legacy/renderers/webPretty/announcement.ts create mode 100644 pages-v2/src/legacy/renderers/webPretty/banner.ts create mode 100644 pages-v2/src/legacy/renderers/webPretty/mainBgImage.ts create mode 100644 pages-v2/src/legacy/renderers/webPretty/sidebar.ts create mode 100644 pages-v2/src/legacy/renderers/webPretty/singleEnt.ts create mode 100644 pages-v2/src/legacy/resources.ts create mode 100644 pages-v2/src/legacy/types.ts create mode 100644 pages-v2/src/legacy/utils/constants.ts create mode 100644 pages-v2/src/legacy/utils/logger.ts create mode 100644 pages-v2/src/legacy/utils/math.ts create mode 100644 pages-v2/src/legacy/utils/ui.ts create mode 100644 pages-v2/src/legacy/web.ts create mode 100644 pages-v2/src/legacy/webPretty.ts create mode 100644 pages-v2/src/legacy/webPretty/announcement.ts create mode 100644 pages-v2/src/legacy/webPretty/banner.ts create mode 100644 pages-v2/src/legacy/webPretty/mainBgImage.ts create mode 100644 pages-v2/src/legacy/webPretty/sidebar.ts create mode 100644 pages-v2/src/legacy/webPretty/singleEnt.ts create mode 100644 pages-v2/src/main.tsx create mode 100644 pages-v2/src/types.ts create mode 100644 pages-v2/src/utils/api.ts create mode 100644 pages-v2/src/utils/constants.ts create mode 100644 pages-v2/src/utils/logger.ts create mode 100644 pages-v2/src/utils/math.ts create mode 100644 pages-v2/src/utils/ui.ts create mode 100644 pages-v2/tsconfig.app.json create mode 100644 pages-v2/tsconfig.json create mode 100644 pages-v2/tsconfig.node.json create mode 100644 pages-v2/vite.config.ts diff --git a/pages-v2/.editorconfig b/pages-v2/.editorconfig new file mode 100644 index 0000000..9aa5361 --- /dev/null +++ b/pages-v2/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true \ No newline at end of file diff --git a/pages-v2/.gitignore b/pages-v2/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/pages-v2/.gitignore @@ -0,0 +1,24 @@ +# 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? diff --git a/pages-v2/README_vite.md b/pages-v2/README_vite.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/pages-v2/README_vite.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/pages-v2/biome.json b/pages-v2/biome.json new file mode 100644 index 0000000..86ebb55 --- /dev/null +++ b/pages-v2/biome.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", + "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, + "files": { "ignoreUnknown": false, "includes": ["**"] }, + "formatter": { + "enabled": true, + "useEditorconfig": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 120, + "attributePosition": "auto", + "bracketSpacing": true + }, + "linter": { "enabled": false, "rules": { "recommended": true } }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "single", + "quoteProperties": "asNeeded", + "trailingCommas": "all", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSameLine": false, + "quoteStyle": "single", + "attributePosition": "auto", + "bracketSpacing": true + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/pages-v2/bun.lock b/pages-v2/bun.lock new file mode 100644 index 0000000..bad076d --- /dev/null +++ b/pages-v2/bun.lock @@ -0,0 +1,476 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "pages-v2", + "dependencies": { + "@types/bootstrap": "^5.2.10", + "@types/luxon": "^3.7.1", + "@types/semver": "^7.7.1", + "bootstrap": "^5.3.8", + "ky": "^1.14.3", + "luxon": "^3.7.2", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "semver": "^7.7.4", + }, + "devDependencies": { + "@biomejs/biome": "^2.4.8", + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^8.0.1", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@biomejs/biome": ["@biomejs/biome@2.4.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.8", "@biomejs/cli-darwin-x64": "2.4.8", "@biomejs/cli-linux-arm64": "2.4.8", "@biomejs/cli-linux-arm64-musl": "2.4.8", "@biomejs/cli-linux-x64": "2.4.8", "@biomejs/cli-linux-x64-musl": "2.4.8", "@biomejs/cli-win32-arm64": "2.4.8", "@biomejs/cli-win32-x64": "2.4.8" }, "bin": { "biome": "bin/biome" } }, "sha512-ponn0oKOky1oRXBV+rlSaUlixUxf1aZvWC19Z41zBfUOUesthrQqL3OtiAlSB1EjFjyWpn98Q64DHelhA6jNlA=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ARx0tECE8I7S2C2yjnWYLNbBdDoPdq3oyNLhMglmuctThwUsuzFWRKrHmIGwIRWKz0Mat9DuzLEDp52hGnrxGQ=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jg9/PsB9vDCJlANE8uhG7qDhb5w0Ix69D7XIIc8IfZPUoiPrbLm33k2Ig3NOJ/7nb3UbesFz3D1aDKm9DvzjhQ=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-5CdrsJct76XG2hpKFwXnEtlT1p+4g4yV+XvvwBpzKsTNLO9c6iLlAxwcae2BJ7ekPGWjNGw9j09T5KGPKKxQig=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Zo9OhBQDJ3IBGPlqHiTISloo5H0+FBIpemqIJdW/0edJ+gEcLR+MZeZozcUyz3o1nXkVA7++DdRKQT0599j9jA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-PdKXspVEaMCQLjtZCn6vfSck/li4KX9KGwSDbZdgIqlrizJ2MnMcE3TvHa2tVfXNmbjMikzcfJpuPWH695yJrw=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-Gi8quv8MEuDdKaPFtS2XjEnMqODPsRg6POT6KhoP+VrkNb+T2ywunVB+TvOU0LX1jAZzfBr+3V1mIbBhzAMKvw=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-LoFatS0tnHv6KkCVpIy3qZCih+MxUMvdYiPWLHRri7mhi2vyOOs8OrbZBcLTUEWCS+ktO72nZMy4F96oMhkOHQ=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.8", "", { "os": "win32", "cpu": "x64" }, "sha512-vAn7iXDoUbqFXqVocuq1sMYAd33p8+mmurqJkWl6CtIhobd/O6moe4rY5AJvzbunn/qZCdiDVcveqtkFh1e7Hg=="], + + "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + + "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="], + + "@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.10", "", { "os": "android", "cpu": "arm64" }, "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm" }, "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.10", "", { "os": "none", "cpu": "arm64" }, "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.10", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "x64" }, "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/bootstrap": ["@types/bootstrap@5.2.10", "", { "dependencies": { "@popperjs/core": "^2.9.2" } }, "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="], + + "@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/type-utils": "8.57.1", "@typescript-eslint/utils": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.1", "@typescript-eslint/types": "^8.57.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1" } }, "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1", "@typescript-eslint/utils": "8.57.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.57.1", "", {}, "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.1", "@typescript-eslint/tsconfig-utils": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg=="], + + "bootstrap": ["bootstrap@5.3.8", "", { "peerDependencies": { "@popperjs/core": "^2.11.8" } }, "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001780", "", {}, "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.321", "", {}, "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], + + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.2", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "ky": ["ky@1.14.3", "", {}, "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "rolldown": ["rolldown@1.0.0-rc.10", "", { "dependencies": { "@oxc-project/types": "=0.120.0", "@rolldown/pluginutils": "1.0.0-rc.10" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-x64": "1.0.0-rc.10", "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.57.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.1", "@typescript-eslint/parser": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1", "@typescript-eslint/utils": "8.57.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "vite": ["vite@8.0.1", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.10", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.10", "", {}, "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + } +} diff --git a/pages-v2/eslint.config.js b/pages-v2/eslint.config.js new file mode 100644 index 0000000..6c9f083 --- /dev/null +++ b/pages-v2/eslint.config.js @@ -0,0 +1,26 @@ +import js from '@eslint/js'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + rules: { + '@typescript-eslint/no-explicit-any': off, + }, + }, +]); diff --git a/pages-v2/index.html b/pages-v2/index.html new file mode 100644 index 0000000..5a6aaa7 --- /dev/null +++ b/pages-v2/index.html @@ -0,0 +1,25 @@ + + + + + + + Arknights: Endfield API Archive + + + + + + + + + + + +
+ + + diff --git a/pages-v2/package.json b/pages-v2/package.json new file mode 100644 index 0000000..9cd0add --- /dev/null +++ b/pages-v2/package.json @@ -0,0 +1,38 @@ +{ + "name": "pages-v2", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@types/bootstrap": "^5.2.10", + "@types/luxon": "^3.7.1", + "@types/semver": "^7.7.1", + "bootstrap": "^5.3.8", + "ky": "^1.14.3", + "luxon": "^3.7.2", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "semver": "^7.7.4" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.8", + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^8.0.1" + } +} diff --git a/pages-v2/public/favicon.svg b/pages-v2/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/pages-v2/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages-v2/public/icons.svg b/pages-v2/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/pages-v2/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pages-v2/src/App.tsx b/pages-v2/src/App.tsx new file mode 100644 index 0000000..011989f --- /dev/null +++ b/pages-v2/src/App.tsx @@ -0,0 +1,271 @@ +import { useEffect, useState } from 'react'; +import AboutTab from './components/tabs/AboutTab'; +import GamePackagesTab from './components/tabs/GamePackagesTab'; +import LauncherTab from './components/tabs/LauncherTab'; +import OverviewTab from './components/tabs/OverviewTab'; +import PatchesTab from './components/tabs/PatchesTab'; +import ResourcesTab from './components/tabs/ResourcesTab'; +import WebTab from './components/tabs/WebTab'; +import type { LauncherWebMainBgImage, MirrorFileEntry, StoredData } from './types'; +import { fetchJson, preloadData } from './utils/api'; +import { BASE_URL } from './utils/constants'; + +const getMirrorUrl = (url: string) => { + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } +}; + +function App() { + const [activeTab, setActiveTab] = useState('overview'); + const [mirrorFileDb, setMirrorFileDb] = useState([]); + const [loading, setLoading] = useState(true); + const [bgImage, setBgImage] = useState(null); + const [bgVisible, setBgVisible] = useState(false); + + useEffect(() => { + const getPreferredTheme = () => { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + }; + const setTheme = (theme: string) => { + document.documentElement.setAttribute('data-bs-theme', theme); + }; + + setTheme(getPreferredTheme()); + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleChange = () => setTheme(getPreferredTheme()); + mediaQuery.addEventListener('change', handleChange); + + return () => mediaQuery.removeEventListener('change', handleChange); + }, []); + + useEffect(() => { + const init = async () => { + try { + await preloadData(); + } catch (e) { + console.warn('Preload failed', e); + } + + // Fetch mirror list + try { + const db = await fetchJson(`${BASE_URL}/mirror_file_list.json`); + setMirrorFileDb(db); + } catch (e) { + console.warn('Failed to fetch mirror list', e); + } finally { + setLoading(false); + } + + // Fetch latest background image + try { + const url = `${BASE_URL}/akEndfield/launcher/web/6/main_bg_image/en-us/all.json`; + const data = await fetchJson[]>(url); + if (data.length > 0) { + // Sort by updatedAt descending + const sorted = data.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); + const latest = sorted.find((entry) => entry.rsp?.main_bg_image?.url); + if (latest?.rsp?.main_bg_image?.url) { + const mirrorUrl = getMirrorUrl(latest.rsp.main_bg_image.url); + + // Preload the image + const img = new Image(); + img.src = mirrorUrl; + img.onload = () => { + setBgImage(mirrorUrl); + // Small delay to trigger transition + setTimeout(() => setBgVisible(true), 1); + }; + } + } + } catch (e) { + console.warn('Failed to fetch background image', e); + } + }; + init(); + }, []); + + const renderContent = () => { + if (loading) { + return ( +
+
+

Loading data...

+
+ ); + } + + switch (activeTab) { + case 'overview': + return ; + case 'game': + return ; + case 'patch': + return ; + case 'resources': + return ; + case 'launcher': + return ; + case 'web-pretty': + return ; + case 'about': + return ; + default: + return null; + } + }; + + return ( + <> + {(() => { + const blurRadius = 4; + const opacity = 0.3; + + const opacityAnimSec = 1; + const blurAnimSec = 1; + + const blurRadiusTmp = bgVisible ? blurRadius : blurRadius * 2; + return ( +
+
+
+ ); + })()} +
+
+

Arknights: Endfield API Archive

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ +
+ {renderContent()} +
+ +
+ +
+ +

+ (C) daydreamer-json and contributors +

+
+ +
+
+
+
+

+ Debug Panel +

+ +
+
+
+

Debug Log

+
+                    
+                  
+
+
+
+
+
+
+ + ); +} + +export default App; diff --git a/pages-v2/src/assets/css/about.css b/pages-v2/src/assets/css/about.css new file mode 100644 index 0000000..81aee59 --- /dev/null +++ b/pages-v2/src/assets/css/about.css @@ -0,0 +1,8 @@ +#endmin-thumbsup { + width: 200px; + position: fixed; + bottom: 0; + left: 50%; + transform: translateX(-50%); + z-index: -1000; +} diff --git a/pages-v2/src/assets/css/essentials.css b/pages-v2/src/assets/css/essentials.css new file mode 100644 index 0000000..3e32066 --- /dev/null +++ b/pages-v2/src/assets/css/essentials.css @@ -0,0 +1,68 @@ +@import "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Noto+Sans+JP:wght@100..900&family=Noto+Sans+SC:wght@100..900&family=Noto+Sans+TC:wght@100..900&display=swap"; +@import "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"; +/* @import "../fonts/line_seed/line_seed.css"; */ +/* @import "../fonts/iosevka/Iosevka.css"; */ +/* @import "../fonts/novecento_sans/novecento_sans.css"; */ +@import "../fonts/novecento_sans/novecento_sans_wide_number_only.css"; +/* @import "../fonts/novecento_sans/novecento_sans_normal_number_only.css"; */ +@import "../fonts/harmonyos_sans/harmonyos_sans.css"; +/* @import "../fonts/dseg/dseg.css"; */ + +:root { + /* --ddjson-custom-font-main: 'Malgun Gothic'; */ + /* --ddjson-custom-font-main: + "Inter", "LINE Seed JP (Web)", "Noto Sans JP", "Noto Sans SC", "SF Pro", + -apple-system, "BlinkMacSystemFont", "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Helvetica Neue", "Helvetica", "Arial", sans-serif, system-ui; */ + --ddjson-custom-font-main: + "Novecento Sans Wide (Number Only)", "HarmonyOS Sans", "Noto Sans JP", + "Noto Sans SC", "SF Pro", -apple-system, "BlinkMacSystemFont", "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Helvetica Neue", "Helvetica", "Arial", + sans-serif, system-ui; + --ddjson-custom-font-mono: + "JetBrains Mono", "SF Mono", "Noto Sans JP", "Noto Sans SC", + "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", + "Courier New", monospace, sans-serif, system-ui; + --bs-font-sans-serif: var(--ddjson-custom-font-main); + --bs-font-monospace: var(--ddjson-custom-font-mono); +} + +html, +body { + /* font-family: var(--ddjson-custom-font-main); */ + font-feature-settings: + "liga" 1, + "calt" 1, + "palt"; + word-break: auto-phrase; +} + +.font-monospace { + font-family: var(--ddjson-custom-font-mono) !important; + font-feature-settings: "palt" 0 !important; +} + +pre, +code, +kbd, +samp, +tt { + /* font-family: var(--ddjson-custom-font-mono); */ + font-feature-settings: "palt" 0; +} + +.material-symbols-outlined { + font-variation-settings: + "FILL" 0, + "wght" 400, + "GRAD" 0, + "opsz" 24; + font-size: inherit; +} + +.user-drag-none { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/pages-v2/src/assets/css/index.css b/pages-v2/src/assets/css/index.css new file mode 100644 index 0000000..33ad31b --- /dev/null +++ b/pages-v2/src/assets/css/index.css @@ -0,0 +1,58 @@ +#debug-log-inner { + display: block; + white-space: pre-wrap !important; +} + +#debug-log-inner div { + padding-left: 15ch; + text-indent: -15ch; + margin: 0; +} + +.accordion-button { + padding: calc(var(--bs-accordion-btn-padding-y) / 1.5) + calc(var(--bs-accordion-btn-padding-x) / 1.5); +} + +.card { + --bs-card-bg: color-mix(in srgb, var(--bs-body-bg), transparent 80%); + backdrop-filter: blur(24px); +} + +.table { + --bs-table-bg: color-mix(in srgb, var(--bs-body-bg), transparent 50%); +} + +.table-transparent { + --bs-table-bg: transparent !important; +} + +.accordion { + --bs-accordion-bg: color-mix(in srgb, var(--bs-body-bg), transparent 50%); +} +.accordion-button:not(.collapsed) { + --bs-accordion-active-bg: color-mix( + in srgb, + var(--bs-primary-bg-subtle), + transparent 25% + ); +} + +.nav-link { + background-color: color-mix(in srgb, var(--bs-body-bg), transparent 80%); +} +.nav-tabs .nav-link.active { + background-color: color-mix( + in srgb, + var(--bs-nav-tabs-link-active-bg), + transparent 25% + ); +} + +.list-group { + --bs-list-group-bg: color-mix(in srgb, var(--bs-body-bg), transparent 50%); +} + +.form-select { + background-color: color-mix(in srgb, var(--bs-body-bg), transparent 50%); +} diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_black.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_black.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_black.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_black.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_black.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_black.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_black.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_black.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_bold.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_bold.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_bold.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_bold.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_bold.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_bold.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_bold.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_bold.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_light.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_light.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_light.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_light.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_light.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_light.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_light.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_light.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_medium.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_medium.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_medium.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_medium.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_medium.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_medium.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_medium.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_medium.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_regular.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_regular.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_regular.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_regular.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_regular.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_regular.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_regular.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_regular.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_semibold.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_semibold.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_semibold.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_semibold.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_semibold.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_semibold.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_semibold.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_semibold.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_thin.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_thin.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_thin.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_thin.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_thin.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_thin.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_thin.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed/harmonyos_sans_condensed_thin.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_black.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_black.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_black.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_black.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_black.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_black.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_black.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_black.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_bold.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_bold.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_bold.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_bold.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_bold.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_bold.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_bold.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_bold.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_light.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_light.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_light.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_light.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_light.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_light.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_light.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_light.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_medium.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_medium.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_medium.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_medium.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_medium.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_medium.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_medium.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_medium.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_regular.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_regular.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_regular.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_regular.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_regular.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_regular.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_regular.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_regular.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_semibold.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_semibold.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_semibold.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_semibold.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_semibold.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_semibold.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_semibold.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_semibold.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_thin.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_thin.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_thin.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_thin.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_thin.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_thin.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_thin.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/condensed_italic/harmonyos_sans_condensed_italic_thin.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/harmonyos_sans.css b/pages-v2/src/assets/fonts/harmonyos_sans/harmonyos_sans.css similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/harmonyos_sans.css rename to pages-v2/src/assets/fonts/harmonyos_sans/harmonyos_sans.css diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_black.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_black.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_black.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_black.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_black.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_black.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_black.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_black.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_bold.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_bold.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_bold.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_bold.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_bold.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_bold.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_bold.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_bold.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_light.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_light.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_light.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_light.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_light.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_light.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_light.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_light.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_medium.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_medium.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_medium.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_medium.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_medium.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_medium.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_medium.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_medium.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_regular.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_regular.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_regular.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_regular.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_regular.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_regular.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_regular.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_regular.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_semibold.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_semibold.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_semibold.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_semibold.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_semibold.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_semibold.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_semibold.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_semibold.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_thin.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_thin.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_thin.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_thin.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_thin.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_thin.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_thin.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/italic/harmonyos_sans_italic_thin.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_black.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_black.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_black.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_black.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_black.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_black.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_black.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_black.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_bold.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_bold.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_bold.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_bold.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_bold.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_bold.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_bold.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_bold.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_light.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_light.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_light.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_light.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_light.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_light.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_light.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_light.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_medium.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_medium.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_medium.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_medium.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_medium.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_medium.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_medium.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_medium.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_regular.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_regular.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_regular.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_regular.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_regular.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_regular.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_regular.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_regular.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_semibold.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_semibold.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_semibold.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_semibold.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_semibold.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_semibold.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_semibold.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_semibold.woff2 diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_thin.ttf b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_thin.ttf similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_thin.ttf rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_thin.ttf diff --git a/pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_thin.woff2 b/pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_thin.woff2 similarity index 100% rename from pages/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_thin.woff2 rename to pages-v2/src/assets/fonts/harmonyos_sans/normal/harmonyos_sans_thin.woff2 diff --git a/pages/src/assets/fonts/iosevka/Iosevka.css b/pages-v2/src/assets/fonts/iosevka/Iosevka.css similarity index 100% rename from pages/src/assets/fonts/iosevka/Iosevka.css rename to pages-v2/src/assets/fonts/iosevka/Iosevka.css diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-Bold.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Bold.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-Bold.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Bold.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-BoldItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-BoldItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-BoldItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-BoldItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-BoldOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-BoldOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-BoldOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-BoldOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-Extended.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Extended.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-Extended.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Extended.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBold.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBold.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBold.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBold.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBoldItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBoldItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBoldItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBoldItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBoldOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBoldOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBoldOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedBoldOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBold.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBold.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBold.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBold.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBoldItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBoldItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBoldItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBoldItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBoldOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBoldOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBoldOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraBoldOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLight.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLight.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLight.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLight.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLightItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLightItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLightItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLightItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLightOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLightOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLightOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedExtraLightOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavy.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavy.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavy.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavy.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavyItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavyItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavyItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavyItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavyOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavyOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavyOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedHeavyOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLight.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLight.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLight.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLight.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLightItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLightItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLightItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLightItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLightOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLightOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLightOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedLightOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMedium.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMedium.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMedium.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMedium.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMediumItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMediumItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMediumItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMediumItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMediumOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMediumOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMediumOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedMediumOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBold.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBold.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBold.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBold.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBoldItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBoldItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBoldItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBoldItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBoldOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBoldOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBoldOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedSemiBoldOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThin.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThin.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThin.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThin.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThinItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThinItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThinItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThinItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThinOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThinOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThinOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtendedThinOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBold.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBold.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBold.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBold.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBoldItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBoldItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBoldItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBoldItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBoldOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBoldOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBoldOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtraBoldOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLight.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLight.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLight.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLight.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLightItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLightItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLightItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLightItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLightOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLightOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLightOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ExtraLightOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-Heavy.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Heavy.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-Heavy.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Heavy.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-HeavyItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-HeavyItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-HeavyItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-HeavyItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-HeavyOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-HeavyOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-HeavyOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-HeavyOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-Italic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Italic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-Italic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Italic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-Light.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Light.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-Light.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Light.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-LightItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-LightItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-LightItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-LightItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-LightOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-LightOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-LightOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-LightOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-Medium.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Medium.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-Medium.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Medium.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-MediumItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-MediumItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-MediumItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-MediumItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-MediumOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-MediumOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-MediumOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-MediumOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-Oblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Oblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-Oblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Oblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-Regular.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Regular.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-Regular.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Regular.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-SemiBold.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-SemiBold.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-SemiBold.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-SemiBold.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-SemiBoldItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-SemiBoldItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-SemiBoldItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-SemiBoldItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-SemiBoldOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-SemiBoldOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-SemiBoldOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-SemiBoldOblique.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-Thin.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Thin.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-Thin.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-Thin.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ThinItalic.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ThinItalic.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ThinItalic.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ThinItalic.woff2 diff --git a/pages/src/assets/fonts/iosevka/woff2/Iosevka-ThinOblique.woff2 b/pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ThinOblique.woff2 similarity index 100% rename from pages/src/assets/fonts/iosevka/woff2/Iosevka-ThinOblique.woff2 rename to pages-v2/src/assets/fonts/iosevka/woff2/Iosevka-ThinOblique.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_bold.otf b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_bold.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_bold.otf rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_bold.otf diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_bold.woff2 b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_bold.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_bold.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_bold.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_book.otf b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_book.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_book.otf rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_book.otf diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_book.woff2 b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_book.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_book.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_book.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_demibold.otf b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_demibold.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_demibold.otf rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_demibold.otf diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_demibold.woff2 b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_demibold.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_demibold.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_demibold.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_light.otf b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_light.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_light.otf rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_light.otf diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_light.woff2 b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_light.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_light.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_light.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_medium.otf b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_medium.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_medium.otf rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_medium.otf diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_medium.woff2 b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_medium.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_medium.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_medium.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_normal.otf b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_normal.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_normal.otf rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_normal.otf diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_normal.woff2 b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_normal.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_normal.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_normal.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultrabold.otf b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultrabold.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultrabold.otf rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultrabold.otf diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultrabold.woff2 b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultrabold.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultrabold.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultrabold.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultralight.otf b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultralight.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultralight.otf rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultralight.otf diff --git a/pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultralight.woff2 b/pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultralight.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultralight.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/condensed/novecento_sans_condensed_ultralight.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_bold.otf b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_bold.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_bold.otf rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_bold.otf diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_bold.woff2 b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_bold.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_bold.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_bold.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_book.otf b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_book.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_book.otf rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_book.otf diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_book.woff2 b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_book.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_book.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_book.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_demibold.otf b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_demibold.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_demibold.otf rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_demibold.otf diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_demibold.woff2 b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_demibold.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_demibold.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_demibold.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_light.otf b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_light.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_light.otf rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_light.otf diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_light.woff2 b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_light.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_light.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_light.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_medium.otf b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_medium.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_medium.otf rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_medium.otf diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_medium.woff2 b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_medium.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_medium.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_medium.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_normal.otf b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_normal.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_normal.otf rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_normal.otf diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_normal.woff2 b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_normal.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_normal.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_normal.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultrabold.otf b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultrabold.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultrabold.otf rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultrabold.otf diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultrabold.woff2 b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultrabold.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultrabold.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultrabold.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultralight.otf b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultralight.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultralight.otf rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultralight.otf diff --git a/pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultralight.woff2 b/pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultralight.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultralight.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/narrow/novecento_sans_narrow_ultralight.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_bold.otf b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_bold.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_bold.otf rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_bold.otf diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_bold.woff2 b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_bold.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_bold.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_bold.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_book.otf b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_book.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_book.otf rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_book.otf diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_book.woff2 b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_book.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_book.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_book.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_demibold.otf b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_demibold.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_demibold.otf rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_demibold.otf diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_demibold.woff2 b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_demibold.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_demibold.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_demibold.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_light.otf b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_light.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_light.otf rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_light.otf diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_light.woff2 b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_light.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_light.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_light.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_medium.otf b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_medium.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_medium.otf rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_medium.otf diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_medium.woff2 b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_medium.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_medium.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_medium.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_normal.otf b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_normal.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_normal.otf rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_normal.otf diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_normal.woff2 b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_normal.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_normal.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_normal.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_ultrabold.otf b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_ultrabold.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_ultrabold.otf rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_ultrabold.otf diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_ultrabold.woff2 b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_ultrabold.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_ultrabold.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_ultrabold.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_ultralight.otf b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_ultralight.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_ultralight.otf rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_ultralight.otf diff --git a/pages/src/assets/fonts/novecento_sans/normal/novecento_sans_ultralight.woff2 b/pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_ultralight.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/normal/novecento_sans_ultralight.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/normal/novecento_sans_ultralight.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/novecento_sans.css b/pages-v2/src/assets/fonts/novecento_sans/novecento_sans.css similarity index 100% rename from pages/src/assets/fonts/novecento_sans/novecento_sans.css rename to pages-v2/src/assets/fonts/novecento_sans/novecento_sans.css diff --git a/pages/src/assets/fonts/novecento_sans/novecento_sans_normal_number_only.css b/pages-v2/src/assets/fonts/novecento_sans/novecento_sans_normal_number_only.css similarity index 100% rename from pages/src/assets/fonts/novecento_sans/novecento_sans_normal_number_only.css rename to pages-v2/src/assets/fonts/novecento_sans/novecento_sans_normal_number_only.css diff --git a/pages/src/assets/fonts/novecento_sans/novecento_sans_wide_number_only.css b/pages-v2/src/assets/fonts/novecento_sans/novecento_sans_wide_number_only.css similarity index 100% rename from pages/src/assets/fonts/novecento_sans/novecento_sans_wide_number_only.css rename to pages-v2/src/assets/fonts/novecento_sans/novecento_sans_wide_number_only.css diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_bold.otf b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_bold.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_bold.otf rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_bold.otf diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_bold.woff2 b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_bold.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_bold.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_bold.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_book.otf b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_book.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_book.otf rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_book.otf diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_book.woff2 b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_book.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_book.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_book.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_demibold.otf b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_demibold.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_demibold.otf rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_demibold.otf diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_demibold.woff2 b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_demibold.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_demibold.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_demibold.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_light.otf b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_light.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_light.otf rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_light.otf diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_light.woff2 b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_light.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_light.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_light.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_medium.otf b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_medium.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_medium.otf rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_medium.otf diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_medium.woff2 b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_medium.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_medium.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_medium.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_normal.otf b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_normal.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_normal.otf rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_normal.otf diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_normal.woff2 b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_normal.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_normal.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_normal.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultrabold.otf b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultrabold.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultrabold.otf rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultrabold.otf diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultrabold.woff2 b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultrabold.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultrabold.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultrabold.woff2 diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultralight.otf b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultralight.otf similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultralight.otf rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultralight.otf diff --git a/pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultralight.woff2 b/pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultralight.woff2 similarity index 100% rename from pages/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultralight.woff2 rename to pages-v2/src/assets/fonts/novecento_sans/wide/novecento_sans_wide_ultralight.woff2 diff --git a/pages/src/assets/img/endmin_thumbsup.png b/pages-v2/src/assets/img/endmin_thumbsup.png similarity index 100% rename from pages/src/assets/img/endmin_thumbsup.png rename to pages-v2/src/assets/img/endmin_thumbsup.png diff --git a/pages/src/assets/img/endmin_thumbsup.webp b/pages-v2/src/assets/img/endmin_thumbsup.webp similarity index 100% rename from pages/src/assets/img/endmin_thumbsup.webp rename to pages-v2/src/assets/img/endmin_thumbsup.webp diff --git a/pages/src/assets/img/endmin_thumbsup_640px.webp b/pages-v2/src/assets/img/endmin_thumbsup_640px.webp similarity index 100% rename from pages/src/assets/img/endmin_thumbsup_640px.webp rename to pages-v2/src/assets/img/endmin_thumbsup_640px.webp diff --git a/pages-v2/src/components/tabs/AboutTab.tsx b/pages-v2/src/components/tabs/AboutTab.tsx new file mode 100644 index 0000000..bb51d6a --- /dev/null +++ b/pages-v2/src/components/tabs/AboutTab.tsx @@ -0,0 +1,45 @@ +import endminImg from '../../assets/img/endmin_thumbsup_640px.webp'; + +export default function AboutTab() { + return ( +
+

About this website

+

+ This site functions as a simplified display page for data from the ak-endfield-api-archive{' '} + repository. +

+

+ The{' '} + + ak-endfield-api-archive + {' '} + repository automatically records various API data changes for Arknights: Endfield. It also archives not only API + changes but also some packages and raw binary data. This is useful for certain users interested in analyzing and + researching game data. +

+ +

Disclaimer

+

+ This project has no affiliation with Hypergryph (GRYPHLINE) and was created solely for{' '} + private use, educational, and research purposes. +
+ Copyright for the archived API data and binary data belongs to their respective copyright holders. +

+

+ I assume no responsibility whatsoever. PLEASE USE IT AT YOUR OWN RISK. +

+ +

Thanks

+ + + + + + + +
Vivi029Added Windows Google Play Games channel
+ + Endmin Thumbs Up +
+ ); +} diff --git a/pages-v2/src/components/tabs/GamePackagesTab.tsx b/pages-v2/src/components/tabs/GamePackagesTab.tsx new file mode 100644 index 0000000..ebad853 --- /dev/null +++ b/pages-v2/src/components/tabs/GamePackagesTab.tsx @@ -0,0 +1,187 @@ +import { DateTime } from 'luxon'; +import { useEffect, useState } from 'react'; +import type { MirrorFileEntry, StoredData } from '../../types'; +import { fetchJson } from '../../utils/api'; +import { BASE_URL, FILE_SIZE_OPTS, gameTargets } from '../../utils/constants'; +import math from '../../utils/math'; +import { generateDownloadLinks } from '../../utils/ui'; + +interface Props { + mirrorFileDb: MirrorFileEntry[]; +} + +interface GamePackageData { + targetName: string; + region: 'os' | 'cn'; + dirName: string; + versions: Array<{ + version: string; + dateStr: string; + packedSizeStr: string; + unpackedSizeStr: string; + packs: Array<{ + fileName: string; + md5: string; + sizeStr: string; + url: string; + }>; + }>; +} + +export default function GamePackagesTab({ mirrorFileDb }: Props) { + const [packages, setPackages] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + const promises = gameTargets.map(async (target) => { + const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all.json`; + try { + const data = await fetchJson[]>(url); + const list = [...data].reverse(); + + const versions = list + .map((e) => { + if (!e) return null; + const version = e.rsp.version; + const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + + const packSizes = e.rsp.pkg.packs.map((f: any) => parseInt(f.package_size)); + const packedSize = math.arrayTotal(packSizes); + const totalSize = parseInt(e.rsp.pkg.total_size); + const unpackedSize = totalSize - packedSize; + + const packs = e.rsp.pkg.packs.map((f: any) => ({ + fileName: new URL(f.url).pathname.split('/').pop() ?? '', + md5: f.md5, + sizeStr: math.formatFileSize(parseInt(f.package_size), FILE_SIZE_OPTS), + url: f.url, + })); + + return { + version, + dateStr, + packedSizeStr: math.formatFileSize(packedSize, FILE_SIZE_OPTS), + unpackedSizeStr: math.formatFileSize(unpackedSize, FILE_SIZE_OPTS), + packs, + }; + }) + .filter((v): v is NonNullable => v !== null); + + return { + targetName: target.name, + region: target.region, + dirName: target.dirName, + versions, + }; + } catch (err) { + return null; + } + }); + + const results = await Promise.all(promises); + const validResults = results.filter((r): r is GamePackageData => r !== null); + + const sortedResults = gameTargets + .map((t) => validResults.find((r) => r.dirName === t.dirName)) + .filter((r): r is GamePackageData => r !== undefined && r !== null); + + setPackages(sortedResults); + setLoading(false); + }; + + fetchData(); + }, []); + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+ {packages.map((pkg) => ( +
+

+ {pkg.region === 'cn' ? 'China' : 'Global'}, {pkg.targetName} +

+
+ {pkg.versions.map((ver, idx) => { + const itemId = `game-${pkg.dirName}-${idx}`; + return ( +
+

+ +

+
+
+ + + + + + + + + + + +
Unpacked Size{ver.unpackedSizeStr}
Packed Size{ver.packedSizeStr}
+
+ + + + + + + + + + + {ver.packs.map((pack, pIdx) => ( + + + + + + + ))} + +
FileMD5 ChecksumSizeDL
{pack.fileName} + {pack.md5} + {pack.sizeStr}
+
+
+
+
+ ); + })} +
+
+ ))} +
+ ); +} diff --git a/pages-v2/src/components/tabs/LauncherTab.tsx b/pages-v2/src/components/tabs/LauncherTab.tsx new file mode 100644 index 0000000..7040da9 --- /dev/null +++ b/pages-v2/src/components/tabs/LauncherTab.tsx @@ -0,0 +1,227 @@ +import { DateTime } from 'luxon'; +import { useEffect, useState } from 'react'; +import type { MirrorFileEntry, StoredData } from '../../types'; +import { fetchJson } from '../../utils/api'; +import { BASE_URL, FILE_SIZE_OPTS, launcherTargets } from '../../utils/constants'; +import math from '../../utils/math'; +import { generateDownloadLinks } from '../../utils/ui'; + +interface Props { + mirrorFileDb: MirrorFileEntry[]; +} + +interface LauncherData { + regionId: string; + app: string; + zips: Array<{ + dateStr: string; + version: string; + fileName: string; + md5: string; + unpackedStr: string; + packedStr: string; + url: string; + }>; + exes: Array<{ + dateStr: string; + version: string; + fileName: string; + sizeStr: string; + url: string; + }>; +} + +export default function LauncherTab({ mirrorFileDb }: Props) { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + const results: LauncherData[] = []; + + for (const region of launcherTargets) { + for (const app of region.apps) { + const itemData: LauncherData = { + regionId: region.id, + app, + zips: [], + exes: [], + }; + + // Zip + try { + const urlZip = `${BASE_URL}/akEndfield/launcher/launcher/${app}/${region.channel}/all.json`; + const dataZip = await fetchJson[]>(urlZip); + itemData.zips = [...dataZip].reverse().map((e) => { + const fileName = new URL(e.rsp.zip_package_url).pathname.split('/').pop() ?? ''; + const unpacked = parseInt(e.rsp.total_size) - parseInt(e.rsp.package_size); + return { + dateStr: DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'), + version: e.rsp.version, + fileName, + md5: e.rsp.md5, + unpackedStr: math.formatFileSize(unpacked, FILE_SIZE_OPTS), + packedStr: math.formatFileSize(parseInt(e.rsp.package_size), FILE_SIZE_OPTS), + url: e.rsp.zip_package_url, + }; + }); + } catch (e) {} + + // Exe + try { + const urlExe = `${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`; + const dataExe = await fetchJson[]>(urlExe); + itemData.exes = [...dataExe].reverse().map((e) => { + const fileName = new URL(e.rsp.exe_url).pathname.split('/').pop() ?? ''; + return { + dateStr: DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'), + version: e.rsp.version, + fileName, + sizeStr: math.formatFileSize(parseInt(e.rsp.exe_size), FILE_SIZE_OPTS), + url: e.rsp.exe_url, + }; + }); + } catch (e) {} + + if (itemData.zips.length > 0 || itemData.exes.length > 0) { + results.push(itemData); + } + } + } + setData(results); + setLoading(false); + }; + fetchData(); + }, []); + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+ {data.map((item) => ( +
+

+ {item.regionId.toUpperCase()} {item.app} +

+
+ {item.zips.length > 0 && ( +
+

+ +

+
+
+
+ + + + + + + + + + + + + + {item.zips.map((z, idx) => ( + + + + + + + + + + ))} + +
DateVersionFileMD5 ChecksumUnpackedPackedDL
{z.dateStr}{z.version}{z.fileName} + {z.md5} + {z.unpackedStr}{z.packedStr}
+
+
+
+
+ )} + + {item.exes.length > 0 && ( +
+

+ +

+
+
+
+ + + + + + + + + + + + {item.exes.map((e, idx) => ( + + + + + + + + ))} + +
DateVersionFileSizeDL
{e.dateStr}{e.version}{e.fileName}{e.sizeStr}
+
+
+
+
+ )} +
+
+ ))} +
+ ); +} diff --git a/pages-v2/src/components/tabs/OverviewTab.tsx b/pages-v2/src/components/tabs/OverviewTab.tsx new file mode 100644 index 0000000..468a7c6 --- /dev/null +++ b/pages-v2/src/components/tabs/OverviewTab.tsx @@ -0,0 +1,307 @@ +import { DateTime } from 'luxon'; +import { useEffect, useState } from 'react'; +import type { MirrorFileEntry, StoredData } from '../../types'; +import { fetchJson } from '../../utils/api'; +import { BASE_URL, FILE_SIZE_OPTS, gameTargets, launcherTargets } from '../../utils/constants'; +import math from '../../utils/math'; + +interface Props { + mirrorFileDb: MirrorFileEntry[]; +} + +interface GameVersionData { + version: string; + date: string; +} + +interface OverviewTableData { + region: string; + channelName: string; + version: string; + packedSize: string; + unpackedSize: string; +} + +interface ResourceVersionData { + platform: string; + version: string; + date: string; +} + +export default function OverviewTab({ mirrorFileDb }: Props) { + const [globalPkg, setGlobalPkg] = useState(null); + const [chinaPkg, setChinaPkg] = useState(null); + const [tableData, setTableData] = useState([]); + const [resourceData, setResourceData] = useState([]); + const [mirrorStats, setMirrorStats] = useState('---'); + + useEffect(() => { + // 1. Latest Version Info + const fetchLatestVersion = async (url: string) => { + try { + const dat = await fetchJson[]>(url); + const latest = dat.at(-1); + if (!latest) return { version: '---', date: '---' }; + return { + version: latest.rsp.version, + date: DateTime.fromISO(latest.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'), + }; + } catch { + return { version: '---', date: '---' }; + } + }; + + fetchLatestVersion(`${BASE_URL}/akEndfield/launcher/game/6/all.json`).then(setGlobalPkg); + fetchLatestVersion(`${BASE_URL}/akEndfield/launcher/game/1/all.json`).then(setChinaPkg); + + // 2. Game Packages Table & Mirror Stats Calculation + const calculateStatsAndTable = async () => { + const mirrorOrigSet = new Set(); + for (const m of mirrorFileDb) { + try { + const u = new URL(m.orig); + u.search = ''; + mirrorOrigSet.add(u.toString()); + } catch {} + } + + const countedUrls = new Set(); + let totalMirrorSize = 0; + + const checkAndAddSize = (url: string, size: number) => { + if (!url || isNaN(size)) return; + try { + const u = new URL(url); + u.search = ''; + const cleanUrl = u.toString(); + if (countedUrls.has(cleanUrl)) return; + if (mirrorOrigSet.has(cleanUrl)) { + totalMirrorSize += size; + countedUrls.add(cleanUrl); + } + } catch {} + }; + + const newTableData: OverviewTableData[] = []; + + // Game Packages + for (const target of gameTargets) { + const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) continue; + + const latest = data[data.length - 1]; + if (!latest) continue; + + const version = latest.rsp.version; + const packedSize = math.arrayTotal(latest.rsp.pkg.packs.map((f: any) => parseInt(f.package_size))); + const totalSize = parseInt(latest.rsp.pkg.total_size); + const unpackedSize = totalSize - packedSize; + + newTableData.push({ + region: target.region === 'cn' ? 'China' : 'Global', + channelName: target.name, + version: version, + packedSize: math.formatFileSize(packedSize, FILE_SIZE_OPTS), + unpackedSize: math.formatFileSize(unpackedSize, FILE_SIZE_OPTS), + }); + + for (const entry of data) { + if (entry.rsp.pkg && entry.rsp.pkg.packs) { + for (const pack of entry.rsp.pkg.packs) { + checkAndAddSize(pack.url, parseInt(pack.package_size)); + } + } + } + } catch (e) { + console.warn('Overview: Failed to fetch game data', target.name, e); + } + } + setTableData(newTableData); + + // Patches (only for stats) + for (const target of gameTargets) { + const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all_patch.json`; + try { + const data = await fetchJson[]>(url); + for (const entry of data) { + if (!entry.rsp.patch) continue; + if (entry.rsp.patch.url) { + checkAndAddSize(entry.rsp.patch.url, parseInt(entry.rsp.patch.package_size)); + } + if (entry.rsp.patch.patches) { + for (const p of entry.rsp.patch.patches) { + checkAndAddSize(p.url, parseInt(p.package_size)); + } + } + } + } catch {} + } + + // Launchers (only for stats) + for (const region of launcherTargets) { + for (const app of region.apps) { + try { + const urlZip = `${BASE_URL}/akEndfield/launcher/launcher/${app}/${region.channel}/all.json`; + const dataZip = await fetchJson[]>(urlZip); + for (const e of dataZip) { + checkAndAddSize(e.rsp.zip_package_url, parseInt(e.rsp.package_size)); + } + } catch {} + try { + const urlExe = `${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`; + const dataExe = await fetchJson[]>(urlExe); + for (const e of dataExe) { + checkAndAddSize(e.rsp.exe_url, parseInt(e.rsp.exe_size)); + } + } catch {} + } + } + + setMirrorStats(math.formatFileSize(totalMirrorSize, { ...FILE_SIZE_OPTS, unit: 'G' })); + }; + + calculateStatsAndTable(); + + // 3. Latest Game Resources + const fetchResources = async () => { + const resPlatforms = ['Windows', 'Android', 'iOS', 'PlayStation']; + const resData = await Promise.all( + resPlatforms.map(async (p) => { + try { + const url = `${BASE_URL}/akEndfield/launcher/game_resources/6/${p}/all.json`; + const dat = await fetchJson[]>(url); + return dat.at(-1); + } catch { + return undefined; + } + }), + ); + + const newResourceData = resPlatforms.map((p, i) => { + const item = resData[i]; + if (!item) { + return { platform: p, version: '---', date: '' }; + } + const initialRes = item.rsp.resources.find((e: any) => e.name === 'initial'); + const mainRes = item.rsp.resources.find((e: any) => e.name === 'main'); + let version = '---'; + if (initialRes && mainRes) { + version = initialRes.version === mainRes.version ? mainRes.version : item.rsp.res_version; + } + return { + platform: p, + version: version, + date: DateTime.fromISO(item.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'), + }; + }); + setResourceData(newResourceData); + }; + fetchResources(); + }, [mirrorFileDb]); + + if (!globalPkg || !chinaPkg) { + return ( +
+
+

Loading overview...

+
+ ); + } + + return ( +
+
+
+

Latest Game Packages

+
+
+

+ {globalPkg.version} +
+ + {globalPkg.date} + +
+ Latest Version (Global) +

+
+
+

+ {chinaPkg.version} +
+ + {chinaPkg.date} + +
+ Latest Version (China) +

+
+
+ +
+ + + + + + + + + + + + {tableData.map((row, i) => ( + + + + + + + + ))} + +
RegionChannelVersionPackedUnpacked
{row.region}{row.channelName}{row.version}{row.packedSize}{row.unpackedSize}
+
+
+
+ +
+
+

Latest Game Resources

+
+ {resourceData.map((res, i) => ( +
+

+ {res.version} +
+ {res.date && ( + <> + + {res.date} + +
+ + )} + {res.platform} +

+
+ ))} +
+
+
+ +
+
+

Mirror Statistics

+

+ {mirrorStats} +
+ uploaded to mirror +

+
+
+
+ ); +} diff --git a/pages-v2/src/components/tabs/PatchesTab.tsx b/pages-v2/src/components/tabs/PatchesTab.tsx new file mode 100644 index 0000000..e7f06bd --- /dev/null +++ b/pages-v2/src/components/tabs/PatchesTab.tsx @@ -0,0 +1,209 @@ +import { DateTime } from 'luxon'; +import { useEffect, useState } from 'react'; +import type { MirrorFileEntry, StoredData } from '../../types'; +import { fetchJson } from '../../utils/api'; +import { BASE_URL, FILE_SIZE_OPTS, gameTargets } from '../../utils/constants'; +import math from '../../utils/math'; +import { generateDownloadLinks } from '../../utils/ui'; + +interface Props { + mirrorFileDb: MirrorFileEntry[]; +} + +interface PatchData { + targetName: string; + region: 'os' | 'cn'; + dirName: string; + patches: Array<{ + version: string; + reqVersion: string; + dateStr: string; + packedSizeStr: string; + unpackedSizeStr: string; + files: Array<{ + fileName: string; + md5: string; + sizeStr: string; + url: string; + }>; + }>; +} + +export default function PatchesTab({ mirrorFileDb }: Props) { + const [patchesData, setPatchesData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + const promises = gameTargets.map(async (target) => { + const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all_patch.json`; + try { + const data = await fetchJson[]>(url); + if (data.length === 0) return null; + + const patches = [...data] + .reverse() + .map((e) => { + if (!e.rsp.patch) return null; + const version = e.rsp.version; + const reqVersion = e.rsp.request_version; + const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + + let packedSize = 0; + if (e.rsp.patch.patches) { + packedSize = math.arrayTotal(e.rsp.patch.patches.map((f: any) => parseInt(f.package_size))); + } + + const totalSize = parseInt(e.rsp.patch.total_size); + const unpackedSize = totalSize - packedSize; + + const files = []; + if (e.rsp.patch.url) { + files.push({ + fileName: new URL(e.rsp.patch.url).pathname.split('/').pop() ?? '', + md5: e.rsp.patch.md5, + sizeStr: math.formatFileSize(parseInt(e.rsp.patch.package_size), FILE_SIZE_OPTS), + url: e.rsp.patch.url, + }); + } + if (e.rsp.patch.patches) { + e.rsp.patch.patches.forEach((f: any) => { + files.push({ + fileName: new URL(f.url).pathname.split('/').pop() ?? '', + md5: f.md5, + sizeStr: math.formatFileSize(parseInt(f.package_size), FILE_SIZE_OPTS), + url: f.url, + }); + }); + } + + return { + version, + reqVersion, + dateStr, + packedSizeStr: math.formatFileSize(packedSize, FILE_SIZE_OPTS), + unpackedSizeStr: math.formatFileSize(unpackedSize, FILE_SIZE_OPTS), + files, + }; + }) + .filter((p): p is NonNullable => p !== null); + + return { + targetName: target.name, + region: target.region, + dirName: target.dirName, + patches, + }; + } catch (err) { + return null; + } + }); + + const results = await Promise.all(promises); + const validResults = results.filter((r): r is PatchData => r !== null); + + const sortedResults = gameTargets + .map((t) => validResults.find((r) => r.dirName === t.dirName)) + .filter((r): r is PatchData => r !== undefined && r !== null); + + setPatchesData(sortedResults); + setLoading(false); + }; + + fetchData(); + }, []); + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+ {patchesData.map((pkg) => ( +
+

+ {pkg.region === 'cn' ? 'China' : 'Global'}, {pkg.targetName} +

+
+ {pkg.patches.map((ver, idx) => { + const itemId = `patch-${pkg.dirName}-${idx}`; + return ( +
+

+ +

+
+
+ + + + + + + + + + + +
Unpacked Size{ver.unpackedSizeStr}
Packed Size{ver.packedSizeStr}
+
+ + + + + + + + + + + {ver.files.map((file, pIdx) => ( + + + + + + + ))} + +
FileMD5 ChecksumSizeDL
{file.fileName} + {file.md5} + {file.sizeStr}
+
+
+
+
+ ); + })} +
+
+ ))} +
+ ); +} diff --git a/pages-v2/src/components/tabs/ResourcesTab.tsx b/pages-v2/src/components/tabs/ResourcesTab.tsx new file mode 100644 index 0000000..6b688a4 --- /dev/null +++ b/pages-v2/src/components/tabs/ResourcesTab.tsx @@ -0,0 +1,284 @@ +import { Tooltip } from 'bootstrap'; +import { DateTime } from 'luxon'; +import { useEffect, useState } from 'react'; +import semver from 'semver'; +import type * as IApiEndfield from '../../../../src/types/api/akEndfield/Api'; +import type { StoredData } from '../../types'; +import { fetchJson } from '../../utils/api'; +import { BASE_URL } from '../../utils/constants'; + +interface ResourceItem { + name?: string; + version: string; + path: string; +} + +interface ResourceGroup { + resVersion: string; + versions: string[]; + dateStr: string; + intervalStr: string; + initialRes: ResourceItem; + mainRes: ResourceItem; + isKick: boolean; +} + +interface PlatformData { + platform: string; + groups: ResourceGroup[]; +} + +interface RegionData { + region: string; + channel: number; + platforms: PlatformData[]; +} + +const PLATFORMS = ['Windows', 'Android', 'iOS', 'PlayStation']; +const TARGETS = [ + { region: 'os', channel: 6, label: 'Global' }, + { region: 'cn', channel: 1, label: 'China' }, +]; + +const DEFAULT_RESOURCE: ResourceItem = { version: '?', path: '#' }; + +const getMirrorUrl = (url: string) => { + if (!url) return ''; + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } +}; + +function useResourcesData() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchAndProcessData = async () => { + try { + const regionPromises = TARGETS.map(async (target) => { + const platformPromises = PLATFORMS.map(async (platform) => { + try { + const url = `${BASE_URL}/akEndfield/launcher/game_resources/${target.channel}/${platform}/all.json`; + const rawData = await fetchJson[]>(url); + + const groups = processRawData(rawData); + return groups.length > 0 ? { platform, groups } : null; + } catch (error) { + console.error(`Failed to fetch ${platform} for ${target.region}:`, error); + return null; + } + }); + + const platformsData = (await Promise.all(platformPromises)).filter((p): p is PlatformData => p !== null); + return platformsData.length > 0 + ? { region: target.region, channel: target.channel, platforms: platformsData } + : null; + }); + + const regionResults = (await Promise.all(regionPromises)).filter((r): r is RegionData => r !== null); + setData(regionResults); + } finally { + setLoading(false); + } + }; + + fetchAndProcessData(); + }, []); + + return { data, loading }; +} + +function processRawData(rawData: StoredData[]): ResourceGroup[] { + const resVersionMap = new Map; updatedAt: string }>(); + + for (const e of rawData) { + const resVer = e.rsp.res_version; + if (!resVersionMap.has(resVer)) { + resVersionMap.set(resVer, { rsp: e.rsp, gameVer: new Set(), updatedAt: e.updatedAt }); + } + if (e.req.version) { + resVersionMap.get(resVer)!.gameVer.add(e.req.version); + } + } + + const resVersionList = Array.from(resVersionMap.values()) + .map((d) => ({ + resVersion: d.rsp.res_version, + rsp: d.rsp, + gameVers: Array.from(d.gameVer).sort((a, b) => semver.rcompare(a, b)), + updatedAt: d.updatedAt, + })) + .sort((a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis()); + + return resVersionList.map((item, i, arr) => { + const currentDate = DateTime.fromISO(item.updatedAt); + const nextItem = arr[i + 1]; + + let intervalStr = '-'; + if (nextItem) { + const nextDate = DateTime.fromISO(nextItem.updatedAt); + intervalStr = currentDate.diff(nextDate, ['days', 'hours', 'minutes', 'seconds']).toFormat('dd:hh:mm:ss'); + } + + const initialRes = item.rsp.resources?.find((e: any) => e.name === 'initial') || DEFAULT_RESOURCE; + const mainRes = item.rsp.resources?.find((e: any) => e.name === 'main') || DEFAULT_RESOURCE; + const isKick = JSON.parse(item.rsp.configs || '{}').kick_flag === true; + + return { + resVersion: item.resVersion, + versions: item.gameVers, + dateStr: currentDate.toFormat('yyyy/MM/dd HH:mm:ss'), + intervalStr, + initialRes, + mainRes, + isKick, + }; + }); +} + +const ResourceLink = ({ basePath, file }: { basePath: string; file: string }) => { + const fullPath = `${basePath}/${file}`; + return ( + <> + + Orig + {' '} + /{' '} + + Mirror + + + ); +}; +const ResourceTable = ({ groups }: { groups: ResourceGroup[] }) => ( +
+ + + + + + + + + + + + + + + {groups.map((group, idx) => ( + + + + + + + + + + + ))} + +
DateIntervalVersionGame version + Kick + InitialMainPatch
{group.dateStr}{group.intervalStr}{group.initialRes.version === group.mainRes.version ? group.mainRes.version : group.resVersion}{group.versions.join(', ')}{group.isKick ? '✅' : ''} + + + + + +
+
+); +const PlatformAccordion = ({ + region, + channel, + platformData, +}: { + region: string; + channel: number; + platformData: PlatformData; +}) => { + const itemId = `res-${region}-${channel}-${platformData.platform}`; + + return ( +
+

+ +

+
+
+ +
+
+
+ ); +}; + +export default function ResourcesTab() { + const { data, loading } = useResourcesData(); + + useEffect(() => { + if (!loading) { + const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); + const tooltipList = Array.from(tooltipTriggerList).map((el) => new Tooltip(el)); + return () => { + for (const t of tooltipList) { + t.dispose(); + } + }; + } + }, [loading]); + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+ {data.map((regionData) => ( +
+

{TARGETS.find((t) => t.region === regionData.region)?.label || regionData.region}

+
+ {regionData.platforms.map((plat) => ( + + ))} +
+
+ ))} +
+ ); +} diff --git a/pages-v2/src/components/tabs/WebTab.tsx b/pages-v2/src/components/tabs/WebTab.tsx new file mode 100644 index 0000000..e95f17e --- /dev/null +++ b/pages-v2/src/components/tabs/WebTab.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react'; +import { gameTargets, launcherWebApiLang } from '../../utils/constants'; + +import AnnouncementSection from './web/AnnouncementSection'; +import BannerSection from './web/BannerSection'; +import MainBgImageSection from './web/MainBgImageSection'; +import SidebarSection from './web/SidebarSection'; +import SingleEntSection from './web/SingleEntSection'; + +export default function WebTab() { + const [targetIdx, setTargetIdx] = useState(0); + const [lang, setLang] = useState(''); + + const target = gameTargets[targetIdx]!; + const langs = launcherWebApiLang[target.region] || []; + + useEffect(() => { + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + if (langs.includes(defaultLang as any)) { + setLang(defaultLang); + } else if (langs.length > 0) { + setLang(langs[0]); + } + }, [targetIdx, target.region, langs]); + + return ( +
+ {/*
+
*/} +
+
+ + +
+ {langs.length > 1 && ( +
+ + +
+ )} + {/*
+
*/} +
+ + + + + + +
+ ); +} diff --git a/pages-v2/src/components/tabs/web/AnnouncementSection.tsx b/pages-v2/src/components/tabs/web/AnnouncementSection.tsx new file mode 100644 index 0000000..258f143 --- /dev/null +++ b/pages-v2/src/components/tabs/web/AnnouncementSection.tsx @@ -0,0 +1,132 @@ +import { DateTime } from 'luxon'; +import { useEffect, useRef, useState } from 'react'; +import type { LauncherWebAnnouncement, StoredData } from '../../../types'; +import { fetchJson } from '../../../utils/api'; +import { BASE_URL } from '../../../utils/constants'; + +interface Props { + target: { region: 'os' | 'cn'; dirName: string }; + lang: string; +} + +interface TabData { + tabName: string; + announcements: any[]; +} + +export default function AnnouncementSection({ target, lang }: Props) { + const [tabs, setTabs] = useState>(new Map()); + const [loading, setLoading] = useState(false); + const [shouldLoad, setShouldLoad] = useState(true); + const collapseRef = useRef(null); + + useEffect(() => { + const el = collapseRef.current; + if (!el) return; + + const handleShow = () => setShouldLoad(true); + el.addEventListener('show.bs.collapse', handleShow); + return () => el.removeEventListener('show.bs.collapse', handleShow); + }, []); + + useEffect(() => { + const load = async () => { + if (!lang || !shouldLoad) return; + setLoading(true); + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/announcement/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + const newTabsMap = new Map(); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + for (const entry of sortedData) { + if (!entry.rsp || !entry.rsp.tabs) continue; + for (const tab of entry.rsp.tabs) { + if (!newTabsMap.has(tab.tab_id)) { + newTabsMap.set(tab.tab_id, { tabName: tab.tabName, announcements: [] }); + } + const targetTab = newTabsMap.get(tab.tab_id)!; + for (const ann of tab.announcements) { + if (!targetTab.announcements.some((a: any) => a.id === ann.id)) { + targetTab.announcements.push(ann); + } + } + } + } + setTabs(newTabsMap); + } catch (e) { + setTabs(new Map()); + } finally { + setLoading(false); + } + }; + load(); + }, [target, lang, shouldLoad]); + + return ( +
+
+

Announcement

+ +
+
+
+ {loading ? ( +
Loading announcements...
+ ) : tabs.size === 0 ? ( +
No announcements found.
+ ) : ( + Array.from(tabs.entries()).map(([tabId, tabData]) => ( +
+
{tabData.tabName}
+
    + {tabData.announcements + .sort((a, b) => parseInt(b.start_ts) - parseInt(a.start_ts)) + .map((ann) => ( +
  • +
    + + {DateTime.fromMillis(parseInt(ann.start_ts)).toFormat('yyyy/MM/dd HH:mm')} + + {ann.content} +
    + {ann.need_token && ( + + Auth + + )} + {ann.jump_url && ( + + Link + + )} + + ID:{ann.id} + +
    +
    +
  • + ))} +
+
+ )) + )} +
+
+
+ ); +} diff --git a/pages-v2/src/components/tabs/web/BannerSection.tsx b/pages-v2/src/components/tabs/web/BannerSection.tsx new file mode 100644 index 0000000..77ccf5f --- /dev/null +++ b/pages-v2/src/components/tabs/web/BannerSection.tsx @@ -0,0 +1,132 @@ +import { DateTime } from 'luxon'; +import { useEffect, useRef, useState } from 'react'; +import type { LauncherWebBanner, StoredData } from '../../../types'; +import { fetchJson } from '../../../utils/api'; +import { BASE_URL } from '../../../utils/constants'; + +interface Props { + target: { region: 'os' | 'cn'; dirName: string }; + lang: string; +} + +const getMirrorUrl = (url: string) => { + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } +}; + +export default function BannerSection({ target, lang }: Props) { + const [bannerMap, setBannerMap] = useState< + Map + >(new Map()); + const [loading, setLoading] = useState(false); + const [shouldLoad, setShouldLoad] = useState(false); + const collapseRef = useRef(null); + + useEffect(() => { + const el = collapseRef.current; + if (!el) return; + + const handleShow = () => setShouldLoad(true); + el.addEventListener('show.bs.collapse', handleShow); + return () => el.removeEventListener('show.bs.collapse', handleShow); + }, []); + + useEffect(() => { + const load = async () => { + if (!lang || !shouldLoad) return; + setLoading(true); + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/banner/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + const newBannerMap = new Map(); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + for (const entry of sortedData) { + if (!entry.rsp || !entry.rsp.banners) continue; + for (const banner of entry.rsp.banners) { + if (!newBannerMap.has(banner.id)) { + newBannerMap.set(banner.id, { banner, firstSeen: entry.updatedAt }); + } + } + } + setBannerMap(newBannerMap); + } catch (e) { + setBannerMap(new Map()); + } finally { + setLoading(false); + } + }; + load(); + }, [target, lang, shouldLoad]); + + return ( +
+
+

Banner

+ +
+
+
+ {loading ? ( +
Loading banners...
+ ) : bannerMap.size === 0 ? ( +
No banners found.
+ ) : ( +
+ {Array.from(bannerMap.entries()).map(([id, { banner, firstSeen }]) => { + const dateStr = DateTime.fromISO(firstSeen).toFormat('yyyy/MM/dd HH:mm'); + const mirrorUrl = getMirrorUrl(banner.url); + const linkUrl = banner.jump_url || mirrorUrl; + return ( + + ); + })} +
+ )} +
+
+
+ ); +} diff --git a/pages-v2/src/components/tabs/web/MainBgImageSection.tsx b/pages-v2/src/components/tabs/web/MainBgImageSection.tsx new file mode 100644 index 0000000..02693d8 --- /dev/null +++ b/pages-v2/src/components/tabs/web/MainBgImageSection.tsx @@ -0,0 +1,131 @@ +import { DateTime } from 'luxon'; +import { useEffect, useRef, useState } from 'react'; +import type { LauncherWebMainBgImage, StoredData } from '../../../types'; +import { fetchJson } from '../../../utils/api'; +import { BASE_URL } from '../../../utils/constants'; + +interface Props { + target: { region: 'os' | 'cn'; dirName: string }; + lang: string; +} + +const getMirrorUrl = (url: string) => { + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } +}; + +export default function MainBgImageSection({ target, lang }: Props) { + const [imageMap, setImageMap] = useState< + Map + >(new Map()); + const [loading, setLoading] = useState(false); + const [shouldLoad, setShouldLoad] = useState(false); + const collapseRef = useRef(null); + + useEffect(() => { + const el = collapseRef.current; + if (!el) return; + + const handleShow = () => setShouldLoad(true); + el.addEventListener('show.bs.collapse', handleShow); + return () => el.removeEventListener('show.bs.collapse', handleShow); + }, []); + + useEffect(() => { + const load = async () => { + if (!lang || !shouldLoad) return; + setLoading(true); + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/main_bg_image/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + const newImageMap = new Map(); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + for (const entry of sortedData) { + if (!entry.rsp || !entry.rsp.main_bg_image) continue; + const img = entry.rsp.main_bg_image; + if (!newImageMap.has(img.md5)) { + newImageMap.set(img.md5, { image: img, firstSeen: entry.updatedAt }); + } + } + setImageMap(newImageMap); + } catch (e) { + setImageMap(new Map()); + } finally { + setLoading(false); + } + }; + load(); + }, [target, lang, shouldLoad]); + + return ( +
+
+

Main Background Image

+ +
+
+
+ {loading ? ( +
Loading background images...
+ ) : imageMap.size === 0 ? ( +
No images found.
+ ) : ( +
+ {Array.from(imageMap.entries()).map(([md5, { image, firstSeen }]) => { + const dateStr = DateTime.fromISO(firstSeen).toFormat('yyyy/MM/dd HH:mm'); + const mirrorUrl = getMirrorUrl(image.url); + const linkUrl = image.video_url ? getMirrorUrl(image.video_url) : mirrorUrl; + return ( + + ); + })} +
+ )} +
+
+
+ ); +} diff --git a/pages-v2/src/components/tabs/web/SidebarSection.tsx b/pages-v2/src/components/tabs/web/SidebarSection.tsx new file mode 100644 index 0000000..2b81945 --- /dev/null +++ b/pages-v2/src/components/tabs/web/SidebarSection.tsx @@ -0,0 +1,150 @@ +import { DateTime } from 'luxon'; +import { useEffect, useRef, useState } from 'react'; +import type { LauncherWebSidebar, StoredData } from '../../../types'; +import { fetchJson } from '../../../utils/api'; +import { BASE_URL } from '../../../utils/constants'; + +interface Props { + target: { region: 'os' | 'cn'; dirName: string }; + lang: string; +} + +const getMirrorUrl = (url: string) => { + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } +}; + +export default function SidebarSection({ target, lang }: Props) { + const [latestSidebar, setLatestSidebar] = useState<{ + sidebars: LauncherWebSidebar['sidebars']; + updatedAt: string; + } | null>(null); + const [loading, setLoading] = useState(false); + const [shouldLoad, setShouldLoad] = useState(false); + const collapseRef = useRef(null); + + useEffect(() => { + const el = collapseRef.current; + if (!el) return; + + const handleShow = () => setShouldLoad(true); + el.addEventListener('show.bs.collapse', handleShow); + return () => el.removeEventListener('show.bs.collapse', handleShow); + }, []); + + useEffect(() => { + const load = async () => { + if (!lang || !shouldLoad) return; + setLoading(true); + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/sidebar/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + const latest = sortedData[0]; + if (latest && latest.rsp && latest.rsp.sidebars) { + setLatestSidebar({ sidebars: latest.rsp.sidebars, updatedAt: latest.updatedAt }); + } else { + setLatestSidebar(null); + } + } catch (e) { + setLatestSidebar(null); + } finally { + setLoading(false); + } + }; + load(); + }, [target, lang, shouldLoad]); + + return ( +
+
+

Sidebar

+ +
+
+
+ {loading ? ( +
Loading sidebar data...
+ ) : !latestSidebar ? ( +
No active sidebars.
+ ) : ( + <> +
+ {latestSidebar.sidebars.map((item, idx) => ( +
+
+
+
+
{item.media}
+ {item.need_token && Auth} +
+ {item.pic && ( +
+ {item.pic.description} +

{item.pic.description}

+
+ )} + {item.jump_url && ( + + Open Link + + )} + {item.sidebar_labels && item.sidebar_labels.length > 0 && ( +
+ {item.sidebar_labels.map((label, lIdx) => ( + + {label.content} +
+ {label.need_token && ( + Auth + )} + +
+
+ ))} +
+ )} +
+
+
+ ))} +
+
+ Last updated: {DateTime.fromISO(latestSidebar.updatedAt).toFormat('yyyy/MM/dd HH:mm')} +
+ + )} +
+
+
+ ); +} diff --git a/pages-v2/src/components/tabs/web/SingleEntSection.tsx b/pages-v2/src/components/tabs/web/SingleEntSection.tsx new file mode 100644 index 0000000..a730390 --- /dev/null +++ b/pages-v2/src/components/tabs/web/SingleEntSection.tsx @@ -0,0 +1,165 @@ +import { DateTime } from 'luxon'; +import { useEffect, useRef, useState } from 'react'; +import type { LauncherWebSingleEnt, StoredData } from '../../../types'; +import { fetchJson } from '../../../utils/api'; +import { BASE_URL } from '../../../utils/constants'; + +interface Props { + target: { region: 'os' | 'cn'; dirName: string }; + lang: string; +} + +const getMirrorUrl = (url: string) => { + if (!url) return ''; + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } +}; + +export default function SingleEntSection({ target, lang }: Props) { + const [entMap, setEntMap] = useState>( + new Map(), + ); + const [loading, setLoading] = useState(false); + const [shouldLoad, setShouldLoad] = useState(false); + const collapseRef = useRef(null); + + useEffect(() => { + const el = collapseRef.current; + if (!el) return; + + const handleShow = () => setShouldLoad(true); + el.addEventListener('show.bs.collapse', handleShow); + return () => el.removeEventListener('show.bs.collapse', handleShow); + }, []); + + useEffect(() => { + const load = async () => { + if (!lang || !shouldLoad) return; + setLoading(true); + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/single_ent/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + const newEntMap = new Map(); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + for (const entry of sortedData) { + if (!entry.rsp || !entry.rsp.single_ent) continue; + const ent = entry.rsp.single_ent; + const key = ent.version_md5 || ent.version_url; + if (!newEntMap.has(key)) { + newEntMap.set(key, { ent, firstSeen: entry.updatedAt }); + } + } + setEntMap(newEntMap); + } catch (e) { + setEntMap(new Map()); + } finally { + setLoading(false); + } + }; + load(); + }, [target, lang, shouldLoad]); + + return ( +
+
+

Single Ent.

+ +
+
+
+ {loading ? ( +
Loading single entry data...
+ ) : entMap.size === 0 ? ( +
No data found.
+ ) : ( +
+ {Array.from(entMap.entries()).map(([key, { ent, firstSeen }]) => ( +
+
+
+
+ + First seen: {DateTime.fromISO(firstSeen).toFormat('yyyy/MM/dd HH:mm')} + + {ent.need_token && Auth} +
+
+ + + Version + +

+ MD5: {ent.version_md5} +

+
+ {ent.button_url && ( +
+ +
+
+ Button +

Normal

+
+ {ent.button_hover_url && ( +
+ Button Hover +

Hover

+
+ )} +
+
+ )} + {ent.jump_url && ( + + Jump URL + + )} +
+
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/pages-v2/src/legacy/api.ts b/pages-v2/src/legacy/api.ts new file mode 100644 index 0000000..f43288f --- /dev/null +++ b/pages-v2/src/legacy/api.ts @@ -0,0 +1,51 @@ +import ky from 'ky'; +import { BASE_URL, gameTargets, launcherTargets, launcherWebApiLang } from './utils/constants.js'; + +const apiCache = new Map>(); + +export function fetchJson(url: string): Promise { + if (!apiCache.has(url)) { + const promise = ky + .get(url) + .json() + .catch((err) => { + apiCache.delete(url); + throw err; + }); + apiCache.set(url, promise); + } + return apiCache.get(url) as Promise; +} + +export async function preloadData() { + const promises: Promise[] = []; + promises.push(fetchJson(`${BASE_URL}/mirror_file_list.json`)); + const launcherWebApiFolderNames = ['announcement', 'banner', 'main_bg_image', 'sidebar', 'single_ent']; + for (const target of gameTargets) { + promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all.json`)); + promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all_patch.json`)); + for (const apiName of launcherWebApiFolderNames) { + for (const lang of launcherWebApiLang[target.region]) { + promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/web/${target.dirName}/${apiName}/${lang}/all.json`)); + } + } + } + const resTargets = [ + { region: 'os', channel: 6 }, + { region: 'cn', channel: 1 }, + ]; + const platforms = ['Windows', 'Android', 'iOS', 'PlayStation']; + for (const target of resTargets) { + for (const platform of platforms) { + promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/game_resources/${target.channel}/${platform}/all.json`)); + } + } + for (const region of launcherTargets) { + for (const app of region.apps) { + promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/launcher/${app}/${region.channel}/all.json`)); + promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`)); + } + } + + await Promise.all(promises); +} diff --git a/pages-v2/src/legacy/essentials.ts b/pages-v2/src/legacy/essentials.ts new file mode 100644 index 0000000..25c33c4 --- /dev/null +++ b/pages-v2/src/legacy/essentials.ts @@ -0,0 +1,16 @@ +import 'bootstrap'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'bootstrap-icons/font/bootstrap-icons.min.css'; + +document.addEventListener('DOMContentLoaded', () => { + const setTheme = (theme: string) => { + document.documentElement.setAttribute('data-bs-theme', theme); + }; + const getPreferredTheme = (): 'light' | 'dark' => { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + }; + setTheme(getPreferredTheme()); + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + setTheme(getPreferredTheme()); + }); +}); diff --git a/pages-v2/src/legacy/gamePackages.ts b/pages-v2/src/legacy/gamePackages.ts new file mode 100644 index 0000000..7186f3b --- /dev/null +++ b/pages-v2/src/legacy/gamePackages.ts @@ -0,0 +1,78 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../api.js'; +import type { MirrorFileEntry, StoredData } from '../types.js'; +import { BASE_URL, FILE_SIZE_OPTS, gameTargets } from '../utils/constants.js'; +import math from '../utils/math.js'; +import { generateDownloadLinks } from '../utils/ui.js'; + +export async function renderGamePackages(container: HTMLElement, mirrorFileDb: MirrorFileEntry[]) { + for (const target of gameTargets) { + const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all.json`; + try { + const data = await fetchJson[]>(url); + const section = document.createElement('div'); + section.className = 'mb-5'; + section.innerHTML = `

${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}

`; + + const accordion = document.createElement('div'); + accordion.className = 'accordion'; + accordion.id = `accordion-game-${target.dirName}`; + + // Reverse order to show latest first + const list = [...data].reverse(); + for (let i = 0; i < list.length; i++) { + const e = list[i]; + if (!e) continue; + const version = e.rsp.version; + const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + const packedSize = math.arrayTotal(e.rsp.pkg.packs.map((f: any) => parseInt(f.package_size))); + const unpackedSize = parseInt(e.rsp.pkg.total_size) - packedSize; + + let rows = ''; + const fileName = (f: any) => new URL(f.url).pathname.split('/').pop() ?? ''; + for (const f of e.rsp.pkg.packs) { + rows += ` + ${fileName(f)} + ${f.md5} + ${math.formatFileSize(parseInt(f.package_size), FILE_SIZE_OPTS)} + ${generateDownloadLinks(f.url, mirrorFileDb)} + `; + } + + const itemId = `game-${target.dirName}-${i}`; + const isExpanded = false; + const item = document.createElement('div'); + item.className = 'accordion-item'; + item.innerHTML = ` +

+ +

+
+
+ + + +
Unpacked Size${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)}
Packed Size${math.formatFileSize(packedSize, FILE_SIZE_OPTS)}
+
+ + + ${rows} +
FileMD5 ChecksumSizeDL
+
+
+
+ `; + accordion.appendChild(item); + } + section.appendChild(accordion); + container.appendChild(section); + } catch (err) { + // Ignore 404 or errors + } + } +} diff --git a/pages-v2/src/legacy/index.ts b/pages-v2/src/legacy/index.ts new file mode 100644 index 0000000..d37f100 --- /dev/null +++ b/pages-v2/src/legacy/index.ts @@ -0,0 +1,57 @@ +import { fetchJson, preloadData } from './api.js'; +import { renderGamePackages } from './renderers/gamePackages.js'; +import { renderLaunchers } from './renderers/launchers.js'; +import { renderOverview } from './renderers/overview.js'; +import { renderPatches } from './renderers/patches.js'; +import { renderResources } from './renderers/resources.js'; +import { renderWebPretty } from './renderers/webPretty.js'; +import type { MirrorFileEntry } from './types.js'; +import { BASE_URL } from './utils/constants.js'; + +document.addEventListener('DOMContentLoaded', () => { + main(); +}); + +let mirrorFileDb: MirrorFileEntry[] = []; + +async function main() { + const contentDiv = document.getElementById('content'); + if (!contentDiv) return; + + await preloadData(); + + try { + mirrorFileDb = await fetchJson(`${BASE_URL}/mirror_file_list.json`); + } catch (e) { + console.warn('Failed to fetch mirror list', e); + } + + const tabsHtml = ` + +
+
+
+
+
+
+
+
+ `; + contentDiv.innerHTML = tabsHtml; + + await Promise.all([ + renderOverview(document.getElementById('tab-overview')!, mirrorFileDb), + renderGamePackages(document.getElementById('tab-game')!, mirrorFileDb), + renderPatches(document.getElementById('tab-patch')!, mirrorFileDb), + renderResources(document.getElementById('tab-resources')!), + renderLaunchers(document.getElementById('tab-launcher')!, mirrorFileDb), + renderWebPretty(document.getElementById('tab-web-pretty')!), + ]); +} diff --git a/pages-v2/src/legacy/launchers.ts b/pages-v2/src/legacy/launchers.ts new file mode 100644 index 0000000..b3cc0e6 --- /dev/null +++ b/pages-v2/src/legacy/launchers.ts @@ -0,0 +1,137 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../api.js'; +import type { MirrorFileEntry, StoredData } from '../types.js'; +import { BASE_URL, FILE_SIZE_OPTS, launcherTargets } from '../utils/constants.js'; +import math from '../utils/math.js'; +import { generateDownloadLinks } from '../utils/ui.js'; + +export async function renderLaunchers(container: HTMLElement, mirrorFileDb: MirrorFileEntry[]) { + for (const region of launcherTargets) { + for (const app of region.apps) { + const section = document.createElement('div'); + section.className = 'mb-5'; + section.innerHTML = `

${region.id.toUpperCase()} ${app}

`; + + const accordion = document.createElement('div'); + accordion.className = 'accordion'; + accordion.id = `accordion-launcher-${region.id}-${app}`; + let itemIndex = 0; + + // Zip + try { + const urlZip = `${BASE_URL}/akEndfield/launcher/launcher/${app}/${region.channel}/all.json`; + const dataZip = await fetchJson[]>(urlZip); + + let rows = ''; + for (const e of [...dataZip].reverse()) { + const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + const fileName = new URL(e.rsp.zip_package_url).pathname.split('/').pop() ?? ''; + const unpacked = parseInt(e.rsp.total_size) - parseInt(e.rsp.package_size); + + rows += ` + ${dateStr} + ${e.rsp.version} + ${fileName} + ${e.rsp.md5} + ${math.formatFileSize(unpacked, FILE_SIZE_OPTS)} + ${math.formatFileSize(parseInt(e.rsp.package_size), FILE_SIZE_OPTS)} + ${generateDownloadLinks(e.rsp.zip_package_url, mirrorFileDb)} + `; + } + + const itemId = `launcher-zip-${region.id}-${app}`; + const isExpanded = false; + itemIndex++; + + const item = document.createElement('div'); + item.className = 'accordion-item'; + item.innerHTML = ` +

+ +

+
+
+
+ + + + + + + + + + + + + ${rows} +
DateVersionFileMD5 ChecksumUnpackedPackedDL
+
+
+
+ `; + accordion.appendChild(item); + } catch (e) {} + + // Exe + try { + const urlExe = `${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`; + const dataExe = await fetchJson[]>(urlExe); + + let rows = ''; + for (const e of [...dataExe].reverse()) { + const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + const fileName = new URL(e.rsp.exe_url).pathname.split('/').pop() ?? ''; + + rows += ` + ${dateStr} + ${e.rsp.version} + ${fileName} + ${math.formatFileSize(parseInt(e.rsp.exe_size), FILE_SIZE_OPTS)} + ${generateDownloadLinks(e.rsp.exe_url, mirrorFileDb)} + `; + } + + const itemId = `launcher-exe-${region.id}-${app}`; + const isExpanded = false; + itemIndex++; + + const item = document.createElement('div'); + item.className = 'accordion-item'; + item.innerHTML = ` +

+ +

+
+
+
+ + + + + + + + + + + ${rows} +
DateVersionFileSizeDL
+
+
+
+ `; + accordion.appendChild(item); + } catch (e) {} + + if (accordion.childElementCount > 0) { + section.appendChild(accordion); + container.appendChild(section); + } + } + } +} diff --git a/pages-v2/src/legacy/overview.ts b/pages-v2/src/legacy/overview.ts new file mode 100644 index 0000000..1476018 --- /dev/null +++ b/pages-v2/src/legacy/overview.ts @@ -0,0 +1,255 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../api.js'; +import type { MirrorFileEntry, StoredData } from '../types.js'; +import { BASE_URL, FILE_SIZE_OPTS, gameTargets, launcherTargets } from '../utils/constants.js'; +import math from '../utils/math.js'; + +export async function renderOverview(container: HTMLElement, mirrorFileDb: MirrorFileEntry[]) { + const mirrorOrigSet = new Set(); + for (const m of mirrorFileDb) { + try { + const u = new URL(m.orig); + u.search = ''; + mirrorOrigSet.add(u.toString()); + } catch {} + } + + const countedUrls = new Set(); + let totalMirrorSize = 0; + + const checkAndAddSize = (url: string, size: number) => { + if (!url || isNaN(size)) return; + try { + const u = new URL(url); + u.search = ''; + const cleanUrl = u.toString(); + if (countedUrls.has(cleanUrl)) return; + if (mirrorOrigSet.has(cleanUrl)) { + totalMirrorSize += size; + countedUrls.add(cleanUrl); + } + } catch {} + }; + + const section = document.createElement('div'); + const sectionIn = document.createElement('div'); + section.className = 'card mb-3'; + sectionIn.className = 'card-body'; + + const [globalPkg, chinaPkg] = await Promise.all([ + (async () => { + const url = `${BASE_URL}/akEndfield/launcher/game/6/all.json`; + const dat = await fetchJson[]>(url); + const latest = dat.at(-1); + if (!latest) return { version: '---', date: '---' }; + return { + version: latest.rsp.version, + date: DateTime.fromISO(latest.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'), + }; + })(), + (async () => { + const url = `${BASE_URL}/akEndfield/launcher/game/1/all.json`; + const dat = await fetchJson[]>(url); + const latest = dat.at(-1); + if (!latest) return { version: '---', date: '---' }; + return { + version: latest.rsp.version, + date: DateTime.fromISO(latest.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'), + }; + })(), + ]); + + sectionIn.innerHTML = ` +

Latest Game Packages

+
+
+

+ ${globalPkg.version}
+ ${globalPkg.date}
+ Latest Version (Global) +

+
+
+

+ ${chinaPkg.version}
+ ${chinaPkg.date}
+ Latest Version (China) +

+
+
+ `; + + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'table-responsive'; + + const table = document.createElement('table'); + table.className = 'table table-striped table-bordered table-sm align-middle text-nowrap'; + table.innerHTML = ` + + + Region + Channel + Version + Packed + Unpacked + + + + `; + const tbody = table.querySelector('tbody')!; + tableWrapper.appendChild(table); + sectionIn.appendChild(tableWrapper); + section.appendChild(sectionIn); + container.appendChild(section); + + // 1. Game Packages + for (const target of gameTargets) { + const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) continue; + + const latest = data[data.length - 1]; + if (!latest) continue; + const version = latest.rsp.version; + const packedSize = math.arrayTotal(latest.rsp.pkg.packs.map((f: any) => parseInt(f.package_size))); + const totalSize = parseInt(latest.rsp.pkg.total_size); + const unpackedSize = totalSize - packedSize; + + const row = document.createElement('tr'); + row.innerHTML = ` + ${target.region === 'cn' ? 'China' : 'Global'} + ${target.name} + ${version} + ${math.formatFileSize(packedSize, FILE_SIZE_OPTS)} + ${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)} + `; + tbody.appendChild(row); + + for (const entry of data) { + if (entry.rsp.pkg && entry.rsp.pkg.packs) { + for (const pack of entry.rsp.pkg.packs) { + checkAndAddSize(pack.url, parseInt(pack.package_size)); + } + } + } + } catch (e) { + console.warn('Overview: Failed to fetch game data', target.name, e); + } + } + + // 2. Patches + for (const target of gameTargets) { + const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all_patch.json`; + try { + const data = await fetchJson[]>(url); + for (const entry of data) { + if (!entry.rsp.patch) continue; + if (entry.rsp.patch.url) { + checkAndAddSize(entry.rsp.patch.url, parseInt(entry.rsp.patch.package_size)); + } + if (entry.rsp.patch.patches) { + for (const p of entry.rsp.patch.patches) { + checkAndAddSize(p.url, parseInt(p.package_size)); + } + } + } + } catch (e) {} + } + + // 4. Launchers + for (const region of launcherTargets) { + for (const app of region.apps) { + try { + const urlZip = `${BASE_URL}/akEndfield/launcher/launcher/${app}/${region.channel}/all.json`; + const dataZip = await fetchJson[]>(urlZip); + for (const e of dataZip) { + checkAndAddSize(e.rsp.zip_package_url, parseInt(e.rsp.package_size)); + } + } catch (e) {} + try { + const urlExe = `${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`; + const dataExe = await fetchJson[]>(urlExe); + for (const e of dataExe) { + checkAndAddSize(e.rsp.exe_url, parseInt(e.rsp.exe_size)); + } + } catch (e) {} + } + } + + // 3. Latest Game Resources (Global) + { + const resPlatforms = ['Windows', 'Android', 'iOS', 'PlayStation']; + const resData = await Promise.all( + resPlatforms.map(async (p) => { + try { + const url = `${BASE_URL}/akEndfield/launcher/game_resources/6/${p}/all.json`; + const dat = await fetchJson[]>(url); + return dat.at(-1); + } catch { + return undefined; + } + }), + ); + + const resSection = document.createElement('div'); + resSection.className = 'card mb-3'; + const resSectionIn = document.createElement('div'); + resSectionIn.className = 'card-body'; + resSectionIn.innerHTML = ` +

Latest Game Resources

+
+ ${resPlatforms + .map((p, i) => { + const item = resData[i]; + if (!item) { + return ` +
+

+ ---
+ ${p} +

+
+ `; + } + + const version = (() => { + const initialRes = item.rsp.resources.find((e: any) => e.name === 'initial'); + const mainRes = item.rsp.resources.find((e: any) => e.name === 'main'); + if (!initialRes || !mainRes) return '---'; + if (initialRes.version === mainRes.version) return mainRes.version; + return item.rsp.res_version; + })(); + + const dateStr = DateTime.fromISO(item.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + + return ` +
+

+ ${version}
+ ${dateStr}
+ ${p} +

+
+ `; + }) + .join('')} +
+ `; + resSection.appendChild(resSectionIn); + container.appendChild(resSection); + } + + const mirrorSection = document.createElement('div'); + mirrorSection.className = 'card'; + mirrorSection.innerHTML = ` +
+

Mirror Statistics

+

+ ${math.formatFileSize(totalMirrorSize, { ...FILE_SIZE_OPTS, unit: 'G' })}
+ uploaded to mirror +

+
+ `; + container.appendChild(mirrorSection); +} diff --git a/pages-v2/src/legacy/patches.ts b/pages-v2/src/legacy/patches.ts new file mode 100644 index 0000000..013ecea --- /dev/null +++ b/pages-v2/src/legacy/patches.ts @@ -0,0 +1,89 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../api.js'; +import type { MirrorFileEntry, StoredData } from '../types.js'; +import { BASE_URL, FILE_SIZE_OPTS, gameTargets } from '../utils/constants.js'; +import math from '../utils/math.js'; +import { generateDownloadLinks } from '../utils/ui.js'; + +export async function renderPatches(container: HTMLElement, mirrorFileDb: MirrorFileEntry[]) { + for (const target of gameTargets) { + const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all_patch.json`; + try { + const data = await fetchJson[]>(url); + if (data.length === 0) continue; + + const section = document.createElement('div'); + section.className = 'mb-5'; + section.innerHTML = `

${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}

`; + + const accordion = document.createElement('div'); + accordion.className = 'accordion'; + accordion.id = `accordion-patch-${target.dirName}`; + + let itemIndex = 0; + for (const e of [...data].reverse()) { + if (!e.rsp.patch) continue; + const version = e.rsp.version; + const reqVersion = e.rsp.request_version; + const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + const packedSize = math.arrayTotal(e.rsp.patch.patches.map((f: any) => parseInt(f.package_size))); + const unpackedSize = parseInt(e.rsp.patch.total_size) - packedSize; + + let rows = ''; + const fileName = (url: string) => new URL(url).pathname.split('/').pop() ?? ''; + if (e.rsp.patch.url) { + rows += ` + ${fileName(e.rsp.patch.url)} + ${e.rsp.patch.md5} + ${math.formatFileSize(parseInt(e.rsp.patch.package_size), FILE_SIZE_OPTS)} + ${generateDownloadLinks(e.rsp.patch.url, mirrorFileDb)} + `; + } + for (const f of e.rsp.patch.patches) { + rows += ` + ${fileName(f.url)} + ${f.md5} + ${math.formatFileSize(parseInt(f.package_size), FILE_SIZE_OPTS)} + ${generateDownloadLinks(f.url, mirrorFileDb)} + `; + } + + const itemId = `patch-${target.dirName}-${itemIndex}`; + const isExpanded = false; + itemIndex++; + + const item = document.createElement('div'); + item.className = 'accordion-item'; + item.innerHTML = ` +

+ +

+
+
+ + + +
Unpacked Size${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)}
Packed Size${math.formatFileSize(packedSize, FILE_SIZE_OPTS)}
+
+ + + ${rows} +
FileMD5 ChecksumSizeDL
+
+
+
+ `; + accordion.appendChild(item); + } + section.appendChild(accordion); + container.appendChild(section); + } catch (err) { + // Ignore + } + } +} diff --git a/pages-v2/src/legacy/renderers/gamePackages.ts b/pages-v2/src/legacy/renderers/gamePackages.ts new file mode 100644 index 0000000..7186f3b --- /dev/null +++ b/pages-v2/src/legacy/renderers/gamePackages.ts @@ -0,0 +1,78 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../api.js'; +import type { MirrorFileEntry, StoredData } from '../types.js'; +import { BASE_URL, FILE_SIZE_OPTS, gameTargets } from '../utils/constants.js'; +import math from '../utils/math.js'; +import { generateDownloadLinks } from '../utils/ui.js'; + +export async function renderGamePackages(container: HTMLElement, mirrorFileDb: MirrorFileEntry[]) { + for (const target of gameTargets) { + const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all.json`; + try { + const data = await fetchJson[]>(url); + const section = document.createElement('div'); + section.className = 'mb-5'; + section.innerHTML = `

${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}

`; + + const accordion = document.createElement('div'); + accordion.className = 'accordion'; + accordion.id = `accordion-game-${target.dirName}`; + + // Reverse order to show latest first + const list = [...data].reverse(); + for (let i = 0; i < list.length; i++) { + const e = list[i]; + if (!e) continue; + const version = e.rsp.version; + const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + const packedSize = math.arrayTotal(e.rsp.pkg.packs.map((f: any) => parseInt(f.package_size))); + const unpackedSize = parseInt(e.rsp.pkg.total_size) - packedSize; + + let rows = ''; + const fileName = (f: any) => new URL(f.url).pathname.split('/').pop() ?? ''; + for (const f of e.rsp.pkg.packs) { + rows += ` + ${fileName(f)} + ${f.md5} + ${math.formatFileSize(parseInt(f.package_size), FILE_SIZE_OPTS)} + ${generateDownloadLinks(f.url, mirrorFileDb)} + `; + } + + const itemId = `game-${target.dirName}-${i}`; + const isExpanded = false; + const item = document.createElement('div'); + item.className = 'accordion-item'; + item.innerHTML = ` +

+ +

+
+
+ + + +
Unpacked Size${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)}
Packed Size${math.formatFileSize(packedSize, FILE_SIZE_OPTS)}
+
+ + + ${rows} +
FileMD5 ChecksumSizeDL
+
+
+
+ `; + accordion.appendChild(item); + } + section.appendChild(accordion); + container.appendChild(section); + } catch (err) { + // Ignore 404 or errors + } + } +} diff --git a/pages-v2/src/legacy/renderers/launchers.ts b/pages-v2/src/legacy/renderers/launchers.ts new file mode 100644 index 0000000..b3cc0e6 --- /dev/null +++ b/pages-v2/src/legacy/renderers/launchers.ts @@ -0,0 +1,137 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../api.js'; +import type { MirrorFileEntry, StoredData } from '../types.js'; +import { BASE_URL, FILE_SIZE_OPTS, launcherTargets } from '../utils/constants.js'; +import math from '../utils/math.js'; +import { generateDownloadLinks } from '../utils/ui.js'; + +export async function renderLaunchers(container: HTMLElement, mirrorFileDb: MirrorFileEntry[]) { + for (const region of launcherTargets) { + for (const app of region.apps) { + const section = document.createElement('div'); + section.className = 'mb-5'; + section.innerHTML = `

${region.id.toUpperCase()} ${app}

`; + + const accordion = document.createElement('div'); + accordion.className = 'accordion'; + accordion.id = `accordion-launcher-${region.id}-${app}`; + let itemIndex = 0; + + // Zip + try { + const urlZip = `${BASE_URL}/akEndfield/launcher/launcher/${app}/${region.channel}/all.json`; + const dataZip = await fetchJson[]>(urlZip); + + let rows = ''; + for (const e of [...dataZip].reverse()) { + const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + const fileName = new URL(e.rsp.zip_package_url).pathname.split('/').pop() ?? ''; + const unpacked = parseInt(e.rsp.total_size) - parseInt(e.rsp.package_size); + + rows += ` + ${dateStr} + ${e.rsp.version} + ${fileName} + ${e.rsp.md5} + ${math.formatFileSize(unpacked, FILE_SIZE_OPTS)} + ${math.formatFileSize(parseInt(e.rsp.package_size), FILE_SIZE_OPTS)} + ${generateDownloadLinks(e.rsp.zip_package_url, mirrorFileDb)} + `; + } + + const itemId = `launcher-zip-${region.id}-${app}`; + const isExpanded = false; + itemIndex++; + + const item = document.createElement('div'); + item.className = 'accordion-item'; + item.innerHTML = ` +

+ +

+
+
+
+ + + + + + + + + + + + + ${rows} +
DateVersionFileMD5 ChecksumUnpackedPackedDL
+
+
+
+ `; + accordion.appendChild(item); + } catch (e) {} + + // Exe + try { + const urlExe = `${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`; + const dataExe = await fetchJson[]>(urlExe); + + let rows = ''; + for (const e of [...dataExe].reverse()) { + const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + const fileName = new URL(e.rsp.exe_url).pathname.split('/').pop() ?? ''; + + rows += ` + ${dateStr} + ${e.rsp.version} + ${fileName} + ${math.formatFileSize(parseInt(e.rsp.exe_size), FILE_SIZE_OPTS)} + ${generateDownloadLinks(e.rsp.exe_url, mirrorFileDb)} + `; + } + + const itemId = `launcher-exe-${region.id}-${app}`; + const isExpanded = false; + itemIndex++; + + const item = document.createElement('div'); + item.className = 'accordion-item'; + item.innerHTML = ` +

+ +

+
+
+
+ + + + + + + + + + + ${rows} +
DateVersionFileSizeDL
+
+
+
+ `; + accordion.appendChild(item); + } catch (e) {} + + if (accordion.childElementCount > 0) { + section.appendChild(accordion); + container.appendChild(section); + } + } + } +} diff --git a/pages-v2/src/legacy/renderers/overview.ts b/pages-v2/src/legacy/renderers/overview.ts new file mode 100644 index 0000000..1476018 --- /dev/null +++ b/pages-v2/src/legacy/renderers/overview.ts @@ -0,0 +1,255 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../api.js'; +import type { MirrorFileEntry, StoredData } from '../types.js'; +import { BASE_URL, FILE_SIZE_OPTS, gameTargets, launcherTargets } from '../utils/constants.js'; +import math from '../utils/math.js'; + +export async function renderOverview(container: HTMLElement, mirrorFileDb: MirrorFileEntry[]) { + const mirrorOrigSet = new Set(); + for (const m of mirrorFileDb) { + try { + const u = new URL(m.orig); + u.search = ''; + mirrorOrigSet.add(u.toString()); + } catch {} + } + + const countedUrls = new Set(); + let totalMirrorSize = 0; + + const checkAndAddSize = (url: string, size: number) => { + if (!url || isNaN(size)) return; + try { + const u = new URL(url); + u.search = ''; + const cleanUrl = u.toString(); + if (countedUrls.has(cleanUrl)) return; + if (mirrorOrigSet.has(cleanUrl)) { + totalMirrorSize += size; + countedUrls.add(cleanUrl); + } + } catch {} + }; + + const section = document.createElement('div'); + const sectionIn = document.createElement('div'); + section.className = 'card mb-3'; + sectionIn.className = 'card-body'; + + const [globalPkg, chinaPkg] = await Promise.all([ + (async () => { + const url = `${BASE_URL}/akEndfield/launcher/game/6/all.json`; + const dat = await fetchJson[]>(url); + const latest = dat.at(-1); + if (!latest) return { version: '---', date: '---' }; + return { + version: latest.rsp.version, + date: DateTime.fromISO(latest.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'), + }; + })(), + (async () => { + const url = `${BASE_URL}/akEndfield/launcher/game/1/all.json`; + const dat = await fetchJson[]>(url); + const latest = dat.at(-1); + if (!latest) return { version: '---', date: '---' }; + return { + version: latest.rsp.version, + date: DateTime.fromISO(latest.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'), + }; + })(), + ]); + + sectionIn.innerHTML = ` +

Latest Game Packages

+
+
+

+ ${globalPkg.version}
+ ${globalPkg.date}
+ Latest Version (Global) +

+
+
+

+ ${chinaPkg.version}
+ ${chinaPkg.date}
+ Latest Version (China) +

+
+
+ `; + + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'table-responsive'; + + const table = document.createElement('table'); + table.className = 'table table-striped table-bordered table-sm align-middle text-nowrap'; + table.innerHTML = ` + + + Region + Channel + Version + Packed + Unpacked + + + + `; + const tbody = table.querySelector('tbody')!; + tableWrapper.appendChild(table); + sectionIn.appendChild(tableWrapper); + section.appendChild(sectionIn); + container.appendChild(section); + + // 1. Game Packages + for (const target of gameTargets) { + const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) continue; + + const latest = data[data.length - 1]; + if (!latest) continue; + const version = latest.rsp.version; + const packedSize = math.arrayTotal(latest.rsp.pkg.packs.map((f: any) => parseInt(f.package_size))); + const totalSize = parseInt(latest.rsp.pkg.total_size); + const unpackedSize = totalSize - packedSize; + + const row = document.createElement('tr'); + row.innerHTML = ` + ${target.region === 'cn' ? 'China' : 'Global'} + ${target.name} + ${version} + ${math.formatFileSize(packedSize, FILE_SIZE_OPTS)} + ${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)} + `; + tbody.appendChild(row); + + for (const entry of data) { + if (entry.rsp.pkg && entry.rsp.pkg.packs) { + for (const pack of entry.rsp.pkg.packs) { + checkAndAddSize(pack.url, parseInt(pack.package_size)); + } + } + } + } catch (e) { + console.warn('Overview: Failed to fetch game data', target.name, e); + } + } + + // 2. Patches + for (const target of gameTargets) { + const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all_patch.json`; + try { + const data = await fetchJson[]>(url); + for (const entry of data) { + if (!entry.rsp.patch) continue; + if (entry.rsp.patch.url) { + checkAndAddSize(entry.rsp.patch.url, parseInt(entry.rsp.patch.package_size)); + } + if (entry.rsp.patch.patches) { + for (const p of entry.rsp.patch.patches) { + checkAndAddSize(p.url, parseInt(p.package_size)); + } + } + } + } catch (e) {} + } + + // 4. Launchers + for (const region of launcherTargets) { + for (const app of region.apps) { + try { + const urlZip = `${BASE_URL}/akEndfield/launcher/launcher/${app}/${region.channel}/all.json`; + const dataZip = await fetchJson[]>(urlZip); + for (const e of dataZip) { + checkAndAddSize(e.rsp.zip_package_url, parseInt(e.rsp.package_size)); + } + } catch (e) {} + try { + const urlExe = `${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`; + const dataExe = await fetchJson[]>(urlExe); + for (const e of dataExe) { + checkAndAddSize(e.rsp.exe_url, parseInt(e.rsp.exe_size)); + } + } catch (e) {} + } + } + + // 3. Latest Game Resources (Global) + { + const resPlatforms = ['Windows', 'Android', 'iOS', 'PlayStation']; + const resData = await Promise.all( + resPlatforms.map(async (p) => { + try { + const url = `${BASE_URL}/akEndfield/launcher/game_resources/6/${p}/all.json`; + const dat = await fetchJson[]>(url); + return dat.at(-1); + } catch { + return undefined; + } + }), + ); + + const resSection = document.createElement('div'); + resSection.className = 'card mb-3'; + const resSectionIn = document.createElement('div'); + resSectionIn.className = 'card-body'; + resSectionIn.innerHTML = ` +

Latest Game Resources

+
+ ${resPlatforms + .map((p, i) => { + const item = resData[i]; + if (!item) { + return ` +
+

+ ---
+ ${p} +

+
+ `; + } + + const version = (() => { + const initialRes = item.rsp.resources.find((e: any) => e.name === 'initial'); + const mainRes = item.rsp.resources.find((e: any) => e.name === 'main'); + if (!initialRes || !mainRes) return '---'; + if (initialRes.version === mainRes.version) return mainRes.version; + return item.rsp.res_version; + })(); + + const dateStr = DateTime.fromISO(item.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + + return ` +
+

+ ${version}
+ ${dateStr}
+ ${p} +

+
+ `; + }) + .join('')} +
+ `; + resSection.appendChild(resSectionIn); + container.appendChild(resSection); + } + + const mirrorSection = document.createElement('div'); + mirrorSection.className = 'card'; + mirrorSection.innerHTML = ` +
+

Mirror Statistics

+

+ ${math.formatFileSize(totalMirrorSize, { ...FILE_SIZE_OPTS, unit: 'G' })}
+ uploaded to mirror +

+
+ `; + container.appendChild(mirrorSection); +} diff --git a/pages-v2/src/legacy/renderers/patches.ts b/pages-v2/src/legacy/renderers/patches.ts new file mode 100644 index 0000000..013ecea --- /dev/null +++ b/pages-v2/src/legacy/renderers/patches.ts @@ -0,0 +1,89 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../api.js'; +import type { MirrorFileEntry, StoredData } from '../types.js'; +import { BASE_URL, FILE_SIZE_OPTS, gameTargets } from '../utils/constants.js'; +import math from '../utils/math.js'; +import { generateDownloadLinks } from '../utils/ui.js'; + +export async function renderPatches(container: HTMLElement, mirrorFileDb: MirrorFileEntry[]) { + for (const target of gameTargets) { + const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all_patch.json`; + try { + const data = await fetchJson[]>(url); + if (data.length === 0) continue; + + const section = document.createElement('div'); + section.className = 'mb-5'; + section.innerHTML = `

${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}

`; + + const accordion = document.createElement('div'); + accordion.className = 'accordion'; + accordion.id = `accordion-patch-${target.dirName}`; + + let itemIndex = 0; + for (const e of [...data].reverse()) { + if (!e.rsp.patch) continue; + const version = e.rsp.version; + const reqVersion = e.rsp.request_version; + const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + const packedSize = math.arrayTotal(e.rsp.patch.patches.map((f: any) => parseInt(f.package_size))); + const unpackedSize = parseInt(e.rsp.patch.total_size) - packedSize; + + let rows = ''; + const fileName = (url: string) => new URL(url).pathname.split('/').pop() ?? ''; + if (e.rsp.patch.url) { + rows += ` + ${fileName(e.rsp.patch.url)} + ${e.rsp.patch.md5} + ${math.formatFileSize(parseInt(e.rsp.patch.package_size), FILE_SIZE_OPTS)} + ${generateDownloadLinks(e.rsp.patch.url, mirrorFileDb)} + `; + } + for (const f of e.rsp.patch.patches) { + rows += ` + ${fileName(f.url)} + ${f.md5} + ${math.formatFileSize(parseInt(f.package_size), FILE_SIZE_OPTS)} + ${generateDownloadLinks(f.url, mirrorFileDb)} + `; + } + + const itemId = `patch-${target.dirName}-${itemIndex}`; + const isExpanded = false; + itemIndex++; + + const item = document.createElement('div'); + item.className = 'accordion-item'; + item.innerHTML = ` +

+ +

+
+
+ + + +
Unpacked Size${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)}
Packed Size${math.formatFileSize(packedSize, FILE_SIZE_OPTS)}
+
+ + + ${rows} +
FileMD5 ChecksumSizeDL
+
+
+
+ `; + accordion.appendChild(item); + } + section.appendChild(accordion); + container.appendChild(section); + } catch (err) { + // Ignore + } + } +} diff --git a/pages-v2/src/legacy/renderers/resources.ts b/pages-v2/src/legacy/renderers/resources.ts new file mode 100644 index 0000000..1c8a883 --- /dev/null +++ b/pages-v2/src/legacy/renderers/resources.ts @@ -0,0 +1,119 @@ +import { DateTime } from 'luxon'; +import * as semver from 'semver'; +import { fetchJson } from '../api.js'; +import type { StoredData } from '../types.js'; +import { BASE_URL } from '../utils/constants.js'; + +export async function renderResources(container: HTMLElement) { + const platforms = ['Windows', 'Android', 'iOS', 'PlayStation']; + const targets = [ + { region: 'os', channel: 6 }, + { region: 'cn', channel: 1 }, + ]; + + for (const target of targets) { + const section = document.createElement('div'); + section.className = 'mb-5'; + section.innerHTML = `

${target.region === 'cn' ? 'China' : 'Global'}

`; + + const accordion = document.createElement('div'); + accordion.className = 'accordion'; + accordion.id = `accordion-res-${target.region}-${target.channel}`; + let itemIndex = 0; + + for (const platform of platforms) { + const url = `${BASE_URL}/akEndfield/launcher/game_resources/${target.channel}/${platform}/all.json`; + try { + const data = await fetchJson[]>(url); + + // Group by res_version + const resVersionMap = new Map; versions: Set }>(); + for (const e of data) { + const resVer = e.rsp.res_version; + if (!resVersionMap.has(resVer)) { + resVersionMap.set(resVer, { rsp: e, versions: new Set() }); + } + resVersionMap.get(resVer)!.versions.add(e.req.version); + } + + const resVersionSet = Array.from(resVersionMap.values()).map((d) => ({ + resVersion: d.rsp.rsp.res_version, + rsp: d.rsp, + versions: Array.from(d.versions).sort(semver.rcompare), + })); + + const sortedSet = resVersionSet.reverse(); + let rows = ''; + for (let i = 0; i < sortedSet.length; i++) { + const item = sortedSet[i]!; + const nextItem = sortedSet[i + 1]; + // Newest first + const currentDate = DateTime.fromISO(item.rsp.updatedAt); + const dateStr = currentDate.toFormat('yyyy/MM/dd HH:mm:ss'); + + const intervalStr = (() => { + if (nextItem) { + const nextDate = DateTime.fromISO(nextItem.rsp.updatedAt); + const diff = currentDate.diff(nextDate); + return diff.toFormat('dd:hh:mm:ss'); + } + return '-'; + })(); + + const initialRes = item.rsp.rsp.resources.find((e: any) => e.name === 'initial'); + const mainRes = item.rsp.rsp.resources.find((e: any) => e.name === 'main'); + const isKick = JSON.parse(item.rsp.rsp.configs).kick_flag === true; + + rows += ` + ${dateStr} + ${intervalStr} + ${initialRes.version} + ${mainRes.version} + ${isKick ? '✅' : ''} + ${item.versions.join(', ')} + `; + } + + const itemId = `res-${target.region}-${target.channel}-${platform}`; + const isExpanded = false; + itemIndex++; + + const item = document.createElement('div'); + item.className = 'accordion-item'; + item.innerHTML = ` +

+ +

+
+
+
+ + + + + + + + + + + + ${rows} +
DateIntervalInitialMainKickGame version
+
+
+
+ `; + accordion.appendChild(item); + } catch (err) { + // Ignore + } + } + if (accordion.childElementCount > 0) { + section.appendChild(accordion); + container.appendChild(section); + } + } +} diff --git a/pages-v2/src/legacy/renderers/web.ts b/pages-v2/src/legacy/renderers/web.ts new file mode 100644 index 0000000..8919f92 --- /dev/null +++ b/pages-v2/src/legacy/renderers/web.ts @@ -0,0 +1,157 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../api.js'; +import type { StoredData } from '../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../utils/constants.js'; + +const apiTypes = ['announcement', 'banner', 'main_bg_image', 'sidebar', 'single_ent']; + +export async function renderWeb(container: HTMLElement) { + for (const target of gameTargets) { + const section = document.createElement('div'); + section.className = 'mb-5'; + section.innerHTML = `

${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}

`; + + const langs = launcherWebApiLang[target.region] || []; + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + + // Language Selector + const langSelectGroup = document.createElement('div'); + langSelectGroup.className = 'input-group mb-3'; + langSelectGroup.innerHTML = 'Language'; + + const langSelect = document.createElement('select'); + langSelect.className = 'form-select'; + + langs.forEach((lang) => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + if (lang === defaultLang) { + option.selected = true; + } + langSelect.appendChild(option); + }); + langSelectGroup.appendChild(langSelect); + + if (langs.length <= 1) { + langSelectGroup.style.display = 'none'; + } + + section.appendChild(langSelectGroup); + + const accordion = document.createElement('div'); + accordion.className = 'accordion'; + accordion.id = `accordion-web-${target.dirName}`; + + const renderApiList = async (lang: string) => { + accordion.innerHTML = '
Loading...
'; + + const results = await Promise.all( + apiTypes.map(async (apiType) => { + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/${apiType}/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) return null; + return { apiType, list: [...data].reverse() }; + } catch (e) { + console.warn(`Failed to load ${url}`, e); + return null; + } + }), + ); + + accordion.innerHTML = ''; + const validResults = results.filter((r): r is NonNullable => r !== null); + + if (validResults.length === 0) { + accordion.innerHTML = '
No data found.
'; + return; + } + + validResults.forEach(({ apiType, list }, idx) => { + const itemId = `web-${target.dirName}-${lang}-${apiType}`; + + const item = document.createElement('div'); + item.className = 'accordion-item'; + + // Header + const header = document.createElement('h2'); + header.className = 'accordion-header'; + header.id = `heading-${itemId}`; + header.innerHTML = ` + + `; + item.appendChild(header); + + // Body + const collapse = document.createElement('div'); + collapse.id = `collapse-${itemId}`; + collapse.className = 'accordion-collapse collapse'; + collapse.setAttribute('aria-labelledby', `heading-${itemId}`); + collapse.setAttribute('data-bs-parent', `#${accordion.id}`); + + const body = document.createElement('div'); + body.className = 'accordion-body'; + + // Select for UpdatedAt + const selectGroup = document.createElement('div'); + selectGroup.className = 'input-group mb-3'; + selectGroup.innerHTML = `History`; + + const select = document.createElement('select'); + select.className = 'form-select'; + select.ariaLabel = 'Select version'; + + list.forEach((entry, idx) => { + const dateStr = DateTime.fromISO(entry.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + const option = document.createElement('option'); + option.value = idx.toString(); + option.textContent = `${dateStr}`; + select.appendChild(option); + }); + selectGroup.appendChild(select); + body.appendChild(selectGroup); + + // Content Area + const contentArea = document.createElement('pre'); + contentArea.className = 'p-3 border rounded overflow-auto'; + contentArea.style.maxHeight = '500px'; + contentArea.style.fontSize = '0.875rem'; + + const updateContent = (index: number) => { + const entry = list[index]; + if (entry) { + contentArea.textContent = JSON.stringify(entry.rsp, null, 2); + } + }; + + // Initial render for this item + updateContent(0); + + select.addEventListener('change', (e) => { + const val = parseInt((e.target as HTMLSelectElement).value, 10); + updateContent(val); + }); + + body.appendChild(contentArea); + collapse.appendChild(body); + item.appendChild(collapse); + accordion.appendChild(item); + }); + }; + + langSelect.addEventListener('change', (e) => { + renderApiList((e.target as HTMLSelectElement).value); + }); + + section.appendChild(accordion); + container.appendChild(section); + + // Initial load + if (defaultLang) { + renderApiList(defaultLang); + } + } +} diff --git a/pages-v2/src/legacy/renderers/webPretty.ts b/pages-v2/src/legacy/renderers/webPretty.ts new file mode 100644 index 0000000..b74c18b --- /dev/null +++ b/pages-v2/src/legacy/renderers/webPretty.ts @@ -0,0 +1,14 @@ +import { renderAnnouncement } from './webPretty/announcement.js'; +import { renderBanner } from './webPretty/banner.js'; +import { renderMainBgImage } from './webPretty/mainBgImage.js'; +import { renderSidebar } from './webPretty/sidebar.js'; +import { renderSingleEnt } from './webPretty/singleEnt.js'; + +export async function renderWebPretty(container: HTMLElement) { + container.innerHTML = ''; + await renderAnnouncement(container); + await renderBanner(container); + await renderMainBgImage(container); + await renderSingleEnt(container); + await renderSidebar(container); +} diff --git a/pages-v2/src/legacy/renderers/webPretty/announcement.ts b/pages-v2/src/legacy/renderers/webPretty/announcement.ts new file mode 100644 index 0000000..9e1d3d1 --- /dev/null +++ b/pages-v2/src/legacy/renderers/webPretty/announcement.ts @@ -0,0 +1,179 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../../api.js'; +import type { LauncherWebAnnouncement, StoredData } from '../../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../../utils/constants.js'; + +export async function renderAnnouncement(container: HTMLElement) { + const outerCard = document.createElement('div'); + outerCard.className = 'card mb-3'; + + const header = document.createElement('div'); + header.className = 'card-header d-flex justify-content-between align-items-center'; + header.style.cursor = 'pointer'; + header.setAttribute('data-bs-toggle', 'collapse'); + header.setAttribute('data-bs-target', '#collapseAnnouncement'); + header.setAttribute('role', 'button'); + header.innerHTML = '

Announcement

'; + outerCard.appendChild(header); + + const collapseDiv = document.createElement('div'); + collapseDiv.id = 'collapseAnnouncement'; + collapseDiv.className = 'collapse'; + outerCard.appendChild(collapseDiv); + + const outerCardBody = document.createElement('div'); + outerCardBody.className = 'card-body'; + collapseDiv.appendChild(outerCardBody); + + // --- UI Controls --- + const controls = document.createElement('div'); + controls.className = 'row g-3 mb-4'; + + const targetCol = document.createElement('div'); + targetCol.className = 'col-md-6'; + targetCol.innerHTML = ''; + const targetSelect = document.createElement('select'); + targetSelect.className = 'form-select'; + gameTargets.forEach((target, idx) => { + const option = document.createElement('option'); + option.value = idx.toString(); + option.textContent = `${target.region === 'cn' ? 'China' : 'Global'} - ${target.name}`; + targetSelect.appendChild(option); + }); + targetCol.appendChild(targetSelect); + + const langCol = document.createElement('div'); + langCol.className = 'col-md-6'; + langCol.innerHTML = ''; + const langSelect = document.createElement('select'); + langSelect.className = 'form-select'; + langCol.appendChild(langSelect); + + controls.appendChild(targetCol); + controls.appendChild(langCol); + outerCardBody.appendChild(controls); + + const contentDiv = document.createElement('div'); + outerCardBody.appendChild(contentDiv); + + // --- Logic --- + const updateLanguages = () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const langs = launcherWebApiLang[target.region] || []; + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + + langSelect.innerHTML = ''; + langs.forEach((lang) => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + if (lang === defaultLang) option.selected = true; + langSelect.appendChild(option); + }); + + langCol.style.display = langs.length <= 1 ? 'none' : 'block'; + }; + + const renderContent = async () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const lang = langSelect.value; + + if (!lang) { + contentDiv.innerHTML = '
No language selected.
'; + return; + } + + contentDiv.innerHTML = '
Loading announcements...
'; + + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/announcement/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) { + contentDiv.innerHTML = '
No data found.
'; + return; + } + + const tabsMap = new Map< + string, + { tabName: string; announcements: Map } + >(); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + for (const entry of sortedData) { + if (!entry.rsp || !entry.rsp.tabs) continue; + for (const tab of entry.rsp.tabs) { + if (!tabsMap.has(tab.tab_id)) { + tabsMap.set(tab.tab_id, { tabName: tab.tabName, announcements: new Map() }); + } + const targetTab = tabsMap.get(tab.tab_id)!; + for (const ann of tab.announcements) { + if (!targetTab.announcements.has(ann.id)) { + targetTab.announcements.set(ann.id, ann); + } + } + } + } + + contentDiv.innerHTML = ''; + if (tabsMap.size === 0) { + contentDiv.innerHTML = '
No announcements found.
'; + return; + } + + for (const [_tabId, tabData] of tabsMap) { + const card = document.createElement('div'); + card.className = 'card mb-4 shadow-sm'; + + const cardHeader = document.createElement('div'); + cardHeader.className = 'card-header bg-secondary text-white fw-bold py-1'; + cardHeader.textContent = tabData.tabName; + card.appendChild(cardHeader); + + const listGroup = document.createElement('ul'); + listGroup.className = 'list-group list-group-flush'; + + const sortedAnnouncements = Array.from(tabData.announcements.values()).sort( + (a, b) => parseInt(b.start_ts, 10) - parseInt(a.start_ts, 10), + ); + + for (const ann of sortedAnnouncements) { + const item = document.createElement('li'); + item.className = 'list-group-item py-2'; + const date = DateTime.fromMillis(parseInt(ann.start_ts, 10)).toFormat('yyyy/MM/dd HH:mm'); + + item.innerHTML = ` +
+ ${date} + ${ann.content} +
+ ${ann.need_token ? 'Auth' : ''} + ${ann.jump_url ? `Link` : ''} + ID:${ann.id} +
+
+ `; + listGroup.appendChild(item); + } + card.appendChild(listGroup); + contentDiv.appendChild(card); + } + } catch (e) { + console.warn(`Failed to load ${url}`, e); + contentDiv.innerHTML = '
Failed to load data.
'; + } + }; + + targetSelect.addEventListener('change', () => { + updateLanguages(); + renderContent(); + }); + langSelect.addEventListener('change', renderContent); + + updateLanguages(); + renderContent(); + container.appendChild(outerCard); +} diff --git a/pages-v2/src/legacy/renderers/webPretty/banner.ts b/pages-v2/src/legacy/renderers/webPretty/banner.ts new file mode 100644 index 0000000..7356452 --- /dev/null +++ b/pages-v2/src/legacy/renderers/webPretty/banner.ts @@ -0,0 +1,175 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../../api.js'; +import type { LauncherWebBanner, StoredData } from '../../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../../utils/constants.js'; + +export async function renderBanner(container: HTMLElement) { + const outerCard = document.createElement('div'); + outerCard.className = 'card mb-3'; + + const header = document.createElement('div'); + header.className = 'card-header d-flex justify-content-between align-items-center'; + header.style.cursor = 'pointer'; + header.setAttribute('data-bs-toggle', 'collapse'); + header.setAttribute('data-bs-target', '#collapseBanner'); + header.setAttribute('role', 'button'); + header.innerHTML = '

Banner

'; + outerCard.appendChild(header); + + const collapseDiv = document.createElement('div'); + collapseDiv.id = 'collapseBanner'; + collapseDiv.className = 'collapse'; + outerCard.appendChild(collapseDiv); + + const outerCardBody = document.createElement('div'); + outerCardBody.className = 'card-body'; + collapseDiv.appendChild(outerCardBody); + + // --- UI Controls --- + const controls = document.createElement('div'); + controls.className = 'row g-3 mb-4'; + + const targetCol = document.createElement('div'); + targetCol.className = 'col-md-6'; + targetCol.innerHTML = ''; + const targetSelect = document.createElement('select'); + targetSelect.className = 'form-select'; + gameTargets.forEach((target, idx) => { + const option = document.createElement('option'); + option.value = idx.toString(); + option.textContent = `${target.region === 'cn' ? 'China' : 'Global'} - ${target.name}`; + targetSelect.appendChild(option); + }); + targetCol.appendChild(targetSelect); + + const langCol = document.createElement('div'); + langCol.className = 'col-md-6'; + langCol.innerHTML = ''; + const langSelect = document.createElement('select'); + langSelect.className = 'form-select'; + langCol.appendChild(langSelect); + + controls.appendChild(targetCol); + controls.appendChild(langCol); + outerCardBody.appendChild(controls); + + const contentDiv = document.createElement('div'); + outerCardBody.appendChild(contentDiv); + + // --- Logic --- + const updateLanguages = () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const langs = launcherWebApiLang[target.region] || []; + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + + langSelect.innerHTML = ''; + langs.forEach((lang) => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + if (lang === defaultLang) option.selected = true; + langSelect.appendChild(option); + }); + + langCol.style.display = langs.length <= 1 ? 'none' : 'block'; + }; + + const getMirrorUrl = (url: string) => { + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } + }; + + const renderContent = async () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const lang = langSelect.value; + + if (!lang) { + contentDiv.innerHTML = '
No language selected.
'; + return; + } + + contentDiv.innerHTML = '
Loading banners...
'; + + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/banner/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) { + contentDiv.innerHTML = '
No data found.
'; + return; + } + + // Collect unique banners by ID from the entire history + const bannerMap = new Map(); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + for (const entry of sortedData) { + if (!entry.rsp || !entry.rsp.banners) continue; + for (const banner of entry.rsp.banners) { + if (!bannerMap.has(banner.id)) { + bannerMap.set(banner.id, { banner, firstSeen: entry.updatedAt }); + } + } + } + + contentDiv.innerHTML = ''; + if (bannerMap.size === 0) { + contentDiv.innerHTML = '
No banners found.
'; + return; + } + + const row = document.createElement('div'); + row.className = 'row row-cols-1 row-cols-md-3 row-cols-lg-4 g-3'; + contentDiv.appendChild(row); + + for (const [id, { banner, firstSeen }] of bannerMap) { + const col = document.createElement('div'); + col.className = 'col'; + + const dateStr = DateTime.fromISO(firstSeen).toFormat('yyyy/MM/dd HH:mm'); + const mirrorUrl = getMirrorUrl(banner.url); + const linkUrl = banner.jump_url || mirrorUrl; + + col.innerHTML = ` + +
+
+ Banner Image +
+ ${banner.need_token ? 'Auth' : ''} +
+
+
+
+ ID: ${id} + ${dateStr} +
+
+
+
+ `; + row.appendChild(col); + } + } catch (e) { + console.warn(`Failed to load ${url}`, e); + contentDiv.innerHTML = '
Failed to load data.
'; + } + }; + + targetSelect.addEventListener('change', () => { + updateLanguages(); + renderContent(); + }); + langSelect.addEventListener('change', renderContent); + + updateLanguages(); + renderContent(); + container.appendChild(outerCard); +} diff --git a/pages-v2/src/legacy/renderers/webPretty/mainBgImage.ts b/pages-v2/src/legacy/renderers/webPretty/mainBgImage.ts new file mode 100644 index 0000000..f815104 --- /dev/null +++ b/pages-v2/src/legacy/renderers/webPretty/mainBgImage.ts @@ -0,0 +1,174 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../../api.js'; +import type { LauncherWebMainBgImage, StoredData } from '../../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../../utils/constants.js'; + +export async function renderMainBgImage(container: HTMLElement) { + const outerCard = document.createElement('div'); + outerCard.className = 'card mb-3'; + + const header = document.createElement('div'); + header.className = 'card-header d-flex justify-content-between align-items-center'; + header.style.cursor = 'pointer'; + header.setAttribute('data-bs-toggle', 'collapse'); + header.setAttribute('data-bs-target', '#collapseMainBgImage'); + header.setAttribute('role', 'button'); + header.innerHTML = '

Main Background Image

'; + outerCard.appendChild(header); + + const collapseDiv = document.createElement('div'); + collapseDiv.id = 'collapseMainBgImage'; + collapseDiv.className = 'collapse'; + outerCard.appendChild(collapseDiv); + + const outerCardBody = document.createElement('div'); + outerCardBody.className = 'card-body'; + collapseDiv.appendChild(outerCardBody); + + // --- UI Controls --- + const controls = document.createElement('div'); + controls.className = 'row g-3 mb-4'; + + const targetCol = document.createElement('div'); + targetCol.className = 'col-md-6'; + targetCol.innerHTML = ''; + const targetSelect = document.createElement('select'); + targetSelect.className = 'form-select'; + gameTargets.forEach((target, idx) => { + const option = document.createElement('option'); + option.value = idx.toString(); + option.textContent = `${target.region === 'cn' ? 'China' : 'Global'} - ${target.name}`; + targetSelect.appendChild(option); + }); + targetCol.appendChild(targetSelect); + + const langCol = document.createElement('div'); + langCol.className = 'col-md-6'; + langCol.innerHTML = ''; + const langSelect = document.createElement('select'); + langSelect.className = 'form-select'; + langCol.appendChild(langSelect); + + controls.appendChild(targetCol); + controls.appendChild(langCol); + outerCardBody.appendChild(controls); + + const contentDiv = document.createElement('div'); + outerCardBody.appendChild(contentDiv); + + // --- Logic --- + const updateLanguages = () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const langs = launcherWebApiLang[target.region] || []; + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + + langSelect.innerHTML = ''; + langs.forEach((lang) => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + if (lang === defaultLang) option.selected = true; + langSelect.appendChild(option); + }); + + langCol.style.display = langs.length <= 1 ? 'none' : 'block'; + }; + + const getMirrorUrl = (url: string) => { + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } + }; + + const renderContent = async () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const lang = langSelect.value; + + if (!lang) { + contentDiv.innerHTML = '
No language selected.
'; + return; + } + + contentDiv.innerHTML = '
Loading background images...
'; + + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/main_bg_image/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) { + contentDiv.innerHTML = '
No data found.
'; + return; + } + + // Collect unique images by MD5 from the entire history + const imageMap = new Map(); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + for (const entry of sortedData) { + if (!entry.rsp || !entry.rsp.main_bg_image) continue; + const img = entry.rsp.main_bg_image; + if (!imageMap.has(img.md5)) { + imageMap.set(img.md5, { image: img, firstSeen: entry.updatedAt }); + } + } + + contentDiv.innerHTML = ''; + if (imageMap.size === 0) { + contentDiv.innerHTML = '
No images found.
'; + return; + } + + const row = document.createElement('div'); + row.className = 'row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3'; + contentDiv.appendChild(row); + + for (const [md5, { image, firstSeen }] of imageMap) { + const col = document.createElement('div'); + col.className = 'col'; + + const dateStr = DateTime.fromISO(firstSeen).toFormat('yyyy/MM/dd HH:mm'); + const mirrorUrl = getMirrorUrl(image.url); + const linkUrl = image.video_url ? getMirrorUrl(image.video_url) : mirrorUrl; + + col.innerHTML = ` + +
+
+ Background Image +
+ ${image.video_url ? 'Video' : ''} +
+
+
+
+ ${md5} + ${dateStr} +
+
+
+
+ `; + row.appendChild(col); + } + } catch (e) { + console.warn(`Failed to load ${url}`, e); + contentDiv.innerHTML = '
Failed to load data.
'; + } + }; + + targetSelect.addEventListener('change', () => { + updateLanguages(); + renderContent(); + }); + langSelect.addEventListener('change', renderContent); + + updateLanguages(); + renderContent(); + container.appendChild(outerCard); +} diff --git a/pages-v2/src/legacy/renderers/webPretty/sidebar.ts b/pages-v2/src/legacy/renderers/webPretty/sidebar.ts new file mode 100644 index 0000000..81eb8c2 --- /dev/null +++ b/pages-v2/src/legacy/renderers/webPretty/sidebar.ts @@ -0,0 +1,195 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../../api.js'; +import type { LauncherWebSidebar, StoredData } from '../../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../../utils/constants.js'; + +export async function renderSidebar(container: HTMLElement) { + const outerCard = document.createElement('div'); + outerCard.className = 'card mb-3'; + + const header = document.createElement('div'); + header.className = 'card-header d-flex justify-content-between align-items-center'; + header.style.cursor = 'pointer'; + header.setAttribute('data-bs-toggle', 'collapse'); + header.setAttribute('data-bs-target', '#collapseSidebar'); + header.setAttribute('role', 'button'); + header.innerHTML = '

Sidebar

'; + outerCard.appendChild(header); + + const collapseDiv = document.createElement('div'); + collapseDiv.id = 'collapseSidebar'; + collapseDiv.className = 'collapse'; + outerCard.appendChild(collapseDiv); + + const outerCardBody = document.createElement('div'); + outerCardBody.className = 'card-body'; + collapseDiv.appendChild(outerCardBody); + + // --- UI Controls --- + const controls = document.createElement('div'); + controls.className = 'row g-3 mb-4'; + + const targetCol = document.createElement('div'); + targetCol.className = 'col-md-6'; + targetCol.innerHTML = ''; + const targetSelect = document.createElement('select'); + targetSelect.className = 'form-select'; + gameTargets.forEach((target, idx) => { + const option = document.createElement('option'); + option.value = idx.toString(); + option.textContent = `${target.region === 'cn' ? 'China' : 'Global'} - ${target.name}`; + targetSelect.appendChild(option); + }); + targetCol.appendChild(targetSelect); + + const langCol = document.createElement('div'); + langCol.className = 'col-md-6'; + langCol.innerHTML = ''; + const langSelect = document.createElement('select'); + langSelect.className = 'form-select'; + langCol.appendChild(langSelect); + + controls.appendChild(targetCol); + controls.appendChild(langCol); + outerCardBody.appendChild(controls); + + const contentDiv = document.createElement('div'); + outerCardBody.appendChild(contentDiv); + + // --- Logic --- + const updateLanguages = () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const langs = launcherWebApiLang[target.region] || []; + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + + langSelect.innerHTML = ''; + langs.forEach((lang) => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + if (lang === defaultLang) option.selected = true; + langSelect.appendChild(option); + }); + + langCol.style.display = langs.length <= 1 ? 'none' : 'block'; + }; + + const getMirrorUrl = (url: string) => { + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } + }; + + const renderContent = async () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const lang = langSelect.value; + + if (!lang) { + contentDiv.innerHTML = '
No language selected.
'; + return; + } + + contentDiv.innerHTML = '
Loading sidebar data...
'; + + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/sidebar/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) { + contentDiv.innerHTML = '
No data found.
'; + return; + } + + // Collect the latest sidebar configuration + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + // We only show the latest version as sidebars are usually state-dependent + const latest = sortedData[0]; + if (!latest || !latest.rsp || !latest.rsp.sidebars) { + contentDiv.innerHTML = '
No active sidebars.
'; + return; + } + + contentDiv.innerHTML = ''; + const row = document.createElement('div'); + row.className = 'row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3'; + contentDiv.appendChild(row); + + for (const item of latest.rsp.sidebars) { + const col = document.createElement('div'); + col.className = 'col'; + + const card = document.createElement('div'); + card.className = 'card h-100 shadow-sm'; + + let innerHtml = ` +
+
+
${item.media}
+ ${item.need_token ? 'Auth' : ''} +
+ `; + + if (item.pic) { + innerHtml += ` +
+ ${item.pic.description} +

${item.pic.description}

+
+ `; + } + + if (item.jump_url) { + innerHtml += ` + Open Link + `; + } + + if (item.sidebar_labels && item.sidebar_labels.length > 0) { + innerHtml += '
'; + for (const label of item.sidebar_labels) { + innerHtml += ` + + ${label.content} +
+ ${label.need_token ? 'Auth' : ''} + +
+
+ `; + } + innerHtml += '
'; + } + + innerHtml += '
'; + card.innerHTML = innerHtml; + col.appendChild(card); + row.appendChild(col); + } + + const infoDiv = document.createElement('div'); + infoDiv.className = 'text-muted small mt-3 text-end'; + infoDiv.textContent = `Last updated: ${DateTime.fromISO(latest.updatedAt).toFormat('yyyy/MM/dd HH:mm')}`; + contentDiv.appendChild(infoDiv); + } catch (e) { + console.warn(`Failed to load ${url}`, e); + contentDiv.innerHTML = '
Failed to load data.
'; + } + }; + + targetSelect.addEventListener('change', () => { + updateLanguages(); + renderContent(); + }); + langSelect.addEventListener('change', renderContent); + + updateLanguages(); + renderContent(); + container.appendChild(outerCard); +} diff --git a/pages-v2/src/legacy/renderers/webPretty/singleEnt.ts b/pages-v2/src/legacy/renderers/webPretty/singleEnt.ts new file mode 100644 index 0000000..2a49a22 --- /dev/null +++ b/pages-v2/src/legacy/renderers/webPretty/singleEnt.ts @@ -0,0 +1,205 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../../api.js'; +import type { LauncherWebSingleEnt, StoredData } from '../../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../../utils/constants.js'; + +export async function renderSingleEnt(container: HTMLElement) { + const outerCard = document.createElement('div'); + outerCard.className = 'card mb-3'; + + const header = document.createElement('div'); + header.className = 'card-header d-flex justify-content-between align-items-center'; + header.style.cursor = 'pointer'; + header.setAttribute('data-bs-toggle', 'collapse'); + header.setAttribute('data-bs-target', '#collapseSingleEnt'); + header.setAttribute('role', 'button'); + header.innerHTML = '

Single Ent.

'; + outerCard.appendChild(header); + + const collapseDiv = document.createElement('div'); + collapseDiv.id = 'collapseSingleEnt'; + collapseDiv.className = 'collapse'; + outerCard.appendChild(collapseDiv); + + const outerCardBody = document.createElement('div'); + outerCardBody.className = 'card-body'; + collapseDiv.appendChild(outerCardBody); + + // --- UI Controls --- + const controls = document.createElement('div'); + controls.className = 'row g-3 mb-4'; + + const targetCol = document.createElement('div'); + targetCol.className = 'col-md-6'; + targetCol.innerHTML = ''; + const targetSelect = document.createElement('select'); + targetSelect.className = 'form-select'; + gameTargets.forEach((target, idx) => { + const option = document.createElement('option'); + option.value = idx.toString(); + option.textContent = `${target.region === 'cn' ? 'China' : 'Global'} - ${target.name}`; + targetSelect.appendChild(option); + }); + targetCol.appendChild(targetSelect); + + const langCol = document.createElement('div'); + langCol.className = 'col-md-6'; + langCol.innerHTML = ''; + const langSelect = document.createElement('select'); + langSelect.className = 'form-select'; + langCol.appendChild(langSelect); + + controls.appendChild(targetCol); + controls.appendChild(langCol); + outerCardBody.appendChild(controls); + + const contentDiv = document.createElement('div'); + outerCardBody.appendChild(contentDiv); + + // --- Logic --- + const updateLanguages = () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const langs = launcherWebApiLang[target.region] || []; + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + + langSelect.innerHTML = ''; + langs.forEach((lang) => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + if (lang === defaultLang) option.selected = true; + langSelect.appendChild(option); + }); + + langCol.style.display = langs.length <= 1 ? 'none' : 'block'; + }; + + const getMirrorUrl = (url: string) => { + if (!url) return ''; + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } + }; + + const renderContent = async () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const lang = langSelect.value; + + if (!lang) { + contentDiv.innerHTML = '
No language selected.
'; + return; + } + + contentDiv.innerHTML = '
Loading single entry data...
'; + + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/single_ent/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) { + contentDiv.innerHTML = '
No data found.
'; + return; + } + + // Collect unique visuals by MD5 from the entire history + const entMap = new Map(); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + for (const entry of sortedData) { + if (!entry.rsp || !entry.rsp.single_ent) continue; + const ent = entry.rsp.single_ent; + const key = ent.version_md5 || ent.version_url; + if (!entMap.has(key)) { + entMap.set(key, { ent, firstSeen: entry.updatedAt }); + } + } + + contentDiv.innerHTML = ''; + if (entMap.size === 0) { + contentDiv.innerHTML = '
No data found.
'; + return; + } + + const row = document.createElement('div'); + row.className = 'row row-cols-1 row-cols-md-2 g-4'; + contentDiv.appendChild(row); + + for (const [_key, { ent, firstSeen }] of entMap) { + const col = document.createElement('div'); + col.className = 'col'; + + const card = document.createElement('div'); + card.className = 'card h-100 shadow-sm'; + + let innerHtml = ` +
+
+ First seen: ${DateTime.fromISO(firstSeen).toFormat('yyyy/MM/dd HH:mm')} + ${ent.need_token ? 'Auth' : ''} +
+
+ + + Version Image + +

MD5: ${ent.version_md5}

+
+ `; + + if (ent.button_url) { + innerHtml += ` +
+ +
+
+ Button +

Normal

+
+ ${ + ent.button_hover_url + ? ` +
+ Button Hover +

Hover

+
+ ` + : '' + } +
+
+ `; + } + + if (ent.jump_url) { + innerHtml += ` + Jump URL + `; + } + + innerHtml += '
'; + card.innerHTML = innerHtml; + col.appendChild(card); + row.appendChild(col); + } + } catch (e) { + console.warn(`Failed to load ${url}`, e); + contentDiv.innerHTML = '
Failed to load data.
'; + } + }; + + targetSelect.addEventListener('change', () => { + updateLanguages(); + renderContent(); + }); + langSelect.addEventListener('change', renderContent); + + updateLanguages(); + renderContent(); + container.appendChild(outerCard); +} diff --git a/pages-v2/src/legacy/resources.ts b/pages-v2/src/legacy/resources.ts new file mode 100644 index 0000000..1c8a883 --- /dev/null +++ b/pages-v2/src/legacy/resources.ts @@ -0,0 +1,119 @@ +import { DateTime } from 'luxon'; +import * as semver from 'semver'; +import { fetchJson } from '../api.js'; +import type { StoredData } from '../types.js'; +import { BASE_URL } from '../utils/constants.js'; + +export async function renderResources(container: HTMLElement) { + const platforms = ['Windows', 'Android', 'iOS', 'PlayStation']; + const targets = [ + { region: 'os', channel: 6 }, + { region: 'cn', channel: 1 }, + ]; + + for (const target of targets) { + const section = document.createElement('div'); + section.className = 'mb-5'; + section.innerHTML = `

${target.region === 'cn' ? 'China' : 'Global'}

`; + + const accordion = document.createElement('div'); + accordion.className = 'accordion'; + accordion.id = `accordion-res-${target.region}-${target.channel}`; + let itemIndex = 0; + + for (const platform of platforms) { + const url = `${BASE_URL}/akEndfield/launcher/game_resources/${target.channel}/${platform}/all.json`; + try { + const data = await fetchJson[]>(url); + + // Group by res_version + const resVersionMap = new Map; versions: Set }>(); + for (const e of data) { + const resVer = e.rsp.res_version; + if (!resVersionMap.has(resVer)) { + resVersionMap.set(resVer, { rsp: e, versions: new Set() }); + } + resVersionMap.get(resVer)!.versions.add(e.req.version); + } + + const resVersionSet = Array.from(resVersionMap.values()).map((d) => ({ + resVersion: d.rsp.rsp.res_version, + rsp: d.rsp, + versions: Array.from(d.versions).sort(semver.rcompare), + })); + + const sortedSet = resVersionSet.reverse(); + let rows = ''; + for (let i = 0; i < sortedSet.length; i++) { + const item = sortedSet[i]!; + const nextItem = sortedSet[i + 1]; + // Newest first + const currentDate = DateTime.fromISO(item.rsp.updatedAt); + const dateStr = currentDate.toFormat('yyyy/MM/dd HH:mm:ss'); + + const intervalStr = (() => { + if (nextItem) { + const nextDate = DateTime.fromISO(nextItem.rsp.updatedAt); + const diff = currentDate.diff(nextDate); + return diff.toFormat('dd:hh:mm:ss'); + } + return '-'; + })(); + + const initialRes = item.rsp.rsp.resources.find((e: any) => e.name === 'initial'); + const mainRes = item.rsp.rsp.resources.find((e: any) => e.name === 'main'); + const isKick = JSON.parse(item.rsp.rsp.configs).kick_flag === true; + + rows += ` + ${dateStr} + ${intervalStr} + ${initialRes.version} + ${mainRes.version} + ${isKick ? '✅' : ''} + ${item.versions.join(', ')} + `; + } + + const itemId = `res-${target.region}-${target.channel}-${platform}`; + const isExpanded = false; + itemIndex++; + + const item = document.createElement('div'); + item.className = 'accordion-item'; + item.innerHTML = ` +

+ +

+
+
+
+ + + + + + + + + + + + ${rows} +
DateIntervalInitialMainKickGame version
+
+
+
+ `; + accordion.appendChild(item); + } catch (err) { + // Ignore + } + } + if (accordion.childElementCount > 0) { + section.appendChild(accordion); + container.appendChild(section); + } + } +} diff --git a/pages-v2/src/legacy/types.ts b/pages-v2/src/legacy/types.ts new file mode 100644 index 0000000..7a57601 --- /dev/null +++ b/pages-v2/src/legacy/types.ts @@ -0,0 +1,72 @@ +export interface MirrorFileEntry { + orig: string; + mirror: string; + origStatus: boolean; +} + +export interface StoredData { + req: any; + rsp: T; + updatedAt: string; +} + +export interface LauncherWebAnnouncement { + data_version: string; + tabs: { + tabName: string; + announcements: { + content: string; + jump_url: string; + start_ts: string; + id: string; + need_token: boolean; + }[]; + tab_id: string; + }[]; +} + +export interface LauncherWebBanner { + data_version: string; + banners: { + url: string; + md5: string; + jump_url: string; + id: string; + need_token: boolean; + }[]; +} + +export interface LauncherWebMainBgImage { + data_version: string; + main_bg_image: { + url: string; + md5: string; + video_url: string; + }; +} + +export interface LauncherWebSingleEnt { + single_ent: { + version_url: string; + version_md5: string; + jump_url: string; + button_url: string; + button_md5: string; + button_hover_url: string; + button_hover_md5: string; + need_token: boolean; + }; +} + +export interface LauncherWebSidebar { + data_version: string; + sidebars: { + display_type: 'DisplayType_RESERVE'; + media: string; + pic: { url: string; md5: string; description: string } | null; + sidebar_labels: { content: string; jump_url: string; need_token: boolean }[]; + grid_info: null; + jump_url: string; + need_token: boolean; + }[]; +} diff --git a/pages-v2/src/legacy/utils/constants.ts b/pages-v2/src/legacy/utils/constants.ts new file mode 100644 index 0000000..7f177e9 --- /dev/null +++ b/pages-v2/src/legacy/utils/constants.ts @@ -0,0 +1,44 @@ +export const BASE_URL = + 'https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output'; + +export const FILE_SIZE_OPTS = { + decimals: 2, + decimalPadding: true, + useBinaryUnit: true, + useBitUnit: false, + unitVisible: true, + unit: null, +}; + +export const gameTargets = [ + { name: 'Official', region: 'os' as const, dirName: '6', channel: 6 }, + { name: 'Epic', region: 'os' as const, dirName: '801', channel: 6 }, + { name: 'Google Play', region: 'os' as const, dirName: '802', channel: 6 }, + { name: 'Official', region: 'cn' as const, dirName: '1', channel: 1 }, + { name: 'Bilibili', region: 'cn' as const, dirName: '2', channel: 2 }, +]; + +export const launcherTargets = [ + { id: 'os', apps: ['EndField', 'Official'], channel: 6 }, + { id: 'cn', apps: ['EndField', 'Arknights', 'Official'], channel: 1 }, +]; + +export const launcherWebApiLang = { + os: [ + 'de-de', + 'en-us', + 'es-mx', + 'fr-fr', + 'id-id', + 'it-it', + 'ja-jp', + 'ko-kr', + 'pt-br', + 'ru-ru', + 'th-th', + 'vi-vn', + 'zh-cn', + 'zh-tw', + ] as const, + cn: ['zh-cn'] as const, +}; diff --git a/pages-v2/src/legacy/utils/logger.ts b/pages-v2/src/legacy/utils/logger.ts new file mode 100644 index 0000000..58e1f45 --- /dev/null +++ b/pages-v2/src/legacy/utils/logger.ts @@ -0,0 +1,12 @@ +import { DateTime } from 'luxon'; + +export default { + write(message: string) { + const debugLogElement = document.querySelector('#debug-log code'); + if (!debugLogElement) return; + const prettyMessage = `${DateTime.now().toFormat('HH:mm:ss.SSS')} > ${message}`; + const divEl = document.createElement('div'); + divEl.textContent = prettyMessage; + debugLogElement.appendChild(divEl); + }, +}; diff --git a/pages-v2/src/legacy/utils/math.ts b/pages-v2/src/legacy/utils/math.ts new file mode 100644 index 0000000..2613df0 --- /dev/null +++ b/pages-v2/src/legacy/utils/math.ts @@ -0,0 +1,163 @@ +import logger from './logger.js'; + +export default { + arrayMax(array: Array) { + return array.reduce((a, b) => Math.max(a, b)); + }, + + arrayMin(array: Array) { + return array.reduce((a, b) => Math.min(a, b)); + }, + + arrayTotal(array: Array) { + return array.reduce((acc, f) => acc + f, 0); + }, + + arrayAvg(array: Array) { + return this.arrayTotal(array) / array.length; + }, + + rounder(method: 'floor' | 'ceil' | 'round', num: number, n: number) { + const pow = Math.pow(10, n); + let result: number; + switch (method) { + case 'floor': + result = Math.floor(num * pow) / pow; + break; + case 'ceil': + result = Math.ceil(num * pow) / pow; + break; + case 'round': + result = Math.round(num * pow) / pow; + break; + } + return { + orig: result, + padded: result.toFixed(n), + }; + }, + + formatFileSize( + bytes: number, + options: { + decimals: number; + decimalPadding: boolean; + useBinaryUnit: boolean; + useBitUnit: boolean; + unitVisible: boolean; + unit: 'B' | 'K' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y' | null; + }, + ) { + const k = options.useBinaryUnit ? 1024 : 1000; + const dm = options.decimals < 0 ? 0 : options.decimals; + + const baseUnits = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + const binaryUnitSuffix = options.useBitUnit ? 'ib' : 'iB'; + const siUnitSuffix = options.useBitUnit ? 'b' : 'B'; + + const getUnitString = (i: number) => { + if (i === 0) return options.useBitUnit ? 'b' : 'B'; + return baseUnits[i] + (options.useBinaryUnit ? binaryUnitSuffix : siUnitSuffix); + }; + + let value = bytes < 0 ? 0 : Math.floor(bytes); + if (options.useBitUnit) { + value *= 8; + } + + let i: number; + if (options.unit !== null) { + i = baseUnits.indexOf(options.unit); + if (i === -1) throw new Error(`Invalid unit: ${options.unit}`); + } else { + if (value === 0) { + i = 0; + } else { + i = Math.floor(Math.log(value) / Math.log(k)); + i = Math.max(0, Math.min(baseUnits.length - 1, i)); // clamp + } + } + + const resultValue = value / Math.pow(k, i); + + let formattedValue: string; + if (options.decimalPadding) { + formattedValue = resultValue.toFixed(dm); + } else { + formattedValue = resultValue.toFixed(dm).replace(/\.?0+$/, ''); + } + + return formattedValue + (options.unitVisible ? ' ' + getUnitString(i) : ''); + }, + + secureRandomFloatInRange(min: number, max: number): number { + if (min > max) [min, max] = [max, min]; + const crypto = globalThis.crypto; + if (!crypto) { + throw new Error('Cryptographically secure random float number gen is not available'); + } + const randomValues = new Uint32Array(2); + crypto.getRandomValues(randomValues); + const highBits = randomValues[1]! & 0x1fffff; // 0x1FFFFF = 2^21 - 1 + const lowBits = randomValues[0]; + const combined = highBits * 0x100000000 + lowBits!; // 0x100000000 = 2^32 + const randomFraction = combined / 0x20000000000000; // 0x20000000000000 = 2^53 + return randomFraction * (max - min) + min; + }, + + secureRandomIntInRange(min: number, max: number, writeLog: boolean = false): number { + if (min === max) { + writeLog ? logger.write(`randomInt: Range=${min}-${max}, Output=${min}`) : undefined; + return min; + } + if (min > max) [min, max] = [max, min]; + const crypto = globalThis.crypto; + if (!crypto) { + throw new Error('Cryptographically secure random int number gen is not available'); + } + + // convert to integer anyway + const minInt = Math.ceil(min); + const maxInt = Math.floor(max); + + // safe integer check + if (!Number.isSafeInteger(minInt) || !Number.isSafeInteger(maxInt)) { + throw new Error('Range boundaries must be within safe integer limits'); + } + + // valid range check + if (minInt > maxInt) { + throw new Error('Invalid range after integer conversion: min > max'); + } + + const range = maxInt - minInt + 1; + + if (range <= 0 || range > Number.MAX_SAFE_INTEGER) { + throw new Error(`Range size must be between 1 and ${Number.MAX_SAFE_INTEGER} inclusive`); + } + + // 53-bit random num gen + const MAX_53 = BigInt(1) << BigInt(53); // 2^53 + const rangeBigInt = BigInt(range); + const maxAcceptable = MAX_53 - (MAX_53 % rangeBigInt); + + // generate + const randomBuffer = new Uint32Array(2); + while (true) { + crypto.getRandomValues(randomBuffer); + const highBits = randomBuffer[1]! & 0x1fffff; // use lower 21-bit only + const lowBits = randomBuffer[0]; + const combined = BigInt(highBits) * BigInt(0x100000000) + BigInt(lowBits!); // 0x100000000 = 2^32 + // accept condition: combined < maxAcceptable + if (combined < maxAcceptable) { + const offset = Number(combined % rangeBigInt); // 0 to range-1 + writeLog + ? logger.write( + `randomInt: Range=${min}-${max}, Raw=0x${new Uint8Array(randomBuffer).toHex()}, Output=${minInt + offset}`, + ) + : undefined; + return minInt + offset; + } + } + }, +}; diff --git a/pages-v2/src/legacy/utils/ui.ts b/pages-v2/src/legacy/utils/ui.ts new file mode 100644 index 0000000..3526ca2 --- /dev/null +++ b/pages-v2/src/legacy/utils/ui.ts @@ -0,0 +1,16 @@ +import type { MirrorFileEntry } from '../types.js'; + +export function generateDownloadLinks(url: string, mirrorFileDb: MirrorFileEntry[]) { + const cleanUrl = new URL(url); + cleanUrl.search = ''; + const mirrorEntry = mirrorFileDb.find((g) => g.orig.includes(cleanUrl.toString())); + + const links: string[] = []; + if (!mirrorEntry || mirrorEntry.origStatus === true) { + links.push(`Orig`); + } + if (mirrorEntry) { + links.push(`Mirror`); + } + return links.join(' / '); +} diff --git a/pages-v2/src/legacy/web.ts b/pages-v2/src/legacy/web.ts new file mode 100644 index 0000000..8919f92 --- /dev/null +++ b/pages-v2/src/legacy/web.ts @@ -0,0 +1,157 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../api.js'; +import type { StoredData } from '../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../utils/constants.js'; + +const apiTypes = ['announcement', 'banner', 'main_bg_image', 'sidebar', 'single_ent']; + +export async function renderWeb(container: HTMLElement) { + for (const target of gameTargets) { + const section = document.createElement('div'); + section.className = 'mb-5'; + section.innerHTML = `

${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}

`; + + const langs = launcherWebApiLang[target.region] || []; + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + + // Language Selector + const langSelectGroup = document.createElement('div'); + langSelectGroup.className = 'input-group mb-3'; + langSelectGroup.innerHTML = 'Language'; + + const langSelect = document.createElement('select'); + langSelect.className = 'form-select'; + + langs.forEach((lang) => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + if (lang === defaultLang) { + option.selected = true; + } + langSelect.appendChild(option); + }); + langSelectGroup.appendChild(langSelect); + + if (langs.length <= 1) { + langSelectGroup.style.display = 'none'; + } + + section.appendChild(langSelectGroup); + + const accordion = document.createElement('div'); + accordion.className = 'accordion'; + accordion.id = `accordion-web-${target.dirName}`; + + const renderApiList = async (lang: string) => { + accordion.innerHTML = '
Loading...
'; + + const results = await Promise.all( + apiTypes.map(async (apiType) => { + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/${apiType}/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) return null; + return { apiType, list: [...data].reverse() }; + } catch (e) { + console.warn(`Failed to load ${url}`, e); + return null; + } + }), + ); + + accordion.innerHTML = ''; + const validResults = results.filter((r): r is NonNullable => r !== null); + + if (validResults.length === 0) { + accordion.innerHTML = '
No data found.
'; + return; + } + + validResults.forEach(({ apiType, list }, idx) => { + const itemId = `web-${target.dirName}-${lang}-${apiType}`; + + const item = document.createElement('div'); + item.className = 'accordion-item'; + + // Header + const header = document.createElement('h2'); + header.className = 'accordion-header'; + header.id = `heading-${itemId}`; + header.innerHTML = ` + + `; + item.appendChild(header); + + // Body + const collapse = document.createElement('div'); + collapse.id = `collapse-${itemId}`; + collapse.className = 'accordion-collapse collapse'; + collapse.setAttribute('aria-labelledby', `heading-${itemId}`); + collapse.setAttribute('data-bs-parent', `#${accordion.id}`); + + const body = document.createElement('div'); + body.className = 'accordion-body'; + + // Select for UpdatedAt + const selectGroup = document.createElement('div'); + selectGroup.className = 'input-group mb-3'; + selectGroup.innerHTML = `History`; + + const select = document.createElement('select'); + select.className = 'form-select'; + select.ariaLabel = 'Select version'; + + list.forEach((entry, idx) => { + const dateStr = DateTime.fromISO(entry.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); + const option = document.createElement('option'); + option.value = idx.toString(); + option.textContent = `${dateStr}`; + select.appendChild(option); + }); + selectGroup.appendChild(select); + body.appendChild(selectGroup); + + // Content Area + const contentArea = document.createElement('pre'); + contentArea.className = 'p-3 border rounded overflow-auto'; + contentArea.style.maxHeight = '500px'; + contentArea.style.fontSize = '0.875rem'; + + const updateContent = (index: number) => { + const entry = list[index]; + if (entry) { + contentArea.textContent = JSON.stringify(entry.rsp, null, 2); + } + }; + + // Initial render for this item + updateContent(0); + + select.addEventListener('change', (e) => { + const val = parseInt((e.target as HTMLSelectElement).value, 10); + updateContent(val); + }); + + body.appendChild(contentArea); + collapse.appendChild(body); + item.appendChild(collapse); + accordion.appendChild(item); + }); + }; + + langSelect.addEventListener('change', (e) => { + renderApiList((e.target as HTMLSelectElement).value); + }); + + section.appendChild(accordion); + container.appendChild(section); + + // Initial load + if (defaultLang) { + renderApiList(defaultLang); + } + } +} diff --git a/pages-v2/src/legacy/webPretty.ts b/pages-v2/src/legacy/webPretty.ts new file mode 100644 index 0000000..b74c18b --- /dev/null +++ b/pages-v2/src/legacy/webPretty.ts @@ -0,0 +1,14 @@ +import { renderAnnouncement } from './webPretty/announcement.js'; +import { renderBanner } from './webPretty/banner.js'; +import { renderMainBgImage } from './webPretty/mainBgImage.js'; +import { renderSidebar } from './webPretty/sidebar.js'; +import { renderSingleEnt } from './webPretty/singleEnt.js'; + +export async function renderWebPretty(container: HTMLElement) { + container.innerHTML = ''; + await renderAnnouncement(container); + await renderBanner(container); + await renderMainBgImage(container); + await renderSingleEnt(container); + await renderSidebar(container); +} diff --git a/pages-v2/src/legacy/webPretty/announcement.ts b/pages-v2/src/legacy/webPretty/announcement.ts new file mode 100644 index 0000000..9e1d3d1 --- /dev/null +++ b/pages-v2/src/legacy/webPretty/announcement.ts @@ -0,0 +1,179 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../../api.js'; +import type { LauncherWebAnnouncement, StoredData } from '../../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../../utils/constants.js'; + +export async function renderAnnouncement(container: HTMLElement) { + const outerCard = document.createElement('div'); + outerCard.className = 'card mb-3'; + + const header = document.createElement('div'); + header.className = 'card-header d-flex justify-content-between align-items-center'; + header.style.cursor = 'pointer'; + header.setAttribute('data-bs-toggle', 'collapse'); + header.setAttribute('data-bs-target', '#collapseAnnouncement'); + header.setAttribute('role', 'button'); + header.innerHTML = '

Announcement

'; + outerCard.appendChild(header); + + const collapseDiv = document.createElement('div'); + collapseDiv.id = 'collapseAnnouncement'; + collapseDiv.className = 'collapse'; + outerCard.appendChild(collapseDiv); + + const outerCardBody = document.createElement('div'); + outerCardBody.className = 'card-body'; + collapseDiv.appendChild(outerCardBody); + + // --- UI Controls --- + const controls = document.createElement('div'); + controls.className = 'row g-3 mb-4'; + + const targetCol = document.createElement('div'); + targetCol.className = 'col-md-6'; + targetCol.innerHTML = ''; + const targetSelect = document.createElement('select'); + targetSelect.className = 'form-select'; + gameTargets.forEach((target, idx) => { + const option = document.createElement('option'); + option.value = idx.toString(); + option.textContent = `${target.region === 'cn' ? 'China' : 'Global'} - ${target.name}`; + targetSelect.appendChild(option); + }); + targetCol.appendChild(targetSelect); + + const langCol = document.createElement('div'); + langCol.className = 'col-md-6'; + langCol.innerHTML = ''; + const langSelect = document.createElement('select'); + langSelect.className = 'form-select'; + langCol.appendChild(langSelect); + + controls.appendChild(targetCol); + controls.appendChild(langCol); + outerCardBody.appendChild(controls); + + const contentDiv = document.createElement('div'); + outerCardBody.appendChild(contentDiv); + + // --- Logic --- + const updateLanguages = () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const langs = launcherWebApiLang[target.region] || []; + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + + langSelect.innerHTML = ''; + langs.forEach((lang) => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + if (lang === defaultLang) option.selected = true; + langSelect.appendChild(option); + }); + + langCol.style.display = langs.length <= 1 ? 'none' : 'block'; + }; + + const renderContent = async () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const lang = langSelect.value; + + if (!lang) { + contentDiv.innerHTML = '
No language selected.
'; + return; + } + + contentDiv.innerHTML = '
Loading announcements...
'; + + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/announcement/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) { + contentDiv.innerHTML = '
No data found.
'; + return; + } + + const tabsMap = new Map< + string, + { tabName: string; announcements: Map } + >(); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + for (const entry of sortedData) { + if (!entry.rsp || !entry.rsp.tabs) continue; + for (const tab of entry.rsp.tabs) { + if (!tabsMap.has(tab.tab_id)) { + tabsMap.set(tab.tab_id, { tabName: tab.tabName, announcements: new Map() }); + } + const targetTab = tabsMap.get(tab.tab_id)!; + for (const ann of tab.announcements) { + if (!targetTab.announcements.has(ann.id)) { + targetTab.announcements.set(ann.id, ann); + } + } + } + } + + contentDiv.innerHTML = ''; + if (tabsMap.size === 0) { + contentDiv.innerHTML = '
No announcements found.
'; + return; + } + + for (const [_tabId, tabData] of tabsMap) { + const card = document.createElement('div'); + card.className = 'card mb-4 shadow-sm'; + + const cardHeader = document.createElement('div'); + cardHeader.className = 'card-header bg-secondary text-white fw-bold py-1'; + cardHeader.textContent = tabData.tabName; + card.appendChild(cardHeader); + + const listGroup = document.createElement('ul'); + listGroup.className = 'list-group list-group-flush'; + + const sortedAnnouncements = Array.from(tabData.announcements.values()).sort( + (a, b) => parseInt(b.start_ts, 10) - parseInt(a.start_ts, 10), + ); + + for (const ann of sortedAnnouncements) { + const item = document.createElement('li'); + item.className = 'list-group-item py-2'; + const date = DateTime.fromMillis(parseInt(ann.start_ts, 10)).toFormat('yyyy/MM/dd HH:mm'); + + item.innerHTML = ` +
+ ${date} + ${ann.content} +
+ ${ann.need_token ? 'Auth' : ''} + ${ann.jump_url ? `Link` : ''} + ID:${ann.id} +
+
+ `; + listGroup.appendChild(item); + } + card.appendChild(listGroup); + contentDiv.appendChild(card); + } + } catch (e) { + console.warn(`Failed to load ${url}`, e); + contentDiv.innerHTML = '
Failed to load data.
'; + } + }; + + targetSelect.addEventListener('change', () => { + updateLanguages(); + renderContent(); + }); + langSelect.addEventListener('change', renderContent); + + updateLanguages(); + renderContent(); + container.appendChild(outerCard); +} diff --git a/pages-v2/src/legacy/webPretty/banner.ts b/pages-v2/src/legacy/webPretty/banner.ts new file mode 100644 index 0000000..7356452 --- /dev/null +++ b/pages-v2/src/legacy/webPretty/banner.ts @@ -0,0 +1,175 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../../api.js'; +import type { LauncherWebBanner, StoredData } from '../../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../../utils/constants.js'; + +export async function renderBanner(container: HTMLElement) { + const outerCard = document.createElement('div'); + outerCard.className = 'card mb-3'; + + const header = document.createElement('div'); + header.className = 'card-header d-flex justify-content-between align-items-center'; + header.style.cursor = 'pointer'; + header.setAttribute('data-bs-toggle', 'collapse'); + header.setAttribute('data-bs-target', '#collapseBanner'); + header.setAttribute('role', 'button'); + header.innerHTML = '

Banner

'; + outerCard.appendChild(header); + + const collapseDiv = document.createElement('div'); + collapseDiv.id = 'collapseBanner'; + collapseDiv.className = 'collapse'; + outerCard.appendChild(collapseDiv); + + const outerCardBody = document.createElement('div'); + outerCardBody.className = 'card-body'; + collapseDiv.appendChild(outerCardBody); + + // --- UI Controls --- + const controls = document.createElement('div'); + controls.className = 'row g-3 mb-4'; + + const targetCol = document.createElement('div'); + targetCol.className = 'col-md-6'; + targetCol.innerHTML = ''; + const targetSelect = document.createElement('select'); + targetSelect.className = 'form-select'; + gameTargets.forEach((target, idx) => { + const option = document.createElement('option'); + option.value = idx.toString(); + option.textContent = `${target.region === 'cn' ? 'China' : 'Global'} - ${target.name}`; + targetSelect.appendChild(option); + }); + targetCol.appendChild(targetSelect); + + const langCol = document.createElement('div'); + langCol.className = 'col-md-6'; + langCol.innerHTML = ''; + const langSelect = document.createElement('select'); + langSelect.className = 'form-select'; + langCol.appendChild(langSelect); + + controls.appendChild(targetCol); + controls.appendChild(langCol); + outerCardBody.appendChild(controls); + + const contentDiv = document.createElement('div'); + outerCardBody.appendChild(contentDiv); + + // --- Logic --- + const updateLanguages = () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const langs = launcherWebApiLang[target.region] || []; + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + + langSelect.innerHTML = ''; + langs.forEach((lang) => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + if (lang === defaultLang) option.selected = true; + langSelect.appendChild(option); + }); + + langCol.style.display = langs.length <= 1 ? 'none' : 'block'; + }; + + const getMirrorUrl = (url: string) => { + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } + }; + + const renderContent = async () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const lang = langSelect.value; + + if (!lang) { + contentDiv.innerHTML = '
No language selected.
'; + return; + } + + contentDiv.innerHTML = '
Loading banners...
'; + + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/banner/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) { + contentDiv.innerHTML = '
No data found.
'; + return; + } + + // Collect unique banners by ID from the entire history + const bannerMap = new Map(); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + for (const entry of sortedData) { + if (!entry.rsp || !entry.rsp.banners) continue; + for (const banner of entry.rsp.banners) { + if (!bannerMap.has(banner.id)) { + bannerMap.set(banner.id, { banner, firstSeen: entry.updatedAt }); + } + } + } + + contentDiv.innerHTML = ''; + if (bannerMap.size === 0) { + contentDiv.innerHTML = '
No banners found.
'; + return; + } + + const row = document.createElement('div'); + row.className = 'row row-cols-1 row-cols-md-3 row-cols-lg-4 g-3'; + contentDiv.appendChild(row); + + for (const [id, { banner, firstSeen }] of bannerMap) { + const col = document.createElement('div'); + col.className = 'col'; + + const dateStr = DateTime.fromISO(firstSeen).toFormat('yyyy/MM/dd HH:mm'); + const mirrorUrl = getMirrorUrl(banner.url); + const linkUrl = banner.jump_url || mirrorUrl; + + col.innerHTML = ` + +
+
+ Banner Image +
+ ${banner.need_token ? 'Auth' : ''} +
+
+
+
+ ID: ${id} + ${dateStr} +
+
+
+
+ `; + row.appendChild(col); + } + } catch (e) { + console.warn(`Failed to load ${url}`, e); + contentDiv.innerHTML = '
Failed to load data.
'; + } + }; + + targetSelect.addEventListener('change', () => { + updateLanguages(); + renderContent(); + }); + langSelect.addEventListener('change', renderContent); + + updateLanguages(); + renderContent(); + container.appendChild(outerCard); +} diff --git a/pages-v2/src/legacy/webPretty/mainBgImage.ts b/pages-v2/src/legacy/webPretty/mainBgImage.ts new file mode 100644 index 0000000..f815104 --- /dev/null +++ b/pages-v2/src/legacy/webPretty/mainBgImage.ts @@ -0,0 +1,174 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../../api.js'; +import type { LauncherWebMainBgImage, StoredData } from '../../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../../utils/constants.js'; + +export async function renderMainBgImage(container: HTMLElement) { + const outerCard = document.createElement('div'); + outerCard.className = 'card mb-3'; + + const header = document.createElement('div'); + header.className = 'card-header d-flex justify-content-between align-items-center'; + header.style.cursor = 'pointer'; + header.setAttribute('data-bs-toggle', 'collapse'); + header.setAttribute('data-bs-target', '#collapseMainBgImage'); + header.setAttribute('role', 'button'); + header.innerHTML = '

Main Background Image

'; + outerCard.appendChild(header); + + const collapseDiv = document.createElement('div'); + collapseDiv.id = 'collapseMainBgImage'; + collapseDiv.className = 'collapse'; + outerCard.appendChild(collapseDiv); + + const outerCardBody = document.createElement('div'); + outerCardBody.className = 'card-body'; + collapseDiv.appendChild(outerCardBody); + + // --- UI Controls --- + const controls = document.createElement('div'); + controls.className = 'row g-3 mb-4'; + + const targetCol = document.createElement('div'); + targetCol.className = 'col-md-6'; + targetCol.innerHTML = ''; + const targetSelect = document.createElement('select'); + targetSelect.className = 'form-select'; + gameTargets.forEach((target, idx) => { + const option = document.createElement('option'); + option.value = idx.toString(); + option.textContent = `${target.region === 'cn' ? 'China' : 'Global'} - ${target.name}`; + targetSelect.appendChild(option); + }); + targetCol.appendChild(targetSelect); + + const langCol = document.createElement('div'); + langCol.className = 'col-md-6'; + langCol.innerHTML = ''; + const langSelect = document.createElement('select'); + langSelect.className = 'form-select'; + langCol.appendChild(langSelect); + + controls.appendChild(targetCol); + controls.appendChild(langCol); + outerCardBody.appendChild(controls); + + const contentDiv = document.createElement('div'); + outerCardBody.appendChild(contentDiv); + + // --- Logic --- + const updateLanguages = () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const langs = launcherWebApiLang[target.region] || []; + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + + langSelect.innerHTML = ''; + langs.forEach((lang) => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + if (lang === defaultLang) option.selected = true; + langSelect.appendChild(option); + }); + + langCol.style.display = langs.length <= 1 ? 'none' : 'block'; + }; + + const getMirrorUrl = (url: string) => { + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } + }; + + const renderContent = async () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const lang = langSelect.value; + + if (!lang) { + contentDiv.innerHTML = '
No language selected.
'; + return; + } + + contentDiv.innerHTML = '
Loading background images...
'; + + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/main_bg_image/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) { + contentDiv.innerHTML = '
No data found.
'; + return; + } + + // Collect unique images by MD5 from the entire history + const imageMap = new Map(); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + for (const entry of sortedData) { + if (!entry.rsp || !entry.rsp.main_bg_image) continue; + const img = entry.rsp.main_bg_image; + if (!imageMap.has(img.md5)) { + imageMap.set(img.md5, { image: img, firstSeen: entry.updatedAt }); + } + } + + contentDiv.innerHTML = ''; + if (imageMap.size === 0) { + contentDiv.innerHTML = '
No images found.
'; + return; + } + + const row = document.createElement('div'); + row.className = 'row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3'; + contentDiv.appendChild(row); + + for (const [md5, { image, firstSeen }] of imageMap) { + const col = document.createElement('div'); + col.className = 'col'; + + const dateStr = DateTime.fromISO(firstSeen).toFormat('yyyy/MM/dd HH:mm'); + const mirrorUrl = getMirrorUrl(image.url); + const linkUrl = image.video_url ? getMirrorUrl(image.video_url) : mirrorUrl; + + col.innerHTML = ` + +
+
+ Background Image +
+ ${image.video_url ? 'Video' : ''} +
+
+
+
+ ${md5} + ${dateStr} +
+
+
+
+ `; + row.appendChild(col); + } + } catch (e) { + console.warn(`Failed to load ${url}`, e); + contentDiv.innerHTML = '
Failed to load data.
'; + } + }; + + targetSelect.addEventListener('change', () => { + updateLanguages(); + renderContent(); + }); + langSelect.addEventListener('change', renderContent); + + updateLanguages(); + renderContent(); + container.appendChild(outerCard); +} diff --git a/pages-v2/src/legacy/webPretty/sidebar.ts b/pages-v2/src/legacy/webPretty/sidebar.ts new file mode 100644 index 0000000..81eb8c2 --- /dev/null +++ b/pages-v2/src/legacy/webPretty/sidebar.ts @@ -0,0 +1,195 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../../api.js'; +import type { LauncherWebSidebar, StoredData } from '../../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../../utils/constants.js'; + +export async function renderSidebar(container: HTMLElement) { + const outerCard = document.createElement('div'); + outerCard.className = 'card mb-3'; + + const header = document.createElement('div'); + header.className = 'card-header d-flex justify-content-between align-items-center'; + header.style.cursor = 'pointer'; + header.setAttribute('data-bs-toggle', 'collapse'); + header.setAttribute('data-bs-target', '#collapseSidebar'); + header.setAttribute('role', 'button'); + header.innerHTML = '

Sidebar

'; + outerCard.appendChild(header); + + const collapseDiv = document.createElement('div'); + collapseDiv.id = 'collapseSidebar'; + collapseDiv.className = 'collapse'; + outerCard.appendChild(collapseDiv); + + const outerCardBody = document.createElement('div'); + outerCardBody.className = 'card-body'; + collapseDiv.appendChild(outerCardBody); + + // --- UI Controls --- + const controls = document.createElement('div'); + controls.className = 'row g-3 mb-4'; + + const targetCol = document.createElement('div'); + targetCol.className = 'col-md-6'; + targetCol.innerHTML = ''; + const targetSelect = document.createElement('select'); + targetSelect.className = 'form-select'; + gameTargets.forEach((target, idx) => { + const option = document.createElement('option'); + option.value = idx.toString(); + option.textContent = `${target.region === 'cn' ? 'China' : 'Global'} - ${target.name}`; + targetSelect.appendChild(option); + }); + targetCol.appendChild(targetSelect); + + const langCol = document.createElement('div'); + langCol.className = 'col-md-6'; + langCol.innerHTML = ''; + const langSelect = document.createElement('select'); + langSelect.className = 'form-select'; + langCol.appendChild(langSelect); + + controls.appendChild(targetCol); + controls.appendChild(langCol); + outerCardBody.appendChild(controls); + + const contentDiv = document.createElement('div'); + outerCardBody.appendChild(contentDiv); + + // --- Logic --- + const updateLanguages = () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const langs = launcherWebApiLang[target.region] || []; + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + + langSelect.innerHTML = ''; + langs.forEach((lang) => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + if (lang === defaultLang) option.selected = true; + langSelect.appendChild(option); + }); + + langCol.style.display = langs.length <= 1 ? 'none' : 'block'; + }; + + const getMirrorUrl = (url: string) => { + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } + }; + + const renderContent = async () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const lang = langSelect.value; + + if (!lang) { + contentDiv.innerHTML = '
No language selected.
'; + return; + } + + contentDiv.innerHTML = '
Loading sidebar data...
'; + + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/sidebar/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) { + contentDiv.innerHTML = '
No data found.
'; + return; + } + + // Collect the latest sidebar configuration + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + // We only show the latest version as sidebars are usually state-dependent + const latest = sortedData[0]; + if (!latest || !latest.rsp || !latest.rsp.sidebars) { + contentDiv.innerHTML = '
No active sidebars.
'; + return; + } + + contentDiv.innerHTML = ''; + const row = document.createElement('div'); + row.className = 'row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3'; + contentDiv.appendChild(row); + + for (const item of latest.rsp.sidebars) { + const col = document.createElement('div'); + col.className = 'col'; + + const card = document.createElement('div'); + card.className = 'card h-100 shadow-sm'; + + let innerHtml = ` +
+
+
${item.media}
+ ${item.need_token ? 'Auth' : ''} +
+ `; + + if (item.pic) { + innerHtml += ` +
+ ${item.pic.description} +

${item.pic.description}

+
+ `; + } + + if (item.jump_url) { + innerHtml += ` + Open Link + `; + } + + if (item.sidebar_labels && item.sidebar_labels.length > 0) { + innerHtml += '
'; + for (const label of item.sidebar_labels) { + innerHtml += ` + + ${label.content} +
+ ${label.need_token ? 'Auth' : ''} + +
+
+ `; + } + innerHtml += '
'; + } + + innerHtml += '
'; + card.innerHTML = innerHtml; + col.appendChild(card); + row.appendChild(col); + } + + const infoDiv = document.createElement('div'); + infoDiv.className = 'text-muted small mt-3 text-end'; + infoDiv.textContent = `Last updated: ${DateTime.fromISO(latest.updatedAt).toFormat('yyyy/MM/dd HH:mm')}`; + contentDiv.appendChild(infoDiv); + } catch (e) { + console.warn(`Failed to load ${url}`, e); + contentDiv.innerHTML = '
Failed to load data.
'; + } + }; + + targetSelect.addEventListener('change', () => { + updateLanguages(); + renderContent(); + }); + langSelect.addEventListener('change', renderContent); + + updateLanguages(); + renderContent(); + container.appendChild(outerCard); +} diff --git a/pages-v2/src/legacy/webPretty/singleEnt.ts b/pages-v2/src/legacy/webPretty/singleEnt.ts new file mode 100644 index 0000000..2a49a22 --- /dev/null +++ b/pages-v2/src/legacy/webPretty/singleEnt.ts @@ -0,0 +1,205 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../../api.js'; +import type { LauncherWebSingleEnt, StoredData } from '../../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../../utils/constants.js'; + +export async function renderSingleEnt(container: HTMLElement) { + const outerCard = document.createElement('div'); + outerCard.className = 'card mb-3'; + + const header = document.createElement('div'); + header.className = 'card-header d-flex justify-content-between align-items-center'; + header.style.cursor = 'pointer'; + header.setAttribute('data-bs-toggle', 'collapse'); + header.setAttribute('data-bs-target', '#collapseSingleEnt'); + header.setAttribute('role', 'button'); + header.innerHTML = '

Single Ent.

'; + outerCard.appendChild(header); + + const collapseDiv = document.createElement('div'); + collapseDiv.id = 'collapseSingleEnt'; + collapseDiv.className = 'collapse'; + outerCard.appendChild(collapseDiv); + + const outerCardBody = document.createElement('div'); + outerCardBody.className = 'card-body'; + collapseDiv.appendChild(outerCardBody); + + // --- UI Controls --- + const controls = document.createElement('div'); + controls.className = 'row g-3 mb-4'; + + const targetCol = document.createElement('div'); + targetCol.className = 'col-md-6'; + targetCol.innerHTML = ''; + const targetSelect = document.createElement('select'); + targetSelect.className = 'form-select'; + gameTargets.forEach((target, idx) => { + const option = document.createElement('option'); + option.value = idx.toString(); + option.textContent = `${target.region === 'cn' ? 'China' : 'Global'} - ${target.name}`; + targetSelect.appendChild(option); + }); + targetCol.appendChild(targetSelect); + + const langCol = document.createElement('div'); + langCol.className = 'col-md-6'; + langCol.innerHTML = ''; + const langSelect = document.createElement('select'); + langSelect.className = 'form-select'; + langCol.appendChild(langSelect); + + controls.appendChild(targetCol); + controls.appendChild(langCol); + outerCardBody.appendChild(controls); + + const contentDiv = document.createElement('div'); + outerCardBody.appendChild(contentDiv); + + // --- Logic --- + const updateLanguages = () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const langs = launcherWebApiLang[target.region] || []; + const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn'; + + langSelect.innerHTML = ''; + langs.forEach((lang) => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + if (lang === defaultLang) option.selected = true; + langSelect.appendChild(option); + }); + + langCol.style.display = langs.length <= 1 ? 'none' : 'block'; + }; + + const getMirrorUrl = (url: string) => { + if (!url) return ''; + try { + const u = new URL(url); + return `https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output/raw/${u.hostname}${u.pathname}`; + } catch { + return url; + } + }; + + const renderContent = async () => { + const targetIdx = parseInt(targetSelect.value, 10); + const target = gameTargets[targetIdx]!; + const lang = langSelect.value; + + if (!lang) { + contentDiv.innerHTML = '
No language selected.
'; + return; + } + + contentDiv.innerHTML = '
Loading single entry data...
'; + + const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/single_ent/${lang}/all.json`; + try { + const data = await fetchJson[]>(url); + if (!data || data.length === 0) { + contentDiv.innerHTML = '
No data found.
'; + return; + } + + // Collect unique visuals by MD5 from the entire history + const entMap = new Map(); + const sortedData = [...data].sort( + (a, b) => DateTime.fromISO(b.updatedAt).toMillis() - DateTime.fromISO(a.updatedAt).toMillis(), + ); + + for (const entry of sortedData) { + if (!entry.rsp || !entry.rsp.single_ent) continue; + const ent = entry.rsp.single_ent; + const key = ent.version_md5 || ent.version_url; + if (!entMap.has(key)) { + entMap.set(key, { ent, firstSeen: entry.updatedAt }); + } + } + + contentDiv.innerHTML = ''; + if (entMap.size === 0) { + contentDiv.innerHTML = '
No data found.
'; + return; + } + + const row = document.createElement('div'); + row.className = 'row row-cols-1 row-cols-md-2 g-4'; + contentDiv.appendChild(row); + + for (const [_key, { ent, firstSeen }] of entMap) { + const col = document.createElement('div'); + col.className = 'col'; + + const card = document.createElement('div'); + card.className = 'card h-100 shadow-sm'; + + let innerHtml = ` +
+
+ First seen: ${DateTime.fromISO(firstSeen).toFormat('yyyy/MM/dd HH:mm')} + ${ent.need_token ? 'Auth' : ''} +
+
+ + + Version Image + +

MD5: ${ent.version_md5}

+
+ `; + + if (ent.button_url) { + innerHtml += ` +
+ +
+
+ Button +

Normal

+
+ ${ + ent.button_hover_url + ? ` +
+ Button Hover +

Hover

+
+ ` + : '' + } +
+
+ `; + } + + if (ent.jump_url) { + innerHtml += ` + Jump URL + `; + } + + innerHtml += '
'; + card.innerHTML = innerHtml; + col.appendChild(card); + row.appendChild(col); + } + } catch (e) { + console.warn(`Failed to load ${url}`, e); + contentDiv.innerHTML = '
Failed to load data.
'; + } + }; + + targetSelect.addEventListener('change', () => { + updateLanguages(); + renderContent(); + }); + langSelect.addEventListener('change', renderContent); + + updateLanguages(); + renderContent(); + container.appendChild(outerCard); +} diff --git a/pages-v2/src/main.tsx b/pages-v2/src/main.tsx new file mode 100644 index 0000000..ce150b6 --- /dev/null +++ b/pages-v2/src/main.tsx @@ -0,0 +1,14 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'bootstrap/dist/js/bootstrap.bundle.min.js' +import './assets/css/essentials.css' +import './assets/css/index.css' +import './assets/css/about.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/pages-v2/src/types.ts b/pages-v2/src/types.ts new file mode 100644 index 0000000..7a57601 --- /dev/null +++ b/pages-v2/src/types.ts @@ -0,0 +1,72 @@ +export interface MirrorFileEntry { + orig: string; + mirror: string; + origStatus: boolean; +} + +export interface StoredData { + req: any; + rsp: T; + updatedAt: string; +} + +export interface LauncherWebAnnouncement { + data_version: string; + tabs: { + tabName: string; + announcements: { + content: string; + jump_url: string; + start_ts: string; + id: string; + need_token: boolean; + }[]; + tab_id: string; + }[]; +} + +export interface LauncherWebBanner { + data_version: string; + banners: { + url: string; + md5: string; + jump_url: string; + id: string; + need_token: boolean; + }[]; +} + +export interface LauncherWebMainBgImage { + data_version: string; + main_bg_image: { + url: string; + md5: string; + video_url: string; + }; +} + +export interface LauncherWebSingleEnt { + single_ent: { + version_url: string; + version_md5: string; + jump_url: string; + button_url: string; + button_md5: string; + button_hover_url: string; + button_hover_md5: string; + need_token: boolean; + }; +} + +export interface LauncherWebSidebar { + data_version: string; + sidebars: { + display_type: 'DisplayType_RESERVE'; + media: string; + pic: { url: string; md5: string; description: string } | null; + sidebar_labels: { content: string; jump_url: string; need_token: boolean }[]; + grid_info: null; + jump_url: string; + need_token: boolean; + }[]; +} diff --git a/pages-v2/src/utils/api.ts b/pages-v2/src/utils/api.ts new file mode 100644 index 0000000..2026f17 --- /dev/null +++ b/pages-v2/src/utils/api.ts @@ -0,0 +1,51 @@ +import ky from 'ky'; +import { BASE_URL, gameTargets, launcherTargets, launcherWebApiLang } from './constants'; + +const apiCache = new Map>(); + +export function fetchJson(url: string): Promise { + if (!apiCache.has(url)) { + const promise = ky + .get(url) + .json() + .catch((err) => { + apiCache.delete(url); + throw err; + }); + apiCache.set(url, promise); + } + return apiCache.get(url) as Promise; +} + +export async function preloadData() { + const promises: Promise[] = []; + promises.push(fetchJson(`${BASE_URL}/mirror_file_list.json`)); + const launcherWebApiFolderNames = ['announcement', 'banner', 'main_bg_image', 'sidebar', 'single_ent']; + for (const target of gameTargets) { + promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all.json`)); + promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all_patch.json`)); + for (const apiName of launcherWebApiFolderNames) { + for (const lang of launcherWebApiLang[target.region]) { + promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/web/${target.dirName}/${apiName}/${lang}/all.json`)); + } + } + } + const resTargets = [ + { region: 'os', channel: 6 }, + { region: 'cn', channel: 1 }, + ]; + const platforms = ['Windows', 'Android', 'iOS', 'PlayStation']; + for (const target of resTargets) { + for (const platform of platforms) { + promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/game_resources/${target.channel}/${platform}/all.json`)); + } + } + for (const region of launcherTargets) { + for (const app of region.apps) { + promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/launcher/${app}/${region.channel}/all.json`)); + promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`)); + } + } + + await Promise.all(promises); +} diff --git a/pages-v2/src/utils/constants.ts b/pages-v2/src/utils/constants.ts new file mode 100644 index 0000000..7f177e9 --- /dev/null +++ b/pages-v2/src/utils/constants.ts @@ -0,0 +1,44 @@ +export const BASE_URL = + 'https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output'; + +export const FILE_SIZE_OPTS = { + decimals: 2, + decimalPadding: true, + useBinaryUnit: true, + useBitUnit: false, + unitVisible: true, + unit: null, +}; + +export const gameTargets = [ + { name: 'Official', region: 'os' as const, dirName: '6', channel: 6 }, + { name: 'Epic', region: 'os' as const, dirName: '801', channel: 6 }, + { name: 'Google Play', region: 'os' as const, dirName: '802', channel: 6 }, + { name: 'Official', region: 'cn' as const, dirName: '1', channel: 1 }, + { name: 'Bilibili', region: 'cn' as const, dirName: '2', channel: 2 }, +]; + +export const launcherTargets = [ + { id: 'os', apps: ['EndField', 'Official'], channel: 6 }, + { id: 'cn', apps: ['EndField', 'Arknights', 'Official'], channel: 1 }, +]; + +export const launcherWebApiLang = { + os: [ + 'de-de', + 'en-us', + 'es-mx', + 'fr-fr', + 'id-id', + 'it-it', + 'ja-jp', + 'ko-kr', + 'pt-br', + 'ru-ru', + 'th-th', + 'vi-vn', + 'zh-cn', + 'zh-tw', + ] as const, + cn: ['zh-cn'] as const, +}; diff --git a/pages-v2/src/utils/logger.ts b/pages-v2/src/utils/logger.ts new file mode 100644 index 0000000..58e1f45 --- /dev/null +++ b/pages-v2/src/utils/logger.ts @@ -0,0 +1,12 @@ +import { DateTime } from 'luxon'; + +export default { + write(message: string) { + const debugLogElement = document.querySelector('#debug-log code'); + if (!debugLogElement) return; + const prettyMessage = `${DateTime.now().toFormat('HH:mm:ss.SSS')} > ${message}`; + const divEl = document.createElement('div'); + divEl.textContent = prettyMessage; + debugLogElement.appendChild(divEl); + }, +}; diff --git a/pages-v2/src/utils/math.ts b/pages-v2/src/utils/math.ts new file mode 100644 index 0000000..3e51447 --- /dev/null +++ b/pages-v2/src/utils/math.ts @@ -0,0 +1,162 @@ +import logger from './logger'; + +export default { + arrayMax(array: Array) { + return array.reduce((a, b) => Math.max(a, b)); + }, + + arrayMin(array: Array) { + return array.reduce((a, b) => Math.min(a, b)); + }, + + arrayTotal(array: Array) { + return array.reduce((acc, f) => acc + f, 0); + }, + + arrayAvg(array: Array) { + return this.arrayTotal(array) / array.length; + }, + + rounder(method: 'floor' | 'ceil' | 'round', num: number, n: number) { + const pow = Math.pow(10, n); + let result: number; + switch (method) { + case 'floor': + result = Math.floor(num * pow) / pow; + break; + case 'ceil': + result = Math.ceil(num * pow) / pow; + break; + case 'round': + result = Math.round(num * pow) / pow; + break; + } + return { + orig: result, + padded: result.toFixed(n), + }; + }, + + formatFileSize( + bytes: number, + options: { + decimals: number; + decimalPadding: boolean; + useBinaryUnit: boolean; + useBitUnit: boolean; + unitVisible: boolean; + unit: 'B' | 'K' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y' | null; + }, + ) { + const k = options.useBinaryUnit ? 1024 : 1000; + const dm = options.decimals < 0 ? 0 : options.decimals; + + const baseUnits = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + const binaryUnitSuffix = options.useBitUnit ? 'ib' : 'iB'; + const siUnitSuffix = options.useBitUnit ? 'b' : 'B'; + + const getUnitString = (i: number) => { + if (i === 0) return options.useBitUnit ? 'b' : 'B'; + return baseUnits[i] + (options.useBinaryUnit ? binaryUnitSuffix : siUnitSuffix); + }; + + let value = bytes < 0 ? 0 : Math.floor(bytes); + if (options.useBitUnit) { + value *= 8; + } + + let i: number; + if (options.unit !== null) { + i = baseUnits.indexOf(options.unit); + if (i === -1) throw new Error(`Invalid unit: ${options.unit}`); + } else { + if (value === 0) { + i = 0; + } else { + i = Math.floor(Math.log(value) / Math.log(k)); + i = Math.max(0, Math.min(baseUnits.length - 1, i)); // clamp + } + } + + const resultValue = value / Math.pow(k, i); + + let formattedValue: string; + if (options.decimalPadding) { + formattedValue = resultValue.toFixed(dm); + } else { + formattedValue = resultValue.toFixed(dm).replace(/\.?0+$/, ''); + } + + return formattedValue + (options.unitVisible ? ' ' + getUnitString(i) : ''); + }, + + secureRandomFloatInRange(min: number, max: number): number { + if (min > max) [min, max] = [max, min]; + const crypto = globalThis.crypto; + if (!crypto) { + throw new Error('Cryptographically secure random float number gen is not available'); + } + const randomValues = new Uint32Array(2); + crypto.getRandomValues(randomValues); + const highBits = randomValues[1]! & 0x1fffff; // 0x1FFFFF = 2^21 - 1 + const lowBits = randomValues[0]; + const combined = highBits * 0x100000000 + lowBits!; // 0x100000000 = 2^32 + const randomFraction = combined / 0x20000000000000; // 0x20000000000000 = 2^53 + return randomFraction * (max - min) + min; + }, + + secureRandomIntInRange(min: number, max: number, writeLog: boolean = false): number { + if (min === max) { + writeLog ? logger.write(`randomInt: Range=${min}-${max}, Output=${min}`) : undefined; + return min; + } + if (min > max) [min, max] = [max, min]; + const crypto = globalThis.crypto; + if (!crypto) { + throw new Error('Cryptographically secure random int number gen is not available'); + } + + // convert to integer anyway + const minInt = Math.ceil(min); + const maxInt = Math.floor(max); + + // safe integer check + if (!Number.isSafeInteger(minInt) || !Number.isSafeInteger(maxInt)) { + throw new Error('Range boundaries must be within safe integer limits'); + } + + // valid range check + if (minInt > maxInt) { + throw new Error('Invalid range after integer conversion: min > max'); + } + + const range = maxInt - minInt + 1; + + if (range <= 0 || range > Number.MAX_SAFE_INTEGER) { + throw new Error(`Range size must be between 1 and ${Number.MAX_SAFE_INTEGER} inclusive`); + } + + // 53-bit random num gen + const MAX_53 = BigInt(1) << BigInt(53); // 2^53 + const rangeBigInt = BigInt(range); + const maxAcceptable = MAX_53 - (MAX_53 % rangeBigInt); + + // generate + const randomBuffer = new Uint32Array(2); + while (true) { + crypto.getRandomValues(randomBuffer); + const highBits = randomBuffer[1]! & 0x1fffff; // use lower 21-bit only + const lowBits = randomBuffer[0]; + const combined = BigInt(highBits) * BigInt(0x100000000) + BigInt(lowBits!); // 0x100000000 = 2^32 + // accept condition: combined < maxAcceptable + if (combined < maxAcceptable) { + const offset = Number(combined % rangeBigInt); // 0 to range-1 + const hex = Array.from(new Uint8Array(randomBuffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + writeLog ? logger.write(`randomInt: Range=${min}-${max}, Raw=0x${hex}, Output=${minInt + offset}`) : undefined; + return minInt + offset; + } + } + }, +}; diff --git a/pages-v2/src/utils/ui.ts b/pages-v2/src/utils/ui.ts new file mode 100644 index 0000000..e784598 --- /dev/null +++ b/pages-v2/src/utils/ui.ts @@ -0,0 +1,16 @@ +import type { MirrorFileEntry } from '../types'; + +export function generateDownloadLinks(url: string, mirrorFileDb: MirrorFileEntry[]) { + const cleanUrl = new URL(url); + cleanUrl.search = ''; + const mirrorEntry = mirrorFileDb.find((g) => g.orig.includes(cleanUrl.toString())); + + const links: string[] = []; + if (!mirrorEntry || mirrorEntry.origStatus === true) { + links.push(`Orig`); + } + if (mirrorEntry) { + links.push(`Mirror`); + } + return links.join(' / '); +} diff --git a/pages-v2/tsconfig.app.json b/pages-v2/tsconfig.app.json new file mode 100644 index 0000000..9ac59ff --- /dev/null +++ b/pages-v2/tsconfig.app.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2023", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"], + "exclude": ["src/legacy"] +} diff --git a/pages-v2/tsconfig.json b/pages-v2/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/pages-v2/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/pages-v2/tsconfig.node.json b/pages-v2/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/pages-v2/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/pages-v2/vite.config.ts b/pages-v2/vite.config.ts new file mode 100644 index 0000000..a74ec84 --- /dev/null +++ b/pages-v2/vite.config.ts @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +});