mirror of
https://github.com/daydreamer-json/ak-endfield-api-archive.git
synced 2026-03-29 03:32:43 +02:00
Compare commits
6 Commits
d581273ed6
...
851bb33639
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
851bb33639 | ||
|
|
178d6623c3 | ||
|
|
71bf8664fd | ||
|
|
32cb37884e | ||
|
|
16f6137a82 | ||
|
|
e9ff6633fa |
23
.github/workflows/main.yml
vendored
23
.github/workflows/main.yml
vendored
@@ -34,11 +34,18 @@ jobs:
|
||||
command: bun run src/main.ts archive
|
||||
- name: Format output data
|
||||
run: bun x oxfmt output
|
||||
- name: Git staging, commit, push
|
||||
continue-on-error: true
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add output
|
||||
git commit -m '[Auto] API update'
|
||||
git push
|
||||
# - name: Git staging, commit, push
|
||||
# continue-on-error: true
|
||||
# run: |
|
||||
# git config user.name "github-actions[bot]"
|
||||
# git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
# git add output
|
||||
# git commit -m '[Auto] API update'
|
||||
# git push
|
||||
- name: Git commit and push
|
||||
uses: iarekylew00t/verified-bot-commit@v2
|
||||
with:
|
||||
message: "[Auto] API update"
|
||||
files: |
|
||||
output
|
||||
if-no-commit: info
|
||||
|
||||
@@ -16,6 +16,12 @@ The APIs currently being monitored are as follows:
|
||||
- Get latest game (Global, China)
|
||||
- Get latest game resources (Global, China)
|
||||
- Get latest launcher (Global, China)
|
||||
- Get launcher web resources (Global, China)
|
||||
- Announcement
|
||||
- Banner
|
||||
- Main background image
|
||||
- Single Ent.
|
||||
- Sidebar
|
||||
- Raw
|
||||
- Game resource json (index, patch)
|
||||
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:40.478+00:00",
|
||||
"req": {
|
||||
"appCode": "6LL0KJuqHBVz33WK",
|
||||
"channel": 1,
|
||||
"subChannel": 1,
|
||||
"lang": "zh-cn",
|
||||
"region": "cn",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://hg-utils-public.hycdn.cn/hg-utils/prod/eppcsuwqpaueijqk/6LL0KJuqHBVz33WK/16/ce/16ce42a4e7380ddd39f57ec9e9cd559f.png",
|
||||
"md5": "d97bee4cbaf67db56ec4a71d70f79603",
|
||||
"jump_url": "https://endfield.hypergryph.com/news/7225",
|
||||
"id": "67",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://hg-utils-public.hycdn.cn/hg-utils/prod/eppcsuwqpaueijqk/6LL0KJuqHBVz33WK/2f/fd/2ffd3caaf5d8a7b77c4454a325db118b.png",
|
||||
"md5": "6ba877cd98e73faa4cd1289270ac5037",
|
||||
"jump_url": "https://endfield.hypergryph.com/news/4795",
|
||||
"id": "61",
|
||||
"need_token": true
|
||||
},
|
||||
{
|
||||
"url": "https://hg-utils-public.hycdn.cn/hg-utils/prod/eppcsuwqpaueijqk/6LL0KJuqHBVz33WK/f3/70/f37018e012f068dce1262a72e9bfe5a6.jpg",
|
||||
"md5": "2dc12b155fdd644b23c9d91041ccea3a",
|
||||
"jump_url": "https://www.skland.com/act/endfield/091950d66a48-ob-incentive?header=0",
|
||||
"id": "55",
|
||||
"need_token": true
|
||||
},
|
||||
{
|
||||
"url": "https://hg-utils-public.hycdn.cn/hg-utils/prod/eppcsuwqpaueijqk/6LL0KJuqHBVz33WK/d4/46/d44695842db87dbedfe9c3e23a6d9d99.webp",
|
||||
"md5": "d9b1ab0af1208fc4c2776946590cb498",
|
||||
"jump_url": "https://endfield.hypergryph.com/news/2675",
|
||||
"id": "51",
|
||||
"need_token": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -17,13 +17,6 @@
|
||||
"id": "67",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://hg-utils-public.hycdn.cn/hg-utils/prod/eppcsuwqpaueijqk/6LL0KJuqHBVz33WK/0b/8f/0b8fb1584c0b8950aa34b196ce49486f.png",
|
||||
"md5": "4bf44e183f5b8a1550bde8b534b6f08d",
|
||||
"jump_url": "https://comic.hypergryph.com/talos-ii-historicus/comic/5032",
|
||||
"id": "65",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://hg-utils-public.hycdn.cn/hg-utils/prod/eppcsuwqpaueijqk/6LL0KJuqHBVz33WK/2f/fd/2ffd3caaf5d8a7b77c4454a325db118b.png",
|
||||
"md5": "6ba877cd98e73faa4cd1289270ac5037",
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:36.285+00:00",
|
||||
"req": {
|
||||
"appCode": "YDUTE5gscDZ229CW",
|
||||
"channel": 6,
|
||||
"subChannel": 6,
|
||||
"lang": "en-us",
|
||||
"region": "os",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/1b/f6/1bf61912e9c16e2af277f8d670823706.png",
|
||||
"md5": "8cd844f9016978b4b693338abce3dc74",
|
||||
"jump_url": "https://game.skport.com/endfield/sign-in?header=0&hg_media=launcher&hg_link_campaign=banner",
|
||||
"id": "233",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/c2/f0/c2f0103a37b0a7d0fdc1d3196b2b7fb2.png",
|
||||
"md5": "9d368a05400a1b7331b9facac7874798",
|
||||
"jump_url": "https://endfield.gryphline.com/news/2662",
|
||||
"id": "228",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/18/44/184490133e005e1a1bb04c9abac71c71.png",
|
||||
"md5": "0a4f2e9fd3119e9eaf3d750b4cb522a9",
|
||||
"jump_url": "https://endfield.gryphline.com/news/7029",
|
||||
"id": "45",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/85/fe/85fe5a907cfdc10f07adc46bdf6c01c3.png",
|
||||
"md5": "dab43fe3ec3eb66d111dfb11045376a0",
|
||||
"jump_url": "https://endfield.gryphline.com/en-us/news/4499",
|
||||
"id": "31",
|
||||
"need_token": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
"id": "228",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/66/fb/66fb91ab6084997477880f9d44c9daee.png",
|
||||
"md5": "de20c41bc0349fd26e5ebae97155fc14",
|
||||
"jump_url": "https://endfield.gryphline.com/news/0753",
|
||||
"id": "224",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/18/44/184490133e005e1a1bb04c9abac71c71.png",
|
||||
"md5": "0a4f2e9fd3119e9eaf3d750b4cb522a9",
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:36.696+00:00",
|
||||
"req": {
|
||||
"appCode": "YDUTE5gscDZ229CW",
|
||||
"channel": 6,
|
||||
"subChannel": 6,
|
||||
"lang": "ja-jp",
|
||||
"region": "os",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/87/31/87317ac7c02255ee5c7a93c18f4acf56.png",
|
||||
"md5": "71e0eab5ccca748c67c409fb491fc077",
|
||||
"jump_url": "https://game.skport.com/endfield/sign-in?header=0&hg_media=launcher&hg_link_campaign=banner",
|
||||
"id": "234",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/90/f9/90f98b009057d10fbe9c470362964f5f.png",
|
||||
"md5": "bf285eff24f25be6e0020fd18276d971",
|
||||
"jump_url": "https://endfield.gryphline.com/news/2662",
|
||||
"id": "229",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/e8/e2/e8e296371815eb7de0123b6bdbf3c400.png",
|
||||
"md5": "a58874d82a940ef3899f28a17dc3a0e4",
|
||||
"jump_url": "https://endfield.gryphline.com/news/7029",
|
||||
"id": "46",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/7d/27/7d2716788b2f66ab62d21e3b16b18fee.png",
|
||||
"md5": "dd2f682917b68975cb1c487e548a7297",
|
||||
"jump_url": "https://endfield.gryphline.com/ja-jp/news/4499",
|
||||
"id": "32",
|
||||
"need_token": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
"id": "229",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/15/71/15716f2c8ed709d912b5b4b3abcabf13.png",
|
||||
"md5": "c5aa00b042861b77fac8fa18aeceaaf5",
|
||||
"jump_url": "https://endfield.gryphline.com/news/0753",
|
||||
"id": "225",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/e8/e2/e8e296371815eb7de0123b6bdbf3c400.png",
|
||||
"md5": "a58874d82a940ef3899f28a17dc3a0e4",
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:36.695+00:00",
|
||||
"req": {
|
||||
"appCode": "YDUTE5gscDZ229CW",
|
||||
"channel": 6,
|
||||
"subChannel": 6,
|
||||
"lang": "ko-kr",
|
||||
"region": "os",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/8c/2a/8c2a91410e87affa564a0730e32d60c1.png",
|
||||
"md5": "6a2dd2261109d1f261050ec2876981d5",
|
||||
"jump_url": "https://game.skport.com/endfield/sign-in?header=0&hg_media=launcher&hg_link_campaign=banner",
|
||||
"id": "235",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/4c/cb/4ccbc87521947666cd344a774ccb9bc1.png",
|
||||
"md5": "20274125de1f12311da91b4e6ef41529",
|
||||
"jump_url": "https://endfield.gryphline.com/news/2662",
|
||||
"id": "230",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/ce/17/ce17d6e1514707c3cc1ea7077b28603e.png",
|
||||
"md5": "7d0bfbdb66ce223db0d062450fab5107",
|
||||
"jump_url": "https://endfield.gryphline.com/news/7029",
|
||||
"id": "47",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/d5/9b/d59b71e5cf1db3d14d32f5126a613eb4.png",
|
||||
"md5": "1ff7a484bb5970232d9d270dca0a1e78",
|
||||
"jump_url": "https://endfield.gryphline.com/ko-kr/news/4499",
|
||||
"id": "33",
|
||||
"need_token": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
"id": "230",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/54/31/54315966a6d144c1af78e189f52a72b8.png",
|
||||
"md5": "5e17833519f95280a02710e15291ec5b",
|
||||
"jump_url": "https://endfield.gryphline.com/news/0753",
|
||||
"id": "226",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/ce/17/ce17d6e1514707c3cc1ea7077b28603e.png",
|
||||
"md5": "7d0bfbdb66ce223db0d062450fab5107",
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:37.198+00:00",
|
||||
"req": {
|
||||
"appCode": "YDUTE5gscDZ229CW",
|
||||
"channel": 6,
|
||||
"subChannel": 6,
|
||||
"lang": "zh-tw",
|
||||
"region": "os",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/16/fa/16fac4922271a038d402f69fe1efa1c6.png",
|
||||
"md5": "181368d495b8a64fa45e646e0fef3307",
|
||||
"jump_url": "https://game.skport.com/endfield/sign-in?header=0&hg_media=launcher&hg_link_campaign=banner",
|
||||
"id": "232",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/54/c8/54c8aa54826f90d1678bae0ae104d21b.png",
|
||||
"md5": "fb59697bf5f5dd484d3e58595159e430",
|
||||
"jump_url": "https://endfield.gryphline.com/news/2662",
|
||||
"id": "227",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/f8/ad/f8ad4151cacf91de676f5eca7db6cecc.png",
|
||||
"md5": "f82881cd8a6ad5930b95b26ea0d77dff",
|
||||
"jump_url": "https://endfield.gryphline.com/news/7029",
|
||||
"id": "44",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/8b/86/8b860a8f7ed5ff9fda3b82cc5545f9af.png",
|
||||
"md5": "d98961176d446c7ab41e07846dc58c6d",
|
||||
"jump_url": "https://endfield.gryphline.com/zh-tw/news/4499",
|
||||
"id": "30",
|
||||
"need_token": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
"id": "227",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/36/26/36260d5c200a00d720d713ef3ad8e537.png",
|
||||
"md5": "0b0b7357da9b65f6048a506e326e24fa",
|
||||
"jump_url": "https://endfield.gryphline.com/news/0753",
|
||||
"id": "223",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/f8/ad/f8ad4151cacf91de676f5eca7db6cecc.png",
|
||||
"md5": "f82881cd8a6ad5930b95b26ea0d77dff",
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:37.385+00:00",
|
||||
"req": {
|
||||
"appCode": "YDUTE5gscDZ229CW",
|
||||
"channel": 6,
|
||||
"subChannel": 801,
|
||||
"lang": "en-us",
|
||||
"region": "os",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/1b/f6/1bf61912e9c16e2af277f8d670823706.png",
|
||||
"md5": "8cd844f9016978b4b693338abce3dc74",
|
||||
"jump_url": "https://game.skport.com/endfield/sign-in?header=0&hg_media=launcher&hg_link_campaign=banner",
|
||||
"id": "233",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/c2/f0/c2f0103a37b0a7d0fdc1d3196b2b7fb2.png",
|
||||
"md5": "9d368a05400a1b7331b9facac7874798",
|
||||
"jump_url": "https://endfield.gryphline.com/news/2662",
|
||||
"id": "228",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/18/44/184490133e005e1a1bb04c9abac71c71.png",
|
||||
"md5": "0a4f2e9fd3119e9eaf3d750b4cb522a9",
|
||||
"jump_url": "https://endfield.gryphline.com/news/7029",
|
||||
"id": "45",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/85/fe/85fe5a907cfdc10f07adc46bdf6c01c3.png",
|
||||
"md5": "dab43fe3ec3eb66d111dfb11045376a0",
|
||||
"jump_url": "https://endfield.gryphline.com/en-us/news/4499",
|
||||
"id": "31",
|
||||
"need_token": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
"id": "228",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/66/fb/66fb91ab6084997477880f9d44c9daee.png",
|
||||
"md5": "de20c41bc0349fd26e5ebae97155fc14",
|
||||
"jump_url": "https://endfield.gryphline.com/news/0753",
|
||||
"id": "224",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/18/44/184490133e005e1a1bb04c9abac71c71.png",
|
||||
"md5": "0a4f2e9fd3119e9eaf3d750b4cb522a9",
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:37.788+00:00",
|
||||
"req": {
|
||||
"appCode": "YDUTE5gscDZ229CW",
|
||||
"channel": 6,
|
||||
"subChannel": 801,
|
||||
"lang": "ja-jp",
|
||||
"region": "os",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/87/31/87317ac7c02255ee5c7a93c18f4acf56.png",
|
||||
"md5": "71e0eab5ccca748c67c409fb491fc077",
|
||||
"jump_url": "https://game.skport.com/endfield/sign-in?header=0&hg_media=launcher&hg_link_campaign=banner",
|
||||
"id": "234",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/90/f9/90f98b009057d10fbe9c470362964f5f.png",
|
||||
"md5": "bf285eff24f25be6e0020fd18276d971",
|
||||
"jump_url": "https://endfield.gryphline.com/news/2662",
|
||||
"id": "229",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/e8/e2/e8e296371815eb7de0123b6bdbf3c400.png",
|
||||
"md5": "a58874d82a940ef3899f28a17dc3a0e4",
|
||||
"jump_url": "https://endfield.gryphline.com/news/7029",
|
||||
"id": "46",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/7d/27/7d2716788b2f66ab62d21e3b16b18fee.png",
|
||||
"md5": "dd2f682917b68975cb1c487e548a7297",
|
||||
"jump_url": "https://endfield.gryphline.com/ja-jp/news/4499",
|
||||
"id": "32",
|
||||
"need_token": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
"id": "229",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/15/71/15716f2c8ed709d912b5b4b3abcabf13.png",
|
||||
"md5": "c5aa00b042861b77fac8fa18aeceaaf5",
|
||||
"jump_url": "https://endfield.gryphline.com/news/0753",
|
||||
"id": "225",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/e8/e2/e8e296371815eb7de0123b6bdbf3c400.png",
|
||||
"md5": "a58874d82a940ef3899f28a17dc3a0e4",
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:37.837+00:00",
|
||||
"req": {
|
||||
"appCode": "YDUTE5gscDZ229CW",
|
||||
"channel": 6,
|
||||
"subChannel": 801,
|
||||
"lang": "ko-kr",
|
||||
"region": "os",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/8c/2a/8c2a91410e87affa564a0730e32d60c1.png",
|
||||
"md5": "6a2dd2261109d1f261050ec2876981d5",
|
||||
"jump_url": "https://game.skport.com/endfield/sign-in?header=0&hg_media=launcher&hg_link_campaign=banner",
|
||||
"id": "235",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/4c/cb/4ccbc87521947666cd344a774ccb9bc1.png",
|
||||
"md5": "20274125de1f12311da91b4e6ef41529",
|
||||
"jump_url": "https://endfield.gryphline.com/news/2662",
|
||||
"id": "230",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/ce/17/ce17d6e1514707c3cc1ea7077b28603e.png",
|
||||
"md5": "7d0bfbdb66ce223db0d062450fab5107",
|
||||
"jump_url": "https://endfield.gryphline.com/news/7029",
|
||||
"id": "47",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/d5/9b/d59b71e5cf1db3d14d32f5126a613eb4.png",
|
||||
"md5": "1ff7a484bb5970232d9d270dca0a1e78",
|
||||
"jump_url": "https://endfield.gryphline.com/ko-kr/news/4499",
|
||||
"id": "33",
|
||||
"need_token": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
"id": "230",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/54/31/54315966a6d144c1af78e189f52a72b8.png",
|
||||
"md5": "5e17833519f95280a02710e15291ec5b",
|
||||
"jump_url": "https://endfield.gryphline.com/news/0753",
|
||||
"id": "226",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/ce/17/ce17d6e1514707c3cc1ea7077b28603e.png",
|
||||
"md5": "7d0bfbdb66ce223db0d062450fab5107",
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:38.364+00:00",
|
||||
"req": {
|
||||
"appCode": "YDUTE5gscDZ229CW",
|
||||
"channel": 6,
|
||||
"subChannel": 801,
|
||||
"lang": "zh-tw",
|
||||
"region": "os",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/16/fa/16fac4922271a038d402f69fe1efa1c6.png",
|
||||
"md5": "181368d495b8a64fa45e646e0fef3307",
|
||||
"jump_url": "https://game.skport.com/endfield/sign-in?header=0&hg_media=launcher&hg_link_campaign=banner",
|
||||
"id": "232",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/54/c8/54c8aa54826f90d1678bae0ae104d21b.png",
|
||||
"md5": "fb59697bf5f5dd484d3e58595159e430",
|
||||
"jump_url": "https://endfield.gryphline.com/news/2662",
|
||||
"id": "227",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/f8/ad/f8ad4151cacf91de676f5eca7db6cecc.png",
|
||||
"md5": "f82881cd8a6ad5930b95b26ea0d77dff",
|
||||
"jump_url": "https://endfield.gryphline.com/news/7029",
|
||||
"id": "44",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/8b/86/8b860a8f7ed5ff9fda3b82cc5545f9af.png",
|
||||
"md5": "d98961176d446c7ab41e07846dc58c6d",
|
||||
"jump_url": "https://endfield.gryphline.com/zh-tw/news/4499",
|
||||
"id": "30",
|
||||
"need_token": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
"id": "227",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/36/26/36260d5c200a00d720d713ef3ad8e537.png",
|
||||
"md5": "0b0b7357da9b65f6048a506e326e24fa",
|
||||
"jump_url": "https://endfield.gryphline.com/news/0753",
|
||||
"id": "223",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/f8/ad/f8ad4151cacf91de676f5eca7db6cecc.png",
|
||||
"md5": "f82881cd8a6ad5930b95b26ea0d77dff",
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:38.482+00:00",
|
||||
"req": {
|
||||
"appCode": "YDUTE5gscDZ229CW",
|
||||
"channel": 6,
|
||||
"subChannel": 802,
|
||||
"lang": "en-us",
|
||||
"region": "os",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/1b/f6/1bf61912e9c16e2af277f8d670823706.png",
|
||||
"md5": "8cd844f9016978b4b693338abce3dc74",
|
||||
"jump_url": "https://game.skport.com/endfield/sign-in?header=0&hg_media=launcher&hg_link_campaign=banner",
|
||||
"id": "233",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/c2/f0/c2f0103a37b0a7d0fdc1d3196b2b7fb2.png",
|
||||
"md5": "9d368a05400a1b7331b9facac7874798",
|
||||
"jump_url": "https://endfield.gryphline.com/news/2662",
|
||||
"id": "228",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/18/44/184490133e005e1a1bb04c9abac71c71.png",
|
||||
"md5": "0a4f2e9fd3119e9eaf3d750b4cb522a9",
|
||||
"jump_url": "https://endfield.gryphline.com/news/7029",
|
||||
"id": "45",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/85/fe/85fe5a907cfdc10f07adc46bdf6c01c3.png",
|
||||
"md5": "dab43fe3ec3eb66d111dfb11045376a0",
|
||||
"jump_url": "https://endfield.gryphline.com/en-us/news/4499",
|
||||
"id": "31",
|
||||
"need_token": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
"id": "228",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/66/fb/66fb91ab6084997477880f9d44c9daee.png",
|
||||
"md5": "de20c41bc0349fd26e5ebae97155fc14",
|
||||
"jump_url": "https://endfield.gryphline.com/news/0753",
|
||||
"id": "224",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/18/44/184490133e005e1a1bb04c9abac71c71.png",
|
||||
"md5": "0a4f2e9fd3119e9eaf3d750b4cb522a9",
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:38.852+00:00",
|
||||
"req": {
|
||||
"appCode": "YDUTE5gscDZ229CW",
|
||||
"channel": 6,
|
||||
"subChannel": 802,
|
||||
"lang": "ja-jp",
|
||||
"region": "os",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/87/31/87317ac7c02255ee5c7a93c18f4acf56.png",
|
||||
"md5": "71e0eab5ccca748c67c409fb491fc077",
|
||||
"jump_url": "https://game.skport.com/endfield/sign-in?header=0&hg_media=launcher&hg_link_campaign=banner",
|
||||
"id": "234",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/90/f9/90f98b009057d10fbe9c470362964f5f.png",
|
||||
"md5": "bf285eff24f25be6e0020fd18276d971",
|
||||
"jump_url": "https://endfield.gryphline.com/news/2662",
|
||||
"id": "229",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/e8/e2/e8e296371815eb7de0123b6bdbf3c400.png",
|
||||
"md5": "a58874d82a940ef3899f28a17dc3a0e4",
|
||||
"jump_url": "https://endfield.gryphline.com/news/7029",
|
||||
"id": "46",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/7d/27/7d2716788b2f66ab62d21e3b16b18fee.png",
|
||||
"md5": "dd2f682917b68975cb1c487e548a7297",
|
||||
"jump_url": "https://endfield.gryphline.com/ja-jp/news/4499",
|
||||
"id": "32",
|
||||
"need_token": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
"id": "229",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/15/71/15716f2c8ed709d912b5b4b3abcabf13.png",
|
||||
"md5": "c5aa00b042861b77fac8fa18aeceaaf5",
|
||||
"jump_url": "https://endfield.gryphline.com/news/0753",
|
||||
"id": "225",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/e8/e2/e8e296371815eb7de0123b6bdbf3c400.png",
|
||||
"md5": "a58874d82a940ef3899f28a17dc3a0e4",
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:38.945+00:00",
|
||||
"req": {
|
||||
"appCode": "YDUTE5gscDZ229CW",
|
||||
"channel": 6,
|
||||
"subChannel": 802,
|
||||
"lang": "ko-kr",
|
||||
"region": "os",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/8c/2a/8c2a91410e87affa564a0730e32d60c1.png",
|
||||
"md5": "6a2dd2261109d1f261050ec2876981d5",
|
||||
"jump_url": "https://game.skport.com/endfield/sign-in?header=0&hg_media=launcher&hg_link_campaign=banner",
|
||||
"id": "235",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/4c/cb/4ccbc87521947666cd344a774ccb9bc1.png",
|
||||
"md5": "20274125de1f12311da91b4e6ef41529",
|
||||
"jump_url": "https://endfield.gryphline.com/news/2662",
|
||||
"id": "230",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/ce/17/ce17d6e1514707c3cc1ea7077b28603e.png",
|
||||
"md5": "7d0bfbdb66ce223db0d062450fab5107",
|
||||
"jump_url": "https://endfield.gryphline.com/news/7029",
|
||||
"id": "47",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/d5/9b/d59b71e5cf1db3d14d32f5126a613eb4.png",
|
||||
"md5": "1ff7a484bb5970232d9d270dca0a1e78",
|
||||
"jump_url": "https://endfield.gryphline.com/ko-kr/news/4499",
|
||||
"id": "33",
|
||||
"need_token": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
"id": "230",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/54/31/54315966a6d144c1af78e189f52a72b8.png",
|
||||
"md5": "5e17833519f95280a02710e15291ec5b",
|
||||
"jump_url": "https://endfield.gryphline.com/news/0753",
|
||||
"id": "226",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/ce/17/ce17d6e1514707c3cc1ea7077b28603e.png",
|
||||
"md5": "7d0bfbdb66ce223db0d062450fab5107",
|
||||
|
||||
@@ -49,5 +49,49 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"updatedAt": "2026-03-07T16:00:39.419+00:00",
|
||||
"req": {
|
||||
"appCode": "YDUTE5gscDZ229CW",
|
||||
"channel": 6,
|
||||
"subChannel": 802,
|
||||
"lang": "zh-tw",
|
||||
"region": "os",
|
||||
"platform": "Windows"
|
||||
},
|
||||
"rsp": {
|
||||
"data_version": "",
|
||||
"banners": [
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/16/fa/16fac4922271a038d402f69fe1efa1c6.png",
|
||||
"md5": "181368d495b8a64fa45e646e0fef3307",
|
||||
"jump_url": "https://game.skport.com/endfield/sign-in?header=0&hg_media=launcher&hg_link_campaign=banner",
|
||||
"id": "232",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/54/c8/54c8aa54826f90d1678bae0ae104d21b.png",
|
||||
"md5": "fb59697bf5f5dd484d3e58595159e430",
|
||||
"jump_url": "https://endfield.gryphline.com/news/2662",
|
||||
"id": "227",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/f8/ad/f8ad4151cacf91de676f5eca7db6cecc.png",
|
||||
"md5": "f82881cd8a6ad5930b95b26ea0d77dff",
|
||||
"jump_url": "https://endfield.gryphline.com/news/7029",
|
||||
"id": "44",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/8b/86/8b860a8f7ed5ff9fda3b82cc5545f9af.png",
|
||||
"md5": "d98961176d446c7ab41e07846dc58c6d",
|
||||
"jump_url": "https://endfield.gryphline.com/zh-tw/news/4499",
|
||||
"id": "30",
|
||||
"need_token": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
"id": "227",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/36/26/36260d5c200a00d720d713ef3ad8e537.png",
|
||||
"md5": "0b0b7357da9b65f6048a506e326e24fa",
|
||||
"jump_url": "https://endfield.gryphline.com/news/0753",
|
||||
"id": "223",
|
||||
"need_token": false
|
||||
},
|
||||
{
|
||||
"url": "https://gl-utils-public.hg-cdn.com/hg-utils/prod/eppcsuwqpaueijqk/YDUTE5gscDZ229CW/f8/ad/f8ad4151cacf91de676f5eca7db6cecc.png",
|
||||
"md5": "f82881cd8a6ad5930b95b26ea0d77dff",
|
||||
|
||||
51
pages/src/assets/ts/api.ts
Normal file
51
pages/src/assets/ts/api.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import ky from 'ky';
|
||||
import { BASE_URL, gameTargets, launcherTargets, launcherWebApiLang } from './utils/constants.js';
|
||||
|
||||
const apiCache = new Map<string, Promise<any>>();
|
||||
|
||||
export function fetchJson<T>(url: string): Promise<T> {
|
||||
if (!apiCache.has(url)) {
|
||||
const promise = ky
|
||||
.get(url)
|
||||
.json<T>()
|
||||
.catch((err) => {
|
||||
apiCache.delete(url);
|
||||
throw err;
|
||||
});
|
||||
apiCache.set(url, promise);
|
||||
}
|
||||
return apiCache.get(url) as Promise<T>;
|
||||
}
|
||||
|
||||
export async function preloadData() {
|
||||
const promises: Promise<any>[] = [];
|
||||
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);
|
||||
}
|
||||
@@ -1,93 +1,19 @@
|
||||
// import * as bootstrap from 'bootstrap';
|
||||
import ky from 'ky';
|
||||
import { DateTime } from 'luxon';
|
||||
import * as semver from 'semver';
|
||||
import math from './utils/math.js';
|
||||
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();
|
||||
});
|
||||
|
||||
const BASE_URL = 'https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output';
|
||||
|
||||
const FILE_SIZE_OPTS = {
|
||||
decimals: 2,
|
||||
decimalPadding: true,
|
||||
useBinaryUnit: true,
|
||||
useBitUnit: false,
|
||||
unitVisible: true,
|
||||
unit: null,
|
||||
};
|
||||
|
||||
interface MirrorFileEntry {
|
||||
orig: string;
|
||||
mirror: string;
|
||||
origStatus: boolean;
|
||||
}
|
||||
|
||||
interface StoredData<T> {
|
||||
req: any;
|
||||
rsp: T;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const gameTargets = [
|
||||
{ name: 'Official', region: 'os', dirName: '6', channel: 6 },
|
||||
{ name: 'Epic', region: 'os', dirName: '801', channel: 6 },
|
||||
{ name: 'Google Play', region: 'os', dirName: '802', channel: 6 },
|
||||
{ name: 'Official', region: 'cn', dirName: '1', channel: 1 },
|
||||
{ name: 'Bilibili', region: 'cn', dirName: '2', channel: 2 },
|
||||
];
|
||||
|
||||
const launcherTargets = [
|
||||
{ id: 'os', apps: ['EndField', 'Official'], channel: 6 },
|
||||
{ id: 'cn', apps: ['EndField', 'Arknights', 'Official'], channel: 1 },
|
||||
];
|
||||
|
||||
const apiCache = new Map<string, Promise<any>>();
|
||||
|
||||
function fetchJson<T>(url: string): Promise<T> {
|
||||
if (!apiCache.has(url)) {
|
||||
const promise = ky
|
||||
.get(url)
|
||||
.json<T>()
|
||||
.catch((err) => {
|
||||
apiCache.delete(url);
|
||||
throw err;
|
||||
});
|
||||
apiCache.set(url, promise);
|
||||
}
|
||||
return apiCache.get(url) as Promise<T>;
|
||||
}
|
||||
|
||||
let mirrorFileDb: MirrorFileEntry[] = [];
|
||||
|
||||
async function preloadData() {
|
||||
const promises: Promise<any>[] = [];
|
||||
promises.push(fetchJson(`${BASE_URL}/mirror_file_list.json`));
|
||||
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`));
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const contentDiv = document.getElementById('content');
|
||||
if (!contentDiv) return;
|
||||
@@ -107,6 +33,7 @@ async function main() {
|
||||
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-patch" type="button">Patches</button></li>
|
||||
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-resources" type="button">Resources</button></li>
|
||||
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-launcher" type="button">Launcher</button></li>
|
||||
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-web-pretty" type="button">Web</button></li>
|
||||
</ul>
|
||||
<div class="tab-content p-3 border border-top-0 rounded-bottom" id="mainTabsContent">
|
||||
<div class="tab-pane fade show active" id="tab-overview" role="tabpanel"></div>
|
||||
@@ -114,589 +41,17 @@ async function main() {
|
||||
<div class="tab-pane fade" id="tab-patch" role="tabpanel"></div>
|
||||
<div class="tab-pane fade" id="tab-resources" role="tabpanel"></div>
|
||||
<div class="tab-pane fade" id="tab-launcher" role="tabpanel"></div>
|
||||
<div class="tab-pane fade" id="tab-web-pretty" role="tabpanel"></div>
|
||||
</div>
|
||||
`;
|
||||
contentDiv.innerHTML = tabsHtml;
|
||||
|
||||
await Promise.all([
|
||||
renderOverview(document.getElementById('tab-overview')!),
|
||||
renderGamePackages(document.getElementById('tab-game')!),
|
||||
renderPatches(document.getElementById('tab-patch')!),
|
||||
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')!),
|
||||
renderLaunchers(document.getElementById('tab-launcher')!, mirrorFileDb),
|
||||
renderWebPretty(document.getElementById('tab-web-pretty')!),
|
||||
]);
|
||||
}
|
||||
|
||||
async function renderOverview(container: HTMLElement) {
|
||||
const mirrorOrigSet = new Set<string>();
|
||||
for (const m of mirrorFileDb) {
|
||||
try {
|
||||
const u = new URL(m.orig);
|
||||
u.search = '';
|
||||
mirrorOrigSet.add(u.toString());
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const countedUrls = new Set<string>();
|
||||
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';
|
||||
sectionIn.innerHTML = `
|
||||
<h3 class="card-title">Latest Game Packages</h3>
|
||||
<p class="text-center lh-1">
|
||||
<span class="fw-bold fs-1">${await (
|
||||
async () => {
|
||||
const url = `${BASE_URL}/akEndfield/launcher/game/6/all.json`;
|
||||
const dat = await fetchJson<StoredData<any>[]>(url);
|
||||
return dat.at(-1)?.rsp.version;
|
||||
}
|
||||
)()}</span><br />
|
||||
Latest Version (Global)
|
||||
</p>
|
||||
<p class="text-center lh-1">
|
||||
<span class="fw-bold fs-1">${await (
|
||||
async () => {
|
||||
const url = `${BASE_URL}/akEndfield/launcher/game/1/all.json`;
|
||||
const dat = await fetchJson<StoredData<any>[]>(url);
|
||||
return dat.at(-1)?.rsp.version;
|
||||
}
|
||||
)()}</span><br />
|
||||
Latest Version (China)
|
||||
</p>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Region</th>
|
||||
<th>Channel</th>
|
||||
<th>Version</th>
|
||||
<th class="text-end">Packed</th>
|
||||
<th class="text-end">Unpacked</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
`;
|
||||
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<StoredData<any>[]>(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 = `
|
||||
<td>${target.region === 'cn' ? 'China' : 'Global'}</td>
|
||||
<td>${target.name}</td>
|
||||
<td>${version}</td>
|
||||
<td class="text-end">${math.formatFileSize(packedSize, FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-end">${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)}</td>
|
||||
`;
|
||||
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<StoredData<any>[]>(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<StoredData<any>[]>(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<StoredData<any>[]>(urlExe);
|
||||
for (const e of dataExe) {
|
||||
checkAndAddSize(e.rsp.exe_url, parseInt(e.rsp.exe_size));
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
const mirrorSection = document.createElement('div');
|
||||
mirrorSection.className = 'card';
|
||||
mirrorSection.innerHTML = `
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Mirror Statistics</h3>
|
||||
<p class="card-text text-center lh-1">
|
||||
<span class="fw-bold fs-1">${math.formatFileSize(totalMirrorSize, { ...FILE_SIZE_OPTS, unit: 'G' })}</span><br />
|
||||
uploaded to mirror
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(mirrorSection);
|
||||
}
|
||||
|
||||
async function renderGamePackages(container: HTMLElement) {
|
||||
for (const target of gameTargets) {
|
||||
const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all.json`;
|
||||
try {
|
||||
const data = await fetchJson<StoredData<any>[]>(url);
|
||||
const section = document.createElement('div');
|
||||
section.className = 'mb-5';
|
||||
section.innerHTML = `<h3 class="mb-3">${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}</h3>`;
|
||||
|
||||
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 += `<tr>
|
||||
<td>${fileName(f)}</td>
|
||||
<td><code>${f.md5}</code></td>
|
||||
<td class="text-end">${math.formatFileSize(parseInt(f.package_size), FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-center">${generateDownloadLinks(f.url)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const itemId = `game-${target.dirName}-${i}`;
|
||||
// const isExpanded = i === 0;
|
||||
const isExpanded = false;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'accordion-item';
|
||||
item.innerHTML = `
|
||||
<h2 class="accordion-header" id="heading-${itemId}">
|
||||
<button class="accordion-button ${isExpanded ? '' : 'collapsed'}" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${itemId}" aria-expanded="${isExpanded}" aria-controls="collapse-${itemId}">
|
||||
<div class="d-flex w-100 justify-content-between me-3">
|
||||
<span class="fw-bold">${version}</span>
|
||||
<span class="text-muted small align-bottom">${dateStr}</span>
|
||||
</div>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-${itemId}" class="accordion-collapse collapse ${isExpanded ? 'show' : ''}" aria-labelledby="heading-${itemId}" data-bs-parent="#${accordion.id}">
|
||||
<div class="accordion-body">
|
||||
<table class="table table-sm table-borderless w-auto mb-2">
|
||||
<tr><td>Unpacked Size</td><td class="text-end fw-bold">${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)}</td></tr>
|
||||
<tr><td>Packed Size</td><td class="text-end fw-bold">${math.formatFileSize(packedSize, FILE_SIZE_OPTS)}</td></tr>
|
||||
</table>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-sm align-middle text-nowrap">
|
||||
<thead><tr><th>File</th><th>MD5 Checksum</th><th class="text-end">Size</th><th class="text-center">DL</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
accordion.appendChild(item);
|
||||
}
|
||||
section.appendChild(accordion);
|
||||
container.appendChild(section);
|
||||
} catch (err) {
|
||||
// Ignore 404 or errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPatches(container: HTMLElement) {
|
||||
for (const target of gameTargets) {
|
||||
const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all_patch.json`;
|
||||
try {
|
||||
const data = await fetchJson<StoredData<any>[]>(url);
|
||||
if (data.length === 0) continue;
|
||||
|
||||
const section = document.createElement('div');
|
||||
section.className = 'mb-5';
|
||||
section.innerHTML = `<h3 class="mb-3">${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}</h3>`;
|
||||
|
||||
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 += `<tr>
|
||||
<td>${fileName(e.rsp.patch.url)}</td>
|
||||
<td><code>${e.rsp.patch.md5}</code></td>
|
||||
<td class="text-end">${math.formatFileSize(parseInt(e.rsp.patch.package_size), FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-center">${generateDownloadLinks(e.rsp.patch.url)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
for (const f of e.rsp.patch.patches) {
|
||||
rows += `<tr>
|
||||
<td>${fileName(f.url)}</td>
|
||||
<td><code>${f.md5}</code></td>
|
||||
<td class="text-end">${math.formatFileSize(parseInt(f.package_size), FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-center">${generateDownloadLinks(f.url)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const itemId = `patch-${target.dirName}-${itemIndex}`;
|
||||
// const isExpanded = itemIndex === 0;
|
||||
const isExpanded = false;
|
||||
itemIndex++;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'accordion-item';
|
||||
item.innerHTML = `
|
||||
<h2 class="accordion-header" id="heading-${itemId}">
|
||||
<button class="accordion-button ${isExpanded ? '' : 'collapsed'}" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${itemId}" aria-expanded="${isExpanded}" aria-controls="collapse-${itemId}">
|
||||
<div class="d-flex w-100 justify-content-between me-3">
|
||||
<span class="fw-bold">${reqVersion} → ${version}</span>
|
||||
<span class="text-muted small">${dateStr}</span>
|
||||
</div>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-${itemId}" class="accordion-collapse collapse ${isExpanded ? 'show' : ''}" aria-labelledby="heading-${itemId}" data-bs-parent="#${accordion.id}">
|
||||
<div class="accordion-body">
|
||||
<table class="table table-sm table-borderless w-auto mb-2">
|
||||
<tr><td>Unpacked Size</td><td class="text-end fw-bold">${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)}</td></tr>
|
||||
<tr><td>Packed Size</td><td class="text-end fw-bold">${math.formatFileSize(packedSize, FILE_SIZE_OPTS)}</td></tr>
|
||||
</table>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-sm align-middle text-nowrap">
|
||||
<thead><tr><th>File</th><th>MD5 Checksum</th><th class="text-end">Size</th><th class="text-center">DL</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
accordion.appendChild(item);
|
||||
}
|
||||
section.appendChild(accordion);
|
||||
container.appendChild(section);
|
||||
} catch (err) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function renderResources(container: HTMLElement) {
|
||||
const platforms = ['Windows', 'Android', 'iOS', 'PlayStation'];
|
||||
// Filter unique channels (OS: 6, CN: 1), excluding Bilibili (2) as per archive.ts logic
|
||||
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 = `<h3 class="mb-3">${target.region === 'cn' ? 'China' : 'Global'}</h3>`;
|
||||
|
||||
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<StoredData<any>[]>(url);
|
||||
|
||||
// Group by res_version
|
||||
const resVersionMap = new Map<string, { rsp: StoredData<any>; versions: Set<string> }>();
|
||||
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),
|
||||
}));
|
||||
|
||||
let rows = '';
|
||||
for (const item of resVersionSet.reverse()) {
|
||||
// Newest first
|
||||
const dateStr = DateTime.fromISO(item.rsp.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss');
|
||||
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 += `<tr>
|
||||
<td style="font-feature-settings: 'tnum'">${dateStr}</td>
|
||||
<td><a href="${initialRes.path}" target="_blank">${initialRes.version}</a></td>
|
||||
<td><a href="${mainRes.path}" target="_blank">${mainRes.version}</a></td>
|
||||
<td class="text-center">${isKick ? '✅' : ''}</td>
|
||||
<td>${item.versions.join(', ')}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const itemId = `res-${target.region}-${target.channel}-${platform}`;
|
||||
// const isExpanded = itemIndex === 0;
|
||||
const isExpanded = false;
|
||||
itemIndex++;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'accordion-item';
|
||||
item.innerHTML = `
|
||||
<h2 class="accordion-header" id="heading-${itemId}">
|
||||
<button class="accordion-button ${isExpanded ? '' : 'collapsed'}" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${itemId}" aria-expanded="${isExpanded}" aria-controls="collapse-${itemId}">
|
||||
${platform}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-${itemId}" class="accordion-collapse collapse ${isExpanded ? 'show' : ''}" aria-labelledby="heading-${itemId}" data-bs-parent="#${accordion.id}">
|
||||
<div class="accordion-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-sm align-middle text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Initial</th>
|
||||
<th>Main</th>
|
||||
<th>Kick</th>
|
||||
<th>Game version</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
accordion.appendChild(item);
|
||||
} catch (err) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
if (accordion.childElementCount > 0) {
|
||||
section.appendChild(accordion);
|
||||
container.appendChild(section);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function renderLaunchers(container: HTMLElement) {
|
||||
for (const region of launcherTargets) {
|
||||
for (const app of region.apps) {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'mb-5';
|
||||
section.innerHTML = `<h3 class="mb-3">${region.id.toUpperCase()} ${app}</h3>`;
|
||||
|
||||
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<StoredData<any>[]>(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 += `<tr>
|
||||
<td>${dateStr}</td>
|
||||
<td>${e.rsp.version}</td>
|
||||
<td>${fileName}</td>
|
||||
<td><code>${e.rsp.md5}</code></td>
|
||||
<td class="text-end">${math.formatFileSize(unpacked, FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-end">${math.formatFileSize(parseInt(e.rsp.package_size), FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-center">${generateDownloadLinks(e.rsp.zip_package_url)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const itemId = `launcher-zip-${region.id}-${app}`;
|
||||
// const isExpanded = itemIndex === 0;
|
||||
const isExpanded = false;
|
||||
itemIndex++;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'accordion-item';
|
||||
item.innerHTML = `
|
||||
<h2 class="accordion-header" id="heading-${itemId}">
|
||||
<button class="accordion-button ${isExpanded ? '' : 'collapsed'}" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${itemId}" aria-expanded="${isExpanded}" aria-controls="collapse-${itemId}">
|
||||
Launcher Packages (zip)
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-${itemId}" class="accordion-collapse collapse ${isExpanded ? 'show' : ''}" aria-labelledby="heading-${itemId}" data-bs-parent="#${accordion.id}">
|
||||
<div class="accordion-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-sm align-middle text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Version</th>
|
||||
<th>File</th>
|
||||
<th>MD5 Checksum</th>
|
||||
<th class="text-end">Unpacked</th>
|
||||
<th class="text-end">Packed</th>
|
||||
<th class="text-center">DL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
accordion.appendChild(item);
|
||||
} catch (e) {}
|
||||
|
||||
// Exe
|
||||
try {
|
||||
const urlExe = `${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`;
|
||||
const dataExe = await fetchJson<StoredData<any>[]>(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 += `<tr>
|
||||
<td>${dateStr}</td>
|
||||
<td>${e.rsp.version}</td>
|
||||
<td>${fileName}</td>
|
||||
<td class="text-end">${math.formatFileSize(parseInt(e.rsp.exe_size), FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-center">${generateDownloadLinks(e.rsp.exe_url)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const itemId = `launcher-exe-${region.id}-${app}`;
|
||||
// const isExpanded = itemIndex === 0;
|
||||
const isExpanded = false;
|
||||
itemIndex++;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'accordion-item';
|
||||
item.innerHTML = `
|
||||
<h2 class="accordion-header" id="heading-${itemId}">
|
||||
<button class="accordion-button ${isExpanded ? '' : 'collapsed'}" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${itemId}" aria-expanded="${isExpanded}" aria-controls="collapse-${itemId}">
|
||||
Launcher Packages (Installer)
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-${itemId}" class="accordion-collapse collapse ${isExpanded ? 'show' : ''}" aria-labelledby="heading-${itemId}" data-bs-parent="#${accordion.id}">
|
||||
<div class="accordion-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-sm align-middle text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Version</th>
|
||||
<th>File</th>
|
||||
<th class="text-end">Size</th>
|
||||
<th class="text-center">DL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
accordion.appendChild(item);
|
||||
} catch (e) {}
|
||||
|
||||
if (accordion.childElementCount > 0) {
|
||||
section.appendChild(accordion);
|
||||
container.appendChild(section);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Utils ---
|
||||
|
||||
function generateDownloadLinks(url: string) {
|
||||
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(`<a href="${url}" target="_blank">Orig</a>`);
|
||||
}
|
||||
if (mirrorEntry) {
|
||||
links.push(`<a href="${mirrorEntry.mirror}" target="_blank">Mirror</a>`);
|
||||
}
|
||||
return links.join(' / ');
|
||||
}
|
||||
|
||||
78
pages/src/assets/ts/renderers/gamePackages.ts
Normal file
78
pages/src/assets/ts/renderers/gamePackages.ts
Normal file
@@ -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<StoredData<any>[]>(url);
|
||||
const section = document.createElement('div');
|
||||
section.className = 'mb-5';
|
||||
section.innerHTML = `<h3 class="mb-3">${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}</h3>`;
|
||||
|
||||
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 += `<tr>
|
||||
<td>${fileName(f)}</td>
|
||||
<td><code>${f.md5}</code></td>
|
||||
<td class="text-end">${math.formatFileSize(parseInt(f.package_size), FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-center">${generateDownloadLinks(f.url, mirrorFileDb)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const itemId = `game-${target.dirName}-${i}`;
|
||||
const isExpanded = false;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'accordion-item';
|
||||
item.innerHTML = `
|
||||
<h2 class="accordion-header" id="heading-${itemId}">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${itemId}" aria-expanded="${isExpanded}" aria-controls="collapse-${itemId}">
|
||||
<div class="d-flex w-100 justify-content-between me-3">
|
||||
<span class="fw-bold">${version}</span>
|
||||
<span class="text-muted small align-bottom">${dateStr}</span>
|
||||
</div>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-${itemId}" class="accordion-collapse collapse ${isExpanded ? 'show' : ''}" aria-labelledby="heading-${itemId}" data-bs-parent="#${accordion.id}">
|
||||
<div class="accordion-body">
|
||||
<table class="table table-sm table-borderless w-auto mb-2">
|
||||
<tr><td>Unpacked Size</td><td class="text-end fw-bold">${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)}</td></tr>
|
||||
<tr><td>Packed Size</td><td class="text-end fw-bold">${math.formatFileSize(packedSize, FILE_SIZE_OPTS)}</td></tr>
|
||||
</table>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-sm align-middle text-nowrap">
|
||||
<thead><tr><th>File</th><th>MD5 Checksum</th><th class="text-end">Size</th><th class="text-center">DL</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
accordion.appendChild(item);
|
||||
}
|
||||
section.appendChild(accordion);
|
||||
container.appendChild(section);
|
||||
} catch (err) {
|
||||
// Ignore 404 or errors
|
||||
}
|
||||
}
|
||||
}
|
||||
137
pages/src/assets/ts/renderers/launchers.ts
Normal file
137
pages/src/assets/ts/renderers/launchers.ts
Normal file
@@ -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 = `<h3 class="mb-3">${region.id.toUpperCase()} ${app}</h3>`;
|
||||
|
||||
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<StoredData<any>[]>(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 += `<tr>
|
||||
<td>${dateStr}</td>
|
||||
<td>${e.rsp.version}</td>
|
||||
<td>${fileName}</td>
|
||||
<td><code>${e.rsp.md5}</code></td>
|
||||
<td class="text-end">${math.formatFileSize(unpacked, FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-end">${math.formatFileSize(parseInt(e.rsp.package_size), FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-center">${generateDownloadLinks(e.rsp.zip_package_url, mirrorFileDb)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const itemId = `launcher-zip-${region.id}-${app}`;
|
||||
const isExpanded = false;
|
||||
itemIndex++;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'accordion-item';
|
||||
item.innerHTML = `
|
||||
<h2 class="accordion-header" id="heading-${itemId}">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${itemId}" aria-expanded="${isExpanded}" aria-controls="collapse-${itemId}">
|
||||
Launcher Packages (zip)
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-${itemId}" class="accordion-collapse collapse ${isExpanded ? 'show' : ''}" aria-labelledby="heading-${itemId}" data-bs-parent="#${accordion.id}">
|
||||
<div class="accordion-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-sm align-middle text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Version</th>
|
||||
<th>File</th>
|
||||
<th>MD5 Checksum</th>
|
||||
<th class="text-end">Unpacked</th>
|
||||
<th class="text-end">Packed</th>
|
||||
<th class="text-center">DL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
accordion.appendChild(item);
|
||||
} catch (e) {}
|
||||
|
||||
// Exe
|
||||
try {
|
||||
const urlExe = `${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`;
|
||||
const dataExe = await fetchJson<StoredData<any>[]>(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 += `<tr>
|
||||
<td>${dateStr}</td>
|
||||
<td>${e.rsp.version}</td>
|
||||
<td>${fileName}</td>
|
||||
<td class="text-end">${math.formatFileSize(parseInt(e.rsp.exe_size), FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-center">${generateDownloadLinks(e.rsp.exe_url, mirrorFileDb)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const itemId = `launcher-exe-${region.id}-${app}`;
|
||||
const isExpanded = false;
|
||||
itemIndex++;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'accordion-item';
|
||||
item.innerHTML = `
|
||||
<h2 class="accordion-header" id="heading-${itemId}">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${itemId}" aria-expanded="${isExpanded}" aria-controls="collapse-${itemId}">
|
||||
Launcher Packages (Installer)
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-${itemId}" class="accordion-collapse collapse ${isExpanded ? 'show' : ''}" aria-labelledby="heading-${itemId}" data-bs-parent="#${accordion.id}">
|
||||
<div class="accordion-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-sm align-middle text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Version</th>
|
||||
<th>File</th>
|
||||
<th class="text-end">Size</th>
|
||||
<th class="text-center">DL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
accordion.appendChild(item);
|
||||
} catch (e) {}
|
||||
|
||||
if (accordion.childElementCount > 0) {
|
||||
section.appendChild(accordion);
|
||||
container.appendChild(section);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
171
pages/src/assets/ts/renderers/overview.ts
Normal file
171
pages/src/assets/ts/renderers/overview.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
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<string>();
|
||||
for (const m of mirrorFileDb) {
|
||||
try {
|
||||
const u = new URL(m.orig);
|
||||
u.search = '';
|
||||
mirrorOrigSet.add(u.toString());
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const countedUrls = new Set<string>();
|
||||
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';
|
||||
sectionIn.innerHTML = `
|
||||
<h3 class="card-title">Latest Game Packages</h3>
|
||||
<p class="text-center lh-1">
|
||||
<span class="fw-bold fs-1">${await (
|
||||
async () => {
|
||||
const url = `${BASE_URL}/akEndfield/launcher/game/6/all.json`;
|
||||
const dat = await fetchJson<StoredData<any>[]>(url);
|
||||
return dat.at(-1)?.rsp.version;
|
||||
}
|
||||
)()}</span><br />
|
||||
Latest Version (Global)
|
||||
</p>
|
||||
<p class="text-center lh-1">
|
||||
<span class="fw-bold fs-1">${await (
|
||||
async () => {
|
||||
const url = `${BASE_URL}/akEndfield/launcher/game/1/all.json`;
|
||||
const dat = await fetchJson<StoredData<any>[]>(url);
|
||||
return dat.at(-1)?.rsp.version;
|
||||
}
|
||||
)()}</span><br />
|
||||
Latest Version (China)
|
||||
</p>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Region</th>
|
||||
<th>Channel</th>
|
||||
<th>Version</th>
|
||||
<th class="text-end">Packed</th>
|
||||
<th class="text-end">Unpacked</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
`;
|
||||
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<StoredData<any>[]>(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 = `
|
||||
<td>${target.region === 'cn' ? 'China' : 'Global'}</td>
|
||||
<td>${target.name}</td>
|
||||
<td>${version}</td>
|
||||
<td class="text-end">${math.formatFileSize(packedSize, FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-end">${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)}</td>
|
||||
`;
|
||||
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<StoredData<any>[]>(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<StoredData<any>[]>(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<StoredData<any>[]>(urlExe);
|
||||
for (const e of dataExe) {
|
||||
checkAndAddSize(e.rsp.exe_url, parseInt(e.rsp.exe_size));
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
const mirrorSection = document.createElement('div');
|
||||
mirrorSection.className = 'card';
|
||||
mirrorSection.innerHTML = `
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Mirror Statistics</h3>
|
||||
<p class="card-text text-center lh-1">
|
||||
<span class="fw-bold fs-1">${math.formatFileSize(totalMirrorSize, { ...FILE_SIZE_OPTS, unit: 'G' })}</span><br />
|
||||
uploaded to mirror
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(mirrorSection);
|
||||
}
|
||||
89
pages/src/assets/ts/renderers/patches.ts
Normal file
89
pages/src/assets/ts/renderers/patches.ts
Normal file
@@ -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<StoredData<any>[]>(url);
|
||||
if (data.length === 0) continue;
|
||||
|
||||
const section = document.createElement('div');
|
||||
section.className = 'mb-5';
|
||||
section.innerHTML = `<h3 class="mb-3">${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}</h3>`;
|
||||
|
||||
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 += `<tr>
|
||||
<td>${fileName(e.rsp.patch.url)}</td>
|
||||
<td><code>${e.rsp.patch.md5}</code></td>
|
||||
<td class="text-end">${math.formatFileSize(parseInt(e.rsp.patch.package_size), FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-center">${generateDownloadLinks(e.rsp.patch.url, mirrorFileDb)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
for (const f of e.rsp.patch.patches) {
|
||||
rows += `<tr>
|
||||
<td>${fileName(f.url)}</td>
|
||||
<td><code>${f.md5}</code></td>
|
||||
<td class="text-end">${math.formatFileSize(parseInt(f.package_size), FILE_SIZE_OPTS)}</td>
|
||||
<td class="text-center">${generateDownloadLinks(f.url, mirrorFileDb)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const itemId = `patch-${target.dirName}-${itemIndex}`;
|
||||
const isExpanded = false;
|
||||
itemIndex++;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'accordion-item';
|
||||
item.innerHTML = `
|
||||
<h2 class="accordion-header" id="heading-${itemId}">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${itemId}" aria-expanded="${isExpanded}" aria-controls="collapse-${itemId}">
|
||||
<div class="d-flex w-100 justify-content-between me-3">
|
||||
<span class="fw-bold">${reqVersion} → ${version}</span>
|
||||
<span class="text-muted small">${dateStr}</span>
|
||||
</div>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-${itemId}" class="accordion-collapse collapse ${isExpanded ? 'show' : ''}" aria-labelledby="heading-${itemId}" data-bs-parent="#${accordion.id}">
|
||||
<div class="accordion-body">
|
||||
<table class="table table-sm table-borderless w-auto mb-2">
|
||||
<tr><td>Unpacked Size</td><td class="text-end fw-bold">${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)}</td></tr>
|
||||
<tr><td>Packed Size</td><td class="text-end fw-bold">${math.formatFileSize(packedSize, FILE_SIZE_OPTS)}</td></tr>
|
||||
</table>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-sm align-middle text-nowrap">
|
||||
<thead><tr><th>File</th><th>MD5 Checksum</th><th class="text-end">Size</th><th class="text-center">DL</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
accordion.appendChild(item);
|
||||
}
|
||||
section.appendChild(accordion);
|
||||
container.appendChild(section);
|
||||
} catch (err) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
103
pages/src/assets/ts/renderers/resources.ts
Normal file
103
pages/src/assets/ts/renderers/resources.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
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 = `<h3 class="mb-3">${target.region === 'cn' ? 'China' : 'Global'}</h3>`;
|
||||
|
||||
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<StoredData<any>[]>(url);
|
||||
|
||||
// Group by res_version
|
||||
const resVersionMap = new Map<string, { rsp: StoredData<any>; versions: Set<string> }>();
|
||||
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),
|
||||
}));
|
||||
|
||||
let rows = '';
|
||||
for (const item of resVersionSet.reverse()) {
|
||||
// Newest first
|
||||
const dateStr = DateTime.fromISO(item.rsp.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss');
|
||||
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 += `<tr>
|
||||
<td style="font-feature-settings: 'tnum'">${dateStr}</td>
|
||||
<td><a href="${initialRes.path}" target="_blank">${initialRes.version}</a></td>
|
||||
<td><a href="${mainRes.path}" target="_blank">${mainRes.version}</a></td>
|
||||
<td class="text-center">${isKick ? '✅' : ''}</td>
|
||||
<td>${item.versions.join(', ')}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const itemId = `res-${target.region}-${target.channel}-${platform}`;
|
||||
const isExpanded = false;
|
||||
itemIndex++;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'accordion-item';
|
||||
item.innerHTML = `
|
||||
<h2 class="accordion-header" id="heading-${itemId}">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${itemId}" aria-expanded="${isExpanded}" aria-controls="collapse-${itemId}">
|
||||
${platform}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-${itemId}" class="accordion-collapse collapse ${isExpanded ? 'show' : ''}" aria-labelledby="heading-${itemId}" data-bs-parent="#${accordion.id}">
|
||||
<div class="accordion-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-sm align-middle text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Initial</th>
|
||||
<th>Main</th>
|
||||
<th>Kick</th>
|
||||
<th>Game version</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
accordion.appendChild(item);
|
||||
} catch (err) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
if (accordion.childElementCount > 0) {
|
||||
section.appendChild(accordion);
|
||||
container.appendChild(section);
|
||||
}
|
||||
}
|
||||
}
|
||||
157
pages/src/assets/ts/renderers/web.ts
Normal file
157
pages/src/assets/ts/renderers/web.ts
Normal file
@@ -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 = `<h3 class="mb-3">${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}</h3>`;
|
||||
|
||||
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 = '<span class="input-group-text">Language</span>';
|
||||
|
||||
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 = '<div class="text-muted p-2">Loading...</div>';
|
||||
|
||||
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<StoredData<any>[]>(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<typeof r> => r !== null);
|
||||
|
||||
if (validResults.length === 0) {
|
||||
accordion.innerHTML = '<div class="text-muted p-2">No data found.</div>';
|
||||
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 = `
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${itemId}" aria-expanded="false" aria-controls="collapse-${itemId}">
|
||||
${apiType}
|
||||
</button>
|
||||
`;
|
||||
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 = `<span class="input-group-text">History</span>`;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
pages/src/assets/ts/renderers/webPretty.ts
Normal file
14
pages/src/assets/ts/renderers/webPretty.ts
Normal file
@@ -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);
|
||||
}
|
||||
179
pages/src/assets/ts/renderers/webPretty/announcement.ts
Normal file
179
pages/src/assets/ts/renderers/webPretty/announcement.ts
Normal file
@@ -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 = '<h3 class="h4 mb-0">Announcement</h3><i class="bi bi-chevron-down"></i>';
|
||||
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 = '<label class="form-label fw-bold">Target</label>';
|
||||
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 = '<label class="form-label fw-bold">Language</label>';
|
||||
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 = '<div class="text-muted p-2">No language selected.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
contentDiv.innerHTML = '<div class="text-muted p-2">Loading announcements...</div>';
|
||||
|
||||
const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/announcement/${lang}/all.json`;
|
||||
try {
|
||||
const data = await fetchJson<StoredData<LauncherWebAnnouncement>[]>(url);
|
||||
if (!data || data.length === 0) {
|
||||
contentDiv.innerHTML = '<div class="text-muted p-2">No data found.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const tabsMap = new Map<
|
||||
string,
|
||||
{ tabName: string; announcements: Map<string, LauncherWebAnnouncement['tabs'][0]['announcements'][0]> }
|
||||
>();
|
||||
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 = '<div class="text-muted p-2">No announcements found.</div>';
|
||||
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 = `
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<span class="text-muted small" style="min-width: 120px;">${date}</span>
|
||||
<span class="flex-grow-1 fw-bold">${ann.content}</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
${ann.need_token ? '<span class="badge bg-warning text-dark px-1 py-0" style="font-size: 0.7rem;">Auth</span>' : ''}
|
||||
${ann.jump_url ? `<a href="${ann.jump_url}" target="_blank" class="btn btn-sm btn-outline-secondary py-0 px-2" style="font-size: 0.75rem;">Link</a>` : ''}
|
||||
<span class="text-muted border-start ps-2" style="font-size: 0.7rem;">ID:${ann.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
listGroup.appendChild(item);
|
||||
}
|
||||
card.appendChild(listGroup);
|
||||
contentDiv.appendChild(card);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to load ${url}`, e);
|
||||
contentDiv.innerHTML = '<div class="text-danger p-2">Failed to load data.</div>';
|
||||
}
|
||||
};
|
||||
|
||||
targetSelect.addEventListener('change', () => {
|
||||
updateLanguages();
|
||||
renderContent();
|
||||
});
|
||||
langSelect.addEventListener('change', renderContent);
|
||||
|
||||
updateLanguages();
|
||||
renderContent();
|
||||
container.appendChild(outerCard);
|
||||
}
|
||||
175
pages/src/assets/ts/renderers/webPretty/banner.ts
Normal file
175
pages/src/assets/ts/renderers/webPretty/banner.ts
Normal file
@@ -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 = '<h3 class="h4 mb-0">Banner</h3><i class="bi bi-chevron-down"></i>';
|
||||
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 = '<label class="form-label fw-bold">Target</label>';
|
||||
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 = '<label class="form-label fw-bold">Language</label>';
|
||||
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 = '<div class="text-muted p-2">No language selected.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
contentDiv.innerHTML = '<div class="text-muted p-2">Loading banners...</div>';
|
||||
|
||||
const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/banner/${lang}/all.json`;
|
||||
try {
|
||||
const data = await fetchJson<StoredData<LauncherWebBanner>[]>(url);
|
||||
if (!data || data.length === 0) {
|
||||
contentDiv.innerHTML = '<div class="text-muted p-2">No data found.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect unique banners by ID from the entire history
|
||||
const bannerMap = new Map<string, { banner: LauncherWebBanner['banners'][0]; firstSeen: string }>();
|
||||
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 = '<div class="text-muted p-2">No banners found.</div>';
|
||||
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 = `
|
||||
<a href="${linkUrl}" target="_blank" class="text-decoration-none text-reset">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="position-relative">
|
||||
<img src="${mirrorUrl}" class="card-img-top rounded" alt="Banner Image" style="object-fit: cover; aspect-ratio: 16 / 9;">
|
||||
<div class="position-absolute top-0 end-0 p-1">
|
||||
${banner.need_token ? '<span class="badge bg-warning text-dark" style="font-size: 0.6rem;">Auth</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body py-1 px-1">
|
||||
<div class="d-flex justify-content-between align-items-center" style="font-size: 0.7rem;">
|
||||
<span class="text-muted text-truncate me-1">ID: ${id}</span>
|
||||
<span class="text-muted flex-shrink-0">${dateStr}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
row.appendChild(col);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to load ${url}`, e);
|
||||
contentDiv.innerHTML = '<div class="text-danger p-2">Failed to load data.</div>';
|
||||
}
|
||||
};
|
||||
|
||||
targetSelect.addEventListener('change', () => {
|
||||
updateLanguages();
|
||||
renderContent();
|
||||
});
|
||||
langSelect.addEventListener('change', renderContent);
|
||||
|
||||
updateLanguages();
|
||||
renderContent();
|
||||
container.appendChild(outerCard);
|
||||
}
|
||||
174
pages/src/assets/ts/renderers/webPretty/mainBgImage.ts
Normal file
174
pages/src/assets/ts/renderers/webPretty/mainBgImage.ts
Normal file
@@ -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 = '<h3 class="h4 mb-0">Main Background Image</h3><i class="bi bi-chevron-down"></i>';
|
||||
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 = '<label class="form-label fw-bold">Target</label>';
|
||||
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 = '<label class="form-label fw-bold">Language</label>';
|
||||
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 = '<div class="text-muted p-2">No language selected.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
contentDiv.innerHTML = '<div class="text-muted p-2">Loading background images...</div>';
|
||||
|
||||
const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/main_bg_image/${lang}/all.json`;
|
||||
try {
|
||||
const data = await fetchJson<StoredData<LauncherWebMainBgImage>[]>(url);
|
||||
if (!data || data.length === 0) {
|
||||
contentDiv.innerHTML = '<div class="text-muted p-2">No data found.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect unique images by MD5 from the entire history
|
||||
const imageMap = new Map<string, { image: LauncherWebMainBgImage['main_bg_image']; firstSeen: string }>();
|
||||
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 = '<div class="text-muted p-2">No images found.</div>';
|
||||
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 = `
|
||||
<a href="${linkUrl}" target="_blank" class="text-decoration-none text-reset">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="position-relative">
|
||||
<img src="${mirrorUrl}" class="card-img-top rounded" alt="Background Image" style="object-fit: cover; aspect-ratio: 16 / 9;">
|
||||
<div class="position-absolute top-0 end-0 p-1">
|
||||
${image.video_url ? '<span class="badge bg-primary" style="font-size: 0.6rem;">Video</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body py-1 px-1">
|
||||
<div class="d-flex justify-content-between align-items-center" style="font-size: 0.7rem;">
|
||||
<span class="text-muted text-truncate font-monospace me-1">${md5}</span>
|
||||
<span class="text-muted flex-shrink-0">${dateStr}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
row.appendChild(col);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to load ${url}`, e);
|
||||
contentDiv.innerHTML = '<div class="text-danger p-2">Failed to load data.</div>';
|
||||
}
|
||||
};
|
||||
|
||||
targetSelect.addEventListener('change', () => {
|
||||
updateLanguages();
|
||||
renderContent();
|
||||
});
|
||||
langSelect.addEventListener('change', renderContent);
|
||||
|
||||
updateLanguages();
|
||||
renderContent();
|
||||
container.appendChild(outerCard);
|
||||
}
|
||||
195
pages/src/assets/ts/renderers/webPretty/sidebar.ts
Normal file
195
pages/src/assets/ts/renderers/webPretty/sidebar.ts
Normal file
@@ -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 = '<h3 class="h4 mb-0">Sidebar</h3><i class="bi bi-chevron-down"></i>';
|
||||
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 = '<label class="form-label fw-bold">Target</label>';
|
||||
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 = '<label class="form-label fw-bold">Language</label>';
|
||||
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 = '<div class="text-muted p-2">No language selected.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
contentDiv.innerHTML = '<div class="text-muted p-2">Loading sidebar data...</div>';
|
||||
|
||||
const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/sidebar/${lang}/all.json`;
|
||||
try {
|
||||
const data = await fetchJson<StoredData<LauncherWebSidebar>[]>(url);
|
||||
if (!data || data.length === 0) {
|
||||
contentDiv.innerHTML = '<div class="text-muted p-2">No data found.</div>';
|
||||
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 = '<div class="text-muted p-2">No active sidebars.</div>';
|
||||
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 = `
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="card-title mb-0">${item.media}</h5>
|
||||
${item.need_token ? '<span class="badge bg-warning text-dark lh-1 py-1">Auth</span>' : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (item.pic) {
|
||||
innerHtml += `
|
||||
<div class="mb-3">
|
||||
<img src="${getMirrorUrl(item.pic.url)}" class="img-fluid rounded" alt="${item.pic.description}">
|
||||
<p class="text-muted small mt-1 mb-0">${item.pic.description}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (item.jump_url) {
|
||||
innerHtml += `
|
||||
<a href="${item.jump_url}" target="_blank" class="btn btn-sm btn-outline-primary mb-2 w-100">Open Link</a>
|
||||
`;
|
||||
}
|
||||
|
||||
if (item.sidebar_labels && item.sidebar_labels.length > 0) {
|
||||
innerHtml += '<div class="list-group list-group-flush border-top mt-2">';
|
||||
for (const label of item.sidebar_labels) {
|
||||
innerHtml += `
|
||||
<a href="${label.jump_url}" target="_blank" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center py-2">
|
||||
<span style="font-size: 0.9rem;">${label.content}</span>
|
||||
<div class="d-flex gap-1">
|
||||
${label.need_token ? '<span class="badge bg-warning text-dark lh-1 py-1">Auth</span>' : ''}
|
||||
<i class="bi bi-box-arrow-up-right small text-muted"></i>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
innerHtml += '</div>';
|
||||
}
|
||||
|
||||
innerHtml += '</div>';
|
||||
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 = '<div class="text-danger p-2">Failed to load data.</div>';
|
||||
}
|
||||
};
|
||||
|
||||
targetSelect.addEventListener('change', () => {
|
||||
updateLanguages();
|
||||
renderContent();
|
||||
});
|
||||
langSelect.addEventListener('change', renderContent);
|
||||
|
||||
updateLanguages();
|
||||
renderContent();
|
||||
container.appendChild(outerCard);
|
||||
}
|
||||
205
pages/src/assets/ts/renderers/webPretty/singleEnt.ts
Normal file
205
pages/src/assets/ts/renderers/webPretty/singleEnt.ts
Normal file
@@ -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 = '<h3 class="h4 mb-0">Single Ent.</h3><i class="bi bi-chevron-down"></i>';
|
||||
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 = '<label class="form-label fw-bold">Target</label>';
|
||||
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 = '<label class="form-label fw-bold">Language</label>';
|
||||
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 = '<div class="text-muted p-2">No language selected.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
contentDiv.innerHTML = '<div class="text-muted p-2">Loading single entry data...</div>';
|
||||
|
||||
const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/single_ent/${lang}/all.json`;
|
||||
try {
|
||||
const data = await fetchJson<StoredData<LauncherWebSingleEnt>[]>(url);
|
||||
if (!data || data.length === 0) {
|
||||
contentDiv.innerHTML = '<div class="text-muted p-2">No data found.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect unique visuals by MD5 from the entire history
|
||||
const entMap = new Map<string, { ent: LauncherWebSingleEnt['single_ent']; firstSeen: string }>();
|
||||
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 = '<div class="text-muted p-2">No data found.</div>';
|
||||
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 = `
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="text-muted small">First seen: ${DateTime.fromISO(firstSeen).toFormat('yyyy/MM/dd HH:mm')}</span>
|
||||
${ent.need_token ? '<span class="badge bg-warning text-dark">Auth</span>' : ''}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold">Version Image</label>
|
||||
<a href="${getMirrorUrl(ent.version_url)}" target="_blank">
|
||||
<img src="${getMirrorUrl(ent.version_url)}" class="img-fluid rounded border" alt="Version Image">
|
||||
</a>
|
||||
<p class="text-muted font-monospace mt-1" style="font-size: 0.7rem; word-break: break-all;">MD5: ${ent.version_md5}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (ent.button_url) {
|
||||
innerHtml += `
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold">Action Button</label>
|
||||
<div class="d-flex gap-2">
|
||||
<div class="flex-grow-1">
|
||||
<img src="${getMirrorUrl(ent.button_url)}" class="img-fluid rounded border bg-light" alt="Button" style="max-height: 60px;">
|
||||
<p class="text-muted small mt-1 mb-0">Normal</p>
|
||||
</div>
|
||||
${
|
||||
ent.button_hover_url
|
||||
? `
|
||||
<div class="flex-grow-1">
|
||||
<img src="${getMirrorUrl(ent.button_hover_url)}" class="img-fluid rounded border bg-light" alt="Button Hover" style="max-height: 60px;">
|
||||
<p class="text-muted small mt-1 mb-0">Hover</p>
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (ent.jump_url) {
|
||||
innerHtml += `
|
||||
<a href="${ent.jump_url}" target="_blank" class="btn btn-sm btn-outline-primary w-100">Jump URL</a>
|
||||
`;
|
||||
}
|
||||
|
||||
innerHtml += '</div>';
|
||||
card.innerHTML = innerHtml;
|
||||
col.appendChild(card);
|
||||
row.appendChild(col);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to load ${url}`, e);
|
||||
contentDiv.innerHTML = '<div class="text-danger p-2">Failed to load data.</div>';
|
||||
}
|
||||
};
|
||||
|
||||
targetSelect.addEventListener('change', () => {
|
||||
updateLanguages();
|
||||
renderContent();
|
||||
});
|
||||
langSelect.addEventListener('change', renderContent);
|
||||
|
||||
updateLanguages();
|
||||
renderContent();
|
||||
container.appendChild(outerCard);
|
||||
}
|
||||
72
pages/src/assets/ts/types.ts
Normal file
72
pages/src/assets/ts/types.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
export interface MirrorFileEntry {
|
||||
orig: string;
|
||||
mirror: string;
|
||||
origStatus: boolean;
|
||||
}
|
||||
|
||||
export interface StoredData<T> {
|
||||
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;
|
||||
}[];
|
||||
}
|
||||
@@ -1 +1,44 @@
|
||||
export default {};
|
||||
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,
|
||||
};
|
||||
|
||||
16
pages/src/assets/ts/utils/ui.ts
Normal file
16
pages/src/assets/ts/utils/ui.ts
Normal file
@@ -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(`<a href="${url}" target="_blank">Orig</a>`);
|
||||
}
|
||||
if (mirrorEntry) {
|
||||
links.push(`<a href="${mirrorEntry.mirror}" target="_blank">Mirror</a>`);
|
||||
}
|
||||
return links.join(' / ');
|
||||
}
|
||||
@@ -300,7 +300,6 @@ async function fetchAndSaveLatestGamePatches(gameTargets: GameTarget[]) {
|
||||
async function fetchAndSaveLatestGameResources(gameTargets: GameTarget[]) {
|
||||
logger.debug('Fetching latestGameRes ...');
|
||||
const platforms = ['Windows', 'Android', 'iOS', 'PlayStation'] as const;
|
||||
const subChns = appConfig.network.api.akEndfield.subChannel;
|
||||
|
||||
const filteredTargets = gameTargets.filter(
|
||||
(t) => t.channel !== appConfig.network.api.akEndfield.channel.cnWinRelBilibili,
|
||||
@@ -698,9 +697,9 @@ async function mainCmdHandler() {
|
||||
await fetchAndSaveLatestGames(gameTargets);
|
||||
await fetchAndSaveLatestGamePatches(gameTargets);
|
||||
await fetchAndSaveLatestGameResources(gameTargets);
|
||||
await fetchAndSaveAllGameResRawData(gameTargets);
|
||||
await fetchAndSaveLatestLauncher(launcherTargets);
|
||||
await fetchAndSaveLatestWebApis(gameTargets);
|
||||
await fetchAndSaveLatestLauncher(launcherTargets);
|
||||
await fetchAndSaveAllGameResRawData(gameTargets);
|
||||
|
||||
await checkMirrorFileDbStatus();
|
||||
await processMirrorQueue();
|
||||
|
||||
Reference in New Issue
Block a user