Sync ALL elements
目前為
// ==UserScript==
// @name Infinite Craft - Auto Dragger Pro - Element Sync Plugin
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Sync ALL elements
// @author Silverfox0338
// @license CC-BY-NC-ND-4.0
// @match https://neal.fun/infinite-craft/
// @icon https://www.google.com/s2/favicons?sz=64&domain=neal.fun
// @grant none
// ==/UserScript==
(function() {
'use strict';
// ============================================================================
// CONSTANTS & CONFIGURATION
// ============================================================================
const CONFIG = {
VERSION: 'v1.1',
// Performance
SCROLL_INTERVAL: 8, // ms between scrolls (was 10)
UI_UPDATE_THROTTLE: 50, // ms between UI updates
OPTIMIZATION_DELAY: 300, // ms to wait after hiding elements
RESTORE_DELAY: 200, // ms before restoring elements
// Retry Logic
MAX_INIT_ATTEMPTS: 40,
INIT_CHECK_INTERVAL: 500,
MAX_NO_CHANGE_COUNT: 40,
RETRY_RAPID_SCROLL_THRESHOLD: 10,
RETRY_SCROLL_RESET_THRESHOLD: 20,
RETRY_DEEP_SCAN_THRESHOLD: 30,
MAX_RETRY_ATTEMPTS: 5,
MIN_ACCEPTABLE_PERCENT: 95,
// UI
TOAST_DURATION: 3000,
TOAST_ANIMATION_DURATION: 300,
MODAL_ANIMATION_DURATION: 300,
LIST_PREVIEW_LIMIT: 15,
// Storage
STORAGE_KEY_ELEMENTS: 'infinecraft_synced_elements',
STORAGE_KEY_THEME: 'infinecraft_theme_preset',
STORAGE_KEY_VERSION: 'sync_last_version',
STORAGE_KEY_EXPORT_HISTORY: 'sync_export_history',
MAX_EXPORT_HISTORY: 10,
// Scroll Recovery
RECOVERY_SCROLL_POSITIONS: [0, 0.2, 0.4, 0.6, 0.8, 1.0],
RECOVERY_POSITION_DELAY: 100,
RAPID_SCROLL_COUNT: 5,
RAPID_SCROLL_DELAY: 50,
// History tracking
SPEED_HISTORY_WINDOW: 3000, // ms to keep speed history
// Animation
MODAL_FADE_DURATION: '0.3s',
TOAST_SLIDE_DURATION: '0.3s'
};
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
const state = {
allElements: [],
pluginInitialized: false,
isScanning: false,
loadingOverlay: null,
hiddenElements: { elements: [], styles: [] },
boundListeners: [],
rafId: null,
lastUIUpdate: 0,
exportHistory: []
};
// ============================================================================
// PERFORMANCE OPTIMIZATION - SAFE MODE
// ============================================================================
function hideGameElements() {
console.log('[SYNC] 🚀 Optimizing performance (safe mode)...');
try {
const instances = document.querySelector('.instances');
if (instances) {
state.hiddenElements.elements.push({
element: instances,
display: instances.style.display,
visibility: instances.style.visibility
});
instances.style.visibility = 'hidden';
console.log('[SYNC] ✓ Hidden .instances');
}
// Batch DOM operations
const craftedItems = document.querySelectorAll('.instance');
const fragment = document.createDocumentFragment();
craftedItems.forEach(item => {
state.hiddenElements.elements.push({
element: item,
display: item.style.display,
visibility: item.style.visibility
});
item.style.visibility = 'hidden';
});
if (craftedItems.length > 0) {
console.log(`[SYNC] ✓ Hidden ${craftedItems.length} items`);
}
const canvases = document.querySelectorAll('canvas');
canvases.forEach(canvas => {
if (!canvas.closest('#sidebar')) {
state.hiddenElements.elements.push({
element: canvas,
visibility: canvas.style.visibility
});
canvas.style.visibility = 'hidden';
}
});
const style = document.createElement('style');
style.id = 'turbo-sync-optimization';
style.textContent = `
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
}
#sidebar, #sidebar * {
animation: none !important;
transition: none !important;
}
`;
document.head.appendChild(style);
state.hiddenElements.styles.push(style);
console.log('[SYNC] 🚀 Safe performance mode ACTIVE!');
} catch (e) {
console.warn('[SYNC] Warning during optimization:', e);
}
}
function restoreGameElements() {
console.log('[SYNC] 🔄 Restoring game elements...');
try {
// Batch restore
state.hiddenElements.elements.forEach(({ element, display, visibility }) => {
if (element) {
if (display !== undefined) element.style.display = display || '';
if (visibility !== undefined) element.style.visibility = visibility || '';
}
});
state.hiddenElements.styles.forEach(style => {
if (style?.parentNode) {
style.parentNode.removeChild(style);
}
});
console.log('[SYNC] ✓ Restored all game elements');
state.hiddenElements = { elements: [], styles: [] };
} catch (e) {
console.warn('[SYNC] Warning during restoration:', e);
// Failsafe
const optimizationStyle = document.getElementById('turbo-sync-optimization');
optimizationStyle?.remove();
try {
document.querySelectorAll('.instances, .instance').forEach(el => {
el.style.visibility = '';
el.style.display = '';
});
} catch (e2) {
console.error('[SYNC] Failed to force restore:', e2);
}
}
}
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
function waitForMainScript() {
return new Promise((resolve) => {
let attempts = 0;
const checkInterval = setInterval(() => {
attempts++;
const mainPanel = document.getElementById('auto-dragger-panel');
if (mainPanel || attempts >= CONFIG.MAX_INIT_ATTEMPTS) {
clearInterval(checkInterval);
resolve(true);
}
}, CONFIG.INIT_CHECK_INTERVAL);
});
}
function isAutoDraggerRunning() {
const statusText = document.getElementById('status-text');
return statusText?.textContent.trim() === 'RUNNING';
}
function getCurrentTheme() {
try {
const badge = document.querySelector('.theme-badge');
if (badge) return badge.textContent.toLowerCase().trim();
} catch (e) {}
return localStorage.getItem(CONFIG.STORAGE_KEY_THEME) || 'hacker';
}
// Memoized theme colors for performance
let cachedTheme = null;
let cachedColors = null;
function getThemeColors() {
const currentTheme = getCurrentTheme();
if (cachedTheme === currentTheme && cachedColors) {
return cachedColors;
}
const themes = {
hacker: {
bg: '#000',
bgOverlay: 'rgba(0, 20, 0, 0.98)',
border: '#0f0',
text: '#0f0',
textSecondary: '#0a0',
accent: '#0f0',
accentDim: 'rgba(0, 255, 0, 0.2)',
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
info: '#3b82f6',
btnBg: '#001a00',
surface: 'rgba(0, 255, 0, 0.05)',
font: "'Courier New', monospace"
},
furry: {
bg: '#fff0f5',
bgOverlay: 'rgba(255, 240, 245, 0.98)',
border: '#ff69b4',
text: '#880044',
textSecondary: '#ff8da1',
accent: '#ff1493',
accentDim: 'rgba(255, 105, 180, 0.1)',
success: '#ff1493',
error: '#ff0000',
warning: '#ffca00',
info: '#00bfff',
btnBg: '#ffe4f0',
surface: 'rgba(255, 255, 255, 0.6)',
font: "'Comic Sans MS', 'Chalkboard SE', sans-serif"
},
synthwave: {
bg: '#140028',
bgOverlay: 'rgba(20, 0, 40, 0.98)',
border: '#00f3ff',
text: '#ff00ff',
textSecondary: '#bd00ff',
accent: '#00f3ff',
accentDim: 'rgba(0, 243, 255, 0.15)',
success: '#00ff9d',
error: '#ff2a2a',
warning: '#ffdd00',
info: '#00f3ff',
btnBg: '#2a0050',
surface: 'rgba(255, 0, 255, 0.05)',
font: "'Courier New', monospace"
}
};
cachedTheme = currentTheme;
cachedColors = themes[currentTheme] || themes.hacker;
return cachedColors;
}
function formatETA(seconds) {
if (seconds < 60) return `${Math.ceil(seconds)}s`;
if (seconds < 3600) {
const mins = Math.floor(seconds / 60);
const secs = Math.ceil(seconds % 60);
return `${mins}m ${secs}s`;
}
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
return `${hours}h ${mins}m`;
}
// ============================================================================
// UI HELPERS - WITH ACCESSIBILITY
// ============================================================================
function showModal(title, message, type = 'info', buttons = [{ text: 'OK', action: null }]) {
return new Promise((resolve) => {
const colors = getThemeColors();
const modal = document.createElement('div');
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'modal-title');
modal.setAttribute('aria-describedby', 'modal-message');
modal.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: ${colors.bg}ee; backdrop-filter: blur(10px); z-index: 100000;
display: flex; align-items: center; justify-content: center;
font-family: ${colors.font}; animation: modalFadeIn ${CONFIG.MODAL_FADE_DURATION} ease;
`;
const typeIcons = { info: 'ℹ️', success: '✓', error: '✗', warning: '⚠️', question: '?' };
const typeColors = {
info: colors.accent,
success: colors.success,
error: colors.error,
warning: colors.warning,
question: colors.accent
};
const icon = typeIcons[type] || typeIcons.info;
const iconColor = typeColors[type] || typeColors.info;
modal.innerHTML = `
<style>
@keyframes modalFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes modalSlideIn { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.modal-box {
background: ${colors.bgOverlay}; border: 2px solid ${colors.border};
border-radius: 8px; padding: 30px 40px; box-shadow: 0 0 40px ${colors.border}80;
min-width: 400px; max-width: 600px; animation: modalSlideIn ${CONFIG.MODAL_FADE_DURATION} ease;
}
.modal-icon { font-size: 48px; text-align: center; margin-bottom: 20px; color: ${iconColor}; text-shadow: 0 0 20px ${iconColor}; font-weight: 700; }
.modal-title { font-size: 20px; font-weight: 700; text-align: center; margin-bottom: 20px; color: ${colors.text}; text-transform: uppercase; letter-spacing: 2px; }
.modal-message { font-size: 14px; line-height: 1.8; text-align: center; margin-bottom: 25px; color: ${colors.textSecondary}; white-space: pre-line; }
.modal-buttons { display: flex; gap: 12px; justify-content: center; }
.modal-btn {
padding: 12px 30px; border: 2px solid ${colors.border}; border-radius: 4px; background: ${colors.btnBg};
color: ${colors.text}; font-family: ${colors.font}; font-size: 12px; font-weight: 700;
cursor: pointer; transition: all 0.2s ease; text-transform: uppercase; letter-spacing: 1px;
}
.modal-btn:hover { background: ${colors.accent}; color: ${colors.bg}; box-shadow: 0 0 15px ${colors.accent}; transform: scale(1.05); }
.modal-btn:focus { outline: 2px solid ${colors.accent}; outline-offset: 2px; }
.modal-btn.primary { background: ${colors.accent}; color: ${colors.bg}; border-color: ${colors.accent}; }
.modal-btn.primary:hover { box-shadow: 0 0 20px ${colors.accent}; }
</style>
<div class="modal-box">
<div class="modal-icon" aria-hidden="true">${icon}</div>
<div class="modal-title" id="modal-title">${title}</div>
<div class="modal-message" id="modal-message">${message}</div>
<div class="modal-buttons" id="modal-buttons"></div>
</div>
`;
document.body.appendChild(modal);
const buttonsContainer = modal.querySelector('#modal-buttons');
buttons.forEach((btn, index) => {
const button = document.createElement('button');
button.className = index === 0 ? 'modal-btn primary' : 'modal-btn';
button.textContent = btn.text;
button.setAttribute('type', 'button');
button.addEventListener('click', () => {
modal.style.opacity = '0';
modal.style.transition = `opacity ${CONFIG.MODAL_FADE_DURATION} ease`;
setTimeout(() => {
modal.remove();
resolve(btn.value !== undefined ? btn.value : true);
if (btn.action) btn.action();
}, CONFIG.TOAST_ANIMATION_DURATION);
});
buttonsContainer.appendChild(button);
if (index === 0) button.focus(); // Auto-focus primary button
});
// ESC to close
const handleEsc = (e) => {
if (e.key === 'Escape') {
modal.querySelector('.modal-btn').click();
document.removeEventListener('keydown', handleEsc);
}
};
document.addEventListener('keydown', handleEsc);
});
}
function showPluginToast(message, type = 'info') {
const colors = getThemeColors();
const typeColors = {
success: colors.success,
error: colors.error,
info: colors.accent,
warning: colors.warning
};
const bgColor = typeColors[type] || typeColors.info;
const textColor = getCurrentTheme() === 'furry' ? '#fff' : '#000';
const toast = document.createElement('div');
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
toast.style.cssText = `
position: fixed; top: 140px; right: 20px; background: ${bgColor}; padding: 16px 24px;
border-radius: 8px; color: ${textColor}; font-family: ${colors.font}; font-size: 13px;
box-shadow: 0 0 20px ${bgColor}; z-index: 10005; max-width: 300px; word-wrap: break-word;
font-weight: 700; border: 2px solid ${colors.border}; animation: pluginSlideIn ${CONFIG.TOAST_SLIDE_DURATION} ease-out;
`;
const theme = getCurrentTheme();
const prefix = theme === 'hacker' ? '⚡ ' : theme === 'furry' ? '★ ' : '◆ ';
const suffix = theme === 'furry' ? ' UwU' : '';
toast.textContent = prefix + '[SYNC] ' + message + suffix;
if (!document.getElementById('plugin-toast-styles')) {
const style = document.createElement('style');
style.id = 'plugin-toast-styles';
style.textContent = `
@keyframes pluginSlideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes pluginSlideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(400px); opacity: 0; } }
`;
document.head.appendChild(style);
}
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = `pluginSlideOut ${CONFIG.TOAST_SLIDE_DURATION} ease-out`;
setTimeout(() => toast.remove(), CONFIG.TOAST_ANIMATION_DURATION);
}, CONFIG.TOAST_DURATION);
}
// ============================================================================
// LOADING OVERLAY - WITH RAF OPTIMIZATION
// ============================================================================
function createLoadingOverlay() {
const colors = getThemeColors();
state.loadingOverlay = document.createElement('div');
state.loadingOverlay.id = 'element-sync-loading';
state.loadingOverlay.setAttribute('role', 'alert');
state.loadingOverlay.setAttribute('aria-live', 'assertive');
state.loadingOverlay.setAttribute('aria-busy', 'true');
state.loadingOverlay.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: ${colors.bg}ee;
z-index: 99999; display: flex; flex-direction: column; align-items: center; justify-content: center;
font-family: ${colors.font}; color: ${colors.text}; backdrop-filter: blur(10px);
`;
state.loadingOverlay.innerHTML = `
<style>
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(0.95); } }
.loading-spinner {
width: 60px; height: 60px; border: 4px solid ${colors.accentDim}; border-top: 4px solid ${colors.accent};
border-radius: 50%; animation: spin 0.3s linear infinite; margin-bottom: 30px;
}
.loading-box {
background: ${colors.bgOverlay}; border: 2px solid ${colors.border}; border-radius: 8px;
padding: 40px 60px; box-shadow: 0 0 40px ${colors.border}80; text-align: center; max-width: 600px;
}
.loading-title { font-size: 24px; font-weight: 700; margin-bottom: 20px; text-transform: uppercase; letter-spacing: 2px; text-shadow: 0 0 10px ${colors.accent}; }
.loading-status { font-size: 14px; margin-bottom: 15px; animation: pulse 2s ease-in-out infinite; color: ${colors.textSecondary}; }
.loading-progress { font-size: 20px; font-weight: 700; margin-bottom: 10px; color: ${colors.accent}; }
.loading-eta { font-size: 16px; font-weight: 600; margin-bottom: 20px; color: ${colors.warning}; }
.loading-speed { font-size: 12px; margin-bottom: 20px; color: ${colors.textSecondary}; }
.loading-bar-container {
width: 400px; height: 20px; background: ${colors.accentDim}; border-radius: 10px;
overflow: hidden; margin-bottom: 20px; border: 1px solid ${colors.border};
}
.loading-bar {
height: 100%; background: ${colors.accent}; width: 0%;
transition: width 0.3s ease; box-shadow: 0 0 10px ${colors.accent};
will-change: width;
}
.loading-info { font-size: 11px; opacity: 0.7; margin-top: 20px; line-height: 1.6; color: ${colors.textSecondary}; }
</style>
<div class="loading-box">
<div class="loading-spinner" aria-hidden="true"></div>
<div class="loading-title">⚡ SYNCING ELEMENTS ⚡</div>
<div class="loading-status" id="loading-status">Optimizing...</div>
<div class="loading-progress" id="loading-progress" aria-live="polite">0 / 0</div>
<div class="loading-eta" id="loading-eta">Calculating ETA...</div>
<div class="loading-speed" id="loading-speed">Speed: 0 items/s</div>
<div class="loading-bar-container"><div class="loading-bar" id="loading-bar"></div></div>
<div class="loading-info">
<div style="font-weight:700;margin-bottom:8px;">⚡ ULTRA-OPTIMIZED MODE ⚡</div>
<div>RAF-powered updates • Smart batching • Maximum velocity</div>
</div>
</div>
`;
document.body.appendChild(state.loadingOverlay);
}
// Use RAF for smooth 60fps updates
function updateLoadingOverlay(status, current, total, eta = null, speed = null) {
if (!state.loadingOverlay) return;
const now = performance.now();
if (now - state.lastUIUpdate < CONFIG.UI_UPDATE_THROTTLE) return;
if (state.rafId) cancelAnimationFrame(state.rafId);
state.rafId = requestAnimationFrame(() => {
const statusEl = document.getElementById('loading-status');
const progressEl = document.getElementById('loading-progress');
const etaEl = document.getElementById('loading-eta');
const speedEl = document.getElementById('loading-speed');
const barEl = document.getElementById('loading-bar');
if (statusEl) statusEl.textContent = status;
if (progressEl) progressEl.textContent = `${current.toLocaleString()} / ${total.toLocaleString()}`;
if (etaEl) etaEl.textContent = eta !== null && eta > 0 ? `ETA: ${formatETA(eta)}` : 'Calculating...';
if (speedEl) speedEl.textContent = speed !== null ? `⚡ ${speed.toFixed(0)} items/s` : 'Calculating...';
if (barEl && total > 0) {
const percentage = Math.min(100, (current / total) * 100);
barEl.style.width = percentage + '%';
}
state.lastUIUpdate = now;
state.rafId = null;
});
}
function removeLoadingOverlay() {
if (state.rafId) {
cancelAnimationFrame(state.rafId);
state.rafId = null;
}
if (state.loadingOverlay) {
state.loadingOverlay.style.opacity = '0';
state.loadingOverlay.style.transition = 'opacity 0.5s ease';
setTimeout(() => {
if (state.loadingOverlay) {
state.loadingOverlay.remove();
state.loadingOverlay = null;
}
}, 500);
}
}
// ============================================================================
// ELEMENT SCANNING - ULTRA OPTIMIZED
// ============================================================================
function getTotalItemCount() {
const searchInput = document.querySelector('.sidebar-input');
if (!searchInput) return null;
const placeholder = searchInput.getAttribute('placeholder') || '';
const match = placeholder.match(/\(([0-9,]+)\)/);
return match ? parseInt(match[1].replace(/,/g, '')) : null;
}
async function scrollAndLoadAllItems(sidebar, totalExpected) {
updateLoadingOverlay('⚡ TURBO SCROLLING...', 0, totalExpected);
const itemsInner = sidebar.querySelector('.items-inner');
if (!itemsInner) {
throw new Error(
'Cannot find element list!\n\n' +
'The game layout may have changed.\n' +
'Try refreshing the page and scanning again.'
);
}
const startTime = Date.now();
let itemHistory = [];
let updateCounter = 0;
let noChangeCounter = 0;
let lastCount = 0;
let retryAttempts = 0;
while (true) {
const currentItemCount = itemsInner.querySelectorAll('[data-item]').length;
updateCounter++;
// Throttled UI updates
if (updateCounter % 10 === 0) {
const now = Date.now();
itemHistory.push({ count: currentItemCount, time: now });
itemHistory = itemHistory.filter(h => now - h.time < CONFIG.SPEED_HISTORY_WINDOW);
let loadingSpeed = 0;
if (itemHistory.length >= 2) {
const oldest = itemHistory[0];
const newest = itemHistory[itemHistory.length - 1];
const timeDiff = (newest.time - oldest.time) / 1000;
const itemDiff = newest.count - oldest.count;
if (timeDiff > 0) loadingSpeed = itemDiff / timeDiff;
}
let eta = null;
if (loadingSpeed > 0 && currentItemCount < totalExpected) {
eta = (totalExpected - currentItemCount) / loadingSpeed;
}
const percentLoaded = (currentItemCount / totalExpected) * 100;
updateLoadingOverlay(
`⚡ ULTRA: ${currentItemCount.toLocaleString()} / ${totalExpected.toLocaleString()} (${percentLoaded.toFixed(1)}%)`,
currentItemCount, totalExpected, eta, loadingSpeed
);
}
if (currentItemCount >= totalExpected) {
console.log(`[SYNC] ✓ 100% Target reached: ${currentItemCount} items`);
break;
}
if (currentItemCount === lastCount) {
noChangeCounter++;
const percentLoaded = (currentItemCount / totalExpected) * 100;
if (noChangeCounter === CONFIG.RETRY_RAPID_SCROLL_THRESHOLD) {
console.log(`[SYNC] ⚠ Slow loading at ${percentLoaded.toFixed(1)}% - rapid scrolls...`);
updateLoadingOverlay(`⚠ Recovery: Rapid Scrolls (${percentLoaded.toFixed(1)}%)`, currentItemCount, totalExpected);
for (let i = 0; i < CONFIG.RAPID_SCROLL_COUNT; i++) {
sidebar.scrollTop = sidebar.scrollHeight;
await new Promise(resolve => setTimeout(resolve, CONFIG.RAPID_SCROLL_DELAY));
}
}
if (noChangeCounter === CONFIG.RETRY_SCROLL_RESET_THRESHOLD) {
console.log(`[SYNC] ⚠ Still stuck - scroll reset...`);
updateLoadingOverlay(`⚠ Recovery: Scroll Reset (${percentLoaded.toFixed(1)}%)`, currentItemCount, totalExpected);
sidebar.scrollTop = 0;
await new Promise(resolve => setTimeout(resolve, CONFIG.RECOVERY_POSITION_DELAY));
sidebar.scrollTop = sidebar.scrollHeight;
await new Promise(resolve => setTimeout(resolve, CONFIG.RECOVERY_POSITION_DELAY * 2));
}
if (noChangeCounter === CONFIG.RETRY_DEEP_SCAN_THRESHOLD) {
if (retryAttempts < CONFIG.MAX_RETRY_ATTEMPTS) {
retryAttempts++;
console.log(`[SYNC] ⚠ Retry ${retryAttempts}/${CONFIG.MAX_RETRY_ATTEMPTS}`);
updateLoadingOverlay(`⚠ Retry ${retryAttempts}/${CONFIG.MAX_RETRY_ATTEMPTS}: Deep Scan`, currentItemCount, totalExpected);
for (const ratio of CONFIG.RECOVERY_SCROLL_POSITIONS) {
sidebar.scrollTop = sidebar.scrollHeight * ratio;
await new Promise(resolve => setTimeout(resolve, CONFIG.RECOVERY_POSITION_DELAY));
}
sidebar.scrollTop = sidebar.scrollHeight;
await new Promise(resolve => setTimeout(resolve, 500));
const recheckCount = itemsInner.querySelectorAll('[data-item]').length;
if (recheckCount > currentItemCount) {
console.log(`[SYNC] ✓ Retry successful! +${recheckCount - currentItemCount}`);
noChangeCounter = 0;
lastCount = recheckCount;
continue;
}
noChangeCounter = 0;
continue;
}
}
if (noChangeCounter >= CONFIG.MAX_NO_CHANGE_COUNT) {
const finalPercent = (currentItemCount / totalExpected) * 100;
if (finalPercent >= CONFIG.MIN_ACCEPTABLE_PERCENT) {
console.log(`[SYNC] ✓ ${finalPercent.toFixed(1)}% loaded - acceptable`);
break;
}
throw new Error(
`Could not load all items!\n\n` +
`Loaded: ${currentItemCount.toLocaleString()} / ${totalExpected.toLocaleString()} (${finalPercent.toFixed(1)}%)\n\n` +
`Recovery suggestions:\n` +
`1. Refresh the page and wait 5 seconds\n` +
`2. Close other heavy tabs\n` +
`3. Clear browser cache\n` +
`4. Try again with fewer browser extensions`
);
}
} else {
noChangeCounter = 0;
if (currentItemCount > lastCount + 50) retryAttempts = 0;
}
lastCount = currentItemCount;
sidebar.scrollTop = sidebar.scrollHeight;
await new Promise(resolve => setTimeout(resolve, CONFIG.SCROLL_INTERVAL));
}
const finalItemCount = itemsInner.querySelectorAll('[data-item]').length;
const elapsedTime = (Date.now() - startTime) / 1000;
const percentLoaded = (finalItemCount / totalExpected) * 100;
console.log(`[SYNC] ✓ Complete: ${finalItemCount}/${totalExpected} (${percentLoaded.toFixed(1)}%) in ${elapsedTime.toFixed(1)}s`);
return {
totalItems: finalItemCount,
itemsInner,
percentLoaded,
expectedTotal: totalExpected
};
}
async function scanAllElements() {
if (state.isScanning) {
showPluginToast('Already scanning...', 'warning');
return;
}
if (isAutoDraggerRunning()) {
await showModal(
'AUTO DRAGGER RUNNING',
'Please stop the Auto Dragger before scanning.\n\nClick [STOP] then try again.',
'error',
[{ text: 'OK' }]
);
return;
}
state.isScanning = true;
state.allElements = [];
const startBtn = document.getElementById('start-btn');
const wasStartDisabled = startBtn?.disabled;
if (startBtn) {
startBtn.disabled = true;
startBtn.textContent = '[SCANNING]';
}
const scanBtn = document.getElementById('sync-scan-btn');
if (scanBtn) {
scanBtn.textContent = '[SCANNING...]';
scanBtn.disabled = true;
}
createLoadingOverlay();
try {
updateLoadingOverlay('⚡ Optimizing performance...', 0, 0);
hideGameElements();
await new Promise(resolve => setTimeout(resolve, CONFIG.OPTIMIZATION_DELAY));
updateLoadingOverlay('Reading item count...', 0, 0);
const totalExpected = getTotalItemCount();
if (!totalExpected) {
throw new Error(
'Cannot read item count!\n\n' +
'Make sure the sidebar is visible.\n' +
'Refresh the page and try again.'
);
}
updateLoadingOverlay('Finding sidebar...', 0, totalExpected);
const sidebar = document.getElementById('sidebar');
if (!sidebar) {
throw new Error(
'Sidebar not found!\n\n' +
'The game layout may have changed.\n' +
'Please refresh and try again.'
);
}
const { totalItems, itemsInner, percentLoaded, expectedTotal } = await scrollAndLoadAllItems(sidebar, totalExpected);
updateLoadingOverlay('⚡ Extracting elements...', 0, totalItems);
// Optimized extraction with Set for O(1) lookups
const allItems = itemsInner.querySelectorAll('[data-item]');
const seenIds = new Set();
const seenTexts = new Set();
const elements = [];
allItems.forEach((item) => {
const emoji = item.getAttribute('data-item-emoji') || '';
const text = item.getAttribute('data-item-text') || '';
const id = item.getAttribute('data-item-id') || '';
const isDiscovery = item.hasAttribute('data-item-discovery');
if (text && !((id && seenIds.has(id)) || seenTexts.has(text))) {
if (id) seenIds.add(id);
seenTexts.add(text);
elements.push({
id,
name: emoji ? `${emoji} ${text}` : text,
emoji,
text,
isFirstDiscovery: isDiscovery,
scanned: new Date().toISOString(),
scannedDate: new Date().toLocaleString()
});
}
});
// Efficient sort with numeric comparison
elements.sort((a, b) => {
const idA = parseInt(a.id);
const idB = parseInt(b.id);
if (!isNaN(idA) && !isNaN(idB)) return idA - idB;
return 0;
});
// Add indices
elements.forEach((e, idx) => e.index = idx + 1);
state.allElements = elements;
const foundCount = elements.length;
saveElements();
updateLoadingOverlay('✓ Complete! Restoring...', foundCount, foundCount);
await new Promise(resolve => setTimeout(resolve, CONFIG.RESTORE_DELAY));
restoreGameElements();
setTimeout(async () => {
removeLoadingOverlay();
showPluginToast(`Synced ${foundCount} elements!`, 'success');
updatePluginUI();
const firstDiscoveryCount = state.allElements.filter(e => e.isFirstDiscovery).length;
let message = `⚡ SYNC COMPLETE ⚡\n\n`;
message += `Scanned: ${totalItems.toLocaleString()} / ${expectedTotal.toLocaleString()} items (${percentLoaded.toFixed(1)}%)\n`;
message += `Total Elements: ${foundCount}\n`;
message += `First Discoveries: ${firstDiscoveryCount}\n`;
if (percentLoaded < 100) {
message += `\n⚠️ WARNING: Only ${percentLoaded.toFixed(1)}% loaded!\n`;
message += `Some items may be missing.\n`;
message += `Consider rescanning for complete results.`;
}
message += `\n\nReady to export!`;
await showModal(
percentLoaded >= 100 ? 'SYNC COMPLETE' : 'SYNC COMPLETE (PARTIAL)',
message,
percentLoaded >= 100 ? 'success' : 'warning',
[{ text: 'AWESOME!' }]
);
}, 500);
} catch (e) {
console.error('[SYNC]', e);
restoreGameElements();
removeLoadingOverlay();
showPluginToast(`Scan failed!`, 'error');
await showModal('SYNC ERROR', `${e.message}`, 'error', [{ text: 'OK' }]);
} finally {
state.isScanning = false;
if (startBtn && !wasStartDisabled) {
startBtn.disabled = false;
startBtn.textContent = '[START]';
}
if (scanBtn) {
scanBtn.textContent = '⚡ [SYNC ALL] ⚡';
scanBtn.disabled = false;
}
}
}
// ============================================================================
// DATA MANAGEMENT - WITH QUOTA HANDLING
// ============================================================================
function saveElements() {
try {
localStorage.setItem(CONFIG.STORAGE_KEY_ELEMENTS, JSON.stringify(state.allElements));
return true;
} catch (e) {
console.error('[SYNC] Save failed:', e);
if (e.name === 'QuotaExceededError') {
showPluginToast('Storage full! Export to free space.', 'error');
showModal(
'STORAGE FULL',
'Your browser storage is full!\n\n' +
'Please export your data, then clear it to free up space.\n\n' +
'Your current scan is safe, but cannot be saved.',
'error',
[{ text: 'OK' }]
);
} else {
showPluginToast('Save failed! Check console.', 'error');
}
return false;
}
}
function loadElements() {
try {
const saved = localStorage.getItem(CONFIG.STORAGE_KEY_ELEMENTS);
if (saved) {
state.allElements = JSON.parse(saved);
console.log(`[SYNC] ✓ Loaded ${state.allElements.length} elements from storage`);
}
} catch (e) {
console.error('[SYNC] Load failed:', e);
state.allElements = [];
showPluginToast('Failed to load saved data', 'warning');
}
}
function loadExportHistory() {
try {
const saved = localStorage.getItem(CONFIG.STORAGE_KEY_EXPORT_HISTORY);
if (saved) state.exportHistory = JSON.parse(saved);
} catch (e) {
state.exportHistory = [];
}
}
function saveExportHistory(type, count) {
state.exportHistory.push({
date: Date.now(),
dateFormatted: new Date().toLocaleString(),
count,
type
});
// Keep only last 10
state.exportHistory = state.exportHistory.slice(-CONFIG.MAX_EXPORT_HISTORY);
try {
localStorage.setItem(CONFIG.STORAGE_KEY_EXPORT_HISTORY, JSON.stringify(state.exportHistory));
} catch (e) {
console.warn('[SYNC] Could not save export history:', e);
}
}
// ============================================================================
// EXPORT FUNCTIONS - OPTIMIZED
// ============================================================================
function exportAsJSON() {
if (state.allElements.length === 0) {
showPluginToast('No elements! Sync first.', 'warning');
return;
}
const firstDiscoveryCount = state.allElements.filter(e => e.isFirstDiscovery).length;
const exportData = {
pluginVersion: CONFIG.VERSION,
exportDate: new Date().toISOString(),
exportDateFormatted: new Date().toLocaleString(),
totalElements: state.allElements.length,
totalFirstDiscoveries: firstDiscoveryCount,
elements: state.allElements.map((e) => ({
index: e.index,
id: e.id,
name: e.name,
emoji: e.emoji,
text: e.text,
isFirstDiscovery: e.isFirstDiscovery,
scannedAt: e.scannedDate,
timestamp: e.scanned
}))
};
try {
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `infinite-craft-ALL-ELEMENTS-${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
saveExportHistory('JSON', state.allElements.length);
showPluginToast(`Exported ${state.allElements.length} elements!`, 'success');
} catch (e) {
console.error('[SYNC] Export failed:', e);
showPluginToast('Export failed! Check console.', 'error');
}
}
function exportAsCSV() {
if (state.allElements.length === 0) {
showPluginToast('No elements!', 'warning');
return;
}
let csv = 'Index,ID,Emoji,Name,First Discovery,Scanned At\n';
state.allElements.forEach(e => {
const name = e.text.replace(/"/g, '""');
csv += `${e.index},${e.id},"${e.emoji}","${name}",${e.isFirstDiscovery ? 'YES' : 'NO'},"${e.scannedDate}"\n`;
});
try {
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `infinite-craft-ALL-ELEMENTS-${Date.now()}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
saveExportHistory('CSV', state.allElements.length);
showPluginToast(`Exported CSV!`, 'success');
} catch (e) {
console.error('[SYNC] CSV export failed:', e);
showPluginToast('CSV failed!', 'error');
}
}
function exportAsTxt() {
if (state.allElements.length === 0) {
showPluginToast('No elements!', 'warning');
return;
}
const firstDiscoveryCount = state.allElements.filter(e => e.isFirstDiscovery).length;
let txt = `INFINITE CRAFT - ALL ELEMENTS\n`;
txt += `Total Elements: ${state.allElements.length}\n`;
txt += `First Discoveries: ${firstDiscoveryCount}\n`;
txt += `Exported: ${new Date().toLocaleString()}\n${'='.repeat(60)}\n\n`;
state.allElements.forEach(e => {
txt += `${e.index}. ${e.name}`;
if (e.isFirstDiscovery) txt += ' 🌟';
txt += '\n';
});
try {
const blob = new Blob([txt], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `infinite-craft-ALL-ELEMENTS-${Date.now()}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
saveExportHistory('TXT', state.allElements.length);
showPluginToast(`Exported TXT!`, 'success');
} catch (e) {
console.error('[SYNC] TXT export failed:', e);
showPluginToast('TXT failed!', 'error');
}
}
async function clearElements() {
if (state.allElements.length === 0) {
showPluginToast('No data!', 'info');
return;
}
const confirmed = await showModal(
'CLEAR DATA',
`Delete all ${state.allElements.length} synced elements?\n\nThis cannot be undone!`,
'warning',
[{ text: 'DELETE', value: true }, { text: 'CANCEL', value: false }]
);
if (confirmed) {
state.allElements = [];
saveElements();
updatePluginUI();
showPluginToast('Cleared!', 'info');
}
}
// ============================================================================
// UI FUNCTIONS - OPTIMIZED
// ============================================================================
function updatePluginUI() {
// Batch DOM reads/writes
requestAnimationFrame(() => {
const counter = document.getElementById('sync-element-count');
if (counter) counter.textContent = state.allElements.length.toLocaleString();
const discoveryCounter = document.getElementById('sync-discovery-count');
if (discoveryCounter) {
const firstDiscoveries = state.allElements.filter(e => e.isFirstDiscovery).length;
discoveryCounter.textContent = firstDiscoveries.toLocaleString();
}
const list = document.getElementById('sync-element-list');
if (list) {
if (state.allElements.length === 0) {
list.innerHTML = '<div style="font-size:11px;opacity:0.7;font-style:italic;text-align:center;padding:20px;">Click [SYNC ALL] to begin!</div>';
} else {
const displayItems = state.allElements.slice(0, CONFIG.LIST_PREVIEW_LIMIT);
const fragment = document.createDocumentFragment();
displayItems.forEach((e) => {
const div = document.createElement('div');
div.className = 'element-item';
div.style.cssText = `margin-bottom:4px;padding:6px 8px;background:var(--theme-surface);border-radius:3px;border-left:3px solid ${e.isFirstDiscovery ? 'var(--theme-success)' : 'var(--theme-info)'}`;
div.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
<span style="font-weight:700;font-size:11px;flex:1;">#${e.index} ${e.name} ${e.isFirstDiscovery ? '🌟' : ''}</span>
<span style="font-size:9px;opacity:0.6;font-weight:600;">ID:${e.id}</span>
</div>
`;
fragment.appendChild(div);
});
list.innerHTML = '';
list.appendChild(fragment);
if (state.allElements.length > CONFIG.LIST_PREVIEW_LIMIT) {
const moreDiv = document.createElement('div');
moreDiv.style.cssText = 'font-size:10px;opacity:0.6;text-align:center;margin-top:8px;font-weight:700;padding:6px;background:var(--theme-accentDim);border-radius:3px;';
moreDiv.textContent = `...${state.allElements.length - CONFIG.LIST_PREVIEW_LIMIT} more (Total: ${state.allElements.length})`;
list.appendChild(moreDiv);
}
}
}
});
}
function injectPluginUI() {
if (document.getElementById('element-sync-plugin-section')) return true;
const mainPanel = document.getElementById('auto-dragger-panel');
if (mainPanel) {
const settingsTab = document.querySelector('[data-tab-content="settings"]');
if (settingsTab) {
const pluginSection = createPluginSection();
settingsTab.insertBefore(pluginSection, settingsTab.firstChild);
setupEventListeners();
updatePluginUI();
return true;
}
}
createStandalonePanel();
setupEventListeners();
updatePluginUI();
return true;
}
function createPluginSection() {
const section = document.createElement('div');
section.id = 'element-sync-plugin-section';
section.className = 'section';
section.style.cssText = 'border: 2px solid var(--theme-info); box-shadow: 0 0 10px var(--theme-info);';
section.innerHTML = `
<div class="section-title" style="display:flex;justify-content:space-between;align-items:center;">
<span>⚡ ELEMENT SYNC ULTRA</span>
<span style="font-size:9px;opacity:0.8;font-weight:700;">${CONFIG.VERSION}</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px;">
<div class="data-box">
<div class="data-box-header">Total Elements</div>
<div class="data-box-value" id="sync-element-count" style="font-size:32px;text-shadow:0 0 10px var(--theme-info);">0</div>
</div>
<div class="data-box">
<div class="data-box-header">First Discoveries</div>
<div class="data-box-value" id="sync-discovery-count" style="font-size:32px;text-shadow:0 0 10px var(--theme-success);">0</div>
</div>
</div>
<div style="display:flex;gap:8px;margin-bottom:12px;">
<button id="sync-scan-btn" class="small-btn" style="flex:1;background:var(--theme-info);color:#000;border:2px solid var(--theme-info);padding:14px !important;font-weight:700;font-size:13px !important;text-shadow:0 0 5px rgba(0,0,0,0.3);">
⚡ [SYNC ALL] ⚡
</button>
</div>
<div class="discoveries" id="sync-element-list" style="max-height:280px;margin-bottom:12px;overflow-y:auto;border:1px solid var(--theme-border);border-radius:4px;padding:8px;background:var(--theme-accentDim);">
<div style="font-size:11px;opacity:0.7;font-style:italic;text-align:center;padding:20px;">Click [SYNC ALL] to begin!</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-bottom:8px;">
<button id="sync-export-json-btn" class="small-btn" style="background:transparent;color:var(--theme-info);border:2px solid var(--theme-info);padding:10px !important;font-weight:700;font-size:11px;">
[JSON]
</button>
<button id="sync-export-csv-btn" class="small-btn" style="background:transparent;color:var(--theme-info);border:2px solid var(--theme-info);padding:10px !important;font-weight:700;font-size:11px;">
[CSV]
</button>
<button id="sync-export-txt-btn" class="small-btn" style="background:transparent;color:var(--theme-info);border:2px solid var(--theme-info);padding:10px !important;font-weight:700;font-size:11px;">
[TXT]
</button>
</div>
<div style="display:flex;gap:8px;margin-bottom:10px;">
<button id="sync-clear-btn" class="small-btn" style="flex:1;background:transparent;color:var(--theme-error);border:2px solid var(--theme-error);padding:10px !important;font-weight:700;font-size:11px;">
[CLEAR DATA]
</button>
</div>
<div style="font-size:9px;color:var(--theme-textSecondary);padding:10px;background:var(--theme-surface);border-radius:4px;border:1px solid var(--theme-border);line-height:1.7;">
<div style="font-weight:700;margin-bottom:8px;color:var(--theme-text);font-size:10px;">⚡ ULTRA-OPTIMIZED:</div>
<div>• RAF-powered 60fps updates</div>
<div>• Smart batching & caching</div>
<div>• 8ms scroll intervals (20% faster)</div>
<div>• Storage quota protection</div>
<div>• Auto-retry with 5 recovery modes</div>
<div>• Marks first discoveries with 🌟</div>
<div style="margin-top:8px;padding-top:8px;border-top:1px solid var(--theme-border);font-weight:600;">
Alt+Y = Sync | Alt+E = Export JSON
</div>
</div>
`;
return section;
}
function createStandalonePanel() {
const colors = getThemeColors();
const panel = document.createElement('div');
panel.id = 'element-sync-standalone-panel';
panel.style.cssText = `
position: fixed; top: 20px; left: 20px; background: ${colors.bgOverlay};
border: 2px solid ${colors.border}; border-radius: 8px; padding: 20px; z-index: 10000;
min-width: 440px; max-width: 500px; font-family: ${colors.font}; color: ${colors.text};
box-shadow: 0 0 40px ${colors.border}80;
`;
const section = createPluginSection();
section.style.border = 'none';
section.style.boxShadow = 'none';
panel.appendChild(section);
document.body.appendChild(panel);
makeDraggable(panel);
}
function makeDraggable(element) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
const header = element.querySelector('.section-title') || element;
header.style.cursor = 'move';
header.onmousedown = (e) => {
if (e.target.tagName === 'BUTTON') return;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = () => {
document.onmouseup = null;
document.onmousemove = null;
};
document.onmousemove = (e) => {
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// Boundary checks
let newTop = element.offsetTop - pos2;
let newLeft = element.offsetLeft - pos1;
// Keep within viewport
newTop = Math.max(0, Math.min(newTop, window.innerHeight - element.offsetHeight));
newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - element.offsetWidth));
element.style.top = newTop + "px";
element.style.left = newLeft + "px";
};
};
}
function cleanup() {
state.boundListeners.forEach(({ el, evt, fn }) => {
if (el) el.removeEventListener(evt, fn);
});
state.boundListeners = [];
}
function setupEventListeners() {
cleanup(); // Remove old listeners first
const scanBtn = document.getElementById('sync-scan-btn');
const exportJsonBtn = document.getElementById('sync-export-json-btn');
const exportCsvBtn = document.getElementById('sync-export-csv-btn');
const exportTxtBtn = document.getElementById('sync-export-txt-btn');
const clearBtn = document.getElementById('sync-clear-btn');
const listeners = [
{ el: scanBtn, evt: 'click', fn: scanAllElements },
{ el: exportJsonBtn, evt: 'click', fn: exportAsJSON },
{ el: exportCsvBtn, evt: 'click', fn: exportAsCSV },
{ el: exportTxtBtn, evt: 'click', fn: exportAsTxt },
{ el: clearBtn, evt: 'click', fn: clearElements }
];
listeners.forEach(({ el, evt, fn }) => {
if (el) {
el.addEventListener(evt, fn);
state.boundListeners.push({ el, evt, fn });
}
});
}
// ============================================================================
// VERSION CHECKING
// ============================================================================
function checkForUpdates() {
const lastVersion = localStorage.getItem(CONFIG.STORAGE_KEY_VERSION);
if (lastVersion && lastVersion !== CONFIG.VERSION) {
showModal(
'PLUGIN UPDATED',
`${lastVersion} → ${CONFIG.VERSION}\n\n` +
`✨ New in this version:\n` +
`• 20% faster scrolling (8ms intervals)\n` +
`• RAF-powered 60fps UI updates\n` +
`• Smart batching & caching\n` +
`• Storage quota protection\n` +
`• Better accessibility (ARIA)\n` +
`• Viewport boundary checks\n\n` +
`Performance is now ULTRA! 🚀`,
'success',
[{ text: 'AWESOME!' }]
);
}
localStorage.setItem(CONFIG.STORAGE_KEY_VERSION, CONFIG.VERSION);
}
// ============================================================================
// KEYBOARD SHORTCUTS
// ============================================================================
document.addEventListener('keydown', (e) => {
if (e.target.matches('input, textarea')) return;
if (e.altKey && e.key.toLowerCase() === 'e') {
e.preventDefault();
if (state.pluginInitialized) exportAsJSON();
}
if (e.altKey && e.key.toLowerCase() === 'y') {
e.preventDefault();
if (state.pluginInitialized && !state.isScanning) scanAllElements();
}
});
// ============================================================================
// INITIALIZATION
// ============================================================================
async function initPlugin() {
if (state.pluginInitialized) return;
console.log(`[SYNC] Initializing ${CONFIG.VERSION}...`);
await waitForMainScript();
loadElements();
loadExportHistory();
setTimeout(() => {
injectPluginUI();
state.pluginInitialized = true;
checkForUpdates();
showPluginToast('Element Sync ULTRA ready!', 'success');
console.log(`[SYNC] ✓ Initialized with ${state.allElements.length} elements`);
}, 2000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPlugin);
} else {
setTimeout(initPlugin, 1000);
}
// ============================================================================
// PUBLIC API
// ============================================================================
window.elementSyncPlugin = {
version: CONFIG.VERSION,
scan: scanAllElements,
exportJSON: exportAsJSON,
exportCSV: exportAsCSV,
exportTXT: exportAsTxt,
clear: clearElements,
getElements: () => state.allElements,
getCount: () => state.allElements.length,
getFirstDiscoveries: () => state.allElements.filter(e => e.isFirstDiscovery),
getTotalItems: getTotalItemCount,
getConfig: () => ({ ...CONFIG }),
getExportHistory: () => state.exportHistory
};
console.log(`[SYNC] ${CONFIG.VERSION} loaded - Type elementSyncPlugin in console for API`);
})();