diff --git a/index.html b/index.html index e028b08..35e95ad 100644 --- a/index.html +++ b/index.html @@ -45,8 +45,8 @@ 🔄 รีเฟรช - @@ -199,12 +199,12 @@ - - @@ -246,7 +246,7 @@
- + @@ -299,7 +299,7 @@
- +
@@ -603,6 +603,31 @@
+ +
+
+ 🌐 อัปเดตข้อมูลเกม & รูปภาพ (Gachabase Sync) + พร้อมใช้งาน +
+

+ ดึงข้อมูลตัวละคร, กรวยแสง, และรูปภาพความละเอียดสูงจาก Gachabase อัตโนมัติเมื่อเกมมีแพตช์ใหม่หรือมีคอนเทนต์เบต้า +

+ +
+
+
+
ฐานข้อมูลตัวละครและกรวยแสง
+
+ ตัวละครในระบบ: 124+ ตัว | กรวยแสง: 172+ ชิ้น (รองรับเนื้อหา 3.0+ และเบต้า) +
+
+ +
+
+
+ @@ -1387,81 +1412,6 @@ - - -
diff --git a/js/app.js b/js/app.js index f0d5f01..3a63ed6 100644 --- a/js/app.js +++ b/js/app.js @@ -2,14 +2,26 @@ import { AVATAR_METADATA, getAvatarInfo } from './avatars-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 { - parseGachabaseInput, - fetchGachabaseHtml, - parseGachabaseHtml, - registerGachabaseItem, - loadCachedGachabaseRegistry, - applyGachabaseToFreeSR + syncGachabaseDatabase, + loadCachedGachabaseData, + getAvatarIconUrl, + getLightconeArtUrl, + getLightconeThumbUrl, + handleAvatarImgError, + handleLcImgError, + GACHABASE_IMAGE_CACHE, + KNOWN_GACHABASE_ASSETS } from './gachabase.js'; +// Expose image resolvers & error handlers to window for inline onclick/onerror attributes +window.handleAvatarImgError = handleAvatarImgError; +window.handleLcImgError = handleLcImgError; +window.getAvatarIconUrl = getAvatarIconUrl; +window.getLightconeArtUrl = getLightconeArtUrl; +window.getLightconeThumbUrl = getLightconeThumbUrl; +window.GACHABASE_IMAGE_CACHE = GACHABASE_IMAGE_CACHE; +window.KNOWN_GACHABASE_ASSETS = KNOWN_GACHABASE_ASSETS; + // Application State const state = { serverMode: localStorage.getItem('gac_srtool_mode') || 'gac', // 'gac' | 'firefly' | 'robin' @@ -66,25 +78,14 @@ const el = { btnQuickAddAllLcs: document.getElementById('btn-quick-add-all-lcs'), btnQuickTeleportTrain: document.getElementById('btn-quick-teleport-train'), btnQuickClearLineup: document.getElementById('btn-quick-clear-lineup'), - btnQuickGachabase: document.getElementById('btn-quick-gachabase'), - btnOpenGachabase: document.getElementById('btn-open-gachabase'), - btnAvatarsGachabase: document.getElementById('btn-avatars-gachabase'), - btnEqGachabase: document.getElementById('btn-eq-gachabase'), - - // Gachabase Modal - modalGachabase: document.getElementById('modal-gachabase'), - btnCloseGachabaseModal: document.getElementById('btn-close-gachabase-modal'), - gachabaseInput: document.getElementById('gachabase-input'), - gachabaseType: document.getElementById('gachabase-type'), - gachabaseBranch: document.getElementById('gachabase-branch'), - gachabaseLang: document.getElementById('gachabase-lang'), - btnGachabaseFetch: document.getElementById('btn-gachabase-fetch'), - gachabaseStatus: document.getElementById('gachabase-status'), - gachabasePreviewContainer: document.getElementById('gachabase-preview-container'), - gachabaseFooterActions: document.getElementById('gachabase-footer-actions'), - btnGachabaseSaveMeta: document.getElementById('btn-gachabase-save-meta'), - btnGachabaseImportE0S1: document.getElementById('btn-gachabase-import-e0s1'), - btnGachabaseImportE6S5: document.getElementById('btn-gachabase-import-e6s5'), + // 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 avatarsGrid: document.getElementById('avatars-grid'), @@ -977,8 +978,8 @@ function renderLineup(lineups) {
Slot ${slot + 1}
${removeSlotBtnHtml} ${info.name}
${info.name}
@@ -1135,8 +1136,8 @@ function renderPickerGrid() { card.innerHTML = `
${meta.name}
@@ -1266,8 +1267,8 @@ function renderAvatarsGrid() {
${meta.name}
@@ -1575,12 +1576,14 @@ const RECOMMENDED_RELIC_BUILDS = { Elation: { cavern: 115, planar: 306, name: "Grand Duke (4) + Inert Salsotto (2)" } }; -// Lightcone Art URL Resolvers (Local asset -> Gachabase -> StarRailRes) +// Lightcone Art URL Resolvers (Metadata -> Gachabase CDN -> Local asset -> StarRailRes) function getLightconeArtSrc(lcId) { const id = parseInt(lcId, 10); const meta = LIGHTCONE_METADATA[id]; if (meta?.images?.fullArt) return meta.images.fullArt; if (meta?.images?.thumbnail) return meta.images.thumbnail; + if (GACHABASE_IMAGE_CACHE.lightcones[id]) return GACHABASE_IMAGE_CACHE.lightcones[id]; + if (KNOWN_GACHABASE_ASSETS.lightcones[id]) return KNOWN_GACHABASE_ASSETS.lightcones[id]; if (id === 23065) { return './assets/lightcones/23065.png'; } @@ -1592,6 +1595,10 @@ function getLightconeArtFallback(lcId) { const meta = LIGHTCONE_METADATA[id]; if (meta?.images?.thumbnail) return meta.images.thumbnail; if (meta?.images?.fullArt) return meta.images.fullArt; + if (GACHABASE_IMAGE_CACHE.lightconeThumbs[id]) return GACHABASE_IMAGE_CACHE.lightconeThumbs[id]; + if (KNOWN_GACHABASE_ASSETS.lightconeThumbs[id]) return KNOWN_GACHABASE_ASSETS.lightconeThumbs[id]; + if (GACHABASE_IMAGE_CACHE.lightcones[id]) return GACHABASE_IMAGE_CACHE.lightcones[id]; + if (KNOWN_GACHABASE_ASSETS.lightcones[id]) return KNOWN_GACHABASE_ASSETS.lightcones[id]; if (id === 23065) { return 'https://cdn.gachabase.net/hsr/assets/500cfc18698932759756f6b3ba4db417.png'; } @@ -1603,6 +1610,8 @@ function getAvatarIconSrc(aid) { const meta = AVATAR_METADATA[id]; if (meta?.images?.icon) return meta.images.icon; if (meta?.images?.square) return meta.images.square; + if (GACHABASE_IMAGE_CACHE.avatars[id]) return GACHABASE_IMAGE_CACHE.avatars[id]; + if (KNOWN_GACHABASE_ASSETS.avatars[id]) return KNOWN_GACHABASE_ASSETS.avatars[id]; return `./assets/avatars/${id}.png`; } @@ -1611,6 +1620,8 @@ function getAvatarSplashSrc(aid) { const meta = AVATAR_METADATA[id]; if (meta?.images?.splash) return meta.images.splash; if (meta?.images?.fullArt) return meta.images.fullArt; + if (GACHABASE_IMAGE_CACHE.avatarSplash[id]) return GACHABASE_IMAGE_CACHE.avatarSplash[id]; + if (KNOWN_GACHABASE_ASSETS.avatarSplash[id]) return KNOWN_GACHABASE_ASSETS.avatarSplash[id]; return `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/image/character_portrait/${id}.png`; } @@ -1888,8 +1899,8 @@ function renderModalSwitcher(currentAvatarId) { item.className = `modal-char-switcher-item ${av.avatar_id === currentAvatarId ? 'active' : ''}`; item.title = `${meta.name} (ID: ${av.avatar_id})`; item.innerHTML = ` - ${meta.name} `; item.addEventListener('click', () => { @@ -1909,12 +1920,9 @@ function renderModalOverviewTab(avatarId) { // Portrait Art if (el.overviewPortraitImg) { - el.overviewPortraitImg.src = `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/image/character_portrait/${aid}.png`; + el.overviewPortraitImg.src = getAvatarSplashSrc(aid); el.overviewPortraitImg.onerror = function() { - this.src = `./assets/avatars/${aid}.png`; - this.onerror = function() { - this.src = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${aid}.png`; - }; + handleAvatarImgError(this, aid); }; } @@ -1940,12 +1948,7 @@ function renderModalOverviewTab(avatarId) { if (el.overviewLcThumb) { el.overviewLcThumb.src = getLightconeArtSrc(lcId); el.overviewLcThumb.onerror = function() { - const fallback = getLightconeArtFallback(lcId); - if (this.src !== fallback) { - this.src = fallback; - } else { - this.src = './assets/logo.png'; - } + handleLcImgError(this, lcId); }; } if (el.overviewLcName) el.overviewLcName.textContent = lcInfo.name || `Light Cone ${lcId}`; @@ -2076,9 +2079,7 @@ function renderModalRelicsTab(avatarId) { if (lcThumbEl) { lcThumbEl.src = getLightconeArtSrc(lcId); lcThumbEl.onerror = function() { - const fb = getLightconeArtFallback(lcId); - if (this.src !== fb) this.src = fb; - else this.src = './assets/logo.png'; + handleLcImgError(this, lcId); }; } if (lcNameEl) lcNameEl.textContent = lcInfo.name || `Light Cone ${lcId}`; @@ -2710,9 +2711,9 @@ function openAvatarDetailModal(avatarItem) { const meta = getAvatarInfo(aid); el.modalCharTitle.textContent = `${t('modalTitleCharEdit')}: ${meta.name} (ID: ${aid})`; - el.modalCharImg.src = `./assets/avatars/${aid}.png`; + el.modalCharImg.src = getAvatarIconSrc(aid); el.modalCharImg.onerror = function() { - this.src = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${aid}.png`; + handleAvatarImgError(this, aid); }; el.modalCharName.textContent = meta.name; el.modalCharMeta.innerHTML = ` @@ -3274,15 +3275,14 @@ function renderShowcaseCard(avatarId) { `; } - const primaryPortrait = `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/image/character_portrait/${aid}.png`; + const primaryPortrait = getAvatarSplashSrc(aid); const fallbackPortrait = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/image/character_portrait/${aid}.png`; const lcArtThumb = getLightconeArtSrc(lcId); - const fallbackLcThumb = getLightconeArtFallback(lcId); el.showcaseCardContainer.innerHTML = `
- ${meta.name} + ${meta.name}
${meta.name}
@@ -3300,7 +3300,7 @@ function renderShowcaseCard(avatarId) {
- ${lcInfo.name} + ${lcInfo.name}
${lcInfo.name}
@@ -4017,15 +4017,14 @@ function renderLightconesGrid() { const wearerBadgeHtml = lc.equip_avatar ? `
- Wearer + Wearer
` : ''; const primaryImg = getLightconeArtSrc(lc.item_id); - const fallbackImg = getLightconeArtFallback(lc.item_id); card.innerHTML = ` - ${meta.name} + ${meta.name}
${roman}
${wearerBadgeHtml}
@@ -4085,7 +4084,7 @@ function renderRelicsGrid() { const wearerBadgeHtml = r.equip_avatar ? `
- Wearer + Wearer
` : ''; @@ -4132,13 +4131,12 @@ function showLcInspector(lc) { const def = lcMeta.def || (lcMeta.rarity === 5 ? 529 : (lcMeta.rarity === 4 ? 396 : 264)); const primaryThumb = getLightconeArtSrc(lc.item_id); - const fallbackThumb = getLightconeArtFallback(lc.item_id); el.eqInspectorCard.style.display = 'flex'; el.eqInspectorCard.innerHTML = `
- ${lcInfo.name} + ${lcInfo.name}
${lcInfo.name}
${'★'.repeat(lcInfo.rarity || 5)}
@@ -4386,7 +4384,7 @@ function updateAddLcPreview(lcId) { const previewThumb = document.getElementById('add-lc-preview-thumb'); if (previewThumb) { previewThumb.src = getLightconeArtSrc(id); - previewThumb.onerror = function() { this.src = getLightconeArtFallback(id); }; + previewThumb.onerror = function() { handleLcImgError(this, id); }; } const previewName = document.getElementById('add-lc-preview-name'); @@ -4463,7 +4461,7 @@ function renderAddLcVisualGrid() { card.dataset.lcId = id; card.innerHTML = ` - ${info.name} + ${info.name}
${info.name}
${'★'.repeat(info.rarity || 5)}
@@ -4706,8 +4704,8 @@ function renderExRanks(exList) {
${info.name}
@@ -4966,6 +4964,7 @@ function setupTabs() { loadJsonData(state.currentJsonMode); } else if (targetId === 'tab-settings') { updateCacheUsageDisplay(); + updateGachabaseSyncUI(); } } }); @@ -4973,323 +4972,134 @@ function setupTabs() { } // ========================================== -// Gachabase Data Fetcher & Importer Logic +// Gachabase Catalog Sync & Dynamic Image Engine // ========================================== -let currentGachabaseItem = null; +let isGachabaseSyncing = false; -function openGachabaseModal(defaultId = null, defaultType = 'character') { - if (!el.modalGachabase) return; - el.modalGachabase.classList.add('active'); - if (defaultId) { - if (el.gachabaseInput) el.gachabaseInput.value = String(defaultId); - if (el.gachabaseType) el.gachabaseType.value = defaultType; - performGachabaseFetch(); - } else { - setTimeout(() => el.gachabaseInput?.focus(), 50); +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 = `ซิงค์ล่าสุดเมื่อ: ${dateStr} (ตัวละครในระบบ: ${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); } } -function closeGachabaseModal() { - if (!el.modalGachabase) return; - el.modalGachabase.classList.remove('active'); -} - -async function performGachabaseFetch() { - const input = el.gachabaseInput?.value?.trim(); - if (!input) { - showToast('กรุณาระบุ ID หรือ URL ของ Gachabase', 'warning'); +async function triggerGachabaseSync(showToastOnDone = true) { + if (isGachabaseSyncing) { + showToast('กำลังซิงค์ข้อมูลจาก Gachabase อยู่แล้ว กรุณารอสักครู่...', 'warning'); return; } - const type = el.gachabaseType?.value || 'character'; - const branch = el.gachabaseBranch?.value || 'beta'; - const lang = el.gachabaseLang?.value || 'en'; + isGachabaseSyncing = true; + const syncButtons = [ + el.btnHeaderGachabaseSync, + el.btnQuickGachabaseSync, + el.btnSyncGachabaseAvatars, + el.btnSyncGachabaseEq, + el.btnSyncGachabaseNow + ].filter(Boolean); - const parsedParams = parseGachabaseInput(input, type, branch, lang); - if (!parsedParams) { - showToast('รูปแบบข้อมูลไม่ถูกต้อง กรุณาระบุ ID ตัวเลขหรือ URL', 'error'); - return; + 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...'; } - if (parsedParams.type && el.gachabaseType) el.gachabaseType.value = parsedParams.type; - if (parsedParams.branch && el.gachabaseBranch) el.gachabaseBranch.value = parsedParams.branch; - - if (el.gachabaseStatus) { - el.gachabaseStatus.style.display = 'block'; - el.gachabaseStatus.style.background = 'rgba(56, 189, 248, 0.15)'; - el.gachabaseStatus.style.border = '1px solid rgba(56, 189, 248, 0.4)'; - el.gachabaseStatus.style.color = 'var(--accent-cyan)'; - el.gachabaseStatus.innerHTML = `🔄 กำลังดึงข้อมูลจาก Gachabase (${parsedParams.url})...`; - } - if (el.gachabasePreviewContainer) el.gachabasePreviewContainer.style.display = 'none'; - if (el.gachabaseFooterActions) el.gachabaseFooterActions.style.display = 'none'; - if (el.btnGachabaseFetch) el.btnGachabaseFetch.disabled = true; + showToast('🌐 เริ่มซิงค์ฐานข้อมูลตัวละครและรูปภาพจาก Gachabase...', 'info'); try { - const { html, finalUrl } = await fetchGachabaseHtml(parsedParams.url, state.apiUrl); - const parsedItem = parseGachabaseHtml(html, lang); - parsedItem.sourceUrl = finalUrl; - parsedItem.branch = parsedParams.branch; - parsedItem.lang = lang; - currentGachabaseItem = parsedItem; + 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 + } + }); - if (el.gachabaseStatus) { - el.gachabaseStatus.style.background = 'rgba(52, 211, 153, 0.15)'; - el.gachabaseStatus.style.border = '1px solid rgba(52, 211, 153, 0.4)'; - el.gachabaseStatus.style.color = 'var(--accent-green)'; - el.gachabaseStatus.innerHTML = `✅ ดึงข้อมูลสำเร็จ: [${parsedItem.rarity}★] ${parsedItem.name} (ID: ${parsedItem.id})`; + initAvatarsList(true); + renderAvatarsGrid(); + loadEquipment(); + if (state.selectedAvatarDetail) { + renderModalSwitcher(state.selectedAvatarDetail.avatar_id); } + await updateGachabaseSyncUI(); - renderGachabasePreview(parsedItem); + if (showToastOnDone) { + showToast(`✅ ซิงค์สำเร็จ! ตรวจพบตัวละครทั้งหมด ${result.totalAvatarsCount} ตัว (+ใหม่ ${result.newAvatarsCount}), กรวยแสง ${result.totalLcsCount} ชิ้น (+ใหม่ ${result.newLcsCount}) พร้อมผูกรูป Gachabase CDN เรียบร้อย`, 'success'); + } } catch (err) { - console.error('Gachabase fetch error:', err); - if (el.gachabaseStatus) { - el.gachabaseStatus.style.background = 'rgba(248, 113, 113, 0.15)'; - el.gachabaseStatus.style.border = '1px solid rgba(248, 113, 113, 0.4)'; - el.gachabaseStatus.style.color = 'var(--accent-red)'; - el.gachabaseStatus.innerHTML = `❌ เกิดข้อผิดพลาดในการดึงข้อมูล: ${err.message}`; + 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 { - if (el.btnGachabaseFetch) el.btnGachabaseFetch.disabled = false; - } -} - -function renderGachabasePreview(item) { - if (!el.gachabasePreviewContainer) return; - el.gachabasePreviewContainer.style.display = 'block'; - if (el.gachabaseFooterActions) el.gachabaseFooterActions.style.display = 'flex'; - - const isChar = item.type === 'character'; - const isLc = item.type === 'lightcone'; - const isRelic = item.type === 'relic'; - - if (el.btnGachabaseImportE0S1) { - el.btnGachabaseImportE0S1.textContent = isChar ? '⚡ นำเข้า FreeSR (E0S1)' : '⚡ เพิ่มเข้า FreeSR (S1)'; - } - if (el.btnGachabaseImportE6S5) { - el.btnGachabaseImportE6S5.textContent = isChar ? '⭐ นำเข้า FreeSR (E6S5)' : '⭐ เพิ่มเข้า FreeSR (S5)'; - } - - const starsHtml = '⭐'.repeat(item.rarity || 5); - - let mainImgUrl = ''; - if (isChar) { - mainImgUrl = item.images.splash || item.images.icon || `./assets/avatars/${item.id}.png`; - } else if (isLc) { - mainImgUrl = item.images.fullArt || item.images.thumbnail || `./assets/lightcones/${item.id}.png`; - } else { - mainImgUrl = item.images.icon || `./assets/logo.png`; - } - - let recHtml = ''; - if (isChar) { - const cavernSets = item.recommended.cavernSets || []; - const planarSets = item.recommended.planarSets || []; - const sigLcId = item.recommended.signatureLcId; - const sigLcMeta = sigLcId ? getLightconeInfo(sigLcId) : null; - - recHtml = ` -
-
- 💡 คำแนะนำการจัด Build จาก Gachabase -
-
-
- 🗡️ กรวยแสงแนะนำ (Signature): - ${sigLcMeta ? sigLcMeta.name : (sigLcId ? `LC ID: ${sigLcId}` : 'ตามสายตัวละคร')} -
-
- 🛡️ เซ็ตรีลิกส์แนะนำ (Cavern): - ${cavernSets.map(s => getRelicSetInfo(s).name).join(', ') || 'ตามประเภทสาย'} -
-
- 🔮 เครื่องประดับแนะนำ (Planar): - ${planarSets.map(s => getRelicSetInfo(s).name).join(', ') || 'ตามประเภทสาย'} -
-
-
- `; - } - - let skillsHtml = ''; - if (isChar && item.skills && item.skills.length > 0) { - skillsHtml = ` -
-

- ⚡ สกิลและการต่อสู้ (${item.skills.length} ท่า) -

-
- ${item.skills.map(s => ` -
- ${s.icon ? `${s.name}` : ''} -
-
- ${s.name} - ${s.type} - ${s.tag ? `${s.tag}` : ''} -
-
${s.description}
-
-
- `).join('')} -
-
- `; - } else if (isLc && item.lcSkill) { - skillsHtml = ` -
-
- ✨ เอฟเฟกต์สกิลพิเศษ: ${item.lcSkill.name || 'Passive Skill'} -
-
${item.lcSkill.description || '-'}
-
- `; - } - - let eidolonsHtml = ''; - if (isChar && item.eidolons && item.eidolons.length > 0) { - eidolonsHtml = ` -
-

- ⭐ กลุ่มดาว Eidolons (E1 - E6) -

-
- ${item.eidolons.map(e => ` -
- E${e.rank} -
-
${e.name}
-
${e.description}
-
-
- `).join('')} -
-
- `; - } - - el.gachabasePreviewContainer.innerHTML = ` -
-
- ${item.name} -
${starsHtml}
- ${isChar && item.maxEnergy ? `
⚡ พลังงานสูงสุด: ${item.maxEnergy}
` : ''} -
- -
-
- ${item.name} - ID: ${item.id} -
- -
- ${isChar ? `${getElementIconHtml(item.element, 20)} ${translateElement(item.element)}` : ''} - ${getPathIconHtml(item.path, 20)} ${translatePath(item.path)} - ${item.branch.toUpperCase()} -
- - ${recHtml} - ${skillsHtml} - ${eidolonsHtml} -
-
- `; -} - -async function handleGachabaseSaveMetaOnly() { - if (!currentGachabaseItem) return; - try { - await registerGachabaseItem(currentGachabaseItem, { - AVATAR_METADATA, - LIGHTCONE_METADATA, - RELIC_SET_METADATA, - SIGNATURE_LIGHTCONES, - idbGet, - idbSet + isGachabaseSyncing = false; + syncButtons.forEach(btn => { + btn.disabled = false; + btn.classList.remove('loading'); }); - initAvatarsList(true); - renderAvatarsGrid(); - loadEquipment(); - showToast(`💾 บันทึก Metadata ของ "${currentGachabaseItem.name}" เข้าสู่ระบบเรียบร้อย`, 'success'); - } catch (err) { - showToast(err.message, 'error'); } } -async function handleGachabaseImportToFreeSR(eLevel, sLevel) { - if (!currentGachabaseItem) return; - try { - await registerGachabaseItem(currentGachabaseItem, { - AVATAR_METADATA, - LIGHTCONE_METADATA, - RELIC_SET_METADATA, - SIGNATURE_LIGHTCONES, - idbGet, - idbSet - }); - if (!state.fullFreesrData) { - state.fullFreesrData = { avatars: {}, lightcones: [], relics: [] }; - } - const result = applyGachabaseToFreeSR(currentGachabaseItem, state.fullFreesrData, { - eLevel, - sLevel, - autoEquipLc: true, - autoEquipRelics: true - }); - await saveFreesrChangesToServer(); - initAvatarsList(true); - renderAvatarsGrid(); - loadEquipment(); - updateDashboard(); +function setupGachabaseSyncEvents() { + const syncButtons = [ + el.btnHeaderGachabaseSync, + el.btnQuickGachabaseSync, + el.btnSyncGachabaseAvatars, + el.btnSyncGachabaseEq, + el.btnSyncGachabaseNow + ].filter(Boolean); - closeGachabaseModal(); - - if (currentGachabaseItem.type === 'character' && state.fullFreesrData.avatars[currentGachabaseItem.id]) { - openAvatarDetailModal(state.fullFreesrData.avatars[currentGachabaseItem.id]); - } - - const rankLabel = currentGachabaseItem.type === 'character' ? (eLevel === 6 ? 'E6S5' : 'E0S1') : `S${sLevel}`; - showToast(`⚡ นำเข้า "${currentGachabaseItem.name}" (${rankLabel}) ลง FreeSR และบันทึกเซิร์ฟเวอร์สำเร็จ!`, 'success'); - } catch (err) { - console.error('Import error:', err); - showToast(err.message, 'error'); - } -} - -function setupGachabaseEvents() { - const openButtons = [el.btnOpenGachabase, el.btnQuickGachabase, el.btnAvatarsGachabase, el.btnEqGachabase]; - openButtons.forEach(btn => { - if (btn) { - btn.addEventListener('click', () => openGachabaseModal()); - } - }); - - el.btnCloseGachabaseModal?.addEventListener('click', closeGachabaseModal); - - document.querySelectorAll('.gachabase-quick-chip').forEach(chip => { - chip.addEventListener('click', () => { - const cid = chip.getAttribute('data-id'); - const ctype = chip.getAttribute('data-type') || 'character'; - if (el.gachabaseInput) el.gachabaseInput.value = cid; - if (el.gachabaseType) el.gachabaseType.value = ctype; - performGachabaseFetch(); + syncButtons.forEach(btn => { + btn.addEventListener('click', () => { + triggerGachabaseSync(true); }); }); - - el.btnGachabaseFetch?.addEventListener('click', performGachabaseFetch); - el.gachabaseInput?.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - performGachabaseFetch(); - } - }); - - el.btnGachabaseSaveMeta?.addEventListener('click', handleGachabaseSaveMetaOnly); - el.btnGachabaseImportE0S1?.addEventListener('click', () => handleGachabaseImportToFreeSR(0, 1)); - el.btnGachabaseImportE6S5?.addEventListener('click', () => handleGachabaseImportToFreeSR(6, 5)); } // Setup Event Listeners @@ -6057,7 +5867,7 @@ function setupEvents() { updateCacheUsageDisplay(); // Modal Backdrop Click to close - [el.modalAvatarDetail, el.modalLineupPicker, el.modalSettings, el.modalGachabase].forEach(modal => { + [el.modalAvatarDetail, el.modalLineupPicker, el.modalSettings].forEach(modal => { if (modal) { modal.addEventListener('click', (e) => { if (e.target === modal) { @@ -6084,13 +5894,11 @@ function startPingLoop() { // Initialize Application async function init() { - await loadCachedGachabaseRegistry({ - AVATAR_METADATA, - LIGHTCONE_METADATA, - RELIC_SET_METADATA, - SIGNATURE_LIGHTCONES, + await loadCachedGachabaseData({ idbGet, - idbSet + idbSet, + AVATAR_METADATA, + LIGHTCONE_METADATA }); initAvatarsList(); await loadCachedStorageData(); @@ -6099,11 +5907,12 @@ async function init() { setupTabs(); setupModalSubtabs(); setupEvents(); - setupGachabaseEvents(); + setupGachabaseSyncEvents(); applyTranslations(); renderLineup(state.statusData?.lineups || DEFAULT_LINEUP); renderAvatarsGrid(); loadEquipment(); + updateGachabaseSyncUI(); refreshStatus(); startPingLoop(); updateCacheUsageDisplay(); diff --git a/js/gachabase.js b/js/gachabase.js index a195296..be1df44 100644 --- a/js/gachabase.js +++ b/js/gachabase.js @@ -1,10 +1,43 @@ -// Gachabase Data Fetcher, Parser & FreeSR Importer Module -// Supports fetching datamined/beta characters, lightcones, and relics from hsr.gachabase.net -// Integrates with GaC-srtool runtime metadata, IndexedDB cache, and FreeSR / persistent saves. +// Gachabase Data Fetcher, Catalog Sync & Image Engine for GaC-srtool +// Connects with hsr.gachabase.net to dynamically update characters, lightcones, +// and high-res assets whenever the game updates or new patches/betas drop. export const GACHABASE_BASE_URL = 'https://hsr.gachabase.net'; export const GACHABASE_CDN_URL = 'https://cdn.gachabase.net'; +// Pre-mapped known beta / newest assets from Gachabase CDN +export const KNOWN_GACHABASE_ASSETS = { + avatars: { + 1501: 'https://cdn.gachabase.net/hsr/assets/09ee6e2618ac2a0dab2aec8d73b0bb01.png', // Sparxie + 1502: 'https://cdn.gachabase.net/hsr/assets/8ca2530c42791593b72d54a850fce97e.png', // Yao Guang + 1505: 'https://cdn.gachabase.net/hsr/assets/723a90e789fd354fb96b450a90c73e30.png', // Evanescia + 1506: 'https://cdn.gachabase.net/hsr/assets/0cbfb275bfef439b16ba6ff9f848eb3a.png', // Silver Wolf Collab + 1511: 'https://cdn.gachabase.net/hsr/assets/2be49dbbe7cfe8f77187dcfea51d9863.png', // Aeon ★ Aha + 1513: 'https://cdn.gachabase.net/hsr/assets/2fdf5d4fe6eb998064438ffd373ce95f.png', // Aventurine • Waveflair + }, + avatarSplash: { + 1511: 'https://cdn.gachabase.net/hsr/assets/f8b5630e85022ab8b2761e4e3f614b42.png', // Aha Splash Art (2048x1914) + 1513: 'https://cdn.gachabase.net/hsr/assets/65b90212f8623ad92243d739ea1fc9ec.png', // Waveflair Splash + 1505: 'https://cdn.gachabase.net/hsr/assets/71cbe3943310cb233e8dbe6ef69caeb1.png', // Evanescia Splash + 1502: 'https://cdn.gachabase.net/hsr/assets/8ca2530c42791593b72d54a850fce97e.png', // Yao Guang + 1501: 'https://cdn.gachabase.net/hsr/assets/09ee6e2618ac2a0dab2aec8d73b0bb01.png', // Sparxie + }, + lightcones: { + 23065: 'https://cdn.gachabase.net/hsr/assets/500cfc18698932759756f6b3ba4db417.png', // Upon the First Echo of "Aha" Full Art + }, + lightconeThumbs: { + 23065: 'https://cdn.gachabase.net/hsr/assets/c794417a81e656bfdf1212202230f80f.png', // Aha LC Thumbnail + } +}; + +// Global in-memory image cache populated dynamically from Gachabase catalogs +export const GACHABASE_IMAGE_CACHE = { + avatars: { ...KNOWN_GACHABASE_ASSETS.avatars }, + avatarSplash: { ...KNOWN_GACHABASE_ASSETS.avatarSplash }, + lightcones: { ...KNOWN_GACHABASE_ASSETS.lightcones }, + lightconeThumbs: { ...KNOWN_GACHABASE_ASSETS.lightconeThumbs } +}; + // Path ID mappings according to HSR engine / Gachabase export const GACHABASE_PATHS = { 1: 'Destruction', @@ -69,76 +102,7 @@ export function cleanRichText(str) { } /** - * Parses user input into normalized search parameters - * Handles full URLs or raw IDs - */ -export function parseGachabaseInput(input, typeHint = 'character', branch = 'beta', lang = 'en') { - const trimmed = (input || '').trim(); - if (!trimmed) return null; - - // Check if input is a full URL - if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { - try { - const urlObj = new URL(trimmed); - const pathname = urlObj.pathname.toLowerCase(); - let detectedType = typeHint; - if (pathname.includes('/characters/') || pathname.includes('/character/')) detectedType = 'character'; - else if (pathname.includes('/lightcones/') || pathname.includes('/lightcone/')) detectedType = 'lightcone'; - else if (pathname.includes('/relics/') || pathname.includes('/relic/')) detectedType = 'relic'; - - // Pattern: /type/:id/:slug?/:branch? - const parts = pathname.split('/').filter(Boolean); - // Expected: ['characters', '1511', 'aeon-aha', 'beta'] - let id = null; - let detectedBranch = urlObj.searchParams.get('branch') || branch; - let detectedLang = urlObj.searchParams.get('lang') || lang; - - if (parts.length >= 2) { - id = parseInt(parts[1], 10); - } - if (parts.length >= 4) { - detectedBranch = parts[3]; - } - - return { - url: trimmed, - id: id || null, - type: detectedType, - branch: detectedBranch || 'beta', - lang: detectedLang || 'en' - }; - } catch (e) { - // Fall through to ID parsing - } - } - - // Treat as numeric ID - const numId = parseInt(trimmed, 10); - if (!isNaN(numId)) { - let resolvedType = typeHint; - // Auto-guess type if default wasn't explicitly changed - if (numId >= 20000 && numId < 30000) resolvedType = 'lightcone'; - else if (numId < 1000) resolvedType = 'relic'; - else if (numId >= 1000 && numId < 9000) resolvedType = 'character'; - - const typeSlug = resolvedType === 'lightcone' ? 'lightcones' : (resolvedType === 'relic' ? 'relics' : 'characters'); - const url = `${GACHABASE_BASE_URL}/${typeSlug}/${numId}/slug/${branch}?lang=${lang}`; - - return { - url, - id: numId, - type: resolvedType, - branch: branch || 'beta', - lang: lang || 'en' - }; - } - - return null; -} - -/** - * Fetches Gachabase HTML content - * Uses GaC-hkrpg-go API proxy if available to avoid CORS, falls back to direct fetch + * 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'); @@ -160,7 +124,7 @@ export async function fetchGachabaseHtml(targetUrl, apiUrl = 'http://localhost:2 } } - // 2. Direct fetch fallback (works if user has CORS unblocker or runs in local browser environment) + // 2. Direct fetch fallback const directResp = await fetch(targetUrl, { method: 'GET', headers: { @@ -175,451 +139,320 @@ export async function fetchGachabaseHtml(targetUrl, apiUrl = 'http://localhost:2 } /** - * Extracts and parses serialized SvelteKit DTO and refs from Gachabase HTML + * Parses SvelteKit streamed data payload from Gachabase catalog listing pages */ -export function parseGachabaseHtml(html, fallbackLang = 'en') { - if (!html || typeof html !== 'string') { - throw new Error('Invalid HTML content received from Gachabase'); - } - - // Look for kit.start(...) serialized state in