diff --git a/CliClient/app/command-ls.js b/CliClient/app/command-ls.js index 0e8a96460..9cae6e2d3 100644 --- a/CliClient/app/command-ls.js +++ b/CliClient/app/command-ls.js @@ -36,7 +36,6 @@ class Command extends BaseCommand { async action(args) { let pattern = args['pattern']; - let suffix = ''; let items = []; let options = args.options; @@ -59,7 +58,6 @@ class Command extends BaseCommand { if (pattern == '/' || !app().currentFolder()) { queryOptions.includeConflictFolder = true; items = await Folder.all(queryOptions); - suffix = '/'; modelType = Folder.modelType(); } else { if (!app().currentFolder()) throw new Error(_('Please select a notebook first.')); @@ -97,7 +95,7 @@ class Command extends BaseCommand { row.push(time.unixMsToLocalDateTime(item.updated_time)); } - let title = item.title + suffix; + let title = item.title; if (!shortIdShown && (seenTitles.indexOf(item.title) >= 0 || !item.title)) { title += ' (' + BaseModel.shortId(item.id) + ')'; } else { diff --git a/CliClient/locales/en_GB.po b/CliClient/locales/en_GB.po index ee9174741..c8dab0eeb 100644 --- a/CliClient/locales/en_GB.po +++ b/CliClient/locales/en_GB.po @@ -318,6 +318,11 @@ msgstr "" msgid "Please open this URL in your browser to authenticate the application:" msgstr "" +msgid "" +"Cannot refresh token: authentication data is missing. Starting the " +"synchronisation again may fix the problem." +msgstr "" + msgid "" "Please set the \"sync.2.path\" config value to the desired synchronisation " "destination." diff --git a/CliClient/locales/fr_FR.po b/CliClient/locales/fr_FR.po index 91f0b5c6e..30888c1d0 100644 --- a/CliClient/locales/fr_FR.po +++ b/CliClient/locales/fr_FR.po @@ -14,6 +14,8 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 2.0.3\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" msgid "No notebook selected." msgstr "Aucun carnet n'est sélectionné." @@ -353,6 +355,11 @@ msgstr "" "Veuillez ouvrir cette URL dans votre navigateur internet pour autoriser le " "logiciel :" +msgid "" +"Cannot refresh token: authentication data is missing. Starting the " +"synchronisation again may fix the problem." +msgstr "" + msgid "" "Please set the \"sync.2.path\" config value to the desired synchronisation " "destination." @@ -512,7 +519,7 @@ msgstr "Nouveau carnet" msgid "There are currently no notes. Create one by clicking on the (+) button." msgstr "" -"Ce carnet ne contient aucune note. Créez-en une en cliquant sur le bouton " +"Ce carnet ne contient aucune note. Créez-en une en appuyant sur le bouton " "(+)." msgid "Log" @@ -583,8 +590,8 @@ msgid "" "Click on the (+) button to create a new note or notebook. Click on the side " "menu to access your existing notebooks." msgstr "" -"Cliquez sur le bouton (+) pour une nouvelle note ou carnet. Ouvrez le menu " -"latéral pour accéder à vos carnets." +"Appuyez sur le bouton (+) pour créer une nouvelle note ou carnet. Ouvrez le " +"menu latéral pour accéder à vos carnets." msgid "You currently have no notebook. Create one by clicking on (+) button." msgstr "" diff --git a/CliClient/locales/joplin.pot b/CliClient/locales/joplin.pot index ee9174741..c8dab0eeb 100644 --- a/CliClient/locales/joplin.pot +++ b/CliClient/locales/joplin.pot @@ -318,6 +318,11 @@ msgstr "" msgid "Please open this URL in your browser to authenticate the application:" msgstr "" +msgid "" +"Cannot refresh token: authentication data is missing. Starting the " +"synchronisation again may fix the problem." +msgstr "" + msgid "" "Please set the \"sync.2.path\" config value to the desired synchronisation " "destination." diff --git a/ReactNativeClient/lib/components/note-body-viewer.js b/ReactNativeClient/lib/components/note-body-viewer.js new file mode 100644 index 000000000..c8ad4046a --- /dev/null +++ b/ReactNativeClient/lib/components/note-body-viewer.js @@ -0,0 +1,189 @@ +import React, { Component } from 'react'; +import { WebView, View } from 'react-native'; +import { globalStyle } from 'lib/components/global-style.js'; +import { Resource } from 'lib/models/resource.js'; +import { shim } from 'lib/shim.js'; +import marked from 'lib/marked.js'; +const Entities = require('html-entities').AllHtmlEntities; +const htmlentities = (new Entities()).encode; + +class NoteBodyViewer extends Component { + + constructor() { + super(); + this.state = { + resources: {}, + } + } + + async loadResource(id) { + const resource = await Resource.load(id); + resource.base64 = await shim.readLocalFileBase64(Resource.fullPath(resource)); + + let newResources = Object.assign({}, this.state.resources); + newResources[id] = resource; + this.setState({ resources: newResources }); + } + + toggleTickAt(body, index) { + let counter = -1; + while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) { + counter++; + + body = body.replace(/- \[(X| )\]/, function(v, p1) { + let s = p1 == ' ' ? 'NOTICK' : 'TICK'; + if (index == counter) { + s = s == 'NOTICK' ? 'TICK' : 'NOTICK'; + } + return '°°JOP°CHECKBOX°' + s + '°°'; + }); + } + + body = body.replace(/°°JOP°CHECKBOX°NOTICK°°/g, '- [ ]'); + body = body.replace(/°°JOP°CHECKBOX°TICK°°/g, '- [X]'); + + return body; + } + + markdownToHtml (body, style) { + // https://necolas.github.io/normalize.css/ + const normalizeCss = ` + html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} + article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible} + pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects} + b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none} + `; + + const css = ` + body { + font-size: ` + style.htmlFontSize + `; + color: ` + style.htmlColor + `; + } + h1 { + font-size: 1.2em; + font-weight: bold; + } + h2 { + font-size: 1em; + font-weight: bold; + } + li { + + } + ul { + padding-left: 1em; + } + a.checkbox { + font-size: 1.4em; + position: relative; + top: 0.1em; + text-decoration: none; + color: ` + style.htmlColor + `; + } + table { + border-collapse: collapse; + } + td, th { + border: 1px solid silver; + padding: .5em 1em .5em 1em; + } + hr { + border: 1px solid ` + style.htmlDividerColor + `; + } + img { + width: 100%; + } + `; + + let counter = -1; + while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) { + body = body.replace(/- \[(X| )\]/, function(v, p1) { + let s = p1 == ' ' ? 'NOTICK' : 'TICK'; + counter++; + return '°°JOP°CHECKBOX°' + s + '°' + counter + '°°'; + }); + } + + const renderer = new marked.Renderer(); + + renderer.link = function (href, title, text) { + if (Resource.isResourceUrl(href)) { + return '[Resource not yet supported: ' + htmlentities(text) + ']'; + } else { + const js = "postMessage(" + JSON.stringify(href) + "); return false;"; + let output = "" + htmlentities(text) + ''; + return output; + } + } + + renderer.image = (href, title, text) => { + const resourceId = Resource.urlToId(href); + if (!this.state.resources[resourceId]) { + this.loadResource(resourceId); + return ''; + } + + const r = this.state.resources[resourceId]; + if (r.mime == 'image/png' || r.mime == 'image/jpg' || r.mime == 'image/gif') { + const src = 'data:' + r.mime + ';base64,' + r.base64; + let output = ''; + return output; + } + + return '[Image: ' + htmlentities(r.title) + '(' + htmlentities(r.mime) + ')]'; + } + + let html = body ? '' + marked(body, { gfm: true, breaks: true, renderer: renderer }) : ''; + + let elementId = 1; + while (html.indexOf('°°JOP°') >= 0) { + html = html.replace(/°°JOP°CHECKBOX°([A-Z]+)°(\d+)°°/, function(v, type, index) { + const js = "postMessage('checkboxclick:" + type + ':' + index + "'); this.textContent = this.textContent == '☐' ? '☑' : '☐'; return false;"; + return '' + (type == 'NOTICK' ? '☐' : '☑') + ''; + }); + } + + let scriptHtml = ''; + + html = '' + html + scriptHtml + ''; + + // console.info(html); + + return html; + } + + render() { + const note = this.props.note; + const style = this.props.style; + const onCheckboxChange = this.props.onCheckboxChange; + + return ( + + { + let msg = event.nativeEvent.data; + + //reg.logger().info('postMessage received: ' + msg); + + if (msg.indexOf('checkboxclick:') === 0) { + msg = msg.split(':'); + let index = Number(msg[msg.length - 1]); + let currentState = msg[msg.length - 2]; // Not really needed but keep it anyway + const newBody = this.toggleTickAt(note.body, index); + if (onCheckboxChange) onCheckboxChange(newBody); + } else if (msg.indexOf('bodyscroll:') === 0) { + msg = msg.split(':'); + this.bodyScrollTop_ = Number(msg[1]); + } else { + Linking.openURL(msg); + } + }} + /> + + ); + } + +} + +export { NoteBodyViewer }; \ No newline at end of file diff --git a/ReactNativeClient/lib/components/note-item.js b/ReactNativeClient/lib/components/note-item.js index 56dff02ae..7d9d00dbb 100644 --- a/ReactNativeClient/lib/components/note-item.js +++ b/ReactNativeClient/lib/components/note-item.js @@ -17,9 +17,11 @@ let styles = { borderBottomColor: globalStyle.dividerColor, alignItems: 'center', paddingLeft: globalStyle.marginLeft, + paddingRight: globalStyle.marginRight, backgroundColor: globalStyle.backgroundColor, }, listItemText: { + flex: 1, color: globalStyle.color, }, }; @@ -70,7 +72,7 @@ class NoteItemComponent extends Component { const listItemStyle = !!Number(note.is_todo) && checkboxChecked ? styles.listItemFadded : styles.listItem; return ( - this.onPress()} underlayColor="#0066FF"> + this.onPress()} underlayColor="#0066FF"> { @@ -237,10 +233,10 @@ class NoteScreenComponent extends BaseScreenComponent { } - async toggleIsTodo_onPress() { - let note = await Note.toggleIsTodo(this.state.note.id); - let newState = { note: note }; - if (!note.id) newState.lastSavedNote = Object.assign({}, note); + toggleIsTodo_onPress() { + let newNote = Note.toggleIsTodo(this.state.note); + let newState = { note: newNote }; + //if (!newNote.id) newState.lastSavedNote = Object.assign({}, newNote); this.setState(newState); } @@ -249,15 +245,6 @@ class NoteScreenComponent extends BaseScreenComponent { this.refreshNoteMetadata(true); } - async loadResource(id) { - const resource = await Resource.load(id); - resource.base64 = await shim.readLocalFileBase64(Resource.fullPath(resource)); - - let newResources = Object.assign({}, this.state.resources); - newResources[id] = resource; - this.setState({ resources: newResources }); - } - async showOnMap_onPress() { if (!this.state.note.id) return; @@ -302,158 +289,11 @@ class NoteScreenComponent extends BaseScreenComponent { let bodyComponent = null; if (this.state.mode == 'view') { - function toggleTickAt(body, index) { - let counter = -1; - while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) { - counter++; + const onCheckboxChange = (newBody) => { + this.saveOneProperty('body', newBody); + }; - body = body.replace(/- \[(X| )\]/, function(v, p1) { - let s = p1 == ' ' ? 'NOTICK' : 'TICK'; - if (index == counter) { - s = s == 'NOTICK' ? 'TICK' : 'NOTICK'; - } - return '°°JOP°CHECKBOX°' + s + '°°'; - }); - } - - body = body.replace(/°°JOP°CHECKBOX°NOTICK°°/g, '- [ ]'); - body = body.replace(/°°JOP°CHECKBOX°TICK°°/g, '- [X]'); - - return body; - } - - const markdownToHtml = (body, style) => { - // https://necolas.github.io/normalize.css/ - const normalizeCss = ` - html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} - article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible} - pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects} - b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none} - `; - - const css = ` - body { - font-size: ` + style.htmlFontSize + `; - color: ` + style.htmlColor + `; - } - h1 { - font-size: 1.2em; - font-weight: bold; - } - h2 { - font-size: 1em; - font-weight: bold; - } - li { - - } - ul { - padding-left: 1em; - } - a.checkbox { - font-size: 1.4em; - position: relative; - top: 0.1em; - text-decoration: none; - color: ` + style.htmlColor + `; - } - table { - border-collapse: collapse; - } - td, th { - border: 1px solid silver; - padding: .5em 1em .5em 1em; - } - hr { - border: 1px solid ` + style.htmlDividerColor + `; - } - img { - width: 100%; - } - `; - - let counter = -1; - while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) { - body = body.replace(/- \[(X| )\]/, function(v, p1) { - let s = p1 == ' ' ? 'NOTICK' : 'TICK'; - counter++; - return '°°JOP°CHECKBOX°' + s + '°' + counter + '°°'; - }); - } - - const renderer = new marked.Renderer(); - - renderer.link = function (href, title, text) { - if (Resource.isResourceUrl(href)) { - return '[Resource not yet supported: ' + htmlentities(text) + ']'; - } else { - const js = "postMessage(" + JSON.stringify(href) + "); return false;"; - let output = "" + htmlentities(text) + ''; - return output; - } - } - - renderer.image = (href, title, text) => { - const resourceId = Resource.urlToId(href); - if (!this.state.resources[resourceId]) { - this.loadResource(resourceId); - return ''; - } - - const r = this.state.resources[resourceId]; - if (r.mime == 'image/png' || r.mime == 'image/jpg' || r.mime == 'image/gif') { - const src = 'data:' + r.mime + ';base64,' + r.base64; - let output = ''; - return output; - } - - return '[Image: ' + htmlentities(r.title) + '(' + htmlentities(r.mime) + ')]'; - } - - let html = note ? '' + marked(body, { gfm: true, breaks: true, renderer: renderer }) : ''; - - let elementId = 1; - while (html.indexOf('°°JOP°') >= 0) { - html = html.replace(/°°JOP°CHECKBOX°([A-Z]+)°(\d+)°°/, function(v, type, index) { - const js = "postMessage('checkboxclick:" + type + ':' + index + "'); this.textContent = this.textContent == '☐' ? '☑' : '☐'; return false;"; - return '' + (type == 'NOTICK' ? '☐' : '☑') + ''; - }); - } - - let scriptHtml = ''; - - html = '' + html + scriptHtml + ''; - - console.info(html); - - return html; - } - - bodyComponent = ( - - { - let msg = event.nativeEvent.data; - - //reg.logger().info('postMessage received: ' + msg); - - if (msg.indexOf('checkboxclick:') === 0) { - msg = msg.split(':'); - let index = Number(msg[msg.length - 1]); - let currentState = msg[msg.length - 2]; // Not really needed but keep it anyway - const newBody = toggleTickAt(note.body, index); - this.saveOneProperty('body', newBody); - } else if (msg.indexOf('bodyscroll:') === 0) { - msg = msg.split(':'); - this.bodyScrollTop_ = Number(msg[1]); - } else { - Linking.openURL(msg); - } - }} - /> - - ); + bodyComponent = { onCheckboxChange(newBody) }}/> } else { const focusBody = !isNew && !!note.title; bodyComponent = ( diff --git a/ReactNativeClient/lib/models/note.js b/ReactNativeClient/lib/models/note.js index 63a33b8cd..919f1d662 100644 --- a/ReactNativeClient/lib/models/note.js +++ b/ReactNativeClient/lib/models/note.js @@ -267,15 +267,17 @@ class Note extends BaseItem { }); } - static async toggleIsTodo(noteId) { - let note = await Note.load(noteId); - const isTodo = !note.is_todo ? 1 : 0; - note.is_todo = isTodo; - if (!note.is_todo) { - note.todo_due = 0; - note.todo_completed = 0; + static toggleIsTodo(note) { + if (!('is_todo' in note)) throw new Error('Missing "is_todo" property'); + + let output = Object.assign({}, note); + output.is_todo = output.is_todo ? 0 : 1; + if (!output.is_todo) { + output.todo_due = 0; + output.todo_completed = 0; } - return note; + + return output; } static async duplicate(noteId, options = null) { diff --git a/ReactNativeClient/lib/synchronizer.js b/ReactNativeClient/lib/synchronizer.js index c2db9a430..098deeb6c 100644 --- a/ReactNativeClient/lib/synchronizer.js +++ b/ReactNativeClient/lib/synchronizer.js @@ -154,7 +154,9 @@ class Synchronizer { error.code = 'alreadyStarted'; throw error; return; - } + } + + this.state_ = 'in_progress'; this.onProgress_ = options.onProgress ? options.onProgress : function(o) {}; this.progressReport_ = { errors: [] }; @@ -175,7 +177,6 @@ class Synchronizer { let outputContext = Object.assign({}, lastContext); - this.state_ = 'in_progress'; this.dispatch({ type: 'SYNC_STARTED' }); @@ -463,7 +464,6 @@ class Synchronizer { this.cancelling_ = false; } - this.state_ = 'idle'; this.progressReport_.completedTime = time.unixMs(); @@ -475,6 +475,8 @@ class Synchronizer { this.progressReport_ = {}; this.dispatch({ type: 'SYNC_COMPLETED' }); + + this.state_ = 'idle'; return outputContext; }