1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-27 10:32:58 +02:00

Merge branch 'master' of github.com:laurent22/joplin

This commit is contained in:
Laurent Cozic 2018-02-14 19:10:42 +00:00
commit cdbb7c4b0d
13 changed files with 133 additions and 75 deletions

View File

@ -15,10 +15,6 @@ class ConfigScreenComponent extends React.Component {
constructor() { constructor() {
super(); super();
this.state = {
settings: {},
};
shared.init(this); shared.init(this);
this.checkSyncConfig_ = async () => { this.checkSyncConfig_ = async () => {
@ -68,9 +64,7 @@ class ConfigScreenComponent extends React.Component {
}; };
const updateSettingValue = (key, value) => { const updateSettingValue = (key, value) => {
const settings = Object.assign({}, this.state.settings); return shared.updateSettingValue(this, key, value);
settings[key] = Setting.formatValue(key, value);
this.setState({ settings: settings });
} }
// Component key needs to be key+value otherwise it doesn't update when the settings change. // Component key needs to be key+value otherwise it doesn't update when the settings change.
@ -142,10 +136,7 @@ class ConfigScreenComponent extends React.Component {
} }
onSaveClick() { onSaveClick() {
for (let n in this.state.settings) { shared.saveSettings(this);
if (!this.state.settings.hasOwnProperty(n)) continue;
Setting.setValue(n, this.state.settings[n]);
}
this.props.dispatch({ type: 'NAV_BACK' }); this.props.dispatch({ type: 'NAV_BACK' });
} }
@ -167,24 +158,11 @@ class ConfigScreenComponent extends React.Component {
}; };
const buttonStyle = { const buttonStyle = {
display: this.state.settings === this.props.settings ? 'none' : 'inline-block', display: this.state.changedSettingKeys.length ? 'inline-block' : 'none',
marginRight: 10, marginRight: 10,
} }
let settingComps = []; const settingComps = shared.settingsToComponents(this, 'desktop', settings);
let keys = Setting.keys(true, 'desktop');
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!(key in settings)) {
console.warn('Missing setting: ' + key);
continue;
}
const md = Setting.settingMetadata(key);
if (md.show && !md.show(settings)) continue;
const comp = this.settingToComponent(key, settings[key]);
if (!comp) continue;
settingComps.push(comp);
}
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']); const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);

View File

@ -1,6 +1,6 @@
{ {
"name": "Joplin", "name": "Joplin",
"version": "1.0.62", "version": "1.0.63",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "Joplin", "name": "Joplin",
"version": "1.0.62", "version": "1.0.63",
"description": "Joplin for Desktop", "description": "Joplin for Desktop",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View File

@ -110,6 +110,7 @@ Select the "WebDAV" synchronisation target and follow the same instructions as f
Known compatible services that use WebDAV: Known compatible services that use WebDAV:
- [Box.com](https://www.box.com/)
- [DriveHQ](https://www.drivehq.com) - [DriveHQ](https://www.drivehq.com)
- [Zimbra](https://www.zimbra.com/) - [Zimbra](https://www.zimbra.com/)

View File

@ -244,7 +244,7 @@ The following commands are available in [command-line mode](#command-line-mode):
Possible values: HH:mm (20:30), h:mm A (8:30 PM). Possible values: HH:mm (20:30), h:mm A (8:30 PM).
Default: "HH:mm" Default: "HH:mm"
uncompletedTodosOnTop Show uncompleted todos on top of the lists. uncompletedTodosOnTop Show uncompleted to-dos on top of the lists.
Type: bool. Type: bool.
Default: true Default: true

View File

@ -133,6 +133,42 @@ class WebDavApi {
return this.valueFromJson(json, keys, 'array'); return this.valueFromJson(json, keys, 'array');
} }
resourcePropByName(resource, outputType, propName) {
const propStats = resource['d:propstat'];
let output = null;
if (!Array.isArray(propStats)) throw new Error('Missing d:propstat property');
for (let i = 0; i < propStats.length; i++) {
const props = propStats[i]['d:prop'];
if (!Array.isArray(props) || !props.length) continue;
const prop = props[0];
if (Array.isArray(prop[propName])) {
output = prop[propName];
break;
}
}
if (outputType === 'string') {
// If the XML has not attribute the value is directly a string
// If the XML node has attributes, the value is under "_".
// Eg for this XML, the string will be under {"_":"Thu, 01 Feb 2018 17:24:05 GMT"}:
// <a:getlastmodified b:dt="dateTime.rfc1123">Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
// For this XML, the value will be "Thu, 01 Feb 2018 17:24:05 GMT"
// <a:getlastmodified>Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
output = output[0];
if (typeof output === 'object' && '_' in output) output = output['_'];
if (typeof output !== 'string') return null;
return output;
}
if (outputType === 'array') {
return output;
}
throw new Error('Invalid output type: ' + outputType);
}
async execPropFind(path, depth, fields = null, options = null) { async execPropFind(path, depth, fields = null, options = null) {
if (fields === null) fields = ['d:getlastmodified']; if (fields === null) fields = ['d:getlastmodified'];

View File

@ -20,11 +20,6 @@ class ConfigScreenComponent extends BaseScreenComponent {
super(); super();
this.styles_ = {}; this.styles_ = {};
this.state = {
settings: {},
settingsChanged: false,
};
shared.init(this); shared.init(this);
this.checkSyncConfig_ = async () => { this.checkSyncConfig_ = async () => {
@ -32,11 +27,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
} }
this.saveButton_press = () => { this.saveButton_press = () => {
for (let n in this.state.settings) { return shared.saveSettings(this);
if (!this.state.settings.hasOwnProperty(n)) continue;
Setting.setValue(n, this.state.settings[n]);
}
this.setState({settingsChanged:false});
}; };
} }
@ -74,6 +65,11 @@ class ConfigScreenComponent extends BaseScreenComponent {
fontSize: theme.fontSize, fontSize: theme.fontSize,
flex: 1, flex: 1,
}, },
descriptionText: {
color: theme.color,
fontSize: theme.fontSize,
flex: 1,
},
settingControl: { settingControl: {
color: theme.color, color: theme.color,
flex: 1, flex: 1,
@ -113,12 +109,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
let output = null; let output = null;
const updateSettingValue = (key, value) => { const updateSettingValue = (key, value) => {
const settings = Object.assign({}, this.state.settings); return shared.updateSettingValue(this, key, value);
settings[key] = Setting.formatValue(key, value);
this.setState({
settings: settings,
settingsChanged: true,
});
} }
const md = Setting.settingMetadata(key); const md = Setting.settingMetadata(key);
@ -187,20 +178,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
render() { render() {
const settings = this.state.settings; const settings = this.state.settings;
const keys = Setting.keys(true, 'mobile'); const settingComps = shared.settingsToComponents(this, 'mobile', settings);
let settingComps = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
//if (key == 'sync.target' && !settings.showAdvancedOptions) continue;
if (!Setting.isPublic(key)) continue;
const md = Setting.settingMetadata(key);
if (md.show && !md.show(settings)) continue;
const comp = this.settingToComponent(key, settings[key]);
if (!comp) continue;
settingComps.push(comp);
}
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']); const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
@ -208,8 +186,8 @@ class ConfigScreenComponent extends BaseScreenComponent {
const messages = shared.checkSyncConfigMessages(this); const messages = shared.checkSyncConfigMessages(this);
const statusComp = !messages.length ? null : ( const statusComp = !messages.length ? null : (
<View style={{flex:1, marginTop: 10}}> <View style={{flex:1, marginTop: 10}}>
<Text>{messages[0]}</Text> <Text style={this.styles().descriptionText}>{messages[0]}</Text>
{messages.length >= 1 ? (<Text style={{marginTop:10}}>{messages[1]}</Text>) : null} {messages.length >= 1 ? (<View style={{marginTop:10}}><Text style={this.styles().descriptionText}>{messages[1]}</Text></View>) : null}
</View>); </View>);
settingComps.push( settingComps.push(
@ -244,7 +222,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
<ScreenHeader <ScreenHeader
title={_('Configuration')} title={_('Configuration')}
showSaveButton={true} showSaveButton={true}
saveButtonDisabled={!this.state.settingsChanged} saveButtonDisabled={!this.state.changedSettingKeys.length}
onSaveButtonPress={this.saveButton_press} onSaveButtonPress={this.saveButton_press}
/> />
<ScrollView > <ScrollView >

View File

@ -7,6 +7,8 @@ const shared = {}
shared.init = function(comp) { shared.init = function(comp) {
if (!comp.state) comp.state = {}; if (!comp.state) comp.state = {};
comp.state.checkSyncConfigResult = null; comp.state.checkSyncConfigResult = null;
comp.state.settings = {};
comp.state.changedSettingKeys = [];
} }
shared.checkSyncConfig = async function(comp, settings) { shared.checkSyncConfig = async function(comp, settings) {
@ -34,4 +36,46 @@ shared.checkSyncConfigMessages = function(comp) {
return output; return output;
} }
shared.updateSettingValue = function(comp, key, value) {
const settings = Object.assign({}, comp.state.settings);
const changedSettingKeys = comp.state.changedSettingKeys.slice();
settings[key] = Setting.formatValue(key, value);
if (changedSettingKeys.indexOf(key) < 0) changedSettingKeys.push(key);
comp.setState({
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]);
Setting.setValue(key, comp.state.settings[key]);
}
comp.setState({ changedSettingKeys: [] });
}
shared.settingsToComponents = function(comp, device, settings) {
const keys = Setting.keys(true, device);
const settingComps = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!Setting.isPublic(key)) continue;
const md = Setting.settingMetadata(key);
if (md.show && !md.show(settings)) continue;
const settingComp = comp.settingToComponent(key, settings[key]);
if (!settingComp) continue;
settingComps.push(settingComp);
}
return settingComps
}
module.exports = shared; module.exports = shared;

View File

@ -27,7 +27,7 @@ class FileApiDriverWebDav {
const result = await this.api().execPropFind(path, 0, [ const result = await this.api().execPropFind(path, 0, [
'd:getlastmodified', 'd:getlastmodified',
'd:resourcetype', 'd:resourcetype',
'd:getcontentlength', // Remove this once PUT call issue is sorted out // 'd:getcontentlength', // Remove this once PUT call issue is sorted out
]); ]);
const resource = this.api().objectFromJson(result, ['d:multistatus', 'd:response', 0]); const resource = this.api().objectFromJson(result, ['d:multistatus', 'd:response', 0]);
@ -39,22 +39,40 @@ class FileApiDriverWebDav {
} }
statFromResource_(resource, path) { statFromResource_(resource, path) {
const isCollection = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:resourcetype', 0, 'd:collection', 0]); // WebDAV implementations are always slighly different from one server to another but, at the minimum,
const lastModifiedString = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getlastmodified', 0]); // a resource should have a propstat key - if not it's probably an error.
const propStat = this.api().arrayFromJson(resource, ['d:propstat']);
if (!Array.isArray(propStat)) throw new Error('Invalid WebDAV resource format: ' + JSON.stringify(resource));
const resourceTypes = this.api().resourcePropByName(resource, 'array', 'd:resourcetype');
let isDir = false;
if (Array.isArray(resourceTypes)) {
for (let i = 0; i < resourceTypes.length; i++) {
const t = resourceTypes[i];
if (typeof t === 'object' && 'd:collection' in t) {
isDir = true;
break;
}
}
}
const lastModifiedString = this.api().resourcePropByName(resource, 'string', 'd:getlastmodified');
// const sizeDONOTUSE = Number(this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getcontentlength', 0])); // const sizeDONOTUSE = Number(this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getcontentlength', 0]));
// if (isNaN(sizeDONOTUSE)) throw new Error('Cannot get content size: ' + JSON.stringify(resource)); // if (isNaN(sizeDONOTUSE)) throw new Error('Cannot get content size: ' + JSON.stringify(resource));
if (!lastModifiedString) throw new Error('Could not get lastModified date: ' + JSON.stringify(resource));
const lastModifiedDate = new Date(lastModifiedString); // Note: Not all WebDAV servers return a getlastmodified date (eg. Seafile, which doesn't return the
// property for folders) so we can only throw an error if it's a file.
if (!lastModifiedString && !isDir) throw new Error('Could not get lastModified date for resource: ' + JSON.stringify(resource));
const lastModifiedDate = lastModifiedString ? new Date(lastModifiedString) : new Date();
if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + lastModifiedString); if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + lastModifiedString);
return { return {
path: path, path: path,
// created_time: lastModifiedDate.getTime(), // created_time: lastModifiedDate.getTime(),
updated_time: lastModifiedDate.getTime(), updated_time: lastModifiedDate.getTime(),
isDir: isCollection === '', isDir: isDir,
// sizeDONOTUSE: sizeDONOTUSE, // This property is used only for the WebDAV PUT hack (see below) so mark it as such so that it can be removed with the hack later on. // sizeDONOTUSE: sizeDONOTUSE, // This property is used only for the WebDAV PUT hack (see below) so mark it as such so that it can be removed with the hack later on.
}; };
} }
@ -260,7 +278,7 @@ class FileApiDriverWebDav {
]); ]);
const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']); const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']);
const stats = this.statsFromResources_(resources) const stats = this.statsFromResources_(resources);
return { return {
items: stats, items: stats,

View File

@ -58,7 +58,7 @@ class Setting extends BaseModel {
// recent: _('Non-completed and recently completed ones'), // recent: _('Non-completed and recently completed ones'),
// nonCompleted: _('Non-completed ones only'), // nonCompleted: _('Non-completed ones only'),
// })}, // })},
'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted todos on top of the lists') }, 'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted to-dos on top of the lists') },
'trackLocation': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Save geo-location with notes') }, 'trackLocation': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Save geo-location with notes') },
'newTodoFocus': { value: 'title', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new to-do:'), options: () => { 'newTodoFocus': { value: 'title', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new to-do:'), options: () => {
return { return {

View File

@ -310,6 +310,7 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
<p>Select the &quot;WebDAV&quot; synchronisation target and follow the same instructions as for Nextcloud above.</p> <p>Select the &quot;WebDAV&quot; synchronisation target and follow the same instructions as for Nextcloud above.</p>
<p>Known compatible services that use WebDAV:</p> <p>Known compatible services that use WebDAV:</p>
<ul> <ul>
<li><a href="https://www.box.com/">Box.com</a></li>
<li><a href="https://www.drivehq.com">DriveHQ</a></li> <li><a href="https://www.drivehq.com">DriveHQ</a></li>
<li><a href="https://www.zimbra.com/">Zimbra</a></li> <li><a href="https://www.zimbra.com/">Zimbra</a></li>
</ul> </ul>
@ -390,14 +391,14 @@ $$
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png" alt=""></td> <td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png" alt=""></td>
<td>Croatian</td> <td>Croatian</td>
<td>hr_HR</td> <td>hr_HR</td>
<td>Hrvoje Mandić <a href="&#x6d;&#x61;&#105;&#x6c;&#x74;&#111;&#x3a;&#116;&#114;&#98;&#x75;&#104;&#x6f;&#x6d;&#x40;&#110;&#101;&#116;&#46;&#x68;&#x72;">&#116;&#114;&#98;&#x75;&#104;&#x6f;&#x6d;&#x40;&#110;&#101;&#116;&#46;&#x68;&#x72;</a></td> <td>Hrvoje Mandić <a href="&#109;&#x61;&#105;&#108;&#116;&#x6f;&#58;&#x74;&#114;&#x62;&#x75;&#104;&#x6f;&#x6d;&#x40;&#x6e;&#101;&#x74;&#46;&#104;&#x72;">&#x74;&#114;&#x62;&#x75;&#104;&#x6f;&#x6d;&#x40;&#x6e;&#101;&#x74;&#46;&#104;&#x72;</a></td>
<td>72%</td> <td>72%</td>
</tr> </tr>
<tr> <tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png" alt=""></td> <td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png" alt=""></td>
<td>Deutsch</td> <td>Deutsch</td>
<td>de_DE</td> <td>de_DE</td>
<td>Tobias Strobel <a href="&#109;&#97;&#x69;&#x6c;&#116;&#111;&#58;&#x67;&#x69;&#116;&#64;&#x73;&#x74;&#114;&#111;&#x62;&#x65;&#x6c;&#x74;&#111;&#x62;&#x69;&#97;&#x73;&#x2e;&#100;&#101;">&#x67;&#x69;&#116;&#64;&#x73;&#x74;&#114;&#111;&#x62;&#x65;&#x6c;&#x74;&#111;&#x62;&#x69;&#97;&#x73;&#x2e;&#100;&#101;</a></td> <td>Tobias Strobel <a href="&#x6d;&#x61;&#105;&#108;&#x74;&#x6f;&#x3a;&#x67;&#x69;&#x74;&#x40;&#x73;&#x74;&#x72;&#111;&#x62;&#x65;&#108;&#116;&#x6f;&#x62;&#105;&#x61;&#115;&#46;&#100;&#101;">&#x67;&#x69;&#x74;&#x40;&#x73;&#x74;&#x72;&#111;&#x62;&#x65;&#108;&#116;&#x6f;&#x62;&#105;&#x61;&#115;&#46;&#100;&#101;</a></td>
<td>91%</td> <td>91%</td>
</tr> </tr>
<tr> <tr>
@ -453,14 +454,14 @@ $$
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png" alt=""></td> <td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png" alt=""></td>
<td>Русский</td> <td>Русский</td>
<td>ru_RU</td> <td>ru_RU</td>
<td>Artyom Karlov <a href="&#109;&#97;&#105;&#108;&#x74;&#x6f;&#x3a;&#97;&#114;&#116;&#x79;&#x6f;&#109;&#46;&#107;&#97;&#x72;&#x6c;&#x6f;&#x76;&#x40;&#103;&#109;&#97;&#105;&#108;&#46;&#99;&#111;&#x6d;">&#97;&#114;&#116;&#x79;&#x6f;&#109;&#46;&#107;&#97;&#x72;&#x6c;&#x6f;&#x76;&#x40;&#103;&#109;&#97;&#105;&#108;&#46;&#99;&#111;&#x6d;</a></td> <td>Artyom Karlov <a href="&#109;&#x61;&#105;&#x6c;&#116;&#111;&#58;&#97;&#x72;&#116;&#121;&#x6f;&#109;&#x2e;&#107;&#97;&#114;&#x6c;&#111;&#118;&#x40;&#x67;&#109;&#x61;&#x69;&#x6c;&#x2e;&#99;&#111;&#x6d;">&#97;&#x72;&#116;&#121;&#x6f;&#109;&#x2e;&#107;&#97;&#114;&#x6c;&#111;&#118;&#x40;&#x67;&#109;&#x61;&#x69;&#x6c;&#x2e;&#99;&#111;&#x6d;</a></td>
<td>94%</td> <td>94%</td>
</tr> </tr>
<tr> <tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png" alt=""></td> <td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png" alt=""></td>
<td>中文 (简体)</td> <td>中文 (简体)</td>
<td>zh_CN</td> <td>zh_CN</td>
<td>RCJacH <a href="&#x6d;&#x61;&#x69;&#x6c;&#116;&#x6f;&#58;&#82;&#x43;&#74;&#97;&#99;&#72;&#64;&#x6f;&#117;&#x74;&#x6c;&#111;&#x6f;&#107;&#46;&#99;&#111;&#x6d;">&#82;&#x43;&#74;&#97;&#99;&#72;&#64;&#x6f;&#117;&#x74;&#x6c;&#111;&#x6f;&#107;&#46;&#99;&#111;&#x6d;</a></td> <td>RCJacH <a href="&#x6d;&#97;&#105;&#x6c;&#116;&#111;&#x3a;&#82;&#x43;&#x4a;&#97;&#99;&#72;&#64;&#x6f;&#117;&#x74;&#108;&#x6f;&#111;&#x6b;&#46;&#x63;&#111;&#x6d;">&#82;&#x43;&#x4a;&#97;&#99;&#72;&#64;&#x6f;&#117;&#x74;&#108;&#x6f;&#111;&#x6b;&#46;&#x63;&#111;&#x6d;</a></td>
<td>75%</td> <td>75%</td>
</tr> </tr>
<tr> <tr>

View File

@ -395,7 +395,7 @@ Possible keys/values:
Possible values: HH:mm (20:30), h:mm A (8:30 PM). Possible values: HH:mm (20:30), h:mm A (8:30 PM).
Default: &quot;HH:mm&quot; Default: &quot;HH:mm&quot;
uncompletedTodosOnTop Show uncompleted todos on top of the lists. uncompletedTodosOnTop Show uncompleted to-dos on top of the lists.
Type: bool. Type: bool.
Default: true Default: true

View File

@ -18,6 +18,8 @@
"*.min.js", "*.min.js",
"ElectronClient/app/gui/note-viewer/highlight/*.pack.js", "ElectronClient/app/gui/note-viewer/highlight/*.pack.js",
"ElectronClient/app/css/font-awesome.min.css", "ElectronClient/app/css/font-awesome.min.css",
"docs/*.html",
"docs/*.svg",
], ],
"folder_exclude_patterns": "folder_exclude_patterns":
[ [