This commit is contained in:
Mo Bitar
2020-02-08 12:11:04 -06:00
parent 13dd41b9ab
commit f6ef4a39e2
26 changed files with 10885 additions and 2044 deletions

View File

@@ -1,48 +1,46 @@
import { PrivilegesManager } from '@/services/privilegesManager';
import { EncryptionIntents, ProtectedActions } from 'snjs';
export class ArchiveManager {
/* @ngInject */
constructor(lockManager, authManager, modelManager, privilegesManager) {
constructor(lockManager, application, authManager, modelManager, privilegesManager) {
this.lockManager = lockManager;
this.authManager = authManager;
this.modelManager = modelManager;
modelManager = modelManager;
this.privilegesManager = privilegesManager;
this.application = application;
}
/*
Public
*/
/** @public */
async downloadBackup(encrypted) {
return this.downloadBackupOfItems(this.modelManager.allItems, encrypted);
return this.downloadBackupOfItems(modelManager.allItems, encrypted);
}
/** @public */
async downloadBackupOfItems(items, encrypted) {
const run = async () => {
// download in Standard Notes format
let keys, authParams;
if(encrypted) {
if(this.authManager.offline() && this.lockManager.hasPasscode()) {
keys = this.lockManager.keys();
authParams = this.lockManager.passcodeAuthParams();
} else {
keys = await this.authManager.keys();
authParams = await this.authManager.getAuthParams();
}
}
this.__itemsData(items, keys, authParams).then((data) => {
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`);
this.downloadData(
data,
`Standard Notes ${modifier} Backup - ${this.formattedDate()}.txt`
);
// download as zipped plain text files
if(!keys) {
this.__downloadZippedItems(items);
if (!encrypted) {
this.downloadZippedItems(items);
}
});
};
if(await this.privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
this.godService.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
if (await this.privilegesManager.actionRequiresPrivilege(ProtectedActions.ManageBackups)) {
this.godService.presentPrivilegesModal(ProtectedActions.ManageBackups, () => {
run();
});
} else {
@@ -50,29 +48,30 @@ export class ArchiveManager {
}
}
/*
Private
*/
__formattedDate() {
/** @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) {
if (matches.length >= 2) {
return matches[1];
}
return string;
}
async __itemsData(items, keys, authParams) {
const data = await this.modelManager.getJSONDataForItems(items, keys, authParams);
const blobData = new Blob([data], {type: 'text/json'});
/** @private */
async itemsData(items, intent) {
const data = await this.application.protocolService.createBackupFile({
subItems: items,
intent: intent
});
const blobData = new Blob([data], { type: 'text/json' });
return blobData;
}
__loadZip(callback) {
if(window.zip) {
callback();
/** @private */
async loadZip() {
if (window.zip) {
return;
}
@@ -81,75 +80,77 @@ export class ArchiveManager {
scriptTag.async = false;
var headTag = document.getElementsByTagName('head')[0];
headTag.appendChild(scriptTag);
scriptTag.onload = function() {
zip.workerScriptsPath = "assets/zip/";
callback();
};
}
__downloadZippedItems(items) {
this.__loadZip(() => {
zip.createWriter(new zip.BlobWriter("application/zip"), (zipWriter) => {
var index = 0;
const nextFile = () => {
var item = items[index];
var name, contents;
if(item.content_type === "Note") {
name = item.content.title;
contents = item.content.text;
} else {
name = item.content_type;
contents = JSON.stringify(item.content, null, 2);
}
if(!name) {
name = "";
}
const blob = new Blob([contents], {type: 'text/plain'});
let filePrefix = name.replace(/\//g, "").replace(/\\+/g, "");
const fileSuffix = `-${item.uuid.split("-")[0]}.txt`;
// Standard max filename length is 255. Slice the note name down to allow filenameEnd
filePrefix = filePrefix.slice(0, (255 - fileSuffix.length));
const fileName = `${item.content_type}/${filePrefix}${fileSuffix}`;
zipWriter.add(fileName, new zip.BlobReader(blob), () => {
index++;
if(index < items.length) {
nextFile();
} else {
zipWriter.close((blob) => {
this.__downloadData(blob, `Standard Notes Backup - ${this.__formattedDate()}.zip`);
zipWriter = null;
});
}
});
};
nextFile();
}, onerror);
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;
__hrefForData(data) {
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;
}
__downloadData(data, fileName) {
/** @private */
downloadData(data, fileName) {
var link = document.createElement('a');
link.setAttribute('download', fileName);
link.href = this.__hrefForData(data);
link.href = this.hrefForData(data);
document.body.appendChild(link);
link.click();
link.remove();

View File

@@ -1,7 +1,8 @@
/* eslint-disable camelcase */
// An interface used by the Desktop app to interact with SN
import _ from 'lodash';
import pull from 'lodash/pull';
import { isDesktopApplication } from '@/utils';
import { SFItemParams, SFModelManager } from 'snjs';
import { EncryptionIntents } from 'snjs';
const COMPONENT_DATA_KEY_INSTALL_ERROR = 'installError';
const COMPONENT_CONTENT_KEY_PACKAGE_INFO = 'package_info';
@@ -12,33 +13,26 @@ export class DesktopManager {
constructor(
$rootScope,
$timeout,
modelManager,
syncManager,
authManager,
lockManager,
appState
application,
appState,
) {
this.lockManager = lockManager;
this.modelManager = modelManager;
this.authManager = authManager;
this.syncManager = syncManager;
this.$rootScope = $rootScope;
this.$timeout = $timeout;
this.appState = appState;
this.timeout = $timeout;
this.updateObservers = [];
this.application = application;
this.componentActivationObservers = [];
this.updateObservers = [];
this.isDesktop = isDesktopApplication();
$rootScope.$on("initial-data-loaded", () => {
$rootScope.$on('initial-data-loaded', () => {
this.dataLoaded = true;
if(this.dataLoadHandler) {
if (this.dataLoadHandler) {
this.dataLoadHandler();
}
});
$rootScope.$on("major-data-change", () => {
if(this.majorDataChangeHandler) {
$rootScope.$on('major-data-change', () => {
if (this.majorDataChangeHandler) {
this.majorDataChangeHandler();
}
});
@@ -56,17 +50,20 @@ export class DesktopManager {
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
/**
* 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);
return this.application.protocolService.payloadByEncryptingPayload({
payload: component.payloadRepresentation(),
intent: EncryptionIntents.FileDecrypted
});
}
// All `components` should be installed
syncComponentsInstallation(components) {
if(!this.isDesktop) {
if (!this.isDesktop) {
return;
}
Promise.all(components.map((component) => {
@@ -91,21 +88,21 @@ export class DesktopManager {
}
searchText(text) {
if(!this.isDesktop) {
if (!this.isDesktop) {
return;
}
this.lastSearchedText = text;
this.searchHandler && this.searchHandler(text);
}
redoSearch() {
if(this.lastSearchedText) {
redoSearch() {
if (this.lastSearchedText) {
this.searchText(this.lastSearchedText);
}
}
deregisterUpdateObserver(observer) {
_.pull(this.updateObservers, observer);
pull(this.updateObservers, observer);
}
// Pass null to cancel search
@@ -114,19 +111,19 @@ export class DesktopManager {
}
desktop_windowGainedFocus() {
this.$rootScope.$broadcast("window-gained-focus");
this.$rootScope.$broadcast('window-gained-focus');
}
desktop_windowLostFocus() {
this.$rootScope.$broadcast("window-lost-focus");
this.$rootScope.$broadcast('window-lost-focus');
}
desktop_onComponentInstallationComplete(componentData, error) {
const component = this.modelManager.findItem(componentData.uuid);
if(!component) {
async desktop_onComponentInstallationComplete(componentData, error) {
const component = await this.application.findItem({ uuid: componentData.uuid });
if (!component) {
return;
}
if(error) {
if (error) {
component.setAppDataItem(
COMPONENT_DATA_KEY_INSTALL_ERROR,
error
@@ -136,35 +133,30 @@ export class DesktopManager {
COMPONENT_CONTENT_KEY_PACKAGE_INFO,
COMPONENT_CONTENT_KEY_LOCAL_URL
];
for(const key of permissableKeys) {
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) {
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};
const observer = { id: Math.random, callback: callback };
this.componentActivationObservers.push(observer);
return observer;
}
desktop_deregisterComponentActivationObserver(observer) {
_.pull(this.componentActivationObservers, observer);
pull(this.componentActivationObservers, observer);
}
/* Notify observers that a component has been registered/activated */
@@ -172,14 +164,14 @@ export class DesktopManager {
const serializedComponent = await this.convertComponentForTransmission(
component
);
this.timeout(() => {
for(const observer of this.componentActivationObservers) {
this.$timeout(() => {
for (const observer of this.componentActivationObservers) {
observer.callback(serializedComponent);
}
});
}
/* Used to resolve "sn://" */
/* Used to resolve 'sn://' */
desktop_setExtServerHost(host) {
this.extServerHost = host;
this.appState.desktopExtensionsReady();
@@ -195,28 +187,16 @@ export class DesktopManager {
desktop_setInitialDataLoadHandler(handler) {
this.dataLoadHandler = handler;
if(this.dataLoaded) {
if (this.dataLoaded) {
this.dataLoadHandler();
}
}
async desktop_requestBackupFile(callback) {
let keys, authParams;
if(this.authManager.offline() && this.lockManager.hasPasscode()) {
keys = this.lockManager.keys();
authParams = this.lockManager.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);
const data = await this.application.protocolService.createBackupFile({
returnIfEmpty: true
});
callback(data);
}
desktop_setMajorDataChangeHandler(handler) {

View File

@@ -1,21 +1,10 @@
export { ActionsManager } from './actionsManager';
export { ArchiveManager } from './archiveManager';
export { AuthManager } from './authManager';
export { ComponentManager } from './componentManager';
export { DatabaseManager } from './databaseManager';
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 { LockManager } from './lockManager';
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';

View File

@@ -1,42 +1,41 @@
export class KeyboardManager {
/** @public */
export const KeyboardKeys = {
Tab: "Tab",
Backspace: "Backspace",
Up: "ArrowUp",
Down: "ArrowDown",
};
/** @public */
export const KeyboardModifiers = {
Shift: "Shift",
Ctrl: "Control",
/** ⌘ key on Mac, ⊞ key on Windows */
Meta: "Meta",
Alt: "Alt",
};
/** @private */
const KeyboardKeyEvents = {
Down: "KeyEventDown",
Up: "KeyEventUp"
};
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) => {
const allModifiers = Object.keys(KeyboardModifiers).map((key) => KeyboardModifiers[key]);
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 == 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)
((event.ctrlKey || event.key === KeyboardModifiers.Ctrl) && modifier === KeyboardModifiers.Ctrl) ||
((event.metaKey || event.key === KeyboardModifiers.Meta) && modifier === KeyboardModifiers.Meta) ||
((event.altKey || event.key === KeyboardModifiers.Alt) && modifier === KeyboardModifiers.Alt) ||
((event.shiftKey || event.key === KeyboardModifiers.Shift) && modifier === KeyboardModifiers.Shift)
);
return matches;
@@ -45,50 +44,50 @@ export class KeyboardManager {
return eventModifiers;
}
eventMatchesKeyAndModifiers(event, key, modifiers = []) {
eventMatchesKeyAndModifiers(event, key, modifiers = []) {
const eventModifiers = this.modifiersForEvent(event);
if(eventModifiers.length != modifiers.length) {
if (eventModifiers.length !== modifiers.length) {
return false;
}
for(const modifier of modifiers) {
if(!eventModifiers.includes(modifier)) {
for (const modifier of modifiers) {
if (!eventModifiers.includes(modifier)) {
return false;
}
}
// Modifers match, check key
if(!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();
return key.toLowerCase() === event.key.toLowerCase();
}
notifyObserver(event, keyEventType) {
for(const observer of this.observers) {
if(observer.element && event.target != observer.element) {
for (const observer of this.observers) {
if (observer.element && event.target !== observer.element) {
continue;
}
if(observer.elements && !observer.elements.includes(event.target)) {
if (observer.elements && !observer.elements.includes(event.target)) {
continue;
}
if(observer.notElement && observer.notElement == event.target) {
if (observer.notElement && observer.notElement === event.target) {
continue;
}
if(observer.notElementIds && observer.notElementIds.includes(event.target.id)) {
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) {
if (this.eventMatchesKeyAndModifiers(event, observer.key, observer.modifiers)) {
const callback = keyEventType === KeyboardKeyEvents.Down ? observer.onKeyDown : observer.onKeyUp;
if (callback) {
callback(event);
}
}
@@ -96,15 +95,15 @@ export class KeyboardManager {
}
handleKeyDown(event) {
this.notifyObserver(event, KeyboardManager.KeyEventDown);
this.notifyObserver(event, KeyboardKeyEvents.Down);
}
handleKeyUp(event) {
this.notifyObserver(event, KeyboardManager.KeyEventUp);
this.notifyObserver(event, KeyboardKeyEvents.Up);
}
addKeyObserver({key, modifiers, onKeyDown, onKeyUp, element, elements, notElement, notElementIds}) {
const observer = {key, modifiers, onKeyDown, onKeyUp, element, elements, notElement, notElementIds};
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;
}

View File

@@ -1,9 +1,5 @@
import _ from 'lodash';
import { isDesktopApplication } from '@/utils';
import {
APP_STATE_EVENT_WINDOW_DID_BLUR,
APP_STATE_EVENT_WINDOW_DID_FOCUS
} from '../state';
import { AppStateEvents } from '../state';
const MILLISECONDS_PER_SECOND = 1000;
const FOCUS_POLL_INTERVAL = 1 * MILLISECONDS_PER_SECOND;
@@ -26,9 +22,9 @@ export class LockManager {
observeVisibility() {
this.appState.addObserver((eventName, data) => {
if(eventName === APP_STATE_EVENT_WINDOW_DID_BLUR) {
if(eventName === AppStateEvents.WindowDidBlur) {
this.documentVisibilityChanged(false);
} else if(eventName === APP_STATE_EVENT_WINDOW_DID_FOCUS) {
} else if(eventName === AppStateEvents.WindowDidFocus) {
this.documentVisibilityChanged(true);
}
});

View File

@@ -1,183 +1,177 @@
/* A class for handling installation of system extensions */
import { isDesktopApplication, dictToArray } from '@/utils';
import { SFPredicate, ContentTypes, CreateMaxPayloadFromAnyObject } from 'snjs';
import { AppStateEvents } from '@/state';
import { isDesktopApplication } from '@/utils';
import { SFPredicate } from 'snjs';
const STREAM_ITEMS_PERMISSION = 'stream-items';
/** A class for handling installation of system extensions */
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";
constructor(application, appState) {
this.application = application;
this.extManagerId = 'org.standardnotes.extensions-manager';
this.batchManagerId = 'org.standardnotes.batch-manager';
this.systemExtensions = [];
this.resolveExtensionsManager();
this.resolveBatchManager();
appState.addObserver(async (eventName) => {
if (eventName === AppStateEvents.ApplicationReady) {
await this.initialize();
}
});
}
isSystemExtension(extension) {
return this.systemExtensions.includes(extension.uuid);
}
resolveExtensionsManager() {
async initialize() {
this.resolveExtensionsManager();
this.resolveBatchManager();
}
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;
extensionsManagerTemplatePayload() {
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
};
const content = {
name: packageInfo.name,
area: 'rooms',
package_info: packageInfo,
permissions: [
{
name: STREAM_ITEMS_PERMISSION,
content_types: [
ContentTypes.Component,
ContentTypes.Theme,
ContentTypes.ServerExtension,
ContentTypes.ActionsExtension,
ContentTypes.Mfa,
ContentTypes.Editor,
ContentTypes.ExtensionRepo
]
}
]
};
if (isDesktopApplication()) {
content.local_url = window._extensions_manager_location;
} else {
content.hosted_url = window._extensions_manager_location;
}
const payload = CreateMaxPayloadFromAnyObject({
object: {
content_type: ContentTypes.Component,
content: content
}
});
return payload;
}
// 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");
async resolveExtensionsManager() {
const contentTypePredicate = new SFPredicate('content_type', '=', ContentTypes.Component);
const packagePredicate = new SFPredicate('package_info.identifier', '=', this.extManagerId);
const predicate = SFPredicate.CompoundPredicate([contentTypePredicate, packagePredicate]);
const extensionsManager = await this.application.singletonManager.findOrCreateSingleton({
predicate: predicate,
createPayload: this.extensionsManagerTemplatePayload()
});
this.systemExtensions.push(extensionsManager.uuid);
let needsSync = false;
if (isDesktopApplication()) {
if (!extensionsManager.local_url) {
extensionsManager.local_url = window._extensions_manager_location;
needsSync = true;
}
if(needsSync) {
this.modelManager.setItemDirty(resolvedSingleton, true);
this.syncManager.sync();
} else {
if (!extensionsManager.hosted_url) {
extensionsManager.hosted_url = window._extensions_manager_location;
needsSync = true;
}
}, (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);
});
}
// Handle addition of SN|ExtensionRepo permission
const permission = extensionsManager.content.permissions.find((p) => p.name === STREAM_ITEMS_PERMISSION);
if (!permission.content_types.includes(ContentTypes.ExtensionRepo)) {
permission.content_types.push(ContentTypes.ExtensionRepo);
needsSync = true;
}
if (needsSync) {
this.application.saveItem({ item: extensionsManager });
}
}
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;
batchManagerTemplatePayload() {
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
};
const allContentTypes = dictToArray(ContentTypes);
const content = {
name: packageInfo.name,
area: 'modal',
package_info: packageInfo,
permissions: [
{
name: STREAM_ITEMS_PERMISSION,
content_types: allContentTypes
}
]
};
if (isDesktopApplication()) {
content.local_url = window._batch_manager_location;
} else {
content.hosted_url = window._batch_manager_location;
}
const payload = CreateMaxPayloadFromAnyObject({
object: {
content_type: ContentTypes.Component,
content: content
}
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);
});
return payload;
}
async resolveBatchManager() {
const contentTypePredicate = new SFPredicate('content_type', '=', ContentTypes.Component);
const packagePredicate = new SFPredicate('package_info.identifier', '=', this.batchManagerId);
const predicate = SFPredicate.CompoundPredicate([contentTypePredicate, packagePredicate]);
const batchManager = await this.application.singletonManager.findOrCreateSingleton({
predicate: predicate,
createPayload: this.batchManagerTemplatePayload()
});
this.systemExtensions.push(batchManager.uuid);
let needsSync = false;
if (isDesktopApplication()) {
if (!batchManager.local_url) {
batchManager.local_url = window._batch_manager_location;
needsSync = true;
}
} else {
if (!batchManager.hosted_url) {
batchManager.hosted_url = window._batch_manager_location;
needsSync = true;
}
}
// Handle addition of SN|ExtensionRepo permission
const permission = batchManager.content.permissions.find((p) => p.name === STREAM_ITEMS_PERMISSION);
if (!permission.content_types.includes(ContentTypes.ExtensionRepo)) {
permission.content_types.push(ContentTypes.ExtensionRepo);
needsSync = true;
}
if (needsSync) {
this.application.saveItem({ item: batchManager });
}
}
}

View File

@@ -1,63 +1,63 @@
import { SFPredicate, SFItem } from 'snjs';
import { SFPredicate, CreateMaxPayloadFromAnyObject } from 'snjs';
import { AppStateEvents } from '../state';
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 const PrefKeys = {
TagsPanelWidth: 'tagsPanelWidth',
NotesPanelWidth: 'notesPanelWidth',
EditorWidth: 'editorWidth',
EditorLeft: 'editorLeft',
EditorMonospaceEnabled: 'monospaceFont',
EditorSpellcheck: 'spellcheck',
EditorResizersEnabled: 'marginResizersEnabled',
SortNotesBy: 'sortBy',
SortNotesReverse: 'sortReverse',
NotesShowArchived: 'showArchived',
NotesHidePinned: 'hidePinned',
NotesHideNotePreview: 'hideNotePreview',
NotesHideDate: 'hideDate',
NotesHideTags: 'hideTags'
};
export class PreferencesManager {
/* @ngInject */
constructor(
modelManager,
singletonManager,
appState,
syncManager
application
) {
this.singletonManager = singletonManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
this.application = application;
this.appState = appState;
this.modelManager.addItemSyncObserver(
'user-prefs',
'SN|UserPreferences',
(allItems, validItems, deletedItems, source, sourceKey) => {
this.preferencesDidChange();
appState.addObserver(async (eventName) => {
if (eventName === AppStateEvents.ApplicationReady) {
await this.initialize();
}
);
});
}
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);
async initialize() {
this.streamPreferences();
await this.loadSingleton();
}
streamPreferences() {
this.application.streamItems({
contentType: 'SN|UserPreferences',
stream: () => {
this.preferencesDidChange();
}
);
});
}
async loadSingleton() {
const contentType = 'SN|UserPreferences';
const predicate = new SFPredicate('content_type', '=', contentType);
this.userPreferences = await this.application.singletonManager.findOrCreateSingleton({
predicate: predicate,
createPayload: CreateMaxPayloadFromAnyObject({
object: {
content_type: contentType
}
})
});
}
preferencesDidChange() {
@@ -65,21 +65,20 @@ export class PreferencesManager {
}
syncUserPreferences() {
if(this.userPreferences) {
this.modelManager.setItemDirty(this.userPreferences);
this.syncManager.sync();
if (this.userPreferences) {
this.application.saveItem({item: this.userPreferences});
}
}
getValue(key, defaultValue) {
if(!this.userPreferences) { return 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) {
if (sync) {
this.syncUserPreferences();
}
}

View File

@@ -1,42 +1,28 @@
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';
import { SNTheme, StorageValueModes, EncryptionIntents } from 'snjs';
import { AppStateEvents } from '@/state';
const CACHED_THEMES_KEY = 'cachedThemes';
export class ThemeManager {
/* @ngInject */
constructor(
componentManager,
application,
appState,
desktopManager,
storageManager,
lockManager,
appState
) {
this.componentManager = componentManager;
this.storageManager = storageManager;
this.application = application;
this.appState = appState;
this.desktopManager = desktopManager;
this.activeThemes = [];
ThemeManager.CachedThemesKey = "cachedThemes";
this.registerObservers();
if (desktopManager.isDesktop) {
appState.addObserver((eventName, data) => {
if (eventName === APP_STATE_EVENT_DESKTOP_EXTS_READY) {
this.activateCachedThemes();
}
});
} else {
if (!desktopManager.isDesktop) {
this.activateCachedThemes();
}
}
activateCachedThemes() {
const cachedThemes = this.getCachedThemes();
async activateCachedThemes() {
const cachedThemes = await this.getCachedThemes();
const writeToCache = false;
for (const theme of cachedThemes) {
this.activateTheme(theme, writeToCache);
@@ -44,6 +30,11 @@ export class ThemeManager {
}
registerObservers() {
this.appState.addObserver((eventName, data) => {
if (eventName === AppStateEvents.DesktopExtsReady) {
this.activateCachedThemes();
}
});
this.desktopManager.registerUpdateObserver((component) => {
// Reload theme if active
if (component.active && component.isTheme()) {
@@ -54,9 +45,9 @@ export class ThemeManager {
}
});
this.componentManager.registerHandler({
identifier: "themeManager",
areas: ["themes"],
this.application.componentManager.registerHandler({
identifier: 'themeManager',
areas: ['themes'],
activationHandler: (component) => {
if (component.active) {
this.activateTheme(component);
@@ -68,14 +59,14 @@ export class ThemeManager {
}
hasActiveTheme() {
return this.componentManager.getActiveThemes().length > 0;
return this.application.componentManager.getActiveThemes().length > 0;
}
deactivateAllThemes() {
var activeThemes = this.componentManager.getActiveThemes();
var activeThemes = this.application.componentManager.getActiveThemes();
for (var theme of activeThemes) {
if (theme) {
this.componentManager.deactivateComponent(theme);
this.application.componentManager.deactivateComponent(theme);
}
}
@@ -86,25 +77,22 @@ export class ThemeManager {
if (_.find(this.activeThemes, { uuid: theme.uuid })) {
return;
}
this.activeThemes.push(theme);
var url = this.componentManager.urlForComponent(theme);
var link = document.createElement("link");
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.type = 'text/css';
link.rel = 'stylesheet';
link.media = 'screen,print';
link.id = theme.uuid;
document.getElementsByTagName("head")[0].appendChild(link);
document.getElementsByTagName('head')[0].appendChild(link);
if (writeToCache) {
this.cacheThemes();
}
}
deactivateTheme(theme) {
var element = document.getElementById(theme.uuid);
const element = document.getElementById(theme.uuid);
if (element) {
element.disabled = true;
element.parentNode.removeChild(element);
@@ -117,20 +105,33 @@ export class ThemeManager {
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 payload = theme.payloadRepresentation();
const processedPayload = await this.application.protocolService.payloadByEncryptingPayload({
payload: payload,
intent: EncryptionIntents.LocalStorageDecrypted
});
return processedPayload;
}));
const data = JSON.stringify(mapped);
return this.storageManager.setItem(ThemeManager.CachedThemesKey, data, StorageManager.Fixed);
return this.application.setValue(
CACHED_THEMES_KEY,
data,
StorageValueModes.Nonwrapped
);
}
async decacheThemes() {
return this.storageManager.removeItem(ThemeManager.CachedThemesKey, StorageManager.Fixed);
return this.application.removeValue(
CACHED_THEMES_KEY,
StorageValueModes.Nonwrapped
);
}
getCachedThemes() {
const cachedThemes = this.storageManager.getItemSync(ThemeManager.CachedThemesKey, StorageManager.Fixed);
async getCachedThemes() {
const cachedThemes = await this.application.getValue(
CACHED_THEMES_KEY,
StorageValueModes.Nonwrapped
);
if (cachedThemes) {
const parsed = JSON.parse(cachedThemes);
return parsed.map((theme) => {