First pass at linting lib dir

pull/1786/head
Laurent Cozic 2019-07-29 15:43:53 +02:00
parent 64b7bc3d62
commit 86dc72b204
170 changed files with 4140 additions and 3119 deletions

View File

@ -4,37 +4,36 @@ ArrayUtils.unique = function(array) {
return array.filter(function(elem, index, self) {
return index === self.indexOf(elem);
});
}
};
ArrayUtils.removeElement = function(array, element) {
const index = array.indexOf(element);
if (index < 0) return array;
array.splice(index, 1);
return array;
}
};
// https://stackoverflow.com/a/10264318/561309
ArrayUtils.binarySearch = function(items, value) {
var startIndex = 0,
stopIndex = items.length - 1,
middle = Math.floor((stopIndex + startIndex)/2);
while(items[middle] != value && startIndex < stopIndex){
var startIndex = 0,
stopIndex = items.length - 1,
middle = Math.floor((stopIndex + startIndex) / 2);
while (items[middle] != value && startIndex < stopIndex) {
//adjust search area
if (value < items[middle]){
if (value < items[middle]) {
stopIndex = middle - 1;
} else if (value > items[middle]){
} else if (value > items[middle]) {
startIndex = middle + 1;
}
//recalculate middle
middle = Math.floor((stopIndex + startIndex)/2);
middle = Math.floor((stopIndex + startIndex) / 2);
}
//make sure it's the right value
return (items[middle] != value) ? -1 : middle;
}
return items[middle] != value ? -1 : middle;
};
ArrayUtils.findByKey = function(array, key, value) {
for (let i = 0; i < array.length; i++) {
@ -43,7 +42,7 @@ ArrayUtils.findByKey = function(array, key, value) {
if (o[key] === value) return o;
}
return null;
}
};
ArrayUtils.contentEquals = function(array1, array2) {
if (array1 === array2) return true;
@ -56,6 +55,6 @@ ArrayUtils.contentEquals = function(array1, array2) {
}
return true;
}
};
module.exports = ArrayUtils;
module.exports = ArrayUtils;

View File

@ -50,7 +50,6 @@ SyncTargetRegistry.addClass(SyncTargetWebDAV);
SyncTargetRegistry.addClass(SyncTargetDropbox);
class BaseApplication {
constructor() {
this.logger_ = new Logger();
this.dbLogger_ = new Logger();
@ -322,9 +321,9 @@ class BaseApplication {
}
generalMiddlewareFn() {
const middleware = store => next => (action) => {
const middleware = store => next => action => {
return this.generalMiddleware(store, next, action);
}
};
return middleware;
}
@ -341,14 +340,14 @@ class BaseApplication {
await reduxSharedMiddleware(store, next, action);
if (this.hasGui() && ["NOTE_UPDATE_ONE", "NOTE_DELETE", "FOLDER_UPDATE_ONE", "FOLDER_DELETE"].indexOf(action.type) >= 0) {
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync(30 * 1000, { syncSteps: ["update_remote", "delete_remote"] });
if (this.hasGui() && ['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (!(await reg.syncTarget().syncStarted())) reg.scheduleSync(30 * 1000, { syncSteps: ['update_remote', 'delete_remote'] });
SearchEngine.instance().scheduleSyncTables();
}
// Don't add FOLDER_UPDATE_ALL as refreshFolders() is calling it too, which
// would cause the sidebar to refresh all the time.
if (this.hasGui() && ["FOLDER_UPDATE_ONE"].indexOf(action.type) >= 0) {
if (this.hasGui() && ['FOLDER_UPDATE_ONE'].indexOf(action.type) >= 0) {
refreshFolders = true;
}
@ -395,17 +394,17 @@ class BaseApplication {
// });
// }
if ((action.type == 'SETTING_UPDATE_ONE' && (action.key == 'dateFormat' || action.key == 'timeFormat')) || (action.type == 'SETTING_UPDATE_ALL')) {
if ((action.type == 'SETTING_UPDATE_ONE' && (action.key == 'dateFormat' || action.key == 'timeFormat')) || action.type == 'SETTING_UPDATE_ALL') {
time.setDateFormat(Setting.value('dateFormat'));
time.setTimeFormat(Setting.value('timeFormat'));
}
if ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'net.ignoreTlsErrors') || (action.type == 'SETTING_UPDATE_ALL')) {
if ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'net.ignoreTlsErrors') || action.type == 'SETTING_UPDATE_ALL') {
// https://stackoverflow.com/questions/20082893/unable-to-verify-leaf-signature
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = Setting.value('net.ignoreTlsErrors') ? '0' : '1';
}
if ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'net.customCertificates') || (action.type == 'SETTING_UPDATE_ALL')) {
if ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'net.customCertificates') || action.type == 'SETTING_UPDATE_ALL') {
const caPaths = Setting.value('net.customCertificates').split(',');
for (let i = 0; i < caPaths.length; i++) {
const f = caPaths[i].trim();
@ -414,7 +413,7 @@ class BaseApplication {
}
}
if ((action.type == 'SETTING_UPDATE_ONE' && (action.key.indexOf('encryption.') === 0)) || (action.type == 'SETTING_UPDATE_ALL')) {
if ((action.type == 'SETTING_UPDATE_ONE' && action.key.indexOf('encryption.') === 0) || action.type == 'SETTING_UPDATE_ALL') {
if (this.hasGui()) {
await EncryptionService.instance().loadMasterKeysFromSettings();
DecryptionWorker.instance().scheduleStart();
@ -439,7 +438,7 @@ class BaseApplication {
refreshFolders = 'now';
}
if (this.hasGui() && action.type == 'SETTING_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTING_UPDATE_ALL') {
if ((this.hasGui() && action.type == 'SETTING_UPDATE_ONE' && action.key == 'sync.interval') || action.type == 'SETTING_UPDATE_ALL') {
reg.setupRecurrentSync();
}
@ -459,7 +458,7 @@ class BaseApplication {
}
}
return result;
return result;
}
dispatch(action) {
@ -523,11 +522,8 @@ class BaseApplication {
console.info('--------------------------------------------------');
console.info(markdown);
console.info('--------------------------------------------------');
}
async start(argv) {
let startFlags = await this.handleStartFlags_(argv);
@ -568,7 +564,7 @@ class BaseApplication {
this.logger_.setLevel(initArgs.logLevel);
reg.setLogger(this.logger_);
reg.dispatch = (o) => {};
reg.dispatch = o => {};
this.dbLogger_.addTarget('file', { path: profileDir + '/log-database.txt' });
this.dbLogger_.setLevel(initArgs.logLevel);
@ -609,9 +605,11 @@ class BaseApplication {
if ('welcomeDisabled' in initArgs) Setting.setValue('welcome.enabled', !initArgs.welcomeDisabled);
if (!Setting.value('api.token')) {
EncryptionService.instance().randomHexString(64).then((token) => {
Setting.setValue('api.token', token);
});
EncryptionService.instance()
.randomHexString(64)
.then(token => {
Setting.setValue('api.token', token);
});
}
time.setDateFormat(Setting.value('dateFormat'));
@ -630,7 +628,9 @@ class BaseApplication {
await EncryptionService.instance().loadMasterKeysFromSettings();
DecryptionWorker.instance().on('resourceMetadataButNotBlobDecrypted', this.decryptionWorker_resourceMetadataButNotBlobDecrypted);
ResourceFetcher.instance().setFileApi(() => { return reg.syncTarget().fileApi() });
ResourceFetcher.instance().setFileApi(() => {
return reg.syncTarget().fileApi();
});
ResourceFetcher.instance().setLogger(this.logger_);
ResourceFetcher.instance().on('downloadComplete', this.resourceFetcher_downloadComplete);
ResourceFetcher.instance().start();
@ -649,7 +649,6 @@ class BaseApplication {
return argv;
}
}
module.exports = { BaseApplication };

View File

@ -4,7 +4,6 @@ const { time } = require('lib/time-utils.js');
const Mutex = require('async-mutex').Mutex;
class BaseModel {
static modelType() {
throw new Error('Must be overriden');
}
@ -15,7 +14,7 @@ class BaseModel {
static addModelMd(model) {
if (!model) return model;
if (Array.isArray(model)) {
let output = [];
for (let i = 0; i < model.length; i++) {
@ -144,9 +143,11 @@ class BaseModel {
if (!options) options = {};
let sql = 'SELECT count(*) as total FROM `' + this.tableName() + '`';
if (options.where) sql += ' WHERE ' + options.where;
return this.db().selectOne(sql).then((r) => {
return r ? r['total'] : 0;
});
return this.db()
.selectOne(sql)
.then(r => {
return r ? r['total'] : 0;
});
}
static load(id) {
@ -175,7 +176,7 @@ class BaseModel {
}
sql += ' ORDER BY ' + items.join(', ');
}
if (options.limit) sql += ' LIMIT ' + options.limit;
return { sql: sql, params: params };
@ -184,7 +185,7 @@ class BaseModel {
static async allIds(options = null) {
let q = this.applySqlOptions(options, 'SELECT id FROM `' + this.tableName() + '`');
const rows = await this.db().selectAll(q.sql, q.params);
return rows.map((r) => r.id);
return rows.map(r => r.id);
}
static async all(options = null) {
@ -230,16 +231,20 @@ class BaseModel {
static modelSelectOne(sql, params = null) {
if (params === null) params = [];
return this.db().selectOne(sql, params).then((model) => {
return this.filter(this.addModelMd(model));
});
return this.db()
.selectOne(sql, params)
.then(model => {
return this.filter(this.addModelMd(model));
});
}
static modelSelectAll(sql, params = null) {
if (params === null) params = [];
return this.db().selectAll(sql, params).then((models) => {
return this.filterArray(this.addModelMd(models));
});
return this.db()
.selectAll(sql, params)
.then(models => {
return this.filterArray(this.addModelMd(models));
});
}
static loadByField(fieldName, fieldValue, options = null) {
@ -284,7 +289,9 @@ class BaseModel {
static saveMutex(modelOrId) {
const noLockMutex = {
acquire: function() { return null; }
acquire: function() {
return null;
},
};
if (!modelOrId) return noLockMutex;
@ -317,7 +324,7 @@ class BaseModel {
}
static saveQuery(o, options) {
let temp = {}
let temp = {};
let fieldNames = this.fieldNames();
for (let i = 0; i < fieldNames.length; i++) {
let n = fieldNames[i];
@ -455,13 +462,12 @@ class BaseModel {
} finally {
this.releaseSaveMutex(o, mutexRelease);
}
return output;
}
static isNew(object, options) {
if (options && ('isNew' in options)) {
if (options && 'isNew' in options) {
// options.isNew can be "auto" too
if (options.isNew === true) return true;
if (options.isNew === false) return false;
@ -489,7 +495,7 @@ class BaseModel {
if (output[n] === true) {
output[n] = 1;
} else if (output[n] === false) {
output[n] = 0;
output[n] = 0;
} else {
const t = this.fieldType(n, Database.TYPE_UNKNOWN);
if (t === Database.TYPE_INT) {
@ -497,7 +503,7 @@ class BaseModel {
}
}
}
return output;
}
@ -513,35 +519,19 @@ class BaseModel {
const idFieldName = options.idFieldName ? options.idFieldName : 'id';
const sql = 'DELETE FROM ' + this.tableName() + ' WHERE ' + idFieldName + ' IN ("' + ids.join('","') + '")';
return this.db().exec(sql);
}
}
static db() {
if (!this.db_) throw new Error('Accessing database before it has been initialised');
return this.db_;
return this.db_;
}
static isReady() {
return !!this.db_;
}
}
BaseModel.typeEnum_ = [
['TYPE_NOTE', 1],
['TYPE_FOLDER', 2],
['TYPE_SETTING', 3],
['TYPE_RESOURCE', 4],
['TYPE_TAG', 5],
['TYPE_NOTE_TAG', 6],
['TYPE_SEARCH', 7],
['TYPE_ALARM', 8],
['TYPE_MASTER_KEY', 9],
['TYPE_ITEM_CHANGE', 10],
['TYPE_NOTE_RESOURCE', 11],
['TYPE_RESOURCE_LOCAL_STATE', 12],
['TYPE_REVISION', 13],
['TYPE_MIGRATION', 14],
];
BaseModel.typeEnum_ = [['TYPE_NOTE', 1], ['TYPE_FOLDER', 2], ['TYPE_SETTING', 3], ['TYPE_RESOURCE', 4], ['TYPE_TAG', 5], ['TYPE_NOTE_TAG', 6], ['TYPE_SEARCH', 7], ['TYPE_ALARM', 8], ['TYPE_MASTER_KEY', 9], ['TYPE_ITEM_CHANGE', 10], ['TYPE_NOTE_RESOURCE', 11], ['TYPE_RESOURCE_LOCAL_STATE', 12], ['TYPE_REVISION', 13], ['TYPE_MIGRATION', 14]];
for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
const e = BaseModel.typeEnum_[i];
@ -552,4 +542,4 @@ BaseModel.db_ = null;
BaseModel.dispatch = function(o) {};
BaseModel.saveMutexes_ = {};
module.exports = BaseModel;
module.exports = BaseModel;

View File

@ -1,7 +1,6 @@
const EncryptionService = require('lib/services/EncryptionService.js');
class BaseSyncTarget {
constructor(db, options = null) {
this.db_ = db;
this.synchronizer_ = null;
@ -19,7 +18,7 @@ class BaseSyncTarget {
}
option(name, defaultValue = null) {
return this.options_ && (name in this.options_) ? this.options_[name] : defaultValue;
return this.options_ && name in this.options_ ? this.options_[name] : defaultValue;
}
logger() {
@ -117,13 +116,12 @@ class BaseSyncTarget {
async syncStarted() {
if (!this.synchronizer_) return false;
if (!await this.isAuthenticated()) return false;
if (!(await this.isAuthenticated())) return false;
const sync = await this.synchronizer();
return sync.state() != 'idle';
}
}
BaseSyncTarget.dispatch = (action) => {};
BaseSyncTarget.dispatch = action => {};
module.exports = BaseSyncTarget;
module.exports = BaseSyncTarget;

View File

@ -1,5 +1,4 @@
class Cache {
async getItem(name) {
let output = null;
try {
@ -22,7 +21,6 @@ class Cache {
// Defaults to not saving to cache
}
}
}
Cache.storage = async function() {
@ -30,6 +28,6 @@ Cache.storage = async function() {
Cache.storage_ = require('node-persist');
await Cache.storage_.init({ dir: require('os').tmpdir() + '/joplin-cache', ttl: 1000 * 60 });
return Cache.storage_;
}
};
module.exports = Cache;
module.exports = Cache;

View File

@ -1,5 +1,5 @@
const { netUtils } = require('lib/net-utils');
const urlParser = require("url");
const urlParser = require('url');
const Setting = require('lib/models/Setting');
const { Logger } = require('lib/logger.js');
const randomClipperPort = require('lib/randomClipperPort');
@ -9,7 +9,6 @@ const ApiResponse = require('lib/services/rest/ApiResponse');
const multiparty = require('multiparty');
class ClipperServer {
constructor() {
this.logger_ = new Logger();
this.startState_ = 'idle';
@ -72,7 +71,7 @@ class ClipperServer {
if (!inUse) return state.port;
}
throw new Error('All potential ports are in use or not available.')
throw new Error('All potential ports are in use or not available.');
}
async start() {
@ -92,51 +91,54 @@ class ClipperServer {
this.server_ = require('http').createServer();
this.server_.on('request', async (request, response) => {
const writeCorsHeaders = (code, contentType = "application/json", additionalHeaders = null) => {
const headers = Object.assign({}, {
"Content-Type": contentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
'Access-Control-Allow-Headers': 'X-Requested-With,content-type',
}, additionalHeaders ? additionalHeaders : {});
const writeCorsHeaders = (code, contentType = 'application/json', additionalHeaders = null) => {
const headers = Object.assign(
{},
{
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
'Access-Control-Allow-Headers': 'X-Requested-With,content-type',
},
additionalHeaders ? additionalHeaders : {}
);
response.writeHead(code, headers);
}
};
const writeResponseJson = (code, object) => {
writeCorsHeaders(code);
response.write(JSON.stringify(object));
response.end();
}
};
const writeResponseText = (code, text) => {
writeCorsHeaders(code, 'text/plain');
response.write(text);
response.end();
}
};
const writeResponseInstance = (code, instance) => {
if (instance.type === 'attachment') {
const filename = instance.attachmentFilename ? instance.attachmentFilename : 'file';
writeCorsHeaders(code, instance.contentType ? instance.contentType : 'application/octet-stream', {
'Content-disposition': 'attachment; filename=' + filename,
'Content-Length': instance.body.length,
});
response.end(instance.body);
} else {
throw new Error('Not implemented');
}
}
if (instance.type === 'attachment') {
const filename = instance.attachmentFilename ? instance.attachmentFilename : 'file';
writeCorsHeaders(code, instance.contentType ? instance.contentType : 'application/octet-stream', {
'Content-disposition': 'attachment; filename=' + filename,
'Content-Length': instance.body.length,
});
response.end(instance.body);
} else {
throw new Error('Not implemented');
}
};
const writeResponse = (code, response) => {
if (response instanceof ApiResponse) {
writeResponseInstance(code, response);
writeResponseInstance(code, response);
} else if (typeof response === 'string') {
writeResponseText(code, response);
} else {
writeResponseJson(code, response);
}
}
};
this.logger().info('Request: ' + request.method + ' ' + request.url);
@ -155,7 +157,7 @@ class ClipperServer {
writeResponse(httpCode, { error: msg.join(': ') });
}
}
};
const contentType = request.headers['content-type'] ? request.headers['content-type'] : '';
@ -164,25 +166,21 @@ class ClipperServer {
response.end();
} else {
if (contentType.indexOf('multipart/form-data') === 0) {
const form = new multiparty.Form();
const form = new multiparty.Form();
form.parse(request, function(error, fields, files) {
if (error) {
form.parse(request, function(error, fields, files) {
if (error) {
writeResponse(error.httpCode ? error.httpCode : 500, error.message);
return;
} else {
execRequest(
request,
fields && fields.props && fields.props.length ? fields.props[0] : '',
files && files.data ? files.data : []
);
execRequest(request, fields && fields.props && fields.props.length ? fields.props[0] : '', files && files.data ? files.data : []);
}
});
});
} else {
if (request.method === 'POST' || request.method === 'PUT') {
let body = '';
request.on('data', (data) => {
request.on('data', data => {
body += data;
});
@ -211,7 +209,6 @@ class ClipperServer {
this.setStartState('idle');
this.setPort(null);
}
}
module.exports = ClipperServer;
module.exports = ClipperServer;

View File

@ -6,7 +6,6 @@ const { time } = require('lib/time-utils');
const EventDispatcher = require('lib/EventDispatcher');
class DropboxApi {
constructor(options) {
this.logger_ = new Logger();
this.options_ = options;
@ -65,7 +64,7 @@ class DropboxApi {
if (options.body) output.push('--data ' + '"' + options.body + '"');
output.push(url);
return output.join(' ');
return output.join(' ');
}
async execAuthToken(authCode) {
@ -80,16 +79,16 @@ class DropboxApi {
for (var property in postData) {
var encodedKey = encodeURIComponent(property);
var encodedValue = encodeURIComponent(postData[property]);
formBody.push(encodedKey + "=" + encodedValue);
formBody.push(encodedKey + '=' + encodedValue);
}
formBody = formBody.join("&");
formBody = formBody.join('&');
const response = await shim.fetch('https://api.dropboxapi.com/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
},
body: formBody
body: formBody,
});
const responseText = await response.text();
@ -145,13 +144,14 @@ class DropboxApi {
response = await shim.uploadBlob(url, fetchOptions);
} else if (options.target == 'string') {
response = await shim.fetch(url, fetchOptions);
} else { // file
} else {
// file
response = await shim.fetchBlob(url, fetchOptions);
}
const responseText = await response.text();
// console.info('Response: ' + responseText);
// console.info('Response: ' + responseText);
let responseJson_ = null;
const loadResponseJson = () => {
@ -163,10 +163,10 @@ class DropboxApi {
return { error: responseText };
}
return responseJson_;
}
};
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
const newError = (message) => {
const newError = message => {
const json = loadResponseJson();
let code = '';
if (json && json.error_summary) {
@ -179,7 +179,7 @@ class DropboxApi {
const error = new JoplinError(method + ' ' + path + ': ' + message + ' (' + response.status + '): ' + shortResponseText, code);
error.httpStatus = response.status;
return error;
}
};
if (!response.ok) {
const json = loadResponseJson();
@ -210,7 +210,6 @@ class DropboxApi {
}
}
}
}
module.exports = DropboxApi;

View File

@ -1,5 +1,4 @@
class EventDispatcher {
constructor() {
this.listeners_ = [];
}
@ -29,7 +28,6 @@ class EventDispatcher {
}
}
}
}
module.exports = EventDispatcher;
module.exports = EventDispatcher;

View File

@ -1,23 +1,21 @@
const TurndownService = require('joplin-turndown')
const TurndownService = require('joplin-turndown');
const markdownUtils = require('lib/markdownUtils');
class HtmlToMd {
parse(html, options = {}) {
const turndownPluginGfm = require('joplin-turndown-plugin-gfm').gfm
const turndownPluginGfm = require('joplin-turndown-plugin-gfm').gfm;
const turndown = new TurndownService({
headingStyle: 'atx',
anchorNames: options.anchorNames ? options.anchorNames.map(n => n.trim().toLowerCase()) : [],
codeBlockStyle: 'fenced',
})
turndown.use(turndownPluginGfm)
});
turndown.use(turndownPluginGfm);
turndown.remove('script');
turndown.remove('style');
let md = turndown.turndown(html)
let md = turndown.turndown(html);
if (options.baseUrl) md = markdownUtils.prependBaseUrl(md, options.baseUrl);
return md;
}
}
module.exports = HtmlToMd;
module.exports = HtmlToMd;

View File

@ -1,10 +1,8 @@
class JoplinError extends Error {
constructor(message, code = null) {
super(message);
this.code = code;
}
}
module.exports = JoplinError;
module.exports = JoplinError;

View File

@ -1,5 +1,4 @@
class ModelCache {
constructor(maxSize) {
this.cache_ = [];
this.maxSize_ = maxSize;
@ -8,7 +7,7 @@ class ModelCache {
fromCache(ModelClass, id) {
for (let i = 0; i < this.cache_.length; i++) {
const c = this.cache_[i];
if (c.id === id && c.modelType === ModelClass.modelType()) return c
if (c.id === id && c.modelType === ModelClass.modelType()) return c;
}
return null;
}
@ -33,7 +32,6 @@ class ModelCache {
this.cache(ModelClass, id, output);
return output;
}
}
module.exports = ModelCache;
module.exports = ModelCache;

View File

@ -26,11 +26,11 @@ ObjectUtils.sortByValue = function(object) {
}
return output;
}
};
ObjectUtils.fieldsEqual = function(o1, o2) {
if ((!o1 || !o2) && (o1 !== o2)) return false;
if ((!o1 || !o2) && o1 !== o2) return false;
for (let k in o1) {
if (!o1.hasOwnProperty(k)) continue;
if (o1[k] !== o2[k]) return false;
@ -42,20 +42,22 @@ ObjectUtils.fieldsEqual = function(o1, o2) {
if (c1.length !== c2.length) return false;
return true;
}
};
ObjectUtils.convertValuesToFunctions = function(o) {
const output = {};
for (let n in o) {
if (!o.hasOwnProperty(n)) continue;
output[n] = () => { return typeof o[n] === 'function' ? o[n]() : o[n]; }
output[n] = () => {
return typeof o[n] === 'function' ? o[n]() : o[n];
};
}
return output;
}
};
ObjectUtils.isEmpty = function(o) {
if (!o) return true;
return Object.keys(o).length === 0 && o.constructor === Object;
}
};
module.exports = ObjectUtils;
module.exports = ObjectUtils;

View File

@ -8,7 +8,6 @@ const { Synchronizer } = require('lib/synchronizer.js');
const { FileApiDriverDropbox } = require('lib/file-api-driver-dropbox.js');
class SyncTargetDropbox extends BaseSyncTarget {
static id() {
return 7;
}
@ -32,7 +31,10 @@ class SyncTargetDropbox extends BaseSyncTarget {
async isAuthenticated() {
const f = await this.fileApi();
return !!f.driver().api().authToken();
return !!f
.driver()
.api()
.authToken();
}
async api() {
@ -48,7 +50,7 @@ class SyncTargetDropbox extends BaseSyncTarget {
secret: params.secret,
});
api.on('authRefreshed', (auth) => {
api.on('authRefreshed', auth => {
this.logger().info('Saving updated Dropbox auth.');
Setting.setValue('sync.' + SyncTargetDropbox.id() + '.auth', auth ? auth : null);
});
@ -67,7 +69,6 @@ class SyncTargetDropbox extends BaseSyncTarget {
if (!(await this.isAuthenticated())) throw new Error('User is not authentified');
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetDropbox;

View File

@ -6,7 +6,6 @@ const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const { Synchronizer } = require('lib/synchronizer.js');
class SyncTargetFilesystem extends BaseSyncTarget {
static id() {
return 2;
}
@ -36,7 +35,6 @@ class SyncTargetFilesystem extends BaseSyncTarget {
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetFilesystem;
module.exports = SyncTargetFilesystem;

View File

@ -6,7 +6,6 @@ const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
const { Synchronizer } = require('lib/synchronizer.js');
class SyncTargetMemory extends BaseSyncTarget {
static id() {
return 1;
}
@ -33,7 +32,6 @@ class SyncTargetMemory extends BaseSyncTarget {
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetMemory;
module.exports = SyncTargetMemory;

View File

@ -11,7 +11,6 @@ const SyncTargetWebDAV = require('lib/SyncTargetWebDAV');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
class SyncTargetNextcloud extends BaseSyncTarget {
static id() {
return 5;
}
@ -51,7 +50,6 @@ class SyncTargetNextcloud extends BaseSyncTarget {
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetNextcloud;
module.exports = SyncTargetNextcloud;

View File

@ -8,7 +8,6 @@ const { Synchronizer } = require('lib/synchronizer.js');
const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
class SyncTargetOneDrive extends BaseSyncTarget {
static id() {
return 3;
}
@ -50,7 +49,7 @@ class SyncTargetOneDrive extends BaseSyncTarget {
this.api_ = new OneDriveApi(this.oneDriveParameters().id, this.oneDriveParameters().secret, isPublic);
this.api_.setLogger(this.logger());
this.api_.on('authRefreshed', (a) => {
this.api_.on('authRefreshed', a => {
this.logger().info('Saving updated OneDrive auth.');
Setting.setValue('sync.' + this.syncTargetId() + '.auth', a ? JSON.stringify(a) : null);
});
@ -67,7 +66,7 @@ class SyncTargetOneDrive extends BaseSyncTarget {
this.api_.setAuth(auth);
}
return this.api_;
}
@ -80,10 +79,9 @@ class SyncTargetOneDrive extends BaseSyncTarget {
}
async initSynchronizer() {
if (!await this.isAuthenticated()) throw new Error('User is not authentified');
if (!(await this.isAuthenticated())) throw new Error('User is not authentified');
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetOneDrive;
module.exports = SyncTargetOneDrive;

View File

@ -9,7 +9,6 @@ const { Synchronizer } = require('lib/synchronizer.js');
const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
class SyncTargetOneDriveDev extends SyncTargetOneDrive {
static id() {
return 4;
}
@ -29,9 +28,8 @@ class SyncTargetOneDriveDev extends SyncTargetOneDrive {
oneDriveParameters() {
return parameters('dev').oneDrive;
}
}
const staticSelf = SyncTargetOneDriveDev;
module.exports = SyncTargetOneDriveDev;
module.exports = SyncTargetOneDriveDev;

View File

@ -1,5 +1,4 @@
class SyncTargetRegistry {
static classById(syncTargetId) {
const info = SyncTargetRegistry.reg_[syncTargetId];
if (!info) throw new Error('Invalid id: ' + syncTargetId);
@ -44,9 +43,8 @@ class SyncTargetRegistry {
}
return output;
}
}
SyncTargetRegistry.reg_ = {};
module.exports = SyncTargetRegistry;
module.exports = SyncTargetRegistry;

View File

@ -7,7 +7,6 @@ const WebDavApi = require('lib/WebDavApi');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
class SyncTargetWebDAV extends BaseSyncTarget {
static id() {
return 6;
}
@ -45,12 +44,12 @@ class SyncTargetWebDAV extends BaseSyncTarget {
static async checkConfig(options) {
const fileApi = await SyncTargetWebDAV.newFileApi_(SyncTargetWebDAV.id(), options);
fileApi.requestRepeatCount_ = 0;
const output = {
ok: false,
errorMessage: '',
};
try {
const result = await fileApi.stat('');
if (!result) throw new Error('WebDAV directory not found: ' + options.path());
@ -78,7 +77,6 @@ class SyncTargetWebDAV extends BaseSyncTarget {
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetWebDAV;
module.exports = SyncTargetWebDAV;

View File

@ -4,7 +4,6 @@ const Setting = require('lib/models/Setting');
const { Logger } = require('lib/logger.js');
class TaskQueue {
constructor(name) {
this.waitingTasks_ = [];
this.processingTasks_ = {};
@ -47,7 +46,7 @@ class TaskQueue {
this.results_[task.id] = r;
this.processQueue_();
}
};
while (this.waitingTasks_.length > 0 && Object.keys(this.processingTasks_).length < this.concurrency()) {
if (this.stopping_) break;
@ -55,19 +54,22 @@ class TaskQueue {
const task = this.waitingTasks_.splice(0, 1)[0];
this.processingTasks_[task.id] = task;
task.callback().then(result => {
completeTask(task, result, null);
}).catch(error => {
if (!error) error = new Error('Unknown error');
completeTask(task, null, error);
});
task
.callback()
.then(result => {
completeTask(task, result, null);
})
.catch(error => {
if (!error) error = new Error('Unknown error');
completeTask(task, null, error);
});
}
this.processingQueue_ = false;
}
isWaiting(taskId) {
return this.waitingTasks_.find(task => task.id === taskId)
return this.waitingTasks_.find(task => task.id === taskId);
}
isProcessing(taskId) {
@ -118,9 +120,8 @@ class TaskQueue {
isStopping() {
return this.stopping_;
}
}
TaskQueue.CONCURRENCY = 5;
module.exports = TaskQueue;
module.exports = TaskQueue;

View File

@ -11,49 +11,53 @@ const view = {
date: time.formatMsToLocal(new Date().getTime(), time.dateFormat()),
time: time.formatMsToLocal(new Date().getTime(), time.timeFormat()),
datetime: time.formatMsToLocal(new Date().getTime()),
custom_datetime: () => { return (text, render) => {
return render(time.formatMsToLocal(new Date().getTime(), text));
}},
}
custom_datetime: () => {
return (text, render) => {
return render(time.formatMsToLocal(new Date().getTime(), text));
};
},
};
// Mustache escapes strings (including /) with the html code by default
// This isn't useful for markdown so it's disabled
Mustache.escape = (text) => { return text; }
Mustache.escape = text => {
return text;
};
TemplateUtils.render = function(input) {
return Mustache.render(input, view);
}
};
TemplateUtils.loadTemplates = async function(filePath) {
let templates = [];
let files = [];
let templates = [];
let files = [];
if (await shim.fsDriver().exists(filePath)) {
try {
files = await shim.fsDriver().readDirStats(filePath);
} catch (error) {
let msg = error.message ? error.message : '';
msg = 'Could not read template names from ' + filePath + '\n' + msg;
error.message = msg;
throw error;
}
files.forEach(async (file) => {
if (file.path.endsWith('.md')) {
try {
let fileString = await shim.fsDriver().readFile(filePath + '/' + file.path, 'utf-8');
templates.push({label: file.path, value: fileString});
} catch (error) {
let msg = error.message ? error.message : '';
msg = 'Could not load template ' + file.path + '\n' + msg;
error.message = msg;
throw error;
}
}
});
if (await shim.fsDriver().exists(filePath)) {
try {
files = await shim.fsDriver().readDirStats(filePath);
} catch (error) {
let msg = error.message ? error.message : '';
msg = 'Could not read template names from ' + filePath + '\n' + msg;
error.message = msg;
throw error;
}
return templates;
files.forEach(async file => {
if (file.path.endsWith('.md')) {
try {
let fileString = await shim.fsDriver().readFile(filePath + '/' + file.path, 'utf-8');
templates.push({ label: file.path, value: fileString });
} catch (error) {
let msg = error.message ? error.message : '';
msg = 'Could not load template ' + file.path + '\n' + msg;
error.message = msg;
throw error;
}
}
});
}
return templates;
};
module.exports = TemplateUtils;

View File

@ -13,7 +13,6 @@ const base64 = require('base-64');
// In general, we should only deal with things in "d:", which is the standard DAV namespace.
class WebDavApi {
constructor(options) {
this.logger_ = new Logger();
this.options_ = options;
@ -52,7 +51,7 @@ class WebDavApi {
async xmlToJson(xml) {
let davNamespaces = []; // Yes, there can be more than one... xmlns:a="DAV:" xmlns:D="DAV:"
const nameProcessor = (name) => {
const nameProcessor = name => {
if (name.indexOf('xmlns:') !== 0) {
// Check if the current name is within the DAV namespace. If it is, normalise it
// by moving it to the "d:" namespace, which is what all the functions are using.
@ -72,13 +71,13 @@ class WebDavApi {
const p = name.split(':');
davNamespaces.push(p[p.length - 1]);
}
}
};
const options = {
tagNameProcessors: [nameProcessor],
attrNameProcessors: [nameProcessor],
attrValueProcessors: [attrValueProcessor]
}
attrValueProcessors: [attrValueProcessor],
};
return new Promise((resolve, reject) => {
parseXmlString(xml, options, (error, result) => {
@ -91,7 +90,7 @@ class WebDavApi {
});
}
valueFromJson(json, keys, type) {
valueFromJson(json, keys, type) {
let output = json;
for (let i = 0; i < keys.length; i++) {
@ -192,14 +191,17 @@ class WebDavApi {
// <propname/>
// </propfind>`;
const body = `<?xml version="1.0" encoding="UTF-8"?>
const body =
`<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:">
<d:prop xmlns:oc="http://owncloud.org/ns">
` + fieldsXml + `
` +
fieldsXml +
`
</d:prop>
</d:propfind>`;
return this.exec('PROPFIND', path, body, { 'Depth': depth }, options);
return this.exec('PROPFIND', path, body, { Depth: depth }, options);
}
requestToCurl_(url, options) {
@ -216,7 +218,7 @@ class WebDavApi {
if (options.body) output.push('--data ' + "'" + options.body + "'");
output.push(url);
return output.join(' ');
return output.join(' ');
}
handleNginxHack_(jsonResponse, newErrorHandler) {
@ -333,7 +335,8 @@ class WebDavApi {
} else if (options.target == 'string') {
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = shim.stringByteLength(body) + '';
response = await shim.fetch(url, fetchOptions);
} else { // file
} else {
// file
response = await shim.fetchBlob(url, fetchOptions);
}
@ -347,7 +350,7 @@ class WebDavApi {
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
const shortResponseText = (responseText + '').substr(0, 1024);
return new JoplinError(method + ' ' + path + ': ' + message + ' (' + code + '): ' + shortResponseText, code);
}
};
let responseJson_ = null;
const loadResponseJson = async () => {
@ -356,7 +359,7 @@ class WebDavApi {
responseJson_ = await this.xmlToJson(responseText);
if (!responseJson_) throw newError('Cannot parse XML response', response.status);
return responseJson_;
}
};
if (!response.ok) {
// When using fetchBlob we only get a string (not xml or json) back
@ -371,13 +374,13 @@ class WebDavApi {
if (json && json['d:error']) {
const code = json['d:error']['s:exception'] ? json['d:error']['s:exception'].join(' ') : response.status;
const message = json['d:error']['s:message'] ? json['d:error']['s:message'].join("\n") : 'Unknown error 1';
const message = json['d:error']['s:message'] ? json['d:error']['s:message'].join('\n') : 'Unknown error 1';
throw newError(message + ' (Exception ' + code + ')', response.status);
}
throw newError('Unknown error 2', response.status);
}
if (options.responseFormat === 'text') return responseText;
// The following methods may have a response depending on the server but it's not
@ -394,7 +397,6 @@ class WebDavApi {
return output;
}
}
module.exports = WebDavApi;
module.exports = WebDavApi;

View File

@ -6,11 +6,10 @@ const Tag = require('lib/models/Tag');
const Resource = require('lib/models/Resource');
const { shim } = require('lib/shim');
const { uuid } = require('lib/uuid');
const { fileExtension, basename} = require('lib/path-utils');
const { fileExtension, basename } = require('lib/path-utils');
const { pregQuote } = require('lib/string-utils');
class WelcomeUtils {
static async createWelcomeItems() {
const output = {
defaultFolderId: null,
@ -21,13 +20,13 @@ class WelcomeUtils {
for (let i = 0; i < folderAssets.length; i++) {
const folderAsset = folderAssets[i];
const folder = await Folder.save({ title: folderAsset.title + ' (' + Setting.appTypeToLabel(Setting.value('appType')) + ')'});
const folder = await Folder.save({ title: folderAsset.title + ' (' + Setting.appTypeToLabel(Setting.value('appType')) + ')' });
if (!output.defaultFolderId) output.defaultFolderId = folder.id;
}
const noteAssets = welcomeAssets.notes;
for (let i = noteAssets.length - 1; i >= 0; i--) {
for (let i = noteAssets.length - 1; i >= 0; i--) {
const noteAsset = noteAssets[i];
let noteBody = noteAsset.body;
@ -44,7 +43,7 @@ class WelcomeUtils {
await shim.fsDriver().remove(tempFilePath);
const regex = new RegExp(pregQuote('(' + resourceUrl + ')'), 'g');
noteBody = noteBody.replace(regex, '(:/' + resource.id + ')');
noteBody = noteBody.replace(regex, '(:/' + resource.id + ')');
}
const note = await Note.save({
@ -77,7 +76,6 @@ class WelcomeUtils {
Setting.setValue('activeFolderId', result.defaultFolderId);
}
}
}
module.exports = WelcomeUtils;
module.exports = WelcomeUtils;

View File

@ -1,9 +1,8 @@
const React = require('react');
const { TouchableOpacity, TouchableWithoutFeedback , Dimensions, Text, Modal, View } = require('react-native');
const { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View } = require('react-native');
const { ItemList } = require('lib/components/ItemList.js');
class Dropdown extends React.Component {
constructor() {
super();
@ -21,7 +20,7 @@ class Dropdown extends React.Component {
// https://stackoverflow.com/questions/30096038/react-native-getting-the-position-of-an-element
this.headerRef_.measure((fx, fy, width, height, px, py) => {
this.setState({
headerSize: { x: px, y: py, width: width, height: height }
headerSize: { x: px, y: py, width: width, height: height },
});
});
}
@ -51,7 +50,7 @@ class Dropdown extends React.Component {
});
const itemWrapperStyle = Object.assign({}, this.props.itemWrapperStyle ? this.props.itemWrapperStyle : {}, {
flex:1,
flex: 1,
justifyContent: 'center',
height: itemHeight,
paddingLeft: 20,
@ -74,9 +73,7 @@ class Dropdown extends React.Component {
marginRight: 10,
});
const itemStyle = Object.assign({}, this.props.itemStyle ? this.props.itemStyle : {}, {
});
const itemStyle = Object.assign({}, this.props.itemStyle ? this.props.itemStyle : {}, {});
let headerLabel = '...';
for (let i = 0; i < items.length; i++) {
@ -91,34 +88,61 @@ class Dropdown extends React.Component {
const closeList = () => {
this.setState({ listVisible: false });
}
};
const itemRenderer= (item) => {
const itemRenderer = item => {
return (
<TouchableOpacity style={itemWrapperStyle} key={item.value} onPress={() => { closeList(); if (this.props.onValueChange) this.props.onValueChange(item.value); }}>
<Text ellipsizeMode="tail" numberOfLines={1} style={itemStyle} key={item.value}>{item.label}</Text>
<TouchableOpacity
style={itemWrapperStyle}
key={item.value}
onPress={() => {
closeList();
if (this.props.onValueChange) this.props.onValueChange(item.value);
}}
>
<Text ellipsizeMode="tail" numberOfLines={1} style={itemStyle} key={item.value}>
{item.label}
</Text>
</TouchableOpacity>
);
}
};
return (
<View style={{flex: 1, flexDirection: 'column' }}>
<TouchableOpacity style={headerWrapperStyle} ref={(ref) => this.headerRef_ = ref} onPress={() => {
this.updateHeaderCoordinates();
this.setState({ listVisible: true });
}}>
<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}>{headerLabel}</Text>
<View style={{ flex: 1, flexDirection: 'column' }}>
<TouchableOpacity
style={headerWrapperStyle}
ref={ref => (this.headerRef_ = ref)}
onPress={() => {
this.updateHeaderCoordinates();
this.setState({ listVisible: true });
}}
>
<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}>
{headerLabel}
</Text>
<Text style={headerArrowStyle}>{'▼'}</Text>
</TouchableOpacity>
<Modal transparent={true} visible={this.state.listVisible} onRequestClose={() => { closeList(); }} >
<TouchableWithoutFeedback onPressOut={() => { closeList() }}>
<View style={{flex:1}}>
<Modal
transparent={true}
visible={this.state.listVisible}
onRequestClose={() => {
closeList();
}}
>
<TouchableWithoutFeedback
onPressOut={() => {
closeList();
}}
>
<View style={{ flex: 1 }}>
<View style={wrapperStyle}>
<ItemList
style={itemListStyle}
items={this.props.items}
itemHeight={itemHeight}
itemRenderer={(item) => { return itemRenderer(item) }}
itemRenderer={item => {
return itemRenderer(item);
}}
/>
</View>
</View>
@ -129,4 +153,4 @@ class Dropdown extends React.Component {
}
}
module.exports = { Dropdown };
module.exports = { Dropdown };

View File

@ -2,7 +2,6 @@ const React = require('react');
const { Text, TouchableHighlight, View, StyleSheet, ScrollView } = require('react-native');
class ItemList extends React.Component {
constructor() {
super();
@ -77,8 +76,8 @@ class ItemList extends React.Component {
const items = this.props.items;
const blankItem = function(key, height) {
return <View key={key} style={{height:height}}></View>
}
return <View key={key} style={{ height: height }}></View>;
};
itemComps = [blankItem('top', this.state.topItemIndex * this.props.itemHeight)];
@ -93,11 +92,20 @@ class ItemList extends React.Component {
}
return (
<ScrollView scrollEventThrottle={500} onLayout={(event) => { this.onLayout(event); }} style={style} onScroll={ (event) => { this.onScroll(event) }}>
{ itemComps }
<ScrollView
scrollEventThrottle={500}
onLayout={event => {
this.onLayout(event);
}}
style={style}
onScroll={event => {
this.onScroll(event);
}}
>
{itemComps}
</ScrollView>
);
}
}
module.exports = { ItemList };
module.exports = { ItemList };

View File

@ -4,7 +4,6 @@ const { themeStyle } = require('lib/components/global-style.js');
const { _ } = require('lib/locale');
class ModalDialog extends React.Component {
constructor() {
super();
this.styles_ = {};
@ -23,17 +22,17 @@ class ModalDialog extends React.Component {
justifyContent: 'center',
},
modalContentWrapper: {
flex:1,
flex: 1,
flexDirection: 'column',
backgroundColor: theme.backgroundColor,
borderWidth: 1,
borderColor:theme.dividerColor,
borderColor: theme.dividerColor,
margin: 20,
padding: 10,
borderRadius: 5,
},
modalContentWrapper2: {
flex:1,
flex: 1,
},
title: Object.assign({}, theme.normalText, {
borderBottomWidth: 1,
@ -59,17 +58,15 @@ class ModalDialog extends React.Component {
return (
<View style={this.styles().modalWrapper}>
<Modal transparent={true} visible={true} onRequestClose={() => { }} >
<Modal transparent={true} visible={true} onRequestClose={() => {}}>
<View elevation={10} style={this.styles().modalContentWrapper}>
<Text style={this.styles().title}>{this.props.title}</Text>
<View style={this.styles().modalContentWrapper2}>
{ContentComponent}
</View>
<View style={this.styles().modalContentWrapper2}>{ContentComponent}</View>
<View style={this.styles().buttonRow}>
<View style={{flex:1}}>
<View style={{ flex: 1 }}>
<Button disabled={!buttonBarEnabled} title={_('OK')} onPress={this.props.onOkPress}></Button>
</View>
<View style={{flex:1, marginLeft: 5}}>
<View style={{ flex: 1, marginLeft: 5 }}>
<Button disabled={!buttonBarEnabled} title={_('Cancel')} onPress={this.props.onCancelPress}></Button>
</View>
</View>
@ -80,4 +77,4 @@ class ModalDialog extends React.Component {
}
}
module.exports = ModalDialog;
module.exports = ModalDialog;

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { StyleSheet, Text } = require('react-native');
const Icon = require('react-native-vector-icons/Ionicons').default;
const ReactNativeActionButton = require('react-native-action-button').default;
@ -14,11 +15,10 @@ const styles = StyleSheet.create({
},
itemText: {
// fontSize: 14, // Cannot currently set fontsize since the bow surrounding the label has a fixed size
}
},
});
class ActionButtonComponent extends React.Component {
constructor() {
super();
this.state = {
@ -59,14 +59,18 @@ class ActionButtonComponent extends React.Component {
if (this.props.folders.length) {
buttons.push({
title: _('New to-do'),
onPress: () => { this.newTodo_press() },
onPress: () => {
this.newTodo_press();
},
color: '#9b59b6',
icon: 'md-checkbox-outline',
});
buttons.push({
title: _('New note'),
onPress: () => { this.newNote_press() },
onPress: () => {
this.newNote_press();
},
color: '#9b59b6',
icon: 'md-document',
});
@ -86,41 +90,41 @@ class ActionButtonComponent extends React.Component {
}
if (!buttonComps.length && !this.props.mainButton) {
return <ReactNativeActionButton style={{ display: 'none' }}/>
return <ReactNativeActionButton style={{ display: 'none' }} />;
}
let mainButton = this.props.mainButton ? this.props.mainButton : {};
let mainIcon = mainButton.icon ? <Icon name={mainButton.icon} style={styles.actionButtonIcon} /> : <Icon name="md-add" style={styles.actionButtonIcon} />
let mainIcon = mainButton.icon ? <Icon name={mainButton.icon} style={styles.actionButtonIcon} /> : <Icon name="md-add" style={styles.actionButtonIcon} />;
if (this.props.multiStates) {
if (!this.props.buttons || !this.props.buttons.length) throw new Error('Multi-state button requires at least one state');
if (this.state.buttonIndex < 0 || this.state.buttonIndex >= this.props.buttons.length) throw new Error('Button index out of bounds: ' + this.state.buttonIndex + '/' + this.props.buttons.length);
let button = this.props.buttons[this.state.buttonIndex];
let mainIcon = <Icon name={button.icon} style={styles.actionButtonIcon} />
let mainIcon = <Icon name={button.icon} style={styles.actionButtonIcon} />;
return (
<ReactNativeActionButton
icon={mainIcon}
buttonColor="rgba(231,76,60,1)"
onPress={() => { button.onPress() }}
onPress={() => {
button.onPress();
}}
/>
);
} else {
return (
<ReactNativeActionButton textStyle={styles.itemText} icon={mainIcon} buttonColor="rgba(231,76,60,1)" onPress={ function() { } }>
{ buttonComps }
<ReactNativeActionButton textStyle={styles.itemText} icon={mainIcon} buttonColor="rgba(231,76,60,1)" onPress={function() {}}>
{buttonComps}
</ReactNativeActionButton>
);
}
}
}
const ActionButton = connect(
(state) => {
return {
folders: state.folders,
locale: state.settings.locale,
};
}
)(ActionButtonComponent)
const ActionButton = connect(state => {
return {
folders: state.folders,
locale: state.settings.locale,
};
})(ActionButtonComponent);
module.exports = { ActionButton };
module.exports = { ActionButton };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { connect } = require('react-redux');
const { NotesScreen } = require('lib/components/screens/notes.js');
const { SearchScreen } = require('lib/components/screens/search.js');
@ -7,13 +8,12 @@ const { _ } = require('lib/locale.js');
const { themeStyle } = require('lib/components/global-style.js');
class AppNavComponent extends Component {
constructor() {
super();
this.previousRouteName_ = null;
this.state = {
autoCompletionBarExtraHeight: 0, // Extra padding for the auto completion bar at the top of the keyboard
}
};
}
UNSAFE_componentWillMount() {
@ -30,12 +30,12 @@ class AppNavComponent extends Component {
this.keyboardDidHideListener = null;
}
keyboardDidShow () {
this.setState({ autoCompletionBarExtraHeight: 30 })
keyboardDidShow() {
this.setState({ autoCompletionBarExtraHeight: 30 });
}
keyboardDidHide () {
this.setState({ autoCompletionBarExtraHeight:0 })
keyboardDidHide() {
this.setState({ autoCompletionBarExtraHeight: 0 });
}
render() {
@ -66,27 +66,24 @@ class AppNavComponent extends Component {
const theme = themeStyle(this.props.theme);
const style = { flex: 1, backgroundColor: theme.backgroundColor }
const style = { flex: 1, backgroundColor: theme.backgroundColor };
return (
<KeyboardAvoidingView behavior={ Platform.OS === 'ios' ? "padding" : null } style={style}>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : null} style={style}>
<NotesScreen visible={notesScreenVisible} navigation={{ state: route }} />
{ searchScreenLoaded && <SearchScreen visible={searchScreenVisible} navigation={{ state: route }} /> }
{ (!notesScreenVisible && !searchScreenVisible) && <Screen navigation={{ state: route }} /> }
{searchScreenLoaded && <SearchScreen visible={searchScreenVisible} navigation={{ state: route }} />}
{!notesScreenVisible && !searchScreenVisible && <Screen navigation={{ state: route }} />}
<View style={{ height: this.state.autoCompletionBarExtraHeight }} />
</KeyboardAvoidingView>
);
}
}
const AppNav = connect(
(state) => {
return {
route: state.route,
theme: state.settings.theme,
};
}
)(AppNavComponent)
const AppNav = connect(state => {
return {
route: state.route,
theme: state.settings.theme,
};
})(AppNavComponent);
module.exports = { AppNav };
module.exports = { AppNav };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { StyleSheet } = require('react-native');
const { globalStyle, themeStyle } = require('lib/components/global-style.js');
@ -14,7 +15,6 @@ const styles_ = StyleSheet.create(styleObject_);
let rootStyles_ = {};
class BaseScreenComponent extends React.Component {
styles() {
return styles_;
}
@ -34,7 +34,6 @@ class BaseScreenComponent extends React.Component {
});
return rootStyles_[themeId];
}
}
module.exports = { BaseScreenComponent };
module.exports = { BaseScreenComponent };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { StyleSheet, View, TouchableHighlight } = require('react-native');
const Icon = require('react-native-vector-icons/Ionicons').default;
@ -11,12 +12,11 @@ const styles = {
};
class Checkbox extends Component {
constructor() {
super();
this.state = {
checked: false,
}
};
}
UNSAFE_componentWillMount() {
@ -55,17 +55,16 @@ class Checkbox extends Component {
alignItems: 'center',
};
if (style && style.display === 'none') return <View/>
if (style && style.display === 'none') return <View />;
//if (style.display) thStyle.display = style.display;
return (
<TouchableHighlight onPress={() => this.onPress()} style={thStyle}>
<Icon name={iconName} style={checkboxIconStyle}/>
<Icon name={iconName} style={checkboxIconStyle} />
</TouchableHighlight>
);
}
}
module.exports = { Checkbox };
module.exports = { Checkbox };

View File

@ -6,25 +6,25 @@ const globalStyle = {
margin: 15, // No text and no interactive component should be within this margin
itemMarginTop: 10,
itemMarginBottom: 10,
backgroundColor: "#ffffff",
color: "#555555", // For regular text
colorError: "red",
colorWarn: "#9A5B00",
colorFaded: "#777777", // For less important text
backgroundColor: '#ffffff',
color: '#555555', // For regular text
colorError: 'red',
colorWarn: '#9A5B00',
colorFaded: '#777777', // For less important text
fontSizeSmaller: 14,
dividerColor: "#dddddd",
strongDividerColor: "#aaaaaa",
dividerColor: '#dddddd',
strongDividerColor: '#aaaaaa',
selectedColor: '#e5e5e5',
headerBackgroundColor: '#F0F0F0',
disabledOpacity: 0.2,
colorUrl: '#7B81FF',
textSelectionColor: "#0096FF",
textSelectionColor: '#0096FF',
raisedBackgroundColor: "#0080EF",
raisedColor: "#003363",
raisedHighlightedColor: "#ffffff",
raisedBackgroundColor: '#0080EF',
raisedColor: '#003363',
raisedHighlightedColor: '#ffffff',
warningBackgroundColor: "#FFD08D",
warningBackgroundColor: '#FFD08D',
// For WebView - must correspond to the properties above
htmlFontSize: '16px',
@ -118,9 +118,9 @@ function themeStyle(theme) {
output.textSelectionColor = '#00AEFF';
output.headerBackgroundColor = '#2D3136';
output.raisedBackgroundColor = "#0F2051";
output.raisedColor = "#788BC3";
output.raisedHighlightedColor = "#ffffff";
output.raisedBackgroundColor = '#0F2051';
output.raisedColor = '#788BC3';
output.raisedHighlightedColor = '#ffffff';
output.htmlColor = 'rgb(220,220,220)';
output.htmlBackgroundColor = 'rgb(29,32,36)';
@ -140,4 +140,4 @@ function themeStyle(theme) {
return addExtraStyles(themeCache_[theme]);
}
module.exports = { globalStyle, themeStyle };
module.exports = { globalStyle, themeStyle };

View File

@ -1,6 +1,7 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { Platform, View } = require('react-native');
const { WebView } = require('react-native-webview');
const { WebView } = require('react-native-webview');
const { themeStyle } = require('lib/components/global-style.js');
const Resource = require('lib/models/Resource.js');
const Setting = require('lib/models/Setting.js');
@ -10,13 +11,12 @@ const MdToHtml = require('lib/renderers/MdToHtml.js');
const shared = require('lib/components/shared/note-screen-shared.js');
class NoteBodyViewer extends Component {
constructor() {
super();
this.state = {
resources: {},
webViewLoaded: false,
}
};
this.isMounted_ = false;
}
@ -47,12 +47,11 @@ class NoteBodyViewer extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
const safeGetNoteProp = (props, propName) => {
if (!props) return null;
if (!props.note) return null;
return props.note[propName];
}
};
// To address https://github.com/laurent22/joplin/issues/433
// If a checkbox in a note is ticked, the body changes, which normally would trigger a re-render
@ -63,7 +62,7 @@ class NoteBodyViewer extends Component {
// will not be displayed immediately.
const currentNoteId = safeGetNoteProp(this.props, 'id');
const nextNoteId = safeGetNoteProp(nextProps, 'id');
if (currentNoteId !== nextNoteId || nextState.webViewLoaded !== this.state.webViewLoaded) return true;
// If the length of the body has changed, then it's something other than a checkbox that has changed,
@ -98,7 +97,7 @@ class NoteBodyViewer extends Component {
},
paddingBottom: '3.8em', // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
highlightedKeywords: this.props.highlightedKeywords,
resources: this.props.noteResources,//await shared.attachedResources(bodyToRender),
resources: this.props.noteResources, //await shared.attachedResources(bodyToRender),
codeTheme: theme.codeThemeCss,
postMessageSyntax: 'window.ReactNativeWebView.postMessage',
};
@ -120,19 +119,22 @@ class NoteBodyViewer extends Component {
}, 10);
`);
html = `
html =
`
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
` + html + `
` +
html +
`
</body>
</html>
`;
let webViewStyle = {'backgroundColor': this.props.webViewStyle.backgroundColor}
let webViewStyle = { backgroundColor: this.props.webViewStyle.backgroundColor };
// On iOS, the onLoadEnd() event is never fired so always
// display the webview (don't do the little trick
// to avoid the white flash).
@ -183,8 +185,8 @@ class NoteBodyViewer extends Component {
mixedContentMode="always"
allowFileAccess={true}
onLoadEnd={() => this.onLoadEnd()}
onError={() => reg.logger().error('WebView error') }
onMessage={(event) => {
onError={() => reg.logger().error('WebView error')}
onMessage={event => {
// Since RN 58 (or 59) messages are now escaped twice???
let msg = unescape(unescape(event.nativeEvent.data));
@ -196,7 +198,7 @@ class NoteBodyViewer extends Component {
} else if (msg.indexOf('markForDownload:') === 0) {
msg = msg.split(':');
const resourceId = msg[1];
if (this.props.onMarkForDownload) this.props.onMarkForDownload({ resourceId: resourceId });
if (this.props.onMarkForDownload) this.props.onMarkForDownload({ resourceId: resourceId });
} else {
this.props.onJoplinLinkClick(msg);
}
@ -205,7 +207,6 @@ class NoteBodyViewer extends Component {
</View>
);
}
}
module.exports = { NoteBodyViewer };
module.exports = { NoteBodyViewer };

View File

@ -1,6 +1,7 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { connect } = require('react-redux');
const { ListView, Text, TouchableOpacity , View, StyleSheet } = require('react-native');
const { ListView, Text, TouchableOpacity, View, StyleSheet } = require('react-native');
const { _ } = require('lib/locale.js');
const { Checkbox } = require('lib/components/checkbox.js');
const { reg } = require('lib/registry.js');
@ -9,7 +10,6 @@ const { time } = require('lib/time-utils.js');
const { globalStyle, themeStyle } = require('lib/components/global-style.js');
class NoteItemComponent extends Component {
constructor() {
super();
this.styles_ = {};
@ -68,19 +68,19 @@ class NoteItemComponent extends Component {
return this.styles_[this.props.theme];
}
async todoCheckbox_change(checked) {
async todoCheckbox_change(checked) {
if (!this.props.note) return;
const newNote = {
id: this.props.note.id,
todo_completed: checked ? time.unixMs() : 0,
}
};
await Note.save(newNote);
}
onPress() {
if (!this.props.note) return;
if (!!this.props.note.encryption_applied) return;
if (this.props.note.encryption_applied) return;
if (this.props.noteSelectionEnabled) {
this.props.dispatch({
@ -126,21 +126,17 @@ class NoteItemComponent extends Component {
const listItemStyle = isTodo ? this.styles().listItemWithCheckbox : this.styles().listItem;
const listItemTextStyle = isTodo ? this.styles().listItemTextWithCheckbox : this.styles().listItemText;
const opacityStyle = isTodo && checkboxChecked ? {opacity: 0.4} : {};
const opacityStyle = isTodo && checkboxChecked ? { opacity: 0.4 } : {};
const isSelected = this.props.noteSelectionEnabled && this.props.selectedNoteIds.indexOf(note.id) >= 0;
const selectionWrapperStyle = isSelected ? this.styles().selectionWrapperSelected : this.styles().selectionWrapper;
return (
<TouchableOpacity onPress={() => this.onPress()} onLongPress={() => this.onLongPress() } activeOpacity={0.5}>
<View style={ selectionWrapperStyle }>
<View style={ opacityStyle }>
<View style={ listItemStyle }>
<Checkbox
style={checkboxStyle}
checked={checkboxChecked}
onChange={(checked) => this.todoCheckbox_change(checked)}
/>
<TouchableOpacity onPress={() => this.onPress()} onLongPress={() => this.onLongPress()} activeOpacity={0.5}>
<View style={selectionWrapperStyle}>
<View style={opacityStyle}>
<View style={listItemStyle}>
<Checkbox style={checkboxStyle} checked={checkboxChecked} onChange={checked => this.todoCheckbox_change(checked)} />
<Text style={listItemTextStyle}>{Note.displayTitle(note)}</Text>
</View>
</View>
@ -148,17 +144,14 @@ class NoteItemComponent extends Component {
</TouchableOpacity>
);
}
}
const NoteItem = connect(
(state) => {
return {
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedNoteIds: state.selectedNoteIds,
};
}
)(NoteItemComponent)
const NoteItem = connect(state => {
return {
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedNoteIds: state.selectedNoteIds,
};
})(NoteItemComponent);
module.exports = { NoteItem };
module.exports = { NoteItem };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { connect } = require('react-redux');
const { ListView, Text, TouchableHighlight, Switch, View, StyleSheet } = require('react-native');
const { _ } = require('lib/locale.js');
@ -11,11 +12,12 @@ const { time } = require('lib/time-utils.js');
const { themeStyle } = require('lib/components/global-style.js');
class NoteListComponent extends Component {
constructor() {
super();
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => { return r1 !== r2; }
rowHasChanged: (r1, r2) => {
return r1 !== r2;
},
});
this.state = {
dataSource: ds,
@ -91,30 +93,28 @@ class NoteListComponent extends Component {
if (this.state.dataSource.getRowCount()) {
return (
<ListView
ref={(ref) => this.rootRef_ = ref}
ref={ref => (this.rootRef_ = ref)}
dataSource={this.state.dataSource}
renderRow={(note) => {
return <NoteItem note={note}/>
renderRow={note => {
return <NoteItem note={note} />;
}}
enableEmptySections={true}
/>
);
} else {
const noItemMessage = _('There are currently no notes. Create one by clicking on the (+) button.');
return <Text style={this.styles().noItemMessage} >{noItemMessage}</Text>;
return <Text style={this.styles().noItemMessage}>{noItemMessage}</Text>;
}
}
}
const NoteList = connect(
(state) => {
return {
items: state.notes,
notesSource: state.notesSource,
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
};
}
)(NoteListComponent)
const NoteList = connect(state => {
return {
items: state.notes,
notesSource: state.notesSource,
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
};
})(NoteListComponent);
module.exports = { NoteList };
module.exports = { NoteList };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { connect } = require('react-redux');
const { Platform, View, Text, Button, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions } = require('react-native');
const Icon = require('react-native-vector-icons/Ionicons').default;
@ -27,7 +28,6 @@ const DialogBox = require('react-native-dialogbox').default;
const PADDING_V = 10;
class ScreenHeaderComponent extends React.PureComponent {
constructor() {
super();
this.styles_ = {};
@ -52,7 +52,7 @@ class ScreenHeaderComponent extends React.PureComponent {
divider: {
borderBottomWidth: 1,
borderColor: theme.dividerColor,
backgroundColor: "#0000ff"
backgroundColor: '#0000ff',
},
sideMenuButton: {
flex: 1,
@ -132,7 +132,7 @@ class ScreenHeaderComponent extends React.PureComponent {
paddingBottom: 15,
},
warningBox: {
backgroundColor: "#ff9900",
backgroundColor: '#ff9900',
flexDirection: 'row',
padding: theme.marginLeft,
},
@ -160,9 +160,9 @@ class ScreenHeaderComponent extends React.PureComponent {
async backButton_press() {
if (this.props.noteSelectionEnabled) {
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
} else {
} else {
await BackButtonService.back();
}
}
}
searchButton_press() {
@ -181,7 +181,7 @@ class ScreenHeaderComponent extends React.PureComponent {
}
menu_select(value) {
if (typeof(value) == 'function') {
if (typeof value == 'function') {
value();
}
}
@ -199,12 +199,11 @@ class ScreenHeaderComponent extends React.PureComponent {
}
render() {
function sideMenuButton(styles, onPress) {
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.sideMenuButton}>
<Icon name='md-menu' style={styles.topIcon} />
<Icon name="md-menu" style={styles.topIcon} />
</View>
</TouchableOpacity>
);
@ -214,7 +213,7 @@ class ScreenHeaderComponent extends React.PureComponent {
return (
<TouchableOpacity onPress={onPress} disabled={disabled}>
<View style={disabled ? styles.backButtonDisabled : styles.backButton}>
<Icon name='md-arrow-back' style={styles.topIcon} />
<Icon name="md-arrow-back" style={styles.topIcon} />
</View>
</TouchableOpacity>
);
@ -223,13 +222,11 @@ class ScreenHeaderComponent extends React.PureComponent {
function saveButton(styles, onPress, disabled, show) {
if (!show) return null;
const icon = disabled ? <Icon name='md-checkmark' style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
const icon = disabled ? <Icon name="md-checkmark" style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
return (
<TouchableOpacity onPress={onPress} disabled={disabled} style={{ padding:0 }}>
<View style={disabled ? styles.saveButtonDisabled : styles.saveButton}>
{ icon }
</View>
<TouchableOpacity onPress={onPress} disabled={disabled} style={{ padding: 0 }}>
<View style={disabled ? styles.saveButtonDisabled : styles.saveButton}>{icon}</View>
</TouchableOpacity>
);
}
@ -238,7 +235,7 @@ class ScreenHeaderComponent extends React.PureComponent {
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.iconButton}>
<Icon name='md-search' style={styles.topIcon} />
<Icon name="md-search" style={styles.topIcon} />
</View>
</TouchableOpacity>
);
@ -248,7 +245,7 @@ class ScreenHeaderComponent extends React.PureComponent {
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.iconButton}>
<Icon name='md-trash' style={styles.topIcon} />
<Icon name="md-trash" style={styles.topIcon} />
</View>
</TouchableOpacity>
);
@ -258,7 +255,7 @@ class ScreenHeaderComponent extends React.PureComponent {
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.iconButton}>
<Icon name='md-funnel' style={styles.topIcon} />
<Icon name="md-funnel" style={styles.topIcon} />
</View>
</TouchableOpacity>
);
@ -272,23 +269,25 @@ class ScreenHeaderComponent extends React.PureComponent {
let o = this.props.menuOptions[i];
if (o.isDivider) {
menuOptionComponents.push(<View key={'menuOption_' + key++} style={this.styles().divider}/>);
menuOptionComponents.push(<View key={'menuOption_' + key++} style={this.styles().divider} />);
} else {
menuOptionComponents.push(
<MenuOption value={o.onPress} key={'menuOption_' + key++} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{o.title}</Text>
</MenuOption>);
</MenuOption>
);
}
}
if (menuOptionComponents.length) {
menuOptionComponents.push(<View key={'menuOption_' + key++} style={this.styles().divider}/>);
menuOptionComponents.push(<View key={'menuOption_' + key++} style={this.styles().divider} />);
}
} else {
menuOptionComponents.push(
<MenuOption value={() => this.deleteButton_press()} key={'menuOption_delete'} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Delete')}</Text>
</MenuOption>);
</MenuOption>
);
}
const createTitleComponent = () => {
@ -297,7 +296,6 @@ class ScreenHeaderComponent extends React.PureComponent {
const folderPickerOptions = this.props.folderPickerOptions;
if (folderPickerOptions && folderPickerOptions.enabled) {
const addFolderChildren = (folders, pickerItems, indent) => {
folders.sort((a, b) => {
const aTitle = a && a.title ? a.title : '';
@ -312,23 +310,23 @@ class ScreenHeaderComponent extends React.PureComponent {
}
return pickerItems;
}
};
const titlePickerItems = (mustSelect) => {
const titlePickerItems = mustSelect => {
const folders = this.props.folders.filter(f => f.id !== Folder.conflictFolderId());
let output = [];
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
const folderTree = Folder.buildTree(folders);
output = addFolderChildren(folderTree, output, 0);
return output;
}
};
return (
<Dropdown
items={titlePickerItems(!!folderPickerOptions.mustSelect)}
itemHeight={35}
labelTransform="trim"
selectedValue={('selectedFolderId' in folderPickerOptions) ? folderPickerOptions.selectedFolderId : null}
selectedValue={'selectedFolderId' in folderPickerOptions ? folderPickerOptions.selectedFolderId : null}
itemListStyle={{
backgroundColor: theme.backgroundColor,
}}
@ -368,13 +366,13 @@ class ScreenHeaderComponent extends React.PureComponent {
);
} else {
let title = 'title' in this.props && this.props.title !== null ? this.props.title : '';
return <Text style={this.styles().titleText}>{title}</Text>
return <Text style={this.styles().titleText}>{title}</Text>;
}
}
};
const warningComp = this.props.showMissingMasterKeyMessage ? (
<TouchableOpacity style={this.styles().warningBox} onPress={() => this.warningBox_press()} activeOpacity={0.8}>
<Text style={{flex:1}}>{_('Press to set the decryption password.')}</Text>
<Text style={{ flex: 1 }}>{_('Press to set the decryption password.')}</Text>
</TouchableOpacity>
) : null;
@ -384,7 +382,7 @@ class ScreenHeaderComponent extends React.PureComponent {
const showBackButton = !!this.props.noteSelectionEnabled || this.props.showBackButton !== false;
let backButtonDisabled = !this.props.historyCanGoBack;
if (!!this.props.noteSelectionEnabled) backButtonDisabled = false;
if (this.props.noteSelectionEnabled) backButtonDisabled = false;
const titleComp = createTitleComponent();
const sideMenuComp = !showSideMenuButton ? null : sideMenuButton(this.styles(), () => this.sideMenuButton_press());
@ -395,59 +393,66 @@ class ScreenHeaderComponent extends React.PureComponent {
const windowHeight = Dimensions.get('window').height - 50;
const contextMenuStyle = { paddingTop: PADDING_V, paddingBottom: PADDING_V };
// HACK: if this button is removed during selection mode, the header layout is broken, so for now just make it 1 pixel large (normally it should be hidden)
if (!!this.props.noteSelectionEnabled) contextMenuStyle.width = 1;
const menuComp = !menuOptionComponents.length || !showContextMenuButton ? null : (
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}>
<MenuTrigger style={contextMenuStyle}>
<Icon name='md-more' style={this.styles().contextMenuTrigger} />
</MenuTrigger>
<MenuOptions>
<ScrollView style={{ maxHeight: windowHeight }}>
{ menuOptionComponents }
</ScrollView>
</MenuOptions>
</Menu>
);
// HACK: if this button is removed during selection mode, the header layout is broken, so for now just make it 1 pixel large (normally it should be hidden)
if (this.props.noteSelectionEnabled) contextMenuStyle.width = 1;
const menuComp =
!menuOptionComponents.length || !showContextMenuButton ? null : (
<Menu onSelect={value => this.menu_select(value)} style={this.styles().contextMenu}>
<MenuTrigger style={contextMenuStyle}>
<Icon name="md-more" style={this.styles().contextMenuTrigger} />
</MenuTrigger>
<MenuOptions>
<ScrollView style={{ maxHeight: windowHeight }}>{menuOptionComponents}</ScrollView>
</MenuOptions>
</Menu>
);
return (
<View style={this.styles().container} >
<View style={{flexDirection:'row', alignItems: 'center'}}>
{ sideMenuComp }
{ backButtonComp }
{ saveButton(this.styles(), () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) }
{ titleComp }
{ searchButtonComp }
{ deleteButtonComp }
{ sortButtonComp }
{ menuComp }
<View style={this.styles().container}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{sideMenuComp}
{backButtonComp}
{saveButton(
this.styles(),
() => {
if (this.props.onSaveButtonPress) this.props.onSaveButtonPress();
},
this.props.saveButtonDisabled === true,
this.props.showSaveButton === true
)}
{titleComp}
{searchButtonComp}
{deleteButtonComp}
{sortButtonComp}
{menuComp}
</View>
{ warningComp }
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
{warningComp}
<DialogBox
ref={dialogbox => {
this.dialogbox = dialogbox;
}}
/>
</View>
);
}
}
ScreenHeaderComponent.defaultProps = {
menuOptions: [],
};
const ScreenHeader = connect(
(state) => {
return {
historyCanGoBack: state.historyCanGoBack,
locale: state.settings.locale,
folders: state.folders,
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedNoteIds: state.selectedNoteIds,
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
};
}
)(ScreenHeaderComponent)
const ScreenHeader = connect(state => {
return {
historyCanGoBack: state.historyCanGoBack,
locale: state.settings.locale,
folders: state.folders,
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedNoteIds: state.selectedNoteIds,
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
};
})(ScreenHeaderComponent);
module.exports = { ScreenHeader };
module.exports = { ScreenHeader };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { ListView, StyleSheet, View, Text, Button, FlatList, TouchableOpacity, TextInput } = require('react-native');
const Setting = require('lib/models/Setting.js');
const { connect } = require('react-redux');
@ -18,7 +19,6 @@ const ModalDialog = require('lib/components/ModalDialog');
const naturalCompare = require('string-natural-compare');
class NoteTagsDialogComponent extends React.Component {
constructor() {
super();
this.styles_ = {};
@ -30,21 +30,21 @@ class NoteTagsDialogComponent extends React.Component {
savingTags: false,
};
const noteHasTag = (tagId) => {
const noteHasTag = tagId => {
for (let i = 0; i < this.state.tagListData.length; i++) {
if (this.state.tagListData[i].id === tagId) return this.state.tagListData[i].selected;
}
return false;
}
};
const newTagTitles = () => {
return this.state.newTags
.split(',')
.map(t => t.trim().toLowerCase())
.filter(t => !!t);
}
};
this.tag_press = (tagId) => {
this.tag_press = tagId => {
const newData = this.state.tagListData.slice();
for (let i = 0; i < newData.length; i++) {
const t = newData[i];
@ -57,19 +57,20 @@ class NoteTagsDialogComponent extends React.Component {
}
this.setState({ tagListData: newData });
}
};
this.renderTag = (data) => {
this.renderTag = data => {
const tag = data.item;
const iconName = noteHasTag(tag.id) ? 'md-checkbox-outline' : 'md-square-outline';
return (
<TouchableOpacity key={tag.id} onPress={() => this.tag_press(tag.id)} style={this.styles().tag}>
<View style={this.styles().tagIconText}>
<Icon name={iconName} style={this.styles().tagCheckbox}/><Text style={this.styles().tagText}>{tag.title}</Text>
<Icon name={iconName} style={this.styles().tagCheckbox} />
<Text style={this.styles().tagText}>{tag.title}</Text>
</View>
</TouchableOpacity>
);
}
};
this.tagKeyExtractor = (tag, index) => tag.id;
@ -89,11 +90,11 @@ class NoteTagsDialogComponent extends React.Component {
}
if (this.props.onCloseRequested) this.props.onCloseRequested();
}
};
this.cancelButton_press = () => {
if (this.props.onCloseRequested) this.props.onCloseRequested();
}
};
}
UNSAFE_componentWillMount() {
@ -106,11 +107,13 @@ class NoteTagsDialogComponent extends React.Component {
const tags = await Tag.tagsByNoteId(noteId);
const tagIds = tags.map(t => t.id);
const tagListData = this.props.tags.map(tag => { return {
id: tag.id,
title: tag.title,
selected: tagIds.indexOf(tag.id) >= 0,
}});
const tagListData = this.props.tags.map(tag => {
return {
id: tag.id,
title: tag.title,
selected: tagIds.indexOf(tag.id) >= 0,
};
});
tagListData.sort((a, b) => {
return naturalCompare.caseInsensitive(a.title, b.title);
@ -143,12 +146,12 @@ class NoteTagsDialogComponent extends React.Component {
color: theme.color,
},
newTagBox: {
flexDirection:'row',
flexDirection: 'row',
alignItems: 'center',
paddingLeft: theme.marginLeft,
paddingRight: theme.marginRight,
borderBottomWidth: 1,
borderBottomColor: theme.dividerColor
borderBottomColor: theme.dividerColor,
},
newTagBoxLabel: Object.assign({}, theme.normalText, { marginRight: 8 }),
newTagBoxInput: Object.assign({}, theme.lineInput, { flex: 1 }),
@ -157,43 +160,37 @@ class NoteTagsDialogComponent extends React.Component {
this.styles_[themeId] = StyleSheet.create(styles);
return this.styles_[themeId];
}
render() {
const theme = themeStyle(this.props.theme);
const dialogContent = (
<View style={{flex:1}}>
<View style={{ flex: 1 }}>
<View style={this.styles().newTagBox}>
<Text style={this.styles().newTagBoxLabel}>{_('New tags:')}</Text><TextInput selectionColor={theme.textSelectionColor} value={this.state.newTags} onChangeText={value => { this.setState({ newTags: value }) }} style={this.styles().newTagBoxInput}/>
<Text style={this.styles().newTagBoxLabel}>{_('New tags:')}</Text>
<TextInput
selectionColor={theme.textSelectionColor}
value={this.state.newTags}
onChangeText={value => {
this.setState({ newTags: value });
}}
style={this.styles().newTagBoxInput}
/>
</View>
<FlatList
data={this.state.tagListData}
renderItem={this.renderTag}
keyExtractor={this.tagKeyExtractor}
/>
<FlatList data={this.state.tagListData} renderItem={this.renderTag} keyExtractor={this.tagKeyExtractor} />
</View>
);
return <ModalDialog
theme={this.props.theme}
ContentComponent={dialogContent}
title={_('Type new tags or select from list')}
onOkPress={this.okButton_press}
onCancelPress={this.cancelButton_press}
buttonBarEnabled={!this.state.savingTags}
/>
return <ModalDialog theme={this.props.theme} ContentComponent={dialogContent} title={_('Type new tags or select from list')} onOkPress={this.okButton_press} onCancelPress={this.cancelButton_press} buttonBarEnabled={!this.state.savingTags} />;
}
}
const NoteTagsDialog = connect(
(state) => {
return {
theme: state.settings.theme,
tags: state.tags,
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
};
}
)(NoteTagsDialogComponent)
const NoteTagsDialog = connect(state => {
return {
theme: state.settings.theme,
tags: state.tags,
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
};
})(NoteTagsDialogComponent);
module.exports = NoteTagsDialog;
module.exports = NoteTagsDialog;

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { View, Button, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('lib/components/screen-header.js');
@ -10,17 +11,12 @@ const Shared = require('lib/components/shared/dropbox-login-shared');
const { themeStyle } = require('lib/components/global-style.js');
class DropboxLoginScreenComponent extends BaseScreenComponent {
constructor() {
super();
this.styles_ = {};
this.shared_ = new Shared(
this,
(msg) => dialogs.info(this, msg),
(msg) => dialogs.error(this, msg)
);
this.shared_ = new Shared(this, msg => dialogs.info(this, msg), msg => dialogs.error(this, msg));
}
UNSAFE_componentWillMount() {
@ -40,7 +36,7 @@ class DropboxLoginScreenComponent extends BaseScreenComponent {
},
stepText: Object.assign({}, theme.normalText, { marginBottom: theme.margin }),
urlText: Object.assign({}, theme.urlText, { marginBottom: theme.margin }),
}
};
this.styles_[themeId] = StyleSheet.create(styles);
return this.styles_[themeId];
@ -51,8 +47,8 @@ class DropboxLoginScreenComponent extends BaseScreenComponent {
return (
<View style={this.styles().screen}>
<ScreenHeader title={_('Login with Dropbox')}/>
<ScreenHeader title={_('Login with Dropbox')} />
<ScrollView style={this.styles().container}>
<Text style={this.styles().stepText}>{_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')}</Text>
<Text style={this.styles().stepText}>{_('Step 1: Open this URL in your browser to authorise the application:')}</Text>
@ -62,25 +58,28 @@ class DropboxLoginScreenComponent extends BaseScreenComponent {
</TouchableOpacity>
</View>
<Text style={this.styles().stepText}>{_('Step 2: Enter the code provided by Dropbox:')}</Text>
<TextInput placeholder={_('Enter code here')} placeholderTextColor={theme.colorFaded} selectionColor={theme.textSelectionColor} value={this.state.authCode} onChangeText={this.shared_.authCodeInput_change} style={theme.lineInput}/>
<View style={{height:10}}></View>
<Button disabled={this.state.checkingAuthToken} title={_("Submit")} onPress={this.shared_.submit_click}></Button>
<TextInput placeholder={_('Enter code here')} placeholderTextColor={theme.colorFaded} selectionColor={theme.textSelectionColor} value={this.state.authCode} onChangeText={this.shared_.authCodeInput_change} style={theme.lineInput} />
<View style={{ height: 10 }}></View>
<Button disabled={this.state.checkingAuthToken} title={_('Submit')} onPress={this.shared_.submit_click}></Button>
{/* Add this extra padding to make sure the view is scrollable when the keyboard is visible on small screens (iPhone SE) */}
<View style={{ height: 200 }}></View>
</ScrollView>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
<DialogBox
ref={dialogbox => {
this.dialogbox = dialogbox;
}}
/>
</View>
);
}
}
const DropboxLoginScreen = connect((state) => {
const DropboxLoginScreen = connect(state => {
return {
theme: state.settings.theme,
};
})(DropboxLoginScreenComponent)
})(DropboxLoginScreenComponent);
module.exports = { DropboxLoginScreen };
module.exports = { DropboxLoginScreen };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { TextInput, TouchableOpacity, Linking, View, Switch, StyleSheet, Text, Button, ScrollView, Platform } = require('react-native');
const EncryptionService = require('lib/services/EncryptionService');
const { connect } = require('react-redux');
@ -14,7 +15,6 @@ const { dialogs } = require('lib/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
class EncryptionConfigScreenComponent extends BaseScreenComponent {
static navigationOptions(options) {
return { header: null };
}
@ -88,7 +88,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
flex: 1,
padding: theme.margin,
},
}
};
this.styles_[themeId] = StyleSheet.create(styles);
return this.styles_[themeId];
@ -99,28 +99,28 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
const onSaveClick = () => {
return shared.onSavePasswordClick(this, mk);
}
};
const onPasswordChange = (text) => {
const onPasswordChange = text => {
return shared.onPasswordChange(this, mk, text);
}
};
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
const active = this.props.activeMasterKeyId === mk.id ? '✔' : '';
const inputStyle = {flex:1, marginRight: 10, color: theme.color};
const inputStyle = { flex: 1, marginRight: 10, color: theme.color };
inputStyle.borderBottomWidth = 1;
inputStyle.borderBottomColor = theme.strongDividerColor;
return (
<View key={mk.id}>
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0,6))}</Text>
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<View style={{flexDirection: 'row', alignItems: 'center'}}>
<Text style={{flex:0, fontSize: theme.fontSize, marginRight: 10, color: theme.color}}>{_('Password:')}</Text>
<TextInput selectionColor={theme.textSelectionColor} secureTextEntry={true} value={password} onChangeText={(text) => onPasswordChange(text)} style={inputStyle}></TextInput>
<Text style={{fontSize: theme.fontSize, marginRight: 10, color: theme.color}}>{passwordOk}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text>
<TextInput selectionColor={theme.textSelectionColor} secureTextEntry={true} value={password} onChangeText={text => onPasswordChange(text)} style={inputStyle}></TextInput>
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
</View>
</View>
@ -139,18 +139,36 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
} catch (error) {
await dialogs.error(this, error.message);
}
}
};
return (
<View style={{flex:1, borderColor: theme.dividerColor, borderWidth: 1, padding: 10, marginTop: 10, marginBottom: 10}}>
<Text style={{fontSize: theme.fontSize, color: theme.color}}>{_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')}</Text>
<TextInput selectionColor={theme.textSelectionColor} style={{margin: 10, color: theme.color, borderWidth: 1, borderColor: theme.dividerColor }} secureTextEntry={true} value={this.state.passwordPromptAnswer} onChangeText={(text) => { this.setState({ passwordPromptAnswer: text }) }}></TextInput>
<View style={{flexDirection: 'row'}}>
<View style={{flex:1 , marginRight:10}} >
<Button title={_('Enable')} onPress={() => { onEnableClick() }}></Button>
<View style={{ flex: 1, borderColor: theme.dividerColor, borderWidth: 1, padding: 10, marginTop: 10, marginBottom: 10 }}>
<Text style={{ fontSize: theme.fontSize, color: theme.color }}>{_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')}</Text>
<TextInput
selectionColor={theme.textSelectionColor}
style={{ margin: 10, color: theme.color, borderWidth: 1, borderColor: theme.dividerColor }}
secureTextEntry={true}
value={this.state.passwordPromptAnswer}
onChangeText={text => {
this.setState({ passwordPromptAnswer: text });
}}
></TextInput>
<View style={{ flexDirection: 'row' }}>
<View style={{ flex: 1, marginRight: 10 }}>
<Button
title={_('Enable')}
onPress={() => {
onEnableClick();
}}
></Button>
</View>
<View style={{flex:1}} >
<Button title={_('Cancel')} onPress={() => { this.setState({ passwordPromptShow: false}) }}></Button>
<View style={{ flex: 1 }}>
<Button
title={_('Cancel')}
onPress={() => {
this.setState({ passwordPromptShow: false });
}}
></Button>
</View>
</View>
</View>
@ -168,7 +186,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(i+1, mk));
mkComps.push(this.renderMasterKey(i + 1, mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
@ -199,30 +217,45 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(<Text style={this.styles().normalText} key={id}>{id}</Text>);
rows.push(
<Text style={this.styles().normalText} key={id}>
{id}
</Text>
);
}
nonExistingMasterKeySection = (
<View>
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
<Text style={this.styles().normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
<View style={{marginTop: 10}}>{rows}</View>
<View style={{ marginTop: 10 }}>{rows}</View>
</View>
);
}
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
const toggleButton = !this.state.passwordPromptShow ? <View style={{marginTop: 10}}><Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button></View> : null;
const toggleButton = !this.state.passwordPromptShow ? (
<View style={{ marginTop: 10 }}>
<Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
</View>
) : null;
return (
<View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader title={_('Encryption Config')}/>
<ScreenHeader title={_('Encryption Config')} />
<ScrollView style={this.styles().container}>
{<View style={{backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
<Text>{_('For more information about End-To-End Encryption (E2EE) and advices on how to enable it please check the documentation:')}</Text>
<TouchableOpacity onPress={() => { Linking.openURL('https://joplinapp.org/e2ee/') }}><Text>https://joplinapp.org/e2ee/</Text></TouchableOpacity>
</View>}
{
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
<Text>{_('For more information about End-To-End Encryption (E2EE) and advices on how to enable it please check the documentation:')}</Text>
<TouchableOpacity
onPress={() => {
Linking.openURL('https://joplinapp.org/e2ee/');
}}
>
<Text>https://joplinapp.org/e2ee/</Text>
</TouchableOpacity>
</View>
}
<Text style={this.styles().titleText}>{_('Status')}</Text>
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
@ -231,26 +264,27 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
{passwordPromptComp}
{mkComps}
{nonExistingMasterKeySection}
<View style={{flex:1, height: 20}}></View>
<View style={{ flex: 1, height: 20 }}></View>
</ScrollView>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
<DialogBox
ref={dialogbox => {
this.dialogbox = dialogbox;
}}
/>
</View>
);
}
}
const EncryptionConfigScreen = connect(
(state) => {
return {
theme: state.settings.theme,
masterKeys: state.masterKeys,
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
notLoadedMasterKeys: state.notLoadedMasterKeys,
};
}
)(EncryptionConfigScreenComponent)
const EncryptionConfigScreen = connect(state => {
return {
theme: state.settings.theme,
masterKeys: state.masterKeys,
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
notLoadedMasterKeys: state.notLoadedMasterKeys,
};
})(EncryptionConfigScreenComponent);
module.exports = { EncryptionConfigScreen };
module.exports = { EncryptionConfigScreen };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { View, Button, TextInput, StyleSheet } = require('react-native');
const { connect } = require('react-redux');
const { ActionButton } = require('lib/components/action-button.js');
@ -12,7 +13,6 @@ const { themeStyle } = require('lib/components/global-style.js');
const { _ } = require('lib/locale.js');
class FolderScreenComponent extends BaseScreenComponent {
static navigationOptions(options) {
return { header: null };
}
@ -52,7 +52,7 @@ class FolderScreenComponent extends BaseScreenComponent {
lastSavedFolder: Object.assign({}, folder),
});
} else {
Folder.load(this.props.folderId).then((folder) => {
Folder.load(this.props.folderId).then(folder => {
this.setState({
folder: folder,
lastSavedFolder: Object.assign({}, folder),
@ -72,7 +72,7 @@ class FolderScreenComponent extends BaseScreenComponent {
this.setState((prevState, props) => {
let folder = Object.assign({}, prevState.folder);
folder[propName] = propValue;
return { folder: folder }
return { folder: folder };
});
}
@ -108,29 +108,23 @@ class FolderScreenComponent extends BaseScreenComponent {
return (
<View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader
title={_('Edit notebook')}
showSaveButton={true}
saveButtonDisabled={saveButtonDisabled}
onSaveButtonPress={() => this.saveFolderButton_press()}
showSideMenuButton={false}
showSearchButton={false}
<ScreenHeader title={_('Edit notebook')} showSaveButton={true} saveButtonDisabled={saveButtonDisabled} onSaveButtonPress={() => this.saveFolderButton_press()} showSideMenuButton={false} showSearchButton={false} />
<TextInput placeholder={_('Enter notebook title')} underlineColorAndroid={theme.strongDividerColor} selectionColor={theme.textSelectionColor} style={this.styles().textInput} autoFocus={true} value={this.state.folder.title} onChangeText={text => this.title_changeText(text)} />
<dialogs.DialogBox
ref={dialogbox => {
this.dialogbox = dialogbox;
}}
/>
<TextInput placeholder={_('Enter notebook title')} underlineColorAndroid={theme.strongDividerColor} selectionColor={theme.textSelectionColor} style={this.styles().textInput} autoFocus={true} value={this.state.folder.title} onChangeText={(text) => this.title_changeText(text)} />
<dialogs.DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
</View>
);
}
}
const FolderScreen = connect(
(state) => {
return {
folderId: state.selectedFolderId,
theme: state.settings.theme,
};
}
)(FolderScreenComponent)
const FolderScreen = connect(state => {
return {
folderId: state.selectedFolderId,
theme: state.settings.theme,
};
})(FolderScreenComponent);
module.exports = { FolderScreen };
module.exports = { FolderScreen };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { ListView, View, Text, Button, StyleSheet, Platform } = require('react-native');
const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
@ -10,7 +11,6 @@ const { BaseScreenComponent } = require('lib/components/base-screen.js');
const { _ } = require('lib/locale.js');
class LogScreenComponent extends BaseScreenComponent {
static navigationOptions(options) {
return { header: null };
}
@ -18,7 +18,9 @@ class LogScreenComponent extends BaseScreenComponent {
constructor() {
super();
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => { return r1 !== r2; }
rowHasChanged: (r1, r2) => {
return r1 !== r2;
},
});
this.state = {
dataSource: ds,
@ -38,16 +40,17 @@ class LogScreenComponent extends BaseScreenComponent {
flexDirection: 'row',
paddingLeft: 1,
paddingRight: 1,
paddingTop:0,
paddingBottom:0,
paddingTop: 0,
paddingBottom: 0,
},
rowText: {
fontSize: 10,
color: theme.color,
color: theme.color,
},
};
if (Platform.OS !== 'ios') { // Crashes on iOS with error "Unrecognized font family 'monospace'"
if (Platform.OS !== 'ios') {
// Crashes on iOS with error "Unrecognized font family 'monospace'"
styles.rowText.fontFamily = 'monospace';
}
@ -69,12 +72,15 @@ class LogScreenComponent extends BaseScreenComponent {
if (showErrorsOnly === null) showErrorsOnly = this.state.showErrorsOnly;
let levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
if (showErrorsOnly) levels = [Logger.LEVEL_WARN, Logger.LEVEL_ERROR]
if (showErrorsOnly) levels = [Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
reg.logger().lastEntries(1000, { levels: levels }).then((entries) => {
const newDataSource = this.state.dataSource.cloneWithRows(entries);
this.setState({ dataSource: newDataSource });
});
reg
.logger()
.lastEntries(1000, { levels: levels })
.then(entries => {
const newDataSource = this.state.dataSource.cloneWithRows(entries);
this.setState({ dataSource: newDataSource });
});
}
toggleErrorsOnly() {
@ -84,47 +90,50 @@ class LogScreenComponent extends BaseScreenComponent {
}
render() {
let renderRow = (item) => {
let renderRow = item => {
let textStyle = this.styles().rowText;
if (item.level == Logger.LEVEL_WARN) textStyle = this.styles().rowTextWarn;
if (item.level == Logger.LEVEL_ERROR) textStyle = this.styles().rowTextError;
return (
<View style={this.styles().row}>
<Text style={textStyle}>{time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss') + ': ' + item.message}</Text>
</View>
);
}
};
// `enableEmptySections` is to fix this warning: https://github.com/FaridSafi/react-native-gifted-listview/issues/39
return (
<View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader title={_('Log')}/>
<ListView
dataSource={this.state.dataSource}
renderRow={renderRow}
enableEmptySections={true}
/>
<View style={{flexDirection: 'row'}}>
<View style={{flex:1, marginRight: 5 }}>
<Button title={_("Refresh")} onPress={() => { this.resfreshLogEntries(); }}/>
<ScreenHeader title={_('Log')} />
<ListView dataSource={this.state.dataSource} renderRow={renderRow} enableEmptySections={true} />
<View style={{ flexDirection: 'row' }}>
<View style={{ flex: 1, marginRight: 5 }}>
<Button
title={_('Refresh')}
onPress={() => {
this.resfreshLogEntries();
}}
/>
</View>
<View style={{flex:1}}>
<Button title={this.state.showErrorsOnly ? _("Show all") : _("Errors only")} onPress={() => { this.toggleErrorsOnly(); }}/>
<View style={{ flex: 1 }}>
<Button
title={this.state.showErrorsOnly ? _('Show all') : _('Errors only')}
onPress={() => {
this.toggleErrorsOnly();
}}
/>
</View>
</View>
</View>
);
}
}
const LogScreen = connect(
(state) => {
return {
theme: state.settings.theme,
};
}
)(LogScreenComponent)
const LogScreen = connect(state => {
return {
theme: state.settings.theme,
};
})(LogScreenComponent);
module.exports = { LogScreen };
module.exports = { LogScreen };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { AppState, View, Button, Text, StyleSheet } = require('react-native');
const { stateUtils } = require('lib/reducer.js');
const { connect } = require('react-redux');
@ -18,7 +19,6 @@ const DialogBox = require('react-native-dialogbox').default;
const { BaseScreenComponent } = require('lib/components/base-screen.js');
class NotesScreenComponent extends BaseScreenComponent {
static navigationOptions(options) {
return { header: null };
}
@ -31,16 +31,16 @@ class NotesScreenComponent extends BaseScreenComponent {
let newProps = Object.assign({}, this.props);
newProps.notesSource = '';
await this.refreshNotes(newProps);
}
};
this.sortButton_press = async () => {
const buttons = [];
const sortNoteOptions = Setting.enumOptions('notes.sortOrder.field');
const makeCheckboxText = function(selected, sign, label) {
const s = sign === 'tick' ? '✓' : '⬤'
return (selected ? (s + ' ') : '') + label;
}
const s = sign === 'tick' ? '✓' : '⬤';
return (selected ? s + ' ' : '') + label;
};
for (let field in sortNoteOptions) {
if (!sortNoteOptions.hasOwnProperty(field)) continue;
@ -69,7 +69,7 @@ class NotesScreenComponent extends BaseScreenComponent {
if (!r) return;
Setting.setValue(r.name, r.value);
}
};
}
styles() {
@ -100,15 +100,11 @@ class NotesScreenComponent extends BaseScreenComponent {
AppState.removeEventListener('change', this.onAppStateChange_);
}
async componentDidUpdate(prevProps) {
if (prevProps.notesOrder !== this.props.notesOrder ||
prevProps.selectedFolderId != this.props.selectedFolderId ||
prevProps.selectedTagId != this.props.selectedTagId ||
prevProps.selectedSmartFilterId != this.props.selectedSmartFilterId ||
prevProps.notesParentType != this.props.notesParentType) {
await this.refreshNotes(this.props);
}
async componentDidUpdate(prevProps) {
if (prevProps.notesOrder !== this.props.notesOrder || prevProps.selectedFolderId != this.props.selectedFolderId || prevProps.selectedTagId != this.props.selectedTagId || prevProps.selectedSmartFilterId != this.props.selectedSmartFilterId || prevProps.notesParentType != this.props.notesParentType) {
await this.refreshNotes(this.props);
}
}
async refreshNotes(props = null) {
if (props === null) props = this.props;
@ -147,18 +143,20 @@ class NotesScreenComponent extends BaseScreenComponent {
}
deleteFolder_onPress(folderId) {
dialogs.confirm(this, _('Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.')).then((ok) => {
dialogs.confirm(this, _('Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.')).then(ok => {
if (!ok) return;
Folder.delete(folderId).then(() => {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'Notes',
smartFilterId: 'c3176726992c11e9ac940492261af972',
Folder.delete(folderId)
.then(() => {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'Notes',
smartFilterId: 'c3176726992c11e9ac940492261af972',
});
})
.catch(error => {
alert(error.message);
});
}).catch((error) => {
alert(error.message);
});
});
}
@ -206,7 +204,7 @@ class NotesScreenComponent extends BaseScreenComponent {
let rootStyle = {
flex: 1,
backgroundColor: theme.backgroundColor,
}
};
if (!this.props.visible) {
rootStyle.flex = 0.001; // This is a bit of a hack but it seems to work fine - it makes the component invisible but without unmounting it
@ -217,52 +215,46 @@ class NotesScreenComponent extends BaseScreenComponent {
<View style={rootStyle}>
<ScreenHeader title={title} showSideMenuButton={true} showBackButton={false} />
</View>
)
);
}
let title = parent ? parent.title : null;
const addFolderNoteButtons = this.props.selectedFolderId && this.props.selectedFolderId != Folder.conflictFolderId();
const thisComp = this;
const actionButtonComp = this.props.noteSelectionEnabled || !this.props.visible ? null : <ActionButton addFolderNoteButtons={addFolderNoteButtons} parentFolderId={this.props.selectedFolderId}></ActionButton>
const actionButtonComp = this.props.noteSelectionEnabled || !this.props.visible ? null : <ActionButton addFolderNoteButtons={addFolderNoteButtons} parentFolderId={this.props.selectedFolderId}></ActionButton>;
return (
<View style={rootStyle}>
<ScreenHeader
title={title}
showBackButton={false}
parentComponent={thisComp}
sortButton_press={this.sortButton_press}
folderPickerOptions={this.folderPickerOptions()}
showSearchButton={true}
showSideMenuButton={true}
<ScreenHeader title={title} showBackButton={false} parentComponent={thisComp} sortButton_press={this.sortButton_press} folderPickerOptions={this.folderPickerOptions()} showSearchButton={true} showSideMenuButton={true} />
<NoteList style={this.styles().noteList} />
{actionButtonComp}
<DialogBox
ref={dialogbox => {
this.dialogbox = dialogbox;
}}
/>
<NoteList style={this.styles().noteList}/>
{ actionButtonComp }
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
</View>
);
}
}
const NotesScreen = connect(
(state) => {
return {
folders: state.folders,
tags: state.tags,
selectedFolderId: state.selectedFolderId,
selectedNoteIds: state.selectedNoteIds,
selectedTagId: state.selectedTagId,
selectedSmartFilterId: state.selectedSmartFilterId,
notesParentType: state.notesParentType,
notes: state.notes,
notesSource: state.notesSource,
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
showCompletedTodos: state.settings.showCompletedTodos,
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
notesOrder: stateUtils.notesOrder(state.settings),
};
}
)(NotesScreenComponent)
const NotesScreen = connect(state => {
return {
folders: state.folders,
tags: state.tags,
selectedFolderId: state.selectedFolderId,
selectedNoteIds: state.selectedNoteIds,
selectedTagId: state.selectedTagId,
selectedSmartFilterId: state.selectedSmartFilterId,
notesParentType: state.notesParentType,
notes: state.notes,
notesSource: state.notesSource,
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
showCompletedTodos: state.settings.showCompletedTodos,
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
notesOrder: stateUtils.notesOrder(state.settings),
};
})(NotesScreenComponent);
module.exports = { NotesScreen };

View File

@ -1,7 +1,8 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { View } = require('react-native');
const { Button, Text } = require('react-native');
const { WebView } = require('react-native-webview');
const { WebView } = require('react-native-webview');
const { connect } = require('react-redux');
const Setting = require('lib/models/Setting.js');
const { ScreenHeader } = require('lib/components/screen-header.js');
@ -11,7 +12,6 @@ const { BaseScreenComponent } = require('lib/components/base-screen.js');
const parseUri = require('lib/parseUri');
class OneDriveLoginScreenComponent extends BaseScreenComponent {
static navigationOptions(options) {
return { header: null };
}
@ -29,11 +29,17 @@ class OneDriveLoginScreenComponent extends BaseScreenComponent {
}
startUrl() {
return reg.syncTarget().api().authCodeUrl(this.redirectUrl());
return reg
.syncTarget()
.api()
.authCodeUrl(this.redirectUrl());
}
redirectUrl() {
return reg.syncTarget().api().nativeClientRedirectUrl();
return reg
.syncTarget()
.api()
.nativeClientRedirectUrl();
}
async webview_load(noIdeaWhatThisIs) {
@ -44,10 +50,13 @@ class OneDriveLoginScreenComponent extends BaseScreenComponent {
const parsedUrl = parseUri(url);
if (!this.authCode_ && parsedUrl && parsedUrl.queryKey && parsedUrl.queryKey.code) {
this.authCode_ = parsedUrl.queryKey.code
this.authCode_ = parsedUrl.queryKey.code;
try {
await reg.syncTarget().api().execTokenRequest(this.authCode_, this.redirectUrl(), true);
await reg
.syncTarget()
.api()
.execTokenRequest(this.authCode_, this.redirectUrl(), true);
this.props.dispatch({ type: 'NAV_BACK' });
reg.scheduleSync(0);
} catch (error) {
@ -83,27 +92,33 @@ class OneDriveLoginScreenComponent extends BaseScreenComponent {
render() {
const source = {
uri: this.state.webviewUrl,
}
};
return (
<View style={this.styles().screen}>
<ScreenHeader title={_('Login with OneDrive')}/>
<ScreenHeader title={_('Login with OneDrive')} />
<WebView
source={source}
onNavigationStateChange={(o) => { this.webview_load(o); }}
onError={() => { this.webview_error(); }}
onNavigationStateChange={o => {
this.webview_load(o);
}}
onError={() => {
this.webview_error();
}}
/>
<Button title={_("Refresh")} onPress={() => { this.retryButton_click(); }}></Button>
<Button
title={_('Refresh')}
onPress={() => {
this.retryButton_click();
}}
></Button>
</View>
);
}
}
const OneDriveLoginScreen = connect(
(state) => {
return {};
}
)(OneDriveLoginScreenComponent)
const OneDriveLoginScreen = connect(state => {
return {};
})(OneDriveLoginScreenComponent);
module.exports = { OneDriveLoginScreen };
module.exports = { OneDriveLoginScreen };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { ListView, StyleSheet, View, TextInput, FlatList, TouchableHighlight } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('lib/components/screen-header.js');
@ -13,7 +14,6 @@ const SearchEngineUtils = require('lib/services/SearchEngineUtils');
const DialogBox = require('react-native-dialogbox').default;
class SearchScreenComponent extends BaseScreenComponent {
static navigationOptions(options) {
return { header: null };
}
@ -43,8 +43,8 @@ class SearchScreenComponent extends BaseScreenComponent {
alignItems: 'center',
borderWidth: 1,
borderColor: theme.dividerColor,
}
}
},
};
styles.searchTextInput = Object.assign({}, theme.lineInput);
styles.searchTextInput.paddingLeft = theme.marginLeft;
@ -111,13 +111,12 @@ class SearchScreenComponent extends BaseScreenComponent {
query = query === null ? this.state.query.trim : query.trim();
let notes = []
let notes = [];
if (query) {
if (!!this.props.settings['db.ftsEnabled']) {
if (this.props.settings['db.ftsEnabled']) {
notes = await SearchEngineUtils.notesForQuery(query);
} else {
} else {
let p = query.split(' ');
let temp = [];
for (let i = 0; i < p.length; i++) {
@ -149,7 +148,7 @@ class SearchScreenComponent extends BaseScreenComponent {
let rootStyle = {
flex: 1,
backgroundColor: theme.backgroundColor,
}
};
if (!this.props.visible) {
rootStyle.flex = 0.001; // This is a bit of a hack but it seems to work fine - it makes the component invisible but without unmounting it
@ -174,39 +173,38 @@ class SearchScreenComponent extends BaseScreenComponent {
<TextInput
style={this.styles().searchTextInput}
autoFocus={this.props.visible}
underlineColorAndroid="#ffffff00"
onSubmitEditing={() => { this.searchTextInput_submit() }}
onChangeText={(text) => this.searchTextInput_changeText(text) }
underlineColorAndroid="#ffffff00"
onSubmitEditing={() => {
this.searchTextInput_submit();
}}
onChangeText={text => this.searchTextInput_changeText(text)}
value={this.state.query}
selectionColor={theme.textSelectionColor}
/>
<TouchableHighlight onPress={() => this.clearButton_press() }>
<Icon name='md-close-circle' style={this.styles().clearIcon} />
<TouchableHighlight onPress={() => this.clearButton_press()}>
<Icon name="md-close-circle" style={this.styles().clearIcon} />
</TouchableHighlight>
</View>
<FlatList
data={this.state.notes}
keyExtractor={(item, index) => item.id}
renderItem={(event) => <NoteItem note={event.item}/>}
/>
<FlatList data={this.state.notes} keyExtractor={(item, index) => item.id} renderItem={event => <NoteItem note={event.item} />} />
</View>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
<DialogBox
ref={dialogbox => {
this.dialogbox = dialogbox;
}}
/>
</View>
);
}
}
const SearchScreen = connect(
(state) => {
return {
query: state.searchQuery,
theme: state.settings.theme,
settings: state.settings,
noteSelectionEnabled: state.noteSelectionEnabled,
};
}
)(SearchScreenComponent)
const SearchScreen = connect(state => {
return {
query: state.searchQuery,
theme: state.settings.theme,
settings: state.settings,
noteSelectionEnabled: state.noteSelectionEnabled,
};
})(SearchScreenComponent);
module.exports = { SearchScreen };
module.exports = { SearchScreen };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { ListView, StyleSheet, View, Text, Button, FlatList } = require('react-native');
const Setting = require('lib/models/Setting.js');
const { connect } = require('react-redux');
@ -22,7 +23,6 @@ const styles = StyleSheet.create({
});
class StatusScreenComponent extends BaseScreenComponent {
static navigationOptions(options) {
return { header: null };
}
@ -82,7 +82,7 @@ class StatusScreenComponent extends BaseScreenComponent {
retryHandler = async () => {
await item.retryHandler();
this.resfreshScreen();
}
};
}
text = item.text;
} else {
@ -95,55 +95,56 @@ class StatusScreenComponent extends BaseScreenComponent {
lines.push({ key: 'divider2_' + i, isDivider: true });
}
return (<FlatList
data={lines}
renderItem={({item}) => {
let style = Object.assign({}, baseStyle);
return (
<FlatList
data={lines}
renderItem={({ item }) => {
let style = Object.assign({}, baseStyle);
if (item.isSection === true) {
style.fontWeight = 'bold';
style.marginBottom = 5;
}
if (item.isSection === true) {
style.fontWeight = 'bold';
style.marginBottom = 5;
}
style.flex = 1;
style.flex = 1;
const retryButton = item.retryHandler ? <View style={{flex:0}}><Button title={_('Retry')} onPress={item.retryHandler}/></View> : null;
if (item.isDivider) {
return (<View style={{borderBottomWidth: 1, borderBottomColor: theme.dividerColor, marginTop: 20, marginBottom: 20}}/>);
} else {
return (
<View style={{flex:1, flexDirection:'row'}}>
<Text style={style}>{item.text}</Text>
{retryButton}
const retryButton = item.retryHandler ? (
<View style={{ flex: 0 }}>
<Button title={_('Retry')} onPress={item.retryHandler} />
</View>
);
}
}}
/>);
}
) : null;
if (item.isDivider) {
return <View style={{ borderBottomWidth: 1, borderBottomColor: theme.dividerColor, marginTop: 20, marginBottom: 20 }} />;
} else {
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<Text style={style}>{item.text}</Text>
{retryButton}
</View>
);
}
}}
/>
);
};
let body = renderBody(this.state.report);
return (
<View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader title={_('Status')}/>
<View style={styles.body}>
{ body }
</View>
<Button title={_("Refresh")} onPress={() => this.resfreshScreen()}/>
<ScreenHeader title={_('Status')} />
<View style={styles.body}>{body}</View>
<Button title={_('Refresh')} onPress={() => this.resfreshScreen()} />
</View>
);
}
}
const StatusScreen = connect(
(state) => {
return {
theme: state.settings.theme,
};
}
)(StatusScreenComponent)
const StatusScreen = connect(state => {
return {
theme: state.settings.theme,
};
})(StatusScreenComponent);
module.exports = { StatusScreen };
module.exports = { StatusScreen };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { ListView, StyleSheet, View, TextInput, FlatList, TouchableHighlight } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('lib/components/screen-header.js');
@ -13,10 +14,9 @@ let styles = {
body: {
flex: 1,
},
}
};
class TagScreenComponent extends BaseScreenComponent {
static navigationOptions(options) {
return { header: null };
}
@ -53,22 +53,23 @@ class TagScreenComponent extends BaseScreenComponent {
return (
<View style={this.styles().screen}>
<ScreenHeader title={title} menuOptions={this.menuOptions()} />
<NoteList style={{flex: 1}}/>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
<NoteList style={{ flex: 1 }} />
<DialogBox
ref={dialogbox => {
this.dialogbox = dialogbox;
}}
/>
</View>
);
}
}
const TagScreen = connect(
(state) => {
return {
tag: tag,
notes: state.notes,
notesSource: state.notesSource,
};
}
)(TagScreenComponent)
const TagScreen = connect(state => {
return {
tag: tag,
notes: state.notes,
notesSource: state.notesSource,
};
})(TagScreenComponent);
module.exports = { TagScreen };
module.exports = { TagScreen };

View File

@ -1,4 +1,5 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { AppState, View, Button, Text, FlatList, StyleSheet, TouchableOpacity } = require('react-native');
const { stateUtils } = require('lib/reducer.js');
const { connect } = require('react-redux');
@ -18,7 +19,6 @@ const DialogBox = require('react-native-dialogbox').default;
const { BaseScreenComponent } = require('lib/components/base-screen.js');
class TagsScreenComponent extends BaseScreenComponent {
static navigationOptions(options) {
return { header: null };
}
@ -74,8 +74,12 @@ class TagsScreenComponent extends BaseScreenComponent {
tagList_renderItem(event) {
const tag = event.item;
return (
<TouchableOpacity onPress={() => { this.tagItem_press({ id: tag.id }) }}>
<View style={ this.styles().listItem }>
<TouchableOpacity
onPress={() => {
this.tagItem_press({ id: tag.id });
}}
>
<View style={this.styles().listItem}>
<Text style={this.styles().listItemText}>{tag.title}</Text>
</View>
</TouchableOpacity>
@ -100,31 +104,21 @@ class TagsScreenComponent extends BaseScreenComponent {
let rootStyle = {
flex: 1,
backgroundColor: theme.backgroundColor,
}
};
return (
<View style={rootStyle}>
<ScreenHeader
title={_('Tags')}
parentComponent={this}
showSearchButton={false}
/>
<FlatList style={{flex:1}}
data={this.state.tags}
renderItem={this.tagList_renderItem}
keyExtractor={this.tagList_keyExtractor}
/>
<ScreenHeader title={_('Tags')} parentComponent={this} showSearchButton={false} />
<FlatList style={{ flex: 1 }} data={this.state.tags} renderItem={this.tagList_renderItem} keyExtractor={this.tagList_keyExtractor} />
</View>
);
}
}
const TagsScreen = connect(
(state) => {
return {
theme: state.settings.theme,
};
}
)(TagsScreenComponent)
const TagsScreen = connect(state => {
return {
theme: state.settings.theme,
};
})(TagsScreenComponent);
module.exports = { TagsScreen };

View File

@ -3,14 +3,14 @@ const SyncTargetRegistry = require('lib/SyncTargetRegistry');
const ObjectUtils = require('lib/ObjectUtils');
const { _ } = require('lib/locale.js');
const shared = {}
const shared = {};
shared.init = function(comp) {
if (!comp.state) comp.state = {};
comp.state.checkSyncConfigResult = null;
comp.state.settings = {};
comp.state.changedSettingKeys = [];
}
};
shared.checkSyncConfig = async function(comp, settings) {
const syncTargetId = settings['sync.target'];
@ -19,7 +19,7 @@ shared.checkSyncConfig = async function(comp, settings) {
comp.setState({ checkSyncConfigResult: 'checking' });
const result = await SyncTargetClass.checkConfig(ObjectUtils.convertValuesToFunctions(options));
comp.setState({ checkSyncConfigResult: result });
}
};
shared.checkSyncConfigMessages = function(comp) {
const result = comp.state.checkSyncConfigResult;
@ -35,7 +35,7 @@ shared.checkSyncConfigMessages = function(comp) {
}
return output;
}
};
shared.updateSettingValue = function(comp, key, value) {
const settings = Object.assign({}, comp.state.settings);
@ -47,18 +47,18 @@ shared.updateSettingValue = function(comp, key, value) {
settings: settings,
changedSettingKeys: changedSettingKeys,
});
}
};
shared.saveSettings = function(comp) {
for (let key in comp.state.settings) {
if (!comp.state.settings.hasOwnProperty(key)) continue;
if (comp.state.changedSettingKeys.indexOf(key) < 0) continue;
console.info("Saving", key, comp.state.settings[key]);
console.info('Saving', key, comp.state.settings[key]);
Setting.setValue(key, comp.state.settings[key]);
}
comp.setState({ changedSettingKeys: [] });
}
};
shared.settingsToComponents = function(comp, device, settings) {
const keys = Setting.keys(true, device);
@ -76,8 +76,8 @@ shared.settingsToComponents = function(comp, device, settings) {
settingComps.push(settingComp);
}
return settingComps
}
return settingComps;
};
shared.settingsToComponents2 = function(comp, device, settings) {
const keys = Setting.keys(true, device);
@ -103,7 +103,7 @@ shared.settingsToComponents2 = function(comp, device, settings) {
sectionComps.push(sectionComp);
}
return sectionComps
}
return sectionComps;
};
module.exports = shared;
module.exports = shared;

View File

@ -5,7 +5,6 @@ const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting');
class Shared {
constructor(comp, showInfoMessageBox, showErrorMessageBox) {
this.comp_ = comp;
@ -20,13 +19,13 @@ class Shared {
this.loginUrl_click = () => {
if (!this.comp_.state.loginUrl) return;
shim.openUrl(this.comp_.state.loginUrl);
}
};
this.authCodeInput_change = (event) => {
this.authCodeInput_change = event => {
this.comp_.setState({
authCode: typeof event === 'object' ? event.target.value : event
authCode: typeof event === 'object' ? event.target.value : event,
});
}
};
this.submit_click = async () => {
this.comp_.setState({ checkingAuthToken: true });
@ -45,7 +44,7 @@ class Shared {
} finally {
this.comp_.setState({ checkingAuthToken: false });
}
}
};
}
syncTargetId() {
@ -67,7 +66,6 @@ class Shared {
loginUrl: api.loginUrl(),
});
}
}
module.exports = Shared;
module.exports = Shared;

View File

@ -18,15 +18,18 @@ shared.constructor = function(comp) {
comp.isMounted_ = false;
comp.refreshStatsIID_ = null;
}
};
shared.initState = function(comp, props) {
comp.setState({
masterKeys: props.masterKeys,
passwords: props.passwords ? props.passwords : {},
}, () => {
comp.checkPasswords();
});
comp.setState(
{
masterKeys: props.masterKeys,
passwords: props.passwords ? props.passwords : {},
},
() => {
comp.checkPasswords();
}
);
comp.refreshStats();
@ -43,12 +46,12 @@ shared.initState = function(comp, props) {
}
comp.refreshStats();
}, 3000);
}
};
shared.refreshStats = async function(comp) {
const stats = await BaseItem.encryptedItemsStats();
comp.setState({ stats: stats });
}
};
shared.checkPasswords = async function(comp) {
const passwordChecks = Object.assign({}, comp.state.passwordChecks);
@ -59,14 +62,14 @@ shared.checkPasswords = async function(comp) {
passwordChecks[mk.id] = ok;
}
comp.setState({ passwordChecks: passwordChecks });
}
};
shared.decryptedStatText = function(comp) {
const stats = comp.state.stats;
const doneCount = stats.encrypted !== null ? (stats.total - stats.encrypted) : '-';
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
const totalCount = stats.total !== null ? stats.total : '-';
return _('Decrypted items: %s / %s', doneCount, totalCount);
}
};
shared.onSavePasswordClick = function(comp, mk) {
const password = comp.state.passwords[mk.id];
@ -77,12 +80,12 @@ shared.onSavePasswordClick = function(comp, mk) {
}
comp.checkPasswords();
}
};
shared.onPasswordChange = function(comp, mk, password) {
const passwords = comp.state.passwords;
passwords[mk.id] = password;
comp.setState({ passwords: passwords });
}
};
module.exports = shared;
module.exports = shared;

View File

@ -17,12 +17,16 @@ const saveNoteMutex_ = new Mutex();
shared.noteExists = async function(noteId) {
const existingNote = await Note.load(noteId);
return !!existingNote;
}
};
shared.saveNoteButton_press = async function(comp, folderId = null, options = null) {
options = Object.assign({}, {
autoTitle: true,
}, options);
options = Object.assign(
{},
{
autoTitle: true,
},
options
);
const releaseMutex = await saveNoteMutex_.acquire();
@ -55,7 +59,7 @@ shared.saveNoteButton_press = async function(comp, folderId = null, options = nu
if (saveOptions.fields && saveOptions.fields.indexOf('title') < 0) saveOptions.fields.push('title');
}
const savedNote = ('fields' in saveOptions) && !saveOptions.fields.length ? Object.assign({}, note) : await Note.save(note, saveOptions);
const savedNote = 'fields' in saveOptions && !saveOptions.fields.length ? Object.assign({}, note) : await Note.save(note, saveOptions);
const stateNote = comp.state.note;
@ -91,7 +95,7 @@ shared.saveNoteButton_press = async function(comp, folderId = null, options = nu
// await shared.refreshAttachedResources(comp, newState.note.body);
if (isNew) {
Note.updateGeolocation(note.id).then((geoNote) => {
Note.updateGeolocation(note.id).then(geoNote => {
const stateNote = comp.state.note;
if (!stateNote || !geoNote) return;
if (stateNote.id !== geoNote.id) return; // Another note has been loaded while geoloc was being retrieved
@ -103,7 +107,7 @@ shared.saveNoteButton_press = async function(comp, folderId = null, options = nu
longitude: geoNote.longitude,
latitude: geoNote.latitude,
altitude: geoNote.altitude,
}
};
const modNote = Object.assign({}, stateNote, geoInfo);
const modLastSavedNote = Object.assign({}, comp.state.lastSavedNote, geoInfo);
@ -122,7 +126,7 @@ shared.saveNoteButton_press = async function(comp, folderId = null, options = nu
}
releaseMutex();
}
};
shared.saveOneProperty = async function(comp, name, value) {
let note = Object.assign({}, comp.state.note);
@ -145,25 +149,25 @@ shared.saveOneProperty = async function(comp, name, value) {
});
} else {
note[name] = value;
comp.setState({ note: note });
comp.setState({ note: note });
}
}
};
shared.noteComponent_change = function(comp, propName, propValue) {
let newState = {}
let newState = {};
let note = Object.assign({}, comp.state.note);
note[propName] = propValue;
newState.note = note;
comp.setState(newState);
}
};
let resourceCache_ = {};
shared.clearResourceCache = function() {
resourceCache_ = {};
}
};
shared.attachedResources = async function(noteBody) {
if (!noteBody) return {};
@ -172,7 +176,7 @@ shared.attachedResources = async function(noteBody) {
const output = {};
for (let i = 0; i < resourceIds.length; i++) {
const id = resourceIds[i];
if (resourceCache_[id]) {
output[id] = resourceCache_[id];
} else {
@ -190,14 +194,14 @@ shared.attachedResources = async function(noteBody) {
}
return output;
}
};
shared.isModified = function(comp) {
if (!comp.state.note || !comp.state.lastSavedNote) return false;
let diff = BaseModel.diffObjects(comp.state.lastSavedNote, comp.state.note);
delete diff.type_;
return !!Object.getOwnPropertyNames(diff).length;
}
};
shared.initState = async function(comp) {
let note = null;
@ -226,13 +230,13 @@ shared.initState = async function(comp) {
}
comp.lastLoadedNoteId_ = note ? note.id : null;
}
};
shared.toggleIsTodo_onPress = function(comp) {
let newNote = Note.toggleIsTodo(comp.state.note);
let newState = { note: newNote };
comp.setState(newState);
}
};
shared.toggleCheckbox = function(ipcMessage, noteBody) {
let newBody = noteBody.split('\n');
@ -264,24 +268,24 @@ shared.toggleCheckbox = function(ipcMessage, noteBody) {
if (!isCrossLine) {
line = line.replace(/- \[ \] /, '- [x] ');
} else {
} else {
line = line.replace(/- \[x\] /i, '- [ ] ');
}
newBody[lineIndex] = line;
return newBody.join('\n')
}
return newBody.join('\n');
};
shared.installResourceHandling = function(refreshResourceHandler) {
ResourceFetcher.instance().on('downloadComplete', refreshResourceHandler);
ResourceFetcher.instance().on('downloadStarted', refreshResourceHandler);
DecryptionWorker.instance().on('resourceDecrypted', refreshResourceHandler);
}
};
shared.uninstallResourceHandling = function(refreshResourceHandler) {
ResourceFetcher.instance().off('downloadComplete', refreshResourceHandler);
ResourceFetcher.instance().off('downloadStarted', refreshResourceHandler);
DecryptionWorker.instance().off('resourceDecrypted', refreshResourceHandler);
}
};
module.exports = shared;

View File

@ -30,6 +30,6 @@ const reduxSharedMiddleware = async function(store, next, action) {
items: await Tag.allWithNotes(),
});
}
}
};
module.exports = reduxSharedMiddleware;
module.exports = reduxSharedMiddleware;

View File

@ -49,11 +49,13 @@ function renderFoldersRecursive_(props, renderItem, items, parentId, depth, orde
shared.renderFolders = function(props, renderItem) {
return renderFoldersRecursive_(props, renderItem, [], '', 0, []);
}
};
shared.renderTags = function(props, renderItem) {
let tags = props.tags.slice();
tags.sort((a, b) => { return a.title < b.title ? -1 : +1; });
tags.sort((a, b) => {
return a.title < b.title ? -1 : +1;
});
let tagItems = [];
const order = [];
for (let i = 0; i < tags.length; i++) {
@ -65,7 +67,7 @@ shared.renderTags = function(props, renderItem) {
items: tagItems,
order: order,
};
}
};
// shared.renderSearches = function(props, renderItem) {
// let searches = props.searches.slice();
@ -88,7 +90,7 @@ shared.synchronize_press = async function(comp) {
const action = comp.props.syncStarted ? 'cancel' : 'start';
if (!await reg.syncTarget().isAuthenticated()) {
if (!(await reg.syncTarget().isAuthenticated())) {
if (reg.syncTarget().authRouteName()) {
comp.props.dispatch({
type: 'NAV_GO',
@ -117,6 +119,6 @@ shared.synchronize_press = async function(comp) {
reg.scheduleSync(0);
return 'sync';
}
}
};
module.exports = shared;
module.exports = shared;

View File

@ -1,5 +1,6 @@
const React = require('react'); const Component = React.Component;
const { TouchableOpacity , Button, Text, Image, StyleSheet, ScrollView, View, Alert } = require('react-native');
const React = require('react');
const Component = React.Component;
const { TouchableOpacity, Button, Text, Image, StyleSheet, ScrollView, View, Alert } = require('react-native');
const { connect } = require('react-redux');
const Icon = require('react-native-vector-icons/Ionicons').default;
const Tag = require('lib/models/Tag.js');
@ -16,7 +17,6 @@ const shared = require('lib/components/shared/side-menu-shared.js');
const { ActionButton } = require('lib/components/action-button.js');
class SideMenuContentNoteComponent extends Component {
constructor() {
super();
@ -32,7 +32,7 @@ class SideMenuContentNoteComponent extends Component {
let styles = {
menu: {
flex: 1,
backgroundColor: theme.backgroundColor
backgroundColor: theme.backgroundColor,
},
button: {
flex: 1,
@ -57,7 +57,7 @@ class SideMenuContentNoteComponent extends Component {
}
renderDivider(key) {
return <View style={{ marginTop: 15, marginBottom: 15, flex: -1, borderBottomWidth: 1, borderBottomColor: globalStyle.dividerColor }} key={key}></View>
return <View style={{ marginTop: 15, marginBottom: 15, flex: -1, borderBottomWidth: 1, borderBottomColor: globalStyle.dividerColor }} key={key}></View>;
}
renderSideBarButton(key, title, iconName, onPressHandler) {
@ -65,7 +65,7 @@ class SideMenuContentNoteComponent extends Component {
const content = (
<View key={key} style={onPressHandler ? this.styles().sideButton : this.styles().sideButtonDisabled}>
{ !iconName ? null : <Icon name={iconName} style={this.styles().sidebarIcon} /> }
{!iconName ? null : <Icon name={iconName} style={this.styles().sidebarIcon} />}
<Text style={this.styles().sideButtonText}>{title}</Text>
</View>
);
@ -96,7 +96,7 @@ class SideMenuContentNoteComponent extends Component {
}
let style = {
flex:1,
flex: 1,
borderRightWidth: 1,
borderRightColor: globalStyle.dividerColor,
backgroundColor: theme.backgroundColor,
@ -105,22 +105,20 @@ class SideMenuContentNoteComponent extends Component {
return (
<View style={style}>
<View style={{flex:1, opacity: this.props.opacity}}>
<View style={{ flex: 1, opacity: this.props.opacity }}>
<ScrollView scrollsToTop={false} style={this.styles().menu}>
{ items }
{items}
</ScrollView>
</View>
</View>
);
}
};
}
const SideMenuContentNote = connect(
(state) => {
return {
theme: state.settings.theme,
};
}
)(SideMenuContentNoteComponent)
const SideMenuContentNote = connect(state => {
return {
theme: state.settings.theme,
};
})(SideMenuContentNoteComponent);
module.exports = { SideMenuContentNote };
module.exports = { SideMenuContentNote };

View File

@ -1,5 +1,6 @@
const React = require('react'); const Component = React.Component;
const { Easing, Animated, TouchableOpacity , Button, Text, Image, StyleSheet, ScrollView, View, Alert } = require('react-native');
const React = require('react');
const Component = React.Component;
const { Easing, Animated, TouchableOpacity, Button, Text, Image, StyleSheet, ScrollView, View, Alert } = require('react-native');
const { connect } = require('react-redux');
const Icon = require('react-native-vector-icons/Ionicons').default;
const Tag = require('lib/models/Tag.js');
@ -16,7 +17,6 @@ const shared = require('lib/components/shared/side-menu-shared.js');
const { ActionButton } = require('lib/components/action-button.js');
class SideMenuContentComponent extends Component {
constructor() {
super();
this.state = {
@ -47,7 +47,7 @@ class SideMenuContentComponent extends Component {
let styles = {
menu: {
flex: 1,
backgroundColor: theme.backgroundColor
backgroundColor: theme.backgroundColor,
},
button: {
flex: 1,
@ -82,7 +82,7 @@ class SideMenuContentComponent extends Component {
styles.folderButtonSelected = Object.assign({}, styles.folderButton);
styles.folderButtonSelected.backgroundColor = theme.selectedColor;
styles.folderIcon = Object.assign({}, theme.icon);
styles.folderIcon.color = theme.colorFaded;//'#0072d5';
styles.folderIcon.color = theme.colorFaded; //'#0072d5';
styles.folderIcon.paddingTop = 3;
styles.sideButton = Object.assign({}, styles.button, { flex: 0 });
@ -96,18 +96,20 @@ class SideMenuContentComponent extends Component {
componentDidUpdate(prevProps) {
if (this.props.syncStarted !== prevProps.syncStarted) {
if (this.props.syncStarted) {
this.syncIconAnimation = Animated.loop(Animated.timing(this.syncIconRotationValue, {
toValue: 1,
duration: 3000,
easing: Easing.linear,
}));
this.syncIconAnimation = Animated.loop(
Animated.timing(this.syncIconRotationValue, {
toValue: 1,
duration: 3000,
easing: Easing.linear,
})
);
this.syncIconAnimation.start();
} else {
if (this.syncIconAnimation) this.syncIconAnimation.stop();
this.syncIconAnimation = null;
}
}
}
}
folder_press(folder) {
@ -127,7 +129,8 @@ class SideMenuContentComponent extends Component {
Alert.alert(
'',
_('Notebook: %s', folder.title), [
_('Notebook: %s', folder.title),
[
{
text: _('Rename'),
onPress: () => {
@ -143,7 +146,7 @@ class SideMenuContentComponent extends Component {
routeName: 'Folder',
folderId: folder.id,
});
}
},
},
{
text: _('Delete'),
@ -168,9 +171,10 @@ class SideMenuContentComponent extends Component {
text: _('Cancel'),
onPress: () => {},
style: 'cancel',
}
], {
cancelable: false
},
],
{
cancelable: false,
}
);
}
@ -240,22 +244,38 @@ class SideMenuContentComponent extends Component {
let iconWrapper = null;
const iconName = this.props.collapsedFolderIds.indexOf(folder.id) >= 0 ? 'md-arrow-dropdown' : 'md-arrow-dropup';
const iconComp = <Icon name={iconName} style={this.styles().folderIcon} />
const iconComp = <Icon name={iconName} style={this.styles().folderIcon} />;
iconWrapper = !hasChildren ? null : (
<TouchableOpacity style={iconWrapperStyle} folderid={folder.id} onPress={() => { if (hasChildren) this.folder_togglePress(folder) }}>
{ iconComp }
<TouchableOpacity
style={iconWrapperStyle}
folderid={folder.id}
onPress={() => {
if (hasChildren) this.folder_togglePress(folder);
}}
>
{iconComp}
</TouchableOpacity>
);
return (
<View key={folder.id} style={{ flex: 1, flexDirection: 'row' }}>
<TouchableOpacity style={{ flex: 1 }} onPress={() => { this.folder_press(folder) }} onLongPress={() => { this.folder_longPress(folder) }}>
<TouchableOpacity
style={{ flex: 1 }}
onPress={() => {
this.folder_press(folder);
}}
onLongPress={() => {
this.folder_longPress(folder);
}}
>
<View style={folderButtonStyle}>
<Text numberOfLines={1} style={this.styles().folderButtonText}>{Folder.displayTitle(folder)}</Text>
<Text numberOfLines={1} style={this.styles().folderButtonText}>
{Folder.displayTitle(folder)}
</Text>
</View>
</TouchableOpacity>
{ iconWrapper }
{iconWrapper}
</View>
);
}
@ -263,14 +283,10 @@ class SideMenuContentComponent extends Component {
renderSideBarButton(key, title, iconName, onPressHandler = null, selected = false) {
const theme = themeStyle(this.props.theme);
let icon = <Icon name={iconName} style={this.styles().sidebarIcon} />
let icon = <Icon name={iconName} style={this.styles().sidebarIcon} />;
if (key === 'synchronize_button') {
icon = (
<Animated.View style={{transform: [{rotate: this.syncIconRotation}]}}>
{icon}
</Animated.View>
);
icon = <Animated.View style={{ transform: [{ rotate: this.syncIconRotation }] }}>{icon}</Animated.View>;
}
const content = (
@ -290,7 +306,7 @@ class SideMenuContentComponent extends Component {
}
makeDivider(key) {
return <View style={{ marginTop: 15, marginBottom: 15, flex: -1, borderBottomWidth: 1, borderBottomColor: globalStyle.dividerColor }} key={key}></View>
return <View style={{ marginTop: 15, marginBottom: 15, flex: -1, borderBottomWidth: 1, borderBottomColor: globalStyle.dividerColor }} key={key}></View>;
}
renderBottomPanel() {
@ -309,7 +325,7 @@ class SideMenuContentComponent extends Component {
items.push(this.makeDivider('divider_2'));
let lines = Synchronizer.reportToLines(this.props.syncReport);
const syncReportText = lines.join("\n");
const syncReportText = lines.join('\n');
let decryptionReportText = '';
if (this.props.decryptionWorker && this.props.decryptionWorker.state !== 'idle' && this.props.decryptionWorker.itemCount) {
@ -326,20 +342,16 @@ class SideMenuContentComponent extends Component {
if (resourceFetcherText) fullReport.push(resourceFetcherText);
if (decryptionReportText) fullReport.push(decryptionReportText);
items.push(this.renderSideBarButton(
'synchronize_button',
!this.props.syncStarted ? _('Synchronise') : _('Cancel'),
'md-sync',
this.synchronize_press
));
items.push(this.renderSideBarButton('synchronize_button', !this.props.syncStarted ? _('Synchronise') : _('Cancel'), 'md-sync', this.synchronize_press));
if (fullReport.length) items.push(<Text key='sync_report' style={this.styles().syncStatus}>{fullReport.join('\n')}</Text>);
if (fullReport.length)
items.push(
<Text key="sync_report" style={this.styles().syncStatus}>
{fullReport.join('\n')}
</Text>
);
return (
<View style={{ flex: 0, flexDirection: 'column', paddingBottom: theme.marginBottom }}>
{ items }
</View>
);
return <View style={{ flex: 0, flexDirection: 'column', paddingBottom: theme.marginBottom }}>{items}</View>;
}
render() {
@ -349,7 +361,7 @@ class SideMenuContentComponent extends Component {
// HACK: inner height of ScrollView doesn't appear to be calculated correctly when
// using padding. So instead creating blank elements for padding bottom and top.
items.push(<View style={{ height: globalStyle.marginTop }} key='bottom_top_hack'/>);
items.push(<View style={{ height: globalStyle.marginTop }} key="bottom_top_hack" />);
items.push(this.renderSideBarButton('all_notes', _('All notes'), 'md-document', this.allNotesButton_press, this.props.notesParentType === 'SmartFilter'));
@ -364,7 +376,7 @@ class SideMenuContentComponent extends Component {
}
let style = {
flex:1,
flex: 1,
borderRightWidth: 1,
borderRightColor: globalStyle.dividerColor,
backgroundColor: theme.backgroundColor,
@ -372,35 +384,33 @@ class SideMenuContentComponent extends Component {
return (
<View style={style}>
<View style={{flex:1, opacity: this.props.opacity}}>
<View style={{ flex: 1, opacity: this.props.opacity }}>
<ScrollView scrollsToTop={false} style={this.styles().menu}>
{ items }
{items}
</ScrollView>
{ this.renderBottomPanel() }
{this.renderBottomPanel()}
</View>
</View>
);
}
};
}
const SideMenuContent = connect(
(state) => {
return {
folders: state.folders,
syncStarted: state.syncStarted,
syncReport: state.syncReport,
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
notesParentType: state.notesParentType,
locale: state.settings.locale,
theme: state.settings.theme,
// Don't do the opacity animation as it means re-rendering the list multiple times
// opacity: state.sideMenuOpenPercent,
collapsedFolderIds: state.collapsedFolderIds,
decryptionWorker: state.decryptionWorker,
resourceFetcher: state.resourceFetcher,
};
}
)(SideMenuContentComponent)
const SideMenuContent = connect(state => {
return {
folders: state.folders,
syncStarted: state.syncStarted,
syncReport: state.syncReport,
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
notesParentType: state.notesParentType,
locale: state.settings.locale,
theme: state.settings.theme,
// Don't do the opacity animation as it means re-rendering the list multiple times
// opacity: state.sideMenuOpenPercent,
collapsedFolderIds: state.collapsedFolderIds,
decryptionWorker: state.decryptionWorker,
resourceFetcher: state.resourceFetcher,
};
})(SideMenuContentComponent);
module.exports = { SideMenuContent };
module.exports = { SideMenuContent };

View File

@ -1,15 +1,14 @@
const React = require('react'); const Component = React.Component;
const React = require('react');
const Component = React.Component;
const { connect } = require('react-redux');
const SideMenu_ = require('react-native-side-menu').default;
class SideMenuComponent extends SideMenu_ {};
class SideMenuComponent extends SideMenu_ {}
const MySideMenu = connect(
(state) => {
return {
isOpen: state.showSideMenu,
};
}
)(SideMenuComponent)
const MySideMenu = connect(state => {
return {
isOpen: state.showSideMenu,
};
})(SideMenuComponent);
module.exports = { SideMenu: MySideMenu };
module.exports = { SideMenu: MySideMenu };

View File

@ -2,10 +2,9 @@ const sqlite3 = require('sqlite3').verbose();
const Promise = require('promise');
class DatabaseDriverNode {
open(options) {
return new Promise((resolve, reject) => {
this.db_ = new sqlite3.Database(options.name, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (error) => {
this.db_ = new sqlite3.Database(options.name, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, error => {
if (error) {
reject(error);
return;
@ -57,7 +56,7 @@ class DatabaseDriverNode {
exec(sql, params = null) {
if (!params) params = {};
return new Promise((resolve, reject) => {
this.db_.run(sql, params, (error) => {
this.db_.run(sql, params, error => {
if (error) {
reject(error);
return;
@ -70,7 +69,6 @@ class DatabaseDriverNode {
lastInsertId() {
throw new Error('NOT IMPLEMENTED');
}
}
module.exports = { DatabaseDriverNode };
module.exports = { DatabaseDriverNode };

View File

@ -1,7 +1,6 @@
const SQLite = require('react-native-sqlite-storage');
class DatabaseDriverReactNative {
constructor() {
this.lastInsertId_ = null;
}
@ -9,12 +8,16 @@ class DatabaseDriverReactNative {
open(options) {
//SQLite.DEBUG(true);
return new Promise((resolve, reject) => {
SQLite.openDatabase({ name: options.name }, (db) => {
this.db_ = db;
resolve();
}, (error) => {
reject(error);
});
SQLite.openDatabase(
{ name: options.name },
db => {
this.db_ = db;
resolve();
},
error => {
reject(error);
}
);
});
}
@ -28,17 +31,22 @@ class DatabaseDriverReactNative {
selectOne(sql, params = null) {
return new Promise((resolve, reject) => {
this.db_.executeSql(sql, params, (r) => {
resolve(r.rows.length ? r.rows.item(0) : null);
}, (error) => {
reject(error);
});
this.db_.executeSql(
sql,
params,
r => {
resolve(r.rows.length ? r.rows.item(0) : null);
},
error => {
reject(error);
}
);
});
}
selectAll(sql, params = null) {
return this.exec(sql, params).then((r) => {
let output = []
return this.exec(sql, params).then(r => {
let output = [];
for (let i = 0; i < r.rows.length; i++) {
output.push(r.rows.item(i));
}
@ -48,19 +56,23 @@ class DatabaseDriverReactNative {
exec(sql, params = null) {
return new Promise((resolve, reject) => {
this.db_.executeSql(sql, params, (r) => {
if ('insertId' in r) this.lastInsertId_ = r.insertId;
resolve(r);
}, (error) => {
reject(error);
});
this.db_.executeSql(
sql,
params,
r => {
if ('insertId' in r) this.lastInsertId_ = r.insertId;
resolve(r);
},
error => {
reject(error);
}
);
});
}
lastInsertId() {
return this.lastInsertId_;
}
}
module.exports = { DatabaseDriverReactNative };
module.exports = { DatabaseDriverReactNative };

View File

@ -6,7 +6,6 @@ const { sprintf } = require('sprintf-js');
const Mutex = require('async-mutex').Mutex;
class Database {
constructor(driver) {
this.debugMode_ = false;
this.driver_ = driver;
@ -48,7 +47,7 @@ class Database {
let p = field.split('.');
if (p.length == 1) return '`' + field + '`';
if (p.length == 2) return p[0] + '.`' + p[1] + '`';
throw new Error('Invalid field format: ' + field);
}
@ -152,7 +151,7 @@ class Database {
if (type == 'fieldType') {
if (s) s = s.toUpperCase();
if (s == 'INTEGER') s = 'INT';
if (!(('TYPE_' + s) in this)) throw new Error('Unkonwn fieldType: ' + s);
if (!('TYPE_' + s in this)) throw new Error('Unkonwn fieldType: ' + s);
return this['TYPE_' + s];
}
if (type == 'syncTarget') {
@ -183,12 +182,12 @@ class Database {
sqlStringToLines(sql) {
let output = [];
let lines = sql.split("\n");
let lines = sql.split('\n');
let statement = '';
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line == '') continue;
if (line.substr(0, 2) == "--") continue;
if (line.substr(0, 2) == '--') continue;
statement += line.trim();
if (line[line.length - 1] == ',') statement += ' ';
if (line[line.length - 1] == ';') {
@ -214,7 +213,7 @@ class Database {
static insertQuery(tableName, data) {
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
let keySql= '';
let keySql = '';
let valueSql = '';
let params = [];
for (let key in data) {
@ -272,7 +271,7 @@ class Database {
for (let n in fields) {
if (!fields.hasOwnProperty(n)) continue;
fieldsWithType.push(this.escapeField(n) + ' ' + fields[n]);
}
}
let sql = `
CREATE TEMPORARY TABLE _BACKUP_TABLE_NAME_(_FIELDS_TYPE_);
@ -288,9 +287,9 @@ class Database {
sql = sql.replace(/_FIELDS_NO_TYPE_/g, this.escapeFields(fieldsNoType).join(','));
sql = sql.replace(/_FIELDS_TYPE_/g, fieldsWithType.join(','));
return sql.trim().split("\n");
return sql.trim().split('\n');
}
wrapQueries(queries) {
let output = [];
for (let i = 0; i < queries.length; i++) {
@ -313,7 +312,6 @@ class Database {
return sql; // Already wrapped
}
}
}
Database.TYPE_UNKNOWN = 0;
@ -321,4 +319,4 @@ Database.TYPE_INT = 1;
Database.TYPE_TEXT = 2;
Database.TYPE_NUMERIC = 3;
module.exports = { Database };
module.exports = { Database };

View File

@ -16,19 +16,18 @@ dialogs.confirm = (parentComponent, message) => {
parentComponent.dialogbox.confirm({
content: message,
ok: {
callback: () => {
resolve(true);
}
},
},
cancel: {
callback: () => {
resolve(false);
}
},
},
});
});
};
@ -55,23 +54,23 @@ dialogs.pop = (parentComponent, message, buttons, options = null) => {
}
parentComponent.dialogbox.pop({
content: message,
content: message,
btns: btns,
buttonFlow: options.buttonFlow,
});
});
}
};
dialogs.error = (parentComponent, message) => {
Keyboard.dismiss();
return parentComponent.dialogbox.alert(message);
}
};
dialogs.info = (parentComponent, message) => {
Keyboard.dismiss();
return parentComponent.dialogbox.alert(message);
}
};
dialogs.DialogBox = DialogBox
dialogs.DialogBox = DialogBox;
module.exports = { dialogs };
module.exports = { dialogs };

View File

@ -3,8 +3,7 @@ const { shim } = require('lib/shim');
const JoplinError = require('lib/JoplinError');
const { basicDelta } = require('lib/file-api');
class FileApiDriverDropbox {
class FileApiDriverDropbox {
constructor(api) {
this.api_ = api;
}
@ -77,12 +76,12 @@ class FileApiDriverDropbox {
try {
const response = await this.api().exec('POST', urlPath, body);
const output = {
items: this.metadataToStats_(response.entries),
hasMore: response.has_more,
context: { cursor: response.cursor },
}
};
return output;
} catch (error) {
@ -124,11 +123,17 @@ class FileApiDriverDropbox {
async get(path, options) {
if (!options) options = {};
if (!options.responseFormat) options.responseFormat = 'text';
try {
const response = await this.api().exec('POST', 'files/download', null, {
'Dropbox-API-Arg': JSON.stringify({ "path": this.makePath_(path) }),
}, options);
const response = await this.api().exec(
'POST',
'files/download',
null,
{
'Dropbox-API-Arg': JSON.stringify({ path: this.makePath_(path) }),
},
options
);
return response;
} catch (error) {
if (this.hasErrorCode_(error, 'not_found')) {
@ -151,21 +156,28 @@ class FileApiDriverDropbox {
// Ignore
} else {
throw error;
}
}
}
}
async put(path, content, options = null) {
// See https://github.com/facebook/react-native/issues/14445#issuecomment-352965210
if (typeof content === 'string') content = shim.Buffer.from(content, 'utf8')
if (typeof content === 'string') content = shim.Buffer.from(content, 'utf8');
try {
await this.api().exec('POST', 'files/upload', content, {
'Dropbox-API-Arg': JSON.stringify({
path: this.makePath_(path),
mode: 'overwrite',
mute: true, // Don't send a notification to user since there can be many of these updates
})}, options);
await this.api().exec(
'POST',
'files/upload',
content,
{
'Dropbox-API-Arg': JSON.stringify({
path: this.makePath_(path),
mode: 'overwrite',
mute: true, // Don't send a notification to user since there can be many of these updates
}),
},
options
);
} catch (error) {
if (this.hasErrorCode_(error, 'restricted_content')) {
throw new JoplinError('Cannot upload because content is restricted by Dropbox', 'rejectedByTarget');
@ -218,7 +230,6 @@ class FileApiDriverDropbox {
await time.sleep(2);
}
}
}
module.exports = { FileApiDriverDropbox };
module.exports = { FileApiDriverDropbox };

View File

@ -6,7 +6,7 @@ const { basicDelta } = require('lib/file-api');
// both clients will not know about each others updates during the next sync. They will simply both sync their note and whoever
// comes last will overwrite (on the remote storage) the note of the other client. Both client will then have a different note at
// that point and that will only be resolved if one of them changes the note and sync (if they don't change it, it will never get resolved).
//
//
// This is compound with the fact that we can't have a reliable delta API on the file system so we need to check all the timestamps
// every time and rely on this exclusively to know about changes.
//
@ -15,7 +15,6 @@ const { basicDelta } = require('lib/file-api');
// will have been modified at the same exact second at some point. If not, it's another bug that needs to be investigated.
class FileApiDriverLocal {
fsErrorToJsError_(error, path = null) {
let msg = error.toString();
if (path !== null) msg += '. Path: ' + path;
@ -66,7 +65,7 @@ class FileApiDriverLocal {
}
async delta(path, options) {
const getStatFn = async (path) => {
const getStatFn = async path => {
const stats = await this.fsDriver().readDirStats(path);
return this.metadataFromStats_(stats);
};
@ -74,7 +73,7 @@ class FileApiDriverLocal {
try {
const output = await basicDelta(path, getStatFn, options);
return output;
} catch(error) {
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
@ -89,7 +88,7 @@ class FileApiDriverLocal {
hasMore: false,
context: null,
};
} catch(error) {
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
@ -128,7 +127,7 @@ class FileApiDriverLocal {
// resolve();
// return;
// }
// fs.mkdirp(path, (error) => {
// if (error) {
// reject(this.fsErrorToJsError_(error));
@ -148,7 +147,7 @@ class FileApiDriverLocal {
await this.fsDriver().copy(options.path, path);
return;
}
await this.fsDriver().writeFile(path, content, 'utf8');
} catch (error) {
throw this.fsErrorToJsError_(error, path);
@ -200,7 +199,7 @@ class FileApiDriverLocal {
}
// let lastError = null;
// for (let i = 0; i < 5; i++) {
// try {
// let output = await fs.move(oldPath, newPath, { overwrite: true });
@ -228,7 +227,6 @@ class FileApiDriverLocal {
await this.fsDriver().remove(baseDir);
await this.fsDriver().mkdir(baseDir);
}
}
module.exports = { FileApiDriverLocal };
module.exports = { FileApiDriverLocal };

View File

@ -3,7 +3,6 @@ const fs = require('fs-extra');
const { basicDelta } = require('lib/file-api');
class FileApiDriverMemory {
constructor() {
this.items_ = [];
this.deletedItems_ = [];
@ -139,7 +138,7 @@ class FileApiDriverMemory {
}
async delta(path, options = null) {
const getStatFn = async (path) => {
const getStatFn = async path => {
let output = this.items_.slice();
for (let i = 0; i < output.length; i++) {
const item = Object.assign({}, output[i]);
@ -156,7 +155,6 @@ class FileApiDriverMemory {
async clearRoot() {
this.items_ = [];
}
}
module.exports = { FileApiDriverMemory };
module.exports = { FileApiDriverMemory };

View File

@ -4,7 +4,6 @@ const { dirname, basename } = require('lib/path-utils.js');
const { OneDriveApi } = require('lib/onedrive-api.js');
class FileApiDriverOneDrive {
constructor(api) {
this.api_ = api;
this.pathCache_ = {};
@ -17,7 +16,7 @@ class FileApiDriverOneDrive {
itemFilter_() {
return {
select: 'name,file,folder,fileSystemInfo,parentReference',
}
};
}
makePath_(path) {
@ -35,7 +34,7 @@ class FileApiDriverOneDrive {
makeItem_(odItem) {
let output = {
path: odItem.name,
isDir: ('folder' in odItem),
isDir: 'folder' in odItem,
};
if ('deleted' in odItem) {
@ -68,8 +67,12 @@ class FileApiDriverOneDrive {
async setTimestamp(path, timestamp) {
let body = {
fileSystemInfo: {
lastModifiedDateTime: moment.unix(timestamp / 1000).utc().format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z',
}
lastModifiedDateTime:
moment
.unix(timestamp / 1000)
.utc()
.format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z',
},
};
let item = await this.api_.execJson('PATCH', this.makePath_(path), null, body);
return this.makeItem_(item);
@ -89,8 +92,8 @@ class FileApiDriverOneDrive {
return {
hasMore: !!r['@odata.nextLink'],
items: this.makeItems_(r.value),
context: r["@odata.nextLink"],
}
context: r['@odata.nextLink'],
};
}
async get(path, options = null) {
@ -165,7 +168,7 @@ class FileApiDriverOneDrive {
let newName = basename(newPath);
// We don't want the modification date to change when we move the file so retrieve it
// now set it in the PATCH operation.
// now set it in the PATCH operation.
let item = await this.api_.execJson('PATCH', this.makePath_(oldPath), this.itemFilter_(), {
name: newName,
@ -205,10 +208,10 @@ class FileApiDriverOneDrive {
const query = this.itemFilter_();
query.select += ',deleted';
return { url: url, query: query };
}
};
const pathDetails = await this.pathDetails_(path);
const pathId = pathDetails.id;
const pathId = pathDetails.id;
let context = options ? options.context : null;
let url = context ? context.nextLink : null;
@ -231,7 +234,7 @@ class FileApiDriverOneDrive {
// The delta token has expired or is invalid and so a full resync is required. This happens for example when all the items
// on the OneDrive App folder are manually deleted. In this case, instead of sending the list of deleted items in the delta
// call, OneDrive simply request the client to re-sync everything.
// call, OneDrive simply request the client to re-sync everything.
// OneDrive provides a URL to resume syncing from but it does not appear to work so below we simply start over from
// the beginning. The synchronizer will ensure that no duplicate are created and conflicts will be resolved.
@ -254,7 +257,7 @@ class FileApiDriverOneDrive {
// is special since it's managed directly by the clients and resources never change - only the
// associated .md file at the root is synced). So in the loop below we check that the parent is
// indeed the root, otherwise the item is skipped.
// (Not sure but it's possible the delta API also returns events for files that are copied outside
// (Not sure but it's possible the delta API also returns events for files that are copied outside
// of the app directory and later deleted or modified. We also don't want to deal with
// these files during sync).
@ -294,7 +297,6 @@ class FileApiDriverOneDrive {
return output;
}
}
module.exports = { FileApiDriverOneDrive };
module.exports = { FileApiDriverOneDrive };

View File

@ -3,13 +3,12 @@ const { time } = require('lib/time-utils.js');
const { basicDelta } = require('lib/file-api');
const { rtrimSlashes, ltrimSlashes } = require('lib/path-utils.js');
const Entities = require('html-entities').AllHtmlEntities;
const html_entity_decode = (new Entities()).decode;
const html_entity_decode = new Entities().decode;
const { shim } = require('lib/shim');
const { basename } = require('lib/path-utils');
const JoplinError = require('lib/JoplinError');
class FileApiDriverWebDav {
class FileApiDriverWebDav {
constructor(api) {
this.api_ = api;
}
@ -24,10 +23,7 @@ class FileApiDriverWebDav {
async stat(path) {
try {
const result = await this.api().execPropFind(path, 0, [
'd:getlastmodified',
'd:resourcetype',
]);
const result = await this.api().execPropFind(path, 0, ['d:getlastmodified', 'd:resourcetype']);
const resource = this.api().objectFromJson(result, ['d:multistatus', 'd:response', 0]);
return this.statFromResource_(resource, path);
@ -80,7 +76,7 @@ class FileApiDriverWebDav {
}
async delta(path, options) {
const getDirStats = async (path) => {
const getDirStats = async path => {
const result = await this.list(path);
return result.items;
};
@ -133,7 +129,7 @@ class FileApiDriverWebDav {
// let currentStat = null;
// let currentText = '';
// // When this is on, the tags from the bloated XML string are replaced by shorter ones,
// // When this is on, the tags from the bloated XML string are replaced by shorter ones,
// // which makes parsing about 25% faster. However it's a bit of a hack so keep it as
// // an option so that it can be disabled if it causes problems.
// const optimizeXml = true;
@ -163,7 +159,7 @@ class FileApiDriverWebDav {
// saxParser.onclosetag = function(tagName) {
// tagName = tagName.toLowerCase();
// if (tagName === tagResponse) {
// if (currentStat.path) { // The list of resources includes the root dir too, which we don't want
// if (!currentStat.updated_time) throw new Error('Resource does not have a getlastmodified prop');
@ -202,12 +198,12 @@ class FileApiDriverWebDav {
// };
// if (optimizeXml) {
// xmlString = xmlString.replace(/<d:status>HTTP\/1\.1 200 OK<\/d:status>/ig, '');
// xmlString = xmlString.replace(/<d:resourcetype\/>/ig, '');
// xmlString = xmlString.replace(/d:getlastmodified/ig, tagGetLastModified);
// xmlString = xmlString.replace(/d:response/ig, tagResponse);
// xmlString = xmlString.replace(/d:propstat/ig, tagPropStat);
// if (replaceUrls) xmlString = xmlString.replace(new RegExp(relativeBaseUrl, 'gi'), '');
// xmlString = xmlString.replace(/<d:status>HTTP\/1\.1 200 OK<\/d:status>/ig, '');
// xmlString = xmlString.replace(/<d:resourcetype\/>/ig, '');
// xmlString = xmlString.replace(/d:getlastmodified/ig, tagGetLastModified);
// xmlString = xmlString.replace(/d:response/ig, tagResponse);
// xmlString = xmlString.replace(/d:propstat/ig, tagPropStat);
// if (replaceUrls) xmlString = xmlString.replace(new RegExp(relativeBaseUrl, 'gi'), '');
// }
// let idx = 0;
@ -228,7 +224,7 @@ class FileApiDriverWebDav {
// For performance reasons, the response of the PROPFIND call is manually parsed with a regex below
// instead of being processed by xml2json like the other WebDAV responses. This is over 2 times faster
// and it means the mobile app does not freeze during sync.
// and it means the mobile app does not freeze during sync.
// async function parsePropFindXml2(xmlString) {
// const regex = /<d:response>[\S\s]*?<d:href>([\S\s]*?)<\/d:href>[\S\s]*?<d:getlastmodified>(.*?)<\/d:getlastmodified>/g;
@ -270,10 +266,7 @@ class FileApiDriverWebDav {
// context: null,
// };
const result = await this.api().execPropFind(path, 1, [
'd:getlastmodified',
'd:resourcetype',
]);
const result = await this.api().execPropFind(path, 1, ['d:getlastmodified', 'd:resourcetype']);
const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']);
const stats = this.statsFromResources_(resources);
@ -320,7 +313,7 @@ class FileApiDriverWebDav {
const stat = await this.stat(path);
if (stat) return;
}
throw error;
}
}
@ -339,8 +332,8 @@ class FileApiDriverWebDav {
async move(oldPath, newPath) {
await this.api().exec('MOVE', oldPath, null, {
'Destination': this.api().baseUrl() + '/' + newPath,
'Overwrite': 'T',
Destination: this.api().baseUrl() + '/' + newPath,
Overwrite: 'T',
});
}
@ -352,7 +345,6 @@ class FileApiDriverWebDav {
await this.delete('');
await this.mkdir('');
}
}
module.exports = { FileApiDriverWebDav };

View File

@ -22,7 +22,7 @@ async function tryAndRepeat(fn, count) {
const shimFetchMaxRetryPrevious = shim.fetchMaxRetrySet(0);
const defer = () => {
shim.fetchMaxRetrySet(shimFetchMaxRetryPrevious);
}
};
while (true) {
try {
@ -37,11 +37,10 @@ async function tryAndRepeat(fn, count) {
retryCount++;
await time.sleep(1 + retryCount * 3);
}
}
}
}
class FileApi {
constructor(baseDir, driver) {
this.baseDir_ = baseDir;
this.driver_ = driver;
@ -165,9 +164,9 @@ class FileApi {
async put(path, content, options = null) {
this.logger().debug('put ' + this.fullPath_(path), options);
if (options && options.source === 'file') {
if (!await this.fsDriver().exists(options.path)) throw new JoplinError('File not found: ' + options.path, 'fileNotFound');
if (!(await this.fsDriver().exists(options.path))) throw new JoplinError('File not found: ' + options.path, 'fileNotFound');
}
return tryAndRepeat(() => this.driver_.put(this.fullPath_(path), content, options), this.requestRepeatCount());
@ -197,7 +196,6 @@ class FileApi {
this.logger().debug('delta ' + this.fullPath_(path));
return tryAndRepeat(() => this.driver_.delta(this.fullPath_(path), options), this.requestRepeatCount());
}
}
function basicDeltaContextFromOptions_(options) {
@ -217,7 +215,7 @@ function basicDeltaContextFromOptions_(options) {
output.filesAtTimestamp = Array.isArray(options.context.filesAtTimestamp) ? options.context.filesAtTimestamp.slice() : [];
output.statsCache = options.context && options.context.statsCache ? options.context.statsCache : null;
output.statIdsCache = options.context && options.context.statIdsCache ? options.context.statIdsCache : null;
output.deletedItemsProcessed = options.context && ('deletedItemsProcessed' in options.context) ? options.context.deletedItemsProcessed : false;
output.deletedItemsProcessed = options.context && 'deletedItemsProcessed' in options.context ? options.context.deletedItemsProcessed : false;
return output;
}
@ -246,9 +244,7 @@ async function basicDelta(path, getDirStatFn, options) {
newContext.statsCache.sort(function(a, b) {
return a.updated_time - b.updated_time;
});
newContext.statIdsCache = newContext.statsCache
.filter(item => BaseItem.isSystemPath(item.path))
.map(item => BaseItem.pathToId(item.path));
newContext.statIdsCache = newContext.statsCache.filter(item => BaseItem.isSystemPath(item.path)).map(item => BaseItem.pathToId(item.path));
newContext.statIdsCache.sort(); // Items must be sorted to use binary search below
}
@ -325,4 +321,4 @@ async function basicDelta(path, getDirStatFn, options) {
};
}
module.exports = { FileApi, basicDelta };
module.exports = { FileApi, basicDelta };

View File

@ -2,17 +2,22 @@ const Folder = require('lib/models/Folder.js');
const Setting = require('lib/models/Setting.js');
class FoldersScreenUtils {
static async allForDisplay(options = {}) {
const orderDir = Setting.value('folders.sortOrder.reverse') ? 'DESC' : 'ASC';
const folderOptions = Object.assign({}, {
caseInsensitive: true,
order: [{
by: 'title',
dir: orderDir,
}]
}, options);
const folderOptions = Object.assign(
{},
{
caseInsensitive: true,
order: [
{
by: 'title',
dir: orderDir,
},
],
},
options
);
let folders = await Folder.all(folderOptions);
@ -40,7 +45,6 @@ class FoldersScreenUtils {
this.refreshFolders();
}, 1000);
}
}
module.exports = { FoldersScreenUtils };
module.exports = { FoldersScreenUtils };

View File

@ -2,7 +2,6 @@ const { filename, fileExtension } = require('lib/path-utils');
const { time } = require('lib/time-utils.js');
class FsDriverBase {
async isDirectory(path) {
const stat = await this.stat(path);
return !stat ? false : stat.isDirectory();
@ -34,7 +33,7 @@ class FsDriverBase {
if (!exists) return nameToTry;
nameToTry = nameNoExt + ' (' + counter + ')' + extension;
counter++;
if (counter >= 1000) nameToTry = nameNoExt + ' (' + ((new Date()).getTime()) + ')' + extension;
if (counter >= 1000) nameToTry = nameNoExt + ' (' + new Date().getTime() + ')' + extension;
if (counter >= 10000) throw new Error('Cannot find unique title');
}
}
@ -61,7 +60,6 @@ class FsDriverBase {
await time.msleep(100);
}
}
}
module.exports = FsDriverBase;
module.exports = FsDriverBase;

View File

@ -1,10 +1,8 @@
class FsDriverDummy {
constructor() {}
appendFileSync(path, string) {}
writeBinaryFile(path, content) {}
readFile(path) {}
}
module.exports = { FsDriverDummy };
module.exports = { FsDriverDummy };

View File

@ -3,7 +3,6 @@ const { time } = require('lib/time-utils.js');
const FsDriverBase = require('lib/fs-driver-base');
class FsDriverNode extends FsDriverBase {
fsErrorToJsError_(error, path = null) {
let msg = error.toString();
if (path !== null) msg += '. Path: ' + path;
@ -183,7 +182,6 @@ class FsDriverNode extends FsDriverBase {
if (encoding === 'ascii') return buffer.toString('ascii');
throw new Error('Unsupported encoding: ' + encoding);
}
}
module.exports.FsDriverNode = FsDriverNode;
module.exports.FsDriverNode = FsDriverNode;

View File

@ -2,7 +2,6 @@ const RNFS = require('react-native-fs');
const FsDriverBase = require('lib/fs-driver-base');
class FsDriverRN extends FsDriverBase {
appendFileSync(path, string) {
throw new Error('Not implemented');
}
@ -32,13 +31,13 @@ class FsDriverRN extends FsDriverBase {
isDirectory: () => stat.isDirectory(),
path: path,
size: stat.size,
};
};
}
async readDirStats(path, options = null) {
if (!options) options = {};
if (!('recursive' in options)) options.recursive = false;
let items = await RNFS.readDir(path);
let output = [];
for (let i = 0; i < items.length; i++) {
@ -96,7 +95,7 @@ class FsDriverRN extends FsDriverBase {
offset: 0,
mode: mode,
stat: stat,
}
};
}
close(handle) {
@ -145,7 +144,6 @@ class FsDriverRN extends FsDriverBase {
handle.offset += length;
return output ? output : null;
}
}
module.exports.FsDriverRN = FsDriverRN;
module.exports.FsDriverRN = FsDriverRN;

View File

@ -2,29 +2,27 @@ const { shim } = require('lib/shim.js');
const { netUtils } = require('lib/net-utils.js');
class GeolocationNode {
static async currentPosition(options = null) {
if (!options) options = {};
const ip = await netUtils.ip();
let response = await shim.fetch('http://ip-api.com/json/' + ip);
if (!response.ok) throw new Error('Could not get geolocation: ' + await response.text());
if (!response.ok) throw new Error('Could not get geolocation: ' + (await response.text()));
response = await response.json();
if (!('lat' in response) || !('lon' in response)) throw new Error('Invalid geolocation response: ' + (response ? JSON.stringify(response) : '<null>'));
return {
timestamp: (new Date()).getTime(),
timestamp: new Date().getTime(),
coords: {
longitude: response.lon,
altitude: 0,
latitude: response.lat
}
}
latitude: response.lat,
},
};
}
}
module.exports = { GeolocationNode };
module.exports = { GeolocationNode };

View File

@ -1,20 +1,19 @@
const Setting = require('lib/models/Setting.js');
class GeolocationReact {
static currentPosition_testResponse() {
return {
mocked: false,
timestamp: (new Date()).getTime(),
timestamp: new Date().getTime(),
coords: {
speed: 0,
heading: 0,
accuracy: 20,
longitude: -3.4596633911132812,
altitude: 0,
latitude: 48.73219093634444
}
}
latitude: 48.73219093634444,
},
};
}
static currentPosition(options = null) {
@ -25,14 +24,17 @@ class GeolocationReact {
if (!('timeout' in options)) options.timeout = 10000;
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition((data) => {
resolve(data);
}, (error) => {
reject(error);
}, options);
navigator.geolocation.getCurrentPosition(
data => {
resolve(data);
},
error => {
reject(error);
},
options
);
});
}
}
module.exports = { GeolocationReact };
module.exports = { GeolocationReact };

View File

@ -1,14 +1,13 @@
const urlUtils = require('lib/urlUtils.js');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const htmlentities = new Entities().encode;
// [\s\S] instead of . for multiline matching
// https://stackoverflow.com/a/16119722/561309
const imageRegex = /<img([\s\S]*?)src=["']([\s\S]*?)["']([\s\S]*?)>/gi
const anchorRegex = /<a([\s\S]*?)href=["']([\s\S]*?)["']([\s\S]*?)>/gi
const imageRegex = /<img([\s\S]*?)src=["']([\s\S]*?)["']([\s\S]*?)>/gi;
const anchorRegex = /<a([\s\S]*?)href=["']([\s\S]*?)["']([\s\S]*?)>/gi;
class HtmlUtils {
headAndBodyHtml(doc) {
const output = [];
if (doc.head) output.push(doc.head.innerHTML);
@ -21,7 +20,7 @@ class HtmlUtils {
const output = [];
let matches;
while (matches = imageRegex.exec(html)) {
while ((matches = imageRegex.exec(html))) {
output.push(matches[2]);
}
@ -84,9 +83,8 @@ class HtmlUtils {
return output.join(' ');
}
}
const htmlUtils = new HtmlUtils();
module.exports = htmlUtils;
module.exports = htmlUtils;

View File

@ -1,11 +1,11 @@
const stringPadding = require('string-padding');
const stringToStream = require('string-to-stream')
const stringToStream = require('string-to-stream');
const BLOCK_OPEN = "[[BLOCK_OPEN]]";
const BLOCK_CLOSE = "[[BLOCK_CLOSE]]";
const NEWLINE = "[[NEWLINE]]";
const NEWLINE_MERGED = "[[MERGED]]";
const SPACE = "[[SPACE]]";
const BLOCK_OPEN = '[[BLOCK_OPEN]]';
const BLOCK_CLOSE = '[[BLOCK_CLOSE]]';
const NEWLINE = '[[NEWLINE]]';
const NEWLINE_MERGED = '[[MERGED]]';
const SPACE = '[[SPACE]]';
function processMdArrayNewLines(md) {
while (md.length && md[0] == BLOCK_OPEN) {
@ -18,7 +18,8 @@ function processMdArrayNewLines(md) {
let temp = [];
let last = '';
for (let i = 0; i < md.length; i++) { let v = md[i];
for (let i = 0; i < md.length; i++) {
let v = md[i];
if (isNewLineBlock(last) && isNewLineBlock(v) && last == v) {
// Skip it
} else {
@ -28,11 +29,10 @@ function processMdArrayNewLines(md) {
}
md = temp;
temp = [];
last = "";
for (let i = 0; i < md.length; i++) { let v = md[i];
last = '';
for (let i = 0; i < md.length; i++) {
let v = md[i];
if (last == BLOCK_CLOSE && v == BLOCK_OPEN) {
temp.pop();
temp.push(NEWLINE_MERGED);
@ -43,11 +43,10 @@ function processMdArrayNewLines(md) {
}
md = temp;
temp = [];
last = "";
for (let i = 0; i < md.length; i++) { let v = md[i];
last = '';
for (let i = 0; i < md.length; i++) {
let v = md[i];
if (last == NEWLINE && (v == NEWLINE_MERGED || v == BLOCK_CLOSE)) {
// Skip it
} else {
@ -57,12 +56,11 @@ function processMdArrayNewLines(md) {
}
md = temp;
// NEW!!!
temp = [];
last = "";
for (let i = 0; i < md.length; i++) { let v = md[i];
last = '';
for (let i = 0; i < md.length; i++) {
let v = md[i];
if (last == NEWLINE && (v == NEWLINE_MERGED || v == BLOCK_OPEN)) {
// Skip it
} else {
@ -72,9 +70,6 @@ function processMdArrayNewLines(md) {
}
md = temp;
if (md.length > 2) {
if (md[md.length - 2] == NEWLINE_MERGED && md[md.length - 1] == NEWLINE) {
md.pop();
@ -84,15 +79,16 @@ function processMdArrayNewLines(md) {
let output = '';
let previous = '';
let start = true;
for (let i = 0; i < md.length; i++) { let v = md[i];
for (let i = 0; i < md.length; i++) {
let v = md[i];
let add = '';
if (v == BLOCK_CLOSE || v == BLOCK_OPEN || v == NEWLINE || v == NEWLINE_MERGED) {
add = "\n";
add = '\n';
} else if (v == SPACE) {
if (previous == SPACE || previous == "\n" || start) {
if (previous == SPACE || previous == '\n' || start) {
continue; // skip
} else {
add = " ";
add = ' ';
}
} else {
add = v;
@ -121,10 +117,10 @@ function processMdArrayNewLines(md) {
output.push(line);
}
return output;
}
};
let lines = output.replace(/\\r/g, '').split('\n');
lines = formatMdLayout(lines)
lines = formatMdLayout(lines);
lines = mergeMultipleNewLines(lines);
return lines.join('\n');
}
@ -140,7 +136,7 @@ function processMdArrayNewLines(md) {
// <li>three</li>
//
// should result in this:
//
//
// - one
// - two
// - three
@ -152,9 +148,9 @@ function processMdArrayNewLines(md) {
// should result in this:
//
// Some long paragraph
//
//
// And another one
//
//
// And the last paragraph
//
// So in one case, one newline between tags, and in another two newlines. In HTML this would be done via CSS, but in Markdown we need
@ -162,37 +158,37 @@ function processMdArrayNewLines(md) {
// differently than if there's a newlines between them. So the function below parses the almost final MD and add new lines depending
// on various rules.
const isHeading = function(line) {
return !!line.match(/^#+\s/);
}
const isHeading = function(line) {
return !!line.match(/^#+\s/);
};
const isListItem = function(line) {
return line && line.trim().indexOf('- ') === 0;
}
const isListItem = function(line) {
return line && line.trim().indexOf('- ') === 0;
};
const isCodeLine = function(line) {
return line && line.indexOf('\t') === 0;
}
const isCodeLine = function(line) {
return line && line.indexOf('\t') === 0;
};
const isTableLine = function(line) {
return line.indexOf('| ') === 0;
}
const isTableLine = function(line) {
return line.indexOf('| ') === 0;
};
const isPlainParagraph = function(line) {
// Note: if a line is no longer than 80 characters, we don't consider it's a paragraph, which
// means no newlines will be added before or after. This is to handle text that has been
// written with "hard" new lines.
if (!line || line.length < 80) return false;
const isPlainParagraph = function(line) {
// Note: if a line is no longer than 80 characters, we don't consider it's a paragraph, which
// means no newlines will be added before or after. This is to handle text that has been
// written with "hard" new lines.
if (!line || line.length < 80) return false;
if (isListItem(line)) return false;
if (isHeading(line)) return false;
if (isCodeLine(line)) return false;
if (isTableLine(line)) return false;
if (isListItem(line)) return false;
if (isHeading(line)) return false;
if (isCodeLine(line)) return false;
if (isTableLine(line)) return false;
return true;
}
return true;
};
function formatMdLayout(lines) {
function formatMdLayout(lines) {
let previous = '';
let newLines = [];
for (let i = 0; i < lines.length; i++) {
@ -202,39 +198,35 @@ function formatMdLayout(lines) {
if (isListItem(previous) && line && !isListItem(line)) {
newLines.push('');
// Add a new line at the beginning of a list of items
// Add a new line at the beginning of a list of items
} else if (isListItem(line) && previous && !isListItem(previous)) {
newLines.push('');
// Add a new line before a heading
// Add a new line before a heading
} else if (isHeading(line) && previous) {
newLines.push('');
// Add a new line after a heading
// Add a new line after a heading
} else if (isHeading(previous) && line) {
newLines.push('');
} else if (isCodeLine(line) && !isCodeLine(previous)) {
newLines.push('');
} else if (!isCodeLine(line) && isCodeLine(previous)) {
newLines.push('');
} else if (isTableLine(line) && !isTableLine(previous)) {
newLines.push('');
} else if (!isTableLine(line) && isTableLine(previous)) {
newLines.push('');
// Add a new line at beginning of paragraph
// Add a new line at beginning of paragraph
} else if (isPlainParagraph(line) && previous) {
newLines.push('');
// Add a new line at end of paragraph
// Add a new line at end of paragraph
} else if (isPlainParagraph(previous) && line) {
newLines.push('');
}
newLines.push(line);
previous = newLines[newLines.length - 1];
}
@ -273,8 +265,8 @@ function collapseWhiteSpaceAndAppend(lines, state, text) {
lines.push(text);
} else {
// Remove all \n and \r from the left and right of the text
while (text.length && (text[0] == "\n" || text[0] == "\r")) text = text.substr(1);
while (text.length && (text[text.length - 1] == "\n" || text[text.length - 1] == "\r")) text = text.substr(0, text.length - 1);
while (text.length && (text[0] == '\n' || text[0] == '\r')) text = text.substr(1);
while (text.length && (text[text.length - 1] == '\n' || text[text.length - 1] == '\r')) text = text.substr(0, text.length - 1);
// Collapse all white spaces to just one. If there are spaces to the left and right of the string
// also collapse them to just one space.
@ -282,7 +274,7 @@ function collapseWhiteSpaceAndAppend(lines, state, text) {
let spaceRight = text.length && text[text.length - 1] == ' ';
text = simplifyString(text);
if (!spaceLeft && !spaceRight && text == "") return lines;
if (!spaceLeft && !spaceRight && text == '') return lines;
if (state.inQuote) {
// Add a ">" at the beginning of the block then at the beginning of each lines. So it turns this:
@ -303,7 +295,7 @@ function collapseWhiteSpaceAndAppend(lines, state, text) {
return lines;
}
const imageMimeTypes = ["image/cgm", "image/fits", "image/g3fax", "image/gif", "image/ief", "image/jp2", "image/jpeg", "image/jpm", "image/jpx", "image/naplps", "image/png", "image/prs.btif", "image/prs.pti", "image/t38", "image/tiff", "image/tiff-fx", "image/vnd.adobe.photoshop", "image/vnd.cns.inf2", "image/vnd.djvu", "image/vnd.dwg", "image/vnd.dxf", "image/vnd.fastbidsheet", "image/vnd.fpx", "image/vnd.fst", "image/vnd.fujixerox.edmics-mmr", "image/vnd.fujixerox.edmics-rlc", "image/vnd.globalgraphics.pgb", "image/vnd.microsoft.icon", "image/vnd.mix", "image/vnd.ms-modi", "image/vnd.net-fpx", "image/vnd.sealed.png", "image/vnd.sealedmedia.softseal.gif", "image/vnd.sealedmedia.softseal.jpg", "image/vnd.svf", "image/vnd.wap.wbmp", "image/vnd.xiff"];
const imageMimeTypes = ['image/cgm', 'image/fits', 'image/g3fax', 'image/gif', 'image/ief', 'image/jp2', 'image/jpeg', 'image/jpm', 'image/jpx', 'image/naplps', 'image/png', 'image/prs.btif', 'image/prs.pti', 'image/t38', 'image/tiff', 'image/tiff-fx', 'image/vnd.adobe.photoshop', 'image/vnd.cns.inf2', 'image/vnd.djvu', 'image/vnd.dwg', 'image/vnd.dxf', 'image/vnd.fastbidsheet', 'image/vnd.fpx', 'image/vnd.fst', 'image/vnd.fujixerox.edmics-mmr', 'image/vnd.fujixerox.edmics-rlc', 'image/vnd.globalgraphics.pgb', 'image/vnd.microsoft.icon', 'image/vnd.mix', 'image/vnd.ms-modi', 'image/vnd.net-fpx', 'image/vnd.sealed.png', 'image/vnd.sealedmedia.softseal.gif', 'image/vnd.sealedmedia.softseal.jpg', 'image/vnd.svf', 'image/vnd.wap.wbmp', 'image/vnd.xiff'];
function isImageMimeType(m) {
return imageMimeTypes.indexOf(m) >= 0;
@ -318,7 +310,7 @@ function tagAttributeToMdText(attr) {
return attr;
}
function addResourceTag(lines, resource, alt = "") {
function addResourceTag(lines, resource, alt = '') {
// Note: refactor to use Resource.markdownTag
if (!alt) alt = resource.title;
@ -327,50 +319,49 @@ function addResourceTag(lines, resource, alt = "") {
alt = tagAttributeToMdText(alt);
if (isImageMimeType(resource.mime)) {
lines.push("![");
lines.push('![');
lines.push(alt);
lines.push("](:/" + resource.id + ")");
lines.push('](:/' + resource.id + ')');
} else {
lines.push("[");
lines.push('[');
lines.push(alt);
lines.push("](:/" + resource.id + ")");
lines.push('](:/' + resource.id + ')');
}
return lines;
}
function isBlockTag(n) {
return ["div", "p", "dl", "dd", 'dt', "center", 'address'].indexOf(n) >= 0;
return ['div', 'p', 'dl', 'dd', 'dt', 'center', 'address'].indexOf(n) >= 0;
}
function isStrongTag(n) {
return n == "strong" || n == "b" || n == 'big';
return n == 'strong' || n == 'b' || n == 'big';
}
function isStrikeTag(n) {
return n == "strike" || n == "s" || n == 'del';
return n == 'strike' || n == 's' || n == 'del';
}
function isEmTag(n) {
return n == "em" || n == "i" || n == "u";
return n == 'em' || n == 'i' || n == 'u';
}
function isAnchor(n) {
return n == "a";
return n == 'a';
}
function isIgnoredEndTag(n) {
return ["en-note", "en-todo", "body", "html", "font", "br", 'hr', 'tbody', 'sup', 'img', 'abbr', 'cite', 'thead', 'small', 'tt', 'sub', 'colgroup', 'col', 'ins', 'caption', 'var', 'map', 'area'].indexOf(n) >= 0;
return ['en-note', 'en-todo', 'body', 'html', 'font', 'br', 'hr', 'tbody', 'sup', 'img', 'abbr', 'cite', 'thead', 'small', 'tt', 'sub', 'colgroup', 'col', 'ins', 'caption', 'var', 'map', 'area'].indexOf(n) >= 0;
}
function isListTag(n) {
return n == "ol" || n == "ul";
return n == 'ol' || n == 'ul';
}
// Elements that don't require any special treatment beside adding a newline character
function isNewLineOnlyEndTag(n) {
return ["div", "p", "li", "h1", "h2", "h3", "h4", "h5", 'h6', "dl", "dd", 'dt', "center", 'address'].indexOf(n) >= 0;
return ['div', 'p', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'dl', 'dd', 'dt', 'center', 'address'].indexOf(n) >= 0;
}
function isInlineCodeTag(n) {
@ -410,7 +401,7 @@ function isSpanStyleBold(attributes) {
let style = attributes.style;
if (style.includes('font-weight: bold;')) {
return true;
} else if (style.search( /font-family:.*,Bold.*;/ ) != -1) {
} else if (style.search(/font-family:.*,Bold.*;/) != -1) {
//console.debug('font-family regex matched');
return true;
} else {
@ -422,14 +413,14 @@ function isSpanStyleBold(attributes) {
function enexXmlToMdArray(stream, resources) {
let remainingResources = resources.slice();
const removeRemainingResource = (id) => {
const removeRemainingResource = id => {
for (let i = 0; i < remainingResources.length; i++) {
const r = remainingResources[i];
if (r.id === id) {
remainingResources.splice(i, 1);
}
}
}
};
return new Promise((resolve, reject) => {
let state = {
@ -443,7 +434,7 @@ function enexXmlToMdArray(stream, resources) {
let options = {};
let strict = false;
var saxStream = require('sax').createStream(strict, options)
var saxStream = require('sax').createStream(strict, options);
let section = {
type: 'text',
@ -453,8 +444,8 @@ function enexXmlToMdArray(stream, resources) {
saxStream.on('error', function(e) {
console.warn(e);
//reject(e);
})
//reject(e);
});
const unwrapInnerText = text => {
const lines = text.split('\n');
@ -463,7 +454,7 @@ function enexXmlToMdArray(stream, resources) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const nextLine = i < lines.length - 1 ? lines[i+1] : '';
const nextLine = i < lines.length - 1 ? lines[i + 1] : '';
if (!line) {
output += '\n';
@ -478,14 +469,14 @@ function enexXmlToMdArray(stream, resources) {
}
return output;
}
};
saxStream.on('text', function(text) {
if (['table', 'tr', 'tbody'].indexOf(section.type) >= 0) return;
text = !state.inPre ? unwrapInnerText(text) : text;
section.lines = collapseWhiteSpaceAndAppend(section.lines, state, text);
})
});
saxStream.on('opentag', function(node) {
const nodeAttributes = attributeToLowerCase(node);
@ -527,7 +518,7 @@ function enexXmlToMdArray(stream, resources) {
lines: [],
parent: section,
isHeader: false,
}
};
section.lines.push(newSection);
section = newSection;
@ -553,7 +544,7 @@ function enexXmlToMdArray(stream, resources) {
} else if (n == 'li') {
section.lines.push(BLOCK_OPEN);
if (!state.lists.length) {
console.warn("Found <li> tag without being inside a list");
console.warn('Found <li> tag without being inside a list');
return;
}
@ -561,14 +552,14 @@ function enexXmlToMdArray(stream, resources) {
container.startedText = false;
const indent = ' '.repeat(state.lists.length - 1);
if (container.tag == "ul") {
section.lines.push(indent + "- ");
if (container.tag == 'ul') {
section.lines.push(indent + '- ');
} else {
section.lines.push(indent + container.counter + '. ');
container.counter++;
}
} else if (isStrongTag(n)) {
section.lines.push("**");
section.lines.push('**');
} else if (isStrikeTag(n)) {
section.lines.push('(');
} else if (isInlineCodeTag(n)) {
@ -576,7 +567,8 @@ function enexXmlToMdArray(stream, resources) {
} else if (n == 'q') {
section.lines.push('"');
} else if (n == 'img') {
if (nodeAttributes.src) { // Many (most?) img tags don't have no source associated, especially when they were imported from HTML
if (nodeAttributes.src) {
// Many (most?) img tags don't have no source associated, especially when they were imported from HTML
let s = '![';
if (nodeAttributes.alt) s += tagAttributeToMdText(nodeAttributes.alt);
s += '](' + nodeAttributes.src + ')';
@ -588,28 +580,34 @@ function enexXmlToMdArray(stream, resources) {
// are handled correctly.
collapseWhiteSpaceAndAppend(section.lines, state, '[');
} else if (isEmTag(n)) {
section.lines.push("*");
} else if (n == "en-todo") {
section.lines.push('*');
} else if (n == 'en-todo') {
let x = nodeAttributes && nodeAttributes.checked && nodeAttributes.checked.toLowerCase() == 'true' ? 'X' : ' ';
section.lines.push('- [' + x + '] ');
} else if (n == "hr") {
} else if (n == 'hr') {
// Needs to be surrounded by new lines so that it's properly rendered as a line when converting to HTML
section.lines.push(NEWLINE);
section.lines.push('* * *');
section.lines.push(NEWLINE);
section.lines.push(NEWLINE);
} else if (n == "h1") {
section.lines.push(BLOCK_OPEN); section.lines.push("# ");
} else if (n == "h2") {
section.lines.push(BLOCK_OPEN); section.lines.push("## ");
} else if (n == "h3") {
section.lines.push(BLOCK_OPEN); section.lines.push("### ");
} else if (n == "h4") {
section.lines.push(BLOCK_OPEN); section.lines.push("#### ");
} else if (n == "h5") {
section.lines.push(BLOCK_OPEN); section.lines.push("##### ");
} else if (n == "h6") {
section.lines.push(BLOCK_OPEN); section.lines.push("###### ");
} else if (n == 'h1') {
section.lines.push(BLOCK_OPEN);
section.lines.push('# ');
} else if (n == 'h2') {
section.lines.push(BLOCK_OPEN);
section.lines.push('## ');
} else if (n == 'h3') {
section.lines.push(BLOCK_OPEN);
section.lines.push('### ');
} else if (n == 'h4') {
section.lines.push(BLOCK_OPEN);
section.lines.push('#### ');
} else if (n == 'h5') {
section.lines.push(BLOCK_OPEN);
section.lines.push('##### ');
} else if (n == 'h6') {
section.lines.push(BLOCK_OPEN);
section.lines.push('###### ');
} else if (n == 'blockquote') {
section.lines.push(BLOCK_OPEN);
state.inQuote = true;
@ -621,16 +619,16 @@ function enexXmlToMdArray(stream, resources) {
type: 'code',
lines: [],
parent: section,
}
};
section.lines.push(newSection);
section = newSection;
} else if (n === 'pre') {
section.lines.push(BLOCK_OPEN);
state.inPre = true;
} else if (n == "br") {
} else if (n == 'br') {
section.lines.push(NEWLINE);
} else if (n == "en-media") {
} else if (n == 'en-media') {
const hash = nodeAttributes.hash;
let resource = null;
@ -705,20 +703,20 @@ function enexXmlToMdArray(stream, resources) {
if (resource && !!resource.id) {
section.lines = addResourceTag(section.lines, resource, nodeAttributes.alt);
}
} else if (n == "span") {
} else if (n == 'span') {
if (isSpanWithStyle(nodeAttributes)) {
state.spanAttributes.push(nodeAttributes);
if (isSpanStyleBold(nodeAttributes)) {
//console.debug('Applying style found in span tag: bold')
section.lines.push("**");
section.lines.push('**');
}
}
} else if (["font", 'sup', 'cite', 'abbr', 'small', 'tt', 'sub', 'colgroup', 'col', 'ins', 'caption', 'var', 'map', 'area'].indexOf(n) >= 0) {
} else if (['font', 'sup', 'cite', 'abbr', 'small', 'tt', 'sub', 'colgroup', 'col', 'ins', 'caption', 'var', 'map', 'area'].indexOf(n) >= 0) {
// Inline tags that can be ignored in Markdown
} else {
console.warn("Unsupported start tag: " + n);
console.warn('Unsupported start tag: ' + n);
}
})
});
saxStream.on('closetag', function(n) {
n = n ? n.toLowerCase() : n;
@ -739,13 +737,13 @@ function enexXmlToMdArray(stream, resources) {
section.lines.push(BLOCK_CLOSE);
state.lists.pop();
} else if (isStrongTag(n)) {
section.lines.push("**");
section.lines.push('**');
} else if (isStrikeTag(n)) {
section.lines.push(')');
} else if (isInlineCodeTag(n)) {
section.lines.push('`');
} else if (isEmTag(n)) {
section.lines.push("*");
section.lines.push('*');
} else if (n == 'q') {
section.lines.push('"');
} else if (n == 'blockquote') {
@ -847,10 +845,9 @@ function enexXmlToMdArray(stream, resources) {
for (let i = section.lines.length - 1; i >= 0; i--) {
const c = section.lines.pop();
if (c === '[') break;
}
}
section.lines.push(url);
} else {
// Eg. converts:
// [ Sign in ](https://example.com)
// to:
@ -874,7 +871,7 @@ function enexXmlToMdArray(stream, resources) {
for (let i = firstBracketIndex + 1; i < lines.length; i++) {
const l = lines[i];
if (l === SPACE || l === ' ' ||!l) {
if (l === SPACE || l === ' ' || !l) {
lines.splice(i, 1);
} else {
break;
@ -882,7 +879,7 @@ function enexXmlToMdArray(stream, resources) {
}
return lines;
}
};
section.lines = trimTextStartAndEndSpaces(section.lines);
@ -892,34 +889,31 @@ function enexXmlToMdArray(stream, resources) {
} else if (isListTag(n)) {
section.lines.push(BLOCK_CLOSE);
state.lists.pop();
} else if (n == "en-media") {
} else if (n == 'en-media') {
// Skip
} else if (n == 'span') {
let attributes = state.spanAttributes.pop();
if (isSpanWithStyle(attributes)) {
if (isSpanStyleBold(attributes)) {
//console.debug('Applying style found in span tag (closing): bold')
section.lines.push("**");
section.lines.push('**');
}
}
} else if (isIgnoredEndTag(n)) {
// Skip
} else {
console.warn("Unsupported end tag: " + n);
console.warn('Unsupported end tag: ' + n);
}
});
})
saxStream.on('attribute', function(attr) {
})
saxStream.on('attribute', function(attr) {});
saxStream.on('end', function() {
resolve({
content: section,
resources: remainingResources,
});
})
});
stream.pipe(saxStream);
});
@ -929,7 +923,7 @@ function tableHasSubTables(table) {
for (let trIndex = 0; trIndex < table.lines.length; trIndex++) {
const tr = table.lines[trIndex];
if (!tr || !tr.lines) continue;
for (let tdIndex = 0; tdIndex < tr.lines.length; tdIndex++) {
const td = tr.lines[tdIndex];
for (let i = 0; i < td.lines.length; i++) {
@ -976,15 +970,17 @@ function drawTable(table) {
const cellText = processMdArrayNewLines(currentCells);
line.push(cellText);
currentCells = [];
}
};
// In here, recursively render the tables
for (let i = 0; i < td.lines.length; i++) {
const c = td.lines[i];
if (typeof c === 'object' && ['table', 'td', 'tr', 'th'].indexOf(c.type) >= 0) { // This is a table
if (typeof c === 'object' && ['table', 'td', 'tr', 'th'].indexOf(c.type) >= 0) {
// This is a table
renderCurrentCells();
currentCells = currentCells.concat(drawTable(c));
} else { // This is plain text
} else {
// This is plain text
currentCells.push(c);
}
}
@ -992,17 +988,18 @@ function drawTable(table) {
renderCurrentCells();
line.push(BLOCK_CLOSE);
} else { // Regular table rendering
} else {
// Regular table rendering
// A cell in a Markdown table cannot have actual new lines so replace
// them with <br>, which are supported by the markdown renderers.
let cellText = processMdArrayNewLines(td.lines, true)
let cellText = processMdArrayNewLines(td.lines, true);
let lines = cellText.split('\n');
lines = postProcessMarkdown(lines);
cellText = lines.join('\n').replace(/\n+/g, "<br>");
cellText = lines.join('\n').replace(/\n+/g, '<br>');
// Inside tables cells, "|" needs to be escaped
cellText = cellText.replace(/\|/g, "\\|");
cellText = cellText.replace(/\|/g, '\\|');
// Previously the width of the cell was as big as the content since it looks nicer, however that often doesn't work
// since the content can be very long, resulting in unreadable markdown. So no solution is perfect but making it a
@ -1019,7 +1016,6 @@ function drawTable(table) {
}
headerLine.push('-'.repeat(width));
}
}
}
@ -1072,7 +1068,7 @@ function postProcessMarkdown(lines) {
}
return lines;
}
};
function cleanUpSpaces(lines) {
const output = [];
@ -1083,10 +1079,10 @@ function postProcessMarkdown(lines) {
if (line.length) {
// eg. " - Some list item" => " - Some list item"
// Note that spaces before the "-" are preserved
line = line.replace(/^(\s+|)-\s+/, '$1- ')
line = line.replace(/^(\s+|)-\s+/, '$1- ');
// eg "Some text " => "Some text"
line = line.replace(/^(.*?)\s+$/, '$1')
line = line.replace(/^(.*?)\s+$/, '$1');
}
output.push(line);
@ -1095,8 +1091,8 @@ function postProcessMarkdown(lines) {
return output;
}
lines = trimEmptyLines(lines)
lines = cleanUpSpaces(lines)
lines = trimEmptyLines(lines);
lines = cleanUpSpaces(lines);
return lines;
}
@ -1109,7 +1105,8 @@ async function enexXmlToMd(xmlString, resources, options = {}) {
for (let i = 0; i < result.content.lines.length; i++) {
let line = result.content.lines[i];
if (typeof line === 'object' && line.type === 'table') { // A table
if (typeof line === 'object' && line.type === 'table') {
// A table
const table = line;
const tableLines = drawTable(table);
mdLines = mdLines.concat(tableLines);
@ -1118,7 +1115,8 @@ async function enexXmlToMd(xmlString, resources, options = {}) {
} else if (typeof line === 'object') {
console.warn('Unhandled object type:', line);
mdLines = mdLines.concat(line.lines);
} else { // an actual line
} else {
// an actual line
mdLines.push(line);
}
}
@ -1132,11 +1130,11 @@ async function enexXmlToMd(xmlString, resources, options = {}) {
firstAttachment = false;
}
let output = processMdArrayNewLines(mdLines).split('\n')
let output = processMdArrayNewLines(mdLines).split('\n');
output = postProcessMarkdown(output);
return output.join('\n');
}
module.exports = { enexXmlToMd, processMdArrayNewLines, NEWLINE, addResourceTag };
module.exports = { enexXmlToMd, processMdArrayNewLines, NEWLINE, addResourceTag };

View File

@ -10,7 +10,7 @@ const Folder = require('lib/models/Folder.js');
const { enexXmlToMd } = require('./import-enex-md-gen.js');
const { time } = require('lib/time-utils.js');
const Levenshtein = require('levenshtein');
const jsSHA = require("jssha");
const jsSHA = require('jssha');
const md5 = require('md5');
//const Promise = require('promise');
@ -53,9 +53,9 @@ function removeUndefinedProperties(note) {
}
function createNoteId(note) {
let shaObj = new jsSHA("SHA-256", "TEXT");
shaObj.update(note.title + '_' + note.body + "_" + note.created_time + "_" + note.updated_time + "_");
let hash = shaObj.getHash("HEX");
let shaObj = new jsSHA('SHA-256', 'TEXT');
shaObj.update(note.title + '_' + note.body + '_' + note.created_time + '_' + note.updated_time + '_');
let hash = shaObj.getHash('HEX');
return hash.substr(0, 32);
}
@ -106,7 +106,7 @@ async function saveNoteResources(note) {
let existingResource = await Resource.load(toSave.id);
if (existingResource) continue;
await filePutContents(Resource.fullPath(toSave), resource.data)
await filePutContents(Resource.fullPath(toSave), resource.data);
await Resource.save(toSave, { isNew: true });
resourcesCreated++;
}
@ -160,7 +160,7 @@ async function saveNoteToStorage(note, fuzzyMatching = false) {
diff.id = existingNote.id;
diff.type_ = existingNote.type_;
await Note.save(diff, { autoTimestamp: false })
await Note.save(diff, { autoTimestamp: false });
result.noteUpdated = true;
} else {
await Note.save(note, {
@ -204,7 +204,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
let notes = [];
let processingNotes = false;
stream.on('error', (error) => {
stream.on('error', error => {
reject(new Error(error.toString()));
});
@ -256,7 +256,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
progressState.notesTagged += result.notesTagged;
importOptions.onProgress(progressState);
}
} catch(error) {
} catch (error) {
console.error(error);
}
@ -265,7 +265,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
return true;
}
saxStream.on('error', (error) => {
saxStream.on('error', error => {
importOptions.onError(error);
});
@ -275,7 +275,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
if (noteAttributes) {
noteAttributes[n] = text;
} else if (noteResourceAttributes) {
noteResourceAttributes[n] = text;
noteResourceAttributes[n] = text;
} else if (noteResource) {
if (n == 'data') {
let attr = currentNodeAttributes();
@ -300,7 +300,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
console.warn('Unsupported note tag: ' + n);
}
}
})
});
saxStream.on('opentag', function(node) {
let n = node.name.toLowerCase();
@ -350,7 +350,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
notes.push(note);
if (notes.length >= 10) {
processNotes().catch((error) => {
processNotes().catch(error => {
importOptions.onError(error);
});
}
@ -371,8 +371,8 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
note.todo_due = dateToTimestamp(noteAttributes['reminder-time'], true);
note.todo_completed = dateToTimestamp(noteAttributes['reminder-done-time'], true);
note.order = dateToTimestamp(noteAttributes['reminder-order'], true);
note.source = !!noteAttributes.source ? 'evernote.' + noteAttributes.source : 'evernote';
note.source_url = !!noteAttributes['source-url'] ? noteAttributes['source-url'] : '';
note.source = noteAttributes.source ? 'evernote.' + noteAttributes.source : 'evernote';
note.source_url = noteAttributes['source-url'] ? noteAttributes['source-url'] : '';
// if (noteAttributes['reminder-time']) {
// console.info('======================================================');
@ -432,7 +432,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
saxStream.on('end', function() {
// Wait till there is no more notes to process.
let iid = setInterval(() => {
processNotes().then((allDone) => {
processNotes().then(allDone => {
if (allDone) {
clearTimeout(iid);
resolve();
@ -445,4 +445,4 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
});
}
module.exports = { importEnex };
module.exports = { importEnex };

View File

@ -120,7 +120,6 @@ INSERT INTO version (version) VALUES (1);
`;
class JoplinDatabase extends Database {
constructor(driver) {
super(driver);
this.initialized_ = false;
@ -194,7 +193,7 @@ class JoplinDatabase extends Database {
queries.push('DELETE FROM settings WHERE key="sync.5.context"');
queries.push('DELETE FROM settings WHERE key="sync.6.context"');
queries.push('DELETE FROM settings WHERE key="sync.7.context"');
queries.push('DELETE FROM settings WHERE key="revisionService.lastProcessedChangeId"');
queries.push('DELETE FROM settings WHERE key="resourceService.lastProcessedChangeId"');
queries.push('DELETE FROM settings WHERE key="searchEngine.lastProcessedChangeId"');
@ -212,7 +211,7 @@ class JoplinDatabase extends Database {
return row;
}
fieldDescription(tableName, fieldName) {
fieldDescription(tableName, fieldName) {
const sp = sprintf;
if (!this.tableDescriptions_) {
@ -253,39 +252,41 @@ class JoplinDatabase extends Database {
let queries = [];
queries.push(this.wrapQuery('DELETE FROM table_fields'));
return this.selectAll('SELECT name FROM sqlite_master WHERE type="table"').then((tableRows) => {
let chain = [];
for (let i = 0; i < tableRows.length; i++) {
let tableName = tableRows[i].name;
if (tableName == 'android_metadata') continue;
if (tableName == 'table_fields') continue;
if (tableName == 'sqlite_sequence') continue;
if (tableName.indexOf('notes_fts') === 0) continue;
chain.push(() => {
return this.selectAll('PRAGMA table_info("' + tableName + '")').then((pragmas) => {
for (let i = 0; i < pragmas.length; i++) {
let item = pragmas[i];
// In SQLite, if the default value is a string it has double quotes around it, so remove them here
let defaultValue = item.dflt_value;
if (typeof defaultValue == 'string' && defaultValue.length >= 2 && defaultValue[0] == '"' && defaultValue[defaultValue.length - 1] == '"') {
defaultValue = defaultValue.substr(1, defaultValue.length - 2);
return this.selectAll('SELECT name FROM sqlite_master WHERE type="table"')
.then(tableRows => {
let chain = [];
for (let i = 0; i < tableRows.length; i++) {
let tableName = tableRows[i].name;
if (tableName == 'android_metadata') continue;
if (tableName == 'table_fields') continue;
if (tableName == 'sqlite_sequence') continue;
if (tableName.indexOf('notes_fts') === 0) continue;
chain.push(() => {
return this.selectAll('PRAGMA table_info("' + tableName + '")').then(pragmas => {
for (let i = 0; i < pragmas.length; i++) {
let item = pragmas[i];
// In SQLite, if the default value is a string it has double quotes around it, so remove them here
let defaultValue = item.dflt_value;
if (typeof defaultValue == 'string' && defaultValue.length >= 2 && defaultValue[0] == '"' && defaultValue[defaultValue.length - 1] == '"') {
defaultValue = defaultValue.substr(1, defaultValue.length - 2);
}
let q = Database.insertQuery('table_fields', {
table_name: tableName,
field_name: item.name,
field_type: Database.enumId('fieldType', item.type),
field_default: defaultValue,
});
queries.push(q);
}
let q = Database.insertQuery('table_fields', {
table_name: tableName,
field_name: item.name,
field_type: Database.enumId('fieldType', item.type),
field_default: defaultValue,
});
queries.push(q);
}
});
});
});
}
}
return promiseChain(chain);
}).then(() => {
return this.transactionExecBatch(queries);
});
return promiseChain(chain);
})
.then(() => {
return this.transactionExecBatch(queries);
});
}
async upgradeDatabase(fromVersion) {
@ -296,7 +297,7 @@ class JoplinDatabase extends Database {
// IMPORTANT:
//
// Whenever adding a new database property, some additional logic might be needed
// Whenever adding a new database property, some additional logic might be needed
// in the synchronizer to handle this property. For example, when adding a property
// that should have a default value, existing remote items will not have this
// default value and thus might cause problems. In that case, the default value
@ -317,14 +318,14 @@ class JoplinDatabase extends Database {
while (currentVersionIndex < existingDatabaseVersions.length - 1) {
const targetVersion = existingDatabaseVersions[currentVersionIndex + 1];
this.logger().info("Converting database to version " + targetVersion);
this.logger().info('Converting database to version ' + targetVersion);
let queries = [];
if (targetVersion == 1) {
queries = this.wrapQueries(this.sqlStringToLines(structureSql));
}
if (targetVersion == 2) {
const newTableSql = `
CREATE TABLE deleted_items (
@ -338,7 +339,7 @@ class JoplinDatabase extends Database {
queries.push({ sql: 'DROP TABLE deleted_items' });
queries.push({ sql: this.sqlStringToLines(newTableSql)[0] });
queries.push({ sql: "CREATE INDEX deleted_items_sync_target ON deleted_items (sync_target)" });
queries.push({ sql: 'CREATE INDEX deleted_items_sync_target ON deleted_items (sync_target)' });
}
if (targetVersion == 3) {
@ -432,7 +433,7 @@ class JoplinDatabase extends Database {
queries.push('CREATE INDEX note_resources_resource_id ON note_resources (resource_id)');
queries.push({ sql: 'INSERT INTO item_changes (item_type, item_id, type, created_time) SELECT 1, id, 1, ? FROM notes', params: [Date.now()] });
}
};
if (targetVersion == 10) {
upgradeVersion10();
@ -474,20 +475,22 @@ class JoplinDatabase extends Database {
queries.push('CREATE INDEX resource_local_states_resource_id ON resource_local_states (resource_id)');
queries.push('CREATE INDEX resource_local_states_resource_fetch_status ON resource_local_states (fetch_status)');
queries = queries.concat(this.alterColumnQueries('resources', {
id: 'TEXT PRIMARY KEY',
title: 'TEXT NOT NULL DEFAULT ""',
mime: 'TEXT NOT NULL',
filename: 'TEXT NOT NULL DEFAULT ""',
created_time: 'INT NOT NULL',
updated_time: 'INT NOT NULL',
user_created_time: 'INT NOT NULL DEFAULT 0',
user_updated_time: 'INT NOT NULL DEFAULT 0',
file_extension: 'TEXT NOT NULL DEFAULT ""',
encryption_cipher_text: 'TEXT NOT NULL DEFAULT ""',
encryption_applied: 'INT NOT NULL DEFAULT 0',
encryption_blob_encrypted: 'INT NOT NULL DEFAULT 0',
}));
queries = queries.concat(
this.alterColumnQueries('resources', {
id: 'TEXT PRIMARY KEY',
title: 'TEXT NOT NULL DEFAULT ""',
mime: 'TEXT NOT NULL',
filename: 'TEXT NOT NULL DEFAULT ""',
created_time: 'INT NOT NULL',
updated_time: 'INT NOT NULL',
user_created_time: 'INT NOT NULL DEFAULT 0',
user_updated_time: 'INT NOT NULL DEFAULT 0',
file_extension: 'TEXT NOT NULL DEFAULT ""',
encryption_cipher_text: 'TEXT NOT NULL DEFAULT ""',
encryption_applied: 'INT NOT NULL DEFAULT 0',
encryption_blob_encrypted: 'INT NOT NULL DEFAULT 0',
})
);
}
if (targetVersion == 15) {
@ -651,7 +654,7 @@ class JoplinDatabase extends Database {
}
latestVersion = targetVersion;
currentVersionIndex++;
}
@ -683,7 +686,7 @@ class JoplinDatabase extends Database {
// Will throw if the database has not been created yet, but this is handled below
versionRow = await this.selectOne('SELECT * FROM version LIMIT 1');
} catch (error) {
if (error.message && (error.message.indexOf('no such table: version') >= 0)) {
if (error.message && error.message.indexOf('no such table: version') >= 0) {
// Ignore
} else {
console.info(error);
@ -712,11 +715,10 @@ class JoplinDatabase extends Database {
});
}
}
}
Database.TYPE_INT = 1;
Database.TYPE_TEXT = 2;
Database.TYPE_NUMERIC = 3;
module.exports = { JoplinDatabase };
module.exports = { JoplinDatabase };

View File

@ -4,6 +4,6 @@ layoutUtils.size = function(preferred, min, max) {
if (preferred < min) return min;
if (typeof max !== 'undefined' && preferred > max) return max;
return preferred;
}
};
module.exports = layoutUtils;
module.exports = layoutUtils;

View File

@ -1,182 +1,182 @@
const { sprintf } = require('sprintf-js');
let codeToLanguageE_ = {};
codeToLanguageE_["aa"] = "Afar";
codeToLanguageE_["ab"] = "Abkhazian";
codeToLanguageE_["af"] = "Afrikaans";
codeToLanguageE_["am"] = "Amharic";
codeToLanguageE_["an"] = "Aragonese";
codeToLanguageE_["ar"] = "Arabic";
codeToLanguageE_["as"] = "Assamese";
codeToLanguageE_["ay"] = "Aymara";
codeToLanguageE_["az"] = "Azerbaijani";
codeToLanguageE_["ba"] = "Bashkir";
codeToLanguageE_["be"] = "Byelorussian";
codeToLanguageE_["bg"] = "Bulgarian";
codeToLanguageE_["bh"] = "Bihari";
codeToLanguageE_["bi"] = "Bislama";
codeToLanguageE_["bn"] = "Bangla";
codeToLanguageE_["bo"] = "Tibetan";
codeToLanguageE_["br"] = "Breton";
codeToLanguageE_["ca"] = "Catalan";
codeToLanguageE_["co"] = "Corsican";
codeToLanguageE_["cs"] = "Czech";
codeToLanguageE_["cy"] = "Welsh";
codeToLanguageE_["da"] = "Danish";
codeToLanguageE_["de"] = "German";
codeToLanguageE_["dz"] = "Bhutani";
codeToLanguageE_["el"] = "Greek";
codeToLanguageE_["en"] = "English";
codeToLanguageE_["eo"] = "Esperanto";
codeToLanguageE_["es"] = "Spanish";
codeToLanguageE_["et"] = "Estonian";
codeToLanguageE_["eu"] = "Basque";
codeToLanguageE_["fa"] = "Persian";
codeToLanguageE_["fi"] = "Finnish";
codeToLanguageE_["fj"] = "Fiji";
codeToLanguageE_["fo"] = "Faroese";
codeToLanguageE_["fr"] = "French";
codeToLanguageE_["fy"] = "Frisian";
codeToLanguageE_["ga"] = "Irish";
codeToLanguageE_["gd"] = "Gaelic";
codeToLanguageE_["gl"] = "Galician";
codeToLanguageE_["gn"] = "Guarani";
codeToLanguageE_["gu"] = "Gujarati";
codeToLanguageE_["ha"] = "Hausa";
codeToLanguageE_["he"] = "Hebrew";
codeToLanguageE_["hi"] = "Hindi";
codeToLanguageE_["hr"] = "Croatian";
codeToLanguageE_["hu"] = "Hungarian";
codeToLanguageE_["hy"] = "Armenian";
codeToLanguageE_["ia"] = "Interlingua";
codeToLanguageE_["id"] = "Indonesian";
codeToLanguageE_["ie"] = "Interlingue";
codeToLanguageE_["ik"] = "Inupiak";
codeToLanguageE_["is"] = "Icelandic";
codeToLanguageE_["it"] = "Italian";
codeToLanguageE_["iu"] = "Inuktitut";
codeToLanguageE_["ja"] = "Japanese";
codeToLanguageE_["jw"] = "Javanese";
codeToLanguageE_["ka"] = "Georgian";
codeToLanguageE_["kk"] = "Kazakh";
codeToLanguageE_["kl"] = "Greenlandic";
codeToLanguageE_["km"] = "Cambodian";
codeToLanguageE_["kn"] = "Kannada";
codeToLanguageE_["ko"] = "Korean";
codeToLanguageE_["ks"] = "Kashmiri";
codeToLanguageE_["ku"] = "Kurdish";
codeToLanguageE_["ky"] = "Kirghiz";
codeToLanguageE_["la"] = "Latin";
codeToLanguageE_["ln"] = "Lingala";
codeToLanguageE_["lo"] = "Laothian";
codeToLanguageE_["lt"] = "Lithuanian";
codeToLanguageE_["lv"] = "Lettish";
codeToLanguageE_["mg"] = "Malagasy";
codeToLanguageE_["mi"] = "Maori";
codeToLanguageE_["mk"] = "Macedonian";
codeToLanguageE_["ml"] = "Malayalam";
codeToLanguageE_["mn"] = "Mongolian";
codeToLanguageE_["mo"] = "Moldavian";
codeToLanguageE_["mr"] = "Marathi";
codeToLanguageE_["ms"] = "Malay";
codeToLanguageE_["mt"] = "Maltese";
codeToLanguageE_["my"] = "Burmese";
codeToLanguageE_["na"] = "Nauru";
codeToLanguageE_["nb"] = "Norwegian";
codeToLanguageE_["ne"] = "Nepali";
codeToLanguageE_["nl"] = "Dutch";
codeToLanguageE_["no"] = "Norwegian";
codeToLanguageE_["oc"] = "Occitan";
codeToLanguageE_["om"] = "Oromo";
codeToLanguageE_["or"] = "Oriya";
codeToLanguageE_["pa"] = "Punjabi";
codeToLanguageE_["pl"] = "Polish";
codeToLanguageE_["ps"] = "Pushto";
codeToLanguageE_["pt"] = "Portuguese";
codeToLanguageE_["qu"] = "Quechua";
codeToLanguageE_["rm"] = "Rhaeto-Romance";
codeToLanguageE_["rn"] = "Kirundi";
codeToLanguageE_["ro"] = "Romanian";
codeToLanguageE_["ru"] = "Russian";
codeToLanguageE_["rw"] = "Kinyarwanda";
codeToLanguageE_["sa"] = "Sanskrit";
codeToLanguageE_["sd"] = "Sindhi";
codeToLanguageE_["sg"] = "Sangho";
codeToLanguageE_["sh"] = "Serbo-Croatian";
codeToLanguageE_["si"] = "Sinhalese";
codeToLanguageE_["sk"] = "Slovak";
codeToLanguageE_["sl"] = "Slovenian";
codeToLanguageE_["sm"] = "Samoan";
codeToLanguageE_["sn"] = "Shona";
codeToLanguageE_["so"] = "Somali";
codeToLanguageE_["sq"] = "Albanian";
codeToLanguageE_["sr"] = "Serbian";
codeToLanguageE_["ss"] = "Siswati";
codeToLanguageE_["st"] = "Sesotho";
codeToLanguageE_["su"] = "Sundanese";
codeToLanguageE_["sv"] = "Swedish";
codeToLanguageE_["sw"] = "Swahili";
codeToLanguageE_["ta"] = "Tamil";
codeToLanguageE_["te"] = "Telugu";
codeToLanguageE_["tg"] = "Tajik";
codeToLanguageE_["th"] = "Thai";
codeToLanguageE_["ti"] = "Tigrinya";
codeToLanguageE_["tk"] = "Turkmen";
codeToLanguageE_["tl"] = "Tagalog";
codeToLanguageE_["tn"] = "Setswana";
codeToLanguageE_["to"] = "Tonga";
codeToLanguageE_["tr"] = "Turkish";
codeToLanguageE_["ts"] = "Tsonga";
codeToLanguageE_["tt"] = "Tatar";
codeToLanguageE_["tw"] = "Twi";
codeToLanguageE_["ug"] = "Uighur";
codeToLanguageE_["uk"] = "Ukrainian";
codeToLanguageE_["ur"] = "Urdu";
codeToLanguageE_["uz"] = "Uzbek";
codeToLanguageE_["vi"] = "Vietnamese";
codeToLanguageE_["vo"] = "Volapuk";
codeToLanguageE_["wo"] = "Wolof";
codeToLanguageE_["xh"] = "Xhosa";
codeToLanguageE_["yi"] = "Yiddish";
codeToLanguageE_["yo"] = "Yoruba";
codeToLanguageE_["za"] = "Zhuang";
codeToLanguageE_["zh"] = "Chinese";
codeToLanguageE_["zu"] = "Zulu";
codeToLanguageE_['aa'] = 'Afar';
codeToLanguageE_['ab'] = 'Abkhazian';
codeToLanguageE_['af'] = 'Afrikaans';
codeToLanguageE_['am'] = 'Amharic';
codeToLanguageE_['an'] = 'Aragonese';
codeToLanguageE_['ar'] = 'Arabic';
codeToLanguageE_['as'] = 'Assamese';
codeToLanguageE_['ay'] = 'Aymara';
codeToLanguageE_['az'] = 'Azerbaijani';
codeToLanguageE_['ba'] = 'Bashkir';
codeToLanguageE_['be'] = 'Byelorussian';
codeToLanguageE_['bg'] = 'Bulgarian';
codeToLanguageE_['bh'] = 'Bihari';
codeToLanguageE_['bi'] = 'Bislama';
codeToLanguageE_['bn'] = 'Bangla';
codeToLanguageE_['bo'] = 'Tibetan';
codeToLanguageE_['br'] = 'Breton';
codeToLanguageE_['ca'] = 'Catalan';
codeToLanguageE_['co'] = 'Corsican';
codeToLanguageE_['cs'] = 'Czech';
codeToLanguageE_['cy'] = 'Welsh';
codeToLanguageE_['da'] = 'Danish';
codeToLanguageE_['de'] = 'German';
codeToLanguageE_['dz'] = 'Bhutani';
codeToLanguageE_['el'] = 'Greek';
codeToLanguageE_['en'] = 'English';
codeToLanguageE_['eo'] = 'Esperanto';
codeToLanguageE_['es'] = 'Spanish';
codeToLanguageE_['et'] = 'Estonian';
codeToLanguageE_['eu'] = 'Basque';
codeToLanguageE_['fa'] = 'Persian';
codeToLanguageE_['fi'] = 'Finnish';
codeToLanguageE_['fj'] = 'Fiji';
codeToLanguageE_['fo'] = 'Faroese';
codeToLanguageE_['fr'] = 'French';
codeToLanguageE_['fy'] = 'Frisian';
codeToLanguageE_['ga'] = 'Irish';
codeToLanguageE_['gd'] = 'Gaelic';
codeToLanguageE_['gl'] = 'Galician';
codeToLanguageE_['gn'] = 'Guarani';
codeToLanguageE_['gu'] = 'Gujarati';
codeToLanguageE_['ha'] = 'Hausa';
codeToLanguageE_['he'] = 'Hebrew';
codeToLanguageE_['hi'] = 'Hindi';
codeToLanguageE_['hr'] = 'Croatian';
codeToLanguageE_['hu'] = 'Hungarian';
codeToLanguageE_['hy'] = 'Armenian';
codeToLanguageE_['ia'] = 'Interlingua';
codeToLanguageE_['id'] = 'Indonesian';
codeToLanguageE_['ie'] = 'Interlingue';
codeToLanguageE_['ik'] = 'Inupiak';
codeToLanguageE_['is'] = 'Icelandic';
codeToLanguageE_['it'] = 'Italian';
codeToLanguageE_['iu'] = 'Inuktitut';
codeToLanguageE_['ja'] = 'Japanese';
codeToLanguageE_['jw'] = 'Javanese';
codeToLanguageE_['ka'] = 'Georgian';
codeToLanguageE_['kk'] = 'Kazakh';
codeToLanguageE_['kl'] = 'Greenlandic';
codeToLanguageE_['km'] = 'Cambodian';
codeToLanguageE_['kn'] = 'Kannada';
codeToLanguageE_['ko'] = 'Korean';
codeToLanguageE_['ks'] = 'Kashmiri';
codeToLanguageE_['ku'] = 'Kurdish';
codeToLanguageE_['ky'] = 'Kirghiz';
codeToLanguageE_['la'] = 'Latin';
codeToLanguageE_['ln'] = 'Lingala';
codeToLanguageE_['lo'] = 'Laothian';
codeToLanguageE_['lt'] = 'Lithuanian';
codeToLanguageE_['lv'] = 'Lettish';
codeToLanguageE_['mg'] = 'Malagasy';
codeToLanguageE_['mi'] = 'Maori';
codeToLanguageE_['mk'] = 'Macedonian';
codeToLanguageE_['ml'] = 'Malayalam';
codeToLanguageE_['mn'] = 'Mongolian';
codeToLanguageE_['mo'] = 'Moldavian';
codeToLanguageE_['mr'] = 'Marathi';
codeToLanguageE_['ms'] = 'Malay';
codeToLanguageE_['mt'] = 'Maltese';
codeToLanguageE_['my'] = 'Burmese';
codeToLanguageE_['na'] = 'Nauru';
codeToLanguageE_['nb'] = 'Norwegian';
codeToLanguageE_['ne'] = 'Nepali';
codeToLanguageE_['nl'] = 'Dutch';
codeToLanguageE_['no'] = 'Norwegian';
codeToLanguageE_['oc'] = 'Occitan';
codeToLanguageE_['om'] = 'Oromo';
codeToLanguageE_['or'] = 'Oriya';
codeToLanguageE_['pa'] = 'Punjabi';
codeToLanguageE_['pl'] = 'Polish';
codeToLanguageE_['ps'] = 'Pushto';
codeToLanguageE_['pt'] = 'Portuguese';
codeToLanguageE_['qu'] = 'Quechua';
codeToLanguageE_['rm'] = 'Rhaeto-Romance';
codeToLanguageE_['rn'] = 'Kirundi';
codeToLanguageE_['ro'] = 'Romanian';
codeToLanguageE_['ru'] = 'Russian';
codeToLanguageE_['rw'] = 'Kinyarwanda';
codeToLanguageE_['sa'] = 'Sanskrit';
codeToLanguageE_['sd'] = 'Sindhi';
codeToLanguageE_['sg'] = 'Sangho';
codeToLanguageE_['sh'] = 'Serbo-Croatian';
codeToLanguageE_['si'] = 'Sinhalese';
codeToLanguageE_['sk'] = 'Slovak';
codeToLanguageE_['sl'] = 'Slovenian';
codeToLanguageE_['sm'] = 'Samoan';
codeToLanguageE_['sn'] = 'Shona';
codeToLanguageE_['so'] = 'Somali';
codeToLanguageE_['sq'] = 'Albanian';
codeToLanguageE_['sr'] = 'Serbian';
codeToLanguageE_['ss'] = 'Siswati';
codeToLanguageE_['st'] = 'Sesotho';
codeToLanguageE_['su'] = 'Sundanese';
codeToLanguageE_['sv'] = 'Swedish';
codeToLanguageE_['sw'] = 'Swahili';
codeToLanguageE_['ta'] = 'Tamil';
codeToLanguageE_['te'] = 'Telugu';
codeToLanguageE_['tg'] = 'Tajik';
codeToLanguageE_['th'] = 'Thai';
codeToLanguageE_['ti'] = 'Tigrinya';
codeToLanguageE_['tk'] = 'Turkmen';
codeToLanguageE_['tl'] = 'Tagalog';
codeToLanguageE_['tn'] = 'Setswana';
codeToLanguageE_['to'] = 'Tonga';
codeToLanguageE_['tr'] = 'Turkish';
codeToLanguageE_['ts'] = 'Tsonga';
codeToLanguageE_['tt'] = 'Tatar';
codeToLanguageE_['tw'] = 'Twi';
codeToLanguageE_['ug'] = 'Uighur';
codeToLanguageE_['uk'] = 'Ukrainian';
codeToLanguageE_['ur'] = 'Urdu';
codeToLanguageE_['uz'] = 'Uzbek';
codeToLanguageE_['vi'] = 'Vietnamese';
codeToLanguageE_['vo'] = 'Volapuk';
codeToLanguageE_['wo'] = 'Wolof';
codeToLanguageE_['xh'] = 'Xhosa';
codeToLanguageE_['yi'] = 'Yiddish';
codeToLanguageE_['yo'] = 'Yoruba';
codeToLanguageE_['za'] = 'Zhuang';
codeToLanguageE_['zh'] = 'Chinese';
codeToLanguageE_['zu'] = 'Zulu';
let codeToLanguage_ = {};
codeToLanguage_["an"] = "Aragonés";
codeToLanguage_["da"] = "Dansk";
codeToLanguage_["de"] = "Deutsch";
codeToLanguage_["en"] = "English";
codeToLanguage_["es"] = "Español";
codeToLanguage_["fr"] = "Français";
codeToLanguage_["he"] = "עיברית";
codeToLanguage_["it"] = "Italiano";
codeToLanguage_["lt"] = "Lietuvių kalba";
codeToLanguage_["nl"] = "Nederlands";
codeToLanguage_["pl"] = "Polski";
codeToLanguage_["pt"] = "Português";
codeToLanguage_["ru"] = "Русский";
codeToLanguage_["sk"] = "Slovenčina";
codeToLanguage_["sq"] = "Shqip";
codeToLanguage_["sr"] = "српски језик";
codeToLanguage_["tr"] = "Türkçe";
codeToLanguage_["ja"] = "日本語";
codeToLanguage_["ko"] = "한국말";
codeToLanguage_["sv"] = "Svenska";
codeToLanguage_["el"] = "ελληνικά";
codeToLanguage_["zh"] = "中文";
codeToLanguage_["ro"] = "Română";
codeToLanguage_["et"] = "Eesti Keel";
codeToLanguage_["vi"] = "Tiếng Việt";
codeToLanguage_["hu"] = "Magyar";
codeToLanguage_['an'] = 'Aragonés';
codeToLanguage_['da'] = 'Dansk';
codeToLanguage_['de'] = 'Deutsch';
codeToLanguage_['en'] = 'English';
codeToLanguage_['es'] = 'Español';
codeToLanguage_['fr'] = 'Français';
codeToLanguage_['he'] = 'עיברית';
codeToLanguage_['it'] = 'Italiano';
codeToLanguage_['lt'] = 'Lietuvių kalba';
codeToLanguage_['nl'] = 'Nederlands';
codeToLanguage_['pl'] = 'Polski';
codeToLanguage_['pt'] = 'Português';
codeToLanguage_['ru'] = 'Русский';
codeToLanguage_['sk'] = 'Slovenčina';
codeToLanguage_['sq'] = 'Shqip';
codeToLanguage_['sr'] = 'српски језик';
codeToLanguage_['tr'] = 'Türkçe';
codeToLanguage_['ja'] = '日本語';
codeToLanguage_['ko'] = '한국말';
codeToLanguage_['sv'] = 'Svenska';
codeToLanguage_['el'] = 'ελληνικά';
codeToLanguage_['zh'] = '中文';
codeToLanguage_['ro'] = 'Română';
codeToLanguage_['et'] = 'Eesti Keel';
codeToLanguage_['vi'] = 'Tiếng Việt';
codeToLanguage_['hu'] = 'Magyar';
let codeToCountry_ = {};
codeToCountry_["BR"] = "Brasil";
codeToCountry_["CR"] = "Costa Rica";
codeToCountry_["CN"] = "中国";
codeToCountry_["GB"] = "UK";
codeToCountry_["US"] = "US";
codeToCountry_['BR'] = 'Brasil';
codeToCountry_['CR'] = 'Costa Rica';
codeToCountry_['CN'] = '中国';
codeToCountry_['GB'] = 'UK';
codeToCountry_['US'] = 'US';
let supportedLocales_ = null;
let localeStats_ = null;
@ -249,7 +249,7 @@ function languageNameInEnglish(languageCode) {
function languageName(languageCode, defaultToEnglish = true) {
if (codeToLanguage_[languageCode]) return codeToLanguage_[languageCode];
if (defaultToEnglish) return languageNameInEnglish(languageCode)
if (defaultToEnglish) return languageNameInEnglish(languageCode);
return '';
}
@ -258,9 +258,8 @@ function languageCodeOnly(canonicalName) {
return canonicalName.substr(0, 2);
}
function countryCodeOnly(canonicalName) {
if (canonicalName.length <= 2) return "";
if (canonicalName.length <= 2) return '';
return canonicalName.substr(3);
}
@ -273,26 +272,26 @@ function countryDisplayName(canonicalName) {
let extraString;
if (countryCode) {
if (languageCode == "zh" && countryCode == "CN") {
extraString = "简体"; // "Simplified" in "Simplified Chinese"
if (languageCode == 'zh' && countryCode == 'CN') {
extraString = '简体'; // "Simplified" in "Simplified Chinese"
} else {
extraString = countryName(countryCode);
}
}
if (languageCode == "zh" && (countryCode == "" || countryCode == "TW")) extraString = "繁體"; // "Traditional" in "Traditional Chinese"
if (languageCode == 'zh' && (countryCode == '' || countryCode == 'TW')) extraString = '繁體'; // "Traditional" in "Traditional Chinese"
if (extraString) output += " (" + extraString + ")";
if (extraString) output += ' (' + extraString + ')';
return output;
}
function localeStrings(canonicalName) {
const locale = closestSupportedLocale(canonicalName);
if (loadedLocales_[locale]) return loadedLocales_[locale];
loadedLocales_[locale] = Object.assign({}, supportedLocales_[locale]);
loadedLocales_[locale] = Object.assign({}, supportedLocales_[locale]);
return loadedLocales_[locale];
}
@ -317,4 +316,4 @@ function _(s, ...args) {
}
}
module.exports = { _, supportedLocales, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };
module.exports = { _, supportedLocales, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };

View File

@ -4,11 +4,10 @@ const { time } = require('lib/time-utils.js');
const { FsDriverDummy } = require('lib/fs-driver-dummy.js');
class Logger {
constructor() {
this.targets_ = [];
this.level_ = Logger.LEVEL_INFO;
this.fileAppendQueue_ = []
this.fileAppendQueue_ = [];
this.lastDbCleanup_ = time.unixMs();
}
@ -49,10 +48,10 @@ class Logger {
if (typeof object === 'object') {
if (object instanceof Error) {
output = object.toString();
if (object.code) output += "\nCode: " + object.code;
if (object.headers) output += "\nHeader: " + JSON.stringify(object.headers);
if (object.request) output += "\nRequest: " + (object.request.substr ? object.request.substr(0, 1024) : '');
if (object.stack) output += "\n" + object.stack;
if (object.code) output += '\nCode: ' + object.code;
if (object.headers) output += '\nHeader: ' + JSON.stringify(object.headers);
if (object.request) output += '\nRequest: ' + (object.request.substr ? object.request.substr(0, 1024) : '');
if (object.stack) output += '\n' + object.stack;
} else {
output = JSON.stringify(object);
}
@ -60,7 +59,7 @@ class Logger {
output = object;
}
return output;
return output;
}
objectsToString(...object) {
@ -81,7 +80,7 @@ class Logger {
\`timestamp\` INT NOT NULL
);
`;
return output.split("\n").join(' ');
return output.split('\n').join(' ');
}
// Only for database at the moment
@ -94,7 +93,7 @@ class Logger {
const target = this.targets_[i];
if (target.type == 'database') {
let sql = 'SELECT * FROM logs WHERE level IN (' + options.levels.join(',') + ') ORDER BY timestamp DESC';
if (limit !== null) sql += ' LIMIT ' + limit
if (limit !== null) sql += ' LIMIT ' + limit;
return await target.database.selectAll(sql);
}
}
@ -103,7 +102,7 @@ class Logger {
targetLevel(target) {
if ('level' in target) return target.level;
return this.level();
return this.level();
}
log(level, ...object) {
@ -117,7 +116,7 @@ class Logger {
for (let i = 0; i < this.targets_.length; i++) {
let target = this.targets_[i];
if (this.targetLevel(target) < level) continue;
if (target.type == 'console') {
@ -128,14 +127,16 @@ class Logger {
console[fn](line + this.objectsToString(...object));
} else if (target.type == 'file') {
let serializedObject = this.objectsToString(...object);
Logger.fsDriver().appendFileSync(target.path, line + serializedObject + "\n");
Logger.fsDriver().appendFileSync(target.path, line + serializedObject + '\n');
} else if (target.type == 'database') {
let msg = this.objectsToString(...object);
let queries = [{
sql: 'INSERT INTO logs (`source`, `level`, `message`, `timestamp`) VALUES (?, ?, ?, ?)',
params: [target.source, level, msg, time.unixMs()],
}];
let queries = [
{
sql: 'INSERT INTO logs (`source`, `level`, `message`, `timestamp`) VALUES (?, ?, ?, ?)',
params: [target.source, level, msg, time.unixMs()],
},
];
const now = time.unixMs();
if (now - this.lastDbCleanup_ > 1000 * 60 * 60) {
@ -152,10 +153,18 @@ class Logger {
}
}
error(...object) { return this.log(Logger.LEVEL_ERROR, ...object); }
warn(...object) { return this.log(Logger.LEVEL_WARN, ...object); }
info(...object) { return this.log(Logger.LEVEL_INFO, ...object); }
debug(...object) { return this.log(Logger.LEVEL_DEBUG, ...object); }
error(...object) {
return this.log(Logger.LEVEL_ERROR, ...object);
}
warn(...object) {
return this.log(Logger.LEVEL_WARN, ...object);
}
info(...object) {
return this.log(Logger.LEVEL_INFO, ...object);
}
debug(...object) {
return this.log(Logger.LEVEL_DEBUG, ...object);
}
static levelStringToId(s) {
if (s == 'none') return Logger.LEVEL_NONE;
@ -187,7 +196,6 @@ class Logger {
}
return output;
}
}
Logger.LEVEL_NONE = 0;
@ -196,4 +204,4 @@ Logger.LEVEL_WARN = 20;
Logger.LEVEL_INFO = 30;
Logger.LEVEL_DEBUG = 40;
module.exports = { Logger };
module.exports = { Logger };

View File

@ -1,4 +1,4 @@
const markJsUtils = {}
const markJsUtils = {};
markJsUtils.markKeyword = (mark, keyword, stringUtils, extraOptions = null) => {
if (typeof keyword === 'string') {
@ -11,24 +11,31 @@ markJsUtils.markKeyword = (mark, keyword, stringUtils, extraOptions = null) => {
const isBasicSearch = ['ja', 'zh', 'ko'].indexOf(keyword.scriptType) >= 0;
let value = keyword.value;
let accuracy = keyword.accuracy ? keyword.accuracy : { value: 'exactly', limiters: ":;.,-_(){}[]!'\"+=".split("") };
let accuracy = keyword.accuracy ? keyword.accuracy : { value: 'exactly', limiters: ':;.,-_(){}[]!\'"+='.split('') };
if (isBasicSearch) accuracy = 'partially';
if (keyword.type === 'regex') {
accuracy = 'complementary';
// Remove the trailing wildcard and "accuracy = complementary" will take care of
// highlighting the relevant keywords.
// Known bug: it will also highlight word that contain the term as a suffix for example for "ent*", it will highlight "present"
// which is incorrect (it should only highlight what starts with "ent") but for now will do. Mark.js doesn't have an option
// to tweak this behaviour.
value = keyword.value.substr(0, keyword.value.length - 1);
}
mark.mark([value], Object.assign({}, {
accuracy: accuracy,
}, extraOptions));
}
mark.mark(
[value],
Object.assign(
{},
{
accuracy: accuracy,
},
extraOptions
)
);
};
if (typeof module !== 'undefined') {
module.exports = markJsUtils;
}
}

View File

@ -4,7 +4,6 @@ const MarkdownIt = require('markdown-it');
const setupLinkify = require('lib/renderers/MdToHtml/setupLinkify');
const markdownUtils = {
// Not really escaping because that's not supported by marked.js
escapeLinkText(text) {
return text.replace(/(\[|\]|\(|\))/g, '_');
@ -30,7 +29,7 @@ const markdownUtils = {
const tokens = markdownIt.parse(md, env);
const output = [];
const searchUrls = (tokens) => {
const searchUrls = tokens => {
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
@ -42,12 +41,12 @@ const markdownUtils = {
}
}
}
if (token.children && token.children.length) {
searchUrls(token.children);
}
}
}
};
searchUrls(tokens);
@ -87,8 +86,6 @@ const markdownUtils = {
return output.join('\n');
},
};
module.exports = markdownUtils;
module.exports = markdownUtils;

View File

@ -3,7 +3,6 @@ const htmlUtils = require('lib/htmlUtils');
const Note = require('lib/models/Note');
class MarkupLanguageUtils {
lib_(language) {
if (language === Note.MARKUP_LANGUAGE_HTML) return htmlUtils;
if (language === Note.MARKUP_LANGUAGE_MARKDOWN) return markdownUtils;
@ -13,9 +12,8 @@ class MarkupLanguageUtils {
extractImageUrls(language, text) {
return this.lib_(language).extractImageUrls(text);
}
}
const markupLanguageUtils = new MarkupLanguageUtils();
module.exports = markupLanguageUtils;
module.exports = markupLanguageUtils;

View File

@ -11,7 +11,7 @@ script.exec = async function() {
let queries = [];
for (const stat of stats) {
if (fileExtension(stat.path) === 'crypted') continue;
if (fileExtension(stat.path) === 'crypted') continue;
const resourceId = Resource.pathToId(stat.path);
if (!resourceId) continue;
@ -24,6 +24,6 @@ script.exec = async function() {
}
await reg.db().transactionExecBatch(queries);
}
};
module.exports = script;
module.exports = script;

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,6 @@ const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
class Alarm extends BaseModel {
static tableName() {
return 'alarms';
}
@ -22,7 +21,9 @@ class Alarm extends BaseModel {
static async alarmIdsWithoutNotes() {
// https://stackoverflow.com/a/4967229/561309
const alarms = await this.db().selectAll('SELECT alarms.id FROM alarms LEFT JOIN notes ON alarms.note_id = notes.id WHERE notes.id IS NULL');
return alarms.map((a) => { return a.id });
return alarms.map(a => {
return a.id;
});
}
static async makeNotification(alarm, note = null) {
@ -37,18 +38,17 @@ class Alarm extends BaseModel {
const output = {
id: alarm.id,
date: new Date(note.todo_due),
title: note.title.substr(0,128),
title: note.title.substr(0, 128),
};
if (note.body) output.body = note.body.substr(0,512);
if (note.body) output.body = note.body.substr(0, 512);
return output;
return output;
}
static async allDue() {
return this.modelSelectAll('SELECT * FROM alarms WHERE trigger_time >= ?', [Date.now()]);
}
}
module.exports = Alarm;
module.exports = Alarm;

View File

@ -10,7 +10,6 @@ const moment = require('moment');
const markdownUtils = require('lib/markdownUtils');
class BaseItem extends BaseModel {
static useUuid() {
return true;
}
@ -38,7 +37,7 @@ class BaseItem extends BaseModel {
if (!item) return titleToTry;
titleToTry = title + ' (' + counter + ')';
counter++;
if (counter >= 100) titleToTry = title + ' (' + ((new Date()).getTime()) + ')';
if (counter >= 100) titleToTry = title + ' (' + new Date().getTime() + ')';
if (counter >= 1000) throw new Error('Cannot find unique title');
}
}
@ -72,18 +71,15 @@ class BaseItem extends BaseModel {
// The fact that we don't check if the item_id still exist in the corresponding item table, means
// that the returned number might be innaccurate (for example if a sync operation was cancelled)
const sql = 'SELECT count(*) as total FROM sync_items WHERE sync_target = ? AND item_type = ?';
const r = await this.db().selectOne(sql, [ syncTarget, itemType ]);
const r = await this.db().selectOne(sql, [syncTarget, itemType]);
return r.total;
}
static systemPath(itemOrId, extension = null) {
if (extension === null)
extension = 'md';
if (extension === null) extension = 'md';
if (typeof itemOrId === 'string')
return itemOrId + '.' + extension;
else
return itemOrId.id + '.' + extension;
if (typeof itemOrId === 'string') return itemOrId + '.' + extension;
else return itemOrId.id + '.' + extension;
}
static isSystemPath(path) {
@ -190,7 +186,9 @@ class BaseItem extends BaseModel {
let conflictNoteIds = [];
if (this.modelType() == BaseModel.TYPE_NOTE) {
const conflictNotes = await this.db().selectAll('SELECT id FROM notes WHERE id IN ("' + ids.join('","') + '") AND is_conflict = 1');
conflictNoteIds = conflictNotes.map((n) => { return n.id });
conflictNoteIds = conflictNotes.map(n => {
return n.id;
});
}
await super.batchDelete(ids, options);
@ -240,7 +238,11 @@ class BaseItem extends BaseModel {
static serialize_format(propName, propValue) {
if (['created_time', 'updated_time', 'sync_time', 'user_updated_time', 'user_created_time'].indexOf(propName) >= 0) {
if (!propValue) return '';
propValue = moment.unix(propValue / 1000).utc().format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
propValue =
moment
.unix(propValue / 1000)
.utc()
.format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
} else if (['title_diff', 'body_diff'].indexOf(propName) >= 0) {
if (!propValue) return '';
propValue = JSON.stringify(propValue);
@ -307,11 +309,11 @@ class BaseItem extends BaseModel {
let temp = [];
if (typeof output.title === "string") temp.push(output.title);
if (typeof output.title === 'string') temp.push(output.title);
if (output.body) temp.push(output.body);
if (output.props.length) temp.push(output.props.join("\n"));
if (output.props.length) temp.push(output.props.join('\n'));
return temp.join("\n\n");
return temp.join('\n\n');
}
static encryptionService() {
@ -333,11 +335,15 @@ class BaseItem extends BaseModel {
if (!Setting.value('encryption.enabled') || !ItemClass.encryptionSupported()) {
// Normally not possible since itemsThatNeedSync should only return decrypted items
if (!!item.encryption_applied) throw new JoplinError('Item is encrypted but encryption is currently disabled', 'cannotSyncEncrypted');
if (item.encryption_applied) throw new JoplinError('Item is encrypted but encryption is currently disabled', 'cannotSyncEncrypted');
return serialized;
}
if (!!item.encryption_applied) { const e = new Error('Trying to encrypt item that is already encrypted'); e.code = 'cannotEncryptEncrypted'; throw e; }
if (item.encryption_applied) {
const e = new Error('Trying to encrypt item that is already encrypted');
e.code = 'cannotEncryptEncrypted';
throw e;
}
const cipherText = await this.encryptionService().encryptString(serialized);
@ -354,7 +360,7 @@ class BaseItem extends BaseModel {
reducedItem.encryption_applied = 1;
reducedItem.encryption_cipher_text = cipherText;
return ItemClass.serialize(reducedItem)
return ItemClass.serialize(reducedItem);
}
static async decrypt(item) {
@ -372,7 +378,7 @@ class BaseItem extends BaseModel {
}
static async unserialize(content) {
let lines = content.split("\n");
let lines = content.split('\n');
let output = {};
let state = 'readingProps';
let body = [];
@ -389,7 +395,7 @@ class BaseItem extends BaseModel {
}
let p = line.indexOf(':');
if (p < 0) throw new Error('Invalid property format: ' + line + ": " + content);
if (p < 0) throw new Error('Invalid property format: ' + line + ': ' + content);
let key = line.substr(0, p).trim();
let value = line.substr(p + 1).trim();
output[key] = value;
@ -397,7 +403,7 @@ class BaseItem extends BaseModel {
body.splice(0, 0, line);
}
}
if (!output.type_) throw new Error('Missing required property: type_: ' + content);
output.type_ = Number(output.type_);
@ -406,7 +412,7 @@ class BaseItem extends BaseModel {
output.title = title[0];
}
if (output.type_ === BaseModel.TYPE_NOTE) output.body = body.join("\n");
if (output.type_ === BaseModel.TYPE_NOTE) output.body = body.join('\n');
const ItemClass = this.itemClass(output.type_);
output = ItemClass.removeUnknownFields(output);
@ -477,10 +483,11 @@ class BaseItem extends BaseModel {
const blobDownloadedButEncryptedSql = 'encryption_blob_encrypted = 1 AND id IN (SELECT resource_id FROM resource_local_states WHERE fetch_status = 2))';
whereSql = ['(encryption_applied = 1 OR (' + blobDownloadedButEncryptedSql + ')'];
}
if (exclusions.length) whereSql.push('id NOT IN ("' + exclusions.join('","') + '")');
const sql = sprintf(`
const sql = sprintf(
`
SELECT *
FROM %s
WHERE %s
@ -509,7 +516,7 @@ class BaseItem extends BaseModel {
for (let i = 0; i < classNames.length; i++) {
const className = classNames[i];
const ItemClass = this.getClass(className);
let fieldNames = ItemClass.fieldNames('items');
let fieldNames = ItemClass.fieldNames('items');
// // NEVER SYNCED:
// 'SELECT * FROM [ITEMS] WHERE id NOT INT (SELECT item_id FROM sync_items WHERE sync_target = ?)'
@ -526,7 +533,8 @@ class BaseItem extends BaseModel {
// First get all the items that have never been synced under this sync target
let sql = sprintf(`
let sql = sprintf(
`
SELECT %s
FROM %s items
WHERE id NOT IN (
@ -535,11 +543,12 @@ class BaseItem extends BaseModel {
%s
LIMIT %d
`,
this.db().escapeFields(fieldNames),
this.db().escapeField(ItemClass.tableName()),
Number(syncTarget),
extraWhere,
limit);
this.db().escapeFields(fieldNames),
this.db().escapeField(ItemClass.tableName()),
Number(syncTarget),
extraWhere,
limit
);
let neverSyncedItem = await ItemClass.modelSelectAll(sql);
@ -552,7 +561,8 @@ class BaseItem extends BaseModel {
if (newLimit > 0) {
fieldNames.push('sync_time');
let sql = sprintf(`
let sql = sprintf(
`
SELECT %s FROM %s items
JOIN sync_items s ON s.item_id = items.id
WHERE sync_target = %d
@ -561,11 +571,12 @@ class BaseItem extends BaseModel {
%s
LIMIT %d
`,
this.db().escapeFields(fieldNames),
this.db().escapeField(ItemClass.tableName()),
Number(syncTarget),
extraWhere,
newLimit);
this.db().escapeFields(fieldNames),
this.db().escapeField(ItemClass.tableName()),
Number(syncTarget),
extraWhere,
newLimit
);
changedItems = await ItemClass.modelSelectAll(sql);
}
@ -583,7 +594,7 @@ class BaseItem extends BaseModel {
}
static syncItemClassNames() {
return BaseItem.syncItemDefinitions_.map((def) => {
return BaseItem.syncItemDefinitions_.map(def => {
return def.className;
});
}
@ -599,7 +610,7 @@ class BaseItem extends BaseModel {
}
static syncItemTypes() {
return BaseItem.syncItemDefinitions_.map((def) => {
return BaseItem.syncItemDefinitions_.map(def => {
return def.type;
});
}
@ -643,7 +654,7 @@ class BaseItem extends BaseModel {
{
sql: 'INSERT INTO sync_items (sync_target, item_type, item_id, item_location, sync_time, sync_disabled, sync_disabled_reason) VALUES (?, ?, ?, ?, ?, ?, ?)',
params: [syncTarget, itemType, itemId, itemLocation, syncTime, syncDisabled ? 1 : 0, syncDisabledReason + ''],
}
},
];
}
@ -680,8 +691,8 @@ class BaseItem extends BaseModel {
static displayTitle(item) {
if (!item) return '';
if (!!item.encryption_applied) return '🔑 ' + _('Encrypted');
return !!item.title ? item.title : _('Untitled');
if (item.encryption_applied) return '🔑 ' + _('Encrypted');
return item.title ? item.title : _('Untitled');
}
static async markAllNonEncryptedForSync() {
@ -691,7 +702,8 @@ class BaseItem extends BaseModel {
const className = classNames[i];
const ItemClass = this.getClass(className);
const sql = sprintf(`
const sql = sprintf(
`
SELECT id
FROM %s
WHERE encryption_applied = 0`,
@ -699,7 +711,9 @@ class BaseItem extends BaseModel {
);
const items = await ItemClass.modelSelectAll(sql);
const ids = items.map((item) => {return item.id});
const ids = items.map(item => {
return item.id;
});
if (!ids.length) continue;
await this.db().exec('UPDATE sync_items SET force_sync = 1 WHERE item_id IN ("' + ids.join('","') + '")');
@ -718,17 +732,20 @@ class BaseItem extends BaseModel {
if (!options) options = {};
if (options.userSideValidation === true) {
if (!!o.encryption_applied) throw new Error(_('Encrypted items cannot be modified'));
if (o.encryption_applied) throw new Error(_('Encrypted items cannot be modified'));
}
return super.save(o, options);
}
static markdownTag(itemOrId) {
const item = typeof itemOrId === 'object' ? itemOrId : {
id: itemOrId,
title: '',
};
const item =
typeof itemOrId === 'object'
? itemOrId
: {
id: itemOrId,
title: '',
};
const output = [];
output.push('[');
@ -737,7 +754,6 @@ class BaseItem extends BaseModel {
output.push('(:/' + item.id + ')');
return output.join('');
}
}
BaseItem.encryptionService_ = null;
@ -747,17 +763,9 @@ BaseItem.revisionService_ = null;
// - itemsThatNeedSync()
// - syncedItems()
BaseItem.syncItemDefinitions_ = [
{ type: BaseModel.TYPE_NOTE, className: 'Note' },
{ type: BaseModel.TYPE_FOLDER, className: 'Folder' },
{ type: BaseModel.TYPE_RESOURCE, className: 'Resource' },
{ type: BaseModel.TYPE_TAG, className: 'Tag' },
{ type: BaseModel.TYPE_NOTE_TAG, className: 'NoteTag' },
{ type: BaseModel.TYPE_MASTER_KEY, className: 'MasterKey' },
{ type: BaseModel.TYPE_REVISION, className: 'Revision' },
];
BaseItem.syncItemDefinitions_ = [{ type: BaseModel.TYPE_NOTE, className: 'Note' }, { type: BaseModel.TYPE_FOLDER, className: 'Folder' }, { type: BaseModel.TYPE_RESOURCE, className: 'Resource' }, { type: BaseModel.TYPE_TAG, className: 'Tag' }, { type: BaseModel.TYPE_NOTE_TAG, className: 'NoteTag' }, { type: BaseModel.TYPE_MASTER_KEY, className: 'MasterKey' }, { type: BaseModel.TYPE_REVISION, className: 'Revision' }];
BaseItem.SYNC_ITEM_LOCATION_LOCAL = 1;
BaseItem.SYNC_ITEM_LOCATION_REMOTE = 2;
module.exports = BaseItem;
module.exports = BaseItem;

View File

@ -10,7 +10,6 @@ const BaseItem = require('lib/models/BaseItem.js');
const { substrWithEllipsis } = require('lib/string-utils.js');
class Folder extends BaseItem {
static tableName() {
return 'folders';
}
@ -18,12 +17,12 @@ class Folder extends BaseItem {
static modelType() {
return BaseModel.TYPE_FOLDER;
}
static newFolder() {
return {
id: null,
title: '',
}
};
}
static fieldToLabel(field) {
@ -36,14 +35,16 @@ class Folder extends BaseItem {
}
static noteIds(parentId) {
return this.db().selectAll('SELECT id FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]).then((rows) => {
let output = [];
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
output.push(row.id);
}
return output;
});
return this.db()
.selectAll('SELECT id FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId])
.then(rows => {
let output = [];
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
output.push(row.id);
}
return output;
});
}
static async subFolderIds(parentId) {
@ -68,7 +69,7 @@ class Folder extends BaseItem {
let folder = await Folder.load(folderId);
if (!folder) return; // noop
if (options.deleteChildren) {
if (options.deleteChildren) {
let noteIds = await Folder.noteIds(folderId);
for (let i = 0; i < noteIds.length; i++) {
await Note.delete(noteIds[i]);
@ -128,7 +129,7 @@ class Folder extends BaseItem {
if (folders[i].id === folder.parent_id) return folders[i];
}
throw new Error('Could not find parent');
}
};
const applyChildTimeToParent = folderId => {
const parent = findFolderParent(folderId);
@ -139,9 +140,9 @@ class Folder extends BaseItem {
} else {
folderIdToTime[parent.id] = folderIdToTime[folderId];
}
applyChildTimeToParent(parent.id);
}
};
for (let folderId in folderIdToTime) {
if (!folderIdToTime.hasOwnProperty(folderId)) continue;
@ -194,24 +195,24 @@ class Folder extends BaseItem {
// https://stackoverflow.com/a/49387427/561309
function getNestedChildren(models, parentId) {
const nestedTreeStructure = [];
const length = models.length;
const nestedTreeStructure = [];
const length = models.length;
for (let i = 0; i < length; i++) {
const model = models[i];
for (let i = 0; i < length; i++) {
const model = models[i];
if (model.parent_id == parentId) {
const children = getNestedChildren(models, model.id);
if (model.parent_id == parentId) {
const children = getNestedChildren(models, model.id);
if (children.length > 0) {
model.children = children;
}
if (children.length > 0) {
model.children = children;
}
nestedTreeStructure.push(model);
}
}
nestedTreeStructure.push(model);
}
}
return nestedTreeStructure;
return nestedTreeStructure;
}
return getNestedChildren(all, '');
@ -329,7 +330,7 @@ class Folder extends BaseItem {
// These "duplicateCheck" and "reservedTitleCheck" should only be done when a user is
// manually creating a folder. They shouldn't be done for example when the folders
// are being synced to avoid any strange side-effects. Technically it's possible to
// are being synced to avoid any strange side-effects. Technically it's possible to
// have folders and notes with duplicate titles (or no title), or with reserved words.
static async save(o, options = null) {
if (!options) options = {};
@ -337,11 +338,11 @@ class Folder extends BaseItem {
if (options.userSideValidation === true) {
if (!('duplicateCheck' in options)) options.duplicateCheck = true;
if (!('reservedTitleCheck' in options)) options.reservedTitleCheck = true;
if (!('stripLeftSlashes' in options)) options.stripLeftSlashes = true;
if (!('stripLeftSlashes' in options)) options.stripLeftSlashes = true;
}
if (options.stripLeftSlashes === true && o.title) {
while (o.title.length && (o.title[0] == '/' || o.title[0] == "\\")) {
while (o.title.length && (o.title[0] == '/' || o.title[0] == '\\')) {
o.title = o.title.substr(1);
}
}
@ -364,7 +365,7 @@ class Folder extends BaseItem {
if (o.title == Folder.conflictFolderTitle()) throw new Error(_('Notebooks cannot be named "%s", which is a reserved title.', o.title));
}
return super.save(o, options).then((folder) => {
return super.save(o, options).then(folder => {
this.dispatch({
type: 'FOLDER_UPDATE_ONE',
item: folder,
@ -372,7 +373,6 @@ class Folder extends BaseItem {
return folder;
});
}
}
module.exports = Folder;
module.exports = Folder;

View File

@ -2,7 +2,6 @@ const BaseModel = require('lib/BaseModel.js');
const Mutex = require('async-mutex').Mutex;
class ItemChange extends BaseModel {
static tableName() {
return 'item_changes';
}
@ -22,10 +21,7 @@ class ItemChange extends BaseModel {
const release = await ItemChange.addChangeMutex_.acquire();
try {
await this.db().transactionExecBatch([
{ sql: 'DELETE FROM item_changes WHERE item_id = ?', params: [itemId] },
{ sql: 'INSERT INTO item_changes (item_type, item_id, type, source, created_time, before_change_item) VALUES (?, ?, ?, ?, ?, ?)', params: [itemType, itemId, type, changeSource, Date.now(), beforeChangeItemJson] },
]);
await this.db().transactionExecBatch([{ sql: 'DELETE FROM item_changes WHERE item_id = ?', params: [itemId] }, { sql: 'INSERT INTO item_changes (item_type, item_id, type, source, created_time, before_change_item) VALUES (?, ?, ?, ?, ?, ?)', params: [itemType, itemId, type, changeSource, Date.now(), beforeChangeItemJson] }]);
} finally {
release();
ItemChange.saveCalls_.pop();
@ -54,7 +50,6 @@ class ItemChange extends BaseModel {
if (!lowestChangeId) return;
return this.db().exec('DELETE FROM item_changes WHERE id <= ?', [lowestChangeId]);
}
}
ItemChange.addChangeMutex_ = new Mutex();
@ -68,4 +63,4 @@ ItemChange.SOURCE_UNSPECIFIED = 1;
ItemChange.SOURCE_SYNC = 2;
ItemChange.SOURCE_DECRYPTION = 2;
module.exports = ItemChange;
module.exports = ItemChange;

View File

@ -2,7 +2,6 @@ const BaseModel = require('lib/BaseModel.js');
const BaseItem = require('lib/models/BaseItem.js');
class MasterKey extends BaseItem {
static tableName() {
return 'master_keys';
}
@ -20,7 +19,7 @@ class MasterKey extends BaseItem {
}
static async save(o, options = null) {
return super.save(o, options).then((item) => {
return super.save(o, options).then(item => {
this.dispatch({
type: 'MASTERKEY_UPDATE_ONE',
item: item,
@ -28,7 +27,6 @@ class MasterKey extends BaseItem {
return item;
});
}
}
module.exports = MasterKey;
module.exports = MasterKey;

View File

@ -5,7 +5,6 @@ const migrationScripts = {
};
class Migration extends BaseModel {
static tableName() {
return 'migrations';
}
@ -21,7 +20,6 @@ class Migration extends BaseModel {
static script(number) {
return migrationScripts[number];
}
}
module.exports = Migration;
module.exports = Migration;

View File

@ -14,7 +14,6 @@ const moment = require('moment');
const lodash = require('lodash');
class Note extends BaseItem {
static tableName() {
return 'notes';
}
@ -34,7 +33,7 @@ class Note extends BaseItem {
}
static async unserializeForEdit(content) {
content += "\n\ntype_: " + BaseModel.TYPE_NOTE;
content += '\n\ntype_: ' + BaseModel.TYPE_NOTE;
let output = await super.unserialize(content);
if (!output.title) output.title = '';
if (!output.body) output.body = '';
@ -83,12 +82,12 @@ class Note extends BaseItem {
static defaultTitleFromBody(body) {
if (body && body.length) {
const lines = body.trim().split("\n");
const lines = body.trim().split('\n');
let output = lines[0].trim();
// Remove the first #, *, etc.
while (output.length) {
const c = output[0];
if (['#', ' ', "\n", "\t", '*', '`', '-'].indexOf(c) >= 0) {
if (['#', ' ', '\n', '\t', '*', '`', '-'].indexOf(c) >= 0) {
output = output.substr(1);
} else {
break;
@ -107,7 +106,7 @@ class Note extends BaseItem {
}
static geoLocationUrlFromLatLong(lat, long) {
return sprintf('https://www.openstreetmap.org/?lat=%s&lon=%s&zoom=20', lat, long)
return sprintf('https://www.openstreetmap.org/?lat=%s&lon=%s&zoom=20', lat, long);
}
static modelType() {
@ -120,16 +119,16 @@ class Note extends BaseItem {
// For example: ![](:/fcca2938a96a22570e8eae2565bc6b0b)
let matches = body.match(/\(:\/[a-zA-Z0-9]{32}\)/g);
if (!matches) matches = [];
matches = matches.map((m) => m.substr(3, 32));
matches = matches.map(m => m.substr(3, 32));
// For example: ![](:/fcca2938a96a22570e8eae2565bc6b0b "Some title")
let matches2 = body.match(/\(:\/[a-zA-Z0-9]{32}\s(.*?)\)/g);
if (!matches2) matches2 = [];
matches2 = matches2.map((m) => m.substr(3, 32));
matches = matches.concat(matches2)
matches2 = matches2.map(m => m.substr(3, 32));
matches = matches.concat(matches2);
// For example: <img src=":/fcca2938a96a22570e8eae2565bc6b0b"/>
const imgRegex = /<img[\s\S]*?src=["']:\/([a-zA-Z0-9]{32})["'][\s\S]*?>/gi
const imgRegex = /<img[\s\S]*?src=["']:\/([a-zA-Z0-9]{32})["'][\s\S]*?>/gi;
const imgMatches = [];
while (true) {
const m = imgRegex.exec(body);
@ -170,7 +169,7 @@ class Note extends BaseItem {
const id = resourceIds[i];
const resource = await Resource.load(id);
if (!resource) continue;
const resourcePath = Resource.relativePath(resource)
const resourcePath = Resource.relativePath(resource);
body = body.replace(new RegExp(':/' + id, 'gi'), resourcePath);
}
@ -178,9 +177,9 @@ class Note extends BaseItem {
}
static async replaceResourceExternalToInternalLinks(body) {
const reString = pregQuote(Resource.baseRelativeDirectoryPath() + '/') + '[a-zA-Z0-9\.]+';
const reString = pregQuote(Resource.baseRelativeDirectoryPath() + '/') + '[a-zA-Z0-9.]+';
const re = new RegExp(reString, 'gi');
body = body.replace(re, (match) => {
body = body.replace(re, match => {
const id = Resource.pathToId(match);
return ':/' + id;
});
@ -201,28 +200,31 @@ class Note extends BaseItem {
// Note: sort logic must be duplicated in previews();
static sortNotes(notes, orders, uncompletedTodosOnTop) {
const noteOnTop = (note) => {
const noteOnTop = note => {
return uncompletedTodosOnTop && note.is_todo && !note.todo_completed;
}
};
const noteFieldComp = (f1, f2) => {
if (f1 === f2) return 0;
return f1 < f2 ? -1 : +1;
}
};
// Makes the sort deterministic, so that if, for example, a and b have the
// same updated_time, they aren't swapped every time a list is refreshed.
const sortIdenticalNotes = (a, b) => {
let r = null;
r = noteFieldComp(a.user_updated_time, b.user_updated_time); if (r) return r;
r = noteFieldComp(a.user_created_time, b.user_created_time); if (r) return r;
r = noteFieldComp(a.user_updated_time, b.user_updated_time);
if (r) return r;
r = noteFieldComp(a.user_created_time, b.user_created_time);
if (r) return r;
const titleA = a.title ? a.title.toLowerCase() : '';
const titleB = b.title ? b.title.toLowerCase() : '';
r = noteFieldComp(titleA, titleB); if (r) return r;
r = noteFieldComp(titleA, titleB);
if (r) return r;
return noteFieldComp(a.id, b.id);
}
};
return notes.sort((a, b) => {
if (noteOnTop(a) && !noteOnTop(b)) return -1;
@ -253,7 +255,9 @@ class Note extends BaseItem {
static previewFieldsSql(fields = null) {
if (fields === null) fields = this.previewFields();
return this.db().escapeFields(fields).join(',');
return this.db()
.escapeFields(fields)
.join(',');
}
static async loadFolderNoteByField(folderId, field, value) {
@ -263,7 +267,7 @@ class Note extends BaseItem {
conditions: ['`' + field + '` = ?'],
conditionsParams: [value],
fields: '*',
}
};
let results = await this.previews(folderId, options);
return results.length ? results[0] : null;
@ -274,12 +278,7 @@ class Note extends BaseItem {
// is used to sort already loaded notes.
if (!options) options = {};
if (!('order' in options)) options.order = [
{ by: 'user_updated_time', dir: 'DESC' },
{ by: 'user_created_time', dir: 'DESC' },
{ by: 'title', dir: 'DESC' },
{ by: 'id', dir: 'DESC' },
];
if (!('order' in options)) options.order = [{ by: 'user_updated_time', dir: 'DESC' }, { by: 'user_created_time', dir: 'DESC' }, { by: 'title', dir: 'DESC' }, { by: 'id', dir: 'DESC' }];
if (!options.conditions) options.conditions = [];
if (!options.conditionsParams) options.conditionsParams = [];
if (!options.fields) options.fields = this.previewFields();
@ -342,7 +341,6 @@ class Note extends BaseItem {
}
if (hasNotes && hasTodos) {
} else if (hasNotes) {
options.conditions.push('is_todo = 0');
} else if (hasTodos) {
@ -485,7 +483,7 @@ class Note extends BaseItem {
}
static toggleIsTodo(note) {
return this.changeNoteType(note, !!note.is_todo ? 'note' : 'todo');
return this.changeNoteType(note, note.is_todo ? 'note' : 'todo');
}
static toggleTodoCompleted(note) {
@ -497,7 +495,7 @@ class Note extends BaseItem {
} else {
note.todo_completed = Date.now();
}
return note;
}
@ -631,7 +629,6 @@ class Note extends BaseItem {
if (markupLanguageId === Note.MARKUP_LANGUAGE_HTML) return 'HTML';
throw new Error('Invalid markup language ID: ' + markupLanguageId);
}
}
Note.updateGeolocationEnabled_ = true;
@ -640,4 +637,4 @@ Note.geolocationUpdating_ = false;
Note.MARKUP_LANGUAGE_MARKDOWN = 1;
Note.MARKUP_LANGUAGE_HTML = 2;
module.exports = Note;
module.exports = Note;

View File

@ -6,7 +6,6 @@ const BaseModel = require('lib/BaseModel.js');
// - If last_seen_time is 0, it means the resource has never been associated with any note.
class NoteResource extends BaseModel {
static tableName() {
return 'note_resources';
}
@ -44,7 +43,7 @@ class NoteResource extends BaseModel {
const queries = [];
for (let i = 0; i < missingResources.length; i++) {
const id = missingResources[i].id;
queries.push({ sql: 'INSERT INTO note_resources (note_id, resource_id, is_associated, last_seen_time) VALUES (?, ?, ?, ?)', params: ["", id, 0, 0] });
queries.push({ sql: 'INSERT INTO note_resources (note_id, resource_id, is_associated, last_seen_time) VALUES (?, ?, ?, ?)', params: ['', id, 0, 0] });
}
await this.db().transactionExecBatch(queries);
}
@ -56,21 +55,23 @@ class NoteResource extends BaseModel {
static async orphanResources(expiryDelay = null) {
if (expiryDelay === null) expiryDelay = 1000 * 60 * 60 * 24 * 10;
const cutOffTime = Date.now() - expiryDelay;
const output = await this.modelSelectAll(`
const output = await this.modelSelectAll(
`
SELECT resource_id, sum(is_associated)
FROM note_resources
GROUP BY resource_id
HAVING sum(is_associated) <= 0
AND last_seen_time < ?
AND last_seen_time != 0
`, [cutOffTime]);
`,
[cutOffTime]
);
return output.map(r => r.resource_id);
}
static async deleteByResource(resourceId) {
await this.db().exec('DELETE FROM note_resources WHERE resource_id = ?', [resourceId]);
}
}
module.exports = NoteResource;
module.exports = NoteResource;

View File

@ -2,7 +2,6 @@ const BaseItem = require('lib/models/BaseItem.js');
const BaseModel = require('lib/BaseModel.js');
class NoteTag extends BaseItem {
static tableName() {
return 'note_tags';
}
@ -24,7 +23,6 @@ class NoteTag extends BaseItem {
}
return output;
}
}
module.exports = NoteTag;
module.exports = NoteTag;

View File

@ -13,7 +13,6 @@ const markdownUtils = require('lib/markdownUtils');
const JoplinError = require('lib/JoplinError');
class Resource extends BaseItem {
static tableName() {
return 'resources';
}
@ -28,7 +27,7 @@ class Resource extends BaseItem {
}
static isSupportedImageMimeType(type) {
const imageMimeTypes = ["image/jpg", "image/jpeg", "image/png", "image/gif", "image/svg+xml", "image/webp"];
const imageMimeTypes = ['image/jpg', 'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp'];
return imageMimeTypes.indexOf(type.toLowerCase()) >= 0;
}
@ -61,7 +60,7 @@ class Resource extends BaseItem {
if (!output) output = resource.id;
let extension = resource.file_extension;
if (!extension) extension = resource.mime ? mime.toFileExtension(resource.mime) : '';
extension = extension ? ('.' + extension) : '';
extension = extension ? '.' + extension : '';
return output + extension;
}
@ -76,7 +75,7 @@ class Resource extends BaseItem {
static filename(resource, encryptedBlob = false) {
let extension = encryptedBlob ? 'crypted' : resource.file_extension;
if (!extension) extension = resource.mime ? mime.toFileExtension(resource.mime) : '';
extension = extension ? ('.' + extension) : '';
extension = extension ? '.' + extension : '';
return resource.id + extension;
}
@ -126,7 +125,7 @@ class Resource extends BaseItem {
// As the identifier is invalid it most likely means that this is not encrypted data
// at all. It can happen for example when there's a crash between the moment the data
// is decrypted and the resource item is updated.
this.logger().warn('Found a resource that was most likely already decrypted but was marked as encrypted. Marked it as decrypted: ' + item.id)
this.logger().warn('Found a resource that was most likely already decrypted but was marked as encrypted. Marked it as decrypted: ' + item.id);
this.fsDriver().move(encryptedPath, plainTextPath);
} else {
throw error;
@ -147,7 +146,7 @@ class Resource extends BaseItem {
if (!Setting.value('encryption.enabled')) {
// Normally not possible since itemsThatNeedSync should only return decrypted items
if (!!resource.encryption_blob_encrypted) throw new Error('Trying to access encrypted resource but encryption is currently disabled');
if (resource.encryption_blob_encrypted) throw new Error('Trying to access encrypted resource but encryption is currently disabled');
return { path: plainTextPath, resource: resource };
}
@ -171,13 +170,13 @@ class Resource extends BaseItem {
if (!tagAlt) tagAlt = '';
let lines = [];
if (Resource.isSupportedImageMimeType(resource.mime)) {
lines.push("![");
lines.push('![');
lines.push(markdownUtils.escapeLinkText(tagAlt));
lines.push("](:/" + resource.id + ")");
lines.push('](:/' + resource.id + ')');
} else {
lines.push("[");
lines.push('[');
lines.push(markdownUtils.escapeLinkText(tagAlt));
lines.push("](:/" + resource.id + ")");
lines.push('](:/' + resource.id + ')');
}
return lines.join('');
}
@ -252,13 +251,10 @@ class Resource extends BaseItem {
}
static async downloadedButEncryptedBlobCount() {
const r = await this.db().selectOne('SELECT count(*) as total FROM resource_local_states WHERE fetch_status = ? AND resource_id IN (SELECT id FROM resources WHERE encryption_blob_encrypted = 1)', [
Resource.FETCH_STATUS_DONE,
]);
const r = await this.db().selectOne('SELECT count(*) as total FROM resource_local_states WHERE fetch_status = ? AND resource_id IN (SELECT id FROM resources WHERE encryption_blob_encrypted = 1)', [Resource.FETCH_STATUS_DONE]);
return r ? r.total : 0;
}
}
Resource.IMAGE_MAX_DIMENSION = 1920;
@ -268,4 +264,4 @@ Resource.FETCH_STATUS_STARTED = 1;
Resource.FETCH_STATUS_DONE = 2;
Resource.FETCH_STATUS_ERROR = 3;
module.exports = Resource;
module.exports = Resource;

View File

@ -2,7 +2,6 @@ const BaseModel = require('lib/BaseModel.js');
const { Database } = require('lib/database.js');
class ResourceLocalState extends BaseModel {
static tableName() {
return 'resource_local_states';
}
@ -15,7 +14,7 @@ class ResourceLocalState extends BaseModel {
if (!resourceId) throw new Error('Resource ID not provided'); // Sanity check
const result = await this.modelSelectOne('SELECT * FROM resource_local_states WHERE resource_id = ?', [resourceId]);
if (!result) {
const defaultRow = this.db().createDefaultRow(this.tableName());
delete defaultRow.id;
@ -27,10 +26,7 @@ class ResourceLocalState extends BaseModel {
}
static async save(o) {
const queries = [
{ sql: 'DELETE FROM resource_local_states WHERE resource_id = ?', params: [o.resource_id] },
Database.insertQuery(this.tableName(), o),
];
const queries = [{ sql: 'DELETE FROM resource_local_states WHERE resource_id = ?', params: [o.resource_id] }, Database.insertQuery(this.tableName(), o)];
return this.db().transactionExecBatch(queries);
}
@ -40,7 +36,6 @@ class ResourceLocalState extends BaseModel {
options.idFieldName = 'resource_id';
return super.batchDelete(ids, options);
}
}
module.exports = ResourceLocalState;
module.exports = ResourceLocalState;

View File

@ -12,7 +12,6 @@ const { sprintf } = require('sprintf-js');
const dmp = new DiffMatchPatch();
class Revision extends BaseItem {
static tableName() {
return 'revisions';
}
@ -57,7 +56,7 @@ class Revision extends BaseItem {
static applyObjectPatch(object, patch) {
patch = JSON.parse(patch);
const output = Object.assign({}, object);
for (let k in patch.new) {
output[k] = patch.new[k];
}
@ -74,7 +73,7 @@ class Revision extends BaseItem {
const countChars = diffLine => {
return unescape(diffLine).length - 1;
}
};
const lines = patch.split('\n');
let added = 0;
@ -113,36 +112,25 @@ class Revision extends BaseItem {
}
static async countRevisions(itemType, itemId) {
const r = await this.db().selectOne('SELECT count(*) as total FROM revisions WHERE item_type = ? AND item_id = ?', [
itemType,
itemId,
]);
const r = await this.db().selectOne('SELECT count(*) as total FROM revisions WHERE item_type = ? AND item_id = ?', [itemType, itemId]);
return r ? r.total : 0;
}
static latestRevision(itemType, itemId) {
return this.modelSelectOne('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? ORDER BY item_updated_time DESC LIMIT 1', [
itemType,
itemId,
]);
return this.modelSelectOne('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? ORDER BY item_updated_time DESC LIMIT 1', [itemType, itemId]);
}
static allByType(itemType, itemId) {
return this.modelSelectAll('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? ORDER BY item_updated_time ASC', [
itemType,
itemId,
]);
return this.modelSelectAll('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? ORDER BY item_updated_time ASC', [itemType, itemId]);
}
static async itemsWithRevisions(itemType, itemIds) {
if (!itemIds.length) return [];
const rows = await this.db().selectAll('SELECT distinct item_id FROM revisions WHERE item_type = ? AND item_id IN ("' + itemIds.join('","') + '")', [
itemType,
]);
const rows = await this.db().selectAll('SELECT distinct item_id FROM revisions WHERE item_type = ? AND item_id IN ("' + itemIds.join('","') + '")', [itemType]);
return rows.map(r => r.item_id);
}
}
static async itemsWithNoRevisions(itemType, itemIds) {
const withRevs = await this.itemsWithRevisions(itemType, itemIds);
@ -180,11 +168,7 @@ class Revision extends BaseItem {
if (!('encryption_applied' in revision) || !!revision.encryption_applied) throw new JoplinError('Target revision is encrypted', 'revision_encrypted');
if (!revs) {
revs = await this.modelSelectAll('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? AND item_updated_time <= ? ORDER BY item_updated_time ASC', [
revision.item_type,
revision.item_id,
revision.item_updated_time,
]);
revs = await this.modelSelectAll('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? AND item_updated_time <= ? ORDER BY item_updated_time ASC', [revision.item_type, revision.item_id, revision.item_updated_time]);
} else {
revs = revs.slice();
}
@ -213,7 +197,7 @@ class Revision extends BaseItem {
for (const revIndex of revIndexes) {
const rev = revs[revIndex];
if (!!rev.encryption_applied) throw new JoplinError(sprintf('Revision "%s" is encrypted', rev.id), 'revision_encrypted');
if (rev.encryption_applied) throw new JoplinError(sprintf('Revision "%s" is encrypted', rev.id), 'revision_encrypted');
output.title = this.applyTextPatch(output.title, rev.title_diff);
output.body = this.applyTextPatch(output.body, rev.body_diff);
output.metadata = this.applyObjectPatch(output.metadata, rev.metadata_diff);
@ -236,11 +220,7 @@ class Revision extends BaseItem {
const doneKey = rev.item_type + '_' + rev.item_id;
if (doneItems[doneKey]) continue;
const keptRev = await this.modelSelectOne('SELECT * FROM revisions WHERE item_updated_time >= ? AND item_type = ? AND item_id = ? ORDER BY item_updated_time ASC LIMIT 1', [
cutOffDate,
rev.item_type,
rev.item_id,
]);
const keptRev = await this.modelSelectOne('SELECT * FROM revisions WHERE item_updated_time >= ? AND item_type = ? AND item_id = ? ORDER BY item_updated_time ASC LIMIT 1', [cutOffDate, rev.item_type, rev.item_id]);
try {
const deleteQueryCondition = 'item_updated_time < ? AND item_id = ?';
@ -249,7 +229,7 @@ class Revision extends BaseItem {
if (!keptRev) {
const hasEncrypted = await this.modelSelectOne('SELECT * FROM revisions WHERE encryption_applied = 1 AND ' + deleteQueryCondition, deleteQueryParams);
if (!!hasEncrypted) throw new JoplinError('One of the revision to be deleted is encrypted', 'revision_encrypted');
if (hasEncrypted) throw new JoplinError('One of the revision to be deleted is encrypted', 'revision_encrypted');
await this.db().transactionExecBatch([deleteQuery]);
} else {
// Note: we don't need to check for encrypted rev here because
@ -257,15 +237,7 @@ class Revision extends BaseItem {
// if a rev is encrypted.
const merged = await this.mergeDiffs(keptRev);
const queries = [
deleteQuery,
{ sql: 'UPDATE revisions SET title_diff = ?, body_diff = ?, metadata_diff = ? WHERE id = ?', params: [
this.createTextPatch('', merged.title),
this.createTextPatch('', merged.body),
this.createObjectPatch({}, merged.metadata),
keptRev.id,
] },
];
const queries = [deleteQuery, { sql: 'UPDATE revisions SET title_diff = ?, body_diff = ?, metadata_diff = ? WHERE id = ?', params: [this.createTextPatch('', merged.title), this.createTextPatch('', merged.body), this.createObjectPatch({}, merged.metadata), keptRev.id] }];
await this.db().transactionExecBatch(queries);
}
@ -285,7 +257,6 @@ class Revision extends BaseItem {
const existingRev = await Revision.latestRevision(itemType, itemId);
return existingRev && existingRev.item_updated_time === updatedTime;
}
}
module.exports = Revision;

View File

@ -2,7 +2,6 @@ const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
class Search extends BaseModel {
static tableName() {
throw new Error('Not using database');
}
@ -17,7 +16,6 @@ class Search extends BaseModel {
output = output.filter(o => !!o);
return output;
}
}
module.exports = Search;
module.exports = Search;

View File

@ -11,7 +11,6 @@ const { _, supportedLocalesToLanguages, defaultLocale } = require('lib/locale.js
const { shim } = require('lib/shim');
class Setting extends BaseModel {
static tableName() {
return 'settings';
}
@ -32,27 +31,105 @@ class Setting extends BaseModel {
// public for the mobile and desktop apps because they are handled separately in menus.
this.metadata_ = {
'sync.target': { value: SyncTargetRegistry.nameToId('dropbox'), type: Setting.TYPE_INT, isEnum: true, public: true, section:'sync', label: () => _('Synchronisation target'), description: (appType) => { return appType !== 'cli' ? null : _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).') }, options: () => {
return SyncTargetRegistry.idAndLabelPlainObject();
}},
'sync.target': {
value: SyncTargetRegistry.nameToId('dropbox'),
type: Setting.TYPE_INT,
isEnum: true,
public: true,
section: 'sync',
label: () => _('Synchronisation target'),
description: appType => {
return appType !== 'cli' ? null : _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).');
},
options: () => {
return SyncTargetRegistry.idAndLabelPlainObject();
},
},
'sync.2.path': { value: '', type: Setting.TYPE_STRING, section:'sync', show: (settings) => {
try {
return settings['sync.target'] == SyncTargetRegistry.nameToId('filesystem')
} catch (error) {
return false;
}
}, filter: (value) => {
return value ? rtrimSlashes(value) : '';
}, public: true, label: () => _('Directory to synchronise with (absolute path)'), description: () => emptyDirWarning },
'sync.2.path': {
value: '',
type: Setting.TYPE_STRING,
section: 'sync',
show: settings => {
try {
return settings['sync.target'] == SyncTargetRegistry.nameToId('filesystem');
} catch (error) {
return false;
}
},
filter: value => {
return value ? rtrimSlashes(value) : '';
},
public: true,
label: () => _('Directory to synchronise with (absolute path)'),
description: () => emptyDirWarning,
},
'sync.5.path': { value: '', type: Setting.TYPE_STRING, section:'sync', show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nextcloud WebDAV URL'), description: () => emptyDirWarning },
'sync.5.username': { value: '', type: Setting.TYPE_STRING, section:'sync', show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nextcloud username') },
'sync.5.password': { value: '', type: Setting.TYPE_STRING, section:'sync', show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nextcloud password'), secure: true },
'sync.5.path': {
value: '',
type: Setting.TYPE_STRING,
section: 'sync',
show: settings => {
return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud');
},
public: true,
label: () => _('Nextcloud WebDAV URL'),
description: () => emptyDirWarning,
},
'sync.5.username': {
value: '',
type: Setting.TYPE_STRING,
section: 'sync',
show: settings => {
return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud');
},
public: true,
label: () => _('Nextcloud username'),
},
'sync.5.password': {
value: '',
type: Setting.TYPE_STRING,
section: 'sync',
show: settings => {
return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud');
},
public: true,
label: () => _('Nextcloud password'),
secure: true,
},
'sync.6.path': { value: '', type: Setting.TYPE_STRING, section:'sync', show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV URL'), description: () => emptyDirWarning },
'sync.6.username': { value: '', type: Setting.TYPE_STRING, section:'sync', show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV username') },
'sync.6.password': { value: '', type: Setting.TYPE_STRING, section:'sync', show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV password'), secure: true },
'sync.6.path': {
value: '',
type: Setting.TYPE_STRING,
section: 'sync',
show: settings => {
return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav');
},
public: true,
label: () => _('WebDAV URL'),
description: () => emptyDirWarning,
},
'sync.6.username': {
value: '',
type: Setting.TYPE_STRING,
section: 'sync',
show: settings => {
return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav');
},
public: true,
label: () => _('WebDAV username'),
},
'sync.6.password': {
value: '',
type: Setting.TYPE_STRING,
section: 'sync',
show: settings => {
return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav');
},
public: true,
label: () => _('WebDAV password'),
secure: true,
},
'sync.3.auth': { value: '', type: Setting.TYPE_STRING, public: false },
'sync.4.auth': { value: '', type: Setting.TYPE_STRING, public: false },
@ -65,151 +142,273 @@ class Setting extends BaseModel {
'sync.6.context': { value: '', type: Setting.TYPE_STRING, public: false },
'sync.7.context': { value: '', type: Setting.TYPE_STRING, public: false },
'sync.resourceDownloadMode': { value: 'always', type: Setting.TYPE_STRING, section: 'sync', public: true, isEnum: true, appTypes: ['mobile', 'desktop'], label: () => _('Attachment download behaviour'), description: () => _('In "Manual" mode, attachments are downloaded only when you click on them. In "Auto", they are downloaded when you open the note. In "Always", all the attachments are downloaded whether you open the note or not.'), options: () => {
return {
'always': _('Always'),
'manual': _('Manual'),
'auto': _('Auto'),
};
}},
'sync.resourceDownloadMode': {
value: 'always',
type: Setting.TYPE_STRING,
section: 'sync',
public: true,
isEnum: true,
appTypes: ['mobile', 'desktop'],
label: () => _('Attachment download behaviour'),
description: () => _('In "Manual" mode, attachments are downloaded only when you click on them. In "Auto", they are downloaded when you open the note. In "Always", all the attachments are downloaded whether you open the note or not.'),
options: () => {
return {
always: _('Always'),
manual: _('Manual'),
auto: _('Auto'),
};
},
},
'sync.maxConcurrentConnections': {value: 5, type: Setting.TYPE_INT, public: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1},
'sync.maxConcurrentConnections': { value: 5, type: Setting.TYPE_INT, public: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
'activeFolderId': { value: '', type: Setting.TYPE_STRING, public: false },
'firstStart': { value: true, type: Setting.TYPE_BOOL, public: false },
'locale': { value: defaultLocale(), type: Setting.TYPE_STRING, isEnum: true, public: true, label: () => _('Language'), options: () => {
return ObjectUtils.sortByValue(supportedLocalesToLanguages({ includeStats: true }));
}},
'dateFormat': { value: Setting.DATE_FORMAT_1, type: Setting.TYPE_STRING, isEnum: true, public: true, label: () => _('Date format'), options: () => {
let options = {}
const now = (new Date('2017-01-30T12:00:00')).getTime();
options[Setting.DATE_FORMAT_1] = time.formatMsToLocal(now, Setting.DATE_FORMAT_1);
options[Setting.DATE_FORMAT_2] = time.formatMsToLocal(now, Setting.DATE_FORMAT_2);
options[Setting.DATE_FORMAT_3] = time.formatMsToLocal(now, Setting.DATE_FORMAT_3);
options[Setting.DATE_FORMAT_4] = time.formatMsToLocal(now, Setting.DATE_FORMAT_4);
options[Setting.DATE_FORMAT_5] = time.formatMsToLocal(now, Setting.DATE_FORMAT_5);
options[Setting.DATE_FORMAT_6] = time.formatMsToLocal(now, Setting.DATE_FORMAT_6);
return options;
}},
'timeFormat': { value: Setting.TIME_FORMAT_1, type: Setting.TYPE_STRING, isEnum: true, public: true, label: () => _('Time format'), options: () => {
let options = {}
const now = (new Date('2017-01-30T20:30:00')).getTime();
options[Setting.TIME_FORMAT_1] = time.formatMsToLocal(now, Setting.TIME_FORMAT_1);
options[Setting.TIME_FORMAT_2] = time.formatMsToLocal(now, Setting.TIME_FORMAT_2);
return options;
}},
'theme': { value: Setting.THEME_LIGHT, type: Setting.TYPE_INT, public: true, appTypes: ['mobile', 'desktop'], isEnum: true, label: () => _('Theme'), section: 'appearance', options: () => {
let output = {};
output[Setting.THEME_LIGHT] = _('Light');
output[Setting.THEME_DARK] = _('Dark');
output[Setting.THEME_SOLARIZED_LIGHT] = _('Solarised Light');
output[Setting.THEME_SOLARIZED_DARK] = _('Solarised Dark');
return output;
}},
'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, appTypes: ['cli'], label: () => _('Uncompleted to-dos on top') },
'showCompletedTodos': { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, appTypes: ['cli'], label: () => _('Show completed to-dos') },
'notes.sortOrder.field': { value: 'user_updated_time', type: Setting.TYPE_STRING, section: 'note', isEnum: true, public: true, appTypes: ['cli'], label: () => _('Sort notes by'), options: () => {
const Note = require('lib/models/Note');
const noteSortFields = ['user_updated_time', 'user_created_time', 'title'];
const options = {};
for (let i = 0; i < noteSortFields.length; i++) {
options[noteSortFields[i]] = toTitleCase(Note.fieldToLabel(noteSortFields[i]));
}
return options;
}},
activeFolderId: { value: '', type: Setting.TYPE_STRING, public: false },
firstStart: { value: true, type: Setting.TYPE_BOOL, public: false },
locale: {
value: defaultLocale(),
type: Setting.TYPE_STRING,
isEnum: true,
public: true,
label: () => _('Language'),
options: () => {
return ObjectUtils.sortByValue(supportedLocalesToLanguages({ includeStats: true }));
},
},
dateFormat: {
value: Setting.DATE_FORMAT_1,
type: Setting.TYPE_STRING,
isEnum: true,
public: true,
label: () => _('Date format'),
options: () => {
let options = {};
const now = new Date('2017-01-30T12:00:00').getTime();
options[Setting.DATE_FORMAT_1] = time.formatMsToLocal(now, Setting.DATE_FORMAT_1);
options[Setting.DATE_FORMAT_2] = time.formatMsToLocal(now, Setting.DATE_FORMAT_2);
options[Setting.DATE_FORMAT_3] = time.formatMsToLocal(now, Setting.DATE_FORMAT_3);
options[Setting.DATE_FORMAT_4] = time.formatMsToLocal(now, Setting.DATE_FORMAT_4);
options[Setting.DATE_FORMAT_5] = time.formatMsToLocal(now, Setting.DATE_FORMAT_5);
options[Setting.DATE_FORMAT_6] = time.formatMsToLocal(now, Setting.DATE_FORMAT_6);
return options;
},
},
timeFormat: {
value: Setting.TIME_FORMAT_1,
type: Setting.TYPE_STRING,
isEnum: true,
public: true,
label: () => _('Time format'),
options: () => {
let options = {};
const now = new Date('2017-01-30T20:30:00').getTime();
options[Setting.TIME_FORMAT_1] = time.formatMsToLocal(now, Setting.TIME_FORMAT_1);
options[Setting.TIME_FORMAT_2] = time.formatMsToLocal(now, Setting.TIME_FORMAT_2);
return options;
},
},
theme: {
value: Setting.THEME_LIGHT,
type: Setting.TYPE_INT,
public: true,
appTypes: ['mobile', 'desktop'],
isEnum: true,
label: () => _('Theme'),
section: 'appearance',
options: () => {
let output = {};
output[Setting.THEME_LIGHT] = _('Light');
output[Setting.THEME_DARK] = _('Dark');
output[Setting.THEME_SOLARIZED_LIGHT] = _('Solarised Light');
output[Setting.THEME_SOLARIZED_DARK] = _('Solarised Dark');
return output;
},
},
uncompletedTodosOnTop: { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, appTypes: ['cli'], label: () => _('Uncompleted to-dos on top') },
showCompletedTodos: { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, appTypes: ['cli'], label: () => _('Show completed to-dos') },
'notes.sortOrder.field': {
value: 'user_updated_time',
type: Setting.TYPE_STRING,
section: 'note',
isEnum: true,
public: true,
appTypes: ['cli'],
label: () => _('Sort notes by'),
options: () => {
const Note = require('lib/models/Note');
const noteSortFields = ['user_updated_time', 'user_created_time', 'title'];
const options = {};
for (let i = 0; i < noteSortFields.length; i++) {
options[noteSortFields[i]] = toTitleCase(Note.fieldToLabel(noteSortFields[i]));
}
return options;
},
},
'notes.sortOrder.reverse': { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
'folders.sortOrder.field': { value: 'title', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['cli'], label: () => _('Sort notebooks by'), options: () => {
const Folder = require('lib/models/Folder');
const folderSortFields = ['title', 'last_note_user_updated_time'];
const options = {};
for (let i = 0; i < folderSortFields.length; i++) {
options[folderSortFields[i]] = toTitleCase(Folder.fieldToLabel(folderSortFields[i]));
}
return options;
}},
'folders.sortOrder.field': {
value: 'title',
type: Setting.TYPE_STRING,
isEnum: true,
public: true,
appTypes: ['cli'],
label: () => _('Sort notebooks by'),
options: () => {
const Folder = require('lib/models/Folder');
const folderSortFields = ['title', 'last_note_user_updated_time'];
const options = {};
for (let i = 0; i < folderSortFields.length; i++) {
options[folderSortFields[i]] = toTitleCase(Folder.fieldToLabel(folderSortFields[i]));
}
return options;
},
},
'folders.sortOrder.reverse': { value: false, type: Setting.TYPE_BOOL, public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
'trackLocation': { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, label: () => _('Save geo-location with notes') },
'newTodoFocus': { value: 'title', type: Setting.TYPE_STRING, section: 'note', isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new to-do:'), options: () => {
return {
'title': _('Focus title'),
'body': _('Focus body'),
};
}},
'newNoteFocus': { value: 'body', type: Setting.TYPE_STRING, section: 'note', isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new note:'), options: () => {
return {
'title': _('Focus title'),
'body': _('Focus body'),
};
}},
trackLocation: { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, label: () => _('Save geo-location with notes') },
newTodoFocus: {
value: 'title',
type: Setting.TYPE_STRING,
section: 'note',
isEnum: true,
public: true,
appTypes: ['desktop'],
label: () => _('When creating a new to-do:'),
options: () => {
return {
title: _('Focus title'),
body: _('Focus body'),
};
},
},
newNoteFocus: {
value: 'body',
type: Setting.TYPE_STRING,
section: 'note',
isEnum: true,
public: true,
appTypes: ['desktop'],
label: () => _('When creating a new note:'),
options: () => {
return {
title: _('Focus title'),
body: _('Focus body'),
};
},
},
'markdown.softbreaks': { value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable soft breaks') },
'markdown.plugin.katex': {value: true, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable math expressions')},
'markdown.plugin.mark': {value: true, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable ==mark== syntax')},
'markdown.plugin.footnote': {value: true, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable footnotes')},
'markdown.plugin.toc': {value: true, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable table of contents extension')},
'markdown.plugin.sub': {value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable ~sub~ syntax')},
'markdown.plugin.sup': {value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable ^sup^ syntax')},
'markdown.plugin.deflist': {value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable deflist syntax')},
'markdown.plugin.abbr': {value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable abbreviation syntax')},
'markdown.plugin.emoji': {value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable markdown emoji')},
'markdown.plugin.insert': {value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable ++insert++ syntax')},
'markdown.plugin.multitable': {value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable multimarkdown table extension')},
'markdown.plugin.katex': { value: true, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable math expressions') },
'markdown.plugin.mark': { value: true, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable ==mark== syntax') },
'markdown.plugin.footnote': { value: true, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable footnotes') },
'markdown.plugin.toc': { value: true, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable table of contents extension') },
'markdown.plugin.sub': { value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable ~sub~ syntax') },
'markdown.plugin.sup': { value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable ^sup^ syntax') },
'markdown.plugin.deflist': { value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable deflist syntax') },
'markdown.plugin.abbr': { value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable abbreviation syntax') },
'markdown.plugin.emoji': { value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable markdown emoji') },
'markdown.plugin.insert': { value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable ++insert++ syntax') },
'markdown.plugin.multitable': { value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable multimarkdown table extension') },
// Tray icon (called AppIndicator) doesn't work in Ubuntu
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
// Might be fixed in Electron 18.x but no non-beta release yet. So for now
// by default we disable it on Linux.
'showTrayIcon': { value: platform !== 'linux', type: Setting.TYPE_BOOL, section:'application', public: true, appTypes: ['desktop'], label: () => _('Show tray icon'), description: () => {
return platform === 'linux' ? _('Note: Does not work in all desktop environments.') : _('This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts.');
}},
showTrayIcon: {
value: platform !== 'linux',
type: Setting.TYPE_BOOL,
section: 'application',
public: true,
appTypes: ['desktop'],
label: () => _('Show tray icon'),
description: () => {
return platform === 'linux' ? _('Note: Does not work in all desktop environments.') : _('This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts.');
},
},
'startMinimized': { value: false, type: Setting.TYPE_BOOL, section:'application', public: true, appTypes: ['desktop'], label: () => _('Start application minimised in the tray icon') },
startMinimized: { value: false, type: Setting.TYPE_BOOL, section: 'application', public: true, appTypes: ['desktop'], label: () => _('Start application minimised in the tray icon') },
'collapsedFolderIds': { value: [], type: Setting.TYPE_ARRAY, public: false },
collapsedFolderIds: { value: [], type: Setting.TYPE_ARRAY, public: false },
'db.ftsEnabled': { value: -1, type: Setting.TYPE_INT, public: false },
'encryption.enabled': { value: false, type: Setting.TYPE_BOOL, public: false },
'encryption.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false },
'encryption.passwordCache': { value: {}, type: Setting.TYPE_OBJECT, public: false, secure: true },
'style.zoom': {value: 100, type: Setting.TYPE_INT, public: true, appTypes: ['desktop'], section: 'appearance', label: () => _('Global zoom percentage'), minimum: 50, maximum: 500, step: 10},
'style.editor.fontSize': {value: 13, type: Setting.TYPE_INT, public: true, appTypes: ['desktop'], section: 'appearance', label: () => _('Editor font size'), minimum: 4, maximum: 50, step: 1},
'style.editor.fontFamily': {value: "", type: Setting.TYPE_STRING, public: true, appTypes: ['desktop'], section: 'appearance', label: () => _('Editor font family'), description: () => _('This must be *monospace* font or it will not work properly. If the font is incorrect or empty, it will default to a generic monospace font.')},
'style.sidebar.width': {value: 150, minimum: 80, maximum: 400, type: Setting.TYPE_INT, public: false, appTypes: ['desktop'] },
'style.noteList.width': {value: 150, minimum: 80, maximum: 400, type: Setting.TYPE_INT, public: false, appTypes: ['desktop'] },
'autoUpdateEnabled': { value: true, type: Setting.TYPE_BOOL, section:'application', public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') },
'autoUpdate.includePreReleases': { value: false, type: Setting.TYPE_BOOL, section:'application', public: true, appTypes: ['desktop'], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
'style.zoom': { value: 100, type: Setting.TYPE_INT, public: true, appTypes: ['desktop'], section: 'appearance', label: () => _('Global zoom percentage'), minimum: 50, maximum: 500, step: 10 },
'style.editor.fontSize': { value: 13, type: Setting.TYPE_INT, public: true, appTypes: ['desktop'], section: 'appearance', label: () => _('Editor font size'), minimum: 4, maximum: 50, step: 1 },
'style.editor.fontFamily': { value: '', type: Setting.TYPE_STRING, public: true, appTypes: ['desktop'], section: 'appearance', label: () => _('Editor font family'), description: () => _('This must be *monospace* font or it will not work properly. If the font is incorrect or empty, it will default to a generic monospace font.') },
'style.sidebar.width': { value: 150, minimum: 80, maximum: 400, type: Setting.TYPE_INT, public: false, appTypes: ['desktop'] },
'style.noteList.width': { value: 150, minimum: 80, maximum: 400, type: Setting.TYPE_INT, public: false, appTypes: ['desktop'] },
autoUpdateEnabled: { value: true, type: Setting.TYPE_BOOL, section: 'application', public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') },
'autoUpdate.includePreReleases': { value: false, type: Setting.TYPE_BOOL, section: 'application', public: true, appTypes: ['desktop'], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
'clipperServer.autoStart': { value: false, type: Setting.TYPE_BOOL, public: false },
'sync.interval': { value: 300, type: Setting.TYPE_INT, section:'sync', isEnum: true, public: true, label: () => _('Synchronisation interval'), options: () => {
return {
0: _('Disabled'),
300: _('%d minutes', 5),
600: _('%d minutes', 10),
1800: _('%d minutes', 30),
3600: _('%d hour', 1),
43200: _('%d hours', 12),
86400: _('%d hours', 24),
};
}},
'noteVisiblePanes': { value: ['editor', 'viewer'], type: Setting.TYPE_ARRAY, public: false, appTypes: ['desktop'] },
'sidebarVisibility': { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
'tagHeaderIsExpanded': { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
'folderHeaderIsExpanded': { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
'editor': { value: '', type: Setting.TYPE_STRING, subType: 'file_path_and_args', public: true, appTypes: ['cli', 'desktop'], label: () => _('Text editor command'), description: () => _('The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.') },
'sync.interval': {
value: 300,
type: Setting.TYPE_INT,
section: 'sync',
isEnum: true,
public: true,
label: () => _('Synchronisation interval'),
options: () => {
return {
0: _('Disabled'),
300: _('%d minutes', 5),
600: _('%d minutes', 10),
1800: _('%d minutes', 30),
3600: _('%d hour', 1),
43200: _('%d hours', 12),
86400: _('%d hours', 24),
};
},
},
noteVisiblePanes: { value: ['editor', 'viewer'], type: Setting.TYPE_ARRAY, public: false, appTypes: ['desktop'] },
sidebarVisibility: { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
tagHeaderIsExpanded: { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
folderHeaderIsExpanded: { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
editor: { value: '', type: Setting.TYPE_STRING, subType: 'file_path_and_args', public: true, appTypes: ['cli', 'desktop'], label: () => _('Text editor command'), description: () => _('The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.') },
'net.customCertificates': { value: '', type: Setting.TYPE_STRING, section:'sync', show: (settings) => { return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav')].indexOf(settings['sync.target']) >= 0 }, public: true, appTypes: ['desktop', 'cli'], label: () => _('Custom TLS certificates'), description: () => _('Comma-separated list of paths to directories to load the certificates from, or path to individual cert files. For example: /my/cert_dir, /other/custom.pem. Note that if you make changes to the TLS settings, you must save your changes before clicking on "Check synchronisation configuration".') },
'net.ignoreTlsErrors': { value: false, type: Setting.TYPE_BOOL, section:'sync', show: (settings) => { return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav')].indexOf(settings['sync.target']) >= 0 }, public: true, appTypes: ['desktop', 'cli'], label: () => _('Ignore TLS certificate errors') },
'net.customCertificates': {
value: '',
type: Setting.TYPE_STRING,
section: 'sync',
show: settings => {
return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav')].indexOf(settings['sync.target']) >= 0;
},
public: true,
appTypes: ['desktop', 'cli'],
label: () => _('Custom TLS certificates'),
description: () => _('Comma-separated list of paths to directories to load the certificates from, or path to individual cert files. For example: /my/cert_dir, /other/custom.pem. Note that if you make changes to the TLS settings, you must save your changes before clicking on "Check synchronisation configuration".'),
},
'net.ignoreTlsErrors': {
value: false,
type: Setting.TYPE_BOOL,
section: 'sync',
show: settings => {
return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav')].indexOf(settings['sync.target']) >= 0;
},
public: true,
appTypes: ['desktop', 'cli'],
label: () => _('Ignore TLS certificate errors'),
},
'api.token': { value: null, type: Setting.TYPE_STRING, public: false },
'resourceService.lastProcessedChangeId': { value: 0, type: Setting.TYPE_INT, public: false },
'searchEngine.lastProcessedChangeId': { value: 0, type: Setting.TYPE_INT, public: false },
'revisionService.lastProcessedChangeId': { value: 0, type: Setting.TYPE_INT, public: false },
'searchEngine.initialIndexingDone': { value: false, type: Setting.TYPE_BOOL, public: false },
'revisionService.enabled': { section: 'revisionService', value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Enable note history') },
'revisionService.ttlDays': { section: 'revisionService', value: 90, type: Setting.TYPE_INT, public: true, minimum: 1, maximum: 365 * 2, step: 1, unitLabel: (value = null) => { return value === null ? _('days') : _('%d days', value) }, label: () => _('Keep note history for') },
'revisionService.ttlDays': {
section: 'revisionService',
value: 90,
type: Setting.TYPE_INT,
public: true,
minimum: 1,
maximum: 365 * 2,
step: 1,
unitLabel: (value = null) => {
return value === null ? _('days') : _('%d days', value);
},
label: () => _('Keep note history for'),
},
'revisionService.intervalBetweenRevisions': { section: 'revisionService', value: 1000 * 60 * 10, type: Setting.TYPE_INT, public: false },
'revisionService.oldNoteInterval': { section: 'revisionService', value: 1000 * 60 * 60 * 24 * 7, type: Setting.TYPE_INT, public: false },
@ -269,7 +468,7 @@ class Setting extends BaseModel {
static load() {
this.cancelScheduleSave();
this.cache_ = [];
return this.modelSelectAll('SELECT * FROM settings').then((rows) => {
return this.modelSelectAll('SELECT * FROM settings').then(rows => {
this.cache_ = [];
for (let i = 0; i < rows.length; i++) {
@ -329,8 +528,8 @@ class Setting extends BaseModel {
// Don't log this to prevent sensitive info (passwords, auth tokens...) to end up in logs
// this.logger().info('Setting: ' + key + ' = ' + c.value + ' => ' + value);
if (('minimum' in md) && value < md.minimum) value = md.minimum;
if (('maximum' in md) && value > md.maximum) value = md.maximum;
if ('minimum' in md && value < md.minimum) value = md.minimum;
if ('maximum' in md && value > md.maximum) value = md.maximum;
c.value = value;
@ -598,7 +797,7 @@ class Setting extends BaseModel {
nameToSections[md.section].metadatas.push(md);
}
}
return sections;
return sections;
}
static sectionNameToLabel(name) {
@ -617,7 +816,6 @@ class Setting extends BaseModel {
if (name === 'cli') return 'CLI';
return name[0].toUpperCase() + name.substr(1).toLowerCase();
}
}
Setting.TYPE_INT = 1;
@ -631,7 +829,7 @@ Setting.THEME_DARK = 2;
Setting.THEME_SOLARIZED_LIGHT = 3;
Setting.THEME_SOLARIZED_DARK = 4;
Setting.DATE_FORMAT_1 = 'DD/MM/YYYY'
Setting.DATE_FORMAT_1 = 'DD/MM/YYYY';
Setting.DATE_FORMAT_2 = 'DD/MM/YY';
Setting.DATE_FORMAT_3 = 'MM/DD/YYYY';
Setting.DATE_FORMAT_4 = 'MM/DD/YY';
@ -655,7 +853,7 @@ Setting.constants_ = {
templateDir: '',
tempDir: '',
openDevTools: false,
}
};
Setting.autoSaveEnabled = true;

View File

@ -6,7 +6,6 @@ const { time } = require('lib/time-utils.js');
const { _ } = require('lib/locale');
class Tag extends BaseItem {
static tableName() {
return 'tags';
}
@ -30,9 +29,12 @@ class Tag extends BaseItem {
let noteIds = await this.noteIds(tagId);
if (!noteIds.length) return [];
return Note.previews(null, Object.assign({}, options, {
conditions: ['id IN ("' + noteIds.join('","') + '")'],
}));
return Note.previews(
null,
Object.assign({}, options, {
conditions: ['id IN ("' + noteIds.join('","') + '")'],
})
);
}
// Untag all the notes and delete tag
@ -167,7 +169,7 @@ class Tag extends BaseItem {
}
}
return super.save(o, options).then((tag) => {
return super.save(o, options).then(tag => {
this.dispatch({
type: 'TAG_UPDATE_ONE',
item: tag,
@ -175,7 +177,6 @@ class Tag extends BaseItem {
return tag;
});
}
}
module.exports = Tag;

Some files were not shown because too many files have changed in this diff Show More