refactor(gachabase): remove gachabase sync system, UI buttons, and catalog fetching
This commit is contained in:
1 parent
8e57a297d0
commit
1c10928849
3 files changed
-496
No files matched your search
-29
@@ -47,10 +47,6 @@
|
|||||||
<button class="btn btn-secondary btn-sm" id="btn-quick-ping" title="รีเฟรชการเชื่อมต่อ" data-i18n="refresh">
|
<button class="btn btn-secondary btn-sm" id="btn-quick-ping" title="รีเฟรชการเชื่อมต่อ" data-i18n="refresh">
|
||||||
🔄 รีเฟรช
|
🔄 รีเฟรช
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-secondary btn-sm" id="btn-header-gachabase-sync" title="ซิงค์และอัปเดตข้อมูลเกม/รูปภาพจาก Gachabase เมื่อเกมอัปเดต" style="border-color: rgba(245, 158, 11, 0.4); color: #fbbf24;" data-i18n="btnGachabaseSync">
|
|
||||||
🌐 ซิงค์ Gachabase
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -308,7 +304,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
<button class="btn btn-secondary" id="btn-sync-gachabase-avatars" title="ซิงค์และอัปเดตข้อมูลตัวละครและรูปภาพจาก Gachabase เมื่อเกมมีแพตช์ใหม่" style="border-color: rgba(245, 158, 11, 0.4); color: #fbbf24;">🌐 ซิงค์ Gachabase</button>
|
|
||||||
<button class="btn btn-primary" id="btn-all-avatars-e0s1" data-i18n="btnAllE0S1" style="background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%); border-color: #38bdf8;">⚡ ปรับ E0S1 ทุกตัว</button>
|
<button class="btn btn-primary" id="btn-all-avatars-e0s1" data-i18n="btnAllE0S1" style="background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%); border-color: #38bdf8;">⚡ ปรับ E0S1 ทุกตัว</button>
|
||||||
<button class="btn btn-gold" id="btn-max-all-avatars" data-i18n="btnMaxAll">⭐ ปรับ Max E6 ทุกตัว</button>
|
<button class="btn btn-gold" id="btn-max-all-avatars" data-i18n="btnMaxAll">⭐ ปรับ Max E6 ทุกตัว</button>
|
||||||
<button class="btn btn-secondary" id="btn-reload-avatars" data-i18n="btnReload">🔄 รีโหลด</button>
|
<button class="btn btn-secondary" id="btn-reload-avatars" data-i18n="btnReload">🔄 รีโหลด</button>
|
||||||
@@ -361,7 +356,6 @@
|
|||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div style="display: flex; gap: 0.4rem;">
|
<div style="display: flex; gap: 0.4rem;">
|
||||||
<button type="button" class="btn btn-sm btn-secondary" id="btn-sync-gachabase-eq" title="ซิงค์และอัปเดตกรวยแสงและรูปภาพจาก Gachabase เมื่อเกมมีแพตช์ใหม่" style="border-color: rgba(245, 158, 11, 0.4); color: #fbbf24;">🌐 ซิงค์ Gachabase</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-success" id="btn-open-add-lc" data-i18n="btnAddLc">+ เพิ่ม LC</button>
|
<button type="button" class="btn btn-sm btn-success" id="btn-open-add-lc" data-i18n="btnAddLc">+ เพิ่ม LC</button>
|
||||||
<button type="button" class="btn btn-sm btn-success" id="btn-open-add-relic" data-i18n="btnAddRelic" style="display: none;">+ เพิ่มรีลิกส์</button>
|
<button type="button" class="btn btn-sm btn-success" id="btn-open-add-relic" data-i18n="btnAddRelic" style="display: none;">+ เพิ่มรีลิกส์</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -761,30 +755,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card 5: Gachabase Dynamic Game Data & Image Sync -->
|
|
||||||
<div class="card" style="margin-top: 0.5rem;">
|
|
||||||
<div class="card-title" style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<span>🌐 อัปเดตข้อมูลเกม & รูปภาพ (Gachabase Sync)</span>
|
|
||||||
<span class="btn-sm btn-secondary" id="badge-gachabase-status" style="font-size: 0.75rem; color: var(--accent-gold); border-color: rgba(245, 158, 11, 0.35);">พร้อมใช้งาน</span>
|
|
||||||
</div>
|
|
||||||
<p class="card-subtitle">
|
|
||||||
ดึงข้อมูลตัวละคร, กรวยแสง, และรูปภาพความละเอียดสูงจาก Gachabase อัตโนมัติเมื่อเกมมีแพตช์ใหม่หรือมีคอนเทนต์เบต้า
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.85rem;">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.25); padding: 0.85rem 1.1rem; border-radius: var(--radius-sm); border: 1px solid var(--border-color); flex-wrap: wrap; gap: 0.75rem;">
|
|
||||||
<div>
|
|
||||||
<div style="font-weight: 700; font-size: 0.95rem; color: #fff;">ฐานข้อมูลตัวละครและกรวยแสง</div>
|
|
||||||
<div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 0.15rem;" id="lbl-gachabase-info">
|
|
||||||
ตัวละครในระบบ: 124+ ตัว | กรวยแสง: 172+ ชิ้น (รองรับเนื้อหา 3.0+ และเบต้า)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-gold" id="btn-sync-gachabase-now" style="font-weight: 700; display: flex; align-items: center; gap: 0.5rem; padding: 0.65rem 1.25rem;">
|
|
||||||
🔄 ซิงค์ข้อมูล & รูปภาพจาก Gachabase
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { AVATAR_METADATA, getAvatarInfo } from './avatars-data.js';
|
|||||||
import { LIGHTCONE_METADATA, RELIC_SET_METADATA, getLightconeInfo, getRelicSetInfo } from './equipment-data.js';
|
import { LIGHTCONE_METADATA, RELIC_SET_METADATA, getLightconeInfo, getRelicSetInfo } from './equipment-data.js';
|
||||||
import { SUPPORTED_LANGUAGES, TRANSLATIONS, getCurrentLanguage, setLanguage, t, translateElement, translatePath } from './i18n.js';
|
import { SUPPORTED_LANGUAGES, TRANSLATIONS, getCurrentLanguage, setLanguage, t, translateElement, translatePath } from './i18n.js';
|
||||||
import {
|
import {
|
||||||
syncGachabaseDatabase,
|
|
||||||
loadCachedGachabaseData,
|
|
||||||
getAvatarIconUrl,
|
getAvatarIconUrl,
|
||||||
getLightconeArtUrl,
|
getLightconeArtUrl,
|
||||||
getLightconeThumbUrl,
|
getLightconeThumbUrl,
|
||||||
@@ -120,14 +118,6 @@ const el = {
|
|||||||
consoleCmdInput: document.getElementById('console-cmd-input'),
|
consoleCmdInput: document.getElementById('console-cmd-input'),
|
||||||
btnConsoleCmdSend: document.getElementById('btn-console-cmd-send'),
|
btnConsoleCmdSend: document.getElementById('btn-console-cmd-send'),
|
||||||
lblConsoleConnStatus: document.getElementById('lbl-console-conn-status'),
|
lblConsoleConnStatus: document.getElementById('lbl-console-conn-status'),
|
||||||
// Gachabase Sync
|
|
||||||
btnHeaderGachabaseSync: document.getElementById('btn-header-gachabase-sync'),
|
|
||||||
btnQuickGachabaseSync: document.getElementById('btn-quick-gachabase-sync'),
|
|
||||||
btnSyncGachabaseAvatars: document.getElementById('btn-sync-gachabase-avatars'),
|
|
||||||
btnSyncGachabaseEq: document.getElementById('btn-sync-gachabase-eq'),
|
|
||||||
btnSyncGachabaseNow: document.getElementById('btn-sync-gachabase-now'),
|
|
||||||
badgeGachabaseStatus: document.getElementById('badge-gachabase-status'),
|
|
||||||
lblGachabaseInfo: document.getElementById('lbl-gachabase-info'),
|
|
||||||
|
|
||||||
// Avatars
|
// Avatars
|
||||||
avatarsGrid: document.getElementById('avatars-grid'),
|
avatarsGrid: document.getElementById('avatars-grid'),
|
||||||
@@ -6766,144 +6756,12 @@ function setupTabs() {
|
|||||||
loadJsonData(state.currentJsonMode);
|
loadJsonData(state.currentJsonMode);
|
||||||
} else if (targetId === 'tab-settings') {
|
} else if (targetId === 'tab-settings') {
|
||||||
updateCacheUsageDisplay();
|
updateCacheUsageDisplay();
|
||||||
updateGachabaseSyncUI();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Gachabase Catalog Sync & Dynamic Image Engine
|
|
||||||
// ==========================================
|
|
||||||
let isGachabaseSyncing = false;
|
|
||||||
|
|
||||||
async function updateGachabaseSyncUI() {
|
|
||||||
if (!el.badgeGachabaseStatus && !el.lblGachabaseInfo) return;
|
|
||||||
try {
|
|
||||||
const lastSync = await idbGet('gac_gachabase_last_sync');
|
|
||||||
if (lastSync && lastSync.timestamp) {
|
|
||||||
const dateStr = new Date(lastSync.timestamp).toLocaleString();
|
|
||||||
if (el.badgeGachabaseStatus) {
|
|
||||||
el.badgeGachabaseStatus.textContent = 'ซิงค์แล้ว';
|
|
||||||
el.badgeGachabaseStatus.style.color = 'var(--accent-green)';
|
|
||||||
el.badgeGachabaseStatus.style.borderColor = 'rgba(52, 211, 153, 0.4)';
|
|
||||||
}
|
|
||||||
if (el.lblGachabaseInfo) {
|
|
||||||
el.lblGachabaseInfo.innerHTML = `ซิงค์ล่าสุดเมื่อ: <strong>${dateStr}</strong> (ตัวละครในระบบ: ${lastSync.totalAvatarsCount || Object.keys(AVATAR_METADATA).length}, กรวยแสง: ${lastSync.totalLcsCount || Object.keys(LIGHTCONE_METADATA).length})`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (el.badgeGachabaseStatus) {
|
|
||||||
el.badgeGachabaseStatus.textContent = 'ยังไม่เคยซิงค์';
|
|
||||||
el.badgeGachabaseStatus.style.color = 'var(--accent-gold)';
|
|
||||||
el.badgeGachabaseStatus.style.borderColor = 'rgba(245, 158, 11, 0.4)';
|
|
||||||
}
|
|
||||||
if (el.lblGachabaseInfo) {
|
|
||||||
el.lblGachabaseInfo.textContent = 'ยังไม่ได้ซิงค์แคตตาล็อกเกมจาก Gachabase กดปุ่มด้านล่างเพื่ออัปเดต';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Error reading gachabase sync status:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function triggerGachabaseSync(showToastOnDone = true) {
|
|
||||||
if (isGachabaseSyncing) {
|
|
||||||
showToast('กำลังซิงค์ข้อมูลจาก Gachabase อยู่แล้ว กรุณารอสักครู่...', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isGachabaseSyncing = true;
|
|
||||||
const syncButtons = [
|
|
||||||
el.btnHeaderGachabaseSync,
|
|
||||||
el.btnQuickGachabaseSync,
|
|
||||||
el.btnSyncGachabaseAvatars,
|
|
||||||
el.btnSyncGachabaseEq,
|
|
||||||
el.btnSyncGachabaseNow
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
syncButtons.forEach(btn => {
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.classList.add('loading');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (el.badgeGachabaseStatus) {
|
|
||||||
el.badgeGachabaseStatus.textContent = 'กำลังซิงค์...';
|
|
||||||
el.badgeGachabaseStatus.style.color = 'var(--accent-cyan)';
|
|
||||||
el.badgeGachabaseStatus.style.borderColor = 'rgba(56, 189, 248, 0.4)';
|
|
||||||
}
|
|
||||||
if (el.lblGachabaseInfo) {
|
|
||||||
el.lblGachabaseInfo.textContent = 'กำลังดึงฐานข้อมูลตัวละครและกรวยแสงจาก Gachabase CDN...';
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast('🌐 เริ่มซิงค์ฐานข้อมูลตัวละครและรูปภาพจาก Gachabase...', 'info');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const currentLang = getCurrentLanguage();
|
|
||||||
const result = await syncGachabaseDatabase({
|
|
||||||
apiUrl: state.apiUrl,
|
|
||||||
branch: 'beta',
|
|
||||||
lang: currentLang === 'th' ? 'th' : 'en',
|
|
||||||
metadataContext: {
|
|
||||||
AVATAR_METADATA,
|
|
||||||
LIGHTCONE_METADATA,
|
|
||||||
SIGNATURE_LIGHTCONES,
|
|
||||||
idbSet,
|
|
||||||
idbGet
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
initAvatarsList(true);
|
|
||||||
renderAvatarsGrid();
|
|
||||||
loadEquipment();
|
|
||||||
if (state.selectedAvatarDetail) {
|
|
||||||
renderModalSwitcher(state.selectedAvatarDetail.avatar_id);
|
|
||||||
}
|
|
||||||
await updateGachabaseSyncUI();
|
|
||||||
|
|
||||||
if (showToastOnDone) {
|
|
||||||
showToast(`✅ ซิงค์สำเร็จ! ตรวจพบตัวละครทั้งหมด ${result.totalAvatarsCount} ตัว (+ใหม่ ${result.newAvatarsCount}), กรวยแสง ${result.totalLcsCount} ชิ้น (+ใหม่ ${result.newLcsCount}) พร้อมผูกรูป Gachabase CDN เรียบร้อย`, 'success');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Gachabase sync failed:', err);
|
|
||||||
if (el.badgeGachabaseStatus) {
|
|
||||||
el.badgeGachabaseStatus.textContent = 'ซิงค์ล้มเหลว';
|
|
||||||
el.badgeGachabaseStatus.style.color = 'var(--accent-red)';
|
|
||||||
el.badgeGachabaseStatus.style.borderColor = 'rgba(248, 113, 113, 0.4)';
|
|
||||||
}
|
|
||||||
if (el.lblGachabaseInfo) {
|
|
||||||
el.lblGachabaseInfo.textContent = `ข้อผิดพลาด: ${err.message}`;
|
|
||||||
}
|
|
||||||
showToast(`❌ ซิงค์ข้อมูล Gachabase ไม่สำเร็จ: ${err.message}`, 'error');
|
|
||||||
} finally {
|
|
||||||
isGachabaseSyncing = false;
|
|
||||||
syncButtons.forEach(btn => {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.classList.remove('loading');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function setupGachabaseSyncEvents() {
|
|
||||||
const syncButtons = [
|
|
||||||
el.btnHeaderGachabaseSync,
|
|
||||||
el.btnQuickGachabaseSync,
|
|
||||||
el.btnSyncGachabaseAvatars,
|
|
||||||
el.btnSyncGachabaseEq,
|
|
||||||
el.btnSyncGachabaseNow
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
syncButtons.forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
triggerGachabaseSync(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup Event Listeners
|
// Setup Event Listeners
|
||||||
function setupEvents() {
|
function setupEvents() {
|
||||||
// Quick Ping & Refresh
|
// Quick Ping & Refresh
|
||||||
@@ -8462,12 +8320,6 @@ function startPingLoop() {
|
|||||||
// Initialize Application
|
// Initialize Application
|
||||||
async function init() {
|
async function init() {
|
||||||
await initGitBranchDisplay();
|
await initGitBranchDisplay();
|
||||||
await loadCachedGachabaseData({
|
|
||||||
idbGet,
|
|
||||||
idbSet,
|
|
||||||
AVATAR_METADATA,
|
|
||||||
LIGHTCONE_METADATA
|
|
||||||
});
|
|
||||||
initAvatarsList();
|
initAvatarsList();
|
||||||
await loadCachedStorageData();
|
await loadCachedStorageData();
|
||||||
setupServerModes();
|
setupServerModes();
|
||||||
@@ -8476,14 +8328,12 @@ async function init() {
|
|||||||
setupModalSubtabs();
|
setupModalSubtabs();
|
||||||
setupEvents();
|
setupEvents();
|
||||||
setupDamageAnalystEvents();
|
setupDamageAnalystEvents();
|
||||||
setupGachabaseSyncEvents();
|
|
||||||
setupGMConsoleEvents();
|
setupGMConsoleEvents();
|
||||||
applyTranslations();
|
applyTranslations();
|
||||||
renderLineup(state.statusData?.lineups || DEFAULT_LINEUP);
|
renderLineup(state.statusData?.lineups || DEFAULT_LINEUP);
|
||||||
renderAvatarsGrid();
|
renderAvatarsGrid();
|
||||||
loadEquipment();
|
loadEquipment();
|
||||||
renderEmptyDamageAnalyst();
|
renderEmptyDamageAnalyst();
|
||||||
updateGachabaseSyncUI();
|
|
||||||
refreshStatus();
|
refreshStatus();
|
||||||
fetchLatestBattleReport(true);
|
fetchLatestBattleReport(true);
|
||||||
startPingLoop();
|
startPingLoop();
|
||||||
|
|||||||
-317
@@ -36,323 +36,6 @@ export const GACHABASE_IMAGE_CACHE = {
|
|||||||
lightconeThumbs: { ...KNOWN_GACHABASE_ASSETS.lightconeThumbs }
|
lightconeThumbs: { ...KNOWN_GACHABASE_ASSETS.lightconeThumbs }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Path ID mappings according to HSR engine / Gachabase
|
|
||||||
export const GACHABASE_PATHS = {
|
|
||||||
1: 'Destruction',
|
|
||||||
2: 'Hunt',
|
|
||||||
3: 'Erudition',
|
|
||||||
4: 'Harmony',
|
|
||||||
5: 'Nihility',
|
|
||||||
6: 'Preservation',
|
|
||||||
7: 'Abundance',
|
|
||||||
8: 'Remembrance',
|
|
||||||
9: 'Elation'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Element / Type ID mappings
|
|
||||||
export const GACHABASE_ELEMENTS = {
|
|
||||||
1: 'Physical',
|
|
||||||
2: 'Fire',
|
|
||||||
4: 'Ice',
|
|
||||||
8: 'Thunder', // Lightning
|
|
||||||
16: 'Wind',
|
|
||||||
32: 'Quantum',
|
|
||||||
64: 'Imaginary'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stat ID mappings
|
|
||||||
export const GACHABASE_STATS = {
|
|
||||||
1: 'HP',
|
|
||||||
2: 'ATK',
|
|
||||||
3: 'DEF',
|
|
||||||
21: 'HP%',
|
|
||||||
22: 'ATK%',
|
|
||||||
23: 'DEF%',
|
|
||||||
31: 'Physical DMG%',
|
|
||||||
32: 'Fire DMG%',
|
|
||||||
33: 'Ice DMG%',
|
|
||||||
34: 'Lightning DMG%',
|
|
||||||
35: 'Wind DMG%',
|
|
||||||
36: 'Quantum DMG%',
|
|
||||||
37: 'Imaginary DMG%',
|
|
||||||
41: 'Break Effect%',
|
|
||||||
42: 'Outgoing Healing Boost%',
|
|
||||||
43: 'Max Energy',
|
|
||||||
44: 'Energy Regeneration Rate%',
|
|
||||||
45: 'Effect Hit Rate%',
|
|
||||||
46: 'Effect RES%',
|
|
||||||
51: 'SPD',
|
|
||||||
52: 'CRIT Rate%',
|
|
||||||
53: 'CRIT DMG%'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strips rich-text tags like <color=#f29e38ff>, </color>, <b>, </b> from text strings
|
|
||||||
*/
|
|
||||||
export function cleanRichText(str) {
|
|
||||||
if (!str || typeof str !== 'string') return '';
|
|
||||||
return str
|
|
||||||
.replace(/<color=[^>]+>/gi, '')
|
|
||||||
.replace(/<\/color>/gi, '')
|
|
||||||
.replace(/<[^>]+>/g, '')
|
|
||||||
.replace(/\\n/g, '\n')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches HTML from targetUrl via backend API proxy or direct fetch
|
|
||||||
*/
|
|
||||||
export async function fetchGachabaseHtml(targetUrl, apiUrl = 'http://localhost:21000') {
|
|
||||||
if (!targetUrl) throw new Error('Target URL is required for Gachabase fetch');
|
|
||||||
|
|
||||||
// 1. Try Backend Proxy
|
|
||||||
if (apiUrl) {
|
|
||||||
try {
|
|
||||||
const cleanApi = apiUrl.replace(/\/+$/, '');
|
|
||||||
const proxyUrl = `${cleanApi}/api/gachabase/fetch?url=${encodeURIComponent(targetUrl)}`;
|
|
||||||
const resp = await fetch(proxyUrl, { method: 'GET', headers: { 'Accept': 'application/json' } });
|
|
||||||
if (resp.ok) {
|
|
||||||
const json = await resp.json();
|
|
||||||
if (json && json.html) {
|
|
||||||
return { html: json.html, finalUrl: json.url || targetUrl };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Backend Gachabase proxy unreachable, attempting direct fetch...', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Direct fetch fallback
|
|
||||||
const directResp = await fetch(targetUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!directResp.ok) {
|
|
||||||
throw new Error(`Direct fetch failed with status ${directResp.status}`);
|
|
||||||
}
|
|
||||||
const html = await directResp.text();
|
|
||||||
return { html, finalUrl: targetUrl };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses SvelteKit streamed data payload from Gachabase catalog listing pages
|
|
||||||
*/
|
|
||||||
export function parseSvelteKitCatalog(html) {
|
|
||||||
if (!html || typeof html !== 'string') return null;
|
|
||||||
const scripts = [...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/gi)];
|
|
||||||
for (const s of scripts) {
|
|
||||||
const text = s[1].trim();
|
|
||||||
if (text.includes('resolve') && text.includes('entries')) {
|
|
||||||
const startIdx = text.indexOf('() => ');
|
|
||||||
if (startIdx !== -1) {
|
|
||||||
let code = text.substring(startIdx + 6).trim();
|
|
||||||
if (code.endsWith(');')) code = code.slice(0, -2);
|
|
||||||
else if (code.endsWith(')')) code = code.slice(0, -1);
|
|
||||||
try {
|
|
||||||
const fn = new Function('return (' + code + ')');
|
|
||||||
const res = fn();
|
|
||||||
return res[0]?.data;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to parse catalog script:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatically synchronizes the full game database from Gachabase
|
|
||||||
* Pulls all characters and lightcones, adds missing ones into metadata,
|
|
||||||
* and caches all Gachabase CDN asset URLs for seamless rendering.
|
|
||||||
*/
|
|
||||||
export async function syncGachabaseDatabase({ apiUrl, branch = 'beta', lang = 'en', metadataContext }) {
|
|
||||||
const {
|
|
||||||
AVATAR_METADATA,
|
|
||||||
LIGHTCONE_METADATA,
|
|
||||||
SIGNATURE_LIGHTCONES,
|
|
||||||
idbSet,
|
|
||||||
idbGet
|
|
||||||
} = metadataContext;
|
|
||||||
|
|
||||||
const charactersUrl = `${GACHABASE_BASE_URL}/characters?lang=${lang}&branch=${branch}`;
|
|
||||||
const lightconesUrl = `${GACHABASE_BASE_URL}/lightcones?lang=${lang}&branch=${branch}`;
|
|
||||||
|
|
||||||
let newAvatarsCount = 0;
|
|
||||||
let newLcsCount = 0;
|
|
||||||
let totalAvatarsCount = 0;
|
|
||||||
let totalLcsCount = 0;
|
|
||||||
|
|
||||||
// 1. Fetch & Parse Characters Catalog
|
|
||||||
try {
|
|
||||||
const { html: charHtml } = await fetchGachabaseHtml(charactersUrl, apiUrl);
|
|
||||||
const charData = parseSvelteKitCatalog(charHtml);
|
|
||||||
|
|
||||||
if (charData && Array.isArray(charData.entries)) {
|
|
||||||
totalAvatarsCount = charData.entries.length;
|
|
||||||
const assetsMap = charData.refs?.assets || {};
|
|
||||||
|
|
||||||
for (const entry of charData.entries) {
|
|
||||||
const aid = entry.id;
|
|
||||||
const hash = entry.assets?.square_icon_path_hash || entry.assets?.round_icon_path_hash;
|
|
||||||
const iconUrl = assetsMap[hash]?.url || KNOWN_GACHABASE_ASSETS.avatars[aid] || `${GACHABASE_CDN_URL}/hsr/assets/${hash}.png`;
|
|
||||||
|
|
||||||
if (iconUrl) {
|
|
||||||
GACHABASE_IMAGE_CACHE.avatars[aid] = iconUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register new / missing characters into AVATAR_METADATA
|
|
||||||
if (AVATAR_METADATA && !AVATAR_METADATA[aid]) {
|
|
||||||
const charName = entry.name?.text || entry.name || `Avatar ${aid}`;
|
|
||||||
const pathName = GACHABASE_PATHS[entry.path_id] || 'Unknown';
|
|
||||||
const elementName = GACHABASE_ELEMENTS[entry.type_id] || 'Unknown';
|
|
||||||
|
|
||||||
AVATAR_METADATA[aid] = {
|
|
||||||
names: {
|
|
||||||
th: charName,
|
|
||||||
en: charName,
|
|
||||||
'zh-CN': charName
|
|
||||||
},
|
|
||||||
path: pathName,
|
|
||||||
element: elementName,
|
|
||||||
rarity: entry.rarity || 5,
|
|
||||||
hasExRank: false,
|
|
||||||
isGachabaseSync: true,
|
|
||||||
images: {
|
|
||||||
icon: iconUrl,
|
|
||||||
square: iconUrl
|
|
||||||
}
|
|
||||||
};
|
|
||||||
newAvatarsCount++;
|
|
||||||
} else if (AVATAR_METADATA && AVATAR_METADATA[aid]) {
|
|
||||||
// Enrich existing metadata with Gachabase CDN image
|
|
||||||
AVATAR_METADATA[aid].images = AVATAR_METADATA[aid].images || {};
|
|
||||||
if (iconUrl) AVATAR_METADATA[aid].images.icon = iconUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to sync characters from Gachabase:', err);
|
|
||||||
throw new Error('ไม่สามารถซิงค์ตัวละครจาก Gachabase ได้: ' + err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Fetch & Parse Lightcones Catalog
|
|
||||||
try {
|
|
||||||
const { html: lcHtml } = await fetchGachabaseHtml(lightconesUrl, apiUrl);
|
|
||||||
const lcData = parseSvelteKitCatalog(lcHtml);
|
|
||||||
|
|
||||||
if (lcData && Array.isArray(lcData.entries)) {
|
|
||||||
totalLcsCount = lcData.entries.length;
|
|
||||||
const assetsMap = lcData.refs?.assets || {};
|
|
||||||
|
|
||||||
for (const entry of lcData.entries) {
|
|
||||||
const lcid = entry.id;
|
|
||||||
const thumbHash = entry.assets?.thumbnail_art_path_hash || entry.assets?.small_icon_path_hash;
|
|
||||||
const fullArtHash = entry.assets?.full_art_path_hash || entry.assets?.big_icon_path_hash;
|
|
||||||
|
|
||||||
const thumbUrl = assetsMap[thumbHash]?.url || KNOWN_GACHABASE_ASSETS.lightconeThumbs[lcid];
|
|
||||||
const fullArtUrl = assetsMap[fullArtHash]?.url || KNOWN_GACHABASE_ASSETS.lightcones[lcid];
|
|
||||||
|
|
||||||
if (thumbUrl) GACHABASE_IMAGE_CACHE.lightconeThumbs[lcid] = thumbUrl;
|
|
||||||
if (fullArtUrl) GACHABASE_IMAGE_CACHE.lightcones[lcid] = fullArtUrl;
|
|
||||||
|
|
||||||
// Register new / missing lightcones into LIGHTCONE_METADATA
|
|
||||||
if (LIGHTCONE_METADATA && !LIGHTCONE_METADATA[lcid]) {
|
|
||||||
const lcName = entry.name?.text || entry.name || `Lightcone ${lcid}`;
|
|
||||||
const pathName = GACHABASE_PATHS[entry.path_id] || 'Unknown';
|
|
||||||
|
|
||||||
LIGHTCONE_METADATA[lcid] = {
|
|
||||||
names: {
|
|
||||||
th: lcName,
|
|
||||||
en: lcName,
|
|
||||||
'zh-CN': lcName
|
|
||||||
},
|
|
||||||
rarity: entry.rarity || 5,
|
|
||||||
path: pathName,
|
|
||||||
isGachabaseSync: true,
|
|
||||||
images: {
|
|
||||||
thumbnail: thumbUrl,
|
|
||||||
fullArt: fullArtUrl
|
|
||||||
}
|
|
||||||
};
|
|
||||||
newLcsCount++;
|
|
||||||
} else if (LIGHTCONE_METADATA && LIGHTCONE_METADATA[lcid]) {
|
|
||||||
LIGHTCONE_METADATA[lcid].images = LIGHTCONE_METADATA[lcid].images || {};
|
|
||||||
if (thumbUrl) LIGHTCONE_METADATA[lcid].images.thumbnail = thumbUrl;
|
|
||||||
if (fullArtUrl) LIGHTCONE_METADATA[lcid].images.fullArt = fullArtUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to sync lightcones from Gachabase:', err);
|
|
||||||
throw new Error('ไม่สามารถซิงค์กรวยแสงจาก Gachabase ได้: ' + err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Save cache to IndexedDB
|
|
||||||
if (idbSet) {
|
|
||||||
try {
|
|
||||||
await idbSet('gac_gachabase_image_cache', GACHABASE_IMAGE_CACHE);
|
|
||||||
await idbSet('gac_gachabase_last_sync', {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
newAvatarsCount,
|
|
||||||
newLcsCount,
|
|
||||||
totalAvatarsCount,
|
|
||||||
totalLcsCount
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to cache Gachabase database to IndexedDB:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
newAvatarsCount,
|
|
||||||
newLcsCount,
|
|
||||||
totalAvatarsCount,
|
|
||||||
totalLcsCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads cached Gachabase images and metadata from IndexedDB
|
|
||||||
*/
|
|
||||||
export async function loadCachedGachabaseData(metadataContext = {}) {
|
|
||||||
const { idbGet, AVATAR_METADATA, LIGHTCONE_METADATA } = metadataContext;
|
|
||||||
if (!idbGet) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cachedImages = await idbGet('gac_gachabase_image_cache');
|
|
||||||
if (cachedImages) {
|
|
||||||
if (cachedImages.avatars) Object.assign(GACHABASE_IMAGE_CACHE.avatars, cachedImages.avatars);
|
|
||||||
if (cachedImages.avatarSplash) Object.assign(GACHABASE_IMAGE_CACHE.avatarSplash, cachedImages.avatarSplash);
|
|
||||||
if (cachedImages.lightcones) Object.assign(GACHABASE_IMAGE_CACHE.lightcones, cachedImages.lightcones);
|
|
||||||
if (cachedImages.lightconeThumbs) Object.assign(GACHABASE_IMAGE_CACHE.lightconeThumbs, cachedImages.lightconeThumbs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach cached image links to metadata
|
|
||||||
if (AVATAR_METADATA) {
|
|
||||||
for (const aid in GACHABASE_IMAGE_CACHE.avatars) {
|
|
||||||
if (AVATAR_METADATA[aid]) {
|
|
||||||
AVATAR_METADATA[aid].images = AVATAR_METADATA[aid].images || {};
|
|
||||||
AVATAR_METADATA[aid].images.icon = GACHABASE_IMAGE_CACHE.avatars[aid];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (LIGHTCONE_METADATA) {
|
|
||||||
for (const lcid in GACHABASE_IMAGE_CACHE.lightcones) {
|
|
||||||
if (LIGHTCONE_METADATA[lcid]) {
|
|
||||||
LIGHTCONE_METADATA[lcid].images = LIGHTCONE_METADATA[lcid].images || {};
|
|
||||||
LIGHTCONE_METADATA[lcid].images.fullArt = GACHABASE_IMAGE_CACHE.lightcones[lcid];
|
|
||||||
LIGHTCONE_METADATA[lcid].images.thumbnail = GACHABASE_IMAGE_CACHE.lightconeThumbs[lcid];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Failed to restore Gachabase cache from IndexedDB:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image resolver for avatars
|
* Image resolver for avatars
|
||||||
* Prioritizes local asset, falls back to Gachabase CDN
|
* Prioritizes local asset, falls back to Gachabase CDN
|
||||||
|
|||||||
Reference in new issue
Block a user