From e4c9fd3f3ad4af1f53edc70940f23c5cf70b69d2 Mon Sep 17 00:00:00 2001 From: SKProCH Date: Wed, 8 Oct 2025 14:33:16 +0300 Subject: [PATCH] Add IndexedDB support for large image data storage; implement async pixel loading and fallback handling. --- Extension/scripts/Acc-Switch.js | 59 +++++++++-- Extension/scripts/Auto-Image.js | 32 +++++- Extension/scripts/utils-manager.js | 154 ++++++++++++++++++++++++++--- 3 files changed, 225 insertions(+), 20 deletions(-) diff --git a/Extension/scripts/Acc-Switch.js b/Extension/scripts/Acc-Switch.js index 1206ec7..b8f1138 100644 --- a/Extension/scripts/Acc-Switch.js +++ b/Extension/scripts/Acc-Switch.js @@ -2526,6 +2526,26 @@ } } + // Decide how to store image pixels (inline for small images, IndexedDB ref for large) + let imageDataForSave = null; + if (state.imageData) { + const width = state.imageData.width; + const height = state.imageData.height; + const totalPixels = state.imageData.totalPixels; + const pixels = state.imageData.pixels; + const USE_IDB_THRESHOLD = 500000; // ~0.5 MB raw RGBA + + if (pixels && pixels.length > USE_IDB_THRESHOLD && window.globalUtilsManager && typeof window.globalUtilsManager.storePixelsInIndexedDB === 'function' && typeof window.globalUtilsManager.generatePixelsRef === 'function') { + const ref = window.globalUtilsManager.generatePixelsRef(width, height); + try { window.globalUtilsManager.storePixelsInIndexedDB(ref, { width, height, totalPixels, pixels }); } catch (e) { console.warn('IDB store failed:', e); } + imageDataForSave = { width, height, totalPixels, pixelsRef: ref, pixelsBackend: 'idb' }; + } else if (pixels) { + imageDataForSave = { width, height, pixels: Array.from(pixels), totalPixels }; + } else { + imageDataForSave = { width, height, totalPixels, pixels: null }; + } + } + const progressData = { timestamp: Date.now(), version: "2.1", @@ -2539,14 +2559,7 @@ colorsChecked: state.colorsChecked, availableColors: state.availableColors, }, - imageData: state.imageData - ? { - width: state.imageData.width, - height: state.imageData.height, - pixels: Array.from(state.imageData.pixels), - totalPixels: state.imageData.totalPixels, - } - : null, + imageData: imageDataForSave, paintedMapPacked: paintedMapPacked, } @@ -2653,6 +2666,36 @@ console.warn('Could not rebuild processor from saved image data:', e); } } + else if (savedData.imageData && savedData.imageData.pixelsRef) { + console.warn('ℹ️ Large image detected; loading pixels from IndexedDB via ref:', savedData.imageData.pixelsRef); + state.imageData = { + width: savedData.imageData.width, + height: savedData.imageData.height, + totalPixels: savedData.imageData.totalPixels, + pixels: null, + }; + state.imageLoaded = false; + + if (window.globalUtilsManager && typeof window.globalUtilsManager.loadPixelsFromIndexedDB === 'function') { + window.globalUtilsManager.loadPixelsFromIndexedDB(savedData.imageData.pixelsRef) + .then(payload => { + if (!payload || !payload.pixels) { + console.warn('⚠️ Pixels not found in IDB. Ask user to reload image.'); + return; + } + state.imageData.pixels = payload.pixels instanceof Uint8Array || payload.pixels instanceof Uint8ClampedArray + ? new Uint8ClampedArray(payload.pixels) + : new Uint8ClampedArray(payload.pixels); + state.imageLoaded = true; + + // Optionally try to restore overlay here if available in this script context + if (window.globalUtilsManager && typeof window.globalUtilsManager.restoreOverlayFromData === 'function') { + window.globalUtilsManager.restoreOverlayFromData().catch(() => {}); + } + }) + .catch(err => console.warn('❌ Failed to load pixels from IDB:', err)); + } + } else if (savedData.imageData && savedData.imageData.pixelsStripped) { console.warn('⚠️ Saved progress did not include raw pixel data due to quota limits. Please reload the image to resume.'); state.imageData = { diff --git a/Extension/scripts/Auto-Image.js b/Extension/scripts/Auto-Image.js index 97203b4..f0fee5b 100644 --- a/Extension/scripts/Auto-Image.js +++ b/Extension/scripts/Auto-Image.js @@ -1474,6 +1474,36 @@ function getText(key, params) { }; console.log('✅ [DEBUG] ImageData restored successfully'); console.log('🔍 [DEBUG] Converted pixels to Uint8ClampedArray, length:', window.state.imageData.pixels.length); + } else if (savedData.imageData && savedData.imageData.pixelsRef) { + console.warn('ℹ️ [DEBUG] Large image detected; loading pixels from IndexedDB via ref:', savedData.imageData.pixelsRef); + window.state.imageData = { + width: savedData.imageData.width, + height: savedData.imageData.height, + totalPixels: savedData.imageData.totalPixels, + pixels: null, + }; + window.state.imageLoaded = false; + + if (window.globalUtilsManager && typeof window.globalUtilsManager.loadPixelsFromIndexedDB === 'function') { + window.globalUtilsManager.loadPixelsFromIndexedDB(savedData.imageData.pixelsRef) + .then(payload => { + if (!payload || !payload.pixels) { + console.warn('⚠️ [DEBUG] Pixels not found in IDB. Ask user to reload image.'); + return; + } + window.state.imageData.pixels = payload.pixels instanceof Uint8Array || payload.pixels instanceof Uint8ClampedArray + ? new Uint8ClampedArray(payload.pixels) + : new Uint8ClampedArray(payload.pixels); + window.state.imageLoaded = true; + console.log('✅ [DEBUG] Pixels loaded from IDB'); + + // Try to restore overlay immediately + if (typeof window.globalUtilsManager.restoreOverlayFromData === 'function') { + window.globalUtilsManager.restoreOverlayFromData().catch(() => {}); + } + }) + .catch(err => console.warn('❌ [DEBUG] Failed to load pixels from IDB:', err)); + } } else if (savedData.imageData && savedData.imageData.pixelsStripped) { console.warn('⚠️ [DEBUG] Saved progress did not include raw pixel data due to quota limits. Please reload the image file to resume.'); // Preserve dimensions for UI, but mark image as not loaded @@ -8269,7 +8299,7 @@ function getText(key, params) { } function generateCoordinates(width, height, mode, direction, snake, blockWidth, blockHeight, startFromX = 0, startFromY = 0) { - const coords = []; + let coords = []; console.log( 'Generating coordinates with \n mode:', mode, diff --git a/Extension/scripts/utils-manager.js b/Extension/scripts/utils-manager.js index 2d91bd0..7464775 100644 --- a/Extension/scripts/utils-manager.js +++ b/Extension/scripts/utils-manager.js @@ -673,7 +673,7 @@ class WPlaceUtilsManager { } buildProgressData() { - return { + const result = { timestamp: Date.now(), version: '2.2', state: { @@ -691,16 +691,42 @@ class WPlaceUtilsManager { blockHeight: window.state.blockHeight, availableColors: window.state.availableColors, }, - imageData: window.state.imageData - ? { - width: window.state.imageData.width, - height: window.state.imageData.height, - pixels: Array.from(window.state.imageData.pixels), - totalPixels: window.state.imageData.totalPixels, - } - : null, - paintedMapPacked: this.buildPaintedMapPacked(), + imageData: null, + paintedMapPacked: null, }; + + // Decide how to store image pixels + if (window.state.imageData) { + const img = window.state.imageData; + const width = img.width; + const height = img.height; + const totalPixels = img.totalPixels; + const pixels = img.pixels; + + const USE_IDB_THRESHOLD = 500000; // ~0.5 MB raw RGBA (before JSON expansion) + + if (pixels && pixels.length) { + const bytes = pixels.length; // Uint8ClampedArray length equals byte size + if (bytes > USE_IDB_THRESHOLD) { + // Store pixels in IndexedDB and keep only the reference in save payload + const ref = this.generatePixelsRef(width, height); + try { + // Fire-and-forget async persist + this.storePixelsInIndexedDB(ref, { width, height, totalPixels, pixels }); + } catch (e) { + console.warn('Failed to schedule IDB pixel store:', e); + } + result.imageData = { width, height, totalPixels, pixelsRef: ref, pixelsBackend: 'idb' }; + } else { + result.imageData = { width, height, pixels: Array.from(pixels), totalPixels }; + } + } else { + result.imageData = { width, height, totalPixels, pixels: null }; + } + } + + result.paintedMapPacked = this.buildPaintedMapPacked(); + return result; } migrateProgress(saved) { @@ -865,6 +891,45 @@ class WPlaceUtilsManager { } catch (e) { console.warn('Could not rebuild processor from saved image data:', e); } + } else if (savedData.imageData && savedData.imageData.pixelsRef) { + // Asynchronously load large pixel data from IndexedDB using the stored reference + console.log('🔄 Loading large image pixels from IndexedDB via ref:', savedData.imageData.pixelsRef); + window.state.imageData = { + width: savedData.imageData.width, + height: savedData.imageData.height, + totalPixels: savedData.imageData.totalPixels, + pixels: null, + }; + window.state.imageLoaded = false; + + this.loadPixelsFromIndexedDB(savedData.imageData.pixelsRef) + .then((payload) => { + if (!payload || !payload.pixels) { + console.warn('⚠️ No pixel payload found in IndexedDB for ref:', savedData.imageData.pixelsRef); + this.showAlert?.('⚠️ Saved image pixels not found in local database. Please reload the image file.', 'warning'); + return; + } + try { + window.state.imageData.pixels = payload.pixels instanceof Uint8Array || payload.pixels instanceof Uint8ClampedArray + ? new Uint8ClampedArray(payload.pixels) + : new Uint8ClampedArray(payload.pixels); + window.state.imageLoaded = true; + + // Rebuild processor and notify modules/UI + this._syncModulesAfterStateRestore(); + this.showAlert?.('✅ Large image restored from local database.', 'success'); + + // Attempt overlay restoration if available + if (typeof this.restoreOverlayFromData === 'function') { + this.restoreOverlayFromData().catch(() => {}); + } + } catch (err) { + console.warn('Failed to apply pixels from IndexedDB:', err); + } + }) + .catch((err) => { + console.warn('Failed to load pixels from IndexedDB:', err); + }); } else if (savedData.imageData && savedData.imageData.pixelsStripped) { console.warn('⚠️ Saved progress did not include raw pixel data due to quota limits. Please reload the image to resume.'); window.state.imageData = { @@ -1293,4 +1358,71 @@ if (!window.globalUtilsManager) { console.error('❌ Failed to reinitialize WPlaceUtilsManager:', error); } }, 100); -} \ No newline at end of file +} + +// === IndexedDB helpers for large pixel storage === +WPlaceUtilsManager.prototype._openPixelDB = function() { + return new Promise((resolve, reject) => { + try { + const request = indexedDB.open('wplace-bot-db', 1); + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains('pixels')) { + const store = db.createObjectStore('pixels', { keyPath: 'ref' }); + try { store.createIndex('createdAt', 'createdAt', { unique: false }); } catch {} + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + } catch (e) { + reject(e); + } + }); +}; + +WPlaceUtilsManager.prototype.generatePixelsRef = function(width, height) { + const rand = Math.random().toString(36).slice(2, 8); + return `img_${width}x${height}_${Date.now().toString(36)}_${rand}`; +}; + +WPlaceUtilsManager.prototype.storePixelsInIndexedDB = async function(ref, payload) { + try { + const db = await this._openPixelDB(); + return await new Promise((resolve, reject) => { + const tx = db.transaction('pixels', 'readwrite'); + const store = tx.objectStore('pixels'); + const data = { + ref, + width: payload.width, + height: payload.height, + totalPixels: payload.totalPixels, + pixels: payload.pixels instanceof Uint8Array || payload.pixels instanceof Uint8ClampedArray + ? new Uint8Array(payload.pixels) + : new Uint8Array(payload.pixels), + createdAt: Date.now(), + }; + const req = store.put(data); + req.onsuccess = () => resolve(true); + req.onerror = () => reject(req.error); + }); + } catch (e) { + console.warn('IDB storePixels failed:', e); + return false; + } +}; + +WPlaceUtilsManager.prototype.loadPixelsFromIndexedDB = async function(ref) { + try { + const db = await this._openPixelDB(); + return await new Promise((resolve, reject) => { + const tx = db.transaction('pixels', 'readonly'); + const store = tx.objectStore('pixels'); + const req = store.get(ref); + req.onsuccess = () => resolve(req.result || null); + req.onerror = () => reject(req.error); + }); + } catch (e) { + console.warn('IDB loadPixels failed:', e); + return null; + } +};