You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-26 22:41:17 +02:00
Started fixing ReactNative app
This commit is contained in:
244
ReactNativeClient/lib/models/base-item.js
Normal file
244
ReactNativeClient/lib/models/base-item.js
Normal file
@@ -0,0 +1,244 @@
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Database } from 'lib/database.js';
|
||||
import { time } from 'lib/time-utils.js';
|
||||
import moment from 'moment';
|
||||
|
||||
class BaseItem extends BaseModel {
|
||||
|
||||
static useUuid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Need to dynamically load the classes like this to avoid circular dependencies
|
||||
static getClass(name) {
|
||||
if (!this.classes_) this.classes_ = {};
|
||||
if (this.classes_[name]) return this.classes_[name];
|
||||
let filename = name.toLowerCase();
|
||||
if (name == 'NoteTag') filename = 'note-tag';
|
||||
this.classes_[name] = require('lib/models/' + filename + '.js')[name];
|
||||
return this.classes_[name];
|
||||
}
|
||||
|
||||
static systemPath(itemOrId) {
|
||||
if (typeof itemOrId === 'string') return itemOrId + '.md';
|
||||
return itemOrId.id + '.md';
|
||||
}
|
||||
|
||||
static itemClass(item) {
|
||||
if (!item) throw new Error('Item cannot be null');
|
||||
|
||||
if (typeof item === 'object') {
|
||||
if (!('type_' in item)) throw new Error('Item does not have a type_ property');
|
||||
return this.itemClass(item.type_);
|
||||
} else {
|
||||
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
||||
let d = BaseItem.syncItemDefinitions_[i];
|
||||
if (Number(item) == d.type) return this.getClass(d.className);
|
||||
}
|
||||
throw new Error('Unknown type: ' + item);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the IDs of the items that have been synced at least once
|
||||
static async syncedItems() {
|
||||
let folders = await this.getClass('Folder').modelSelectAll('SELECT id FROM folders WHERE sync_time > 0');
|
||||
let notes = await this.getClass('Note').modelSelectAll('SELECT id FROM notes WHERE is_conflict = 0 AND sync_time > 0');
|
||||
let resources = await this.getClass('Resource').modelSelectAll('SELECT id FROM resources WHERE sync_time > 0');
|
||||
let tags = await this.getClass('Tag').modelSelectAll('SELECT id FROM tags WHERE sync_time > 0');
|
||||
let noteTags = await this.getClass('NoteTag').modelSelectAll('SELECT id FROM note_tags WHERE sync_time > 0');
|
||||
return folders.concat(notes).concat(resources).concat(tags).concat(noteTags);
|
||||
}
|
||||
|
||||
static pathToId(path) {
|
||||
let s = path.split('.');
|
||||
return s[0];
|
||||
}
|
||||
|
||||
static loadItemByPath(path) {
|
||||
return this.loadItemById(this.pathToId(path));
|
||||
}
|
||||
|
||||
static async loadItemById(id) {
|
||||
let classes = this.syncItemClassNames();
|
||||
for (let i = 0; i < classes.length; i++) {
|
||||
let item = await this.getClass(classes[i]).load(id);
|
||||
if (item) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static loadItemByField(itemType, field, value) {
|
||||
let ItemClass = this.itemClass(itemType);
|
||||
return ItemClass.loadByField(field, value);
|
||||
}
|
||||
|
||||
static loadItem(itemType, id) {
|
||||
let ItemClass = this.itemClass(itemType);
|
||||
return ItemClass.load(id);
|
||||
}
|
||||
|
||||
static deleteItem(itemType, id) {
|
||||
let ItemClass = this.itemClass(itemType);
|
||||
return ItemClass.delete(id);
|
||||
}
|
||||
|
||||
static async delete(id, options = null) {
|
||||
let trackDeleted = true;
|
||||
if (options && options.trackDeleted !== null && options.trackDeleted !== undefined) trackDeleted = options.trackDeleted;
|
||||
|
||||
await super.delete(id, options);
|
||||
|
||||
if (trackDeleted) {
|
||||
await this.db().exec('INSERT INTO deleted_items (item_type, item_id, deleted_time) VALUES (?, ?, ?)', [this.modelType(), id, time.unixMs()]);
|
||||
}
|
||||
}
|
||||
|
||||
static deletedItems() {
|
||||
return this.db().selectAll('SELECT * FROM deleted_items');
|
||||
}
|
||||
|
||||
static remoteDeletedItem(itemId) {
|
||||
return this.db().exec('DELETE FROM deleted_items WHERE item_id = ?', [itemId]);
|
||||
}
|
||||
|
||||
static serialize_format(propName, propValue) {
|
||||
if (['created_time', 'updated_time'].indexOf(propName) >= 0) {
|
||||
if (!propValue) return '';
|
||||
propValue = moment.unix(propValue / 1000).utc().format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
|
||||
} else if (propValue === null || propValue === undefined) {
|
||||
propValue = '';
|
||||
}
|
||||
|
||||
return propValue;
|
||||
}
|
||||
|
||||
static unserialize_format(type, propName, propValue) {
|
||||
if (propName[propName.length - 1] == '_') return propValue; // Private property
|
||||
|
||||
let ItemClass = this.itemClass(type);
|
||||
|
||||
if (['created_time', 'updated_time'].indexOf(propName) >= 0) {
|
||||
if (!propValue) return 0;
|
||||
propValue = moment(propValue, 'YYYY-MM-DDTHH:mm:ss.SSSZ').format('x');
|
||||
} else {
|
||||
propValue = Database.formatValue(ItemClass.fieldType(propName), propValue);
|
||||
}
|
||||
|
||||
return propValue;
|
||||
}
|
||||
|
||||
static async serialize(item, type = null, shownKeys = null) {
|
||||
item = this.filter(item);
|
||||
|
||||
let output = [];
|
||||
|
||||
if ('title' in item) {
|
||||
output.push(item.title);
|
||||
output.push('');
|
||||
}
|
||||
|
||||
if ('body' in item) {
|
||||
output.push(item.body);
|
||||
if (shownKeys.length) output.push('');
|
||||
}
|
||||
|
||||
for (let i = 0; i < shownKeys.length; i++) {
|
||||
let key = shownKeys[i];
|
||||
let value = null;
|
||||
if (typeof key === 'function') {
|
||||
let r = await key();
|
||||
key = r.key;
|
||||
value = r.value;
|
||||
} else {
|
||||
value = this.serialize_format(key, item[key]);
|
||||
}
|
||||
|
||||
output.push(key + ': ' + value);
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
|
||||
static async unserialize(content) {
|
||||
let lines = content.split("\n");
|
||||
let output = {};
|
||||
let state = 'readingProps';
|
||||
let body = [];
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
let line = lines[i];
|
||||
|
||||
if (state == 'readingProps') {
|
||||
line = line.trim();
|
||||
|
||||
if (line == '') {
|
||||
state = 'readingBody';
|
||||
continue;
|
||||
}
|
||||
|
||||
let p = line.indexOf(':');
|
||||
if (p < 0) throw new Error('Invalid property format: ' + line + ": " + content);
|
||||
let key = line.substr(0, p).trim();
|
||||
let value = line.substr(p + 1).trim();
|
||||
output[key] = value;
|
||||
} else if (state == 'readingBody') {
|
||||
body.splice(0, 0, line);
|
||||
}
|
||||
}
|
||||
|
||||
if (!output.type_) throw new Error('Missing required property: type_: ' + content);
|
||||
output.type_ = Number(output.type_);
|
||||
|
||||
if (body.length) {
|
||||
let title = body.splice(0, 2);
|
||||
output.title = title[0];
|
||||
}
|
||||
|
||||
if (body.length) output.body = body.join("\n");
|
||||
|
||||
for (let n in output) {
|
||||
if (!output.hasOwnProperty(n)) continue;
|
||||
output[n] = await this.unserialize_format(output.type_, n, output[n]);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static async itemsThatNeedSync(limit = 100) {
|
||||
let items = await this.getClass('Folder').modelSelectAll('SELECT * FROM folders WHERE sync_time < updated_time LIMIT ' + limit);
|
||||
if (items.length) return { hasMore: true, items: items };
|
||||
|
||||
items = await this.getClass('Resource').modelSelectAll('SELECT * FROM resources WHERE sync_time < updated_time LIMIT ' + limit);
|
||||
if (items.length) return { hasMore: true, items: items };
|
||||
|
||||
items = await this.getClass('Note').modelSelectAll('SELECT * FROM notes WHERE sync_time < updated_time AND is_conflict = 0 LIMIT ' + limit);
|
||||
if (items.length) return { hasMore: true, items: items };
|
||||
|
||||
items = await this.getClass('Tag').modelSelectAll('SELECT * FROM tags WHERE sync_time < updated_time LIMIT ' + limit);
|
||||
if (items.length) return { hasMore: true, items: items };
|
||||
|
||||
items = await this.getClass('NoteTag').modelSelectAll('SELECT * FROM note_tags WHERE sync_time < updated_time LIMIT ' + limit);
|
||||
return { hasMore: items.length >= limit, items: items };
|
||||
}
|
||||
|
||||
static syncItemClassNames() {
|
||||
return BaseItem.syncItemDefinitions_.map((def) => {
|
||||
return def.className;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Also update:
|
||||
// - itemsThatNeedSync()
|
||||
// - syncedItems()
|
||||
|
||||
BaseItem.syncItemDefinitions_ = [
|
||||
{ type: BaseModel.TYPE_NOTE, className: 'Note' },
|
||||
{ type: BaseModel.TYPE_FOLDER, className: 'Folder' },
|
||||
{ type: BaseModel.TYPE_RESOURCE, className: 'Resource' },
|
||||
{ type: BaseModel.TYPE_TAG, className: 'Tag' },
|
||||
{ type: BaseModel.TYPE_NOTE_TAG, className: 'NoteTag' },
|
||||
];
|
||||
|
||||
export { BaseItem };
|
||||
96
ReactNativeClient/lib/models/folder.js
Normal file
96
ReactNativeClient/lib/models/folder.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Log } from 'lib/log.js';
|
||||
import { promiseChain } from 'lib/promise-utils.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { Setting } from 'lib/models/setting.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import moment from 'moment';
|
||||
import { BaseItem } from 'lib/models/base-item.js';
|
||||
import lodash from 'lodash';
|
||||
|
||||
class Folder extends BaseItem {
|
||||
|
||||
static tableName() {
|
||||
return 'folders';
|
||||
}
|
||||
|
||||
static async serialize(folder) {
|
||||
let fieldNames = this.fieldNames();
|
||||
fieldNames.push('type_');
|
||||
lodash.pull(fieldNames, 'parent_id', 'sync_time');
|
||||
return super.serialize(folder, 'folder', fieldNames);
|
||||
}
|
||||
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_FOLDER;
|
||||
}
|
||||
|
||||
static newFolder() {
|
||||
return {
|
||||
id: null,
|
||||
title: '',
|
||||
}
|
||||
}
|
||||
|
||||
static noteIds(parentId) {
|
||||
return this.db().selectAll('SELECT id FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]).then((rows) => {
|
||||
let output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let row = rows[i];
|
||||
output.push(row.id);
|
||||
}
|
||||
return output;
|
||||
});
|
||||
}
|
||||
|
||||
static async delete(folderId, options = null) {
|
||||
let folder = await Folder.load(folderId);
|
||||
if (!folder) throw new Error('Trying to delete non-existing notebook: ' + folderId);
|
||||
|
||||
let noteIds = await Folder.noteIds(folderId);
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
await Note.delete(noteIds[i]);
|
||||
}
|
||||
|
||||
await super.delete(folderId, options);
|
||||
|
||||
this.dispatch({
|
||||
type: 'FOLDER_DELETE',
|
||||
folderId: folderId,
|
||||
});
|
||||
}
|
||||
|
||||
static async all(options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
let folders = await super.all(options);
|
||||
if (!options.includeNotes) return folders;
|
||||
|
||||
if (options.limit) options.limit -= folders.length;
|
||||
|
||||
let notes = await Note.all(options);
|
||||
return folders.concat(notes);
|
||||
}
|
||||
|
||||
static defaultFolder() {
|
||||
return this.modelSelectOne('SELECT * FROM folders ORDER BY created_time DESC LIMIT 1');
|
||||
}
|
||||
|
||||
static async save(o, options = null) {
|
||||
if (options && options.duplicateCheck === true && o.title) {
|
||||
let existingFolder = await Folder.loadByTitle(o.title);
|
||||
if (existingFolder) throw new Error(_('A notebook with this title already exists: "%s"', o.title));
|
||||
}
|
||||
|
||||
return super.save(o, options).then((folder) => {
|
||||
this.dispatch({
|
||||
type: 'FOLDERS_UPDATE_ONE',
|
||||
folder: folder,
|
||||
});
|
||||
return folder;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Folder };
|
||||
25
ReactNativeClient/lib/models/note-tag.js
Normal file
25
ReactNativeClient/lib/models/note-tag.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Database } from 'lib/database.js';
|
||||
import { BaseItem } from 'lib/models/base-item.js';
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import lodash from 'lodash';
|
||||
|
||||
class NoteTag extends BaseItem {
|
||||
|
||||
static tableName() {
|
||||
return 'note_tags';
|
||||
}
|
||||
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_NOTE_TAG;
|
||||
}
|
||||
|
||||
static async serialize(item, type = null, shownKeys = null) {
|
||||
let fieldNames = this.fieldNames();
|
||||
fieldNames.push('type_');
|
||||
lodash.pull(fieldNames, 'sync_time');
|
||||
return super.serialize(item, 'note_tag', fieldNames);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { NoteTag };
|
||||
147
ReactNativeClient/lib/models/note.js
Normal file
147
ReactNativeClient/lib/models/note.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Log } from 'lib/log.js';
|
||||
import { Folder } from 'lib/models/folder.js';
|
||||
import { GeolocationReact } from 'lib/geolocation-react.js';
|
||||
import { BaseItem } from 'lib/models/base-item.js';
|
||||
import { Setting } from 'lib/models/setting.js';
|
||||
import moment from 'moment';
|
||||
import lodash from 'lodash';
|
||||
|
||||
class Note extends BaseItem {
|
||||
|
||||
static tableName() {
|
||||
return 'notes';
|
||||
}
|
||||
|
||||
static async serialize(note, type = null, shownKeys = null) {
|
||||
let fieldNames = this.fieldNames();
|
||||
fieldNames.push('type_');
|
||||
lodash.pull(fieldNames, 'is_conflict', 'sync_time', 'body'); // Exclude 'body' since it's going to be added separately at the top of the note
|
||||
return super.serialize(note, 'note', fieldNames);
|
||||
}
|
||||
|
||||
static async serializeForEdit(note) {
|
||||
return super.serialize(note, 'note', []);
|
||||
}
|
||||
|
||||
static async unserializeForEdit(content) {
|
||||
content += "\n\ntype_: " + BaseModel.TYPE_NOTE;
|
||||
return super.unserialize(content);
|
||||
}
|
||||
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_NOTE;
|
||||
}
|
||||
|
||||
static new(parentId = '') {
|
||||
let output = super.new();
|
||||
output.parent_id = parentId;
|
||||
return output;
|
||||
}
|
||||
|
||||
static newTodo(parentId = '') {
|
||||
let output = this.new(parentId);
|
||||
output.is_todo = true;
|
||||
return output;
|
||||
}
|
||||
|
||||
static previewFields() {
|
||||
return ['id', 'title', 'body', 'is_todo', 'todo_completed', 'parent_id', 'updated_time'];
|
||||
}
|
||||
|
||||
static previewFieldsSql() {
|
||||
return this.db().escapeFields(this.previewFields()).join(',');
|
||||
}
|
||||
|
||||
static loadFolderNoteByField(folderId, field, value) {
|
||||
return this.modelSelectOne('SELECT * FROM notes WHERE is_conflict = 0 AND `parent_id` = ? AND `' + field + '` = ?', [folderId, value]);
|
||||
}
|
||||
|
||||
static previews(parentId, options = null) {
|
||||
if (!options) options = {};
|
||||
if (!options.orderBy) options.orderBy = 'updated_time';
|
||||
if (!options.orderByDir) options.orderByDir = 'DESC';
|
||||
if (!options.conditions) options.conditions = [];
|
||||
if (!options.conditionsParams) options.conditionsParams = [];
|
||||
if (!options.fields) options.fields = this.previewFields();
|
||||
|
||||
options.conditions.push('is_conflict = 0');
|
||||
|
||||
options.conditions.push('parent_id = ?');
|
||||
options.conditionsParams.push(parentId);
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
return this.search(options);
|
||||
}
|
||||
|
||||
static preview(noteId) {
|
||||
return this.modelSelectOne('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE is_conflict = 0 AND id = ?', [noteId]);
|
||||
}
|
||||
|
||||
static conflictedNotes() {
|
||||
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 1');
|
||||
}
|
||||
|
||||
static unconflictedNotes() {
|
||||
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0');
|
||||
}
|
||||
|
||||
static updateGeolocation(noteId) {
|
||||
Log.info('Updating lat/long of note ' + noteId);
|
||||
|
||||
let geoData = null;
|
||||
return GeolocationReact.currentPosition().then((data) => {
|
||||
Log.info('Got lat/long');
|
||||
geoData = data;
|
||||
return Note.load(noteId);
|
||||
}).then((note) => {
|
||||
if (!note) return; // Race condition - note has been deleted in the meantime
|
||||
note.longitude = geoData.coords.longitude;
|
||||
note.latitude = geoData.coords.latitude;
|
||||
note.altitude = geoData.coords.altitude;
|
||||
return Note.save(note);
|
||||
}).catch((error) => {
|
||||
Log.info('Cannot get location:', error);
|
||||
});
|
||||
}
|
||||
|
||||
static filter(note) {
|
||||
if (!note) return note;
|
||||
|
||||
let output = super.filter(note);
|
||||
if ('longitude' in output) output.longitude = Number(!output.longitude ? 0 : output.longitude).toFixed(8);
|
||||
if ('latitude' in output) output.latitude = Number(!output.latitude ? 0 : output.latitude).toFixed(8);
|
||||
if ('altitude' in output) output.altitude = Number(!output.altitude ? 0 : output.altitude).toFixed(4);
|
||||
return output;
|
||||
}
|
||||
|
||||
static save(o, options = null) {
|
||||
let isNew = this.isNew(o, options);
|
||||
if (isNew && !o.source) o.source = Setting.value('appName');
|
||||
if (isNew && !o.source_application) o.source_application = Setting.value('appId');
|
||||
|
||||
return super.save(o, options).then((result) => {
|
||||
// 'result' could be a partial one at this point (if, for example, only one property of it was saved)
|
||||
// so call this.preview() so that the right fields are populated.
|
||||
return this.load(result.id);
|
||||
}).then((note) => {
|
||||
this.dispatch({
|
||||
type: 'NOTES_UPDATE_ONE',
|
||||
note: note,
|
||||
});
|
||||
return note;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Note };
|
||||
50
ReactNativeClient/lib/models/resource.js
Normal file
50
ReactNativeClient/lib/models/resource.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { BaseItem } from 'lib/models/base-item.js';
|
||||
import { Setting } from 'lib/models/setting.js';
|
||||
import { mime } from 'lib/mime-utils.js';
|
||||
import { filename } from 'lib/path-utils.js';
|
||||
import lodash from 'lodash';
|
||||
|
||||
class Resource extends BaseItem {
|
||||
|
||||
static tableName() {
|
||||
return 'resources';
|
||||
}
|
||||
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_RESOURCE;
|
||||
}
|
||||
|
||||
static async serialize(item, type = null, shownKeys = null) {
|
||||
let fieldNames = this.fieldNames();
|
||||
fieldNames.push('type_');
|
||||
lodash.pull(fieldNames, 'sync_time');
|
||||
return super.serialize(item, 'resource', fieldNames);
|
||||
}
|
||||
|
||||
static fullPath(resource) {
|
||||
let extension = mime.toFileExtension(resource.mime);
|
||||
extension = extension ? '.' + extension : '';
|
||||
return Setting.value('resourceDir') + '/' + resource.id + extension;
|
||||
}
|
||||
|
||||
static pathToId(path) {
|
||||
return filename(path);
|
||||
}
|
||||
|
||||
static content(resource) {
|
||||
// TODO: node-only, and should probably be done with streams
|
||||
const fs = require('fs-extra');
|
||||
return fs.readFile(this.fullPath(resource));
|
||||
}
|
||||
|
||||
static setContent(resource, content) {
|
||||
// TODO: node-only, and should probably be done with streams
|
||||
const fs = require('fs-extra');
|
||||
let buffer = new Buffer(content);
|
||||
return fs.writeFile(this.fullPath(resource), buffer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Resource };
|
||||
11
ReactNativeClient/lib/models/session.js
Normal file
11
ReactNativeClient/lib/models/session.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
|
||||
class Session extends BaseModel {
|
||||
|
||||
static login(email, password) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Session };
|
||||
157
ReactNativeClient/lib/models/setting.js
Normal file
157
ReactNativeClient/lib/models/setting.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Database } from 'lib/database.js';
|
||||
|
||||
class Setting extends BaseModel {
|
||||
|
||||
static tableName() {
|
||||
return 'settings';
|
||||
}
|
||||
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_SETTING;
|
||||
}
|
||||
|
||||
static defaultSetting(key) {
|
||||
if (!(key in this.defaults_)) throw new Error('Unknown key: ' + key);
|
||||
let output = Object.assign({}, this.defaults_[key]);
|
||||
output.key = key;
|
||||
return output;
|
||||
}
|
||||
|
||||
static keys() {
|
||||
if (this.keys_) return this.keys_;
|
||||
this.keys_ = [];
|
||||
for (let n in this.defaults_) {
|
||||
if (!this.defaults_.hasOwnProperty(n)) continue;
|
||||
this.keys_.push(n);
|
||||
}
|
||||
return this.keys_;
|
||||
}
|
||||
|
||||
static publicKeys() {
|
||||
let output = [];
|
||||
for (let n in this.defaults_) {
|
||||
if (!this.defaults_.hasOwnProperty(n)) continue;
|
||||
if (this.defaults_[n].public) output.push(n);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static load() {
|
||||
this.cancelScheduleUpdate();
|
||||
this.cache_ = [];
|
||||
return this.modelSelectAll('SELECT * FROM settings').then((rows) => {
|
||||
this.cache_ = rows;
|
||||
});
|
||||
}
|
||||
|
||||
static setConstant(key, value) {
|
||||
this.constants_[key] = value;
|
||||
}
|
||||
|
||||
static setValue(key, value) {
|
||||
if (!this.cache_) throw new Error('Settings have not been initialized!');
|
||||
|
||||
for (let i = 0; i < this.cache_.length; i++) {
|
||||
if (this.cache_[i].key == key) {
|
||||
if (this.cache_[i].value === value) return;
|
||||
this.cache_[i].value = value;
|
||||
this.scheduleUpdate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let s = this.defaultSetting(key);
|
||||
s.value = value;
|
||||
this.cache_.push(s);
|
||||
this.scheduleUpdate();
|
||||
}
|
||||
|
||||
static value(key) {
|
||||
if (key in this.constants_) return this.constants_[key];
|
||||
|
||||
if (!this.cache_) throw new Error('Settings have not been initialized!');
|
||||
|
||||
for (let i = 0; i < this.cache_.length; i++) {
|
||||
if (this.cache_[i].key == key) {
|
||||
return this.cache_[i].value;
|
||||
}
|
||||
}
|
||||
|
||||
let s = this.defaultSetting(key);
|
||||
return s.value;
|
||||
}
|
||||
|
||||
// Currently only supports objects with properties one level deep
|
||||
static object(key) {
|
||||
let output = {};
|
||||
let keys = this.keys();
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let k = keys[i].split('.');
|
||||
if (k[0] == key) {
|
||||
output[k[1]] = this.value(keys[i]);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// Currently only supports objects with properties one level deep
|
||||
static setObject(key, object) {
|
||||
for (let n in object) {
|
||||
if (!object.hasOwnProperty(n)) continue;
|
||||
this.setValue(key + '.' + n, object[n]);
|
||||
}
|
||||
}
|
||||
|
||||
static saveAll() {
|
||||
if (!this.updateTimeoutId_) return Promise.resolve();
|
||||
|
||||
this.logger().info('Saving settings...');
|
||||
clearTimeout(this.updateTimeoutId_);
|
||||
this.updateTimeoutId_ = null;
|
||||
|
||||
let queries = [];
|
||||
queries.push('DELETE FROM settings');
|
||||
for (let i = 0; i < this.cache_.length; i++) {
|
||||
let s = Object.assign({}, this.cache_[i]);
|
||||
delete s.public;
|
||||
queries.push(Database.insertQuery(this.tableName(), s));
|
||||
}
|
||||
|
||||
return BaseModel.db().transactionExecBatch(queries).then(() => {
|
||||
this.logger().info('Settings have been saved.');
|
||||
});
|
||||
}
|
||||
|
||||
static scheduleUpdate() {
|
||||
if (this.updateTimeoutId_) clearTimeout(this.updateTimeoutId_);
|
||||
|
||||
this.updateTimeoutId_ = setTimeout(() => {
|
||||
this.saveAll();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
static cancelScheduleUpdate() {
|
||||
if (this.updateTimeoutId_) clearTimeout(this.updateTimeoutId_);
|
||||
this.updateTimeoutId_ = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Setting.defaults_ = {
|
||||
'clientId': { value: '', type: 'string', public: false },
|
||||
'activeFolderId': { value: '', type: 'string', public: false },
|
||||
'sync.onedrive.auth': { value: '', type: 'string', public: false },
|
||||
'sync.local.path': { value: '', type: 'string', public: true },
|
||||
'sync.target': { value: 'onedrive', type: 'string', public: true },
|
||||
'editor': { value: '', type: 'string', public: true },
|
||||
};
|
||||
|
||||
// Contains constants that are set by the application and
|
||||
// cannot be modified by the user:
|
||||
Setting.constants_ = {
|
||||
'appName': 'joplin',
|
||||
'appId': 'SET_ME', // Each app should set this identifier
|
||||
}
|
||||
|
||||
export { Setting };
|
||||
68
ReactNativeClient/lib/models/tag.js
Normal file
68
ReactNativeClient/lib/models/tag.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { BaseModel } from 'lib/base-model.js';
|
||||
import { Database } from 'lib/database.js';
|
||||
import { BaseItem } from 'lib/models/base-item.js';
|
||||
import { NoteTag } from 'lib/models/note-tag.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { time } from 'lib/time-utils.js';
|
||||
import lodash from 'lodash';
|
||||
|
||||
class Tag extends BaseItem {
|
||||
|
||||
static tableName() {
|
||||
return 'tags';
|
||||
}
|
||||
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_TAG;
|
||||
}
|
||||
|
||||
static async serialize(item, type = null, shownKeys = null) {
|
||||
let fieldNames = this.fieldNames();
|
||||
fieldNames.push('type_');
|
||||
lodash.pull(fieldNames, 'sync_time');
|
||||
return super.serialize(item, 'tag', fieldNames);
|
||||
}
|
||||
|
||||
static async tagNoteIds(tagId) {
|
||||
let rows = await this.db().selectAll('SELECT note_id FROM note_tags WHERE tag_id = ?', [tagId]);
|
||||
let output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
output.push(rows[i].note_id);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static async notes(tagId) {
|
||||
let noteIds = await this.tagNoteIds(tagId);
|
||||
if (!noteIds.length) return [];
|
||||
|
||||
return Note.search({
|
||||
conditions: ['id IN ("' + noteIds.join('","') + '")'],
|
||||
});
|
||||
}
|
||||
|
||||
static async addNote(tagId, noteId) {
|
||||
let hasIt = await this.hasNote(tagId, noteId);
|
||||
if (hasIt) return;
|
||||
|
||||
return NoteTag.save({
|
||||
tag_id: tagId,
|
||||
note_id: noteId,
|
||||
});
|
||||
}
|
||||
|
||||
static async removeNote(tagId, noteId) {
|
||||
let noteTags = await NoteTag.modelSelectAll('SELECT id FROM note_tags WHERE tag_id = ? and note_id = ?', [tagId, noteId]);
|
||||
for (let i = 0; i < noteTags.length; i++) {
|
||||
await NoteTag.delete(noteTags[i].id);
|
||||
}
|
||||
}
|
||||
|
||||
static async hasNote(tagId, noteId) {
|
||||
let r = await this.db().selectOne('SELECT note_id FROM note_tags WHERE tag_id = ? AND note_id = ? LIMIT 1', [tagId, noteId]);
|
||||
return !!r;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Tag };
|
||||
Reference in New Issue
Block a user