Desktop manager TS

This commit is contained in:
Mo Bitar
2020-04-13 09:00:09 -05:00
parent aed8d4a3da
commit 421216bf1b
10 changed files with 421 additions and 385 deletions

View File

@@ -1,6 +0,0 @@
/* @ngInject */
export function trusted($sce) {
return function(url) {
return $sce.trustAsResourceUrl(url);
};
}

View File

@@ -0,0 +1,6 @@
/* @ngInject */
export function trusted($sce: ng.ISCEService) {
return function(url: string) {
return $sce.trustAsResourceUrl(url);
};
}

View File

@@ -4,10 +4,10 @@ import { SKAlert } from 'sn-stylekit';
export class AlertService extends SNAlertService {
async alert(
title,
text,
closeButtonText = "OK",
onClose
title: string,
text: string,
closeButtonText = 'OK',
onClose: () => void
) {
return new Promise((resolve) => {
const buttons = [
@@ -16,7 +16,7 @@ export class AlertService extends SNAlertService {
style: "neutral",
action: async () => {
if (onClose) {
this.deviceInterface.timeout(onClose);
this.deviceInterface!.timeout(onClose);
}
resolve(true);
}
@@ -28,12 +28,12 @@ export class AlertService extends SNAlertService {
}
async confirm(
title,
text,
confirmButtonText = "Confirm",
cancelButtonText = "Cancel",
onConfirm,
onCancel,
title: string,
text: string,
confirmButtonText = 'Confirm',
cancelButtonText = 'Cancel',
onConfirm: () => void,
onCancel: () => void,
destructive = false
) {
return new Promise((resolve, reject) => {
@@ -43,7 +43,7 @@ export class AlertService extends SNAlertService {
style: "neutral",
action: async () => {
if (onCancel) {
this.deviceInterface.timeout(onCancel);
this.deviceInterface!.timeout(onCancel);
}
reject(false);
}
@@ -53,7 +53,7 @@ export class AlertService extends SNAlertService {
style: destructive ? "danger" : "info",
action: async () => {
if (onConfirm) {
this.deviceInterface.timeout(onConfirm);
this.deviceInterface!.timeout(onConfirm);
}
resolve(true);
}

View File

@@ -1,149 +0,0 @@
import { EncryptionIntents, ProtectedAction } from 'snjs';
export class ArchiveManager {
constructor(application) {
this.application = application;
}
/** @public */
async downloadBackup(encrypted) {
return this.downloadBackupOfItems(this.application.modelManager.allItems, encrypted);
}
/** @public */
async downloadBackupOfItems(items, encrypted) {
const run = async () => {
// download in Standard Notes format
const intent = encrypted
? EncryptionIntents.FileEncrypted
: EncryptionIntents.FileDecrypted;
this.itemsData(items, intent).then((data) => {
const modifier = encrypted ? "Encrypted" : "Decrypted";
this.downloadData(
data,
`Standard Notes ${modifier} Backup - ${this.formattedDate()}.txt`
);
// download as zipped plain text files
if (!encrypted) {
this.downloadZippedItems(items);
}
});
};
if (await this.application.privilegesService.actionRequiresPrivilege(ProtectedAction.ManageBackups)) {
this.application.presentPrivilegesModal(ProtectedAction.ManageBackups, () => {
run();
});
} else {
run();
}
}
/** @private */
formattedDate() {
var string = `${new Date()}`;
// Match up to the first parenthesis, i.e do not include '(Central Standard Time)'
var matches = string.match(/^(.*?) \(/);
if (matches.length >= 2) {
return matches[1];
}
return string;
}
/** @private */
async itemsData(items, intent) {
const data = await this.application.createBackupFile({
subItems: items,
intent: intent
});
const blobData = new Blob([data], { type: 'text/json' });
return blobData;
}
/** @private */
async loadZip() {
if (window.zip) {
return;
}
var scriptTag = document.createElement('script');
scriptTag.src = "/assets/zip/zip.js";
scriptTag.async = false;
var headTag = document.getElementsByTagName('head')[0];
headTag.appendChild(scriptTag);
return new Promise((resolve, reject) => {
scriptTag.onload = function () {
zip.workerScriptsPath = "assets/zip/";
resolve();
};
});
}
/** @private */
async downloadZippedItems(items) {
await this.loadZip();
zip.createWriter(new zip.BlobWriter("application/zip"), (zipWriter) => {
var index = 0;
const nextFile = () => {
var item = items[index];
var name, contents;
if (item.content_type === "Note") {
name = item.content.title;
contents = item.content.text;
} else {
name = item.content_type;
contents = JSON.stringify(item.content, null, 2);
}
if (!name) {
name = "";
}
const blob = new Blob([contents], { type: 'text/plain' });
let filePrefix = name.replace(/\//g, "").replace(/\\+/g, "");
const fileSuffix = `-${item.uuid.split("-")[0]}.txt`;
// Standard max filename length is 255. Slice the note name down to allow filenameEnd
filePrefix = filePrefix.slice(0, (255 - fileSuffix.length));
const fileName = `${item.content_type}/${filePrefix}${fileSuffix}`;
zipWriter.add(fileName, new zip.BlobReader(blob), () => {
index++;
if (index < items.length) {
nextFile();
} else {
zipWriter.close((blob) => {
this.downloadData(blob, `Standard Notes Backup - ${this.formattedDate()}.zip`);
zipWriter = null;
});
}
});
};
nextFile();
}, onerror);
}
/** @private */
hrefForData(data) {
// If we are replacing a previously generated file we need to
// manually revoke the object URL to avoid memory leaks.
if (this.textFile !== null) {
window.URL.revokeObjectURL(this.textFile);
}
this.textFile = window.URL.createObjectURL(data);
// returns a URL you can use as a href
return this.textFile;
}
/** @private */
downloadData(data, fileName) {
var link = document.createElement('a');
link.setAttribute('download', fileName);
link.href = this.hrefForData(data);
document.body.appendChild(link);
link.click();
link.remove();
}
}

View File

@@ -0,0 +1,154 @@
import { WebApplication } from '@/application';
import { EncryptionIntent, ProtectedAction, SNItem, ContentType, SNNote } from 'snjs';
export class ArchiveManager {
private readonly application: WebApplication
private textFile?: string
constructor(application: WebApplication) {
this.application = application;
}
public async downloadBackup(encrypted: boolean) {
return this.downloadBackupOfItems(this.application.allItems(), encrypted);
}
public async downloadBackupOfItems(items: SNItem[], encrypted: boolean) {
const run = async () => {
// download in Standard Notes format
const intent = encrypted
? EncryptionIntent.FileEncrypted
: EncryptionIntent.FileDecrypted;
this.itemsData(items, intent).then((data) => {
const modifier = encrypted ? 'Encrypted' : 'Decrypted';
this.downloadData(
data!,
`Standard Notes ${modifier} Backup - ${this.formattedDate()}.txt`
);
// download as zipped plain text files
if (!encrypted) {
this.downloadZippedItems(items);
}
});
};
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[]) {
await this.loadZip();
this.zip.createWriter(
new this.zip.BlobWriter('application/zip'),
(zipWriter: any) => {
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' });
let filePrefix = name.replace(/\//g, '').replace(/\\+/g, '');
const fileSuffix = `-${item.uuid.split('-')[0]}.txt`;
// Standard max filename length is 255. Slice the note name down to allow filenameEnd
filePrefix = filePrefix.slice(0, (255 - fileSuffix.length));
const fileName = `${item.content_type}/${filePrefix}${fileSuffix}`;
zipWriter.add(fileName, new 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();
}
}

View File

@@ -1,215 +0,0 @@
/* eslint-disable camelcase */
// An interface used by the Desktop app to interact with SN
import { isDesktopApplication } from '@/utils';
import { EncryptionIntents, ApplicationService, ApplicationEvent, removeFromArray } 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 extends ApplicationService {
constructor(
$rootScope,
$timeout,
application
) {
super(application);
this.$rootScope = $rootScope;
this.$timeout = $timeout;
this.componentActivationObservers = [];
this.updateObservers = [];
this.isDesktop = isDesktopApplication();
}
deinit() {
this.componentActivationObservers.length = 0;
this.updateObservers.length = 0;
super.deinit();
}
/** @override */
onAppEvent(eventName) {
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) {
return this.application.protocolService.payloadByEncryptingPayload({
payload: component.payloadRepresentation(),
intent: EncryptionIntents.FileDecrypted
});
}
// 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 () => {
removeFromArray(this.updateObservers, observer);
};
}
searchText(text) {
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) {
this.searchHandler = handler;
}
desktop_windowGainedFocus() {
this.$rootScope.$broadcast('window-gained-focus');
}
desktop_windowLostFocus() {
this.$rootScope.$broadcast('window-lost-focus');
}
async desktop_onComponentInstallationComplete(componentData, error) {
const component = await this.application.findItem({ uuid: 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];
}
component.setAppDataItem(
COMPONENT_DATA_KEY_INSTALL_ERROR,
null
);
}
this.application.saveItem({ item: component });
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) {
removeFromArray(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.application.getAppState().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) {
const data = await this.application.createBackupFile({
returnIfEmpty: true
});
callback(data);
}
desktop_setMajorDataChangeHandler(handler) {
this.majorDataChangeHandler = handler;
}
desktop_didBeginBackup() {
this.application.getAppState().beganBackupDownload();
}
desktop_didFinishBackup(success) {
this.application.getAppState().endedBackupDownload(success);
}
}

View File

@@ -0,0 +1,245 @@
import { SNComponent, PurePayload } from 'snjs';
/* eslint-disable camelcase */
import { WebApplication } from '@/application';
// An interface used by the Desktop app to interact with SN
import { isDesktopApplication } from '@/utils';
import { EncryptionIntent, ApplicationService, ApplicationEvent, removeFromArray } from 'snjs';
import { ComponentMutator } from '@/../../../../snjs/dist/@types/models';
import { AppDataField } from '@/../../../../snjs/dist/@types/models/core/item';
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);
}
}

View File

@@ -2,7 +2,7 @@ import _ from 'lodash';
import {
ApplicationEvent,
StorageValueModes,
EncryptionIntents,
EncryptionIntent,
ApplicationService,
} from 'snjs';
import { AppStateEvent } from '@/services/state';
@@ -123,7 +123,7 @@ export class ThemeManager extends ApplicationService {
const payload = theme.payloadRepresentation();
const processedPayload = await this.application.protocolService.payloadByEncryptingPayload({
payload: payload,
intent: EncryptionIntents.LocalStorageDecrypted
intent: EncryptionIntent.LocalStorageDecrypted
});
return processedPayload;
}));

View File

@@ -0,0 +1 @@
declare module "sn-stylekit";