From ed749a747f0581a1ce127600750af85583b25f34 Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Sun, 11 Jun 2017 17:32:39 +0200 Subject: [PATCH] Issue #640 - migrate local browser save to indexeddb --- src/js/app.js | 3 + .../dialogs/BrowseLocalController.js | 36 ++-- src/js/controller/settings/SaveController.js | 8 +- .../storage/IndexedDbStorageService.js | 171 ++++++++++++++++++ src/js/service/storage/StorageService.js | 7 +- .../migrate/MigrateLocalStorageToIndexedDb.js | 69 +++++++ src/piskel-script-list.js | 2 + 7 files changed, 273 insertions(+), 23 deletions(-) create mode 100644 src/js/service/storage/IndexedDbStorageService.js create mode 100644 src/js/service/storage/migrate/MigrateLocalStorageToIndexedDb.js diff --git a/src/js/app.js b/src/js/app.js index e38a469..5875331 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -114,6 +114,9 @@ this.canvasBackgroundController = new pskl.controller.CanvasBackgroundController(); this.canvasBackgroundController.init(); + this.indexedDbStorageService = new pskl.service.storage.IndexedDbStorageService(this.piskelController); + this.indexedDbStorageService.init(); + this.localStorageService = new pskl.service.storage.LocalStorageService(this.piskelController); this.localStorageService.init(); diff --git a/src/js/controller/dialogs/BrowseLocalController.js b/src/js/controller/dialogs/BrowseLocalController.js index 8ed9eb7..6b765d7 100644 --- a/src/js/controller/dialogs/BrowseLocalController.js +++ b/src/js/controller/dialogs/BrowseLocalController.js @@ -10,7 +10,7 @@ this.localStorageItemTemplate_ = pskl.utils.Template.get('local-storage-item-template'); - this.service_ = pskl.app.localStorageService; + this.service_ = pskl.app.indexedDbStorageService; this.piskelList = $('.local-piskel-list'); this.prevSessionContainer = $('.previous-session'); @@ -36,24 +36,24 @@ }; ns.BrowseLocalController.prototype.fillLocalPiskelsList_ = function () { - var html = ''; - var keys = this.service_.getKeys(); - - keys.sort(function (k1, k2) { - if (k1.date < k2.date) {return 1;} - if (k1.date > k2.date) {return -1;} - return 0; - }); - - keys.forEach((function (key) { - var date = pskl.utils.DateUtils.format(key.date, '{{Y}}/{{M}}/{{D}} {{H}}:{{m}}'); - html += pskl.utils.Template.replace(this.localStorageItemTemplate_, { - name : key.name, - date : date + this.service_.getKeys().then(function (keys) { + var html = ''; + keys.sort(function (k1, k2) { + if (k1.date < k2.date) {return 1;} + if (k1.date > k2.date) {return -1;} + return 0; }); - }).bind(this)); - var tableBody_ = this.piskelList.get(0).tBodies[0]; - tableBody_.innerHTML = html; + keys.forEach((function (key) { + var date = pskl.utils.DateUtils.format(key.date, '{{Y}}/{{M}}/{{D}} {{H}}:{{m}}'); + html += pskl.utils.Template.replace(this.localStorageItemTemplate_, { + name : key.name, + date : date + }); + }).bind(this)); + + var tableBody_ = this.piskelList.get(0).tBodies[0]; + tableBody_.innerHTML = html; + }.bind(this)); }; })(); diff --git a/src/js/controller/settings/SaveController.js b/src/js/controller/settings/SaveController.js index dbabe52..9eaa73e 100644 --- a/src/js/controller/settings/SaveController.js +++ b/src/js/controller/settings/SaveController.js @@ -34,7 +34,7 @@ this.saveDesktopAsNewButton = document.querySelector('#save-desktop-as-new-button'); this.saveFileDownloadButton = document.querySelector('#save-file-download-button'); - this.safeAddEventListener_(this.saveLocalStorageButton, 'click', this.saveToLocalStorage_); + this.safeAddEventListener_(this.saveLocalStorageButton, 'click', this.saveToIndexedDb_); this.safeAddEventListener_(this.saveGalleryButton, 'click', this.saveToGallery_); this.safeAddEventListener_(this.saveDesktopButton, 'click', this.saveToDesktop_); this.safeAddEventListener_(this.saveDesktopAsNewButton, 'click', this.saveToDesktopAsNew_); @@ -99,7 +99,7 @@ if (pskl.app.isLoggedIn()) { this.saveToGallery_(); } else { - this.saveToLocalStorage_(); + this.saveToIndexedDb_(); } }; @@ -111,8 +111,8 @@ this.saveTo_('saveToGallery', false); }; - ns.SaveController.prototype.saveToLocalStorage_ = function () { - this.saveTo_('saveToLocalStorage', false); + ns.SaveController.prototype.saveToIndexedDb_ = function () { + this.saveTo_('saveToIndexedDb', false); }; ns.SaveController.prototype.saveToDesktop_ = function () { diff --git a/src/js/service/storage/IndexedDbStorageService.js b/src/js/service/storage/IndexedDbStorageService.js new file mode 100644 index 0000000..073854a --- /dev/null +++ b/src/js/service/storage/IndexedDbStorageService.js @@ -0,0 +1,171 @@ +(function () { + var ns = $.namespace('pskl.service.storage'); + var DB_NAME = 'PiskelDatabase'; + var DB_VERSION = 1; + + ns.IndexedDbStorageService = function (piskelController) { + this.piskelController = piskelController; + }; + + ns.IndexedDbStorageService.prototype.init = function () { + var request = window.indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = this.onRequestError_.bind(this); + request.onupgradeneeded = this.onUpgradeNeeded_.bind(this); + request.onsuccess = this.onRequestSuccess_.bind(this); + }; + + ns.IndexedDbStorageService.prototype.save = function (piskel) { + var name = piskel.getDescriptor().name; + var description = piskel.getDescriptor().description; + var date = Date.now(); + var serialized = pskl.utils.serialization.Serializer.serialize(piskel); + + return this.save_(name, description, date, serialized); + }; + + ns.IndexedDbStorageService.prototype.save_ = function (name, description, date, serialized) { + var deferred = Q.defer(); + var objectStore = this.db.transaction(['piskels'], 'readwrite').objectStore('piskels'); + + var getRequest = objectStore.get(name); + getRequest.onsuccess = function (event) { + console.log('get request successful for name: ' + name); + var data = event.target.result; + if (typeof data !== 'undefined') { + + data.serialized = serialized; + data.date = date; + data.description = description; + + var putRequest = objectStore.put(data); + putRequest.onerror = function(event) { + console.log('put request failed for name: ' + name); + deferred.reject(); + }; + putRequest.onsuccess = function(event) { + console.log('put request successful for name: ' + name); + deferred.resolve(); + }; + } else { + var request = objectStore.add({ + name: name, + description: description, + serialized: serialized, + date: date + }); + + request.onerror = function(event) { + console.log('Failed to save a piskel'); + deferred.reject(); + }; + request.onsuccess = function(event) { + console.log('Successfully saved a piskel'); + deferred.resolve(); + }; + } + }; + + getRequest.onerror = function () { + console.log('get request failed for name: ' + name); + deferred.reject(); + }; + + return deferred.promise; + }; + + ns.IndexedDbStorageService.prototype.load = function (name) { + var objectStore = this.db.transaction(['piskels'], 'readwrite').objectStore('piskels'); + + var getRequest = objectStore.get(name); + getRequest.onsuccess = function (event) { + console.log('get request successful for name: ' + name); + var data = event.target.result; + if (typeof data !== 'undefined') { + var serialized = data.serialized; + pskl.utils.serialization.Deserializer.deserialize( + JSON.parse(serialized), + function (piskel) { + pskl.app.piskelController.setPiskel(piskel); + } + ); + } else { + console.log('no local browser save found for name: ' + name); + } + }; + + getRequest.onerror = function () { + console.log('get request failed for name: ' + name); + }; + }; + + ns.IndexedDbStorageService.prototype.remove = function (name) { + var objectStore = this.db.transaction(['piskels'], 'readwrite').objectStore('piskels'); + var deleteRequest = objectStore.delete(name); + deleteRequest.onsuccess = function (event) { + console.log('successfully deleted local browser save for name: ' + name); + }; + + deleteRequest.onerror = function (event) { + console.log('failed to delete local browser save for name: ' + name); + }; + }; + + ns.IndexedDbStorageService.prototype.list = function () { + var deferred = Q.defer(); + var piskels = []; + var objectStore = this.db.transaction(['piskels']).objectStore('piskels'); + + var cursor = objectStore.openCursor(); + cursor.onsuccess = function(event) { + var cursor = event.target.result; + if (cursor) { + piskels.push({ + name: cursor.value.name, + date: cursor.value.date, + description: cursor.value.description + }); + cursor.continue(); + } else { + console.log('Cursor consumed all availabled piskels'); + deferred.resolve(piskels); + } + }; + + cursor.onerror = function () { + deferred.reject(); + }; + + return deferred.promise; + }; + + ns.IndexedDbStorageService.prototype.getKeys = function () { + return this.list(); + }; + + ns.IndexedDbStorageService.prototype.onRequestError_ = function (event) { + console.log('Failed to initialize IndexedDB, local browser saves will be unavailable.'); + }; + + ns.IndexedDbStorageService.prototype.onRequestSuccess_ = function (event) { + this.db = event.target.result; + console.log('Successfully initialized IndexedDB, local browser saves are available.'); + }; + + ns.IndexedDbStorageService.prototype.onUpgradeNeeded_ = function (event) { + // Set this.db early to allow migration scripts to access it in oncomplete. + this.db = event.target.result; + + // Create an object store "piskels" with the autoIncrement flag set as true. + var objectStore = this.db.createObjectStore('piskels', { keyPath : 'name' }); + objectStore.transaction.oncomplete = function(event) { + // Migrate existing sprites from LocalStorage + pskl.service.storage.migrate.MigrateLocalStorageToIndexedDb.migrate().then(function () { + console.log('Successfully migrated local storage data to indexed db'); + }).catch(function (e) { + console.log('Failed to migrate local storage data to indexed db'); + console.error(e); + }); + }; + }; +})(); diff --git a/src/js/service/storage/StorageService.js b/src/js/service/storage/StorageService.js index e4407c4..e55d7a1 100644 --- a/src/js/service/storage/StorageService.js +++ b/src/js/service/storage/StorageService.js @@ -27,10 +27,15 @@ return this.delegateSave_(pskl.app.galleryStorageService, piskel); }; + // @deprecated, use saveToIndexedDb unless indexedDb is not available. ns.StorageService.prototype.saveToLocalStorage = function (piskel) { return this.delegateSave_(pskl.app.localStorageService, piskel); }; + ns.StorageService.prototype.saveToIndexedDb = function (piskel) { + return this.delegateSave_(pskl.app.indexedDbStorageService, piskel); + }; + ns.StorageService.prototype.saveToFileDownload = function (piskel) { return this.delegateSave_(pskl.app.fileDownloadStorageService, piskel); }; @@ -67,7 +72,7 @@ // wrap in timeout in order to start saving only after event.preventDefault // has been done window.setTimeout(function () { - this.saveToLocalStorage(this.piskelController.getPiskel()); + this.saveToIndexedDb(this.piskelController.getPiskel()); }.bind(this), 0); } }; diff --git a/src/js/service/storage/migrate/MigrateLocalStorageToIndexedDb.js b/src/js/service/storage/migrate/MigrateLocalStorageToIndexedDb.js new file mode 100644 index 0000000..fa86369 --- /dev/null +++ b/src/js/service/storage/migrate/MigrateLocalStorageToIndexedDb.js @@ -0,0 +1,69 @@ +(function () { + var ns = $.namespace('pskl.service.storage.migrate'); + + // Simple migration helper to move local storage saves to indexed db. + ns.MigrateLocalStorageToIndexedDb = {}; + + ns.MigrateLocalStorageToIndexedDb.migrate = function () { + var deferred = Q.defer(); + + var localStorageService = pskl.app.localStorageService; + var indexedDbStorageService = pskl.app.indexedDbStorageService; + + var localStorageKeys = localStorageService.getKeys(); + var migrationData = localStorageKeys.map(function (key) { + return { + name: key.name, + description: key.description, + date: key.date, + serialized: localStorageService.getPiskel(key.name) + }; + }); + + // Define the sequential migration process. + // Wait for each sprite to be saved before saving the next one. + var success = true; + var migrateSprite = function (index) { + var data = migrationData[index]; + if (!data) { + console.log('Data migration from local storage to indexed db finished.'); + if (success) { + ns.MigrateLocalStorageToIndexedDb.deleteLocalStoragePiskels(); + } + + deferred.resolve(); + return; + } + indexedDbStorageService.save_( + data.name, + data.description, + data.date, + data.serialized + ).then(function () { + migrateSprite(index + 1); + }).catch(function (e) { + var success = false; + console.error('Failed to migrate local storage sprite for name: ' + data.name); + migrateSprite(index + 1); + }); + }; + + // Start the migration. + migrateSprite(0); + + return deferred.promise; + }; + + ns.MigrateLocalStorageToIndexedDb.deleteLocalStoragePiskels = function () { + var localStorageKeys = pskl.app.localStorageService.getKeys(); + + // Remove all sprites. + localStorageKeys.forEach(function (key) { + window.localStorage.removeItem('piskel.' + key.name); + }); + + // Remove keys. + window.localStorage.removeItem('piskel.keys'); + }; + +})(); diff --git a/src/piskel-script-list.js b/src/piskel-script-list.js index 5969d73..5f829eb 100644 --- a/src/piskel-script-list.js +++ b/src/piskel-script-list.js @@ -168,8 +168,10 @@ "js/widgets/Wizard.js", // Services + "js/service/storage/migrate/MigrateLocalStorageToIndexedDb.js", "js/service/storage/StorageService.js", "js/service/storage/FileDownloadStorageService.js", + "js/service/storage/IndexedDbStorageService.js", "js/service/storage/LocalStorageService.js", "js/service/storage/GalleryStorageService.js", "js/service/storage/DesktopStorageService.js",