Fixes for release apk

pull/41/head
Laurent Cozic 2017-07-06 19:48:17 +00:00
parent 8751aa1a34
commit 216a6780cb
17 changed files with 465 additions and 386 deletions

View File

@ -350,8 +350,8 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
id: noteResource.id, id: noteResource.id,
data: decodedData, data: decodedData,
mime: noteResource.mime, mime: noteResource.mime,
title: noteResource.filename, title: noteResource.filename ? noteResource.filename : '',
filename: noteResource.filename, filename: noteResource.filename ? noteResource.filename : '',
}; };
note.resources.push(r); note.resources.push(r);

View File

@ -8,6 +8,7 @@ import { FileApiDriverOneDrive } from 'lib/file-api-driver-onedrive.js';
import { FileApiDriverMemory } from 'lib/file-api-driver-memory.js'; import { FileApiDriverMemory } from 'lib/file-api-driver-memory.js';
import { FileApiDriverLocal } from 'lib/file-api-driver-local.js'; import { FileApiDriverLocal } from 'lib/file-api-driver-local.js';
import { OneDriveApiNodeUtils } from './onedrive-api-node-utils.js'; import { OneDriveApiNodeUtils } from './onedrive-api-node-utils.js';
import { JoplinDatabase } from 'lib/joplin-database.js';
import { Database } from 'lib/database.js'; import { Database } from 'lib/database.js';
import { DatabaseDriverNode } from 'lib/database-driver-node.js'; import { DatabaseDriverNode } from 'lib/database-driver-node.js';
import { BaseModel } from 'lib/base-model.js'; import { BaseModel } from 'lib/base-model.js';
@ -927,7 +928,12 @@ async function main() {
await fs.mkdirp(resourceDir, 0o755); await fs.mkdirp(resourceDir, 0o755);
await fs.mkdirp(tempDir, 0o755); await fs.mkdirp(tempDir, 0o755);
// let logDatabase = new Database(new DatabaseDriverNode());
// await logDatabase.open({ name: profileDir + '/database-log.sqlite' });
// await logDatabase.exec(Logger.databaseCreateTableSql());
logger.addTarget('file', { path: profileDir + '/log.txt' }); logger.addTarget('file', { path: profileDir + '/log.txt' });
// logger.addTarget('database', { database: logDatabase, source: 'main' });
logger.setLevel(logLevel); logger.setLevel(logLevel);
dbLogger.addTarget('file', { path: profileDir + '/log-database.txt' }); dbLogger.addTarget('file', { path: profileDir + '/log-database.txt' });
@ -947,7 +953,7 @@ async function main() {
BaseItem.loadClass('Tag', Tag); BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag); BaseItem.loadClass('NoteTag', NoteTag);
database_ = new Database(new DatabaseDriverNode()); database_ = new JoplinDatabase(new DatabaseDriverNode());
database_.setLogger(dbLogger); database_.setLogger(dbLogger);
await database_.open({ name: profileDir + '/database.sqlite' }); await database_.open({ name: profileDir + '/database.sqlite' });
BaseModel.db_ = database_; BaseModel.db_ = database_;

View File

@ -1,5 +1,5 @@
import fs from 'fs-extra'; import fs from 'fs-extra';
import { Database } from 'lib/database.js'; import { JoplinDatabase } from 'lib/joplin-database.js';
import { DatabaseDriverNode } from 'lib/database-driver-node.js'; import { DatabaseDriverNode } from 'lib/database-driver-node.js';
import { BaseModel } from 'lib/base-model.js'; import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js'; import { Folder } from 'lib/models/folder.js';
@ -25,8 +25,11 @@ const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver; Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver; Resource.fsDriver_ = fsDriver;
const logDir = __dirname + '/../tests/logs';
fs.mkdirpSync(logDir, 0o755);
const logger = new Logger(); const logger = new Logger();
logger.addTarget('file', { path: __dirname + '/../tests/logs/log.txt' }); logger.addTarget('file', { path: logDir + '/log.txt' });
logger.setLevel(Logger.LEVEL_DEBUG); logger.setLevel(Logger.LEVEL_DEBUG);
BaseItem.loadClass('Note', Note); BaseItem.loadClass('Note', Note);
@ -87,7 +90,7 @@ function setupDatabase(id = null) {
return fs.unlink(filePath).catch(() => { return fs.unlink(filePath).catch(() => {
// Don't care if the file doesn't exist // Don't care if the file doesn't exist
}).then(() => { }).then(() => {
databases_[id] = new Database(new DatabaseDriverNode()); databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
databases_[id].setLogger(logger); databases_[id].setLogger(logger);
return databases_[id].open({ name: filePath }).then(() => { return databases_[id].open({ name: filePath }).then(() => {
BaseModel.db_ = databases_[id]; BaseModel.db_ = databases_[id];

View File

@ -83,62 +83,73 @@ def enableSeparateBuildPerCPUArchitecture = false
def enableProguardInReleaseBuilds = false def enableProguardInReleaseBuilds = false
android { android {
compileSdkVersion 23 compileSdkVersion 23
buildToolsVersion "23.0.1" buildToolsVersion "23.0.1"
defaultConfig { defaultConfig {
applicationId "com.awesomeproject" applicationId "com.awesomeproject"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 22 targetSdkVersion 22
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
ndk { ndk {
abiFilters "armeabi-v7a", "x86" abiFilters "armeabi-v7a", "x86"
} }
} }
splits { splits {
abi { abi {
reset() reset()
enable enableSeparateBuildPerCPUArchitecture enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86" include "armeabi-v7a", "x86"
} }
} }
buildTypes { signingConfigs {
release { release {
minifyEnabled enableProguardInReleaseBuilds if (project.hasProperty('JOPLIN_RELEASE_STORE_FILE')) {
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" storeFile file(JOPLIN_RELEASE_STORE_FILE)
} storePassword JOPLIN_RELEASE_STORE_PASSWORD
} keyAlias JOPLIN_RELEASE_KEY_ALIAS
// applicationVariants are e.g. debug, release keyPassword JOPLIN_RELEASE_KEY_PASSWORD
applicationVariants.all { variant -> }
variant.outputs.each { output -> }
// For each separate APK per architecture, set a unique version code as described here: }
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits buildTypes {
def versionCodes = ["armeabi-v7a":1, "x86":2] release {
def abi = output.getFilter(OutputFile.ABI) minifyEnabled enableProguardInReleaseBuilds
if (abi != null) { // null for the universal-debug, universal-release variants proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
output.versionCodeOverride = signingConfig signingConfigs.release
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode }
} }
} // applicationVariants are e.g. debug, release
} applicationVariants.all { variant ->
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
def versionCodes = ["armeabi-v7a":1, "x86":2]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
}
}
}
} }
dependencies { dependencies {
compile project(':react-native-fs') compile project(':react-native-fs')
compile fileTree(dir: "libs", include: ["*.jar"]) compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1" compile "com.android.support:appcompat-v7:23.0.1"
compile "com.facebook.react:react-native:+" // From node_modules compile "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-sqlite-storage') compile project(':react-native-sqlite-storage')
compile project(':react-native-fetch-blob') compile project(':react-native-fetch-blob')
} }
// Run this once to be able to run the application with BUCK // Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use // puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) { task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile from configurations.compile
into 'libs' into 'libs'
} }
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"

View File

@ -4,12 +4,12 @@ import com.facebook.react.ReactActivity;
public class MainActivity extends ReactActivity { public class MainActivity extends ReactActivity {
/** /**
* Returns the name of the main component registered from JavaScript. * Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component. * This is used to schedule rendering of the component.
*/ */
@Override @Override
protected String getMainComponentName() { protected String getMainComponentName() {
return "AwesomeProject"; return "AwesomeProject";
} }
} }

View File

@ -37,6 +37,7 @@ class ItemListComponent extends Component {
await Note.save({ id: note.id, todo_completed: checked }); await Note.save({ id: note.id, todo_completed: checked });
} }
listView_itemLongPress(itemId) {}
listView_itemPress(itemId) {} listView_itemPress(itemId) {}
render() { render() {
@ -50,8 +51,8 @@ class ItemListComponent extends Component {
return ( return (
<TouchableHighlight onPress={onPress} onLongPress={onLongPress}> <TouchableHighlight onPress={onPress} onLongPress={onLongPress}>
<View style={{flexDirection: 'row'}}> <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} [{item.id}]</Text> { !!Number(item.is_todo) && <Checkbox checked={!!Number(item.todo_completed)} onChange={(checked) => { this.todoCheckbox_change(item.id, checked) }}/> }<Text>{item.title}</Text>
</View> </View>
</TouchableHighlight> </TouchableHighlight>
); );

View File

@ -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() { render() {
let key = 0; let key = 0;
let menuOptionComponents = []; let menuOptionComponents = [];
@ -72,15 +56,10 @@ class ScreenHeaderComponent extends Component {
menuOptionComponents.push(<View key={'menuOption_' + key++} style={styles.divider}/>); menuOptionComponents.push(<View key={'menuOption_' + key++} style={styles.divider}/>);
} }
menuOptionComponents.push( // menuOptionComponents.push(
<MenuOption value={() => this.menu_synchronize()} key={'menuOption_' + key++}> // <MenuOption value={1} key={'menuOption_' + key++}>
<Text>{_('Synchronize')}</Text> // <Text>{_('Configuration')}</Text>
</MenuOption>); // </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); let title = 'title' in this.props && this.props.title !== null ? this.props.title : _(this.props.navState.routeName);

View File

@ -85,7 +85,7 @@ class NoteScreenComponent extends React.Component {
<View style={{ flexDirection: 'row' }}> <View style={{ flexDirection: 'row' }}>
{ isTodo && <Checkbox checked={!!Number(note.todo_completed)} /> }<TextInput style={{flex:1}} value={note.title} onChangeText={(text) => this.title_changeText(text)} /> { isTodo && <Checkbox checked={!!Number(note.todo_completed)} /> }<TextInput style={{flex:1}} value={note.title} onChangeText={(text) => this.title_changeText(text)} />
</View> </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 } { todoComponents }
<Button title="Save note" onPress={() => this.saveNoteButton_press()} /> <Button title="Save note" onPress={() => this.saveNoteButton_press()} />
</View> </View>

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { WebView, Button } from 'react-native'; import { WebView, Button, Text } from 'react-native';
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Log } from 'lib/log.js' import { Log } from 'lib/log.js'
import { Setting } from 'lib/models/setting.js' import { Setting } from 'lib/models/setting.js'
@ -22,10 +22,14 @@ class OneDriveLoginScreenComponent extends React.Component {
componentWillMount() { componentWillMount() {
this.setState({ this.setState({
webviewUrl: reg.oneDriveApi().authCodeUrl(this.redirectUrl()), webviewUrl: this.startUrl(),
}); });
} }
startUrl() {
return reg.oneDriveApi().authCodeUrl(this.redirectUrl());
}
redirectUrl() { redirectUrl() {
return 'https://login.microsoftonline.com/common/oauth2/nativeclient'; return 'https://login.microsoftonline.com/common/oauth2/nativeclient';
} }
@ -37,7 +41,7 @@ class OneDriveLoginScreenComponent extends React.Component {
const url = noIdeaWhatThisIs.url; const url = noIdeaWhatThisIs.url;
if (!this.authCode_ && url.indexOf(this.redirectUrl() + '?code=') === 0) { if (!this.authCode_ && url.indexOf(this.redirectUrl() + '?code=') === 0) {
console.info('URL: ' + url); Log.info('URL: ' + url);
let code = url.split('?code='); let code = url.split('?code=');
this.authCode_ = code[1]; 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() { render() {
const source = { const source = {
uri: this.state.webviewUrl, uri: this.state.webviewUrl,
@ -60,7 +86,10 @@ class OneDriveLoginScreenComponent extends React.Component {
source={source} source={source}
style={{marginTop: 20}} style={{marginTop: 20}}
onNavigationStateChange={(o) => { this.webview_load(o); }} onNavigationStateChange={(o) => { this.webview_load(o); }}
onError={(error) => { this.webview_error(error); }}
/> />
<Button title="Retry" onPress={() => { this.retryButton_click(); }}></Button>
</View> </View>
); );
} }

View File

@ -1,8 +1,10 @@
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Button } from 'react-native'; import { Button, Text } from 'react-native';
import { Log } from 'lib/log.js'; import { Log } from 'lib/log.js';
import { Note } from 'lib/models/note.js'; import { Note } from 'lib/models/note.js';
import { NotesScreenUtils } from 'lib/components/screens/notes-utils.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 React = require('react');
const { const {
@ -11,7 +13,6 @@ const {
ScrollView, ScrollView,
View, View,
Image, Image,
Text,
} = require('react-native'); } = require('react-native');
const { Component } = React; const { Component } = React;
@ -41,6 +42,11 @@ const styles = StyleSheet.create({
class SideMenuContentComponent extends Component { class SideMenuContentComponent extends Component {
constructor() {
super();
this.state = { syncReportText: '' };
}
folder_press(folder) { folder_press(folder) {
this.props.dispatch({ this.props.dispatch({
type: 'SIDE_MENU_CLOSE', type: 'SIDE_MENU_CLOSE',
@ -49,19 +55,57 @@ class SideMenuContentComponent extends Component {
NotesScreenUtils.openNoteList(folder.id); 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() { render() {
let buttons = []; let keyIndex = 0;
let key = () => {
return 'smitem_' + (keyIndex++);
}
let items = [];
for (let i = 0; i < this.props.folders.length; i++) { for (let i = 0; i < this.props.folders.length; i++) {
let f = this.props.folders[i]; let f = this.props.folders[i];
let title = f.title ? f.title : ''; let title = f.title ? f.title : '';
buttons.push( items.push(
<Button style={styles.button} title={title} onPress={() => { this.folder_press(f) }} key={f.id} /> <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 ( return (
<ScrollView scrollsToTop={false} style={styles.menu}> <ScrollView scrollsToTop={false} style={styles.menu}>
{ buttons } { items }
</ScrollView> </ScrollView>
); );
} }

View File

@ -2,120 +2,16 @@ import { uuid } from 'lib/uuid.js';
import { promiseChain } from 'lib/promise-utils.js'; import { promiseChain } from 'lib/promise-utils.js';
import { Logger } from 'lib/logger.js' import { Logger } from 'lib/logger.js'
import { time } from 'lib/time-utils.js' import { time } from 'lib/time-utils.js'
import { _ } from 'lib/locale.js'
import { sprintf } from 'sprintf-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 { class Database {
constructor(driver) { constructor(driver) {
this.debugMode_ = false; this.debugMode_ = false;
this.initialized_ = false;
this.tableFields_ = null;
this.driver_ = driver; this.driver_ = driver;
this.inTransaction_ = false; this.inTransaction_ = false;
this.logger_ = new Logger(); this.logger_ = new Logger();
this.logger_.addTarget('console');
this.logger_.setLevel(Logger.LEVEL_DEBUG);
} }
// Converts the SQLite error to a regular JS error // Converts the SQLite error to a regular JS error
@ -133,10 +29,6 @@ class Database {
return this.logger_; return this.logger_;
} }
initialized() {
return this.initialized_;
}
driver() { driver() {
return this.driver_; return this.driver_;
} }
@ -144,7 +36,6 @@ class Database {
async open(options) { async open(options) {
await this.driver().open(options); await this.driver().open(options);
this.logger().info('Database was open successfully'); this.logger().info('Database was open successfully');
return this.initialize();
} }
escapeField(field) { escapeField(field) {
@ -251,26 +142,12 @@ class Database {
if (s == 'string') return 2; if (s == 'string') return 2;
} }
if (type == 'fieldType') { if (type == 'fieldType') {
if (s == 'INTEGER') s = 'INT';
return this['TYPE_' + s]; return this['TYPE_' + s];
} }
throw new Error('Unknown enum type or value: ' + 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) { static formatValue(type, value) {
if (value === null || value === undefined) return null; if (value === null || value === undefined) return null;
if (type == this.TYPE_INT) return Number(value); 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; Database.TYPE_INT = 1;

View 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 };

View File

@ -1,5 +1,6 @@
import moment from 'moment'; import moment from 'moment';
import { _ } from 'lib/locale.js'; import { _ } from 'lib/locale.js';
import { time } from 'lib/time-utils.js';
import { FsDriverDummy } from 'lib/fs-driver-dummy.js'; import { FsDriverDummy } from 'lib/fs-driver-dummy.js';
class Logger { class Logger {
@ -37,6 +38,36 @@ class Logger {
this.targets_.push(target); 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) { log(level, object) {
if (this.level() < level || !this.targets_.length) return; 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; let line = moment().format('YYYY-MM-DD HH:mm:ss') + ': ' + levelString;
for (let i = 0; i < this.targets_.length; i++) { for (let i = 0; i < this.targets_.length; i++) {
let t = this.targets_[i]; let target = this.targets_[i];
if (t.type == 'console') { if (target.type == 'console') {
let fn = 'debug'; let fn = 'debug';
if (level = Logger.LEVEL_ERROR) fn = 'error'; if (level = Logger.LEVEL_ERROR) fn = 'error';
if (level = Logger.LEVEL_WARN) fn = 'warn'; if (level = Logger.LEVEL_WARN) fn = 'warn';
@ -58,49 +89,18 @@ class Logger {
} else { } else {
console[fn](line + object); console[fn](line + object);
} }
} else if (t.type == 'file') { } else if (target.type == 'file') {
let serializedObject = ''; let serializedObject = this.objectToString(object);
Logger.fsDriver().appendFileSync(target.path, line + serializedObject + "\n");
if (typeof object === 'object') { } else if (target.type == 'vorpal') {
if (object instanceof Error) { target.vorpal.log(object);
serializedObject = object.toString(); } else if (target.type == 'database') {
if (object.stack) serializedObject += "\n" + object.stack; let msg = this.objectToString(object);
} else { target.database.exec('INSERT INTO logs (`source`, `level`, `message`, `timestamp`) VALUES (?, ?, ?, ?)', [target.source, level, msg, time.unixMs()]);
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);
} }
} }
} }
// 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); } error(object) { return this.log(Logger.LEVEL_ERROR, object); }
warn(object) { return this.log(Logger.LEVEL_WARN, object); } warn(object) { return this.log(Logger.LEVEL_WARN, object); }
info(object) { return this.log(Logger.LEVEL_INFO, object); } info(object) { return this.log(Logger.LEVEL_INFO, object); }

View File

@ -1,4 +1,3 @@
import { Database } from 'lib/database.js';
import { BaseItem } from 'lib/models/base-item.js'; import { BaseItem } from 'lib/models/base-item.js';
import { BaseModel } from 'lib/base-model.js'; import { BaseModel } from 'lib/base-model.js';
import lodash from 'lodash'; import lodash from 'lodash';

View File

@ -1,5 +1,4 @@
import { BaseModel } from 'lib/base-model.js'; import { BaseModel } from 'lib/base-model.js';
import { Database } from 'lib/database.js';
import { BaseItem } from 'lib/models/base-item.js'; import { BaseItem } from 'lib/models/base-item.js';
import { NoteTag } from 'lib/models/note-tag.js'; import { NoteTag } from 'lib/models/note-tag.js';
import { Note } from 'lib/models/note.js'; import { Note } from 'lib/models/note.js';

View File

@ -15,7 +15,7 @@ import { Tag } from 'lib/models/tag.js'
import { NoteTag } from 'lib/models/note-tag.js' import { NoteTag } from 'lib/models/note-tag.js'
import { BaseItem } from 'lib/models/base-item.js' import { BaseItem } from 'lib/models/base-item.js'
import { BaseModel } from 'lib/base-model.js' import { BaseModel } from 'lib/base-model.js'
import { Database } from 'lib/database.js' import { JoplinDatabase } from 'lib/joplin-database.js'
import { ItemList } from 'lib/components/item-list.js' import { ItemList } from 'lib/components/item-list.js'
import { NotesScreen } from 'lib/components/screens/notes.js' import { NotesScreen } from 'lib/components/screens/notes.js'
import { NotesScreenUtils } from 'lib/components/screens/notes-utils.js' import { NotesScreenUtils } from 'lib/components/screens/notes-utils.js'
@ -32,6 +32,7 @@ import { SideMenu } from 'lib/components/side-menu.js';
import { SideMenuContent } from 'lib/components/side-menu-content.js'; import { SideMenuContent } from 'lib/components/side-menu-content.js';
import { DatabaseDriverReactNative } from 'lib/database-driver-react-native'; import { DatabaseDriverReactNative } from 'lib/database-driver-react-native';
import { reg } from 'lib/registry.js'; import { reg } from 'lib/registry.js';
import RNFetchBlob from 'react-native-fetch-blob';
let defaultState = { let defaultState = {
notes: [], notes: [],
@ -194,9 +195,6 @@ const AppNavigator = StackNavigator({
OneDriveLogin: { screen: OneDriveLoginScreen }, OneDriveLogin: { screen: OneDriveLoginScreen },
}); });
import RNFetchBlob from 'react-native-fetch-blob'
class AppComponent extends React.Component { class AppComponent extends React.Component {
async componentDidMount() { async componentDidMount() {
@ -235,7 +233,7 @@ class AppComponent extends React.Component {
} }
} }
let db = new Database(new DatabaseDriverReactNative()); let db = new JoplinDatabase(new DatabaseDriverReactNative());
reg.setDb(db); reg.setDb(db);
BaseModel.dispatch = this.props.dispatch; BaseModel.dispatch = this.props.dispatch;
@ -249,7 +247,7 @@ class AppComponent extends React.Component {
BaseItem.loadClass('NoteTag', NoteTag); BaseItem.loadClass('NoteTag', NoteTag);
try { try {
await db.open({ name: '/storage/emulated/0/Download/joplin-44.sqlite' }) await db.open({ name: '/storage/emulated/0/Download/joplin-48.sqlite' })
Log.info('Database is ready.'); Log.info('Database is ready.');
//await db.exec('DELETE FROM notes'); //await db.exec('DELETE FROM notes');

View File

@ -4,11 +4,6 @@
{ {
"path": ".", "path": ".",
"folder_exclude_patterns": [ "folder_exclude_patterns": [
"var",
"vendor",
"QtClient/build-JoplinQtClient-Visual_C_32_bits-Debug",
"QtClient/data/resources",
"app/data/uploads",
"CliClient/node_modules", "CliClient/node_modules",
"CliClient/build", "CliClient/build",
"CliClient/tests-build", "CliClient/tests-build",
@ -27,29 +22,11 @@
"_vieux", "_vieux",
], ],
"file_exclude_patterns": [ "file_exclude_patterns": [
"*.pro.user",
"*.pro.user.*",
"*.iml",
"*.map", "*.map",
"CliClient/app/src", "CliClient/app/src",
"CliClient/app/lib", "CliClient/app/lib",
"*.jar", "*.jar",
] ]
} }
],
"build_systems":
[
{
"name": "Build evernote-import",
"shell_cmd": "D:\\Programmes\\cygwin\\bin\\bash.exe --login D:\\Web\\www\\joplin\\QtClient\\evernote-import\\build.sh"
},
{
"name": "Qt Creator - Build and run",
"shell_cmd": "D:\\NonPortableApps\\AutoHotkey\\AutoHotkey.exe D:\\Docs\\PROGS\\AutoHotKey\\QtRun\\QtRun.ahk"
},
{
"name": "Build QtClient CLI - Linux",
"shell_cmd": "/home/laurent/src/notes/QtClient/JoplinQtClient/build.sh"
}
] ]
} }