You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-06-15 23:00:36 +02:00
Fixes for release apk
This commit is contained in:
@ -37,6 +37,7 @@ class ItemListComponent extends Component {
|
||||
await Note.save({ id: note.id, todo_completed: checked });
|
||||
}
|
||||
|
||||
listView_itemLongPress(itemId) {}
|
||||
listView_itemPress(itemId) {}
|
||||
|
||||
render() {
|
||||
@ -50,8 +51,8 @@ class ItemListComponent extends Component {
|
||||
|
||||
return (
|
||||
<TouchableHighlight onPress={onPress} onLongPress={onLongPress}>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
{ !!Number(item.is_todo) && <Checkbox checked={!!Number(item.todo_completed)} onChange={(checked) => { this.todoCheckbox_change(item.id, checked) }}/> }<Text>{item.title} [{item.id}]</Text>
|
||||
<View style={{flexDirection: 'row', paddingLeft: 10, paddingTop:5, paddingBottom:5 }}>
|
||||
{ !!Number(item.is_todo) && <Checkbox checked={!!Number(item.todo_completed)} onChange={(checked) => { this.todoCheckbox_change(item.id, checked) }}/> }<Text>{item.title}</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
|
@ -41,22 +41,6 @@ class ScreenHeaderComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async menu_synchronize() {
|
||||
if (reg.oneDriveApi().auth()) {
|
||||
const sync = await reg.synchronizer();
|
||||
try {
|
||||
sync.start();
|
||||
} catch (error) {
|
||||
Log.error(error);
|
||||
}
|
||||
} else {
|
||||
this.props.dispatch({
|
||||
type: 'Navigation/NAVIGATE',
|
||||
routeName: 'OneDriveLogin',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let key = 0;
|
||||
let menuOptionComponents = [];
|
||||
@ -72,15 +56,10 @@ class ScreenHeaderComponent extends Component {
|
||||
menuOptionComponents.push(<View key={'menuOption_' + key++} style={styles.divider}/>);
|
||||
}
|
||||
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.menu_synchronize()} key={'menuOption_' + key++}>
|
||||
<Text>{_('Synchronize')}</Text>
|
||||
</MenuOption>);
|
||||
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={1} key={'menuOption_' + key++}>
|
||||
<Text>{_('Configuration')}</Text>
|
||||
</MenuOption>);
|
||||
// menuOptionComponents.push(
|
||||
// <MenuOption value={1} key={'menuOption_' + key++}>
|
||||
// <Text>{_('Configuration')}</Text>
|
||||
// </MenuOption>);
|
||||
|
||||
let title = 'title' in this.props && this.props.title !== null ? this.props.title : _(this.props.navState.routeName);
|
||||
|
||||
|
@ -85,7 +85,7 @@ class NoteScreenComponent extends React.Component {
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
{ isTodo && <Checkbox checked={!!Number(note.todo_completed)} /> }<TextInput style={{flex:1}} value={note.title} onChangeText={(text) => this.title_changeText(text)} />
|
||||
</View>
|
||||
<TextInput style={{flex: 1, textAlignVertical: 'top'}} multiline={true} value={note.body} onChangeText={(text) => this.body_changeText(text)} />
|
||||
<TextInput style={{flex: 1, textAlignVertical: 'top', fontFamily: 'monospace'}} multiline={true} value={note.body} onChangeText={(text) => this.body_changeText(text)} />
|
||||
{ todoComponents }
|
||||
<Button title="Save note" onPress={() => this.saveNoteButton_press()} />
|
||||
</View>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { WebView, Button } from 'react-native';
|
||||
import { WebView, Button, Text } from 'react-native';
|
||||
import { connect } from 'react-redux'
|
||||
import { Log } from 'lib/log.js'
|
||||
import { Setting } from 'lib/models/setting.js'
|
||||
@ -22,10 +22,14 @@ class OneDriveLoginScreenComponent extends React.Component {
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
webviewUrl: reg.oneDriveApi().authCodeUrl(this.redirectUrl()),
|
||||
webviewUrl: this.startUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
startUrl() {
|
||||
return reg.oneDriveApi().authCodeUrl(this.redirectUrl());
|
||||
}
|
||||
|
||||
redirectUrl() {
|
||||
return 'https://login.microsoftonline.com/common/oauth2/nativeclient';
|
||||
}
|
||||
@ -37,7 +41,7 @@ class OneDriveLoginScreenComponent extends React.Component {
|
||||
const url = noIdeaWhatThisIs.url;
|
||||
|
||||
if (!this.authCode_ && url.indexOf(this.redirectUrl() + '?code=') === 0) {
|
||||
console.info('URL: ' + url);
|
||||
Log.info('URL: ' + url);
|
||||
|
||||
let code = url.split('?code=');
|
||||
this.authCode_ = code[1];
|
||||
@ -48,6 +52,28 @@ class OneDriveLoginScreenComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
async webview_error(error) {
|
||||
Log.error(error);
|
||||
}
|
||||
|
||||
retryButton_click() {
|
||||
// It seems the only way it would reload the page is by loading an unrelated
|
||||
// URL, waiting a bit, and then loading the actual URL. There's probably
|
||||
// a better way to do this.
|
||||
|
||||
this.setState({
|
||||
webviewUrl: 'https://microsoft.com',
|
||||
});
|
||||
this.forceUpdate();
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
webviewUrl: this.startUrl(),
|
||||
});
|
||||
this.forceUpdate();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
render() {
|
||||
const source = {
|
||||
uri: this.state.webviewUrl,
|
||||
@ -60,7 +86,10 @@ class OneDriveLoginScreenComponent extends React.Component {
|
||||
source={source}
|
||||
style={{marginTop: 20}}
|
||||
onNavigationStateChange={(o) => { this.webview_load(o); }}
|
||||
onError={(error) => { this.webview_error(error); }}
|
||||
/>
|
||||
|
||||
<Button title="Retry" onPress={() => { this.retryButton_click(); }}></Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { Button } from 'react-native';
|
||||
import { Button, Text } from 'react-native';
|
||||
import { Log } from 'lib/log.js';
|
||||
import { Note } from 'lib/models/note.js';
|
||||
import { NotesScreenUtils } from 'lib/components/screens/notes-utils.js'
|
||||
import { reg } from 'lib/registry.js';
|
||||
import { _ } from 'lib/locale.js';
|
||||
|
||||
const React = require('react');
|
||||
const {
|
||||
@ -11,7 +13,6 @@ const {
|
||||
ScrollView,
|
||||
View,
|
||||
Image,
|
||||
Text,
|
||||
} = require('react-native');
|
||||
const { Component } = React;
|
||||
|
||||
@ -41,6 +42,11 @@ const styles = StyleSheet.create({
|
||||
|
||||
class SideMenuContentComponent extends Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { syncReportText: '' };
|
||||
}
|
||||
|
||||
folder_press(folder) {
|
||||
this.props.dispatch({
|
||||
type: 'SIDE_MENU_CLOSE',
|
||||
@ -49,19 +55,57 @@ class SideMenuContentComponent extends Component {
|
||||
NotesScreenUtils.openNoteList(folder.id);
|
||||
}
|
||||
|
||||
async synchronize_press() {
|
||||
if (reg.oneDriveApi().auth()) {
|
||||
let options = {
|
||||
onProgress: (report) => {
|
||||
let line = [];
|
||||
line.push(_('Items to upload: %d/%d.', report.createRemote + report.updateRemote, report.remotesToUpdate));
|
||||
line.push(_('Remote items to delete: %d/%d.', report.deleteRemote, report.remotesToDelete));
|
||||
line.push(_('Items to download: %d/%d.', report.createLocal + report.updateLocal, report.localsToUdpate));
|
||||
line.push(_('Local items to delete: %d/%d.', report.deleteLocal, report.localsToDelete));
|
||||
this.setState({ syncReportText: line.join("\n") });
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const sync = await reg.synchronizer()
|
||||
sync.start(options);
|
||||
} catch (error) {
|
||||
Log.error(error);
|
||||
}
|
||||
} else {
|
||||
this.props.dispatch({
|
||||
type: 'Navigation/NAVIGATE',
|
||||
routeName: 'OneDriveLogin',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let buttons = [];
|
||||
let keyIndex = 0;
|
||||
let key = () => {
|
||||
return 'smitem_' + (keyIndex++);
|
||||
}
|
||||
|
||||
let items = [];
|
||||
for (let i = 0; i < this.props.folders.length; i++) {
|
||||
let f = this.props.folders[i];
|
||||
let title = f.title ? f.title : '';
|
||||
buttons.push(
|
||||
<Button style={styles.button} title={title} onPress={() => { this.folder_press(f) }} key={f.id} />
|
||||
items.push(
|
||||
<Button style={styles.button} title={title} onPress={() => { this.folder_press(f) }} key={key()} />
|
||||
);
|
||||
}
|
||||
|
||||
items.push(<Text key={key()}></Text>); // DIVIDER
|
||||
|
||||
items.push(<Button style={styles.button} title="Synchronize" onPress={() => { this.synchronize_press() }} key={key()} />);
|
||||
|
||||
items.push(<Text key={key()}>{this.state.syncReportText}</Text>);
|
||||
|
||||
return (
|
||||
<ScrollView scrollsToTop={false} style={styles.menu}>
|
||||
{ buttons }
|
||||
{ items }
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
@ -2,120 +2,16 @@ import { uuid } from 'lib/uuid.js';
|
||||
import { promiseChain } from 'lib/promise-utils.js';
|
||||
import { Logger } from 'lib/logger.js'
|
||||
import { time } from 'lib/time-utils.js'
|
||||
import { _ } from 'lib/locale.js'
|
||||
import { sprintf } from 'sprintf-js';
|
||||
|
||||
const structureSql = `
|
||||
CREATE TABLE folders (
|
||||
id TEXT PRIMARY KEY,
|
||||
parent_id TEXT NOT NULL DEFAULT "",
|
||||
title TEXT NOT NULL DEFAULT "",
|
||||
created_time INT NOT NULL,
|
||||
updated_time INT NOT NULL,
|
||||
sync_time INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX folders_title ON folders (title);
|
||||
CREATE INDEX folders_updated_time ON folders (updated_time);
|
||||
CREATE INDEX folders_sync_time ON folders (sync_time);
|
||||
|
||||
CREATE TABLE notes (
|
||||
id TEXT PRIMARY KEY,
|
||||
parent_id TEXT NOT NULL DEFAULT "",
|
||||
title TEXT NOT NULL DEFAULT "",
|
||||
body TEXT NOT NULL DEFAULT "",
|
||||
created_time INT NOT NULL,
|
||||
updated_time INT NOT NULL,
|
||||
sync_time INT NOT NULL DEFAULT 0,
|
||||
is_conflict INT NOT NULL DEFAULT 0,
|
||||
latitude NUMERIC NOT NULL DEFAULT 0,
|
||||
longitude NUMERIC NOT NULL DEFAULT 0,
|
||||
altitude NUMERIC NOT NULL DEFAULT 0,
|
||||
author TEXT NOT NULL DEFAULT "",
|
||||
source_url TEXT NOT NULL DEFAULT "",
|
||||
is_todo INT NOT NULL DEFAULT 0,
|
||||
todo_due INT NOT NULL DEFAULT 0,
|
||||
todo_completed INT NOT NULL DEFAULT 0,
|
||||
source TEXT NOT NULL DEFAULT "",
|
||||
source_application TEXT NOT NULL DEFAULT "",
|
||||
application_data TEXT NOT NULL DEFAULT "",
|
||||
\`order\` INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX notes_title ON notes (title);
|
||||
CREATE INDEX notes_updated_time ON notes (updated_time);
|
||||
CREATE INDEX notes_sync_time ON notes (sync_time);
|
||||
CREATE INDEX notes_is_conflict ON notes (is_conflict);
|
||||
CREATE INDEX notes_is_todo ON notes (is_todo);
|
||||
CREATE INDEX notes_order ON notes (\`order\`);
|
||||
|
||||
CREATE TABLE deleted_items (
|
||||
id INTEGER PRIMARY KEY,
|
||||
item_type INT NOT NULL,
|
||||
item_id TEXT NOT NULL,
|
||||
deleted_time INT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE tags (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL DEFAULT "",
|
||||
created_time INT NOT NULL,
|
||||
updated_time INT NOT NULL,
|
||||
sync_time INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE note_tags (
|
||||
id TEXT PRIMARY KEY,
|
||||
note_id TEXT NOT NULL,
|
||||
tag_id TEXT NOT NULL,
|
||||
created_time INT NOT NULL,
|
||||
updated_time INT NOT NULL,
|
||||
sync_time INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE resources (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL DEFAULT "",
|
||||
mime TEXT NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
created_time INT NOT NULL,
|
||||
updated_time INT NOT NULL,
|
||||
sync_time INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE settings (
|
||||
\`key\` TEXT PRIMARY KEY,
|
||||
\`value\` TEXT,
|
||||
\`type\` INT
|
||||
);
|
||||
|
||||
CREATE TABLE table_fields (
|
||||
id INTEGER PRIMARY KEY,
|
||||
table_name TEXT,
|
||||
field_name TEXT,
|
||||
field_type INT,
|
||||
field_default TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE version (
|
||||
version INT
|
||||
);
|
||||
|
||||
INSERT INTO version (version) VALUES (1);
|
||||
`;
|
||||
|
||||
class Database {
|
||||
|
||||
constructor(driver) {
|
||||
this.debugMode_ = false;
|
||||
this.initialized_ = false;
|
||||
this.tableFields_ = null;
|
||||
this.driver_ = driver;
|
||||
this.inTransaction_ = false;
|
||||
|
||||
this.logger_ = new Logger();
|
||||
this.logger_.addTarget('console');
|
||||
this.logger_.setLevel(Logger.LEVEL_DEBUG);
|
||||
}
|
||||
|
||||
// Converts the SQLite error to a regular JS error
|
||||
@ -133,10 +29,6 @@ class Database {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
initialized() {
|
||||
return this.initialized_;
|
||||
}
|
||||
|
||||
driver() {
|
||||
return this.driver_;
|
||||
}
|
||||
@ -144,7 +36,6 @@ class Database {
|
||||
async open(options) {
|
||||
await this.driver().open(options);
|
||||
this.logger().info('Database was open successfully');
|
||||
return this.initialize();
|
||||
}
|
||||
|
||||
escapeField(field) {
|
||||
@ -251,26 +142,12 @@ class Database {
|
||||
if (s == 'string') return 2;
|
||||
}
|
||||
if (type == 'fieldType') {
|
||||
if (s == 'INTEGER') s = 'INT';
|
||||
return this['TYPE_' + s];
|
||||
}
|
||||
throw new Error('Unknown enum type or value: ' + type + ', ' + s);
|
||||
}
|
||||
|
||||
tableFieldNames(tableName) {
|
||||
let tf = this.tableFields(tableName);
|
||||
let output = [];
|
||||
for (let i = 0; i < tf.length; i++) {
|
||||
output.push(tf[i].name);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
tableFields(tableName) {
|
||||
if (!this.tableFields_) throw new Error('Fields have not been loaded yet');
|
||||
if (!this.tableFields_[tableName]) throw new Error('Unknown table: ' + tableName);
|
||||
return this.tableFields_[tableName];
|
||||
}
|
||||
|
||||
static formatValue(type, value) {
|
||||
if (value === null || value === undefined) return null;
|
||||
if (type == this.TYPE_INT) return Number(value);
|
||||
@ -369,98 +246,6 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
refreshTableFields() {
|
||||
this.logger().info('Initializing tables...');
|
||||
let queries = [];
|
||||
queries.push(this.wrapQuery('DELETE FROM table_fields'));
|
||||
|
||||
return this.selectAll('SELECT name FROM sqlite_master WHERE type="table"').then((tableRows) => {
|
||||
let chain = [];
|
||||
for (let i = 0; i < tableRows.length; i++) {
|
||||
let tableName = tableRows[i].name;
|
||||
if (tableName == 'android_metadata') continue;
|
||||
if (tableName == 'table_fields') continue;
|
||||
chain.push(() => {
|
||||
return this.selectAll('PRAGMA table_info("' + tableName + '")').then((pragmas) => {
|
||||
for (let i = 0; i < pragmas.length; i++) {
|
||||
let item = pragmas[i];
|
||||
// In SQLite, if the default value is a string it has double quotes around it, so remove them here
|
||||
let defaultValue = item.dflt_value;
|
||||
if (typeof defaultValue == 'string' && defaultValue.length >= 2 && defaultValue[0] == '"' && defaultValue[defaultValue.length - 1] == '"') {
|
||||
defaultValue = defaultValue.substr(1, defaultValue.length - 2);
|
||||
}
|
||||
let q = Database.insertQuery('table_fields', {
|
||||
table_name: tableName,
|
||||
field_name: item.name,
|
||||
field_type: Database.enumId('fieldType', item.type),
|
||||
field_default: defaultValue,
|
||||
});
|
||||
queries.push(q);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return promiseChain(chain);
|
||||
}).then(() => {
|
||||
return this.transactionExecBatch(queries);
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.logger().info('Checking for database schema update...');
|
||||
|
||||
for (let initLoopCount = 1; initLoopCount <= 2; initLoopCount++) {
|
||||
try {
|
||||
let row = await this.selectOne('SELECT * FROM version LIMIT 1');
|
||||
this.logger().info('Current database version', row);
|
||||
|
||||
// TODO: version update logic
|
||||
// TODO: only do this if db has been updated:
|
||||
// return this.refreshTableFields();
|
||||
} catch (error) {
|
||||
if (error && error.code != 0 && error.code != 'SQLITE_ERROR') throw this.sqliteErrorToJsError(error);
|
||||
|
||||
// Assume that error was:
|
||||
// { message: 'no such table: version (code 1): , while compiling: SELECT * FROM version', code: 0 }
|
||||
// which means the database is empty and the tables need to be created.
|
||||
// If it's any other error there's nothing we can do anyway.
|
||||
|
||||
this.logger().info('Database is new - creating the schema...');
|
||||
|
||||
let queries = this.wrapQueries(this.sqlStringToLines(structureSql));
|
||||
queries.push(this.wrapQuery('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumId('settings', 'string') + '")'));
|
||||
|
||||
try {
|
||||
await this.transactionExecBatch(queries);
|
||||
this.logger().info('Database schema created successfully');
|
||||
await this.refreshTableFields();
|
||||
} catch (error) {
|
||||
throw this.sqliteErrorToJsError(error);
|
||||
}
|
||||
|
||||
// Now that the database has been created, go through the normal initialisation process
|
||||
continue;
|
||||
}
|
||||
|
||||
this.tableFields_ = {};
|
||||
|
||||
let rows = await this.selectAll('SELECT * FROM table_fields');
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let row = rows[i];
|
||||
if (!this.tableFields_[row.table_name]) this.tableFields_[row.table_name] = [];
|
||||
this.tableFields_[row.table_name].push({
|
||||
name: row.field_name,
|
||||
type: row.field_type,
|
||||
default: Database.formatValue(row.field_type, row.field_default),
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Database.TYPE_INT = 1;
|
||||
|
248
ReactNativeClient/lib/joplin-database.js
Normal file
248
ReactNativeClient/lib/joplin-database.js
Normal file
@ -0,0 +1,248 @@
|
||||
import { uuid } from 'lib/uuid.js';
|
||||
import { promiseChain } from 'lib/promise-utils.js';
|
||||
import { time } from 'lib/time-utils.js'
|
||||
import { Database } from 'lib/database.js'
|
||||
|
||||
const structureSql = `
|
||||
CREATE TABLE folders (
|
||||
id TEXT PRIMARY KEY,
|
||||
parent_id TEXT NOT NULL DEFAULT "",
|
||||
title TEXT NOT NULL DEFAULT "",
|
||||
created_time INT NOT NULL,
|
||||
updated_time INT NOT NULL,
|
||||
sync_time INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX folders_title ON folders (title);
|
||||
CREATE INDEX folders_updated_time ON folders (updated_time);
|
||||
CREATE INDEX folders_sync_time ON folders (sync_time);
|
||||
|
||||
CREATE TABLE notes (
|
||||
id TEXT PRIMARY KEY,
|
||||
parent_id TEXT NOT NULL DEFAULT "",
|
||||
title TEXT NOT NULL DEFAULT "",
|
||||
body TEXT NOT NULL DEFAULT "",
|
||||
created_time INT NOT NULL,
|
||||
updated_time INT NOT NULL,
|
||||
sync_time INT NOT NULL DEFAULT 0,
|
||||
is_conflict INT NOT NULL DEFAULT 0,
|
||||
latitude NUMERIC NOT NULL DEFAULT 0,
|
||||
longitude NUMERIC NOT NULL DEFAULT 0,
|
||||
altitude NUMERIC NOT NULL DEFAULT 0,
|
||||
author TEXT NOT NULL DEFAULT "",
|
||||
source_url TEXT NOT NULL DEFAULT "",
|
||||
is_todo INT NOT NULL DEFAULT 0,
|
||||
todo_due INT NOT NULL DEFAULT 0,
|
||||
todo_completed INT NOT NULL DEFAULT 0,
|
||||
source TEXT NOT NULL DEFAULT "",
|
||||
source_application TEXT NOT NULL DEFAULT "",
|
||||
application_data TEXT NOT NULL DEFAULT "",
|
||||
\`order\` INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX notes_title ON notes (title);
|
||||
CREATE INDEX notes_updated_time ON notes (updated_time);
|
||||
CREATE INDEX notes_sync_time ON notes (sync_time);
|
||||
CREATE INDEX notes_is_conflict ON notes (is_conflict);
|
||||
CREATE INDEX notes_is_todo ON notes (is_todo);
|
||||
CREATE INDEX notes_order ON notes (\`order\`);
|
||||
|
||||
CREATE TABLE deleted_items (
|
||||
id INTEGER PRIMARY KEY,
|
||||
item_type INT NOT NULL,
|
||||
item_id TEXT NOT NULL,
|
||||
deleted_time INT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE tags (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL DEFAULT "",
|
||||
created_time INT NOT NULL,
|
||||
updated_time INT NOT NULL,
|
||||
sync_time INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX tags_title ON tags (title);
|
||||
CREATE INDEX tags_updated_time ON tags (updated_time);
|
||||
CREATE INDEX tags_sync_time ON tags (sync_time);
|
||||
|
||||
CREATE TABLE note_tags (
|
||||
id TEXT PRIMARY KEY,
|
||||
note_id TEXT NOT NULL,
|
||||
tag_id TEXT NOT NULL,
|
||||
created_time INT NOT NULL,
|
||||
updated_time INT NOT NULL,
|
||||
sync_time INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX note_tags_note_id ON note_tags (note_id);
|
||||
CREATE INDEX note_tags_tag_id ON note_tags (tag_id);
|
||||
CREATE INDEX note_tags_updated_time ON note_tags (updated_time);
|
||||
CREATE INDEX note_tags_sync_time ON note_tags (sync_time);
|
||||
|
||||
CREATE TABLE resources (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL DEFAULT "",
|
||||
mime TEXT NOT NULL,
|
||||
filename TEXT NOT NULL DEFAULT "",
|
||||
created_time INT NOT NULL,
|
||||
updated_time INT NOT NULL,
|
||||
sync_time INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX resources_title ON resources (title);
|
||||
CREATE INDEX resources_updated_time ON resources (updated_time);
|
||||
CREATE INDEX resources_sync_time ON resources (sync_time);
|
||||
|
||||
CREATE TABLE settings (
|
||||
\`key\` TEXT PRIMARY KEY,
|
||||
\`value\` TEXT,
|
||||
\`type\` INT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE table_fields (
|
||||
id INTEGER PRIMARY KEY,
|
||||
table_name TEXT NOT NULL,
|
||||
field_name TEXT NOT NULL,
|
||||
field_type INT NOT NULL,
|
||||
field_default TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE version (
|
||||
version INT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO version (version) VALUES (1);
|
||||
`;
|
||||
|
||||
class JoplinDatabase extends Database {
|
||||
|
||||
constructor(driver) {
|
||||
super(driver);
|
||||
this.initialized_ = false;
|
||||
this.tableFields_ = null;
|
||||
}
|
||||
|
||||
initialized() {
|
||||
return this.initialized_;
|
||||
}
|
||||
|
||||
async open(options) {
|
||||
await super.open(options);
|
||||
return this.initialize();
|
||||
}
|
||||
|
||||
tableFieldNames(tableName) {
|
||||
let tf = this.tableFields(tableName);
|
||||
let output = [];
|
||||
for (let i = 0; i < tf.length; i++) {
|
||||
output.push(tf[i].name);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
tableFields(tableName) {
|
||||
if (!this.tableFields_) throw new Error('Fields have not been loaded yet');
|
||||
if (!this.tableFields_[tableName]) throw new Error('Unknown table: ' + tableName);
|
||||
return this.tableFields_[tableName];
|
||||
}
|
||||
|
||||
refreshTableFields() {
|
||||
this.logger().info('Initializing tables...');
|
||||
let queries = [];
|
||||
queries.push(this.wrapQuery('DELETE FROM table_fields'));
|
||||
|
||||
return this.selectAll('SELECT name FROM sqlite_master WHERE type="table"').then((tableRows) => {
|
||||
let chain = [];
|
||||
for (let i = 0; i < tableRows.length; i++) {
|
||||
let tableName = tableRows[i].name;
|
||||
if (tableName == 'android_metadata') continue;
|
||||
if (tableName == 'table_fields') continue;
|
||||
chain.push(() => {
|
||||
return this.selectAll('PRAGMA table_info("' + tableName + '")').then((pragmas) => {
|
||||
for (let i = 0; i < pragmas.length; i++) {
|
||||
let item = pragmas[i];
|
||||
// In SQLite, if the default value is a string it has double quotes around it, so remove them here
|
||||
let defaultValue = item.dflt_value;
|
||||
if (typeof defaultValue == 'string' && defaultValue.length >= 2 && defaultValue[0] == '"' && defaultValue[defaultValue.length - 1] == '"') {
|
||||
defaultValue = defaultValue.substr(1, defaultValue.length - 2);
|
||||
}
|
||||
let q = Database.insertQuery('table_fields', {
|
||||
table_name: tableName,
|
||||
field_name: item.name,
|
||||
field_type: Database.enumId('fieldType', item.type),
|
||||
field_default: defaultValue,
|
||||
});
|
||||
queries.push(q);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return promiseChain(chain);
|
||||
}).then(() => {
|
||||
return this.transactionExecBatch(queries);
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.logger().info('Checking for database schema update...');
|
||||
|
||||
for (let initLoopCount = 1; initLoopCount <= 2; initLoopCount++) {
|
||||
try {
|
||||
let row = await this.selectOne('SELECT * FROM version LIMIT 1');
|
||||
this.logger().info('Current database version', row);
|
||||
|
||||
// TODO: version update logic
|
||||
// TODO: only do this if db has been updated:
|
||||
// return this.refreshTableFields();
|
||||
} catch (error) {
|
||||
if (error && error.code != 0 && error.code != 'SQLITE_ERROR') throw this.sqliteErrorToJsError(error);
|
||||
|
||||
// Assume that error was:
|
||||
// { message: 'no such table: version (code 1): , while compiling: SELECT * FROM version', code: 0 }
|
||||
// which means the database is empty and the tables need to be created.
|
||||
// If it's any other error there's nothing we can do anyway.
|
||||
|
||||
this.logger().info('Database is new - creating the schema...');
|
||||
|
||||
let queries = this.wrapQueries(this.sqlStringToLines(structureSql));
|
||||
queries.push(this.wrapQuery('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumId('settings', 'string') + '")'));
|
||||
|
||||
try {
|
||||
await this.transactionExecBatch(queries);
|
||||
this.logger().info('Database schema created successfully');
|
||||
await this.refreshTableFields();
|
||||
} catch (error) {
|
||||
throw this.sqliteErrorToJsError(error);
|
||||
}
|
||||
|
||||
// Now that the database has been created, go through the normal initialisation process
|
||||
continue;
|
||||
}
|
||||
|
||||
this.tableFields_ = {};
|
||||
|
||||
let rows = await this.selectAll('SELECT * FROM table_fields');
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let row = rows[i];
|
||||
if (!this.tableFields_[row.table_name]) this.tableFields_[row.table_name] = [];
|
||||
this.tableFields_[row.table_name].push({
|
||||
name: row.field_name,
|
||||
type: row.field_type,
|
||||
default: Database.formatValue(row.field_type, row.field_default),
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Database.TYPE_INT = 1;
|
||||
Database.TYPE_TEXT = 2;
|
||||
Database.TYPE_NUMERIC = 3;
|
||||
|
||||
export { JoplinDatabase };
|
@ -1,5 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import { _ } from 'lib/locale.js';
|
||||
import { time } from 'lib/time-utils.js';
|
||||
import { FsDriverDummy } from 'lib/fs-driver-dummy.js';
|
||||
|
||||
class Logger {
|
||||
@ -37,6 +38,36 @@ class Logger {
|
||||
this.targets_.push(target);
|
||||
}
|
||||
|
||||
objectToString(object) {
|
||||
let output = '';
|
||||
|
||||
if (typeof object === 'object') {
|
||||
if (object instanceof Error) {
|
||||
output = object.toString();
|
||||
if (object.stack) output += "\n" + object.stack;
|
||||
} else {
|
||||
output = JSON.stringify(object);
|
||||
}
|
||||
} else {
|
||||
output = object;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static databaseCreateTableSql() {
|
||||
let output = `
|
||||
CREATE TABLE logs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
source TEXT,
|
||||
level INT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
\`timestamp\` INT NOT NULL
|
||||
);
|
||||
`;
|
||||
return output.split("\n").join(' ');
|
||||
}
|
||||
|
||||
log(level, object) {
|
||||
if (this.level() < level || !this.targets_.length) return;
|
||||
|
||||
@ -47,8 +78,8 @@ class Logger {
|
||||
let line = moment().format('YYYY-MM-DD HH:mm:ss') + ': ' + levelString;
|
||||
|
||||
for (let i = 0; i < this.targets_.length; i++) {
|
||||
let t = this.targets_[i];
|
||||
if (t.type == 'console') {
|
||||
let target = this.targets_[i];
|
||||
if (target.type == 'console') {
|
||||
let fn = 'debug';
|
||||
if (level = Logger.LEVEL_ERROR) fn = 'error';
|
||||
if (level = Logger.LEVEL_WARN) fn = 'warn';
|
||||
@ -58,49 +89,18 @@ class Logger {
|
||||
} else {
|
||||
console[fn](line + object);
|
||||
}
|
||||
} else if (t.type == 'file') {
|
||||
let serializedObject = '';
|
||||
|
||||
if (typeof object === 'object') {
|
||||
if (object instanceof Error) {
|
||||
serializedObject = object.toString();
|
||||
if (object.stack) serializedObject += "\n" + object.stack;
|
||||
} else {
|
||||
serializedObject = JSON.stringify(object);
|
||||
}
|
||||
} else {
|
||||
serializedObject = object;
|
||||
}
|
||||
|
||||
Logger.fsDriver().appendFileSync(t.path, line + serializedObject + "\n");
|
||||
|
||||
// this.fileAppendQueue_.push({
|
||||
// path: t.path,
|
||||
// line: line + serializedObject + "\n",
|
||||
// });
|
||||
|
||||
// this.scheduleFileAppendQueueProcessing_();
|
||||
} else if (t.type == 'vorpal') {
|
||||
t.vorpal.log(object);
|
||||
} else if (target.type == 'file') {
|
||||
let serializedObject = this.objectToString(object);
|
||||
Logger.fsDriver().appendFileSync(target.path, line + serializedObject + "\n");
|
||||
} else if (target.type == 'vorpal') {
|
||||
target.vorpal.log(object);
|
||||
} else if (target.type == 'database') {
|
||||
let msg = this.objectToString(object);
|
||||
target.database.exec('INSERT INTO logs (`source`, `level`, `message`, `timestamp`) VALUES (?, ?, ?, ?)', [target.source, level, msg, time.unixMs()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scheduleFileAppendQueueProcessing_() {
|
||||
// if (this.fileAppendQueueTID_) return;
|
||||
|
||||
// this.fileAppendQueueTID_ = setTimeout(async () => {
|
||||
// this.fileAppendQueueTID_ = null;
|
||||
|
||||
// let queue = this.fileAppendQueue_.slice(0);
|
||||
// for (let i = 0; i < queue.length; i++) {
|
||||
// let t = queue[i];
|
||||
// await fs.appendFile(t.path, t.line);
|
||||
// }
|
||||
// this.fileAppendQueue_.splice(0, queue.length);
|
||||
// }, 1);
|
||||
// }
|
||||
|
||||
error(object) { return this.log(Logger.LEVEL_ERROR, object); }
|
||||
warn(object) { return this.log(Logger.LEVEL_WARN, object); }
|
||||
info(object) { return this.log(Logger.LEVEL_INFO, object); }
|
||||
|
@ -1,4 +1,3 @@
|
||||
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';
|
||||
|
@ -1,5 +1,4 @@
|
||||
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';
|
||||
|
Reference in New Issue
Block a user