Refactors most controllers and directives into classes for more organized and maintainable code
This commit is contained in:
279
app/assets/javascripts/services/actionsManager.js
Normal file
279
app/assets/javascripts/services/actionsManager.js
Normal file
@@ -0,0 +1,279 @@
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import { Action, SFModelManager, SFItemParams, protocolManager } from 'snjs';
|
||||
|
||||
export class ActionsManager {
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$compile,
|
||||
$rootScope,
|
||||
$timeout,
|
||||
alertManager,
|
||||
authManager,
|
||||
httpManager,
|
||||
modelManager,
|
||||
syncManager,
|
||||
) {
|
||||
this.$compile = $compile;
|
||||
this.$rootScope = $rootScope;
|
||||
this.$timeout = $timeout;
|
||||
this.alertManager = alertManager;
|
||||
this.authManager = authManager;
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
/* Used when decrypting old items with new keys. This array is only kept in memory. */
|
||||
this.previousPasswords = [];
|
||||
}
|
||||
|
||||
get extensions() {
|
||||
return this.modelManager.validItemsForContentType('Extension');
|
||||
}
|
||||
|
||||
extensionsInContextOfItem(item) {
|
||||
return this.extensions.filter((ext) => {
|
||||
return _.includes(ext.supported_types, item.content_type) ||
|
||||
ext.actionsWithContextForItem(item).length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an extension in the context of a certain item.
|
||||
* The server then has the chance to respond with actions that are
|
||||
* relevant just to this item. The response extension is not saved,
|
||||
* just displayed as a one-time thing.
|
||||
*/
|
||||
async loadExtensionInContextOfItem(extension, item) {
|
||||
const params = {
|
||||
content_type: item.content_type,
|
||||
item_uuid: item.uuid
|
||||
};
|
||||
const emptyFunc = () => { };
|
||||
return this.httpManager.getAbsolute(extension.url, params, emptyFunc).then((response) => {
|
||||
this.updateExtensionFromRemoteResponse(extension, response);
|
||||
return extension;
|
||||
}).catch((response) => {
|
||||
console.error("Error loading extension", response);
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
updateExtensionFromRemoteResponse(extension, response) {
|
||||
if (response.description) {
|
||||
extension.description = response.description;
|
||||
}
|
||||
if (response.supported_types) {
|
||||
extension.supported_types = response.supported_types;
|
||||
}
|
||||
if (response.actions) {
|
||||
extension.actions = response.actions.map((action) => {
|
||||
return new Action(action);
|
||||
})
|
||||
} else {
|
||||
extension.actions = [];
|
||||
}
|
||||
}
|
||||
|
||||
async executeAction(action, extension, item) {
|
||||
action.running = true;
|
||||
let result;
|
||||
switch (action.verb) {
|
||||
case 'get':
|
||||
result = await this.handleGetAction(action);
|
||||
break;
|
||||
case 'render':
|
||||
result = await this.handleRenderAction(action);
|
||||
break;
|
||||
case 'show':
|
||||
result = await this.handleShowAction(action);
|
||||
break;
|
||||
case 'post':
|
||||
result = await this.handlePostAction(action, item, extension);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
action.lastExecuted = new Date();
|
||||
action.running = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
async decryptResponse(response, keys) {
|
||||
const responseItem = response.item;
|
||||
await protocolManager.decryptItem(responseItem, keys);
|
||||
if (!responseItem.errorDecrypting) {
|
||||
return {
|
||||
response: response,
|
||||
item: responseItem
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.auth_params) {
|
||||
/**
|
||||
* In some cases revisions were missing auth params.
|
||||
* Instruct the user to email us to get this remedied.
|
||||
*/
|
||||
this.alertManager.alert({
|
||||
text: `We were unable to decrypt this revision using your current keys,
|
||||
and this revision is missing metadata that would allow us to try different
|
||||
keys to decrypt it. This can likely be fixed with some manual intervention.
|
||||
Please email hello@standardnotes.org for assistance.`
|
||||
});
|
||||
return {};
|
||||
}
|
||||
|
||||
/* Try previous passwords */
|
||||
const triedPasswords = [];
|
||||
for (const passwordCandidate of this.previousPasswords) {
|
||||
if (triedPasswords.includes(passwordCandidate)) {
|
||||
continue;
|
||||
}
|
||||
triedPasswords.push(passwordCandidate);
|
||||
const keyResults = await protocolManager.computeEncryptionKeysForUser(
|
||||
passwordCandidate,
|
||||
response.auth_params
|
||||
);
|
||||
if (!keyResults) {
|
||||
continue;
|
||||
}
|
||||
const nestedResponse = await this.decryptResponse(
|
||||
response,
|
||||
keyResults
|
||||
);
|
||||
if (nestedResponse.item) {
|
||||
return nestedResponse;
|
||||
}
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.presentPasswordModal((password) => {
|
||||
this.previousPasswords.push(password);
|
||||
const result = this.decryptResponse(response, keys);
|
||||
resolve(result);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
async handlePostAction(action, item, extension) {
|
||||
const decrypted = action.access_type === 'decrypted';
|
||||
const itemParams = await this.outgoingParamsForItem(item, extension, decrypted);
|
||||
const params = {
|
||||
items: [itemParams]
|
||||
}
|
||||
const emptyFunc = () => { };
|
||||
return this.httpManager.postAbsolute(action.url, params, emptyFunc).then((response) => {
|
||||
action.error = false;
|
||||
return {response: response};
|
||||
}).catch((response) => {
|
||||
action.error = true;
|
||||
console.error("Action error response:", response);
|
||||
this.alertManager.alert({
|
||||
text: "An issue occurred while processing this action. Please try again."
|
||||
});
|
||||
return { response: response };
|
||||
})
|
||||
}
|
||||
|
||||
async handleShowAction(action) {
|
||||
const win = window.open(action.url, '_blank');
|
||||
if (win) {
|
||||
win.focus();
|
||||
}
|
||||
return { response: null };
|
||||
}
|
||||
|
||||
async handleGetAction(action) {
|
||||
const emptyFunc = () => {};
|
||||
const onConfirm = async () => {
|
||||
return this.httpManager.getAbsolute(action.url, {}, emptyFunc)
|
||||
.then(async (response) => {
|
||||
action.error = false;
|
||||
await this.decryptResponse(response, await this.authManager.keys());
|
||||
const items = await this.modelManager.mapResponseItemsToLocalModels(
|
||||
[response.item],
|
||||
SFModelManager.MappingSourceRemoteActionRetrieved
|
||||
);
|
||||
for (const mappedItem of items) {
|
||||
this.modelManager.setItemDirty(mappedItem, true);
|
||||
}
|
||||
this.syncManager.sync();
|
||||
return {
|
||||
response: response,
|
||||
item: response.item
|
||||
};
|
||||
}).catch((response) => {
|
||||
const error = (response && response.error)
|
||||
|| { message: "An issue occurred while processing this action. Please try again." }
|
||||
this.alertManager.alert({ text: error.message });
|
||||
action.error = true;
|
||||
return { error: error };
|
||||
})
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.alertManager.confirm({
|
||||
text: "Are you sure you want to replace the current note contents with this action's results?",
|
||||
onConfirm: () => {
|
||||
onConfirm().then(resolve)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async handleRenderAction(action) {
|
||||
const emptyFunc = () => {};
|
||||
return this.httpManager.getAbsolute(action.url, {}, emptyFunc).then(async (response) => {
|
||||
action.error = false;
|
||||
const result = await this.decryptResponse(response, await this.authManager.keys());
|
||||
const item = this.modelManager.createItem(result.item);
|
||||
return {
|
||||
response: result.response,
|
||||
item: item
|
||||
};
|
||||
}).catch((response) => {
|
||||
const error = (response && response.error)
|
||||
|| { message: "An issue occurred while processing this action. Please try again." }
|
||||
this.alertManager.alert({ text: error.message });
|
||||
action.error = true;
|
||||
return { error: error };
|
||||
})
|
||||
}
|
||||
|
||||
async outgoingParamsForItem(item, extension, decrypted = false) {
|
||||
let keys = await this.authManager.keys();
|
||||
if (decrypted) {
|
||||
keys = null;
|
||||
}
|
||||
const itemParams = new SFItemParams(
|
||||
item,
|
||||
keys,
|
||||
await this.authManager.getAuthParams()
|
||||
);
|
||||
return itemParams.paramsForExtension();
|
||||
}
|
||||
|
||||
presentRevisionPreviewModal(uuid, content) {
|
||||
const scope = this.$rootScope.$new(true);
|
||||
scope.uuid = uuid;
|
||||
scope.content = content;
|
||||
const el = this.$compile(
|
||||
`<revision-preview-modal uuid='uuid' content='content'
|
||||
class='sk-modal'></revision-preview-modal>`
|
||||
)(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
presentPasswordModal(callback) {
|
||||
const scope = this.$rootScope.$new(true);
|
||||
scope.type = "password";
|
||||
scope.title = "Decryption Assistance";
|
||||
scope.message = `Unable to decrypt this item with your current keys.
|
||||
Please enter your account password at the time of this revision.`;
|
||||
scope.callback = callback;
|
||||
const el = this.$compile(
|
||||
`<input-modal type='type' message='message'
|
||||
title='title' callback='callback'></input-modal>`
|
||||
)(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
}
|
||||
71
app/assets/javascripts/services/alertManager.js
Normal file
71
app/assets/javascripts/services/alertManager.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { SFAlertManager } from 'snjs';
|
||||
import { SKAlert } from 'sn-stylekit';
|
||||
|
||||
export class AlertManager extends SFAlertManager {
|
||||
/* @ngInject */
|
||||
constructor($timeout) {
|
||||
super();
|
||||
this.$timeout = $timeout;
|
||||
}
|
||||
|
||||
async alert({
|
||||
title,
|
||||
text,
|
||||
closeButtonText = "OK",
|
||||
onClose} = {}
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const buttons = [
|
||||
{
|
||||
text: closeButtonText,
|
||||
style: "neutral",
|
||||
action: async () => {
|
||||
if(onClose) {
|
||||
this.$timeout(onClose);
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
const alert = new SKAlert({title, text, buttons});
|
||||
alert.present();
|
||||
});
|
||||
}
|
||||
|
||||
async confirm({
|
||||
title,
|
||||
text,
|
||||
confirmButtonText = "Confirm",
|
||||
cancelButtonText = "Cancel",
|
||||
onConfirm,
|
||||
onCancel,
|
||||
destructive = false
|
||||
} = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const buttons = [
|
||||
{
|
||||
text: cancelButtonText,
|
||||
style: "neutral",
|
||||
action: async () => {
|
||||
if(onCancel) {
|
||||
this.$timeout(onCancel);
|
||||
}
|
||||
reject(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
text: confirmButtonText,
|
||||
style: destructive ? "danger" : "info",
|
||||
action: async () => {
|
||||
if(onConfirm) {
|
||||
this.$timeout(onConfirm);
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
},
|
||||
];
|
||||
const alert = new SKAlert({title, text, buttons});
|
||||
alert.present();
|
||||
});
|
||||
}
|
||||
}
|
||||
157
app/assets/javascripts/services/archiveManager.js
Normal file
157
app/assets/javascripts/services/archiveManager.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import { PrivilegesManager } from '@/services/privilegesManager';
|
||||
|
||||
export class ArchiveManager {
|
||||
/* @ngInject */
|
||||
constructor(passcodeManager, authManager, modelManager, privilegesManager) {
|
||||
this.passcodeManager = passcodeManager;
|
||||
this.authManager = authManager;
|
||||
this.modelManager = modelManager;
|
||||
this.privilegesManager = privilegesManager;
|
||||
}
|
||||
|
||||
/*
|
||||
Public
|
||||
*/
|
||||
|
||||
async downloadBackup(encrypted) {
|
||||
return this.downloadBackupOfItems(this.modelManager.allItems, encrypted);
|
||||
}
|
||||
|
||||
async downloadBackupOfItems(items, encrypted) {
|
||||
const run = async () => {
|
||||
// download in Standard Notes format
|
||||
let keys, authParams;
|
||||
if(encrypted) {
|
||||
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
||||
keys = this.passcodeManager.keys();
|
||||
authParams = this.passcodeManager.passcodeAuthParams();
|
||||
} else {
|
||||
keys = await this.authManager.keys();
|
||||
authParams = await this.authManager.getAuthParams();
|
||||
}
|
||||
}
|
||||
this.__itemsData(items, keys, authParams).then((data) => {
|
||||
const modifier = encrypted ? "Encrypted" : "Decrypted";
|
||||
this.__downloadData(data, `Standard Notes ${modifier} Backup - ${this.__formattedDate()}.txt`);
|
||||
|
||||
// download as zipped plain text files
|
||||
if(!keys) {
|
||||
this.__downloadZippedItems(items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if(await this.privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
|
||||
this.privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
|
||||
run();
|
||||
});
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Private
|
||||
*/
|
||||
|
||||
__formattedDate() {
|
||||
var string = `${new Date()}`;
|
||||
// Match up to the first parenthesis, i.e do not include '(Central Standard Time)'
|
||||
var matches = string.match(/^(.*?) \(/);
|
||||
if(matches.length >= 2) {
|
||||
return matches[1]
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
async __itemsData(items, keys, authParams) {
|
||||
const data = await this.modelManager.getJSONDataForItems(items, keys, authParams);
|
||||
const blobData = new Blob([data], {type: 'text/json'});
|
||||
return blobData;
|
||||
}
|
||||
|
||||
__loadZip(callback) {
|
||||
if(window.zip) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var scriptTag = document.createElement('script');
|
||||
scriptTag.src = "/assets/zip/zip.js";
|
||||
scriptTag.async = false;
|
||||
var headTag = document.getElementsByTagName('head')[0];
|
||||
headTag.appendChild(scriptTag);
|
||||
scriptTag.onload = function() {
|
||||
zip.workerScriptsPath = "assets/zip/";
|
||||
callback();
|
||||
};
|
||||
}
|
||||
|
||||
__downloadZippedItems(items) {
|
||||
this.__loadZip(() => {
|
||||
zip.createWriter(new zip.BlobWriter("application/zip"), (zipWriter) => {
|
||||
var index = 0;
|
||||
|
||||
const nextFile = () => {
|
||||
var item = items[index];
|
||||
var name, contents;
|
||||
|
||||
if(item.content_type === "Note") {
|
||||
name = item.content.title;
|
||||
contents = item.content.text;
|
||||
} else {
|
||||
name = item.content_type;
|
||||
contents = JSON.stringify(item.content, null, 2);
|
||||
}
|
||||
|
||||
if(!name) {
|
||||
name = "";
|
||||
}
|
||||
|
||||
const blob = new Blob([contents], {type: 'text/plain'});
|
||||
let filePrefix = name.replace(/\//g, "").replace(/\\+/g, "");
|
||||
const fileSuffix = `-${item.uuid.split("-")[0]}.txt`;
|
||||
// Standard max filename length is 255. Slice the note name down to allow filenameEnd
|
||||
filePrefix = filePrefix.slice(0, (255 - fileSuffix.length));
|
||||
const fileName = `${item.content_type}/${filePrefix}${fileSuffix}`
|
||||
zipWriter.add(fileName, new zip.BlobReader(blob), () => {
|
||||
index++;
|
||||
if(index < items.length) {
|
||||
nextFile();
|
||||
} else {
|
||||
zipWriter.close((blob) => {
|
||||
this.__downloadData(blob, `Standard Notes Backup - ${this.__formattedDate()}.zip`);
|
||||
zipWriter = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
nextFile();
|
||||
}, onerror);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
__hrefForData(data) {
|
||||
// If we are replacing a previously generated file we need to
|
||||
// manually revoke the object URL to avoid memory leaks.
|
||||
if (this.textFile !== null) {
|
||||
window.URL.revokeObjectURL(this.textFile);
|
||||
}
|
||||
|
||||
this.textFile = window.URL.createObjectURL(data);
|
||||
|
||||
// returns a URL you can use as a href
|
||||
return this.textFile;
|
||||
}
|
||||
|
||||
__downloadData(data, fileName) {
|
||||
var link = document.createElement('a');
|
||||
link.setAttribute('download', fileName);
|
||||
link.href = this.__hrefForData(data);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
140
app/assets/javascripts/services/authManager.js
Normal file
140
app/assets/javascripts/services/authManager.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import angular from 'angular';
|
||||
import { StorageManager } from './storageManager';
|
||||
import { protocolManager, SFAuthManager } from 'snjs';
|
||||
|
||||
export class AuthManager extends SFAuthManager {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
modelManager,
|
||||
singletonManager,
|
||||
storageManager,
|
||||
dbManager,
|
||||
httpManager,
|
||||
$rootScope,
|
||||
$timeout,
|
||||
$compile
|
||||
) {
|
||||
super(storageManager, httpManager, null, $timeout);
|
||||
this.$rootScope = $rootScope;
|
||||
this.$compile = $compile;
|
||||
this.modelManager = modelManager;
|
||||
this.singletonManager = singletonManager;
|
||||
this.storageManager = storageManager;
|
||||
this.dbManager = dbManager;
|
||||
}
|
||||
|
||||
loadInitialData() {
|
||||
const userData = this.storageManager.getItemSync("user");
|
||||
if(userData) {
|
||||
this.user = JSON.parse(userData);
|
||||
} else {
|
||||
// legacy, check for uuid
|
||||
const idData = this.storageManager.getItemSync("uuid");
|
||||
if(idData) {
|
||||
this.user = {uuid: idData};
|
||||
}
|
||||
}
|
||||
this.checkForSecurityUpdate();
|
||||
}
|
||||
|
||||
offline() {
|
||||
return !this.user;
|
||||
}
|
||||
|
||||
isEphemeralSession() {
|
||||
if(this.ephemeral == null || this.ephemeral == undefined) {
|
||||
this.ephemeral = JSON.parse(this.storageManager.getItemSync("ephemeral", StorageManager.Fixed));
|
||||
}
|
||||
return this.ephemeral;
|
||||
}
|
||||
|
||||
setEphemeral(ephemeral) {
|
||||
this.ephemeral = ephemeral;
|
||||
if(ephemeral) {
|
||||
this.storageManager.setModelStorageMode(StorageManager.Ephemeral);
|
||||
this.storageManager.setItemsMode(StorageManager.Ephemeral);
|
||||
} else {
|
||||
this.storageManager.setModelStorageMode(StorageManager.Fixed);
|
||||
this.storageManager.setItemsMode(this.storageManager.bestStorageMode());
|
||||
this.storageManager.setItem("ephemeral", JSON.stringify(false), StorageManager.Fixed);
|
||||
}
|
||||
}
|
||||
|
||||
async getAuthParamsForEmail(url, email, extraParams) {
|
||||
return super.getAuthParamsForEmail(url, email, extraParams);
|
||||
}
|
||||
|
||||
async login(url, email, password, ephemeral, strictSignin, extraParams) {
|
||||
return super.login(url, email, password, strictSignin, extraParams).then((response) => {
|
||||
if(!response.error) {
|
||||
this.setEphemeral(ephemeral);
|
||||
this.checkForSecurityUpdate();
|
||||
}
|
||||
|
||||
return response;
|
||||
})
|
||||
}
|
||||
|
||||
async register(url, email, password, ephemeral) {
|
||||
return super.register(url, email, password).then((response) => {
|
||||
if(!response.error) {
|
||||
this.setEphemeral(ephemeral);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
}
|
||||
|
||||
async changePassword(url, email, current_server_pw, newKeys, newAuthParams) {
|
||||
return super.changePassword(url, email, current_server_pw, newKeys, newAuthParams).then((response) => {
|
||||
if(!response.error) {
|
||||
this.checkForSecurityUpdate();
|
||||
}
|
||||
return response;
|
||||
})
|
||||
}
|
||||
|
||||
async handleAuthResponse(response, email, url, authParams, keys) {
|
||||
try {
|
||||
await super.handleAuthResponse(response, email, url, authParams, keys);
|
||||
this.user = response.user;
|
||||
this.storageManager.setItem("user", JSON.stringify(response.user));
|
||||
} catch (e) {
|
||||
this.dbManager.displayOfflineAlert();
|
||||
}
|
||||
}
|
||||
|
||||
async verifyAccountPassword(password) {
|
||||
let authParams = await this.getAuthParams();
|
||||
let keys = await protocolManager.computeEncryptionKeysForUser(password, authParams);
|
||||
let success = keys.mk === (await this.keys()).mk;
|
||||
return success;
|
||||
}
|
||||
|
||||
async checkForSecurityUpdate() {
|
||||
if(this.offline()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let latest = protocolManager.version();
|
||||
let updateAvailable = await this.protocolVersion() !== latest;
|
||||
if(updateAvailable !== this.securityUpdateAvailable) {
|
||||
this.securityUpdateAvailable = updateAvailable;
|
||||
this.$rootScope.$broadcast("security-update-status-changed");
|
||||
}
|
||||
|
||||
return this.securityUpdateAvailable;
|
||||
}
|
||||
|
||||
presentPasswordWizard(type) {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.type = type;
|
||||
var el = this.$compile( "<password-wizard type='type'></password-wizard>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
signOut() {
|
||||
super.signout();
|
||||
this.user = null;
|
||||
this._authParams = null;
|
||||
}
|
||||
}
|
||||
50
app/assets/javascripts/services/componentManager.js
Normal file
50
app/assets/javascripts/services/componentManager.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import angular from 'angular';
|
||||
import { SNComponentManager, SFAlertManager } from 'snjs';
|
||||
import { isDesktopApplication, getPlatformString } from '@/utils';
|
||||
|
||||
export class ComponentManager extends SNComponentManager {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
modelManager,
|
||||
syncManager,
|
||||
desktopManager,
|
||||
nativeExtManager,
|
||||
$rootScope,
|
||||
$timeout,
|
||||
$compile
|
||||
) {
|
||||
super({
|
||||
modelManager,
|
||||
syncManager,
|
||||
desktopManager,
|
||||
nativeExtManager,
|
||||
alertManager: new SFAlertManager(),
|
||||
$uiRunner: $rootScope.safeApply,
|
||||
$timeout: $timeout,
|
||||
environment: isDesktopApplication() ? "desktop" : "web",
|
||||
platform: getPlatformString()
|
||||
});
|
||||
|
||||
// this.loggingEnabled = true;
|
||||
|
||||
this.$compile = $compile;
|
||||
this.$rootScope = $rootScope;
|
||||
}
|
||||
|
||||
openModalComponent(component) {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.component = component;
|
||||
var el = this.$compile( "<component-modal component='component' class='sk-modal'></component-modal>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
presentPermissionsDialog(dialog) {
|
||||
let scope = this.$rootScope.$new(true);
|
||||
scope.permissionsString = dialog.permissionsString;
|
||||
scope.component = dialog.component;
|
||||
scope.callback = dialog.callback;
|
||||
|
||||
var el = this.$compile( "<permissions-modal component='component' permissions-string='permissionsString' callback='callback' class='sk-modal'></permissions-modal>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
}
|
||||
179
app/assets/javascripts/services/dbManager.js
Normal file
179
app/assets/javascripts/services/dbManager.js
Normal file
@@ -0,0 +1,179 @@
|
||||
export class DBManager {
|
||||
/* @ngInject */
|
||||
constructor(alertManager) {
|
||||
this.locked = true;
|
||||
this.alertManager = alertManager;
|
||||
}
|
||||
|
||||
displayOfflineAlert() {
|
||||
var message = "There was an issue loading your offline database. This could happen for two reasons:";
|
||||
message += "\n\n1. You're in a private window in your browser. We can't save your data without access to the local database. Please use a non-private window.";
|
||||
message += "\n\n2. You have two windows of the app open at the same time. Please close any other app instances and reload the page.";
|
||||
this.alertManager.alert({text: message});
|
||||
}
|
||||
|
||||
setLocked(locked) {
|
||||
this.locked = locked;
|
||||
}
|
||||
|
||||
async openDatabase({onUpgradeNeeded} = {}) {
|
||||
if(this.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = window.indexedDB.open("standardnotes", 1);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = (event) => {
|
||||
if(event.target.errorCode) {
|
||||
this.alertManager.alert({text: "Offline database issue: " + event.target.errorCode});
|
||||
} else {
|
||||
this.displayOfflineAlert();
|
||||
}
|
||||
console.error("Offline database issue:", event);
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const db = event.target.result;
|
||||
db.onversionchange = function(event) {
|
||||
db.close();
|
||||
};
|
||||
db.onerror = function(errorEvent) {
|
||||
console.error("Database error: " + errorEvent.target.errorCode);
|
||||
}
|
||||
resolve(db);
|
||||
};
|
||||
|
||||
request.onblocked = (event) => {
|
||||
console.error("Request blocked error:", event.target.errorCode);
|
||||
}
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
db.onversionchange = function(event) {
|
||||
db.close();
|
||||
};
|
||||
|
||||
// Create an objectStore for this database
|
||||
const objectStore = db.createObjectStore("items", { keyPath: "uuid" });
|
||||
objectStore.createIndex("uuid", "uuid", { unique: true });
|
||||
objectStore.transaction.oncomplete = function(event) {
|
||||
// Ready to store values in the newly created objectStore.
|
||||
if(db.version === 1 && onUpgradeNeeded) {
|
||||
onUpgradeNeeded();
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
async getAllModels() {
|
||||
const db = await this.openDatabase();
|
||||
const objectStore = db.transaction("items").objectStore("items");
|
||||
const items = [];
|
||||
return new Promise(async (resolve, reject) => {
|
||||
objectStore.openCursor().onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor) {
|
||||
items.push(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve(items);
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
async saveModel(item) {
|
||||
this.saveModels([item]);
|
||||
}
|
||||
|
||||
async saveModels(items) {
|
||||
const showGenericError = (error) => {
|
||||
this.alertManager.alert({text: `Unable to save changes locally due to an unknown system issue. Issue Code: ${error.code} Issue Name: ${error.name}.`});
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if(items.length === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const db = await this.openDatabase();
|
||||
const transaction = db.transaction("items", "readwrite");
|
||||
transaction.oncomplete = (event) => {};
|
||||
transaction.onerror = function(event) {
|
||||
console.error("Transaction error:", event.target.errorCode);
|
||||
showGenericError(event.target.error);
|
||||
};
|
||||
transaction.onblocked = function(event) {
|
||||
console.error("Transaction blocked error:", event.target.errorCode);
|
||||
showGenericError(event.target.error);
|
||||
};
|
||||
transaction.onabort = function(event) {
|
||||
console.error("Offline saving aborted:", event);
|
||||
const error = event.target.error;
|
||||
if(error.name == "QuotaExceededError") {
|
||||
this.alertManager.alert({text: "Unable to save changes locally because your device is out of space. Please free up some disk space and try again, otherwise, your data may end up in an inconsistent state."});
|
||||
} else {
|
||||
showGenericError(error);
|
||||
}
|
||||
reject(error);
|
||||
};
|
||||
|
||||
const itemObjectStore = transaction.objectStore("items");
|
||||
|
||||
const putItem = async (item) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = itemObjectStore.put(item);
|
||||
request.onerror = (event) => {
|
||||
console.error("DB put error:", event.target.error);
|
||||
resolve();
|
||||
}
|
||||
request.onsuccess = resolve;
|
||||
})
|
||||
}
|
||||
|
||||
for(const item of items) {
|
||||
await putItem(item);
|
||||
}
|
||||
|
||||
resolve();
|
||||
})
|
||||
}
|
||||
|
||||
async deleteModel(item) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const db = await this.openDatabase();
|
||||
const request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid);
|
||||
request.onsuccess = (event) => {
|
||||
resolve();
|
||||
}
|
||||
request.onerror = (event) => {
|
||||
reject();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async clearAllModels() {
|
||||
const deleteRequest = window.indexedDB.deleteDatabase("standardnotes");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
deleteRequest.onerror = function(event) {
|
||||
console.error("Error deleting database.");
|
||||
resolve();
|
||||
};
|
||||
|
||||
deleteRequest.onsuccess = function(event) {
|
||||
resolve();
|
||||
};
|
||||
|
||||
deleteRequest.onblocked = function(event) {
|
||||
console.error("Delete request blocked");
|
||||
this.alertManager.alert({text: "Your browser is blocking Standard Notes from deleting the local database. Make sure there are no other open windows of this app and try again. If the issue persists, please manually delete app data to sign out."})
|
||||
resolve();
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
235
app/assets/javascripts/services/desktopManager.js
Normal file
235
app/assets/javascripts/services/desktopManager.js
Normal file
@@ -0,0 +1,235 @@
|
||||
// An interface used by the Desktop app to interact with SN
|
||||
import _ from 'lodash';
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import { SFItemParams, SFModelManager } from 'snjs';
|
||||
|
||||
const COMPONENT_DATA_KEY_INSTALL_ERROR = 'installError';
|
||||
const COMPONENT_CONTENT_KEY_PACKAGE_INFO = 'package_info';
|
||||
const COMPONENT_CONTENT_KEY_LOCAL_URL = 'local_url';
|
||||
|
||||
export class DesktopManager {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$rootScope,
|
||||
$timeout,
|
||||
modelManager,
|
||||
syncManager,
|
||||
authManager,
|
||||
passcodeManager,
|
||||
appState
|
||||
) {
|
||||
this.passcodeManager = passcodeManager;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.syncManager = syncManager;
|
||||
this.$rootScope = $rootScope;
|
||||
this.appState = appState;
|
||||
this.timeout = $timeout;
|
||||
this.updateObservers = [];
|
||||
this.componentActivationObservers = [];
|
||||
|
||||
this.isDesktop = isDesktopApplication();
|
||||
|
||||
$rootScope.$on("initial-data-loaded", () => {
|
||||
this.dataLoaded = true;
|
||||
if(this.dataLoadHandler) {
|
||||
this.dataLoadHandler();
|
||||
}
|
||||
});
|
||||
|
||||
$rootScope.$on("major-data-change", () => {
|
||||
if(this.majorDataChangeHandler) {
|
||||
this.majorDataChangeHandler();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
saveBackup() {
|
||||
this.majorDataChangeHandler && this.majorDataChangeHandler();
|
||||
}
|
||||
|
||||
getExtServerHost() {
|
||||
console.assert(
|
||||
this.extServerHost,
|
||||
'extServerHost is null'
|
||||
);
|
||||
return this.extServerHost;
|
||||
}
|
||||
|
||||
/*
|
||||
Sending a component in its raw state is really slow for the desktop app
|
||||
Keys are not passed into ItemParams, so the result is not encrypted
|
||||
*/
|
||||
async convertComponentForTransmission(component) {
|
||||
return new SFItemParams(component).paramsForExportFile(true);
|
||||
}
|
||||
|
||||
// All `components` should be installed
|
||||
syncComponentsInstallation(components) {
|
||||
if(!this.isDesktop) {
|
||||
return;
|
||||
}
|
||||
Promise.all(components.map((component) => {
|
||||
return this.convertComponentForTransmission(component);
|
||||
})).then((data) => {
|
||||
this.installationSyncHandler(data);
|
||||
})
|
||||
}
|
||||
|
||||
async installComponent(component) {
|
||||
this.installComponentHandler(
|
||||
await this.convertComponentForTransmission(component)
|
||||
);
|
||||
}
|
||||
|
||||
registerUpdateObserver(callback) {
|
||||
const observer = {
|
||||
callback: callback
|
||||
};
|
||||
this.updateObservers.push(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
searchText(text) {
|
||||
if(!this.isDesktop) {
|
||||
return;
|
||||
}
|
||||
this.lastSearchedText = text;
|
||||
this.searchHandler && this.searchHandler(text);
|
||||
}
|
||||
|
||||
redoSearch() {
|
||||
if(this.lastSearchedText) {
|
||||
this.searchText(this.lastSearchedText);
|
||||
}
|
||||
}
|
||||
|
||||
deregisterUpdateObserver(observer) {
|
||||
_.pull(this.updateObservers, observer);
|
||||
}
|
||||
|
||||
// Pass null to cancel search
|
||||
desktop_setSearchHandler(handler) {
|
||||
this.searchHandler = handler;
|
||||
}
|
||||
|
||||
desktop_windowGainedFocus() {
|
||||
this.$rootScope.$broadcast("window-gained-focus");
|
||||
}
|
||||
|
||||
desktop_windowLostFocus() {
|
||||
this.$rootScope.$broadcast("window-lost-focus");
|
||||
}
|
||||
|
||||
desktop_onComponentInstallationComplete(componentData, error) {
|
||||
const component = this.modelManager.findItem(componentData.uuid);
|
||||
if(!component) {
|
||||
return;
|
||||
}
|
||||
if(error) {
|
||||
component.setAppDataItem(
|
||||
COMPONENT_DATA_KEY_INSTALL_ERROR,
|
||||
error
|
||||
);
|
||||
} else {
|
||||
const permissableKeys = [
|
||||
COMPONENT_CONTENT_KEY_PACKAGE_INFO,
|
||||
COMPONENT_CONTENT_KEY_LOCAL_URL
|
||||
];
|
||||
for(const key of permissableKeys) {
|
||||
component[key] = componentData.content[key];
|
||||
}
|
||||
this.modelManager.notifySyncObserversOfModels(
|
||||
[component],
|
||||
SFModelManager.MappingSourceDesktopInstalled
|
||||
);
|
||||
component.setAppDataItem(
|
||||
COMPONENT_DATA_KEY_INSTALL_ERROR,
|
||||
null
|
||||
);
|
||||
}
|
||||
this.modelManager.setItemDirty(component);
|
||||
this.syncManager.sync();
|
||||
this.timeout(() => {
|
||||
for(const observer of this.updateObservers) {
|
||||
observer.callback(component);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
desktop_registerComponentActivationObserver(callback) {
|
||||
const observer = {id: Math.random, callback: callback};
|
||||
this.componentActivationObservers.push(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
desktop_deregisterComponentActivationObserver(observer) {
|
||||
_.pull(this.componentActivationObservers, observer);
|
||||
}
|
||||
|
||||
/* Notify observers that a component has been registered/activated */
|
||||
async notifyComponentActivation(component) {
|
||||
const serializedComponent = await this.convertComponentForTransmission(
|
||||
component
|
||||
);
|
||||
this.timeout(() => {
|
||||
for(const observer of this.componentActivationObservers) {
|
||||
observer.callback(serializedComponent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Used to resolve "sn://" */
|
||||
desktop_setExtServerHost(host) {
|
||||
this.extServerHost = host;
|
||||
this.appState.desktopExtensionsReady();
|
||||
}
|
||||
|
||||
desktop_setComponentInstallationSyncHandler(handler) {
|
||||
this.installationSyncHandler = handler;
|
||||
}
|
||||
|
||||
desktop_setInstallComponentHandler(handler) {
|
||||
this.installComponentHandler = handler;
|
||||
}
|
||||
|
||||
desktop_setInitialDataLoadHandler(handler) {
|
||||
this.dataLoadHandler = handler;
|
||||
if(this.dataLoaded) {
|
||||
this.dataLoadHandler();
|
||||
}
|
||||
}
|
||||
|
||||
async desktop_requestBackupFile(callback) {
|
||||
let keys, authParams;
|
||||
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
||||
keys = this.passcodeManager.keys();
|
||||
authParams = this.passcodeManager.passcodeAuthParams();
|
||||
} else {
|
||||
keys = await this.authManager.keys();
|
||||
authParams = await this.authManager.getAuthParams();
|
||||
}
|
||||
const nullOnEmpty = true;
|
||||
this.modelManager.getAllItemsJSONData(
|
||||
keys,
|
||||
authParams,
|
||||
nullOnEmpty
|
||||
).then((data) => {
|
||||
callback(data);
|
||||
})
|
||||
}
|
||||
|
||||
desktop_setMajorDataChangeHandler(handler) {
|
||||
this.majorDataChangeHandler = handler;
|
||||
}
|
||||
|
||||
desktop_didBeginBackup() {
|
||||
this.appState.beganBackupDownload();
|
||||
}
|
||||
|
||||
desktop_didFinishBackup(success) {
|
||||
this.appState.endedBackupDownload({
|
||||
success: success
|
||||
});
|
||||
}
|
||||
}
|
||||
13
app/assets/javascripts/services/httpManager.js
Normal file
13
app/assets/javascripts/services/httpManager.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { SFHttpManager } from 'snjs';
|
||||
|
||||
export class HttpManager extends SFHttpManager {
|
||||
/* @ngInject */
|
||||
constructor(storageManager, $timeout) {
|
||||
// calling callbacks in a $timeout allows UI to update
|
||||
super($timeout);
|
||||
|
||||
this.setJWTRequestHandler(async () => {
|
||||
return storageManager.getItem('jwt');
|
||||
});
|
||||
}
|
||||
}
|
||||
21
app/assets/javascripts/services/index.js
Normal file
21
app/assets/javascripts/services/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export { ActionsManager } from './actionsManager';
|
||||
export { ArchiveManager } from './archiveManager';
|
||||
export { AuthManager } from './authManager';
|
||||
export { ComponentManager } from './componentManager';
|
||||
export { DBManager } from './dbManager';
|
||||
export { DesktopManager } from './desktopManager';
|
||||
export { HttpManager } from './httpManager';
|
||||
export { KeyboardManager } from './keyboardManager';
|
||||
export { MigrationManager } from './migrationManager';
|
||||
export { ModelManager } from './modelManager';
|
||||
export { NativeExtManager } from './nativeExtManager';
|
||||
export { PasscodeManager } from './passcodeManager';
|
||||
export { PrivilegesManager } from './privilegesManager';
|
||||
export { SessionHistory } from './sessionHistory';
|
||||
export { SingletonManager } from './singletonManager';
|
||||
export { StatusManager } from './statusManager';
|
||||
export { StorageManager } from './storageManager';
|
||||
export { SyncManager } from './syncManager';
|
||||
export { ThemeManager } from './themeManager';
|
||||
export { AlertManager } from './alertManager';
|
||||
export { PreferencesManager } from './preferencesManager';
|
||||
115
app/assets/javascripts/services/keyboardManager.js
Normal file
115
app/assets/javascripts/services/keyboardManager.js
Normal file
@@ -0,0 +1,115 @@
|
||||
export class KeyboardManager {
|
||||
|
||||
constructor() {
|
||||
this.observers = [];
|
||||
|
||||
KeyboardManager.KeyTab = "Tab";
|
||||
KeyboardManager.KeyBackspace = "Backspace";
|
||||
KeyboardManager.KeyUp = "ArrowUp";
|
||||
KeyboardManager.KeyDown = "ArrowDown";
|
||||
|
||||
KeyboardManager.KeyModifierShift = "Shift";
|
||||
KeyboardManager.KeyModifierCtrl = "Control";
|
||||
// ⌘ key on Mac, ⊞ key on Windows
|
||||
KeyboardManager.KeyModifierMeta = "Meta";
|
||||
KeyboardManager.KeyModifierAlt = "Alt";
|
||||
|
||||
KeyboardManager.KeyEventDown = "KeyEventDown";
|
||||
KeyboardManager.KeyEventUp = "KeyEventUp";
|
||||
|
||||
KeyboardManager.AllModifiers = [
|
||||
KeyboardManager.KeyModifierShift,
|
||||
KeyboardManager.KeyModifierCtrl,
|
||||
KeyboardManager.KeyModifierMeta,
|
||||
KeyboardManager.KeyModifierAlt
|
||||
]
|
||||
|
||||
window.addEventListener('keydown', this.handleKeyDown.bind(this));
|
||||
window.addEventListener('keyup', this.handleKeyUp.bind(this));
|
||||
}
|
||||
|
||||
modifiersForEvent(event) {
|
||||
let eventModifiers = KeyboardManager.AllModifiers.filter((modifier) => {
|
||||
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
|
||||
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
|
||||
let matches = (
|
||||
((event.ctrlKey || event.key == KeyboardManager.KeyModifierCtrl) && modifier === KeyboardManager.KeyModifierCtrl) ||
|
||||
((event.metaKey || event.key == KeyboardManager.KeyModifierMeta) && modifier === KeyboardManager.KeyModifierMeta) ||
|
||||
((event.altKey || event.key == KeyboardManager.KeyModifierAlt) && modifier === KeyboardManager.KeyModifierAlt) ||
|
||||
((event.shiftKey || event.key == KeyboardManager.KeyModifierShift) && modifier === KeyboardManager.KeyModifierShift)
|
||||
)
|
||||
|
||||
return matches;
|
||||
})
|
||||
|
||||
return eventModifiers;
|
||||
}
|
||||
|
||||
eventMatchesKeyAndModifiers(event, key, modifiers = []) {
|
||||
let eventModifiers = this.modifiersForEvent(event);
|
||||
|
||||
if(eventModifiers.length != modifiers.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(let modifier of modifiers) {
|
||||
if(!eventModifiers.includes(modifier)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Modifers match, check key
|
||||
if(!key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
|
||||
// In our case we don't differentiate between the two.
|
||||
return key.toLowerCase() == event.key.toLowerCase();
|
||||
}
|
||||
|
||||
notifyObserver(event, keyEventType) {
|
||||
for(let observer of this.observers) {
|
||||
if(observer.element && event.target != observer.element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(observer.elements && !observer.elements.includes(event.target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(observer.notElement && observer.notElement == event.target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(observer.notElementIds && observer.notElementIds.includes(event.target.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(this.eventMatchesKeyAndModifiers(event, observer.key, observer.modifiers)) {
|
||||
let callback = keyEventType == KeyboardManager.KeyEventDown ? observer.onKeyDown : observer.onKeyUp;
|
||||
if(callback) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown(event) {
|
||||
this.notifyObserver(event, KeyboardManager.KeyEventDown);
|
||||
}
|
||||
|
||||
handleKeyUp(event) {
|
||||
this.notifyObserver(event, KeyboardManager.KeyEventUp);
|
||||
}
|
||||
|
||||
addKeyObserver({key, modifiers, onKeyDown, onKeyUp, element, elements, notElement, notElementIds}) {
|
||||
let observer = {key, modifiers, onKeyDown, onKeyUp, element, elements, notElement, notElementIds};
|
||||
this.observers.push(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
removeKeyObserver(observer) {
|
||||
this.observers.splice(this.observers.indexOf(observer), 1);
|
||||
}
|
||||
}
|
||||
172
app/assets/javascripts/services/migrationManager.js
Normal file
172
app/assets/javascripts/services/migrationManager.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import { SFMigrationManager } from 'snjs';
|
||||
import { ComponentManager } from '@/services/componentManager';
|
||||
|
||||
export class MigrationManager extends SFMigrationManager {
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
modelManager,
|
||||
syncManager,
|
||||
componentManager,
|
||||
storageManager,
|
||||
statusManager,
|
||||
authManager,
|
||||
desktopManager
|
||||
) {
|
||||
super(modelManager, syncManager, storageManager, authManager);
|
||||
this.componentManager = componentManager;
|
||||
this.statusManager = statusManager;
|
||||
this.desktopManager = desktopManager;
|
||||
}
|
||||
|
||||
registeredMigrations() {
|
||||
return [
|
||||
this.editorToComponentMigration(),
|
||||
this.componentUrlToHostedUrl(),
|
||||
this.removeTagReferencesFromNotes()
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
Migrate SN|Editor to SN|Component. Editors are deprecated as of November 2017. Editors using old APIs must
|
||||
convert to using the new component API.
|
||||
*/
|
||||
|
||||
editorToComponentMigration() {
|
||||
return {
|
||||
name: "editor-to-component",
|
||||
content_type: "SN|Editor",
|
||||
handler: async (editors) => {
|
||||
// Convert editors to components
|
||||
for(var editor of editors) {
|
||||
// If there's already a component for this url, then skip this editor
|
||||
if(editor.url && !this.componentManager.componentForUrl(editor.url)) {
|
||||
var component = this.modelManager.createItem({
|
||||
content_type: "SN|Component",
|
||||
content: {
|
||||
url: editor.url,
|
||||
name: editor.name,
|
||||
area: "editor-editor"
|
||||
}
|
||||
})
|
||||
component.setAppDataItem("data", editor.data);
|
||||
this.modelManager.addItem(component);
|
||||
this.modelManager.setItemDirty(component, true);
|
||||
}
|
||||
}
|
||||
|
||||
for(let editor of editors) {
|
||||
this.modelManager.setItemToBeDeleted(editor);
|
||||
}
|
||||
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Migrate component.url fields to component.hosted_url. This involves rewriting any note data that relied on the
|
||||
component.url value to store clientData, such as the CodeEditor, which stores the programming language for the note
|
||||
in the note's clientData[component.url]. We want to rewrite any matching items to transfer that clientData into
|
||||
clientData[component.uuid].
|
||||
|
||||
April 3, 2019 note: it seems this migration is mis-named. The first part of the description doesn't match what the code is actually doing.
|
||||
It has nothing to do with url/hosted_url relationship and more to do with just mapping client data from the note's hosted_url to its uuid
|
||||
|
||||
Created: July 6, 2018
|
||||
*/
|
||||
componentUrlToHostedUrl() {
|
||||
return {
|
||||
name: "component-url-to-hosted-url",
|
||||
content_type: "SN|Component",
|
||||
handler: async (components) => {
|
||||
let hasChanges = false;
|
||||
let notes = this.modelManager.validItemsForContentType("Note");
|
||||
for(let note of notes) {
|
||||
for(let component of components) {
|
||||
let clientData = note.getDomainDataItem(component.hosted_url, ComponentManager.ClientDataDomain);
|
||||
if(clientData) {
|
||||
note.setDomainDataItem(component.uuid, clientData, ComponentManager.ClientDataDomain);
|
||||
note.setDomainDataItem(component.hosted_url, null, ComponentManager.ClientDataDomain);
|
||||
this.modelManager.setItemDirty(note, true);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(hasChanges) {
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Migrate notes which have relationships on tags to migrate those relationships to the tags themselves.
|
||||
That is, notes.content.references should not include any mention of tags.
|
||||
This will apply to notes created before the schema change. Now, only tags reference notes.
|
||||
Created: April 3, 2019
|
||||
*/
|
||||
removeTagReferencesFromNotes() {
|
||||
return {
|
||||
name: "remove-tag-references-from-notes",
|
||||
content_type: "Note",
|
||||
handler: async (notes) => {
|
||||
|
||||
let needsSync = false;
|
||||
let status = this.statusManager.addStatusFromString("Optimizing data...");
|
||||
let dirtyCount = 0;
|
||||
|
||||
for(let note of notes) {
|
||||
if(!note.content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let references = note.content.references;
|
||||
// Remove any tag references, and transfer them to the tag if neccessary.
|
||||
let newReferences = [];
|
||||
|
||||
for(let reference of references) {
|
||||
if(reference.content_type != "Tag") {
|
||||
newReferences.push(reference);
|
||||
continue;
|
||||
}
|
||||
|
||||
// is Tag content_type, we will not be adding this to newReferences
|
||||
let tag = this.modelManager.findItem(reference.uuid);
|
||||
if(tag && !tag.hasRelationshipWithItem(note)) {
|
||||
tag.addItemAsRelationship(note);
|
||||
this.modelManager.setItemDirty(tag, true);
|
||||
dirtyCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if(newReferences.length != references.length) {
|
||||
note.content.references = newReferences;
|
||||
this.modelManager.setItemDirty(note, true);
|
||||
dirtyCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if(dirtyCount > 0) {
|
||||
if(isDesktopApplication()) {
|
||||
this.desktopManager.saveBackup();
|
||||
}
|
||||
|
||||
status = this.statusManager.replaceStatusWithString(status, `${dirtyCount} items optimized.`);
|
||||
await this.syncManager.sync();
|
||||
|
||||
status = this.statusManager.replaceStatusWithString(status, `Optimization complete.`);
|
||||
setTimeout(() => {
|
||||
this.statusManager.removeStatus(status);
|
||||
}, 2000);
|
||||
} else {
|
||||
this.statusManager.removeStatus(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
170
app/assets/javascripts/services/modelManager.js
Normal file
170
app/assets/javascripts/services/modelManager.js
Normal file
@@ -0,0 +1,170 @@
|
||||
import _ from 'lodash';
|
||||
import { SFModelManager, SNSmartTag, SFPredicate } from 'snjs';
|
||||
|
||||
export class ModelManager extends SFModelManager {
|
||||
/* @ngInject */
|
||||
constructor(storageManager, $timeout) {
|
||||
super($timeout);
|
||||
this.notes = [];
|
||||
this.tags = [];
|
||||
this.components = [];
|
||||
|
||||
this.storageManager = storageManager;
|
||||
|
||||
this.buildSystemSmartTags();
|
||||
}
|
||||
|
||||
handleSignout() {
|
||||
super.handleSignout();
|
||||
this.notes.length = 0;
|
||||
this.tags.length = 0;
|
||||
this.components.length = 0;
|
||||
}
|
||||
|
||||
noteCount() {
|
||||
return this.notes.filter((n) => !n.dummy).length;
|
||||
}
|
||||
|
||||
removeAllItemsFromMemory() {
|
||||
for(var item of this.items) {
|
||||
item.deleted = true;
|
||||
}
|
||||
this.notifySyncObserversOfModels(this.items);
|
||||
this.handleSignout();
|
||||
}
|
||||
|
||||
findTag(title) {
|
||||
return _.find(this.tags, { title: title });
|
||||
}
|
||||
|
||||
findOrCreateTagByTitle(title) {
|
||||
let tag = this.findTag(title);
|
||||
if(!tag) {
|
||||
tag = this.createItem({content_type: "Tag", content: {title: title}});
|
||||
this.addItem(tag);
|
||||
this.setItemDirty(tag, true);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
addItems(items, globalOnly = false) {
|
||||
super.addItems(items, globalOnly);
|
||||
|
||||
items.forEach((item) => {
|
||||
// In some cases, you just want to add the item to this.items, and not to the individual arrays
|
||||
// This applies when you want to keep an item syncable, but not display it via the individual arrays
|
||||
if(!globalOnly) {
|
||||
if(item.content_type == "Tag") {
|
||||
if(!_.find(this.tags, {uuid: item.uuid})) {
|
||||
this.tags.splice(_.sortedIndexBy(this.tags, item, function(item){
|
||||
if (item.title) return item.title.toLowerCase();
|
||||
else return ''
|
||||
}), 0, item);
|
||||
}
|
||||
} else if(item.content_type == "Note") {
|
||||
if(!_.find(this.notes, {uuid: item.uuid})) {
|
||||
this.notes.unshift(item);
|
||||
}
|
||||
} else if(item.content_type == "SN|Component") {
|
||||
if(!_.find(this.components, {uuid: item.uuid})) {
|
||||
this.components.unshift(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resortTag(tag) {
|
||||
_.pull(this.tags, tag);
|
||||
this.tags.splice(_.sortedIndexBy(this.tags, tag, function(tag){
|
||||
if (tag.title) return tag.title.toLowerCase();
|
||||
else return ''
|
||||
}), 0, tag);
|
||||
}
|
||||
|
||||
setItemToBeDeleted(item) {
|
||||
super.setItemToBeDeleted(item);
|
||||
|
||||
// remove from relevant array, but don't remove from all items.
|
||||
// This way, it's removed from the display, but still synced via get dirty items
|
||||
this.removeItemFromRespectiveArray(item);
|
||||
}
|
||||
|
||||
removeItemLocally(item, callback) {
|
||||
super.removeItemLocally(item, callback);
|
||||
this.removeItemFromRespectiveArray(item);
|
||||
this.storageManager.deleteModel(item).then(callback);
|
||||
}
|
||||
|
||||
removeItemFromRespectiveArray(item) {
|
||||
if(item.content_type == "Tag") {
|
||||
_.remove(this.tags, {uuid: item.uuid});
|
||||
} else if(item.content_type == "Note") {
|
||||
_.remove(this.notes, {uuid: item.uuid});
|
||||
} else if(item.content_type == "SN|Component") {
|
||||
_.remove(this.components, {uuid: item.uuid});
|
||||
}
|
||||
}
|
||||
|
||||
notesMatchingSmartTag(tag) {
|
||||
let contentTypePredicate = new SFPredicate("content_type", "=", "Note");
|
||||
let predicates = [contentTypePredicate, tag.content.predicate];
|
||||
if(!tag.content.isTrashTag) {
|
||||
let notTrashedPredicate = new SFPredicate("content.trashed", "=", false);
|
||||
predicates.push(notTrashedPredicate);
|
||||
}
|
||||
let results = this.itemsMatchingPredicates(predicates);
|
||||
return results;
|
||||
}
|
||||
|
||||
trashSmartTag() {
|
||||
return this.systemSmartTags.find((tag) => tag.content.isTrashTag);
|
||||
}
|
||||
|
||||
trashedItems() {
|
||||
return this.notesMatchingSmartTag(this.trashSmartTag());
|
||||
}
|
||||
|
||||
emptyTrash() {
|
||||
let notes = this.trashedItems();
|
||||
for(let note of notes) {
|
||||
this.setItemToBeDeleted(note);
|
||||
}
|
||||
}
|
||||
|
||||
buildSystemSmartTags() {
|
||||
this.systemSmartTags = SNSmartTag.systemSmartTags();
|
||||
}
|
||||
|
||||
getSmartTagWithId(id) {
|
||||
return this.getSmartTags().find((candidate) => candidate.uuid == id);
|
||||
}
|
||||
|
||||
getSmartTags() {
|
||||
let userTags = this.validItemsForContentType("SN|SmartTag").sort((a, b) => {
|
||||
return a.content.title < b.content.title ? -1 : 1;
|
||||
});
|
||||
return this.systemSmartTags.concat(userTags);
|
||||
}
|
||||
|
||||
/*
|
||||
Misc
|
||||
*/
|
||||
|
||||
humanReadableDisplayForContentType(contentType) {
|
||||
return {
|
||||
"Note" : "note",
|
||||
"Tag" : "tag",
|
||||
"SN|SmartTag": "smart tag",
|
||||
"Extension" : "action-based extension",
|
||||
"SN|Component" : "component",
|
||||
"SN|Editor" : "editor",
|
||||
"SN|Theme" : "theme",
|
||||
"SF|Extension" : "server extension",
|
||||
"SF|MFA" : "two-factor authentication setting",
|
||||
"SN|FileSafe|Credentials": "FileSafe credential",
|
||||
"SN|FileSafe|FileMetadata": "FileSafe file",
|
||||
"SN|FileSafe|Integration": "FileSafe integration"
|
||||
}[contentType];
|
||||
}
|
||||
}
|
||||
183
app/assets/javascripts/services/nativeExtManager.js
Normal file
183
app/assets/javascripts/services/nativeExtManager.js
Normal file
@@ -0,0 +1,183 @@
|
||||
/* A class for handling installation of system extensions */
|
||||
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import { SFPredicate } from 'snjs';
|
||||
|
||||
export class NativeExtManager {
|
||||
/* @ngInject */
|
||||
constructor(modelManager, syncManager, singletonManager) {
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
this.singletonManager = singletonManager;
|
||||
|
||||
this.extManagerId = "org.standardnotes.extensions-manager";
|
||||
this.batchManagerId = "org.standardnotes.batch-manager";
|
||||
this.systemExtensions = [];
|
||||
|
||||
this.resolveExtensionsManager();
|
||||
this.resolveBatchManager();
|
||||
}
|
||||
|
||||
isSystemExtension(extension) {
|
||||
return this.systemExtensions.includes(extension.uuid);
|
||||
}
|
||||
|
||||
resolveExtensionsManager() {
|
||||
|
||||
let contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
|
||||
let packagePredicate = new SFPredicate("package_info.identifier", "=", this.extManagerId);
|
||||
|
||||
this.singletonManager.registerSingleton([contentTypePredicate, packagePredicate], (resolvedSingleton) => {
|
||||
// Resolved Singleton
|
||||
this.systemExtensions.push(resolvedSingleton.uuid);
|
||||
|
||||
var needsSync = false;
|
||||
if(isDesktopApplication()) {
|
||||
if(!resolvedSingleton.local_url) {
|
||||
resolvedSingleton.local_url = window._extensions_manager_location;
|
||||
needsSync = true;
|
||||
}
|
||||
} else {
|
||||
if(!resolvedSingleton.hosted_url) {
|
||||
resolvedSingleton.hosted_url = window._extensions_manager_location;
|
||||
needsSync = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle addition of SN|ExtensionRepo permission
|
||||
let permission = resolvedSingleton.content.permissions.find((p) => p.name == "stream-items");
|
||||
if(!permission.content_types.includes("SN|ExtensionRepo")) {
|
||||
permission.content_types.push("SN|ExtensionRepo");
|
||||
needsSync = true;
|
||||
}
|
||||
|
||||
if(needsSync) {
|
||||
this.modelManager.setItemDirty(resolvedSingleton, true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}, (valueCallback) => {
|
||||
// Safe to create. Create and return object.
|
||||
let url = window._extensions_manager_location;
|
||||
if(!url) {
|
||||
console.error("window._extensions_manager_location must be set.");
|
||||
return;
|
||||
}
|
||||
|
||||
let packageInfo = {
|
||||
name: "Extensions",
|
||||
identifier: this.extManagerId
|
||||
}
|
||||
|
||||
var item = {
|
||||
content_type: "SN|Component",
|
||||
content: {
|
||||
name: packageInfo.name,
|
||||
area: "rooms",
|
||||
package_info: packageInfo,
|
||||
permissions: [
|
||||
{
|
||||
name: "stream-items",
|
||||
content_types: [
|
||||
"SN|Component", "SN|Theme", "SF|Extension",
|
||||
"Extension", "SF|MFA", "SN|Editor", "SN|ExtensionRepo"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if(isDesktopApplication()) {
|
||||
item.content.local_url = window._extensions_manager_location;
|
||||
} else {
|
||||
item.content.hosted_url = window._extensions_manager_location;
|
||||
}
|
||||
|
||||
var component = this.modelManager.createItem(item);
|
||||
this.modelManager.addItem(component);
|
||||
|
||||
this.modelManager.setItemDirty(component, true);
|
||||
this.syncManager.sync();
|
||||
|
||||
this.systemExtensions.push(component.uuid);
|
||||
|
||||
valueCallback(component);
|
||||
});
|
||||
}
|
||||
|
||||
resolveBatchManager() {
|
||||
|
||||
let contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
|
||||
let packagePredicate = new SFPredicate("package_info.identifier", "=", this.batchManagerId);
|
||||
|
||||
this.singletonManager.registerSingleton([contentTypePredicate, packagePredicate], (resolvedSingleton) => {
|
||||
// Resolved Singleton
|
||||
this.systemExtensions.push(resolvedSingleton.uuid);
|
||||
|
||||
var needsSync = false;
|
||||
if(isDesktopApplication()) {
|
||||
if(!resolvedSingleton.local_url) {
|
||||
resolvedSingleton.local_url = window._batch_manager_location;
|
||||
needsSync = true;
|
||||
}
|
||||
} else {
|
||||
if(!resolvedSingleton.hosted_url) {
|
||||
resolvedSingleton.hosted_url = window._batch_manager_location;
|
||||
needsSync = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(needsSync) {
|
||||
this.modelManager.setItemDirty(resolvedSingleton, true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}, (valueCallback) => {
|
||||
// Safe to create. Create and return object.
|
||||
let url = window._batch_manager_location;
|
||||
if(!url) {
|
||||
console.error("window._batch_manager_location must be set.");
|
||||
return;
|
||||
}
|
||||
|
||||
let packageInfo = {
|
||||
name: "Batch Manager",
|
||||
identifier: this.batchManagerId
|
||||
}
|
||||
|
||||
var item = {
|
||||
content_type: "SN|Component",
|
||||
content: {
|
||||
name: packageInfo.name,
|
||||
area: "modal",
|
||||
package_info: packageInfo,
|
||||
permissions: [
|
||||
{
|
||||
name: "stream-items",
|
||||
content_types: [
|
||||
"Note", "Tag", "SN|SmartTag",
|
||||
"SN|Component", "SN|Theme", "SN|UserPreferences",
|
||||
"SF|Extension", "Extension", "SF|MFA", "SN|Editor",
|
||||
"SN|FileSafe|Credentials", "SN|FileSafe|FileMetadata", "SN|FileSafe|Integration"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if(isDesktopApplication()) {
|
||||
item.content.local_url = window._batch_manager_location;
|
||||
} else {
|
||||
item.content.hosted_url = window._batch_manager_location;
|
||||
}
|
||||
|
||||
var component = this.modelManager.createItem(item);
|
||||
this.modelManager.addItem(component);
|
||||
|
||||
this.modelManager.setItemDirty(component, true);
|
||||
this.syncManager.sync();
|
||||
|
||||
this.systemExtensions.push(component.uuid);
|
||||
|
||||
valueCallback(component);
|
||||
});
|
||||
}
|
||||
}
|
||||
285
app/assets/javascripts/services/passcodeManager.js
Normal file
285
app/assets/javascripts/services/passcodeManager.js
Normal file
@@ -0,0 +1,285 @@
|
||||
import _ from 'lodash';
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import { StorageManager } from './storageManager';
|
||||
import { protocolManager } from 'snjs';
|
||||
|
||||
const MillisecondsPerSecond = 1000;
|
||||
|
||||
export class PasscodeManager {
|
||||
/* @ngInject */
|
||||
constructor($rootScope, authManager, storageManager, syncManager) {
|
||||
this.authManager = authManager;
|
||||
this.storageManager = storageManager;
|
||||
this.syncManager = syncManager;
|
||||
this.$rootScope = $rootScope;
|
||||
|
||||
this._hasPasscode = this.storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null;
|
||||
this._locked = this._hasPasscode;
|
||||
|
||||
this.visibilityObservers = [];
|
||||
this.passcodeChangeObservers = [];
|
||||
|
||||
this.configureAutoLock();
|
||||
}
|
||||
|
||||
addPasscodeChangeObserver(callback) {
|
||||
this.passcodeChangeObservers.push(callback);
|
||||
}
|
||||
|
||||
lockApplication() {
|
||||
window.location.reload();
|
||||
this.cancelAutoLockTimer();
|
||||
}
|
||||
|
||||
isLocked() {
|
||||
return this._locked;
|
||||
}
|
||||
|
||||
hasPasscode() {
|
||||
return this._hasPasscode;
|
||||
}
|
||||
|
||||
keys() {
|
||||
return this._keys;
|
||||
}
|
||||
|
||||
addVisibilityObserver(callback) {
|
||||
this.visibilityObservers.push(callback);
|
||||
return callback;
|
||||
}
|
||||
|
||||
removeVisibilityObserver(callback) {
|
||||
_.pull(this.visibilityObservers, callback);
|
||||
}
|
||||
|
||||
notifiyVisibilityObservers(visible) {
|
||||
for(let callback of this.visibilityObservers) {
|
||||
callback(visible);
|
||||
}
|
||||
}
|
||||
|
||||
async setAutoLockInterval(interval) {
|
||||
return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.FixedEncrypted);
|
||||
}
|
||||
|
||||
async getAutoLockInterval() {
|
||||
let interval = await this.storageManager.getItem(PasscodeManager.AutoLockIntervalKey, StorageManager.FixedEncrypted);
|
||||
if(interval) {
|
||||
return JSON.parse(interval);
|
||||
} else {
|
||||
return PasscodeManager.AutoLockIntervalNone;
|
||||
}
|
||||
}
|
||||
|
||||
passcodeAuthParams() {
|
||||
var authParams = JSON.parse(this.storageManager.getItemSync("offlineParams", StorageManager.Fixed));
|
||||
if(authParams && !authParams.version) {
|
||||
var keys = this.keys();
|
||||
if(keys && keys.ak) {
|
||||
// If there's no version stored, and there's an ak, it has to be 002. Newer versions would have their version stored in authParams.
|
||||
authParams.version = "002";
|
||||
} else {
|
||||
authParams.version = "001";
|
||||
}
|
||||
}
|
||||
return authParams;
|
||||
}
|
||||
|
||||
async verifyPasscode(passcode) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
var params = this.passcodeAuthParams();
|
||||
let keys = await protocolManager.computeEncryptionKeysForUser(passcode, params);
|
||||
if(keys.pw !== params.hash) {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
unlock(passcode, callback) {
|
||||
var params = this.passcodeAuthParams();
|
||||
protocolManager.computeEncryptionKeysForUser(passcode, params).then((keys) => {
|
||||
if(keys.pw !== params.hash) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this._keys = keys;
|
||||
this._authParams = params;
|
||||
this.decryptLocalStorage(keys, params).then(() => {
|
||||
this._locked = false;
|
||||
callback(true);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
setPasscode(passcode, callback) {
|
||||
var uuid = protocolManager.crypto.generateUUIDSync();
|
||||
|
||||
protocolManager.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => {
|
||||
let keys = results.keys;
|
||||
let authParams = results.authParams;
|
||||
|
||||
authParams.hash = keys.pw;
|
||||
this._keys = keys;
|
||||
this._hasPasscode = true;
|
||||
this._authParams = authParams;
|
||||
|
||||
// Encrypting will initially clear localStorage
|
||||
this.encryptLocalStorage(keys, authParams);
|
||||
|
||||
// After it's cleared, it's safe to write to it
|
||||
this.storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed);
|
||||
callback(true);
|
||||
|
||||
this.notifyObserversOfPasscodeChange();
|
||||
});
|
||||
}
|
||||
|
||||
changePasscode(newPasscode, callback) {
|
||||
this.setPasscode(newPasscode, callback);
|
||||
}
|
||||
|
||||
clearPasscode() {
|
||||
this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral
|
||||
this.storageManager.removeItem("offlineParams", StorageManager.Fixed);
|
||||
this._keys = null;
|
||||
this._hasPasscode = false;
|
||||
|
||||
this.notifyObserversOfPasscodeChange();
|
||||
}
|
||||
|
||||
notifyObserversOfPasscodeChange() {
|
||||
for(var observer of this.passcodeChangeObservers) {
|
||||
observer();
|
||||
}
|
||||
}
|
||||
|
||||
encryptLocalStorage(keys, authParams) {
|
||||
this.storageManager.setKeys(keys, authParams);
|
||||
// Switch to Ephemeral storage, wiping Fixed storage
|
||||
// Last argument is `force`, which we set to true because in the case of changing passcode
|
||||
this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true);
|
||||
}
|
||||
|
||||
async decryptLocalStorage(keys, authParams) {
|
||||
this.storageManager.setKeys(keys, authParams);
|
||||
return this.storageManager.decryptStorage();
|
||||
}
|
||||
|
||||
configureAutoLock() {
|
||||
PasscodeManager.AutoLockPollFocusInterval = 1 * MillisecondsPerSecond;
|
||||
|
||||
PasscodeManager.AutoLockIntervalNone = 0;
|
||||
PasscodeManager.AutoLockIntervalImmediate = 1;
|
||||
PasscodeManager.AutoLockIntervalOneMinute = 60 * MillisecondsPerSecond;
|
||||
PasscodeManager.AutoLockIntervalFiveMinutes = 300 * MillisecondsPerSecond;
|
||||
PasscodeManager.AutoLockIntervalOneHour = 3600 * MillisecondsPerSecond;
|
||||
|
||||
PasscodeManager.AutoLockIntervalKey = "AutoLockIntervalKey";
|
||||
|
||||
if(isDesktopApplication()) {
|
||||
// desktop only
|
||||
this.$rootScope.$on("window-lost-focus", () => {
|
||||
this.documentVisibilityChanged(false);
|
||||
})
|
||||
this.$rootScope.$on("window-gained-focus", () => {
|
||||
this.documentVisibilityChanged(true);
|
||||
})
|
||||
} else {
|
||||
// tab visibility listener, web only
|
||||
document.addEventListener('visibilitychange', (e) => {
|
||||
let visible = document.visibilityState == "visible";
|
||||
this.documentVisibilityChanged(visible);
|
||||
});
|
||||
|
||||
// verify document is in focus every so often as visibilitychange event is not triggered
|
||||
// on a typical window blur event but rather on tab changes
|
||||
this.pollFocusTimeout = setInterval(() => {
|
||||
let hasFocus = document.hasFocus();
|
||||
|
||||
if(hasFocus && this.lastFocusState == "hidden") {
|
||||
this.documentVisibilityChanged(true);
|
||||
} else if(!hasFocus && this.lastFocusState == "visible") {
|
||||
this.documentVisibilityChanged(false);
|
||||
}
|
||||
|
||||
// save this to compare against next time around
|
||||
this.lastFocusState = hasFocus ? "visible" : "hidden";
|
||||
}, PasscodeManager.AutoLockPollFocusInterval);
|
||||
}
|
||||
}
|
||||
|
||||
getAutoLockIntervalOptions() {
|
||||
return [
|
||||
{
|
||||
value: PasscodeManager.AutoLockIntervalNone,
|
||||
label: "Off"
|
||||
},
|
||||
{
|
||||
value: PasscodeManager.AutoLockIntervalImmediate,
|
||||
label: "Immediately"
|
||||
},
|
||||
{
|
||||
value: PasscodeManager.AutoLockIntervalOneMinute,
|
||||
label: "1m"
|
||||
},
|
||||
{
|
||||
value: PasscodeManager.AutoLockIntervalFiveMinutes,
|
||||
label: "5m"
|
||||
},
|
||||
{
|
||||
value: PasscodeManager.AutoLockIntervalOneHour,
|
||||
label: "1h"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
documentVisibilityChanged(visible) {
|
||||
if(visible) {
|
||||
// check to see if lockAfterDate is not null, and if the application isn't locked.
|
||||
// if that's the case, it needs to be locked immediately.
|
||||
if(this.lockAfterDate && new Date() > this.lockAfterDate && !this.isLocked()) {
|
||||
this.lockApplication();
|
||||
} else {
|
||||
if(!this.isLocked()) {
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}
|
||||
this.cancelAutoLockTimer();
|
||||
} else {
|
||||
this.beginAutoLockTimer();
|
||||
}
|
||||
|
||||
this.notifiyVisibilityObservers(visible);
|
||||
}
|
||||
|
||||
async beginAutoLockTimer() {
|
||||
var interval = await this.getAutoLockInterval();
|
||||
if(interval == PasscodeManager.AutoLockIntervalNone) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a timeout if possible, but if the computer is put to sleep, timeouts won't work.
|
||||
// Need to set a date as backup. this.lockAfterDate does not need to be persisted, as
|
||||
// living in memory seems sufficient. If memory is cleared, then the application will lock anyway.
|
||||
let addToNow = (seconds) => {
|
||||
let date = new Date();
|
||||
date.setSeconds(date.getSeconds() + seconds);
|
||||
return date;
|
||||
}
|
||||
|
||||
this.lockAfterDate = addToNow(interval / MillisecondsPerSecond);
|
||||
this.lockTimeout = setTimeout(() => {
|
||||
this.lockApplication();
|
||||
// We don't need to look at this anymore since we've succeeded with timeout lock
|
||||
this.lockAfterDate = null;
|
||||
}, interval);
|
||||
}
|
||||
|
||||
cancelAutoLockTimer() {
|
||||
clearTimeout(this.lockTimeout);
|
||||
this.lockAfterDate = null;
|
||||
}
|
||||
}
|
||||
86
app/assets/javascripts/services/preferencesManager.js
Normal file
86
app/assets/javascripts/services/preferencesManager.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { SFPredicate, SFItem } from 'snjs';
|
||||
|
||||
export const PREF_TAGS_PANEL_WIDTH = 'tagsPanelWidth';
|
||||
export const PREF_NOTES_PANEL_WIDTH = 'notesPanelWidth';
|
||||
export const PREF_EDITOR_WIDTH = 'editorWidth';
|
||||
export const PREF_EDITOR_LEFT = 'editorLeft';
|
||||
export const PREF_EDITOR_MONOSPACE_ENABLED = 'monospaceFont';
|
||||
export const PREF_EDITOR_SPELLCHECK = 'spellcheck';
|
||||
export const PREF_EDITOR_RESIZERS_ENABLED = 'marginResizersEnabled';
|
||||
export const PREF_SORT_NOTES_BY = 'sortBy';
|
||||
export const PREF_SORT_NOTES_REVERSE = 'sortReverse';
|
||||
export const PREF_NOTES_SHOW_ARCHIVED = 'showArchived';
|
||||
export const PREF_NOTES_HIDE_PINNED = 'hidePinned';
|
||||
export const PREF_NOTES_HIDE_NOTE_PREVIEW = 'hideNotePreview';
|
||||
export const PREF_NOTES_HIDE_DATE = 'hideDate';
|
||||
export const PREF_NOTES_HIDE_TAGS = 'hideTags';
|
||||
|
||||
export class PreferencesManager {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
modelManager,
|
||||
singletonManager,
|
||||
appState,
|
||||
syncManager
|
||||
) {
|
||||
this.singletonManager = singletonManager;
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
this.appState = appState;
|
||||
|
||||
this.modelManager.addItemSyncObserver(
|
||||
'user-prefs',
|
||||
'SN|UserPreferences',
|
||||
(allItems, validItems, deletedItems, source, sourceKey) => {
|
||||
this.preferencesDidChange();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
load() {
|
||||
const prefsContentType = 'SN|UserPreferences';
|
||||
const contentTypePredicate = new SFPredicate(
|
||||
'content_type',
|
||||
'=',
|
||||
prefsContentType
|
||||
);
|
||||
this.singletonManager.registerSingleton(
|
||||
[contentTypePredicate],
|
||||
(resolvedSingleton) => {
|
||||
this.userPreferences = resolvedSingleton;
|
||||
},
|
||||
(valueCallback) => {
|
||||
// Safe to create. Create and return object.
|
||||
const prefs = new SFItem({content_type: prefsContentType});
|
||||
this.modelManager.addItem(prefs);
|
||||
this.modelManager.setItemDirty(prefs);
|
||||
this.syncManager.sync();
|
||||
valueCallback(prefs);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
preferencesDidChange() {
|
||||
this.appState.setUserPreferences(this.userPreferences);
|
||||
}
|
||||
|
||||
syncUserPreferences() {
|
||||
if(this.userPreferences) {
|
||||
this.modelManager.setItemDirty(this.userPreferences);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}
|
||||
|
||||
getValue(key, defaultValue) {
|
||||
if(!this.userPreferences) { return defaultValue; }
|
||||
const value = this.userPreferences.getAppDataItem(key);
|
||||
return (value !== undefined && value != null) ? value : defaultValue;
|
||||
}
|
||||
|
||||
setUserPrefValue(key, value, sync) {
|
||||
this.userPreferences.setAppDataItem(key, value);
|
||||
if(sync) {
|
||||
this.syncUserPreferences();
|
||||
}
|
||||
}
|
||||
}
|
||||
81
app/assets/javascripts/services/privilegesManager.js
Normal file
81
app/assets/javascripts/services/privilegesManager.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import angular from 'angular';
|
||||
import { SFPrivilegesManager } from 'snjs';
|
||||
|
||||
export class PrivilegesManager extends SFPrivilegesManager {
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
passcodeManager,
|
||||
authManager,
|
||||
syncManager,
|
||||
singletonManager,
|
||||
modelManager,
|
||||
storageManager,
|
||||
$rootScope,
|
||||
$compile
|
||||
) {
|
||||
super(modelManager, syncManager, singletonManager);
|
||||
|
||||
this.$rootScope = $rootScope;
|
||||
this.$compile = $compile;
|
||||
|
||||
this.setDelegate({
|
||||
isOffline: async () => {
|
||||
return authManager.offline();
|
||||
},
|
||||
hasLocalPasscode: async () => {
|
||||
return passcodeManager.hasPasscode();
|
||||
},
|
||||
saveToStorage: async (key, value) => {
|
||||
return storageManager.setItem(key, value, storageManager.bestStorageMode());
|
||||
},
|
||||
getFromStorage: async (key) => {
|
||||
return storageManager.getItem(key, storageManager.bestStorageMode());
|
||||
},
|
||||
verifyAccountPassword: async (password) => {
|
||||
return authManager.verifyAccountPassword(password);
|
||||
},
|
||||
verifyLocalPasscode: async (passcode) => {
|
||||
return passcodeManager.verifyPasscode(passcode);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async presentPrivilegesModal(action, onSuccess, onCancel) {
|
||||
if (this.authenticationInProgress()) {
|
||||
onCancel && onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
const customSuccess = async () => {
|
||||
onSuccess && await onSuccess();
|
||||
this.currentAuthenticationElement = null;
|
||||
}
|
||||
const customCancel = async () => {
|
||||
onCancel && await onCancel();
|
||||
this.currentAuthenticationElement = null;
|
||||
}
|
||||
|
||||
const scope = this.$rootScope.$new(true);
|
||||
scope.action = action;
|
||||
scope.onSuccess = customSuccess;
|
||||
scope.onCancel = customCancel;
|
||||
const el = this.$compile(`
|
||||
<privileges-auth-modal action='action' on-success='onSuccess'
|
||||
on-cancel='onCancel' class='sk-modal'></privileges-auth-modal>
|
||||
`)(scope);
|
||||
angular.element(document.body).append(el);
|
||||
|
||||
this.currentAuthenticationElement = el;
|
||||
}
|
||||
|
||||
presentPrivilegesManagementModal() {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
var el = this.$compile("<privileges-management-modal class='sk-modal'></privileges-management-modal>")(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
authenticationInProgress() {
|
||||
return this.currentAuthenticationElement != null;
|
||||
}
|
||||
}
|
||||
44
app/assets/javascripts/services/sessionHistory.js
Normal file
44
app/assets/javascripts/services/sessionHistory.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NoteHistoryEntry } from '@/models/noteHistoryEntry';
|
||||
import { SFSessionHistoryManager , SFItemHistory } from 'snjs';
|
||||
|
||||
export class SessionHistory extends SFSessionHistoryManager {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
modelManager,
|
||||
storageManager,
|
||||
authManager,
|
||||
passcodeManager,
|
||||
$timeout
|
||||
) {
|
||||
SFItemHistory.HistoryEntryClassMapping = {
|
||||
"Note" : NoteHistoryEntry
|
||||
}
|
||||
|
||||
// Session History can be encrypted with passcode keys. If it changes, we need to resave session
|
||||
// history with the new keys.
|
||||
passcodeManager.addPasscodeChangeObserver(() => {
|
||||
this.saveToDisk();
|
||||
})
|
||||
|
||||
var keyRequestHandler = async () => {
|
||||
let offline = authManager.offline();
|
||||
let auth_params = offline ? passcodeManager.passcodeAuthParams() : await authManager.getAuthParams();
|
||||
let keys = offline ? passcodeManager.keys() : await authManager.keys();
|
||||
|
||||
return {
|
||||
keys: keys,
|
||||
offline: offline,
|
||||
auth_params: auth_params
|
||||
}
|
||||
}
|
||||
|
||||
var contentTypes = ["Note"];
|
||||
super(
|
||||
modelManager,
|
||||
storageManager,
|
||||
keyRequestHandler,
|
||||
contentTypes,
|
||||
$timeout
|
||||
);
|
||||
}
|
||||
}
|
||||
10
app/assets/javascripts/services/singletonManager.js
Normal file
10
app/assets/javascripts/services/singletonManager.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { SFSingletonManager } from 'snjs';
|
||||
|
||||
export class SingletonManager extends SFSingletonManager {
|
||||
// constructor needed for angularjs injection to work
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
/* @ngInject */
|
||||
constructor(modelManager, syncManager) {
|
||||
super(modelManager, syncManager);
|
||||
}
|
||||
}
|
||||
64
app/assets/javascripts/services/statusManager.js
Normal file
64
app/assets/javascripts/services/statusManager.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class StatusManager {
|
||||
constructor() {
|
||||
this.statuses = [];
|
||||
this.observers = [];
|
||||
}
|
||||
|
||||
statusFromString(string) {
|
||||
return {string: string};
|
||||
}
|
||||
|
||||
replaceStatusWithString(status, string) {
|
||||
this.removeStatus(status);
|
||||
return this.addStatusFromString(string);
|
||||
}
|
||||
|
||||
addStatusFromString(string) {
|
||||
return this.addStatus(this.statusFromString(string));
|
||||
}
|
||||
|
||||
addStatus(status) {
|
||||
if(typeof status !== "object") {
|
||||
console.error("Attempting to set non-object status", status);
|
||||
return;
|
||||
}
|
||||
|
||||
this.statuses.push(status);
|
||||
this.notifyObservers();
|
||||
return status;
|
||||
}
|
||||
|
||||
removeStatus(status) {
|
||||
_.pull(this.statuses, status);
|
||||
this.notifyObservers();
|
||||
return null;
|
||||
}
|
||||
|
||||
getStatusString() {
|
||||
let result = "";
|
||||
this.statuses.forEach((status, index) => {
|
||||
if(index > 0) {
|
||||
result += " ";
|
||||
}
|
||||
result += status.string;
|
||||
})
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
notifyObservers() {
|
||||
for(const observer of this.observers) {
|
||||
observer(this.getStatusString());
|
||||
}
|
||||
}
|
||||
|
||||
addStatusObserver(callback) {
|
||||
this.observers.push(callback);
|
||||
}
|
||||
|
||||
removeStatusObserver(callback) {
|
||||
_.pull(this.statuses, callback);
|
||||
}
|
||||
}
|
||||
241
app/assets/javascripts/services/storageManager.js
Normal file
241
app/assets/javascripts/services/storageManager.js
Normal file
@@ -0,0 +1,241 @@
|
||||
import { protocolManager, SNEncryptedStorage, SFStorageManager , SFItemParams } from 'snjs';
|
||||
|
||||
export class MemoryStorage {
|
||||
constructor() {
|
||||
this.memory = {};
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
return this.memory[key] || null;
|
||||
}
|
||||
|
||||
getItemSync(key) {
|
||||
return this.getItem(key);
|
||||
}
|
||||
|
||||
get length() {
|
||||
return Object.keys(this.memory).length;
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
this.memory[key] = value;
|
||||
}
|
||||
|
||||
removeItem(key) {
|
||||
delete this.memory[key];
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.memory = {};
|
||||
}
|
||||
|
||||
keys() {
|
||||
return Object.keys(this.memory);
|
||||
}
|
||||
|
||||
key(index) {
|
||||
return Object.keys(this.memory)[index];
|
||||
}
|
||||
}
|
||||
|
||||
export class StorageManager extends SFStorageManager {
|
||||
|
||||
/* @ngInject */
|
||||
constructor(dbManager, alertManager) {
|
||||
super();
|
||||
this.dbManager = dbManager;
|
||||
this.alertManager = alertManager;
|
||||
}
|
||||
|
||||
initialize(hasPasscode, ephemeral) {
|
||||
if(hasPasscode) {
|
||||
// We don't want to save anything in fixed storage except for actual item data (in IndexedDB)
|
||||
this.storage = this.memoryStorage;
|
||||
this.itemsStorageMode = StorageManager.FixedEncrypted;
|
||||
} else if(ephemeral) {
|
||||
// We don't want to save anything in fixed storage as well as IndexedDB
|
||||
this.storage = this.memoryStorage;
|
||||
this.itemsStorageMode = StorageManager.Ephemeral;
|
||||
} else {
|
||||
this.storage = localStorage;
|
||||
this.itemsStorageMode = StorageManager.Fixed;
|
||||
}
|
||||
|
||||
this.modelStorageMode = ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed;
|
||||
}
|
||||
|
||||
get memoryStorage() {
|
||||
if(!this._memoryStorage) {
|
||||
this._memoryStorage = new MemoryStorage();
|
||||
}
|
||||
return this._memoryStorage;
|
||||
}
|
||||
|
||||
setItemsMode(mode, force) {
|
||||
var newStorage = this.getVault(mode);
|
||||
if(newStorage !== this.storage || mode !== this.itemsStorageMode || force) {
|
||||
// transfer storages
|
||||
var length = this.storage.length;
|
||||
for(var i = 0; i < length; i++) {
|
||||
var key = this.storage.key(i);
|
||||
newStorage.setItem(key, this.storage.getItem(key));
|
||||
}
|
||||
|
||||
this.itemsStorageMode = mode;
|
||||
if(newStorage !== this.storage) {
|
||||
// Only clear if this.storage isn't the same reference as newStorage
|
||||
this.storage.clear();
|
||||
}
|
||||
this.storage = newStorage;
|
||||
|
||||
if(mode == StorageManager.FixedEncrypted) {
|
||||
this.writeEncryptedStorageToDisk();
|
||||
} else if(mode == StorageManager.Fixed) {
|
||||
// Remove encrypted storage
|
||||
this.removeItem("encryptedStorage", StorageManager.Fixed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getVault(vaultKey) {
|
||||
if(vaultKey) {
|
||||
if(vaultKey == StorageManager.Ephemeral || vaultKey == StorageManager.FixedEncrypted) {
|
||||
return this.memoryStorage;
|
||||
} else {
|
||||
return localStorage;
|
||||
}
|
||||
} else {
|
||||
return this.storage;
|
||||
}
|
||||
}
|
||||
|
||||
async setItem(key, value, vaultKey) {
|
||||
var storage = this.getVault(vaultKey);
|
||||
try {
|
||||
storage.setItem(key, value);
|
||||
} catch (e) {
|
||||
console.error("Exception while trying to setItem in StorageManager:", e);
|
||||
this.alertManager.alert({text: "The application's local storage is out of space. If you have Session History save-to-disk enabled, please disable it, and try again."});
|
||||
}
|
||||
|
||||
if(vaultKey === StorageManager.FixedEncrypted || (!vaultKey && this.itemsStorageMode === StorageManager.FixedEncrypted)) {
|
||||
return this.writeEncryptedStorageToDisk();
|
||||
}
|
||||
}
|
||||
|
||||
async getItem(key, vault) {
|
||||
return this.getItemSync(key, vault);
|
||||
}
|
||||
|
||||
getItemSync(key, vault) {
|
||||
var storage = this.getVault(vault);
|
||||
return storage.getItem(key);
|
||||
}
|
||||
|
||||
async removeItem(key, vault) {
|
||||
var storage = this.getVault(vault);
|
||||
return storage.removeItem(key);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
this.memoryStorage.clear();
|
||||
localStorage.clear();
|
||||
}
|
||||
|
||||
storageAsHash() {
|
||||
var hash = {};
|
||||
var length = this.storage.length;
|
||||
for(var i = 0; i < length; i++) {
|
||||
var key = this.storage.key(i);
|
||||
hash[key] = this.storage.getItem(key)
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
setKeys(keys, authParams) {
|
||||
this.encryptedStorageKeys = keys;
|
||||
this.encryptedStorageAuthParams = authParams;
|
||||
}
|
||||
|
||||
async writeEncryptedStorageToDisk() {
|
||||
var encryptedStorage = new SNEncryptedStorage();
|
||||
// Copy over totality of current storage
|
||||
encryptedStorage.content.storage = this.storageAsHash();
|
||||
|
||||
// Save new encrypted storage in Fixed storage
|
||||
var params = new SFItemParams(encryptedStorage, this.encryptedStorageKeys, this.encryptedStorageAuthParams);
|
||||
const syncParams = await params.paramsForSync();
|
||||
this.setItem("encryptedStorage", JSON.stringify(syncParams), StorageManager.Fixed);
|
||||
}
|
||||
|
||||
async decryptStorage() {
|
||||
var stored = JSON.parse(this.getItemSync("encryptedStorage", StorageManager.Fixed));
|
||||
await protocolManager.decryptItem(stored, this.encryptedStorageKeys);
|
||||
var encryptedStorage = new SNEncryptedStorage(stored);
|
||||
|
||||
for(var key of Object.keys(encryptedStorage.content.storage)) {
|
||||
this.setItem(key, encryptedStorage.storage[key]);
|
||||
}
|
||||
}
|
||||
|
||||
hasPasscode() {
|
||||
return this.getItemSync("encryptedStorage", StorageManager.Fixed) !== null;
|
||||
}
|
||||
|
||||
bestStorageMode() {
|
||||
return this.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Fixed;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Model Storage
|
||||
|
||||
If using ephemeral storage, we don't need to write it to anything as references will be held already by controllers
|
||||
and the global modelManager service.
|
||||
*/
|
||||
|
||||
setModelStorageMode(mode) {
|
||||
if(mode == this.modelStorageMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(mode == StorageManager.Ephemeral) {
|
||||
// Clear IndexedDB
|
||||
this.dbManager.clearAllModels(null);
|
||||
} else {
|
||||
// Fixed
|
||||
}
|
||||
|
||||
this.modelStorageMode = mode;
|
||||
}
|
||||
|
||||
async getAllModels() {
|
||||
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||
return this.dbManager.getAllModels();
|
||||
}
|
||||
}
|
||||
|
||||
async saveModel(item) {
|
||||
return this.saveModels([item]);
|
||||
}
|
||||
|
||||
async saveModels(items, onsuccess, onerror) {
|
||||
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||
return this.dbManager.saveModels(items);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteModel(item) {
|
||||
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||
return this.dbManager.deleteModel(item);
|
||||
}
|
||||
}
|
||||
|
||||
async clearAllModels() {
|
||||
return this.dbManager.clearAllModels();
|
||||
}
|
||||
}
|
||||
|
||||
StorageManager.FixedEncrypted = "FixedEncrypted"; // encrypted memoryStorage + localStorage persistence
|
||||
StorageManager.Ephemeral = "Ephemeral"; // memoryStorage
|
||||
StorageManager.Fixed = "Fixed"; // localStorage
|
||||
30
app/assets/javascripts/services/syncManager.js
Normal file
30
app/assets/javascripts/services/syncManager.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import angular from 'angular';
|
||||
import { SFSyncManager } from 'snjs';
|
||||
|
||||
export class SyncManager extends SFSyncManager {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
modelManager,
|
||||
storageManager,
|
||||
httpManager,
|
||||
$timeout,
|
||||
$interval,
|
||||
$compile,
|
||||
$rootScope
|
||||
) {
|
||||
super(modelManager, storageManager, httpManager, $timeout, $interval);
|
||||
this.$rootScope = $rootScope;
|
||||
this.$compile = $compile;
|
||||
|
||||
// this.loggingEnabled = true;
|
||||
}
|
||||
|
||||
presentConflictResolutionModal(items, callback) {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.item1 = items[0];
|
||||
scope.item2 = items[1];
|
||||
scope.callback = callback;
|
||||
var el = this.$compile( "<conflict-resolution-modal item1='item1' item2='item2' callback='callback' class='sk-modal'></conflict-resolution-modal>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
}
|
||||
150
app/assets/javascripts/services/themeManager.js
Normal file
150
app/assets/javascripts/services/themeManager.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import { SNTheme, SFItemParams } from 'snjs';
|
||||
import { StorageManager } from './storageManager';
|
||||
import {
|
||||
APP_STATE_EVENT_DESKTOP_EXTS_READY
|
||||
} from '@/state';
|
||||
|
||||
export class ThemeManager {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
componentManager,
|
||||
desktopManager,
|
||||
storageManager,
|
||||
passcodeManager,
|
||||
appState
|
||||
) {
|
||||
this.componentManager = componentManager;
|
||||
this.storageManager = storageManager;
|
||||
this.desktopManager = desktopManager;
|
||||
this.activeThemes = [];
|
||||
|
||||
ThemeManager.CachedThemesKey = "cachedThemes";
|
||||
|
||||
this.registerObservers();
|
||||
|
||||
// When a passcode is added, all local storage will be encrypted (it doesn't know what was
|
||||
// originally saved as Fixed or FixedEncrypted). We want to rewrite cached themes here to Fixed
|
||||
// so that it's readable without authentication.
|
||||
passcodeManager.addPasscodeChangeObserver(() => {
|
||||
this.cacheThemes();
|
||||
})
|
||||
|
||||
if (desktopManager.isDesktop) {
|
||||
appState.addObserver((eventName, data) => {
|
||||
if (eventName === APP_STATE_EVENT_DESKTOP_EXTS_READY) {
|
||||
this.activateCachedThemes();
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.activateCachedThemes();
|
||||
}
|
||||
}
|
||||
|
||||
activateCachedThemes() {
|
||||
const cachedThemes = this.getCachedThemes();
|
||||
const writeToCache = false;
|
||||
for (const theme of cachedThemes) {
|
||||
this.activateTheme(theme, writeToCache);
|
||||
}
|
||||
}
|
||||
|
||||
registerObservers() {
|
||||
this.desktopManager.registerUpdateObserver((component) => {
|
||||
// Reload theme if active
|
||||
if (component.active && component.isTheme()) {
|
||||
this.deactivateTheme(component);
|
||||
setTimeout(() => {
|
||||
this.activateTheme(component);
|
||||
}, 10);
|
||||
}
|
||||
})
|
||||
|
||||
this.componentManager.registerHandler({
|
||||
identifier: "themeManager",
|
||||
areas: ["themes"],
|
||||
activationHandler: (component) => {
|
||||
if (component.active) {
|
||||
this.activateTheme(component);
|
||||
} else {
|
||||
this.deactivateTheme(component);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hasActiveTheme() {
|
||||
return this.componentManager.getActiveThemes().length > 0;
|
||||
}
|
||||
|
||||
deactivateAllThemes() {
|
||||
var activeThemes = this.componentManager.getActiveThemes();
|
||||
for (var theme of activeThemes) {
|
||||
if (theme) {
|
||||
this.componentManager.deactivateComponent(theme);
|
||||
}
|
||||
}
|
||||
|
||||
this.decacheThemes();
|
||||
}
|
||||
|
||||
activateTheme(theme, writeToCache = true) {
|
||||
if (_.find(this.activeThemes, { uuid: theme.uuid })) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeThemes.push(theme);
|
||||
|
||||
var url = this.componentManager.urlForComponent(theme);
|
||||
var link = document.createElement("link");
|
||||
link.href = url;
|
||||
link.type = "text/css";
|
||||
link.rel = "stylesheet";
|
||||
link.media = "screen,print";
|
||||
link.id = theme.uuid;
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
|
||||
if (writeToCache) {
|
||||
this.cacheThemes();
|
||||
}
|
||||
}
|
||||
|
||||
deactivateTheme(theme) {
|
||||
var element = document.getElementById(theme.uuid);
|
||||
if (element) {
|
||||
element.disabled = true;
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
|
||||
_.remove(this.activeThemes, { uuid: theme.uuid });
|
||||
|
||||
this.cacheThemes();
|
||||
}
|
||||
|
||||
async cacheThemes() {
|
||||
let mapped = await Promise.all(this.activeThemes.map(async (theme) => {
|
||||
let transformer = new SFItemParams(theme);
|
||||
let params = await transformer.paramsForLocalStorage();
|
||||
return params;
|
||||
}));
|
||||
let data = JSON.stringify(mapped);
|
||||
return this.storageManager.setItem(ThemeManager.CachedThemesKey, data, StorageManager.Fixed);
|
||||
}
|
||||
|
||||
async decacheThemes() {
|
||||
return this.storageManager.removeItem(ThemeManager.CachedThemesKey, StorageManager.Fixed);
|
||||
}
|
||||
|
||||
getCachedThemes() {
|
||||
let cachedThemes = this.storageManager.getItemSync(ThemeManager.CachedThemesKey, StorageManager.Fixed);
|
||||
if (cachedThemes) {
|
||||
let parsed = JSON.parse(cachedThemes);
|
||||
return parsed.map((theme) => {
|
||||
return new SNTheme(theme);
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user