Desktop manager TS
This commit is contained in:
@@ -1,6 +0,0 @@
|
|||||||
/* @ngInject */
|
|
||||||
export function trusted($sce) {
|
|
||||||
return function(url) {
|
|
||||||
return $sce.trustAsResourceUrl(url);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
6
app/assets/javascripts/filters/trusted.ts
Normal file
6
app/assets/javascripts/filters/trusted.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/* @ngInject */
|
||||||
|
export function trusted($sce: ng.ISCEService) {
|
||||||
|
return function(url: string) {
|
||||||
|
return $sce.trustAsResourceUrl(url);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,10 +4,10 @@ import { SKAlert } from 'sn-stylekit';
|
|||||||
|
|
||||||
export class AlertService extends SNAlertService {
|
export class AlertService extends SNAlertService {
|
||||||
async alert(
|
async alert(
|
||||||
title,
|
title: string,
|
||||||
text,
|
text: string,
|
||||||
closeButtonText = "OK",
|
closeButtonText = 'OK',
|
||||||
onClose
|
onClose: () => void
|
||||||
) {
|
) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const buttons = [
|
const buttons = [
|
||||||
@@ -16,7 +16,7 @@ export class AlertService extends SNAlertService {
|
|||||||
style: "neutral",
|
style: "neutral",
|
||||||
action: async () => {
|
action: async () => {
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
this.deviceInterface.timeout(onClose);
|
this.deviceInterface!.timeout(onClose);
|
||||||
}
|
}
|
||||||
resolve(true);
|
resolve(true);
|
||||||
}
|
}
|
||||||
@@ -28,12 +28,12 @@ export class AlertService extends SNAlertService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async confirm(
|
async confirm(
|
||||||
title,
|
title: string,
|
||||||
text,
|
text: string,
|
||||||
confirmButtonText = "Confirm",
|
confirmButtonText = 'Confirm',
|
||||||
cancelButtonText = "Cancel",
|
cancelButtonText = 'Cancel',
|
||||||
onConfirm,
|
onConfirm: () => void,
|
||||||
onCancel,
|
onCancel: () => void,
|
||||||
destructive = false
|
destructive = false
|
||||||
) {
|
) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -43,7 +43,7 @@ export class AlertService extends SNAlertService {
|
|||||||
style: "neutral",
|
style: "neutral",
|
||||||
action: async () => {
|
action: async () => {
|
||||||
if (onCancel) {
|
if (onCancel) {
|
||||||
this.deviceInterface.timeout(onCancel);
|
this.deviceInterface!.timeout(onCancel);
|
||||||
}
|
}
|
||||||
reject(false);
|
reject(false);
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ export class AlertService extends SNAlertService {
|
|||||||
style: destructive ? "danger" : "info",
|
style: destructive ? "danger" : "info",
|
||||||
action: async () => {
|
action: async () => {
|
||||||
if (onConfirm) {
|
if (onConfirm) {
|
||||||
this.deviceInterface.timeout(onConfirm);
|
this.deviceInterface!.timeout(onConfirm);
|
||||||
}
|
}
|
||||||
resolve(true);
|
resolve(true);
|
||||||
}
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
154
app/assets/javascripts/services/archiveManager.ts
Normal file
154
app/assets/javascripts/services/archiveManager.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
245
app/assets/javascripts/services/desktopManager.ts
Normal file
245
app/assets/javascripts/services/desktopManager.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import _ from 'lodash';
|
|||||||
import {
|
import {
|
||||||
ApplicationEvent,
|
ApplicationEvent,
|
||||||
StorageValueModes,
|
StorageValueModes,
|
||||||
EncryptionIntents,
|
EncryptionIntent,
|
||||||
ApplicationService,
|
ApplicationService,
|
||||||
} from 'snjs';
|
} from 'snjs';
|
||||||
import { AppStateEvent } from '@/services/state';
|
import { AppStateEvent } from '@/services/state';
|
||||||
@@ -123,7 +123,7 @@ export class ThemeManager extends ApplicationService {
|
|||||||
const payload = theme.payloadRepresentation();
|
const payload = theme.payloadRepresentation();
|
||||||
const processedPayload = await this.application.protocolService.payloadByEncryptingPayload({
|
const processedPayload = await this.application.protocolService.payloadByEncryptingPayload({
|
||||||
payload: payload,
|
payload: payload,
|
||||||
intent: EncryptionIntents.LocalStorageDecrypted
|
intent: EncryptionIntent.LocalStorageDecrypted
|
||||||
});
|
});
|
||||||
return processedPayload;
|
return processedPayload;
|
||||||
}));
|
}));
|
||||||
|
|||||||
1
app/assets/javascripts/typings/stylekit.d.ts
vendored
Normal file
1
app/assets/javascripts/typings/stylekit.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare module "sn-stylekit";
|
||||||
Reference in New Issue
Block a user