From 5ad054537f02f2531ab00002559a448d5acedd24 Mon Sep 17 00:00:00 2001 From: daydreamer-json Date: Sun, 8 Mar 2026 07:39:12 +0900 Subject: [PATCH] refactor(pages): enhance web rendering components and type safety --- pages/src/assets/ts/index.ts | 8 +- pages/src/assets/ts/renderers/webPretty.ts | 14 ++ .../ts/renderers/webPretty/announcement.ts | 179 +++++++++++++++ .../assets/ts/renderers/webPretty/banner.ts | 175 +++++++++++++++ .../ts/renderers/webPretty/mainBgImage.ts | 174 +++++++++++++++ .../assets/ts/renderers/webPretty/sidebar.ts | 195 +++++++++++++++++ .../ts/renderers/webPretty/singleEnt.ts | 205 ++++++++++++++++++ pages/src/assets/ts/types.ts | 61 ++++++ src/cmds/archive.ts | 5 +- 9 files changed, 1009 insertions(+), 7 deletions(-) create mode 100644 pages/src/assets/ts/renderers/webPretty.ts create mode 100644 pages/src/assets/ts/renderers/webPretty/announcement.ts create mode 100644 pages/src/assets/ts/renderers/webPretty/banner.ts create mode 100644 pages/src/assets/ts/renderers/webPretty/mainBgImage.ts create mode 100644 pages/src/assets/ts/renderers/webPretty/sidebar.ts create mode 100644 pages/src/assets/ts/renderers/webPretty/singleEnt.ts diff --git a/pages/src/assets/ts/index.ts b/pages/src/assets/ts/index.ts index d251d22..d37f100 100644 --- a/pages/src/assets/ts/index.ts +++ b/pages/src/assets/ts/index.ts @@ -4,7 +4,7 @@ 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 { renderWeb } from './renderers/web.js'; +import { renderWebPretty } from './renderers/webPretty.js'; import type { MirrorFileEntry } from './types.js'; import { BASE_URL } from './utils/constants.js'; @@ -33,7 +33,7 @@ async function main() { - +
@@ -41,7 +41,7 @@ async function main() {
-
+
`; contentDiv.innerHTML = tabsHtml; @@ -52,6 +52,6 @@ async function main() { renderPatches(document.getElementById('tab-patch')!, mirrorFileDb), renderResources(document.getElementById('tab-resources')!), renderLaunchers(document.getElementById('tab-launcher')!, mirrorFileDb), - renderWeb(document.getElementById('tab-web')!), + renderWebPretty(document.getElementById('tab-web-pretty')!), ]); } diff --git a/pages/src/assets/ts/renderers/webPretty.ts b/pages/src/assets/ts/renderers/webPretty.ts new file mode 100644 index 0000000..b74c18b --- /dev/null +++ b/pages/src/assets/ts/renderers/webPretty.ts @@ -0,0 +1,14 @@ +import { renderAnnouncement } from './webPretty/announcement.js'; +import { renderBanner } from './webPretty/banner.js'; +import { renderMainBgImage } from './webPretty/mainBgImage.js'; +import { renderSidebar } from './webPretty/sidebar.js'; +import { renderSingleEnt } from './webPretty/singleEnt.js'; + +export async function renderWebPretty(container: HTMLElement) { + container.innerHTML = ''; + await renderAnnouncement(container); + await renderBanner(container); + await renderMainBgImage(container); + await renderSingleEnt(container); + await renderSidebar(container); +} diff --git a/pages/src/assets/ts/renderers/webPretty/announcement.ts b/pages/src/assets/ts/renderers/webPretty/announcement.ts new file mode 100644 index 0000000..9e1d3d1 --- /dev/null +++ b/pages/src/assets/ts/renderers/webPretty/announcement.ts @@ -0,0 +1,179 @@ +import { DateTime } from 'luxon'; +import { fetchJson } from '../../api.js'; +import type { LauncherWebAnnouncement, StoredData } from '../../types.js'; +import { BASE_URL, gameTargets, launcherWebApiLang } from '../../utils/constants.js'; + +export async function renderAnnouncement(container: HTMLElement) { + const outerCard = document.createElement('div'); + outerCard.className = 'card mb-3'; + + const header = document.createElement('div'); + header.className = 'card-header d-flex justify-content-between align-items-center'; + header.style.cursor = 'pointer'; + header.setAttribute('data-bs-toggle', 'collapse'); + header.setAttribute('data-bs-target', '#collapseAnnouncement'); + header.setAttribute('role', 'button'); + header.innerHTML = '

Announcement

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

Banner

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

Main Background Image

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

Sidebar

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

${item.pic.description}

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

Single Ent.

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

MD5: ${ent.version_md5}

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

Normal

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

Hover

+
+ ` + : '' + } +
+
+ `; + } + + if (ent.jump_url) { + innerHtml += ` + Jump URL + `; + } + + innerHtml += '
'; + card.innerHTML = innerHtml; + col.appendChild(card); + row.appendChild(col); + } + } catch (e) { + console.warn(`Failed to load ${url}`, e); + contentDiv.innerHTML = '
Failed to load data.
'; + } + }; + + targetSelect.addEventListener('change', () => { + updateLanguages(); + renderContent(); + }); + langSelect.addEventListener('change', renderContent); + + updateLanguages(); + renderContent(); + container.appendChild(outerCard); +} diff --git a/pages/src/assets/ts/types.ts b/pages/src/assets/ts/types.ts index 6647c5d..eafb940 100644 --- a/pages/src/assets/ts/types.ts +++ b/pages/src/assets/ts/types.ts @@ -9,3 +9,64 @@ export interface StoredData { rsp: T; updatedAt: string; } + +export interface LauncherWebAnnouncement { + data_version: string; + tabs: { + tabName: string; + announcements: { + content: string; + jump_url: string; + start_ts: string; + id: string; + need_token: boolean; + }[]; + tab_id: string; + }[]; +} + +export interface LauncherWebBanner { + data_version: string; + banners: { + url: string; + md5: string; + jump_url: string; + id: string; + need_token: boolean; + }[]; +} + +export interface LauncherWebMainBgImage { + data_version: string; + main_bg_image: { + url: string; + md5: string; + video_url: string; + }; +} + +export interface LauncherWebSingleEnt { + single_ent: { + version_url: string; + version_md5: string; + jump_url: string; + button_url: string; + button_md5: string; + button_hover_url: string; + button_hover_md5: string; + need_token: boolean; + }; +} + +export interface LauncherWebSidebar { + data_version: string; + sidebars: { + display_type: 'DisplayType_RESERVE'; + media: string; + pic: { url: string; md5: string; description: string } | null; + sidebar_labels: { content: string; jump_url: string; need_token: boolean }[]; + grid_info: null; + jump_url: string; + need_token: boolean; + }[]; +} diff --git a/src/cmds/archive.ts b/src/cmds/archive.ts index 7d99dfa..5af9e91 100644 --- a/src/cmds/archive.ts +++ b/src/cmds/archive.ts @@ -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();