diff --git a/.travis.yml b/.travis.yml
index 41a1490f6..96df8d0be 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,6 @@
+# Only build tags
+if: tag IS present
+
rvm: 2.3.3
matrix:
diff --git a/CliClient/app/autocompletion.js b/CliClient/app/autocompletion.js
index cca40e206..a03f1c5e5 100644
--- a/CliClient/app/autocompletion.js
+++ b/CliClient/app/autocompletion.js
@@ -48,7 +48,7 @@ async function handleAutocompletionPromise(line) {
if (options.length > 1 && options[1].indexOf(next) === 0) {
l.push(options[1]);
} else if (options[0].indexOf(next) === 0) {
- l.push(options[2]);
+ l.push(options[0]);
}
}
if (l.length === 0) {
diff --git a/ElectronClient/app/gui/ConfigScreen.jsx b/ElectronClient/app/gui/ConfigScreen.jsx
index ed72bd11f..9187c71ae 100644
--- a/ElectronClient/app/gui/ConfigScreen.jsx
+++ b/ElectronClient/app/gui/ConfigScreen.jsx
@@ -89,18 +89,19 @@ class ConfigScreenComponent extends React.Component {
updateSettingValue(key, !value)
}
+ // Hack: The {key+value.toString()} is needed as otherwise the checkbox doesn't update when the state changes.
+ // There's probably a better way to do this but can't figure it out.
+
return (
-
+
);
} else if (md.type === Setting.TYPE_STRING) {
const onTextChange = (event) => {
- const settings = Object.assign({}, this.state.settings);
- settings[key] = event.target.value;
- this.setState({ settings: settings });
+ updateSettingValue(key, event.target.value);
}
return (
@@ -109,6 +110,17 @@ class ConfigScreenComponent extends React.Component {
{onTextChange(event)}} />
);
+ } else if (md.type === Setting.TYPE_INT) {
+ const onNumChange = (event) => {
+ updateSettingValue(key, event.target.value);
+ };
+
+ return (
+
+
+
{onNumChange(event)}} min={md.minimum} max={md.maximum} step={md.step}/>
+
+ );
} else {
console.warn('Type not implemented: ' + key);
}
diff --git a/ElectronClient/app/package-lock.json b/ElectronClient/app/package-lock.json
index 271555646..02ea987da 100644
--- a/ElectronClient/app/package-lock.json
+++ b/ElectronClient/app/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "Joplin",
- "version": "0.10.47",
+ "version": "0.10.48",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -29,9 +29,9 @@
"optional": true
},
"@types/node": {
- "version": "7.0.46",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.46.tgz",
- "integrity": "sha512-u+JAi1KtmaUoU/EHJkxoiuvzyo91FCE41Z9TZWWcOUU3P8oUdlDLdrGzCGWySPgbRMD17B0B+1aaJLYI9egQ6A==",
+ "version": "7.0.52",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.52.tgz",
+ "integrity": "sha512-jjpyQsKGsOF/wUElNjfPULk+d8PKvJOIXk3IUeBYYmNCy5dMWfrI+JiixYNw8ppKOlcRwWTXFl0B+i5oGrf95Q==",
"dev": true
},
"ajv": {
@@ -1302,12 +1302,12 @@
"dev": true
},
"electron": {
- "version": "1.7.9",
- "resolved": "https://registry.npmjs.org/electron/-/electron-1.7.9.tgz",
- "integrity": "sha1-rdVOn4+D7QL2UZ7BATX2mLGTNs8=",
+ "version": "1.7.11",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-1.7.11.tgz",
+ "integrity": "sha1-mTtqp54OeafPzDafTIE/vZoLCNk=",
"dev": true,
"requires": {
- "@types/node": "7.0.46",
+ "@types/node": "7.0.52",
"electron-download": "3.3.0",
"extract-zip": "1.6.6"
}
diff --git a/ElectronClient/app/package.json b/ElectronClient/app/package.json
index 7a0564d36..22cd60b0e 100644
--- a/ElectronClient/app/package.json
+++ b/ElectronClient/app/package.json
@@ -1,6 +1,6 @@
{
"name": "Joplin",
- "version": "0.10.47",
+ "version": "0.10.48",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {
@@ -41,7 +41,7 @@
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-react": "^6.24.1",
- "electron": "^1.7.9",
+ "electron": "^1.7.11",
"electron-builder": "^19.45.4"
},
"optionalDependencies": {
diff --git a/ElectronClient/app/theme.js b/ElectronClient/app/theme.js
index ca27c8945..bf194838d 100644
--- a/ElectronClient/app/theme.js
+++ b/ElectronClient/app/theme.js
@@ -1,7 +1,7 @@
const Setting = require('lib/models/Setting.js');
const globalStyle = {
- fontSize: 12,
+ fontSize: 12 * Setting.value('style.zoom')/100,
fontFamily: 'sans-serif',
margin: 15, // No text and no interactive component should be within this margin
itemMarginTop: 10,
diff --git a/ReactNativeClient/lib/file-api-driver-local.js b/ReactNativeClient/lib/file-api-driver-local.js
index 8ee94173b..7d664cc4b 100644
--- a/ReactNativeClient/lib/file-api-driver-local.js
+++ b/ReactNativeClient/lib/file-api-driver-local.js
@@ -1,5 +1,5 @@
-const BaseItem = require('lib/models/BaseItem.js');
const { time } = require('lib/time-utils.js');
+const { basicDelta } = require('lib/file-api');
// NOTE: when synchronising with the file system the time resolution is the second (unlike milliseconds for OneDrive for instance).
// What it means is that if, for example, client 1 changes a note at time t, and client 2 changes the same note within the same second,
@@ -65,113 +65,15 @@ class FileApiDriverLocal {
}
}
- contextFromOptions_(options) {
- let output = {
- timestamp: 0,
- filesAtTimestamp: [],
- statsCache: null,
+ async delta(path, options) {
+ const getStatFn = async (path) => {
+ const stats = await this.fsDriver().readDirStats(path);
+ return this.metadataFromStats_(stats);
};
- if (!options || !options.context) return output;
- const d = new Date(options.context.timestamp);
-
- output.timestamp = isNaN(d.getTime()) ? 0 : options.context.timestamp;
- output.filesAtTimestamp = Array.isArray(options.context.filesAtTimestamp) ? options.context.filesAtTimestamp.slice() : [];
- output.statsCache = options.context && options.context.statsCache ? options.context.statsCache : null;
-
- return output;
- }
-
- async delta(path, options) {
- const outputLimit = 1000;
- const itemIds = await options.allItemIdsHandler();
-
try {
- const context = this.contextFromOptions_(options);
-
- let newContext = {
- timestamp: context.timestamp,
- filesAtTimestamp: context.filesAtTimestamp.slice(),
- statsCache: context.statsCache,
- };
-
- // Stats are cached until all items have been processed (until hasMore is false)
- if (newContext.statsCache === null) {
- const stats = await this.fsDriver().readDirStats(path);
- newContext.statsCache = this.metadataFromStats_(stats);
- newContext.statsCache.sort(function(a, b) {
- return a.updated_time - b.updated_time;
- });
- }
-
- let output = [];
-
- // Find out which files have been changed since the last time. Note that we keep
- // both the timestamp of the most recent change, *and* the items that exactly match
- // this timestamp. This to handle cases where an item is modified while this delta
- // function is running. For example:
- // t0: Item 1 is changed
- // t0: Sync items - run delta function
- // t0: While delta() is running, modify Item 2
- // Since item 2 was modified within the same millisecond, it would be skipped in the
- // next sync if we relied exclusively on a timestamp.
- for (let i = 0; i < newContext.statsCache.length; i++) {
- const stat = newContext.statsCache[i];
-
- if (stat.isDir) continue;
-
- if (stat.updated_time < context.timestamp) continue;
-
- // Special case for items that exactly match the timestamp
- if (stat.updated_time === context.timestamp) {
- if (context.filesAtTimestamp.indexOf(stat.path) >= 0) continue;
- }
-
- if (stat.updated_time > newContext.timestamp) {
- newContext.timestamp = stat.updated_time;
- newContext.filesAtTimestamp = [];
- }
-
- newContext.filesAtTimestamp.push(stat.path);
- output.push(stat);
-
- if (output.length >= outputLimit) break;
- }
-
- if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
-
- let deletedItems = [];
- for (let i = 0; i < itemIds.length; i++) {
- if (output.length + deletedItems.length >= outputLimit) break;
-
- const itemId = itemIds[i];
- let found = false;
- for (let j = 0; j < newContext.statsCache.length; j++) {
- const item = newContext.statsCache[j];
- if (BaseItem.pathToId(item.path) == itemId) {
- found = true;
- break;
- }
- }
-
- if (!found) {
- deletedItems.push({
- path: BaseItem.systemPath(itemId),
- isDeleted: true,
- });
- }
- }
-
- output = output.concat(deletedItems);
-
- const hasMore = output.length >= outputLimit;
- if (!hasMore) newContext.statsCache = null;
-
- return {
- hasMore: hasMore,
- context: newContext,
- items: output,
- };
+ const output = await basicDelta(path, getStatFn, options);
+ return output;
} catch(error) {
throw this.fsErrorToJsError_(error, path);
}
diff --git a/ReactNativeClient/lib/file-api-driver-memory.js b/ReactNativeClient/lib/file-api-driver-memory.js
index 317796ec2..29ac3fdb3 100644
--- a/ReactNativeClient/lib/file-api-driver-memory.js
+++ b/ReactNativeClient/lib/file-api-driver-memory.js
@@ -1,5 +1,6 @@
const { time } = require('lib/time-utils.js');
const fs = require('fs-extra');
+const { basicDelta } = require('lib/file-api');
class FileApiDriverMemory {
@@ -144,48 +145,17 @@ class FileApiDriverMemory {
}
async delta(path, options = null) {
- let limit = 3;
-
- let output = {
- hasMore: false,
- context: {},
- items: [],
+ const getStatFn = async (path) => {
+ let output = this.items_.slice();
+ for (let i = 0; i < output.length; i++) {
+ const item = Object.assign({}, output[i]);
+ item.path = item.path.substr(path.length + 1);
+ output[i] = item;
+ }
+ return output;
};
- let context = options ? options.context : null;
- let fromTime = 0;
-
- if (context) fromTime = context.fromTime;
-
- let sortedItems = this.items_.slice().concat(this.deletedItems_);
- sortedItems.sort((a, b) => {
- if (a.updated_time < b.updated_time) return -1;
- if (a.updated_time > b.updated_time) return +1;
- return 0;
- });
-
- let hasMore = false;
- let items = [];
- let maxTime = 0;
- for (let i = 0; i < sortedItems.length; i++) {
- let item = sortedItems[i];
- if (item.updated_time >= fromTime) {
- item = Object.assign({}, item);
- item.path = item.path.substr(path.length + 1);
- items.push(item);
- if (item.updated_time > maxTime) maxTime = item.updated_time;
- }
-
- if (items.length >= limit) {
- hasMore = true;
- break;
- }
- }
-
- output.items = items;
- output.hasMore = hasMore;
- output.context = { fromTime: maxTime };
-
+ const output = await basicDelta(path, getStatFn, options);
return output;
}
diff --git a/ReactNativeClient/lib/file-api.js b/ReactNativeClient/lib/file-api.js
index ebb387686..4c25d33af 100644
--- a/ReactNativeClient/lib/file-api.js
+++ b/ReactNativeClient/lib/file-api.js
@@ -1,6 +1,7 @@
const { isHidden } = require('lib/path-utils.js');
const { Logger } = require('lib/logger.js');
const { shim } = require('lib/shim');
+const BaseItem = require('lib/models/BaseItem.js');
const JoplinError = require('lib/JoplinError');
class FileApi {
@@ -120,4 +121,113 @@ class FileApi {
}
-module.exports = { FileApi };
\ No newline at end of file
+function basicDeltaContextFromOptions_(options) {
+ let output = {
+ timestamp: 0,
+ filesAtTimestamp: [],
+ statsCache: null,
+ };
+
+ if (!options || !options.context) return output;
+ const d = new Date(options.context.timestamp);
+
+ output.timestamp = isNaN(d.getTime()) ? 0 : options.context.timestamp;
+ output.filesAtTimestamp = Array.isArray(options.context.filesAtTimestamp) ? options.context.filesAtTimestamp.slice() : [];
+ output.statsCache = options.context && options.context.statsCache ? options.context.statsCache : null;
+
+ return output;
+}
+
+// This is the basic delta algorithm, which can be used in case the cloud service does not have
+// a built-on delta API. OneDrive and Dropbox have one for example, but Nextcloud and obviously
+// the file system do not.
+async function basicDelta(path, getStatFn, options) {
+ const outputLimit = 1000;
+ const itemIds = await options.allItemIdsHandler();
+ if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
+
+ const context = basicDeltaContextFromOptions_(options);
+
+ let newContext = {
+ timestamp: context.timestamp,
+ filesAtTimestamp: context.filesAtTimestamp.slice(),
+ statsCache: context.statsCache,
+ };
+
+ // Stats are cached until all items have been processed (until hasMore is false)
+ if (newContext.statsCache === null) {
+ newContext.statsCache = await getStatFn(path);
+ newContext.statsCache.sort(function(a, b) {
+ return a.updated_time - b.updated_time;
+ });
+ }
+
+ let output = [];
+
+ // Find out which files have been changed since the last time. Note that we keep
+ // both the timestamp of the most recent change, *and* the items that exactly match
+ // this timestamp. This to handle cases where an item is modified while this delta
+ // function is running. For example:
+ // t0: Item 1 is changed
+ // t0: Sync items - run delta function
+ // t0: While delta() is running, modify Item 2
+ // Since item 2 was modified within the same millisecond, it would be skipped in the
+ // next sync if we relied exclusively on a timestamp.
+ for (let i = 0; i < newContext.statsCache.length; i++) {
+ const stat = newContext.statsCache[i];
+
+ if (stat.isDir) continue;
+
+ if (stat.updated_time < context.timestamp) continue;
+
+ // Special case for items that exactly match the timestamp
+ if (stat.updated_time === context.timestamp) {
+ if (context.filesAtTimestamp.indexOf(stat.path) >= 0) continue;
+ }
+
+ if (stat.updated_time > newContext.timestamp) {
+ newContext.timestamp = stat.updated_time;
+ newContext.filesAtTimestamp = [];
+ }
+
+ newContext.filesAtTimestamp.push(stat.path);
+ output.push(stat);
+
+ if (output.length >= outputLimit) break;
+ }
+
+ let deletedItems = [];
+ for (let i = 0; i < itemIds.length; i++) {
+ if (output.length + deletedItems.length >= outputLimit) break;
+
+ const itemId = itemIds[i];
+ let found = false;
+ for (let j = 0; j < newContext.statsCache.length; j++) {
+ const item = newContext.statsCache[j];
+ if (BaseItem.pathToId(item.path) == itemId) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ deletedItems.push({
+ path: BaseItem.systemPath(itemId),
+ isDeleted: true,
+ });
+ }
+ }
+
+ output = output.concat(deletedItems);
+
+ const hasMore = output.length >= outputLimit;
+ if (!hasMore) newContext.statsCache = null;
+
+ return {
+ hasMore: hasMore,
+ context: newContext,
+ items: output,
+ };
+}
+
+module.exports = { FileApi, basicDelta };
\ No newline at end of file
diff --git a/ReactNativeClient/lib/models/Setting.js b/ReactNativeClient/lib/models/Setting.js
index ff57cb96a..68472b5d1 100644
--- a/ReactNativeClient/lib/models/Setting.js
+++ b/ReactNativeClient/lib/models/Setting.js
@@ -63,6 +63,8 @@ class Setting extends BaseModel {
'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 },
+ 'style.zoom': {value: "100", type: Setting.TYPE_INT, public: true, appTypes: ['desktop'], label: () => _('Set application zoom percentage'), minimum: "50", maximum: "500", step: "10"},
+ 'autoUpdateEnabled': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') },
'sync.interval': { value: 300, type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation interval'), options: () => {
return {
0: _('Disabled'),
@@ -75,7 +77,6 @@ class Setting extends BaseModel {
};
}},
'noteVisiblePanes': { value: ['editor', 'viewer'], type: Setting.TYPE_ARRAY, public: false, appTypes: ['desktop'] },
- 'autoUpdateEnabled': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') },
'showAdvancedOptions': { value: false, type: Setting.TYPE_BOOL, public: true, appTypes: ['mobile' ], label: () => _('Show advanced options') },
'sync.target': { value: SyncTargetRegistry.nameToId('onedrive'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: () => _('The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.'), options: () => {
return SyncTargetRegistry.idAndLabelPlainObject();
diff --git a/ReactNativeClient/lib/synchronizer.js b/ReactNativeClient/lib/synchronizer.js
index 1ea1621bf..743f889b5 100644
--- a/ReactNativeClient/lib/synchronizer.js
+++ b/ReactNativeClient/lib/synchronizer.js
@@ -489,7 +489,7 @@ class Synchronizer {
if (action == 'createLocal' || action == 'updateLocal') {
if (content === null) {
- this.logger().warn('Remote has been deleted between now and the list() call? In that case it will be handled during the next sync: ' + path);
+ this.logger().warn('Remote has been deleted between now and the delta() call? In that case it will be handled during the next sync: ' + path);
continue;
}
content = ItemClass.filter(content);