add a browse backups dialog

This commit is contained in:
juliandescottes 2017-06-18 23:24:15 +02:00 committed by Julian Descottes
parent 30ea7fa079
commit ee45cdcc45
14 changed files with 327 additions and 41 deletions

View File

@ -75,9 +75,10 @@
rel="tooltip" data-placement="left" title="Performance problem detected, learn more.">&nbsp;</div>
<!-- dialogs partials -->
@@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', {})

View File

@ -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();

View File

@ -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
}
};

View File

@ -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;
};
})();

View File

@ -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));
}
};
})();

View File

@ -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);
});
}
};
})();

View File

@ -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, {

View File

@ -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;
}
};

View File

@ -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, ',');
}

View File

@ -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;
};
})();

View File

@ -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",

View File

@ -0,0 +1,43 @@
<script type="text/template" id="templates/dialogs/browse-backups.html">
<div class="dialog-wrapper">
<h3 class="dialog-head">
Browse backups
<span class="dialog-close">X</span>
</h3>
<div class="dialog-content backups-wizard-container"></div>
<div class="browse-backups-disclaimer"></div>
</div>
</script>
<script type="text/template" id="backups-select-session">
<div class="backups-step-container">
<div class="backups-step-content">
<div class="session-list">
</div>
</div>
</div>
</script>
<script type="text/template" id="session-list-item">
<div class="session-item">
{{name}} - {{description}}
<button class="button" data-session-id="{{id}}" data-action="delete">Delete</button>
<button class="button button-primary" data-session-id="{{id}}" data-action="view">View</button>
</div>
</script>
<script type="text/template" id="backups-session-details">
<div class="backups-step-container">
<div class="backups-step-content">
<div class="snapshot-list"></div>
</div>
<button class="button back-button">back</button>
</div>
</script>
<script type="text/template" id="snapshot-list-item">
<div class="snapshot-item">
{{name}} - {{description}}. Snapshot taken at {{date}}.
<button class="button button-primary" data-action="load" data-snapshot-id="{{id}}">Load</button>
</div>
</script>

View File

@ -38,16 +38,11 @@
<div class="settings-title">
Recover recent sessions
</div>
<div class="settings-item previous-session">
</div>
</div>
</script>
<script type="text/template" id="previous-session-info-template">
<div>
Restore a backup of <span class="highlight">{{name}}</span>, saved at <span style="color:white">{{date}}</span> ?
<div style="margin-top:10px;">
<button type="button" class="button button-primary restore-session-button">Restore</button>
<div class="settings-item">
Browse backups of previous sessions.
<div style="margin-top:10px;">
<button type="button" class="button button-primary browse-backups-button">Browse backups</button>
</div>
</div>
</div>
</script>

View File

@ -74,7 +74,10 @@ describe('BackupService test', function () {
};
var preparePiskelMocks = function (session_id, name, description, hash, serialized) {
mockPiskel.sessionId = session_id;
// Update the session id.
pskl.app.sessionId = session_id;
// Update the piskel mock.
mockPiskel._descriptor.name = name;
mockPiskel._descriptor.description = description;
mockPiskel._hash = hash;