mirror of
https://github.com/daydreamer-json/ak-endfield-api-archive.git
synced 2026-04-05 23:22:20 +02:00
feat(pages): add pages-v2 react variant
This commit is contained in:
51
pages-v2/src/utils/api.ts
Normal file
51
pages-v2/src/utils/api.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import ky from 'ky';
|
||||
import { BASE_URL, gameTargets, launcherTargets, launcherWebApiLang } from './constants';
|
||||
|
||||
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);
|
||||
}
|
||||
44
pages-v2/src/utils/constants.ts
Normal file
44
pages-v2/src/utils/constants.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export const BASE_URL =
|
||||
'https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output';
|
||||
|
||||
export const FILE_SIZE_OPTS = {
|
||||
decimals: 2,
|
||||
decimalPadding: true,
|
||||
useBinaryUnit: true,
|
||||
useBitUnit: false,
|
||||
unitVisible: true,
|
||||
unit: null,
|
||||
};
|
||||
|
||||
export const gameTargets = [
|
||||
{ name: 'Official', region: 'os' as const, dirName: '6', channel: 6 },
|
||||
{ name: 'Epic', region: 'os' as const, dirName: '801', channel: 6 },
|
||||
{ name: 'Google Play', region: 'os' as const, dirName: '802', channel: 6 },
|
||||
{ name: 'Official', region: 'cn' as const, dirName: '1', channel: 1 },
|
||||
{ name: 'Bilibili', region: 'cn' as const, dirName: '2', channel: 2 },
|
||||
];
|
||||
|
||||
export const launcherTargets = [
|
||||
{ id: 'os', apps: ['EndField', 'Official'], channel: 6 },
|
||||
{ id: 'cn', apps: ['EndField', 'Arknights', 'Official'], channel: 1 },
|
||||
];
|
||||
|
||||
export const launcherWebApiLang = {
|
||||
os: [
|
||||
'de-de',
|
||||
'en-us',
|
||||
'es-mx',
|
||||
'fr-fr',
|
||||
'id-id',
|
||||
'it-it',
|
||||
'ja-jp',
|
||||
'ko-kr',
|
||||
'pt-br',
|
||||
'ru-ru',
|
||||
'th-th',
|
||||
'vi-vn',
|
||||
'zh-cn',
|
||||
'zh-tw',
|
||||
] as const,
|
||||
cn: ['zh-cn'] as const,
|
||||
};
|
||||
12
pages-v2/src/utils/logger.ts
Normal file
12
pages-v2/src/utils/logger.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
export default {
|
||||
write(message: string) {
|
||||
const debugLogElement = document.querySelector('#debug-log code');
|
||||
if (!debugLogElement) return;
|
||||
const prettyMessage = `${DateTime.now().toFormat('HH:mm:ss.SSS')} > ${message}`;
|
||||
const divEl = document.createElement('div');
|
||||
divEl.textContent = prettyMessage;
|
||||
debugLogElement.appendChild(divEl);
|
||||
},
|
||||
};
|
||||
162
pages-v2/src/utils/math.ts
Normal file
162
pages-v2/src/utils/math.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import logger from './logger';
|
||||
|
||||
export default {
|
||||
arrayMax(array: Array<number>) {
|
||||
return array.reduce((a, b) => Math.max(a, b));
|
||||
},
|
||||
|
||||
arrayMin(array: Array<number>) {
|
||||
return array.reduce((a, b) => Math.min(a, b));
|
||||
},
|
||||
|
||||
arrayTotal(array: Array<number>) {
|
||||
return array.reduce((acc, f) => acc + f, 0);
|
||||
},
|
||||
|
||||
arrayAvg(array: Array<number>) {
|
||||
return this.arrayTotal(array) / array.length;
|
||||
},
|
||||
|
||||
rounder(method: 'floor' | 'ceil' | 'round', num: number, n: number) {
|
||||
const pow = Math.pow(10, n);
|
||||
let result: number;
|
||||
switch (method) {
|
||||
case 'floor':
|
||||
result = Math.floor(num * pow) / pow;
|
||||
break;
|
||||
case 'ceil':
|
||||
result = Math.ceil(num * pow) / pow;
|
||||
break;
|
||||
case 'round':
|
||||
result = Math.round(num * pow) / pow;
|
||||
break;
|
||||
}
|
||||
return {
|
||||
orig: result,
|
||||
padded: result.toFixed(n),
|
||||
};
|
||||
},
|
||||
|
||||
formatFileSize(
|
||||
bytes: number,
|
||||
options: {
|
||||
decimals: number;
|
||||
decimalPadding: boolean;
|
||||
useBinaryUnit: boolean;
|
||||
useBitUnit: boolean;
|
||||
unitVisible: boolean;
|
||||
unit: 'B' | 'K' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y' | null;
|
||||
},
|
||||
) {
|
||||
const k = options.useBinaryUnit ? 1024 : 1000;
|
||||
const dm = options.decimals < 0 ? 0 : options.decimals;
|
||||
|
||||
const baseUnits = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
|
||||
const binaryUnitSuffix = options.useBitUnit ? 'ib' : 'iB';
|
||||
const siUnitSuffix = options.useBitUnit ? 'b' : 'B';
|
||||
|
||||
const getUnitString = (i: number) => {
|
||||
if (i === 0) return options.useBitUnit ? 'b' : 'B';
|
||||
return baseUnits[i] + (options.useBinaryUnit ? binaryUnitSuffix : siUnitSuffix);
|
||||
};
|
||||
|
||||
let value = bytes < 0 ? 0 : Math.floor(bytes);
|
||||
if (options.useBitUnit) {
|
||||
value *= 8;
|
||||
}
|
||||
|
||||
let i: number;
|
||||
if (options.unit !== null) {
|
||||
i = baseUnits.indexOf(options.unit);
|
||||
if (i === -1) throw new Error(`Invalid unit: ${options.unit}`);
|
||||
} else {
|
||||
if (value === 0) {
|
||||
i = 0;
|
||||
} else {
|
||||
i = Math.floor(Math.log(value) / Math.log(k));
|
||||
i = Math.max(0, Math.min(baseUnits.length - 1, i)); // clamp
|
||||
}
|
||||
}
|
||||
|
||||
const resultValue = value / Math.pow(k, i);
|
||||
|
||||
let formattedValue: string;
|
||||
if (options.decimalPadding) {
|
||||
formattedValue = resultValue.toFixed(dm);
|
||||
} else {
|
||||
formattedValue = resultValue.toFixed(dm).replace(/\.?0+$/, '');
|
||||
}
|
||||
|
||||
return formattedValue + (options.unitVisible ? ' ' + getUnitString(i) : '');
|
||||
},
|
||||
|
||||
secureRandomFloatInRange(min: number, max: number): number {
|
||||
if (min > max) [min, max] = [max, min];
|
||||
const crypto = globalThis.crypto;
|
||||
if (!crypto) {
|
||||
throw new Error('Cryptographically secure random float number gen is not available');
|
||||
}
|
||||
const randomValues = new Uint32Array(2);
|
||||
crypto.getRandomValues(randomValues);
|
||||
const highBits = randomValues[1]! & 0x1fffff; // 0x1FFFFF = 2^21 - 1
|
||||
const lowBits = randomValues[0];
|
||||
const combined = highBits * 0x100000000 + lowBits!; // 0x100000000 = 2^32
|
||||
const randomFraction = combined / 0x20000000000000; // 0x20000000000000 = 2^53
|
||||
return randomFraction * (max - min) + min;
|
||||
},
|
||||
|
||||
secureRandomIntInRange(min: number, max: number, writeLog: boolean = false): number {
|
||||
if (min === max) {
|
||||
writeLog ? logger.write(`randomInt: Range=${min}-${max}, Output=${min}`) : undefined;
|
||||
return min;
|
||||
}
|
||||
if (min > max) [min, max] = [max, min];
|
||||
const crypto = globalThis.crypto;
|
||||
if (!crypto) {
|
||||
throw new Error('Cryptographically secure random int number gen is not available');
|
||||
}
|
||||
|
||||
// convert to integer anyway
|
||||
const minInt = Math.ceil(min);
|
||||
const maxInt = Math.floor(max);
|
||||
|
||||
// safe integer check
|
||||
if (!Number.isSafeInteger(minInt) || !Number.isSafeInteger(maxInt)) {
|
||||
throw new Error('Range boundaries must be within safe integer limits');
|
||||
}
|
||||
|
||||
// valid range check
|
||||
if (minInt > maxInt) {
|
||||
throw new Error('Invalid range after integer conversion: min > max');
|
||||
}
|
||||
|
||||
const range = maxInt - minInt + 1;
|
||||
|
||||
if (range <= 0 || range > Number.MAX_SAFE_INTEGER) {
|
||||
throw new Error(`Range size must be between 1 and ${Number.MAX_SAFE_INTEGER} inclusive`);
|
||||
}
|
||||
|
||||
// 53-bit random num gen
|
||||
const MAX_53 = BigInt(1) << BigInt(53); // 2^53
|
||||
const rangeBigInt = BigInt(range);
|
||||
const maxAcceptable = MAX_53 - (MAX_53 % rangeBigInt);
|
||||
|
||||
// generate
|
||||
const randomBuffer = new Uint32Array(2);
|
||||
while (true) {
|
||||
crypto.getRandomValues(randomBuffer);
|
||||
const highBits = randomBuffer[1]! & 0x1fffff; // use lower 21-bit only
|
||||
const lowBits = randomBuffer[0];
|
||||
const combined = BigInt(highBits) * BigInt(0x100000000) + BigInt(lowBits!); // 0x100000000 = 2^32
|
||||
// accept condition: combined < maxAcceptable
|
||||
if (combined < maxAcceptable) {
|
||||
const offset = Number(combined % rangeBigInt); // 0 to range-1
|
||||
const hex = Array.from(new Uint8Array(randomBuffer))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
writeLog ? logger.write(`randomInt: Range=${min}-${max}, Raw=0x${hex}, Output=${minInt + offset}`) : undefined;
|
||||
return minInt + offset;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
16
pages-v2/src/utils/ui.ts
Normal file
16
pages-v2/src/utils/ui.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { MirrorFileEntry } from '../types';
|
||||
|
||||
export function generateDownloadLinks(url: string, mirrorFileDb: MirrorFileEntry[]) {
|
||||
const cleanUrl = new URL(url);
|
||||
cleanUrl.search = '';
|
||||
const mirrorEntry = mirrorFileDb.find((g) => g.orig.includes(cleanUrl.toString()));
|
||||
|
||||
const links: string[] = [];
|
||||
if (!mirrorEntry || mirrorEntry.origStatus === true) {
|
||||
links.push(`<a href="${url}" target="_blank">Orig</a>`);
|
||||
}
|
||||
if (mirrorEntry) {
|
||||
links.push(`<a href="${mirrorEntry.mirror}" target="_blank">Mirror</a>`);
|
||||
}
|
||||
return links.join(' / ');
|
||||
}
|
||||
Reference in New Issue
Block a user