Merge branch '004' into develop
This commit is contained in:
@@ -1,297 +0,0 @@
|
||||
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,
|
||||
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]
|
||||
};
|
||||
/* Needed until SNJS detects null function */
|
||||
const emptyFunc = () => { };
|
||||
return this.httpManager.postAbsolute(
|
||||
action.url,
|
||||
params,
|
||||
emptyFunc,
|
||||
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) {
|
||||
/* Needed until SNJS detects null function */
|
||||
const emptyFunc = () => {};
|
||||
const onConfirm = async () => {
|
||||
return this.httpManager.getAbsolute(action.url, {}, emptyFunc, 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) {
|
||||
/* Needed until SNJS detects null function */
|
||||
const emptyFunc = () => {};
|
||||
return this.httpManager.getAbsolute(
|
||||
action.url,
|
||||
{},
|
||||
emptyFunc,
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
101
app/assets/javascripts/services/alertService.ts
Normal file
101
app/assets/javascripts/services/alertService.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/* eslint-disable prefer-promise-reject-errors */
|
||||
import { SNAlertService, ButtonType, DismissBlockingDialog } from 'snjs';
|
||||
import { SKAlert } from 'sn-stylekit';
|
||||
|
||||
/** @returns a promise resolving to true if the user confirmed, false if they canceled */
|
||||
export function confirmDialog({
|
||||
text,
|
||||
title,
|
||||
confirmButtonText = 'Confirm',
|
||||
cancelButtonText = 'Cancel',
|
||||
confirmButtonStyle = 'info',
|
||||
}: {
|
||||
text: string;
|
||||
title?: string;
|
||||
confirmButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
confirmButtonStyle?: 'danger' | 'info';
|
||||
}) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const alert = new SKAlert({
|
||||
title,
|
||||
text,
|
||||
buttons: [
|
||||
{
|
||||
text: cancelButtonText,
|
||||
style: 'neutral',
|
||||
action() {
|
||||
resolve(false);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: confirmButtonText,
|
||||
style: confirmButtonStyle,
|
||||
action() {
|
||||
resolve(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
alert.present();
|
||||
});
|
||||
}
|
||||
|
||||
export function alertDialog({
|
||||
title,
|
||||
text,
|
||||
closeButtonText = 'OK',
|
||||
}: {
|
||||
title?: string;
|
||||
text: string;
|
||||
closeButtonText?: string;
|
||||
}) {
|
||||
return new Promise<void>((resolve) => {
|
||||
const alert = new SKAlert({
|
||||
title,
|
||||
text,
|
||||
buttons: [
|
||||
{
|
||||
text: closeButtonText,
|
||||
style: 'neutral',
|
||||
action: resolve,
|
||||
},
|
||||
],
|
||||
});
|
||||
alert.present();
|
||||
});
|
||||
}
|
||||
|
||||
export class AlertService implements SNAlertService {
|
||||
/**
|
||||
* @deprecated use the standalone `alertDialog` function instead
|
||||
*/
|
||||
alert(text: string, title?: string, closeButtonText?: string) {
|
||||
return alertDialog({ text, title, closeButtonText });
|
||||
}
|
||||
|
||||
confirm(
|
||||
text: string,
|
||||
title?: string,
|
||||
confirmButtonText?: string,
|
||||
confirmButtonType?: ButtonType,
|
||||
cancelButtonText?: string
|
||||
): Promise<boolean> {
|
||||
return confirmDialog({
|
||||
text,
|
||||
title,
|
||||
confirmButtonText,
|
||||
cancelButtonText,
|
||||
confirmButtonStyle:
|
||||
confirmButtonType === ButtonType.Danger ? 'danger' : 'info',
|
||||
});
|
||||
}
|
||||
|
||||
blockingDialog(text: string) {
|
||||
const alert = new SKAlert({ text });
|
||||
alert.present();
|
||||
return () => {
|
||||
alert.dismiss();
|
||||
};
|
||||
}
|
||||
}
|
||||
176
app/assets/javascripts/services/archiveManager.ts
Normal file
176
app/assets/javascripts/services/archiveManager.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { EncryptionIntent, ProtectedAction, SNItem, ContentType, SNNote } from 'snjs';
|
||||
|
||||
function zippableTxtName(name: string, suffix = ""): string {
|
||||
const sanitizedName = name
|
||||
.replace(/\//g, '')
|
||||
.replace(/\\+/g, '')
|
||||
.replace(/:/g, ' ')
|
||||
.replace(/\./g, ' ');
|
||||
const nameEnd = suffix + ".txt";
|
||||
const maxFileNameLength = 255;
|
||||
return sanitizedName.slice(0, maxFileNameLength - nameEnd.length) + nameEnd;
|
||||
}
|
||||
|
||||
export class ArchiveManager {
|
||||
|
||||
private readonly application: WebApplication
|
||||
private textFile?: string
|
||||
|
||||
constructor(application: WebApplication) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
public async downloadBackup(encrypted: boolean) {
|
||||
const items = this.application.allItems();
|
||||
|
||||
const run = async () => {
|
||||
// download in Standard Notes format
|
||||
const intent = encrypted
|
||||
? EncryptionIntent.FileEncrypted
|
||||
: EncryptionIntent.FileDecrypted;
|
||||
const data = await this.itemsData(items, intent)
|
||||
if (encrypted) {
|
||||
this.downloadData(
|
||||
data!,
|
||||
`Standard Notes Encrypted Backup - ${this.formattedDate()}.txt`
|
||||
);
|
||||
} else {
|
||||
const data = await this.application.createBackupFile(items, intent);
|
||||
if (data) {
|
||||
/** download as zipped plain text files */
|
||||
this.downloadZippedItems(
|
||||
items,
|
||||
/** Add the backup file to the archive */
|
||||
(zipWriter, zip) => new Promise((resolve) => {
|
||||
const blob = new Blob([data], { type: 'text/plain' });
|
||||
const fileName = zippableTxtName(
|
||||
`Standard Notes Decrypted Backup - ${this.formattedDate()}`
|
||||
);
|
||||
zipWriter.add(fileName, new zip.BlobReader(blob), resolve);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
await this.application.privilegesService!
|
||||
.actionRequiresPrivilege(ProtectedAction.ManageBackups)
|
||||
) {
|
||||
this.application.presentPrivilegesModal(
|
||||
ProtectedAction.ManageBackups,
|
||||
() => {
|
||||
run();
|
||||
});
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
private formattedDate() {
|
||||
const string = `${new Date()}`;
|
||||
// Match up to the first parenthesis, i.e do not include '(Central Standard Time)'
|
||||
const matches = string.match(/^(.*?) \(/);
|
||||
if (matches && matches.length >= 2) {
|
||||
return matches[1];
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
private async itemsData(items: SNItem[], intent: EncryptionIntent) {
|
||||
const data = await this.application.createBackupFile(items, intent);
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
const blobData = new Blob([data], { type: 'text/json' });
|
||||
return blobData;
|
||||
}
|
||||
|
||||
private get zip() {
|
||||
return (window as any).zip;
|
||||
}
|
||||
|
||||
private async loadZip() {
|
||||
if (this.zip) {
|
||||
return;
|
||||
}
|
||||
const scriptTag = document.createElement('script');
|
||||
scriptTag.src = '/assets/zip/zip.js';
|
||||
scriptTag.async = false;
|
||||
const headTag = document.getElementsByTagName('head')[0];
|
||||
headTag.appendChild(scriptTag);
|
||||
return new Promise((resolve) => {
|
||||
scriptTag.onload = () => {
|
||||
this.zip.workerScriptsPath = 'assets/zip/';
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async downloadZippedItems(
|
||||
items: SNItem[],
|
||||
onOpenZip: (zipWriter: any, zip: any) => Promise<void>
|
||||
) {
|
||||
await this.loadZip();
|
||||
this.zip.createWriter(
|
||||
new this.zip.BlobWriter('application/zip'),
|
||||
async (zipWriter: any) => {
|
||||
await onOpenZip(zipWriter, this.zip);
|
||||
let index = 0;
|
||||
const nextFile = () => {
|
||||
const item = items[index];
|
||||
let name, contents;
|
||||
if (item.content_type === ContentType.Note) {
|
||||
const note = item as SNNote;
|
||||
name = note.title;
|
||||
contents = note.text;
|
||||
} else {
|
||||
name = item.content_type;
|
||||
contents = JSON.stringify(item.content, null, 2);
|
||||
}
|
||||
if (!name) {
|
||||
name = '';
|
||||
}
|
||||
const blob = new Blob([contents], { type: 'text/plain' });
|
||||
const fileName = item.content_type + '/' +
|
||||
zippableTxtName(name, `-${item.uuid.split('-')[0]}`);
|
||||
zipWriter.add(fileName, new this.zip.BlobReader(blob), () => {
|
||||
index++;
|
||||
if (index < items.length) {
|
||||
nextFile();
|
||||
} else {
|
||||
zipWriter.close((blob: any) => {
|
||||
this.downloadData(
|
||||
blob,
|
||||
`Standard Notes Backup - ${this.formattedDate()}.zip`
|
||||
);
|
||||
zipWriter = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
nextFile();
|
||||
}, onerror);
|
||||
}
|
||||
|
||||
private hrefForData(data: Blob) {
|
||||
// If we are replacing a previously generated file we need to
|
||||
// manually revoke the object URL to avoid memory leaks.
|
||||
if (this.textFile) {
|
||||
window.URL.revokeObjectURL(this.textFile);
|
||||
}
|
||||
this.textFile = window.URL.createObjectURL(data);
|
||||
// returns a URL you can use as a href
|
||||
return this.textFile;
|
||||
}
|
||||
|
||||
private downloadData(data: Blob, fileName: string) {
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('download', fileName);
|
||||
link.href = this.hrefForData(data);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
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) {
|
||||
const authParams = await this.getAuthParams();
|
||||
const keys = await protocolManager.computeEncryptionKeysForUser(password, authParams);
|
||||
const success = keys.mk === (await this.keys()).mk;
|
||||
return success;
|
||||
}
|
||||
|
||||
async checkForSecurityUpdate() {
|
||||
if(this.offline()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const latest = protocolManager.version();
|
||||
const 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;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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) {
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
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();
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
// 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
|
||||
});
|
||||
}
|
||||
}
|
||||
243
app/assets/javascripts/services/desktopManager.ts
Normal file
243
app/assets/javascripts/services/desktopManager.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { SNComponent, PurePayload, ComponentMutator, AppDataField } from 'snjs';
|
||||
/* eslint-disable camelcase */
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
// An interface used by the Desktop app to interact with SN
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import { EncryptionIntent, ApplicationService, ApplicationEvent, removeFromArray } from 'snjs';
|
||||
|
||||
type UpdateObserverCallback = (component: SNComponent) => void
|
||||
type ComponentActivationCallback = (payload: PurePayload) => void
|
||||
type ComponentActivationObserver = {
|
||||
id: string,
|
||||
callback: ComponentActivationCallback
|
||||
}
|
||||
|
||||
export class DesktopManager extends ApplicationService {
|
||||
|
||||
$rootScope: ng.IRootScopeService
|
||||
$timeout: ng.ITimeoutService
|
||||
componentActivationObservers: ComponentActivationObserver[] = []
|
||||
updateObservers: {
|
||||
callback: UpdateObserverCallback
|
||||
}[] = []
|
||||
isDesktop = isDesktopApplication();
|
||||
|
||||
dataLoaded = false
|
||||
dataLoadHandler?: () => void
|
||||
majorDataChangeHandler?: () => void
|
||||
extServerHost?: string
|
||||
installationSyncHandler?: (payloads: PurePayload[]) => void
|
||||
installComponentHandler?: (payload: PurePayload) => void
|
||||
lastSearchedText?: string
|
||||
searchHandler?: (text?: string) => void
|
||||
|
||||
constructor(
|
||||
$rootScope: ng.IRootScopeService,
|
||||
$timeout: ng.ITimeoutService,
|
||||
application: WebApplication
|
||||
) {
|
||||
super(application);
|
||||
this.$rootScope = $rootScope;
|
||||
this.$timeout = $timeout;
|
||||
}
|
||||
|
||||
get webApplication() {
|
||||
return this.application as WebApplication;
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.componentActivationObservers.length = 0;
|
||||
this.updateObservers.length = 0;
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
async onAppEvent(eventName: ApplicationEvent) {
|
||||
super.onAppEvent(eventName);
|
||||
if (eventName === ApplicationEvent.LocalDataLoaded) {
|
||||
this.dataLoaded = true;
|
||||
if (this.dataLoadHandler) {
|
||||
this.dataLoadHandler();
|
||||
}
|
||||
} else if (eventName === ApplicationEvent.MajorDataChange) {
|
||||
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: SNComponent) {
|
||||
return this.application!.protocolService!.payloadByEncryptingPayload(
|
||||
component.payloadRepresentation(),
|
||||
EncryptionIntent.FileDecrypted
|
||||
);
|
||||
}
|
||||
|
||||
// All `components` should be installed
|
||||
syncComponentsInstallation(components: SNComponent[]) {
|
||||
if (!this.isDesktop) {
|
||||
return;
|
||||
}
|
||||
Promise.all(components.map((component) => {
|
||||
return this.convertComponentForTransmission(component);
|
||||
})).then((payloads) => {
|
||||
this.installationSyncHandler!(payloads);
|
||||
});
|
||||
}
|
||||
|
||||
async installComponent(component: SNComponent) {
|
||||
this.installComponentHandler!(
|
||||
await this.convertComponentForTransmission(component)
|
||||
);
|
||||
}
|
||||
|
||||
registerUpdateObserver(callback: UpdateObserverCallback) {
|
||||
const observer = {
|
||||
callback: callback
|
||||
};
|
||||
this.updateObservers.push(observer);
|
||||
return () => {
|
||||
removeFromArray(this.updateObservers, observer);
|
||||
};
|
||||
}
|
||||
|
||||
searchText(text?: string) {
|
||||
if (!this.isDesktop) {
|
||||
return;
|
||||
}
|
||||
this.lastSearchedText = text;
|
||||
this.searchHandler && this.searchHandler(text);
|
||||
}
|
||||
|
||||
redoSearch() {
|
||||
if (this.lastSearchedText) {
|
||||
this.searchText(this.lastSearchedText);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass null to cancel search
|
||||
desktop_setSearchHandler(handler: (text?: string) => void) {
|
||||
this.searchHandler = handler;
|
||||
}
|
||||
|
||||
desktop_windowGainedFocus() {
|
||||
this.$rootScope.$broadcast('window-gained-focus');
|
||||
}
|
||||
|
||||
desktop_windowLostFocus() {
|
||||
this.$rootScope.$broadcast('window-lost-focus');
|
||||
}
|
||||
|
||||
async desktop_onComponentInstallationComplete(
|
||||
componentData: any,
|
||||
error: any
|
||||
) {
|
||||
const component = this.application!.findItem(componentData.uuid);
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
const updatedComponent = await this.application!.changeAndSaveItem(
|
||||
component.uuid,
|
||||
(m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
if (error) {
|
||||
mutator.setAppDataItem(
|
||||
AppDataField.ComponentInstallError,
|
||||
error
|
||||
);
|
||||
} else {
|
||||
mutator.local_url = componentData.content.local_url;
|
||||
mutator.package_info = componentData.content.package_info;
|
||||
mutator.setAppDataItem(
|
||||
AppDataField.ComponentInstallError,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
this.$timeout(() => {
|
||||
for (const observer of this.updateObservers) {
|
||||
observer.callback(updatedComponent as SNComponent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
desktop_registerComponentActivationObserver(callback: ComponentActivationCallback) {
|
||||
const observer = { id: `${Math.random}`, callback: callback };
|
||||
this.componentActivationObservers.push(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
desktop_deregisterComponentActivationObserver(observer: ComponentActivationObserver) {
|
||||
removeFromArray(this.componentActivationObservers, observer);
|
||||
}
|
||||
|
||||
/* Notify observers that a component has been registered/activated */
|
||||
async notifyComponentActivation(component: SNComponent) {
|
||||
const serializedComponent = await this.convertComponentForTransmission(
|
||||
component
|
||||
);
|
||||
this.$timeout(() => {
|
||||
for (const observer of this.componentActivationObservers) {
|
||||
observer.callback(serializedComponent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Used to resolve 'sn://' */
|
||||
desktop_setExtServerHost(host: string) {
|
||||
this.extServerHost = host;
|
||||
this.webApplication.getAppState().desktopExtensionsReady();
|
||||
}
|
||||
|
||||
desktop_setComponentInstallationSyncHandler(handler: (payloads: PurePayload[]) => void) {
|
||||
this.installationSyncHandler = handler;
|
||||
}
|
||||
|
||||
desktop_setInstallComponentHandler(handler: (payload: PurePayload) => void) {
|
||||
this.installComponentHandler = handler;
|
||||
}
|
||||
|
||||
desktop_setInitialDataLoadHandler(handler: () => void) {
|
||||
this.dataLoadHandler = handler;
|
||||
if (this.dataLoaded) {
|
||||
this.dataLoadHandler();
|
||||
}
|
||||
}
|
||||
|
||||
async desktop_requestBackupFile(callback: (data: any) => void) {
|
||||
const data = await this.application!.createBackupFile(
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
callback(data);
|
||||
}
|
||||
|
||||
desktop_setMajorDataChangeHandler(handler: () => void) {
|
||||
this.majorDataChangeHandler = handler;
|
||||
}
|
||||
|
||||
desktop_didBeginBackup() {
|
||||
this.webApplication.getAppState().beganBackupDownload();
|
||||
}
|
||||
|
||||
desktop_didFinishBackup(success: boolean) {
|
||||
this.webApplication.getAppState().endedBackupDownload(success);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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';
|
||||
9
app/assets/javascripts/services/index.ts
Normal file
9
app/assets/javascripts/services/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { AlertService } from './alertService';
|
||||
export { ArchiveManager } from './archiveManager';
|
||||
export { DesktopManager } from './desktopManager';
|
||||
export { KeyboardManager } from './keyboardManager';
|
||||
export { LockManager } from './lockManager';
|
||||
export { NativeExtManager } from './nativeExtManager';
|
||||
export { PreferencesManager } from './preferencesManager';
|
||||
export { StatusManager } from './statusManager';
|
||||
export { ThemeManager } from './themeManager';
|
||||
@@ -1,115 +0,0 @@
|
||||
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) {
|
||||
const 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.
|
||||
const 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 = []) {
|
||||
const eventModifiers = this.modifiersForEvent(event);
|
||||
|
||||
if(eventModifiers.length != modifiers.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(const 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(const 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)) {
|
||||
const 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}) {
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
147
app/assets/javascripts/services/keyboardManager.ts
Normal file
147
app/assets/javascripts/services/keyboardManager.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { removeFromArray } from 'snjs';
|
||||
export enum KeyboardKey {
|
||||
Tab = "Tab",
|
||||
Backspace = "Backspace",
|
||||
Up = "ArrowUp",
|
||||
Down = "ArrowDown",
|
||||
};
|
||||
|
||||
export enum KeyboardModifier {
|
||||
Shift = "Shift",
|
||||
Ctrl = "Control",
|
||||
/** ⌘ key on Mac, ⊞ key on Windows */
|
||||
Meta = "Meta",
|
||||
Alt = "Alt",
|
||||
};
|
||||
|
||||
enum KeyboardKeyEvent {
|
||||
Down = "KeyEventDown",
|
||||
Up = "KeyEventUp"
|
||||
};
|
||||
|
||||
type KeyboardObserver = {
|
||||
key?: KeyboardKey | string
|
||||
modifiers?: KeyboardModifier[]
|
||||
onKeyDown?: (event: KeyboardEvent) => void
|
||||
onKeyUp?: (event: KeyboardEvent) => void
|
||||
element?: HTMLElement
|
||||
elements?: HTMLElement[]
|
||||
notElement?: HTMLElement
|
||||
notElementIds?: string[]
|
||||
}
|
||||
|
||||
export class KeyboardManager {
|
||||
|
||||
private observers: KeyboardObserver[] = []
|
||||
private handleKeyDown: any
|
||||
private handleKeyUp: any
|
||||
|
||||
constructor() {
|
||||
this.handleKeyDown = (event: KeyboardEvent) => {
|
||||
this.notifyObserver(event, KeyboardKeyEvent.Down);
|
||||
}
|
||||
this.handleKeyUp = (event: KeyboardEvent) => {
|
||||
this.notifyObserver(event, KeyboardKeyEvent.Up);
|
||||
}
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
public deinit() {
|
||||
this.observers.length = 0;
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
window.removeEventListener('keyup', this.handleKeyUp);
|
||||
this.handleKeyDown = undefined;
|
||||
this.handleKeyUp = undefined;
|
||||
}
|
||||
|
||||
modifiersForEvent(event: KeyboardEvent) {
|
||||
const allModifiers = Object.values(KeyboardModifier);
|
||||
const eventModifiers = 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.
|
||||
const matches = (
|
||||
(
|
||||
(event.ctrlKey || event.key === KeyboardModifier.Ctrl)
|
||||
&& modifier === KeyboardModifier.Ctrl
|
||||
) ||
|
||||
(
|
||||
(event.metaKey || event.key === KeyboardModifier.Meta)
|
||||
&& modifier === KeyboardModifier.Meta
|
||||
) ||
|
||||
(
|
||||
(event.altKey || event.key === KeyboardModifier.Alt)
|
||||
&& modifier === KeyboardModifier.Alt
|
||||
) ||
|
||||
(
|
||||
(event.shiftKey || event.key === KeyboardModifier.Shift)
|
||||
&& modifier === KeyboardModifier.Shift
|
||||
)
|
||||
);
|
||||
|
||||
return matches;
|
||||
});
|
||||
|
||||
return eventModifiers;
|
||||
}
|
||||
|
||||
eventMatchesKeyAndModifiers(
|
||||
event: KeyboardEvent,
|
||||
key: KeyboardKey | string,
|
||||
modifiers: KeyboardModifier[] = []
|
||||
) {
|
||||
const eventModifiers = this.modifiersForEvent(event);
|
||||
if (eventModifiers.length !== modifiers.length) {
|
||||
return false;
|
||||
}
|
||||
for (const 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: KeyboardEvent, keyEvent: KeyboardKeyEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
for (const observer of this.observers) {
|
||||
if (observer.element && event.target !== observer.element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.elements && !observer.elements.includes(target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.notElement && observer.notElement === event.target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.notElementIds && observer.notElementIds.includes(target.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.eventMatchesKeyAndModifiers(event, observer.key!, observer.modifiers)) {
|
||||
const callback = keyEvent === KeyboardKeyEvent.Down
|
||||
? observer.onKeyDown
|
||||
: observer.onKeyUp;
|
||||
if (callback) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addKeyObserver(observer: KeyboardObserver) {
|
||||
this.observers.push(observer);
|
||||
return () => {
|
||||
removeFromArray(this.observers, observer);
|
||||
};
|
||||
}
|
||||
}
|
||||
157
app/assets/javascripts/services/lockManager.ts
Normal file
157
app/assets/javascripts/services/lockManager.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import { AppStateEvent } from '@/ui_models/app_state';
|
||||
|
||||
const MILLISECONDS_PER_SECOND = 1000;
|
||||
const FOCUS_POLL_INTERVAL = 1 * MILLISECONDS_PER_SECOND;
|
||||
const LOCK_INTERVAL_NONE = 0;
|
||||
const LOCK_INTERVAL_IMMEDIATE = 1;
|
||||
const LOCK_INTERVAL_ONE_MINUTE = 60 * MILLISECONDS_PER_SECOND;
|
||||
const LOCK_INTERVAL_FIVE_MINUTES = 300 * MILLISECONDS_PER_SECOND;
|
||||
const LOCK_INTERVAL_ONE_HOUR = 3600 * MILLISECONDS_PER_SECOND;
|
||||
|
||||
const STORAGE_KEY_AUTOLOCK_INTERVAL = "AutoLockIntervalKey";
|
||||
|
||||
export class LockManager {
|
||||
|
||||
private application: WebApplication
|
||||
private unsubState: any
|
||||
private pollFocusInterval: any
|
||||
private lastFocusState?: 'hidden' | 'visible'
|
||||
private lockAfterDate?: Date
|
||||
private lockTimeout?: any
|
||||
|
||||
constructor(application: WebApplication) {
|
||||
this.application = application;
|
||||
setImmediate(() => {
|
||||
this.observeVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
observeVisibility() {
|
||||
this.unsubState = this.application.getAppState().addObserver(
|
||||
async (eventName) => {
|
||||
if (eventName === AppStateEvent.WindowDidBlur) {
|
||||
this.documentVisibilityChanged(false);
|
||||
} else if (eventName === AppStateEvent.WindowDidFocus) {
|
||||
this.documentVisibilityChanged(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
if (!isDesktopApplication()) {
|
||||
this.beginWebFocusPolling();
|
||||
}
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.unsubState();
|
||||
if (this.pollFocusInterval) {
|
||||
clearInterval(this.pollFocusInterval);
|
||||
}
|
||||
}
|
||||
|
||||
async setAutoLockInterval(interval: number) {
|
||||
return this.application!.setValue(
|
||||
STORAGE_KEY_AUTOLOCK_INTERVAL,
|
||||
interval
|
||||
);
|
||||
}
|
||||
|
||||
async getAutoLockInterval() {
|
||||
const interval = await this.application!.getValue(
|
||||
STORAGE_KEY_AUTOLOCK_INTERVAL
|
||||
);
|
||||
if (interval) {
|
||||
return interval;
|
||||
} else {
|
||||
return LOCK_INTERVAL_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
beginWebFocusPolling() {
|
||||
this.pollFocusInterval = setInterval(() => {
|
||||
const 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';
|
||||
}, FOCUS_POLL_INTERVAL);
|
||||
}
|
||||
|
||||
getAutoLockIntervalOptions() {
|
||||
return [
|
||||
{
|
||||
value: LOCK_INTERVAL_NONE,
|
||||
label: "Off"
|
||||
},
|
||||
{
|
||||
value: LOCK_INTERVAL_IMMEDIATE,
|
||||
label: "Immediately"
|
||||
},
|
||||
{
|
||||
value: LOCK_INTERVAL_ONE_MINUTE,
|
||||
label: "1m"
|
||||
},
|
||||
{
|
||||
value: LOCK_INTERVAL_FIVE_MINUTES,
|
||||
label: "5m"
|
||||
},
|
||||
{
|
||||
value: LOCK_INTERVAL_ONE_HOUR,
|
||||
label: "1h"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async documentVisibilityChanged(visible: boolean) {
|
||||
if (visible) {
|
||||
const locked = await this.application.isLocked();
|
||||
if (
|
||||
!locked &&
|
||||
this.lockAfterDate &&
|
||||
new Date() > this.lockAfterDate
|
||||
) {
|
||||
this.application.lock();
|
||||
}
|
||||
this.cancelAutoLockTimer();
|
||||
} else {
|
||||
this.beginAutoLockTimer();
|
||||
}
|
||||
}
|
||||
|
||||
async beginAutoLockTimer() {
|
||||
var interval = await this.getAutoLockInterval();
|
||||
if (interval === LOCK_INTERVAL_NONE) {
|
||||
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 is sufficient. If memory is cleared, then the
|
||||
* application will lock anyway.
|
||||
*/
|
||||
const addToNow = (seconds: number) => {
|
||||
const date = new Date();
|
||||
date.setSeconds(date.getSeconds() + seconds);
|
||||
return date;
|
||||
};
|
||||
this.lockAfterDate = addToNow(interval / MILLISECONDS_PER_SECOND);
|
||||
this.lockTimeout = setTimeout(() => {
|
||||
this.cancelAutoLockTimer();
|
||||
this.application.lock();
|
||||
this.lockAfterDate = undefined;
|
||||
}, interval);
|
||||
}
|
||||
|
||||
cancelAutoLockTimer() {
|
||||
clearTimeout(this.lockTimeout);
|
||||
this.lockAfterDate = undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
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(const 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;
|
||||
const notes = this.modelManager.validItemsForContentType("Note");
|
||||
for(const note of notes) {
|
||||
for(const component of components) {
|
||||
const 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) => {
|
||||
|
||||
const needsSync = false;
|
||||
let status = this.statusManager.addStatusFromString("Optimizing data...");
|
||||
let dirtyCount = 0;
|
||||
|
||||
for(const note of notes) {
|
||||
if(!note.content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const references = note.content.references;
|
||||
// Remove any tag references, and transfer them to the tag if neccessary.
|
||||
const newReferences = [];
|
||||
|
||||
for(const reference of references) {
|
||||
if(reference.content_type != "Tag") {
|
||||
newReferences.push(reference);
|
||||
continue;
|
||||
}
|
||||
|
||||
// is Tag content_type, we will not be adding this to newReferences
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
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) {
|
||||
const contentTypePredicate = new SFPredicate("content_type", "=", "Note");
|
||||
const predicates = [contentTypePredicate, tag.content.predicate];
|
||||
if(!tag.content.isTrashTag) {
|
||||
const notTrashedPredicate = new SFPredicate("content.trashed", "=", false);
|
||||
predicates.push(notTrashedPredicate);
|
||||
}
|
||||
const results = this.itemsMatchingPredicates(predicates);
|
||||
return results;
|
||||
}
|
||||
|
||||
trashSmartTag() {
|
||||
return this.systemSmartTags.find((tag) => tag.content.isTrashTag);
|
||||
}
|
||||
|
||||
trashedItems() {
|
||||
return this.notesMatchingSmartTag(this.trashSmartTag());
|
||||
}
|
||||
|
||||
emptyTrash() {
|
||||
const notes = this.trashedItems();
|
||||
for(const note of notes) {
|
||||
this.setItemToBeDeleted(note);
|
||||
}
|
||||
}
|
||||
|
||||
buildSystemSmartTags() {
|
||||
this.systemSmartTags = SNSmartTag.systemSmartTags();
|
||||
}
|
||||
|
||||
getSmartTagWithId(id) {
|
||||
return this.getSmartTags().find((candidate) => candidate.uuid == id);
|
||||
}
|
||||
|
||||
getSmartTags() {
|
||||
const 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];
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
/* 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() {
|
||||
|
||||
const contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
|
||||
const 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
|
||||
const 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.
|
||||
const url = window._extensions_manager_location;
|
||||
if(!url) {
|
||||
console.error("window._extensions_manager_location must be set.");
|
||||
return;
|
||||
}
|
||||
|
||||
const 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() {
|
||||
|
||||
const contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
|
||||
const 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.
|
||||
const url = window._batch_manager_location;
|
||||
if(!url) {
|
||||
console.error("window._batch_manager_location must be set.");
|
||||
return;
|
||||
}
|
||||
|
||||
const 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
206
app/assets/javascripts/services/nativeExtManager.ts
Normal file
206
app/assets/javascripts/services/nativeExtManager.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import {
|
||||
SNPredicate,
|
||||
ContentType,
|
||||
SNComponent,
|
||||
ApplicationService,
|
||||
ComponentAction,
|
||||
FillItemContent,
|
||||
ComponentMutator,
|
||||
Copy,
|
||||
dictToArray
|
||||
} from 'snjs';
|
||||
import { PayloadContent } from '@node_modules/snjs/dist/@types/protocol/payloads/generator';
|
||||
import { ComponentPermission } from '@node_modules/snjs/dist/@types/models/app/component';
|
||||
|
||||
/** A class for handling installation of system extensions */
|
||||
export class NativeExtManager extends ApplicationService {
|
||||
extManagerId = 'org.standardnotes.extensions-manager';
|
||||
batchManagerId = 'org.standardnotes.batch-manager';
|
||||
|
||||
/** @override */
|
||||
async onAppLaunch() {
|
||||
super.onAppLaunch();
|
||||
this.reload();
|
||||
}
|
||||
|
||||
get extManagerPred() {
|
||||
const extManagerId = 'org.standardnotes.extensions-manager';
|
||||
return SNPredicate.CompoundPredicate([
|
||||
new SNPredicate('content_type', '=', ContentType.Component),
|
||||
new SNPredicate('package_info.identifier', '=', extManagerId)
|
||||
]);
|
||||
}
|
||||
|
||||
get batchManagerPred() {
|
||||
const batchMgrId = 'org.standardnotes.batch-manager';
|
||||
return SNPredicate.CompoundPredicate([
|
||||
new SNPredicate('content_type', '=', ContentType.Component),
|
||||
new SNPredicate('package_info.identifier', '=', batchMgrId)
|
||||
]);
|
||||
}
|
||||
|
||||
get extMgrUrl() {
|
||||
return (window as any)._extensions_manager_location;
|
||||
}
|
||||
|
||||
get batchMgrUrl() {
|
||||
return (window as any)._batch_manager_location;
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.application!.singletonManager!.registerPredicate(this.extManagerPred);
|
||||
this.application!.singletonManager!.registerPredicate(this.batchManagerPred);
|
||||
this.resolveExtensionsManager();
|
||||
this.resolveBatchManager();
|
||||
}
|
||||
|
||||
async resolveExtensionsManager() {
|
||||
const extensionsManager = (await this.application!.singletonManager!.findOrCreateSingleton(
|
||||
this.extManagerPred,
|
||||
ContentType.Component,
|
||||
this.extensionsManagerTemplateContent()
|
||||
)) as SNComponent;
|
||||
let needsSync = false;
|
||||
if (isDesktopApplication()) {
|
||||
if (!extensionsManager.local_url) {
|
||||
await this.application!.changeItem(extensionsManager.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.local_url = this.extMgrUrl;
|
||||
});
|
||||
needsSync = true;
|
||||
}
|
||||
} else {
|
||||
if (!extensionsManager.hosted_url) {
|
||||
await this.application!.changeItem(extensionsManager.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.hosted_url = this.extMgrUrl;
|
||||
});
|
||||
needsSync = true;
|
||||
}
|
||||
}
|
||||
// Handle addition of SN|ExtensionRepo permission
|
||||
const permissions = Copy(extensionsManager!.permissions) as ComponentPermission[];
|
||||
const permission = permissions.find((p) => {
|
||||
return p.name === ComponentAction.StreamItems
|
||||
});
|
||||
if (permission && !permission.content_types!.includes(ContentType.ExtensionRepo)) {
|
||||
permission.content_types!.push(ContentType.ExtensionRepo);
|
||||
await this.application!.changeItem(extensionsManager.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.permissions = permissions;
|
||||
});
|
||||
needsSync = true;
|
||||
}
|
||||
if (needsSync) {
|
||||
this.application!.saveItem(extensionsManager.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
extensionsManagerTemplateContent() {
|
||||
const url = this.extMgrUrl;
|
||||
if (!url) {
|
||||
throw Error('this.extMgrUrl must be set.');
|
||||
}
|
||||
const packageInfo = {
|
||||
name: 'Extensions',
|
||||
identifier: this.extManagerId
|
||||
};
|
||||
const content = FillItemContent({
|
||||
name: packageInfo.name,
|
||||
area: 'rooms',
|
||||
package_info: packageInfo,
|
||||
permissions: [
|
||||
{
|
||||
name: ComponentAction.StreamItems,
|
||||
content_types: [
|
||||
ContentType.Component,
|
||||
ContentType.Theme,
|
||||
ContentType.ServerExtension,
|
||||
ContentType.ActionsExtension,
|
||||
ContentType.Mfa,
|
||||
ContentType.Editor,
|
||||
ContentType.ExtensionRepo
|
||||
]
|
||||
}
|
||||
]
|
||||
}) as PayloadContent;
|
||||
if (isDesktopApplication()) {
|
||||
content.local_url = this.extMgrUrl;
|
||||
} else {
|
||||
content.hosted_url = this.extMgrUrl;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
async resolveBatchManager() {
|
||||
const batchManager = (await this.application!.singletonManager!.findOrCreateSingleton(
|
||||
this.batchManagerPred,
|
||||
ContentType.Component,
|
||||
this.batchManagerTemplateContent()
|
||||
)) as SNComponent;
|
||||
let needsSync = false;
|
||||
if (isDesktopApplication()) {
|
||||
if (!batchManager.local_url) {
|
||||
await this.application!.changeItem(batchManager.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.local_url = this.batchMgrUrl;
|
||||
});
|
||||
needsSync = true;
|
||||
}
|
||||
} else {
|
||||
if (!batchManager.hosted_url) {
|
||||
await this.application!.changeItem(batchManager.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.hosted_url = this.batchMgrUrl;
|
||||
});
|
||||
needsSync = true;
|
||||
}
|
||||
}
|
||||
// Handle addition of SN|ExtensionRepo permission
|
||||
const permissions = Copy(batchManager!.permissions) as ComponentPermission[];
|
||||
const permission = permissions.find((p) => {
|
||||
return p.name === ComponentAction.StreamItems
|
||||
});
|
||||
if (permission && !permission.content_types!.includes(ContentType.ExtensionRepo)) {
|
||||
permission.content_types!.push(ContentType.ExtensionRepo);
|
||||
await this.application!.changeItem(batchManager.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.permissions = permissions;
|
||||
});
|
||||
needsSync = true;
|
||||
}
|
||||
if (needsSync) {
|
||||
this.application!.saveItem(batchManager.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
batchManagerTemplateContent() {
|
||||
const url = this.batchMgrUrl;
|
||||
if (!url) {
|
||||
throw Error('window._batch_manager_location must be set.');
|
||||
}
|
||||
const packageInfo = {
|
||||
name: 'Batch Manager',
|
||||
identifier: this.batchManagerId
|
||||
};
|
||||
const allContentType = dictToArray(ContentType);
|
||||
const content = FillItemContent({
|
||||
name: packageInfo.name,
|
||||
area: 'modal',
|
||||
package_info: packageInfo,
|
||||
permissions: [
|
||||
{
|
||||
name: ComponentAction.StreamItems,
|
||||
content_types: allContentType
|
||||
}
|
||||
]
|
||||
});
|
||||
if (isDesktopApplication()) {
|
||||
content.local_url = this.batchMgrUrl;
|
||||
} else {
|
||||
content.hosted_url = this.batchMgrUrl;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
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(const callback of this.visibilityObservers) {
|
||||
callback(visible);
|
||||
}
|
||||
}
|
||||
|
||||
async setAutoLockInterval(interval) {
|
||||
return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.FixedEncrypted);
|
||||
}
|
||||
|
||||
async getAutoLockInterval() {
|
||||
const 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();
|
||||
const 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) => {
|
||||
const keys = results.keys;
|
||||
const 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) => {
|
||||
const 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(() => {
|
||||
const 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.
|
||||
const addToNow = (seconds) => {
|
||||
const 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;
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
83
app/assets/javascripts/services/preferencesManager.ts
Normal file
83
app/assets/javascripts/services/preferencesManager.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import {
|
||||
SNPredicate,
|
||||
ContentType,
|
||||
ApplicationService,
|
||||
SNUserPrefs,
|
||||
WebPrefKey,
|
||||
UserPrefsMutator,
|
||||
FillItemContent
|
||||
} from 'snjs';
|
||||
|
||||
export class PreferencesManager extends ApplicationService {
|
||||
|
||||
private userPreferences!: SNUserPrefs
|
||||
private loadingPrefs = false;
|
||||
|
||||
/** @override */
|
||||
async onAppLaunch() {
|
||||
super.onAppLaunch();
|
||||
this.reloadSingleton();
|
||||
this.streamPreferences();
|
||||
}
|
||||
|
||||
get webApplication() {
|
||||
return this.application as WebApplication;
|
||||
}
|
||||
|
||||
streamPreferences() {
|
||||
this.application!.streamItems(
|
||||
ContentType.UserPrefs,
|
||||
() => {
|
||||
this.reloadSingleton();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async reloadSingleton() {
|
||||
if(this.loadingPrefs) {
|
||||
return;
|
||||
}
|
||||
this.loadingPrefs = true;
|
||||
const contentType = ContentType.UserPrefs;
|
||||
const predicate = new SNPredicate('content_type', '=', contentType);
|
||||
const previousRef = this.userPreferences;
|
||||
this.userPreferences = (await this.application!.singletonManager!.findOrCreateSingleton(
|
||||
predicate,
|
||||
contentType,
|
||||
FillItemContent({})
|
||||
)) as SNUserPrefs;
|
||||
this.loadingPrefs = false;
|
||||
const didChange = !previousRef || (
|
||||
this.userPreferences.lastSyncBegan?.getTime() !== previousRef?.lastSyncBegan?.getTime()
|
||||
)
|
||||
if (didChange) {
|
||||
this.webApplication.getAppState().setUserPreferences(this.userPreferences);
|
||||
}
|
||||
}
|
||||
|
||||
syncUserPreferences() {
|
||||
if (this.userPreferences) {
|
||||
this.application!.saveItem(this.userPreferences.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
getValue(key: WebPrefKey, defaultValue?: any) {
|
||||
if (!this.userPreferences) { return defaultValue; }
|
||||
const value = this.userPreferences.getPref(key);
|
||||
return (value !== undefined && value !== null) ? value : defaultValue;
|
||||
}
|
||||
|
||||
async setUserPrefValue(key: WebPrefKey, value: any, sync = false) {
|
||||
await this.application!.changeItem(
|
||||
this.userPreferences.uuid,
|
||||
(m) => {
|
||||
const mutator = m as UserPrefsMutator;
|
||||
mutator.setWebPref(key, value);
|
||||
}
|
||||
)
|
||||
if (sync) {
|
||||
this.syncUserPreferences();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
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 () => {
|
||||
const offline = authManager.offline();
|
||||
const auth_params = offline ? passcodeManager.passcodeAuthParams() : await authManager.getAuthParams();
|
||||
const 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,43 @@
|
||||
import _ from 'lodash';
|
||||
import { removeFromArray } from 'snjs';
|
||||
import { FooterStatus } from '@/types';
|
||||
|
||||
type StatusCallback = (string: string) => void
|
||||
|
||||
export class StatusManager {
|
||||
constructor() {
|
||||
this.statuses = [];
|
||||
this.observers = [];
|
||||
}
|
||||
|
||||
statusFromString(string) {
|
||||
private statuses: FooterStatus[] = []
|
||||
private observers: StatusCallback[] = []
|
||||
|
||||
statusFromString(string: string) {
|
||||
return {string: string};
|
||||
}
|
||||
|
||||
replaceStatusWithString(status, string) {
|
||||
replaceStatusWithString(status: FooterStatus, string: string) {
|
||||
this.removeStatus(status);
|
||||
return this.addStatusFromString(string);
|
||||
}
|
||||
|
||||
addStatusFromString(string) {
|
||||
addStatusFromString(string: string) {
|
||||
return this.addStatus(this.statusFromString(string));
|
||||
}
|
||||
|
||||
addStatus(status) {
|
||||
if(typeof status !== "object") {
|
||||
console.error("Attempting to set non-object status", status);
|
||||
return;
|
||||
}
|
||||
|
||||
addStatus(status: FooterStatus) {
|
||||
this.statuses.push(status);
|
||||
this.notifyObservers();
|
||||
return status;
|
||||
}
|
||||
|
||||
removeStatus(status) {
|
||||
_.pull(this.statuses, status);
|
||||
removeStatus(status: FooterStatus) {
|
||||
removeFromArray(this.statuses, status);
|
||||
this.notifyObservers();
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getStatusString() {
|
||||
let result = "";
|
||||
let result = '';
|
||||
this.statuses.forEach((status, index) => {
|
||||
if(index > 0) {
|
||||
result += " ";
|
||||
result += ' ';
|
||||
}
|
||||
result += status.string;
|
||||
});
|
||||
@@ -54,11 +51,10 @@ export class StatusManager {
|
||||
}
|
||||
}
|
||||
|
||||
addStatusObserver(callback) {
|
||||
addStatusObserver(callback: StatusCallback) {
|
||||
this.observers.push(callback);
|
||||
}
|
||||
|
||||
removeStatusObserver(callback) {
|
||||
_.pull(this.statuses, callback);
|
||||
return () => {
|
||||
removeFromArray(this.observers, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
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
|
||||
@@ -1,30 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
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() {
|
||||
const mapped = await Promise.all(this.activeThemes.map(async (theme) => {
|
||||
const transformer = new SFItemParams(theme);
|
||||
const params = await transformer.paramsForLocalStorage();
|
||||
return params;
|
||||
}));
|
||||
const data = JSON.stringify(mapped);
|
||||
return this.storageManager.setItem(ThemeManager.CachedThemesKey, data, StorageManager.Fixed);
|
||||
}
|
||||
|
||||
async decacheThemes() {
|
||||
return this.storageManager.removeItem(ThemeManager.CachedThemesKey, StorageManager.Fixed);
|
||||
}
|
||||
|
||||
getCachedThemes() {
|
||||
const cachedThemes = this.storageManager.getItemSync(ThemeManager.CachedThemesKey, StorageManager.Fixed);
|
||||
if (cachedThemes) {
|
||||
const parsed = JSON.parse(cachedThemes);
|
||||
return parsed.map((theme) => {
|
||||
return new SNTheme(theme);
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
179
app/assets/javascripts/services/themeManager.ts
Normal file
179
app/assets/javascripts/services/themeManager.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
StorageValueModes,
|
||||
EncryptionIntent,
|
||||
ApplicationService,
|
||||
SNTheme,
|
||||
ComponentArea,
|
||||
removeFromArray,
|
||||
ApplicationEvent
|
||||
} from 'snjs';
|
||||
import { AppStateEvent } from '@/ui_models/app_state';
|
||||
|
||||
const CACHED_THEMES_KEY = 'cachedThemes';
|
||||
|
||||
export class ThemeManager extends ApplicationService {
|
||||
|
||||
private activeThemes: string[] = []
|
||||
private unsubState?: () => void
|
||||
private unregisterDesktop!: () => void
|
||||
private unregisterComponent!: () => void
|
||||
|
||||
/** @override */
|
||||
async onAppLaunch() {
|
||||
super.onAppLaunch();
|
||||
this.unsubState = this.webApplication.getAppState().addObserver(
|
||||
async (eventName) => {
|
||||
if (eventName === AppStateEvent.DesktopExtsReady) {
|
||||
this.activateCachedThemes();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onAppEvent(event: ApplicationEvent) {
|
||||
super.onAppEvent(event);
|
||||
if (event === ApplicationEvent.SignedOut) {
|
||||
this.deactivateAllThemes();
|
||||
}
|
||||
}
|
||||
|
||||
get webApplication() {
|
||||
return this.application as WebApplication;
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.unsubState?.();
|
||||
(this.unsubState as any) = undefined;
|
||||
this.activeThemes.length = 0;
|
||||
this.unregisterDesktop();
|
||||
this.unregisterComponent();
|
||||
(this.unregisterDesktop as any) = undefined;
|
||||
(this.unregisterComponent as any) = undefined;
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async onAppStart() {
|
||||
super.onAppStart();
|
||||
this.registerObservers();
|
||||
if (!this.webApplication.getDesktopService().isDesktop) {
|
||||
this.activateCachedThemes();
|
||||
}
|
||||
}
|
||||
|
||||
private async activateCachedThemes() {
|
||||
const cachedThemes = await this.getCachedThemes();
|
||||
const writeToCache = false;
|
||||
for (const theme of cachedThemes) {
|
||||
this.activateTheme(theme, writeToCache);
|
||||
}
|
||||
}
|
||||
|
||||
private registerObservers() {
|
||||
this.unregisterDesktop = this.webApplication.getDesktopService()
|
||||
.registerUpdateObserver((component) => {
|
||||
if (component.active && component.isTheme()) {
|
||||
this.deactivateTheme(component.uuid);
|
||||
setTimeout(() => {
|
||||
this.activateTheme(component as SNTheme);
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
|
||||
this.unregisterComponent = this.application!.componentManager!.registerHandler({
|
||||
identifier: 'themeManager',
|
||||
areas: [ComponentArea.Themes],
|
||||
activationHandler: (uuid, component) => {
|
||||
if (component?.active) {
|
||||
this.activateTheme(component as SNTheme);
|
||||
} else {
|
||||
this.deactivateTheme(uuid);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private deactivateAllThemes() {
|
||||
for (const uuid of this.activeThemes) {
|
||||
this.deactivateTheme(uuid, false);
|
||||
}
|
||||
this.activeThemes = [];
|
||||
this.decacheThemes();
|
||||
}
|
||||
|
||||
private activateTheme(theme: SNTheme, writeToCache = true) {
|
||||
if (this.activeThemes.find((uuid) => uuid === theme.uuid)) {
|
||||
return;
|
||||
}
|
||||
this.activeThemes.push(theme.uuid);
|
||||
const url = this.application!.componentManager!.urlForComponent(theme)!;
|
||||
const 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();
|
||||
}
|
||||
}
|
||||
|
||||
private deactivateTheme(uuid: string, recache = true) {
|
||||
const element = document.getElementById(uuid) as HTMLLinkElement;
|
||||
if (element) {
|
||||
element.disabled = true;
|
||||
element.parentNode!.removeChild(element);
|
||||
}
|
||||
removeFromArray(this.activeThemes, uuid);
|
||||
if (recache) {
|
||||
this.cacheThemes();
|
||||
}
|
||||
}
|
||||
|
||||
private async cacheThemes() {
|
||||
const themes = this.application!.getAll(this.activeThemes) as SNTheme[];
|
||||
const mapped = await Promise.all(themes.map(async (theme) => {
|
||||
const payload = theme.payloadRepresentation();
|
||||
const processedPayload = await this.application!.protocolService!.payloadByEncryptingPayload(
|
||||
payload,
|
||||
EncryptionIntent.LocalStorageDecrypted
|
||||
);
|
||||
return processedPayload;
|
||||
}));
|
||||
return this.application!.setValue(
|
||||
CACHED_THEMES_KEY,
|
||||
mapped,
|
||||
StorageValueModes.Nonwrapped
|
||||
);
|
||||
}
|
||||
|
||||
private async decacheThemes() {
|
||||
if (this.application) {
|
||||
return this.application.removeValue(
|
||||
CACHED_THEMES_KEY,
|
||||
StorageValueModes.Nonwrapped
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getCachedThemes() {
|
||||
const cachedThemes = await this.application!.getValue(
|
||||
CACHED_THEMES_KEY,
|
||||
StorageValueModes.Nonwrapped
|
||||
);
|
||||
if (cachedThemes) {
|
||||
const themes = [];
|
||||
for (const cachedTheme of cachedThemes) {
|
||||
const payload = this.application!.createPayloadFromObject(cachedTheme);
|
||||
const theme = this.application!.createItemFromPayload(payload) as SNTheme;
|
||||
themes.push(theme);
|
||||
}
|
||||
return themes;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user