1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-24 08:12:24 +02:00

Fixes for release apk

This commit is contained in:
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,
data: decodedData,
mime: noteResource.mime,
title: noteResource.filename,
filename: noteResource.filename,
title: noteResource.filename ? noteResource.filename : '',
filename: noteResource.filename ? noteResource.filename : '',
};
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 { FileApiDriverLocal } from 'lib/file-api-driver-local.js';
import { OneDriveApiNodeUtils } from './onedrive-api-node-utils.js';
import { JoplinDatabase } from 'lib/joplin-database.js';
import { Database } from 'lib/database.js';
import { DatabaseDriverNode } from 'lib/database-driver-node.js';
import { BaseModel } from 'lib/base-model.js';
@ -927,7 +928,12 @@ async function main() {
await fs.mkdirp(resourceDir, 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('database', { database: logDatabase, source: 'main' });
logger.setLevel(logLevel);
dbLogger.addTarget('file', { path: profileDir + '/log-database.txt' });
@ -947,7 +953,7 @@ async function main() {
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
database_ = new Database(new DatabaseDriverNode());
database_ = new JoplinDatabase(new DatabaseDriverNode());
database_.setLogger(dbLogger);
await database_.open({ name: profileDir + '/database.sqlite' });
BaseModel.db_ = database_;

View File

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

View File

@ -83,62 +83,73 @@ def enableSeparateBuildPerCPUArchitecture = false
def enableProguardInReleaseBuilds = false
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
applicationId "com.awesomeproject"
minSdkVersion 16
targetSdkVersion 22
versionCode 1
versionName "1.0"
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
splits {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86"
}
}
buildTypes {
release {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
// 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
}
}
}
defaultConfig {
applicationId "com.awesomeproject"
minSdkVersion 16
targetSdkVersion 22
versionCode 1
versionName "1.0"
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
splits {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86"
}
}
signingConfigs {
release {
if (project.hasProperty('JOPLIN_RELEASE_STORE_FILE')) {
storeFile file(JOPLIN_RELEASE_STORE_FILE)
storePassword JOPLIN_RELEASE_STORE_PASSWORD
keyAlias JOPLIN_RELEASE_KEY_ALIAS
keyPassword JOPLIN_RELEASE_KEY_PASSWORD
}
}
}
buildTypes {
release {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
signingConfig signingConfigs.release
}
}
// 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 {
compile project(':react-native-fs')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1"
compile "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-sqlite-storage')
compile project(':react-native-fetch-blob')
compile project(':react-native-fs')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1"
compile "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-sqlite-storage')
compile project(':react-native-fetch-blob')
}
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
from configurations.compile
into 'libs'
}
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 {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "AwesomeProject";
}
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "AwesomeProject";
}
}

View File

@ -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>
);

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() {
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);

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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;

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 { _ } 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); }

View File

@ -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';

View File

@ -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';

View File

@ -15,7 +15,7 @@ import { Tag } from 'lib/models/tag.js'
import { NoteTag } from 'lib/models/note-tag.js'
import { BaseItem } from 'lib/models/base-item.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 { NotesScreen } from 'lib/components/screens/notes.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 { DatabaseDriverReactNative } from 'lib/database-driver-react-native';
import { reg } from 'lib/registry.js';
import RNFetchBlob from 'react-native-fetch-blob';
let defaultState = {
notes: [],
@ -194,9 +195,6 @@ const AppNavigator = StackNavigator({
OneDriveLogin: { screen: OneDriveLoginScreen },
});
import RNFetchBlob from 'react-native-fetch-blob'
class AppComponent extends React.Component {
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);
BaseModel.dispatch = this.props.dispatch;
@ -249,7 +247,7 @@ class AppComponent extends React.Component {
BaseItem.loadClass('NoteTag', NoteTag);
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.');
//await db.exec('DELETE FROM notes');

View File

@ -4,11 +4,6 @@
{
"path": ".",
"folder_exclude_patterns": [
"var",
"vendor",
"QtClient/build-JoplinQtClient-Visual_C_32_bits-Debug",
"QtClient/data/resources",
"app/data/uploads",
"CliClient/node_modules",
"CliClient/build",
"CliClient/tests-build",
@ -27,29 +22,11 @@
"_vieux",
],
"file_exclude_patterns": [
"*.pro.user",
"*.pro.user.*",
"*.iml",
"*.map",
"CliClient/app/src",
"CliClient/app/lib",
"*.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"
}
]
}