diff --git a/ReactNativeClient/lib/base-model.js b/ReactNativeClient/lib/base-model.js index aa23191e5..c4623ad5d 100644 --- a/ReactNativeClient/lib/base-model.js +++ b/ReactNativeClient/lib/base-model.js @@ -116,11 +116,18 @@ class BaseModel { static applySqlOptions(options, sql, params = null) { if (!options) options = {}; - if (options.orderBy) { - sql += ' ORDER BY ' + options.orderBy; - if (options.caseInsensitive === true) sql += ' COLLATE NOCASE'; - if (options.orderByDir) sql += ' ' + options.orderByDir; + if (options.order && options.order.length) { + let items = []; + for (let i = 0; i < options.order.length; i++) { + const o = options.order[i]; + let item = o.by; + if (options.caseInsensitive === true) item += ' COLLATE NOCASE'; + if (o.dir) item += ' ' + o.dir; + items.push(item); + } + sql += ' ORDER BY ' + items.join(', '); } + if (options.limit) sql += ' LIMIT ' + options.limit; return { sql: sql, params: params }; diff --git a/ReactNativeClient/lib/components/screens/note.js b/ReactNativeClient/lib/components/screens/note.js index fc98a75b9..daf952db7 100644 --- a/ReactNativeClient/lib/components/screens/note.js +++ b/ReactNativeClient/lib/components/screens/note.js @@ -3,6 +3,7 @@ import { BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linkin import { connect } from 'react-redux' import { Log } from 'lib/log.js' import { Note } from 'lib/models/note.js' +import { Resource } from 'lib/models/resource.js' import { Folder } from 'lib/models/folder.js' import { BaseModel } from 'lib/base-model.js' import { ActionButton } from 'lib/components/action-button.js'; @@ -13,6 +14,7 @@ import { Checkbox } from 'lib/components/checkbox.js' import { _ } from 'lib/locale.js'; import marked from 'lib/marked.js'; import { reg } from 'lib/registry.js'; +import { shim } from 'lib/shim.js'; import { BaseScreenComponent } from 'lib/components/base-screen.js'; import { dialogs } from 'lib/dialogs.js'; import { globalStyle } from 'lib/components/global-style.js'; @@ -71,6 +73,7 @@ class NoteScreenComponent extends BaseScreenComponent { folder: null, lastSavedNote: null, isLoading: true, + resources: {}, }; this.bodyScrollTop_ = 0; @@ -244,6 +247,15 @@ 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; @@ -353,6 +365,9 @@ class NoteScreenComponent extends BaseScreenComponent { hr { border: 1px solid ` + style.htmlDividerColor + `; } + img { + width: 100%; + } `; let counter = -1; @@ -365,10 +380,32 @@ class NoteScreenComponent extends BaseScreenComponent { } const renderer = new marked.Renderer(); + renderer.link = function (href, title, text) { - const js = "postMessage(" + JSON.stringify(href) + "); return false;"; - let output = "" + text + ''; - return output; + if (Resource.isResourceUrl(href)) { + return '[Resource not yet supported: ' + href + ']'; // TODO: add title + } else { + const js = "postMessage(" + JSON.stringify(href) + "); return false;"; + let output = "" + 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: ' + r.title + '(' + r.mime + ')]'; } let html = note ? '' + marked(body, { gfm: true, breaks: true, renderer: renderer }) : ''; @@ -376,7 +413,7 @@ class NoteScreenComponent extends BaseScreenComponent { 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;"; + const js = "postMessage('checkboxclick:" + type + ':' + index + "'); this.textContent = this.textContent == '☐' ? '☑' : '☐'; return false;"; return '' + (type == 'NOTICK' ? '☐' : '☑') + ''; }); } @@ -395,7 +432,7 @@ class NoteScreenComponent extends BaseScreenComponent { onMessage={(event) => { let msg = event.nativeEvent.data; - reg.logger().info('postMessage received: ' + msg); + //reg.logger().info('postMessage received: ' + msg); if (msg.indexOf('checkboxclick:') === 0) { msg = msg.split(':'); diff --git a/ReactNativeClient/lib/components/screens/notes.js b/ReactNativeClient/lib/components/screens/notes.js index 3a1e025ef..8219bcb31 100644 --- a/ReactNativeClient/lib/components/screens/notes.js +++ b/ReactNativeClient/lib/components/screens/notes.js @@ -26,8 +26,7 @@ class NotesScreenComponent extends BaseScreenComponent { } async componentWillReceiveProps(newProps) { - if (newProps.notesOrder.orderBy != this.props.notesOrder.orderBy || - newProps.notesOrder.orderByDir != this.props.notesOrder.orderByDir || + if (newProps.notesOrder !== this.props.notesOrder || newProps.selectedFolderId != this.props.selectedFolderId || newProps.selectedTagId != this.props.selectedTagId || newProps.notesParentType != this.props.notesParentType) { @@ -39,8 +38,8 @@ class NotesScreenComponent extends BaseScreenComponent { if (props === null) props = this.props; let options = { - orderBy: props.notesOrder.orderBy, - orderByDir: props.notesOrder.orderByDir, + order: props.notesOrder, + uncompletedTodosOnTop: props.uncompletedTodosOnTop, }; const parent = this.parentItem(props); @@ -153,6 +152,7 @@ const NotesScreen = connect( notes: state.notes, notesOrder: state.notesOrder, notesSource: state.notesSource, + uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop, }; } )(NotesScreenComponent) diff --git a/ReactNativeClient/lib/models/note.js b/ReactNativeClient/lib/models/note.js index 1cbe0eb23..2b1677a23 100644 --- a/ReactNativeClient/lib/models/note.js +++ b/ReactNativeClient/lib/models/note.js @@ -63,14 +63,14 @@ class Note extends BaseItem { return output; } - static sortNotes(notes, order) { - return notes.sort((a, b) => { - let r = -1; - if (a[order.orderBy] < b[order.orderBy]) r = +1; - if (order.orderByDir == 'ASC') r = -r; - return r; - }); - } + // static sortNotes(notes, order) { + // return notes.sort((a, b) => { + // // let r = -1; + // // if (a[order.orderBy] < b[order.orderBy]) r = +1; + // // if (order.orderByDir == 'ASC') r = -r; + // // return r; + // }); + // } static previewFields() { return ['id', 'title', 'body', 'is_todo', 'todo_completed', 'parent_id', 'updated_time']; @@ -95,13 +95,13 @@ class Note extends BaseItem { return results.length ? results[0] : null; } - static previews(parentId, options = null) { + static async previews(parentId, options = null) { if (!options) options = {}; - if (!options.orderBy) options.orderBy = 'updated_time'; - if (!options.orderByDir) options.orderByDir = 'DESC'; + if (!options.order) options.order = { by: 'updated_time', dir: 'DESC' }; if (!options.conditions) options.conditions = []; if (!options.conditionsParams) options.conditionsParams = []; if (!options.fields) options.fields = this.previewFields(); + if (!options.uncompletedTodosOnTop) options.uncompletedTodosOnTop = false; if (parentId == Folder.conflictFolderId()) { options.conditions.push('is_conflict = 1'); @@ -120,16 +120,47 @@ class Note extends BaseItem { options.conditionsParams.push(pattern); } + let hasNotes = true; + let hasTodos = true; if (options.itemTypes && options.itemTypes.length) { - if (options.itemTypes.indexOf('note') >= 0 && options.itemTypes.indexOf('todo') >= 0) { - // Fetch everything - } else if (options.itemTypes.indexOf('note') >= 0) { - options.conditions.push('is_todo = 0'); - } else if (options.itemTypes.indexOf('todo') >= 0) { - options.conditions.push('is_todo = 1'); + if (options.itemTypes.indexOf('note') < 0) { + hasNotes = false; + } else if (options.itemTypes.indexOf('todo') < 0) { + hasTodos = false; } } + if (options.uncompletedTodosOnTop && hasTodos) { + let cond = options.conditions.slice(); + cond.push('is_todo = 1'); + cond.push('(todo_completed <= 0 OR todo_completed IS NULL)'); + let tempOptions = Object.assign({}, options); + tempOptions.conditions = cond; + + let uncompletedTodos = await this.search(tempOptions); + + cond = options.conditions.slice(); + if (hasNotes && hasTodos) { + cond.push('(is_todo = 0 OR (is_todo = 1 AND todo_completed > 0))'); + } else { + cond.push('(is_todo = 1 AND todo_completed > 0)'); + } + + tempOptions = Object.assign({}, options); + tempOptions.conditions = cond; + let theRest = await this.search(tempOptions); + + return uncompletedTodos.concat(theRest); + } + + if (hasNotes && hasTodos) { + + } else if (hasNotes) { + options.conditions.push('is_todo = 0'); + } else if (hasTodos) { + options.conditions.push('is_todo = 1'); + } + return this.search(options); } diff --git a/ReactNativeClient/lib/models/resource.js b/ReactNativeClient/lib/models/resource.js index 90dbddb27..69810de77 100644 --- a/ReactNativeClient/lib/models/resource.js +++ b/ReactNativeClient/lib/models/resource.js @@ -44,6 +44,15 @@ class Resource extends BaseItem { return this.fsDriver().writeBinaryFile(this.fullPath(resource), content); } + static isResourceUrl(url) { + return url.length === 34 && url[0] === ':' && url[1] === '/'; + } + + static urlToId(url) { + if (!this.isResourceUrl(url)) throw new Error('Not a valid resource URL: ' + url); + return url.substr(2); + } + } export { Resource }; \ No newline at end of file diff --git a/ReactNativeClient/lib/models/setting.js b/ReactNativeClient/lib/models/setting.js index d46c23b4e..441e3becc 100644 --- a/ReactNativeClient/lib/models/setting.js +++ b/ReactNativeClient/lib/models/setting.js @@ -42,20 +42,21 @@ class Setting extends BaseModel { this.cancelScheduleSave(); this.cache_ = []; return this.modelSelectAll('SELECT * FROM settings').then((rows) => { - this.cache_ = rows; + this.cache_ = []; - for (let i = 0; i < this.cache_.length; i++) { - let c = this.cache_[i]; + // Old keys - can be removed later + const ignore = ['clientId', 'sync.onedrive.auth', 'syncInterval', 'todoOnTop', 'todosOnTop']; - if (c.key == 'clientId') continue; // For older clients - if (c.key == 'sync.onedrive.auth') continue; // For older clients - if (c.key == 'syncInterval') continue; // For older clients + for (let i = 0; i < rows.length; i++) { + let c = rows[i]; + + if (ignore.indexOf(c.key) >= 0) continue; // console.info(c.key + ' = ' + c.value); c.value = this.formatValue(c.key, c.value); - this.cache_[i] = c; + this.cache_.push(c); } const keys = this.keys(); @@ -303,6 +304,7 @@ Setting.metadata_ = { recent: _('Non-completed and recently completed ones'), nonCompleted: _('Non-completed ones only'), })}, + 'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted todos on top of the lists') }, 'trackLocation': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Save location with notes') }, 'sync.interval': { value: 300, type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation interval'), options: () => { return { diff --git a/ReactNativeClient/lib/services/report.js b/ReactNativeClient/lib/services/report.js index adbf17423..1d1fd787b 100644 --- a/ReactNativeClient/lib/services/report.js +++ b/ReactNativeClient/lib/services/report.js @@ -71,7 +71,7 @@ class ReportService { section.body = []; let folders = await Folder.all({ - orderBy: 'title', + order: { by: 'title', dir: 'ASC' }, caseInsensitive: true, }); diff --git a/ReactNativeClient/lib/shim-init-react.js b/ReactNativeClient/lib/shim-init-react.js index f0928c0c1..a1d520d5b 100644 --- a/ReactNativeClient/lib/shim-init-react.js +++ b/ReactNativeClient/lib/shim-init-react.js @@ -38,6 +38,10 @@ function shimInit() { throw new Error('fetchBlob: ' + method + ' ' + url + ': ' + error.toString()); } } + + shim.readLocalFileBase64 = async function(path) { + return RNFetchBlob.fs.readFile(path, 'base64') + } } export { shimInit } \ No newline at end of file diff --git a/ReactNativeClient/lib/shim.js b/ReactNativeClient/lib/shim.js index a011ed864..ed7f383f9 100644 --- a/ReactNativeClient/lib/shim.js +++ b/ReactNativeClient/lib/shim.js @@ -13,5 +13,6 @@ shim.fetch = typeof fetch !== 'undefined' ? fetch : null; shim.FormData = typeof FormData !== 'undefined' ? FormData : null; shim.fs = null; shim.FileApiDriverLocal = null; +shim.readLocalFileBase64 = () => { throw new Error('Not implemented'); } export { shim }; \ No newline at end of file diff --git a/ReactNativeClient/root.js b/ReactNativeClient/root.js index 98f4b9d15..c3ab8d759 100644 --- a/ReactNativeClient/root.js +++ b/ReactNativeClient/root.js @@ -49,10 +49,9 @@ let defaultState = { screens: {}, loading: true, historyCanGoBack: false, - notesOrder: { - orderBy: 'updated_time', - orderByDir: 'DESC', - }, + notesOrder: [ + { by: 'updated_time', dir: 'DESC' }, + ], syncStarted: false, syncReport: {}, searchQuery: '', @@ -87,6 +86,27 @@ function reducerActionsAreSame(a1, a2) { return true; } +function updateStateFromSettings(action, newState) { + // if (action.type == 'SETTINGS_UPDATE_ALL' || action.key == 'uncompletedTodosOnTop') { + // let newNotesOrder = []; + // for (let i = 0; i < newState.notesOrder.length; i++) { + // const o = newState.notesOrder[i]; + // if (o.by == 'is_todo') continue; + // newNotesOrder.push(o); + // } + + // if (newState.settings['uncompletedTodosOnTop']) { + // newNotesOrder.unshift({ by: 'is_todo', dir: 'DESC' }); + // } + + // newState.notesOrder = newNotesOrder; + + // console.info('NEW', newNotesOrder); + // } + + return newState; +} + const reducer = (state = defaultState, action) => { let newState = state; let historyGoingBack = false; @@ -179,6 +199,7 @@ const reducer = (state = defaultState, action) => { newState = Object.assign({}, state); newState.settings = action.settings; + newState = updateStateFromSettings(action, newState); break; case 'SETTINGS_UPDATE_ONE': @@ -187,6 +208,7 @@ const reducer = (state = defaultState, action) => { let newSettings = Object.assign({}, state.settings); newSettings[action.key] = action.value; newState.settings = newSettings; + newState = updateStateFromSettings(action, newState); break; // Replace all the notes with the provided array @@ -223,7 +245,7 @@ const reducer = (state = defaultState, action) => { if (!found && ('parent_id' in modNote) && modNote.parent_id == state.selectedFolderId) newNotes.push(modNote); - newNotes = Note.sortNotes(newNotes, state.notesOrder); + //newNotes = Note.sortNotes(newNotes, state.notesOrder); newState = Object.assign({}, state); newState.notes = newNotes; break;