Session history
This commit is contained in:
@@ -22,7 +22,9 @@ angular.module('app')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager, syncManager, modelManager, themeManager, componentManager, storageManager) {
|
||||
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager, syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory) {
|
||||
|
||||
this.showSessionHistory = true;
|
||||
|
||||
this.spellcheck = true;
|
||||
this.componentManager = componentManager;
|
||||
|
||||
@@ -7,7 +7,7 @@ class MenuRow {
|
||||
this.scope = {
|
||||
circle: "=",
|
||||
label: "=",
|
||||
subtite: "=",
|
||||
subtitle: "=",
|
||||
hasButton: "=",
|
||||
buttonText: "=",
|
||||
buttonClass: "=",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
class RevisionPreviewModal {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/revision-preview-modal.html";
|
||||
this.scope = {
|
||||
revision: "=",
|
||||
show: "=",
|
||||
callback: "="
|
||||
};
|
||||
}
|
||||
|
||||
link($scope, el, attrs) {
|
||||
|
||||
$scope.dismiss = function() {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
|
||||
controller($scope, modelManager, syncManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.restore = function(asCopy) {
|
||||
if(!asCopy && !confirm("Are you sure you want to replace the current note's contents with what you see in this preview?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var item;
|
||||
if(asCopy) {
|
||||
var contentCopy = Object.assign({}, $scope.revision.content);
|
||||
if(contentCopy.title) { contentCopy.title += " (copy)"; }
|
||||
item = modelManager.createItem({content_type: "Note", content: contentCopy});
|
||||
modelManager.addItem(item);
|
||||
} else {
|
||||
item = modelManager.findItem($scope.revision.itemUuid);
|
||||
item.content = Object.assign({}, $scope.revision.content);
|
||||
item.mapContentToLocalProperties(item.content);
|
||||
}
|
||||
item.setDirty(true);
|
||||
syncManager.sync();
|
||||
|
||||
$scope.dismiss();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('revisionPreviewModal', () => new RevisionPreviewModal);
|
||||
@@ -0,0 +1,72 @@
|
||||
class SessionHistoryMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/session-history-menu.html";
|
||||
this.scope = {
|
||||
item: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, modelManager, sessionHistory, actionsManager, $timeout) {
|
||||
'ngInject';
|
||||
|
||||
$scope.diskEnabled = sessionHistory.diskEnabled;
|
||||
|
||||
$scope.reloadHistory = function() {
|
||||
$scope.history = sessionHistory.historyForItem($scope.item);
|
||||
}
|
||||
$scope.reloadHistory();
|
||||
|
||||
$scope.openRevision = function(revision) {
|
||||
actionsManager.presentRevisionPreviewModal(revision);
|
||||
}
|
||||
|
||||
$scope.classForRevision = function(revision) {
|
||||
var vector = revision.operationVector();
|
||||
if(vector == 0) {
|
||||
return "default";
|
||||
} else if(vector == 1) {
|
||||
return "success";
|
||||
} else if(vector == -1) {
|
||||
return "danger";
|
||||
}
|
||||
}
|
||||
|
||||
$scope.clearItemHistory = function() {
|
||||
if(!confirm("Are you sure you want to delete the local session history for this note?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessionHistory.clearItemHistory($scope.item).then(() => {
|
||||
$timeout(() => {
|
||||
$scope.reloadHistory();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
$scope.clearAllHistory = function() {
|
||||
if(!confirm("Are you sure you want to delete the local session history for all notes?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessionHistory.clearAllHistory().then(() => {
|
||||
$timeout(() => {
|
||||
$scope.reloadHistory();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
$scope.toggleDiskSaving = function() {
|
||||
sessionHistory.toggleDiskSaving().then(() => {
|
||||
$timeout(() => {
|
||||
$scope.diskEnabled = sessionHistory.diskEnabled;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('sessionHistoryMenu', () => new SessionHistoryMenu);
|
||||
@@ -198,8 +198,14 @@ class ActionsManager {
|
||||
})
|
||||
}
|
||||
|
||||
presentPasswordModal(callback) {
|
||||
presentRevisionPreviewModal(revision) {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.revision = revision;
|
||||
var el = this.$compile( "<revision-preview-modal revision='revision' callback='callback' class='modal'></revision-preview-modal>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
presentPasswordModal(callback) {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.type = "password";
|
||||
scope.title = "Decryption Assistance";
|
||||
|
||||
213
app/assets/javascripts/app/services/sessionHistory.js
Normal file
213
app/assets/javascripts/app/services/sessionHistory.js
Normal file
@@ -0,0 +1,213 @@
|
||||
class SessionHistory {
|
||||
|
||||
constructor(modelManager, storageManager, authManager, passcodeManager, $timeout) {
|
||||
this.modelManager = modelManager;
|
||||
this.storageManager = storageManager;
|
||||
this.authManager = authManager;
|
||||
this.passcodeManager = passcodeManager;
|
||||
this.$timeout = $timeout;
|
||||
|
||||
this.loadFromDisk().then(() => {
|
||||
this.modelManager.addItemSyncObserver("session-history", "Note", (allItems, validItems, deletedItems, source, sourceKey) => {
|
||||
for(let item of allItems) {
|
||||
this.addRevision(item);
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
async encryptionParams() {
|
||||
let offline = this.authManager.offline();
|
||||
let auth_params = offline ? this.passcodeManager.passcodeAuthParams() : await this.authManager.getAuthParams();
|
||||
let keys = offline ? this.passcodeManager.keys() : await this.authManager.keys();
|
||||
return {keys, auth_params};
|
||||
}
|
||||
|
||||
addRevision(item) {
|
||||
var added = this.historyContainer.addRevision(item);
|
||||
|
||||
if(added) {
|
||||
if(this.diskTimeout) {this.$timeout.cancel(this.diskTimeout)};
|
||||
this.diskTimeout = this.$timeout(() => {
|
||||
this.saveToDisk();
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
historyForItem(item) {
|
||||
return this.historyContainer.historyForItem(item);
|
||||
}
|
||||
|
||||
async clearItemHistory(item) {
|
||||
delete this.historyContainer.clearItemHistory(item);
|
||||
return this.saveToDisk();
|
||||
}
|
||||
|
||||
async clearAllHistory() {
|
||||
this.historyContainer.clearAllHistory();
|
||||
return this.storageManager.removeItem("sessionHistory");
|
||||
}
|
||||
|
||||
async toggleDiskSaving() {
|
||||
this.diskEnabled = !this.diskEnabled;
|
||||
|
||||
if(this.diskEnabled) {
|
||||
this.storageManager.setItem("persistSessionHistory", JSON.stringify(true));
|
||||
this.saveToDisk();
|
||||
} else {
|
||||
this.storageManager.removeItem("persistSessionHistory");
|
||||
}
|
||||
}
|
||||
|
||||
async saveToDisk() {
|
||||
if(!this.diskEnabled) {
|
||||
return;
|
||||
}
|
||||
let encryptionParams = await this.encryptionParams();
|
||||
var itemParams = new SFItemParams(this.historyContainer, encryptionParams.keys, encryptionParams.auth_params);
|
||||
itemParams.paramsForSync().then((syncParams) => {
|
||||
console.log("Saving to disk", syncParams);
|
||||
this.storageManager.setItem("sessionHistory", JSON.stringify(syncParams));
|
||||
})
|
||||
}
|
||||
|
||||
async loadFromDisk() {
|
||||
var diskValue = await this.storageManager.getItem("persistSessionHistory");
|
||||
if(diskValue) {
|
||||
this.diskEnabled = JSON.parse(diskValue);
|
||||
}
|
||||
|
||||
var historyValue = await this.storageManager.getItem("sessionHistory");
|
||||
if(historyValue) {
|
||||
historyValue = JSON.parse(historyValue);
|
||||
let encryptionParams = await this.encryptionParams();
|
||||
await SFJS.itemTransformer.decryptItem(historyValue, encryptionParams.keys);
|
||||
var historyContainer = new HistoryContainer(historyValue);
|
||||
this.historyContainer = historyContainer;
|
||||
} else {
|
||||
this.historyContainer = new HistoryContainer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryContainer extends SFItem {
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
|
||||
if(!this.content.itemsDictionary) {
|
||||
this.content.itemsDictionary = {};
|
||||
}
|
||||
|
||||
var objectKeys = Object.keys(this.content.itemsDictionary);
|
||||
objectKeys.forEach((key) => {
|
||||
var value = this.content.itemsDictionary[key];
|
||||
this.content.itemsDictionary[key] = new ItemHistory(value);
|
||||
});
|
||||
}
|
||||
|
||||
addRevision(item) {
|
||||
if(!this.content.itemsDictionary[item.uuid]) {
|
||||
this.content.itemsDictionary[item.uuid] = new ItemHistory();
|
||||
}
|
||||
var itemHistory = this.content.itemsDictionary[item.uuid];
|
||||
return itemHistory.addRevision(item);
|
||||
}
|
||||
|
||||
historyForItem(item) {
|
||||
return this.content.itemsDictionary[item.uuid];
|
||||
}
|
||||
|
||||
clearItemHistory(item) {
|
||||
delete this.content.itemsDictionary[item.uuid];
|
||||
}
|
||||
|
||||
clearAllHistory() {
|
||||
this.content.itemsDictionary = {};
|
||||
}
|
||||
}
|
||||
|
||||
class ItemHistory {
|
||||
|
||||
constructor(json_obj = {}) {
|
||||
if(!this.revisions) {
|
||||
this.revisions = [];
|
||||
}
|
||||
if(json_obj.revisions) {
|
||||
for(var revision of json_obj.revisions) {
|
||||
this.revisions.push(new NoteRevision(revision, this.revisions[this.revisions.length - 1], revision.date));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addRevision(item) {
|
||||
var previousRevision = this.revisions[this.revisions.length - 1];
|
||||
var prospectiveRevision = new NoteRevision(item, previousRevision, item.updated_at);
|
||||
if(prospectiveRevision.isSameAsRevision(previousRevision)) {
|
||||
return;
|
||||
}
|
||||
this.revisions.push(prospectiveRevision);
|
||||
return prospectiveRevision;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ItemRevision {
|
||||
|
||||
constructor(item, previousRevision, date) {
|
||||
if(typeof(date) == "string") {
|
||||
this.date = new Date(date);
|
||||
} else {
|
||||
this.date = date;
|
||||
}
|
||||
this.itemUuid = item.uuid;
|
||||
this.hasPreviousRevision = previousRevision != null;
|
||||
this.content = Object.assign({}, item.content);
|
||||
}
|
||||
|
||||
isSameAsRevision(revision) {
|
||||
if(!revision) {
|
||||
return false;
|
||||
}
|
||||
return JSON.stringify(this.content) === JSON.stringify(revision.content);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NoteRevision extends ItemRevision {
|
||||
constructor(item, previousRevision, date) {
|
||||
super(item, previousRevision, date);
|
||||
if(previousRevision) {
|
||||
this.textCharDiffLength = this.content.text.length - previousRevision.content.text.length;
|
||||
} else {
|
||||
this.textCharDiffLength = this.content.text.length;
|
||||
}
|
||||
}
|
||||
|
||||
previewTitle() {
|
||||
return this.date.toLocaleString();
|
||||
}
|
||||
|
||||
operationVector() {
|
||||
if(!this.hasPreviousRevision || this.textCharDiffLength == 0) {
|
||||
return 0;
|
||||
} else if(this.textCharDiffLength < 0) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
previewSubTitle() {
|
||||
if(!this.hasPreviousRevision) {
|
||||
return `${this.textCharDiffLength} characters loaded`
|
||||
} else if(this.textCharDiffLength < 0) {
|
||||
return `${this.textCharDiffLength * -1} characters removed`
|
||||
} else if(this.textCharDiffLength > 0) {
|
||||
return `${this.textCharDiffLength} characters added`
|
||||
} else {
|
||||
return "Title changed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app').service('sessionHistory', SessionHistory);
|
||||
Reference in New Issue
Block a user