diff --git a/src/index.html b/src/index.html index cc2dbe57..65c8f62a 100644 --- a/src/index.html +++ b/src/index.html @@ -75,9 +75,10 @@ rel="tooltip" data-placement="left" title="Performance problem detected, learn more."> - @@include('templates/dialogs/create-palette.html', {}) + @@include('templates/dialogs/browse-backups.html', {}) @@include('templates/dialogs/browse-local.html', {}) @@include('templates/dialogs/cheatsheet.html', {}) + @@include('templates/dialogs/create-palette.html', {}) @@include('templates/dialogs/import.html', {}) @@include('templates/dialogs/performance-info.html', {}) @@include('templates/dialogs/unsupported-browser.html', {}) diff --git a/src/js/app.js b/src/js/app.js index 58753317..3ef2718e 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -18,6 +18,9 @@ */ this.isAppEngineVersion = !!pskl.appEngineToken_; + // This id is used to keep track of sessions in the BackupService. + this.sessionId = pskl.utils.Uuid.generate(); + this.shortcutService = new pskl.service.keyboard.ShortcutService(); this.shortcutService.init(); diff --git a/src/js/controller/dialogs/DialogsController.js b/src/js/controller/dialogs/DialogsController.js index 3008b2a8..f43113b8 100644 --- a/src/js/controller/dialogs/DialogsController.js +++ b/src/js/controller/dialogs/DialogsController.js @@ -25,6 +25,10 @@ 'unsupported-browser' : { template : 'templates/dialogs/unsupported-browser.html', controller : ns.UnsupportedBrowserController + }, + 'browse-backups' : { + template : 'templates/dialogs/browse-backups.html', + controller : ns.backups.BrowseBackups } }; diff --git a/src/js/controller/dialogs/backups/BrowseBackups.js b/src/js/controller/dialogs/backups/BrowseBackups.js new file mode 100644 index 00000000..dbc76d07 --- /dev/null +++ b/src/js/controller/dialogs/backups/BrowseBackups.js @@ -0,0 +1,81 @@ +(function () { + var ns = $.namespace('pskl.controller.dialogs.backups'); + + var stepDefinitions = { + 'SELECT_SESSION' : { + controller : ns.steps.SelectSession, + template : 'backups-select-session' + }, + 'SESSION_DETAILS' : { + controller : ns.steps.SessionDetails, + template : 'backups-session-details' + }, + }; + + ns.BrowseBackups = function (piskelController, args) { + this.piskelController = piskelController; + + // Merge data object used by steps to communicate and share their + // results. + this.mergeData = { + sessions: [], + selectedSession : null + }; + }; + + pskl.utils.inherit(ns.BrowseBackups, pskl.controller.dialogs.AbstractDialogController); + + ns.BrowseBackups.prototype.init = function () { + this.superclass.init.call(this); + + // Prepare wizard steps. + this.steps = this.createSteps_(); + + // Start wizard widget. + var wizardContainer = document.querySelector('.backups-wizard-container'); + this.wizard = new pskl.widgets.Wizard(this.steps, wizardContainer); + this.wizard.init(); + + this.wizard.goTo('SELECT_SESSION'); + }; + + ns.BrowseBackups.prototype.back = function () { + this.wizard.back(); + this.wizard.getCurrentStep().instance.onShow(); + }; + + ns.BrowseBackups.prototype.next = function () { + var step = this.wizard.getCurrentStep(); + if (step.name === 'SELECT_SESSION') { + this.wizard.goTo('SESSION_DETAILS'); + } + }; + + ns.BrowseBackups.prototype.destroy = function (file) { + Object.keys(this.steps).forEach(function (stepName) { + var step = this.steps[stepName]; + step.instance.destroy(); + step.instance = null; + step.el = null; + }.bind(this)); + + this.superclass.destroy.call(this); + }; + + ns.BrowseBackups.prototype.createSteps_ = function () { + var steps = {}; + Object.keys(stepDefinitions).forEach(function (stepName) { + var definition = stepDefinitions[stepName]; + var el = pskl.utils.Template.getAsHTML(definition.template); + var instance = new definition.controller(this.piskelController, this, el); + instance.init(); + steps[stepName] = { + name: stepName, + el: el, + instance: instance + }; + }.bind(this)); + + return steps; + }; +})(); diff --git a/src/js/controller/dialogs/backups/steps/SelectSession.js b/src/js/controller/dialogs/backups/steps/SelectSession.js new file mode 100644 index 00000000..966787cf --- /dev/null +++ b/src/js/controller/dialogs/backups/steps/SelectSession.js @@ -0,0 +1,60 @@ +(function () { + var ns = $.namespace('pskl.controller.dialogs.backups.steps'); + + ns.SelectSession = function (piskelController, backupsController, container) { + this.piskelController = piskelController; + this.backupsController = backupsController; + this.container = container; + }; + + ns.SelectSession.prototype.addEventListener = function (el, type, cb) { + pskl.utils.Event.addEventListener(el, type, cb, this); + }; + + ns.SelectSession.prototype.init = function () { + this.addEventListener(this.container, 'click', this.onContainerClick_); + }; + + ns.SelectSession.prototype.onShow = function () { + this.update(); + }; + + ns.SelectSession.prototype.update = function () { + pskl.app.backupService.list().then(function (sessions) { + var html = ''; + if (sessions.length === 0) { + html = 'No session found ...'; + } else { + var sessionItemTemplate = pskl.utils.Template.get('session-list-item'); + var html = ''; + sessions.forEach(function (session) { + html += pskl.utils.Template.replace(sessionItemTemplate, session); + }); + } + this.container.querySelector('.session-list').innerHTML = html; + }.bind(this)); + }; + + ns.SelectSession.prototype.destroy = function () { + pskl.utils.Event.removeAllEventListeners(this); + }; + + ns.SelectSession.prototype.onContainerClick_ = function (evt) { + var sessionId = evt.target.dataset.sessionId; + if (!sessionId) { + return; + } + + var action = evt.target.dataset.action; + if (action == 'view') { + this.backupsController.mergeData.selectedSession = sessionId; + this.backupsController.next(); + } else if (action == 'delete') { + pskl.app.backupService.deleteSession(sessionId).then(function () { + // Refresh the list of sessions + this.update(); + }.bind(this)); + } + }; + +})(); diff --git a/src/js/controller/dialogs/backups/steps/SessionDetails.js b/src/js/controller/dialogs/backups/steps/SessionDetails.js new file mode 100644 index 00000000..cadc5ae7 --- /dev/null +++ b/src/js/controller/dialogs/backups/steps/SessionDetails.js @@ -0,0 +1,55 @@ +(function () { + var ns = $.namespace('pskl.controller.dialogs.backups.steps'); + + ns.SessionDetails = function (piskelController, backupsController, container) { + this.piskelController = piskelController; + this.backupsController = backupsController; + this.container = container; + }; + + ns.SessionDetails.prototype.init = function () { + this.backButton = this.container.querySelector('.back-button'); + this.addEventListener(this.backButton, 'click', this.onBackClick_); + this.addEventListener(this.container, 'click', this.onContainerClick_); + }; + + ns.SessionDetails.prototype.destroy = function () { + pskl.utils.Event.removeAllEventListeners(this); + }; + + ns.SessionDetails.prototype.addEventListener = function (el, type, cb) { + pskl.utils.Event.addEventListener(el, type, cb, this); + }; + + ns.SessionDetails.prototype.onShow = function () { + var sessionId = this.backupsController.mergeData.selectedSession; + pskl.app.backupService.getSnapshotsBySessionId(sessionId).then(function (snapshots) { + var html = ''; + if (snapshots.length === 0) { + html = 'No snapshot found...'; + } else { + var sessionItemTemplate = pskl.utils.Template.get('snapshot-list-item'); + var html = ''; + snapshots.forEach(function (snapshot) { + snapshot.date = pskl.utils.DateUtils.format(snapshot.date, '{{Y}}/{{M}}/{{D}} {{H}}:{{m}}'); + html += pskl.utils.Template.replace(sessionItemTemplate, snapshot); + }); + } + this.container.querySelector('.snapshot-list').innerHTML = html; + }.bind(this)); + }; + + ns.SessionDetails.prototype.onBackClick_ = function () { + this.backupsController.back(this); + }; + + ns.SessionDetails.prototype.onContainerClick_ = function (evt) { + var action = evt.target.dataset.action; + if (action == 'load') { + var snapshotId = evt.target.dataset.snapshotId * 1; + pskl.app.backupService.loadSnapshotById(snapshotId).then(function () { + $.publish(Events.DIALOG_HIDE); + }); + } + }; +})(); diff --git a/src/js/controller/settings/ImportController.js b/src/js/controller/settings/ImportController.js index 85714eb6..5056efc3 100644 --- a/src/js/controller/settings/ImportController.js +++ b/src/js/controller/settings/ImportController.js @@ -14,6 +14,7 @@ this.hiddenOpenPiskelInput = document.querySelector('[name="open-piskel-input"]'); this.addEventListener('.browse-local-button', 'click', this.onBrowseLocalClick_); + this.addEventListener('.browse-backups-button', 'click', this.onBrowseBackupsClick_); this.addEventListener('.file-input-button', 'click', this.onFileInputClick_); // different handlers, depending on the Environment @@ -23,25 +24,6 @@ this.addEventListener(this.hiddenOpenPiskelInput, 'change', this.onOpenPiskelChange_); this.addEventListener('.open-piskel-button', 'click', this.onOpenPiskelClick_); } - - this.initRestoreSession_(); - }; - - ns.ImportController.prototype.initRestoreSession_ = function () { - var previousSessionContainer = document.querySelector('.previous-session'); - pskl.app.backupService.getPreviousPiskelInfo().then(function (previousInfo) { - if (previousInfo) { - var previousSessionTemplate_ = pskl.utils.Template.get('previous-session-info-template'); - var date = pskl.utils.DateUtils.format(previousInfo.date, '{{H}}:{{m}} - {{Y}}/{{M}}/{{D}}'); - previousSessionContainer.innerHTML = pskl.utils.Template.replace(previousSessionTemplate_, { - name : previousInfo.name, - date : date - }); - this.addEventListener('.restore-session-button', 'click', this.onRestorePreviousSessionClick_); - } else { - previousSessionContainer.innerHTML = 'No piskel backup was found on this browser.'; - } - }.bind(this)); }; ns.ImportController.prototype.closeDrawer_ = function () { @@ -78,6 +60,13 @@ this.closeDrawer_(); }; + ns.ImportController.prototype.onBrowseBackupsClick_ = function (evt) { + $.publish(Events.DIALOG_SHOW, { + dialogId : 'browse-backups' + }); + this.closeDrawer_(); + }; + ns.ImportController.prototype.openPiskelFile_ = function (file) { if (this.isPiskel_(file)) { $.publish(Events.DIALOG_SHOW, { diff --git a/src/js/database/BackupDatabase.js b/src/js/database/BackupDatabase.js index 1bd7a38f..ce96787f 100644 --- a/src/js/database/BackupDatabase.js +++ b/src/js/database/BackupDatabase.js @@ -89,6 +89,18 @@ return _requestPromise(request); }; + /** + * Send a get request for the provided snapshotId. + * Returns a promise that resolves the request event. + */ + ns.BackupDatabase.prototype.getSnapshot = function (snapshotId) { + var objectStore = this.openObjectStore_(); + var request = objectStore.get(snapshotId); + return _requestPromise(request).then(function (event) { + return event.target.result; + }); + }; + /** * Get the last (most recent) snapshot that satisfies the accept filter provided. * Returns a promise that will resolve with the first matching snapshot (or null @@ -175,6 +187,7 @@ startDate: snapshot.date, endDate: snapshot.date, name: snapshot.name, + description: snapshot.description, id: snapshot.session_id }; }; @@ -183,8 +196,12 @@ var s = sessions[snapshot.session_id]; s.startDate = Math.min(s.startDate, snapshot.date); s.endDate = Math.max(s.endDate, snapshot.date); - if (s.endDate === snapshot.endDate) { + + if (s.endDate === snapshot.date) { + // If the endDate was updated, update also the session metadata to + // reflect the latest state. s.name = snapshot.name; + s.description = snapshot.description; } }; diff --git a/src/js/model/Piskel.js b/src/js/model/Piskel.js index 75e8af7c..629eb126 100644 --- a/src/js/model/Piskel.js +++ b/src/js/model/Piskel.js @@ -16,9 +16,6 @@ this.descriptor = descriptor; this.savePath = null; this.fps = fps; - // This id is used to keep track of sessions in the BackupService. - this.sessionId = pskl.utils.Uuid.generate(); - } else { throw 'Missing arguments in Piskel constructor : ' + Array.prototype.join.call(arguments, ','); } diff --git a/src/js/service/BackupService.js b/src/js/service/BackupService.js index d925078c..3d33fda3 100644 --- a/src/js/service/BackupService.js +++ b/src/js/service/BackupService.js @@ -14,7 +14,9 @@ ns.BackupService = function (piskelController, backupDatabase) { this.piskelController = piskelController; - this.lastHash = null; + // Immediately store the current when initializing the Service to avoid storing + // empty sessions. + this.lastHash = this.piskelController.getPiskel().getHash(); this.nextSnapshotDate = -1; // backupDatabase can be provided for testing purposes. @@ -49,14 +51,14 @@ var descriptor = piskel.getDescriptor(); var date = this.currentDate_(); var snapshot = { - session_id: piskel.sessionId, + session_id: pskl.app.sessionId, date: date, name: descriptor.name, description: descriptor.description, serialized: pskl.utils.serialization.Serializer.serialize(piskel) }; - return this.backupDatabase.getSnapshotsBySessionId(piskel.sessionId).then(function (snapshots) { + return this.getSnapshotsBySessionId(pskl.app.sessionId).then(function (snapshots) { var latest = snapshots[0]; if (latest && date < this.nextSnapshotDate) { @@ -87,7 +89,7 @@ return s1.startDate - s2.startDate; })[0].id; - return this.backupDatabase.deleteSnapshotsForSession(oldestSession); + return this.deleteSession(oldestSession); }.bind(this)); }.bind(this)); } @@ -96,21 +98,54 @@ }); }; + ns.BackupService.prototype.getSnapshotsBySessionId = function (sessionId) { + return this.backupDatabase.getSnapshotsBySessionId(sessionId); + }; + + ns.BackupService.prototype.deleteSession = function (sessionId) { + return this.backupDatabase.deleteSnapshotsForSession(sessionId); + }; + ns.BackupService.prototype.getPreviousPiskelInfo = function () { - var sessionId = this.piskelController.getPiskel().sessionId; return this.backupDatabase.findLastSnapshot(function (snapshot) { - return snapshot.session_id !== sessionId; + return snapshot.session_id !== pskl.app.sessionId; }); }; + ns.BackupService.prototype.list = function() { + return this.backupDatabase.getSessions(); + }; + + ns.BackupService.prototype.loadSnapshotById = function(snapshotId) { + var deferred = Q.defer(); + + this.backupDatabase.getSnapshot(snapshotId).then(function (snapshot) { + pskl.utils.serialization.Deserializer.deserialize( + JSON.parse(snapshot.serialized), + function (piskel) { + pskl.app.piskelController.setPiskel(piskel); + deferred.resolve(); + } + ); + }); + + return deferred.promise; + }; + + // Load "latest" backup snapshot. ns.BackupService.prototype.load = function() { + var deferred = Q.defer(); + this.getPreviousPiskelInfo().then(function (snapshot) { pskl.utils.serialization.Deserializer.deserialize( JSON.parse(snapshot.serialized), function (piskel) { pskl.app.piskelController.setPiskel(piskel); + deferred.resolve(); } ); }); + + return deferred.promise; }; })(); diff --git a/src/piskel-script-list.js b/src/piskel-script-list.js index 1a6b2263..a747e61b 100644 --- a/src/piskel-script-list.js +++ b/src/piskel-script-list.js @@ -149,6 +149,9 @@ "js/controller/dialogs/CreatePaletteController.js", "js/controller/dialogs/BrowseLocalController.js", "js/controller/dialogs/CheatsheetController.js", + "js/controller/dialogs/backups/steps/SelectSession.js", + "js/controller/dialogs/backups/steps/SessionDetails.js", + "js/controller/dialogs/backups/BrowseBackups.js", "js/controller/dialogs/importwizard/steps/AbstractImportStep.js", "js/controller/dialogs/importwizard/steps/AdjustSize.js", "js/controller/dialogs/importwizard/steps/ImageImport.js", diff --git a/src/templates/dialogs/browse-backups.html b/src/templates/dialogs/browse-backups.html new file mode 100644 index 00000000..52f5fdef --- /dev/null +++ b/src/templates/dialogs/browse-backups.html @@ -0,0 +1,43 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/templates/settings/import.html b/src/templates/settings/import.html index de9c9224..088563d6 100644 --- a/src/templates/settings/import.html +++ b/src/templates/settings/import.html @@ -38,16 +38,11 @@