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
+276 -48
+276 -48
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
function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(elem => {
@@ -676,7 +738,17 @@ async function refreshStatus() {
} catch (err) {
setConnectionStatus(false, t('offline'));
el.lblApiUrl.textContent = state.apiUrl;
el.statAvatars.textContent = state.avatarsList.length;
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;
}
if (el.badgePersistenceMode) {
if (state.serverMode === 'gac') {
@@ -968,6 +1040,20 @@ async function assignAvatarToLineup(slot, avatarId) {
// Load and Render Avatars
async function loadAvatars() {
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) {
try {
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() {
if (!state.fullFreesrData) return;
saveFreesrCache(state.fullFreesrData);
if (!state.isConnected) return;
if (state.serverMode === 'firefly' || state.serverMode === 'robin') {
try {
await apiCall('/srtools', 'POST', { data: state.fullFreesrData });
@@ -3295,9 +3383,20 @@ async function loadEquipment() {
const res = await apiCall('/api/freesr');
if (res && res.data) {
state.fullFreesrData = res.data;
saveFreesrCache(res.data);
lcs = res.data.lightcones || [];
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;
@@ -3856,20 +3955,35 @@ async function handleAddRelicSubmit(e) {
async function loadPersistentSettings() {
if (state.serverMode !== 'gac') return;
try {
const res = await apiCall('/api/persistent');
if (!res || !res.data) return;
state.persistentData = res.data;
if (res.data.main_character) {
el.persistentMcSelect.value = res.data.main_character;
let data = state.persistentData;
if (state.isConnected) {
const res = await apiCall('/api/persistent');
if (res && res.data) {
data = res.data;
state.persistentData = data;
savePersistentCache(data);
}
} else if (!data) {
const cached = localStorage.getItem('gac_srtool_persistent_cache');
if (cached) {
try {
data = JSON.parse(cached);
state.persistentData = data;
} catch (e) {}
}
}
if (res.data.march_type) {
el.persistentMarchSelect.value = res.data.march_type;
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;
el.toggleCastoriceGlobal.checked = res.data.enable_castorice_global !== false;
if (el.toggleAhaGlobal) el.toggleAhaGlobal.checked = res.data.enable_aha_global !== false;
if (el.toggleSwGlobal) el.toggleSwGlobal.checked = data.enable_sw_global !== false;
if (el.toggleCastoriceGlobal) el.toggleCastoriceGlobal.checked = data.enable_castorice_global !== false;
if (el.toggleAhaGlobal) el.toggleAhaGlobal.checked = data.enable_aha_global !== false;
} catch (err) {
console.warn('Persistent settings loading error:', err);
}
@@ -3952,17 +4066,41 @@ async function loadJsonData(mode) {
try {
if (state.fullFreesrData) {
el.jsonEditor.value = JSON.stringify(state.fullFreesrData, null, 2);
el.jsonStatusInfo.textContent = 'freesr-data.json (Loaded from memory)';
} else {
el.jsonStatusInfo.textContent = 'freesr-data.json (โหลดจากหน่วยความจำ/แคช)';
} else if (state.isConnected) {
const res = await apiCall('/api/freesr');
state.fullFreesrData = res.data;
saveFreesrCache(res.data);
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) {
el.jsonStatusInfo.textContent = `Offline: สามารถพิมพ์หรือวาง JSON เองได้`;
if (!el.jsonEditor.value) {
el.jsonEditor.value = JSON.stringify({ avatars: {}, lightcones: [], relics: [], battle_config: {} }, null, 2);
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 เองได้`;
if (!el.jsonEditor.value) {
el.jsonEditor.value = JSON.stringify({ avatars: {}, lightcones: [], relics: [], battle_config: {} }, null, 2);
}
}
}
} else {
@@ -3972,17 +4110,41 @@ async function loadJsonData(mode) {
try {
if (state.persistentData) {
el.jsonEditor.value = JSON.stringify(state.persistentData, null, 2);
el.jsonStatusInfo.textContent = 'persistent (Loaded from memory)';
} else {
el.jsonStatusInfo.textContent = 'persistent (โหลดจากหน่วยความจำ/แคช)';
} else if (state.isConnected) {
const res = await apiCall('/api/persistent');
state.persistentData = res.data;
savePersistentCache(res.data);
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) {
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);
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 เองได้`;
if (!el.jsonEditor.value) {
el.jsonEditor.value = JSON.stringify({ lineups: DEFAULT_LINEUP, main_character: "FemaleHarmony", march_type: "MarchHunt" }, null, 2);
}
}
}
}
@@ -3994,6 +4156,8 @@ async function saveJsonData() {
const parsed = JSON.parse(el.jsonEditor.value);
if (state.currentJsonMode === 'freesr') {
state.fullFreesrData = parsed;
saveFreesrCache(parsed);
loadEquipment();
if (state.isConnected) {
await saveFreesrChangesToServer();
}
@@ -4010,9 +4174,10 @@ async function saveJsonData() {
}
renderAvatarsGrid();
}
showToast('บันทึก freesr-data.json เรียบร้อย', 'success');
showToast('บันทึก freesr-data.json ลงในแคชและเซิร์ฟเวอร์เรียบร้อย', 'success');
} else {
state.persistentData = parsed;
savePersistentCache(parsed);
if (state.isConnected && state.serverMode === 'gac') {
await apiCall('/api/persistent', 'POST', parsed);
}
@@ -4021,8 +4186,9 @@ async function saveJsonData() {
state.statusData.lineups = parsed.lineups;
renderLineup(parsed.lineups);
}
showToast('บันทึกไฟล์ persistent เรียบร้อย', 'success');
showToast('บันทึกไฟล์ persistent ลงในแคชและเซิร์ฟเวอร์เรียบร้อย', 'success');
}
updateCacheUsageDisplay();
refreshStatus();
} catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
@@ -4510,35 +4676,86 @@ function setupEvents() {
try {
const parsed = JSON.parse(event.target.result);
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
// If freesr-data.json was uploaded
if (parsed.avatars) {
let recognized = false;
// If freesr-data.json was uploaded (has avatars, lightcones, or relics)
if (parsed.avatars || parsed.lightcones || parsed.relics) {
recognized = true;
state.fullFreesrData = parsed;
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;
saveFreesrCache(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;
}
}
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);
}
renderAvatarsGrid();
}
// If persistent or has lineups
if (parsed.lineups) {
state.statusData = state.statusData || {};
state.statusData.lineups = parsed.lineups;
renderLineup(parsed.lineups);
if (parsed.main_character || parsed.march_type || (parsed.lineups && !parsed.avatars)) {
recognized = true;
state.persistentData = parsed;
savePersistentCache(parsed);
if (parsed.lineups) {
state.statusData = state.statusData || {};
state.statusData.lineups = 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) {
showToast(t('jsonInvalid'), 'error');
showToast(t('jsonInvalid') || 'ไฟล์ JSON ไม่ถูกต้อง', 'error');
}
};
reader.readAsText(file);
e.target.value = '';
});
// -------------------------------------------------------------
@@ -4686,8 +4903,13 @@ function setupEvents() {
if (keepUrl) localStorage.setItem('gac_srtool_api_url', keepUrl);
if (keepMode) localStorage.setItem('gac_srtool_mode', keepMode);
if (keepLang) localStorage.setItem('gac_srtool_lang', keepLang);
state.fullFreesrData = null;
state.persistentData = null;
loadAvatars();
loadEquipment();
updateCacheUsageDisplay();
showToast(t('toastCacheCleared'), 'success');
showToast(t('toastCacheCleared') || 'ล้างแคชในเบราว์เซอร์เรียบร้อยแล้ว', 'success');
refreshStatus();
});
}
@@ -4695,6 +4917,8 @@ function setupEvents() {
if (el.btnResetSettings) {
el.btnResetSettings.addEventListener('click', () => {
localStorage.clear();
state.fullFreesrData = null;
state.persistentData = null;
state.apiUrl = 'http://localhost:21000';
state.serverMode = 'gac';
state.creativeMode = false;
@@ -4710,8 +4934,10 @@ function setupEvents() {
if (el.settingShowcaseAura) el.settingShowcaseAura.checked = true;
if (el.toggleCreativeMode) el.toggleCreativeMode.checked = false;
if (el.badgeCreativeMode) el.badgeCreativeMode.style.display = 'none';
loadAvatars();
loadEquipment();
updateCacheUsageDisplay();
showToast(t('settingsReset'), 'info');
showToast(t('settingsReset') || 'รีเซ็ตเป็นค่าเริ่มต้นเรียบร้อย', 'info');
refreshStatus();
});
}
@@ -4747,6 +4973,7 @@ function startPingLoop() {
// Initialize Application
function init() {
initAvatarsList();
loadCachedStorageData();
setupServerModes();
setupLanguageSelector();
setupTabs();
@@ -4758,6 +4985,7 @@ function init() {
loadEquipment();
refreshStatus();
startPingLoop();
updateCacheUsageDisplay();
}
// Run on DOM load