mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Merge branch 'master' into webdav
This commit is contained in:
commit
6ade09c228
@ -1,3 +1,6 @@
|
||||
# Only build tags
|
||||
if: tag IS present
|
||||
|
||||
rvm: 2.3.3
|
||||
|
||||
matrix:
|
||||
|
@ -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) {
|
||||
|
@ -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 (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div key={key+value.toString()} style={rowStyle}>
|
||||
<div style={controlStyle}>
|
||||
<input id={'setting_checkbox_' + key} type="checkbox" checked={!!value} onChange={(event) => { onCheckboxClick(event) }}/><label onClick={(event) => { onCheckboxClick(event) }} style={labelStyle} htmlFor={'setting_checkbox_' + key}>{md.label()}</label>
|
||||
<input id={'setting_checkbox_' + key} type="checkbox" checked={!!value} onChange={(event) => { onCheckboxClick(event) }}/><label onClick={(event) => { onCheckboxClick(event) }} style={labelStyle} htmlFor={'setting_checkbox_' + key}>{md.label()}</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} 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 {
|
||||
<input type="text" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_INT) {
|
||||
const onNumChange = (event) => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}><label>{md.label()}</label></div>
|
||||
<input type="number" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onNumChange(event)}} min={md.minimum} max={md.maximum} step={md.step}/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
console.warn('Type not implemented: ' + key);
|
||||
}
|
||||
|
16
ElectronClient/app/package-lock.json
generated
16
ElectronClient/app/package-lock.json
generated
@ -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"
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 };
|
||||
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 };
|
@ -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();
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user