feat: persist uploaded JSON and freesr data to browser localStorage cache

This commit is contained in:
CAtlas0784 committed 2026-10-17 02:45:50 +07:00
1 parent 06ef6edceb
commit e1cbda63cf
1 file changed
+253 -25
+253 -25
View File
@@ -479,6 +479,68 @@ function updateFilterDropdownTranslations() {
} }
} }
// Storage Cache Handlers for Offline & Upload Persistence
function saveFreesrCache(data) {
if (!data) return;
try {
localStorage.setItem('gac_srtool_freesr_cache', JSON.stringify(data));
updateCacheUsageDisplay();
} catch (e) {
console.warn('Failed to save freesr cache to localStorage:', e);
}
}
function savePersistentCache(data) {
if (!data) return;
try {
localStorage.setItem('gac_srtool_persistent_cache', JSON.stringify(data));
updateCacheUsageDisplay();
} catch (e) {
console.warn('Failed to save persistent cache to localStorage:', e);
}
}
function loadCachedStorageData() {
try {
const cachedFreesr = localStorage.getItem('gac_srtool_freesr_cache');
if (cachedFreesr) {
const parsed = JSON.parse(cachedFreesr);
state.fullFreesrData = parsed;
if (parsed.avatars) {
for (const aid in parsed.avatars) {
const av = parsed.avatars[aid];
const found = state.avatarsList.find(a => a.avatar_id === parseInt(aid, 10));
if (found) {
found.level = av.level || 80;
found.promotion = av.promotion || 6;
found.rank = av.data?.rank ?? 6;
}
}
}
if (parsed.lineups && (!state.statusData || !state.statusData.lineups)) {
state.statusData = state.statusData || {};
state.statusData.lineups = parsed.lineups;
}
}
} catch (e) {
console.warn('Failed to load cached freesr data:', e);
}
try {
const cachedPersistent = localStorage.getItem('gac_srtool_persistent_cache');
if (cachedPersistent) {
const parsed = JSON.parse(cachedPersistent);
state.persistentData = parsed;
if (parsed.lineups && (!state.statusData || !state.statusData.lineups)) {
state.statusData = state.statusData || {};
state.statusData.lineups = parsed.lineups;
}
}
} catch (e) {
console.warn('Failed to load cached persistent data:', e);
}
}
// Apply multi-language translations across UI // Apply multi-language translations across UI
function applyTranslations() { function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(elem => { document.querySelectorAll('[data-i18n]').forEach(elem => {
@@ -676,7 +738,17 @@ async function refreshStatus() {
} catch (err) { } catch (err) {
setConnectionStatus(false, t('offline')); setConnectionStatus(false, t('offline'));
el.lblApiUrl.textContent = state.apiUrl; el.lblApiUrl.textContent = state.apiUrl;
if (state.fullFreesrData) {
const avCount = Object.keys(state.fullFreesrData.avatars || {}).length;
const lcCount = (state.fullFreesrData.lightcones || []).length;
const relicCount = (state.fullFreesrData.relics || []).length;
el.statAvatars.textContent = avCount || state.avatarsList.length;
el.statLightcones.textContent = lcCount;
el.statRelics.textContent = relicCount;
} else {
el.statAvatars.textContent = state.avatarsList.length; el.statAvatars.textContent = state.avatarsList.length;
}
if (el.badgePersistenceMode) { if (el.badgePersistenceMode) {
if (state.serverMode === 'gac') { if (state.serverMode === 'gac') {
@@ -968,6 +1040,20 @@ async function assignAvatarToLineup(slot, avatarId) {
// Load and Render Avatars // Load and Render Avatars
async function loadAvatars() { async function loadAvatars() {
initAvatarsList(); initAvatarsList();
// Sync from cached / in-memory freesr data first
if (state.fullFreesrData?.avatars) {
for (const aid in state.fullFreesrData.avatars) {
const serverAv = state.fullFreesrData.avatars[aid];
const found = state.avatarsList.find(a => a.avatar_id === parseInt(aid, 10));
if (found) {
found.level = serverAv.level || 80;
found.promotion = serverAv.promotion || 6;
found.rank = serverAv.data?.rank ?? 6;
}
}
}
if (state.isConnected) { if (state.isConnected) {
try { try {
const res = await apiCall('/api/freesr/avatars'); const res = await apiCall('/api/freesr/avatars');
@@ -3257,9 +3343,11 @@ async function maxAllAvatars() {
} }
} }
// Push local freesr changes to server // Push local freesr changes to server and save to local browser cache
async function saveFreesrChangesToServer() { async function saveFreesrChangesToServer() {
if (!state.fullFreesrData) return; if (!state.fullFreesrData) return;
saveFreesrCache(state.fullFreesrData);
if (!state.isConnected) return;
if (state.serverMode === 'firefly' || state.serverMode === 'robin') { if (state.serverMode === 'firefly' || state.serverMode === 'robin') {
try { try {
await apiCall('/srtools', 'POST', { data: state.fullFreesrData }); await apiCall('/srtools', 'POST', { data: state.fullFreesrData });
@@ -3295,9 +3383,20 @@ async function loadEquipment() {
const res = await apiCall('/api/freesr'); const res = await apiCall('/api/freesr');
if (res && res.data) { if (res && res.data) {
state.fullFreesrData = res.data; state.fullFreesrData = res.data;
saveFreesrCache(res.data);
lcs = res.data.lightcones || []; lcs = res.data.lightcones || [];
relics = res.data.relics || []; relics = res.data.relics || [];
} }
} else {
const cached = localStorage.getItem('gac_srtool_freesr_cache');
if (cached) {
try {
const parsed = JSON.parse(cached);
state.fullFreesrData = parsed;
lcs = parsed.lightcones || [];
relics = parsed.relics || [];
} catch (e) {}
}
} }
if (el.badgeTotalLc) el.badgeTotalLc.textContent = lcs.length; if (el.badgeTotalLc) el.badgeTotalLc.textContent = lcs.length;
@@ -3856,20 +3955,35 @@ async function handleAddRelicSubmit(e) {
async function loadPersistentSettings() { async function loadPersistentSettings() {
if (state.serverMode !== 'gac') return; if (state.serverMode !== 'gac') return;
try { try {
let data = state.persistentData;
if (state.isConnected) {
const res = await apiCall('/api/persistent'); const res = await apiCall('/api/persistent');
if (!res || !res.data) return; if (res && res.data) {
state.persistentData = res.data; data = res.data;
state.persistentData = data;
if (res.data.main_character) { savePersistentCache(data);
el.persistentMcSelect.value = res.data.main_character;
} }
if (res.data.march_type) { } else if (!data) {
el.persistentMarchSelect.value = res.data.march_type; const cached = localStorage.getItem('gac_srtool_persistent_cache');
if (cached) {
try {
data = JSON.parse(cached);
state.persistentData = data;
} catch (e) {}
}
}
if (!data) return;
if (data.main_character && el.persistentMcSelect) {
el.persistentMcSelect.value = data.main_character;
}
if (data.march_type && el.persistentMarchSelect) {
el.persistentMarchSelect.value = data.march_type;
} }
el.toggleSwGlobal.checked = res.data.enable_sw_global !== false; if (el.toggleSwGlobal) el.toggleSwGlobal.checked = data.enable_sw_global !== false;
el.toggleCastoriceGlobal.checked = res.data.enable_castorice_global !== false; if (el.toggleCastoriceGlobal) el.toggleCastoriceGlobal.checked = data.enable_castorice_global !== false;
if (el.toggleAhaGlobal) el.toggleAhaGlobal.checked = res.data.enable_aha_global !== false; if (el.toggleAhaGlobal) el.toggleAhaGlobal.checked = data.enable_aha_global !== false;
} catch (err) { } catch (err) {
console.warn('Persistent settings loading error:', err); console.warn('Persistent settings loading error:', err);
} }
@@ -3952,19 +4066,43 @@ async function loadJsonData(mode) {
try { try {
if (state.fullFreesrData) { if (state.fullFreesrData) {
el.jsonEditor.value = JSON.stringify(state.fullFreesrData, null, 2); el.jsonEditor.value = JSON.stringify(state.fullFreesrData, null, 2);
el.jsonStatusInfo.textContent = 'freesr-data.json (Loaded from memory)'; el.jsonStatusInfo.textContent = 'freesr-data.json (โหลดจากหน่วยความจำ/แคช)';
} else { } else if (state.isConnected) {
const res = await apiCall('/api/freesr'); const res = await apiCall('/api/freesr');
state.fullFreesrData = res.data; state.fullFreesrData = res.data;
saveFreesrCache(res.data);
el.jsonEditor.value = JSON.stringify(res.data, null, 2); el.jsonEditor.value = JSON.stringify(res.data, null, 2);
el.jsonStatusInfo.textContent = 'freesr-data.json (Loaded from server)'; el.jsonStatusInfo.textContent = 'freesr-data.json (โหลดจากเซิร์ฟเวอร์)';
} else {
const cached = localStorage.getItem('gac_srtool_freesr_cache');
if (cached) {
const parsed = JSON.parse(cached);
state.fullFreesrData = parsed;
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
el.jsonStatusInfo.textContent = 'freesr-data.json (โหลดจากแคชของเบราว์เซอร์)';
} else {
el.jsonStatusInfo.textContent = 'Offline: ไม่มีแคช สามารถอัปโหลดหรือพิมพ์ JSON เองได้';
if (!el.jsonEditor.value) {
el.jsonEditor.value = JSON.stringify({ avatars: {}, lightcones: [], relics: [], battle_config: {} }, null, 2);
}
}
} }
} catch (err) { } catch (err) {
const cached = localStorage.getItem('gac_srtool_freesr_cache');
if (cached) {
try {
const parsed = JSON.parse(cached);
state.fullFreesrData = parsed;
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
el.jsonStatusInfo.textContent = 'freesr-data.json (โหลดจากแคชของเบราว์เซอร์)';
} catch (e) {}
} else {
el.jsonStatusInfo.textContent = `Offline: สามารถพิมพ์หรือวาง JSON เองได้`; el.jsonStatusInfo.textContent = `Offline: สามารถพิมพ์หรือวาง JSON เองได้`;
if (!el.jsonEditor.value) { if (!el.jsonEditor.value) {
el.jsonEditor.value = JSON.stringify({ avatars: {}, lightcones: [], relics: [], battle_config: {} }, null, 2); el.jsonEditor.value = JSON.stringify({ avatars: {}, lightcones: [], relics: [], battle_config: {} }, null, 2);
} }
} }
}
} else { } else {
el.btnSelectFreesr.className = 'btn btn-sm btn-secondary'; el.btnSelectFreesr.className = 'btn btn-sm btn-secondary';
el.btnSelectPersistent.className = 'btn btn-sm btn-primary active'; el.btnSelectPersistent.className = 'btn btn-sm btn-primary active';
@@ -3972,20 +4110,44 @@ async function loadJsonData(mode) {
try { try {
if (state.persistentData) { if (state.persistentData) {
el.jsonEditor.value = JSON.stringify(state.persistentData, null, 2); el.jsonEditor.value = JSON.stringify(state.persistentData, null, 2);
el.jsonStatusInfo.textContent = 'persistent (Loaded from memory)'; el.jsonStatusInfo.textContent = 'persistent (โหลดจากหน่วยความจำ/แคช)';
} else { } else if (state.isConnected) {
const res = await apiCall('/api/persistent'); const res = await apiCall('/api/persistent');
state.persistentData = res.data; state.persistentData = res.data;
savePersistentCache(res.data);
el.jsonEditor.value = JSON.stringify(res.data, null, 2); el.jsonEditor.value = JSON.stringify(res.data, null, 2);
el.jsonStatusInfo.textContent = 'persistent (Loaded from server)'; el.jsonStatusInfo.textContent = 'persistent (โหลดจากเซิร์ฟเวอร์)';
} else {
const cached = localStorage.getItem('gac_srtool_persistent_cache');
if (cached) {
const parsed = JSON.parse(cached);
state.persistentData = parsed;
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
el.jsonStatusInfo.textContent = 'persistent (โหลดจากแคชของเบราว์เซอร์)';
} else {
el.jsonStatusInfo.textContent = 'Offline: ไม่มีแคช สามารถอัปโหลดหรือพิมพ์ JSON เองได้';
if (!el.jsonEditor.value) {
el.jsonEditor.value = JSON.stringify({ lineups: DEFAULT_LINEUP, main_character: "FemaleHarmony", march_type: "MarchHunt" }, null, 2);
}
}
} }
} catch (err) { } catch (err) {
const cached = localStorage.getItem('gac_srtool_persistent_cache');
if (cached) {
try {
const parsed = JSON.parse(cached);
state.persistentData = parsed;
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
el.jsonStatusInfo.textContent = 'persistent (โหลดจากแคชของเบราว์เซอร์)';
} catch (e) {}
} else {
el.jsonStatusInfo.textContent = `Offline: สามารถพิมพ์หรือวาง JSON เองได้`; el.jsonStatusInfo.textContent = `Offline: สามารถพิมพ์หรือวาง JSON เองได้`;
if (!el.jsonEditor.value) { if (!el.jsonEditor.value) {
el.jsonEditor.value = JSON.stringify({ lineups: DEFAULT_LINEUP, main_character: "FemaleHarmony", march_type: "MarchHunt" }, null, 2); el.jsonEditor.value = JSON.stringify({ lineups: DEFAULT_LINEUP, main_character: "FemaleHarmony", march_type: "MarchHunt" }, null, 2);
} }
} }
} }
}
} }
// Save JSON Tab Data // Save JSON Tab Data
@@ -3994,6 +4156,8 @@ async function saveJsonData() {
const parsed = JSON.parse(el.jsonEditor.value); const parsed = JSON.parse(el.jsonEditor.value);
if (state.currentJsonMode === 'freesr') { if (state.currentJsonMode === 'freesr') {
state.fullFreesrData = parsed; state.fullFreesrData = parsed;
saveFreesrCache(parsed);
loadEquipment();
if (state.isConnected) { if (state.isConnected) {
await saveFreesrChangesToServer(); await saveFreesrChangesToServer();
} }
@@ -4010,9 +4174,10 @@ async function saveJsonData() {
} }
renderAvatarsGrid(); renderAvatarsGrid();
} }
showToast('บันทึก freesr-data.json เรียบร้อย', 'success'); showToast('บันทึก freesr-data.json ลงในแคชและเซิร์ฟเวอร์เรียบร้อย', 'success');
} else { } else {
state.persistentData = parsed; state.persistentData = parsed;
savePersistentCache(parsed);
if (state.isConnected && state.serverMode === 'gac') { if (state.isConnected && state.serverMode === 'gac') {
await apiCall('/api/persistent', 'POST', parsed); await apiCall('/api/persistent', 'POST', parsed);
} }
@@ -4021,8 +4186,9 @@ async function saveJsonData() {
state.statusData.lineups = parsed.lineups; state.statusData.lineups = parsed.lineups;
renderLineup(parsed.lineups); renderLineup(parsed.lineups);
} }
showToast('บันทึกไฟล์ persistent เรียบร้อย', 'success'); showToast('บันทึกไฟล์ persistent ลงในแคชและเซิร์ฟเวอร์เรียบร้อย', 'success');
} }
updateCacheUsageDisplay();
refreshStatus(); refreshStatus();
} catch (err) { } catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error'); showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
@@ -4511,9 +4677,15 @@ function setupEvents() {
const parsed = JSON.parse(event.target.result); const parsed = JSON.parse(event.target.result);
el.jsonEditor.value = JSON.stringify(parsed, null, 2); el.jsonEditor.value = JSON.stringify(parsed, null, 2);
// If freesr-data.json was uploaded let recognized = false;
if (parsed.avatars) {
// If freesr-data.json was uploaded (has avatars, lightcones, or relics)
if (parsed.avatars || parsed.lightcones || parsed.relics) {
recognized = true;
state.fullFreesrData = parsed; state.fullFreesrData = parsed;
saveFreesrCache(parsed);
if (parsed.avatars) {
for (const aid in parsed.avatars) { for (const aid in parsed.avatars) {
const av = parsed.avatars[aid]; const av = parsed.avatars[aid];
const found = state.avatarsList.find(a => a.avatar_id === parseInt(aid, 10)); const found = state.avatarsList.find(a => a.avatar_id === parseInt(aid, 10));
@@ -4526,19 +4698,64 @@ function setupEvents() {
renderAvatarsGrid(); renderAvatarsGrid();
} }
loadEquipment();
if (parsed.battle_config?.custom_battle_lineup || parsed.lineups) {
const lineups = parsed.lineups || parsed.battle_config?.custom_battle_lineup;
state.statusData = state.statusData || {};
state.statusData.lineups = lineups;
renderLineup(lineups);
}
}
// If persistent or has lineups // If persistent or has lineups
if (parsed.main_character || parsed.march_type || (parsed.lineups && !parsed.avatars)) {
recognized = true;
state.persistentData = parsed;
savePersistentCache(parsed);
if (parsed.lineups) { if (parsed.lineups) {
state.statusData = state.statusData || {}; state.statusData = state.statusData || {};
state.statusData.lineups = parsed.lineups; state.statusData.lineups = parsed.lineups;
renderLineup(parsed.lineups); renderLineup(parsed.lineups);
} }
if (parsed.main_character && el.persistentMcSelect) {
el.persistentMcSelect.value = parsed.main_character;
}
if (parsed.march_type && el.persistentMarchSelect) {
el.persistentMarchSelect.value = parsed.march_type;
}
if (el.toggleSwGlobal && parsed.enable_sw_global !== undefined) {
el.toggleSwGlobal.checked = parsed.enable_sw_global !== false;
}
if (el.toggleCastoriceGlobal && parsed.enable_castorice_global !== undefined) {
el.toggleCastoriceGlobal.checked = parsed.enable_castorice_global !== false;
}
if (el.toggleAhaGlobal && parsed.enable_aha_global !== undefined) {
el.toggleAhaGlobal.checked = parsed.enable_aha_global !== false;
}
}
showToast(`โหลดไฟล์ ${file.name} สำเร็จและซิงค์ข้อมูลแล้ว`, 'success'); if (!recognized) {
if (state.currentJsonMode === 'freesr') {
state.fullFreesrData = parsed;
saveFreesrCache(parsed);
loadEquipment();
} else {
state.persistentData = parsed;
savePersistentCache(parsed);
}
}
updateCacheUsageDisplay();
refreshStatus();
showToast(`บันทึกไฟล์ ${file.name} ลงในแคชของเบราว์เซอร์และซิงค์ข้อมูลสำเร็จ!`, 'success');
} catch (err) { } catch (err) {
showToast(t('jsonInvalid'), 'error'); showToast(t('jsonInvalid') || 'ไฟล์ JSON ไม่ถูกต้อง', 'error');
} }
}; };
reader.readAsText(file); reader.readAsText(file);
e.target.value = '';
}); });
// ------------------------------------------------------------- // -------------------------------------------------------------
@@ -4686,8 +4903,13 @@ function setupEvents() {
if (keepUrl) localStorage.setItem('gac_srtool_api_url', keepUrl); if (keepUrl) localStorage.setItem('gac_srtool_api_url', keepUrl);
if (keepMode) localStorage.setItem('gac_srtool_mode', keepMode); if (keepMode) localStorage.setItem('gac_srtool_mode', keepMode);
if (keepLang) localStorage.setItem('gac_srtool_lang', keepLang); if (keepLang) localStorage.setItem('gac_srtool_lang', keepLang);
state.fullFreesrData = null;
state.persistentData = null;
loadAvatars();
loadEquipment();
updateCacheUsageDisplay(); updateCacheUsageDisplay();
showToast(t('toastCacheCleared'), 'success'); showToast(t('toastCacheCleared') || 'ล้างแคชในเบราว์เซอร์เรียบร้อยแล้ว', 'success');
refreshStatus();
}); });
} }
@@ -4695,6 +4917,8 @@ function setupEvents() {
if (el.btnResetSettings) { if (el.btnResetSettings) {
el.btnResetSettings.addEventListener('click', () => { el.btnResetSettings.addEventListener('click', () => {
localStorage.clear(); localStorage.clear();
state.fullFreesrData = null;
state.persistentData = null;
state.apiUrl = 'http://localhost:21000'; state.apiUrl = 'http://localhost:21000';
state.serverMode = 'gac'; state.serverMode = 'gac';
state.creativeMode = false; state.creativeMode = false;
@@ -4710,8 +4934,10 @@ function setupEvents() {
if (el.settingShowcaseAura) el.settingShowcaseAura.checked = true; if (el.settingShowcaseAura) el.settingShowcaseAura.checked = true;
if (el.toggleCreativeMode) el.toggleCreativeMode.checked = false; if (el.toggleCreativeMode) el.toggleCreativeMode.checked = false;
if (el.badgeCreativeMode) el.badgeCreativeMode.style.display = 'none'; if (el.badgeCreativeMode) el.badgeCreativeMode.style.display = 'none';
loadAvatars();
loadEquipment();
updateCacheUsageDisplay(); updateCacheUsageDisplay();
showToast(t('settingsReset'), 'info'); showToast(t('settingsReset') || 'รีเซ็ตเป็นค่าเริ่มต้นเรียบร้อย', 'info');
refreshStatus(); refreshStatus();
}); });
} }
@@ -4747,6 +4973,7 @@ function startPingLoop() {
// Initialize Application // Initialize Application
function init() { function init() {
initAvatarsList(); initAvatarsList();
loadCachedStorageData();
setupServerModes(); setupServerModes();
setupLanguageSelector(); setupLanguageSelector();
setupTabs(); setupTabs();
@@ -4758,6 +4985,7 @@ function init() {
loadEquipment(); loadEquipment();
refreshStatus(); refreshStatus();
startPingLoop(); startPingLoop();
updateCacheUsageDisplay();
} }
// Run on DOM load // Run on DOM load