You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-13 22:12:50 +02:00
Merge branch 'dropbox'
This commit is contained in:
1
CliClient/.gitignore
vendored
1
CliClient/.gitignore
vendored
@@ -19,3 +19,4 @@ tests/sync
|
|||||||
out.txt
|
out.txt
|
||||||
linkToLocal.sh
|
linkToLocal.sh
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
tests/support/dropbox-auth.txt
|
@@ -79,9 +79,25 @@ class Command extends BaseCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
} else if (syncTargetMd.name === 'dropbox') { // Dropbox
|
||||||
|
const api = await syncTarget.api();
|
||||||
|
const loginUrl = api.loginUrl();
|
||||||
|
this.stdout(_('To allow Joplin to synchronise with Dropbox, please follow the steps below:'));
|
||||||
|
this.stdout(_('Step 1: Open this URL in your browser to authorise the application:'));
|
||||||
|
this.stdout(loginUrl);
|
||||||
|
const authCode = await this.prompt(_('Step 2: Enter the code provided by Dropbox:'), { type: 'string' });
|
||||||
|
if (!authCode) {
|
||||||
|
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stdout(_('Not authentified with %s. Please provide any missing credentials.', syncTarget.label()));
|
const response = await api.execAuthToken(authCode);
|
||||||
|
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', response.access_token);
|
||||||
|
api.setAuthToken(response.access_token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stdout(_('Not authentified with %s. Please provide any missing credentials.', syncTargetMd.label));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +116,7 @@ class Command extends BaseCommand {
|
|||||||
this.releaseLockFn_ = null;
|
this.releaseLockFn_ = null;
|
||||||
|
|
||||||
// Lock is unique per profile/database
|
// Lock is unique per profile/database
|
||||||
|
// TODO: use SQLite database to do lock?
|
||||||
const lockFilePath = require('os').tmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41
|
const lockFilePath = require('os').tmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41
|
||||||
if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock');
|
if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock');
|
||||||
|
|
||||||
@@ -130,7 +147,7 @@ class Command extends BaseCommand {
|
|||||||
|
|
||||||
const syncTarget = reg.syncTarget(this.syncTargetId_);
|
const syncTarget = reg.syncTarget(this.syncTargetId_);
|
||||||
|
|
||||||
if (!syncTarget.isAuthenticated()) {
|
if (!await syncTarget.isAuthenticated()) {
|
||||||
app().gui().showConsole();
|
app().gui().showConsole();
|
||||||
app().gui().maximizeConsole();
|
app().gui().maximizeConsole();
|
||||||
|
|
||||||
@@ -197,7 +214,7 @@ class Command extends BaseCommand {
|
|||||||
|
|
||||||
const syncTarget = reg.syncTarget(syncTargetId);
|
const syncTarget = reg.syncTarget(syncTargetId);
|
||||||
|
|
||||||
if (syncTarget.isAuthenticated()) {
|
if (await syncTarget.isAuthenticated()) {
|
||||||
const sync = await syncTarget.synchronizer();
|
const sync = await syncTarget.synchronizer();
|
||||||
if (sync) await sync.cancel();
|
if (sync) await sync.cancel();
|
||||||
} else {
|
} else {
|
||||||
|
5
CliClient/package-lock.json
generated
5
CliClient/package-lock.json
generated
@@ -983,11 +983,6 @@
|
|||||||
"wrappy": "1.0.2"
|
"wrappy": "1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"os-tmpdir": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
|
||||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
|
|
||||||
},
|
|
||||||
"parse-data-uri": {
|
"parse-data-uri": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-data-uri/-/parse-data-uri-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-data-uri/-/parse-data-uri-0.2.0.tgz",
|
||||||
|
@@ -339,22 +339,17 @@ describe('Synchronizer', function() {
|
|||||||
it('should delete local folder', asyncTest(async () => {
|
it('should delete local folder', asyncTest(async () => {
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let folder2 = await Folder.save({ title: "folder2" });
|
let folder2 = await Folder.save({ title: "folder2" });
|
||||||
await synchronizer().start();
|
let context1 = await synchronizer().start();
|
||||||
|
|
||||||
await switchClient(2);
|
await switchClient(2);
|
||||||
|
|
||||||
await synchronizer().start();
|
let context2 = await synchronizer().start();
|
||||||
|
|
||||||
await sleep(0.1);
|
|
||||||
|
|
||||||
await Folder.delete(folder2.id);
|
await Folder.delete(folder2.id);
|
||||||
|
await synchronizer().start({ context: context2 });
|
||||||
await synchronizer().start();
|
|
||||||
|
|
||||||
await switchClient(1);
|
await switchClient(1);
|
||||||
|
|
||||||
await synchronizer().start();
|
await synchronizer().start({ context: context1 });
|
||||||
|
|
||||||
let items = await allItems();
|
let items = await allItems();
|
||||||
await localItemsSameAsRemote(items, expect);
|
await localItemsSameAsRemote(items, expect);
|
||||||
}));
|
}));
|
||||||
@@ -547,11 +542,11 @@ describe('Synchronizer', function() {
|
|||||||
let n1 = await Note.save({ title: "mynote" });
|
let n1 = await Note.save({ title: "mynote" });
|
||||||
let n2 = await Note.save({ title: "mynote2" });
|
let n2 = await Note.save({ title: "mynote2" });
|
||||||
let tag = await Tag.save({ title: 'mytag' });
|
let tag = await Tag.save({ title: 'mytag' });
|
||||||
await synchronizer().start();
|
let context1 = await synchronizer().start();
|
||||||
|
|
||||||
await switchClient(2);
|
await switchClient(2);
|
||||||
|
|
||||||
await synchronizer().start();
|
let context2 = await synchronizer().start();
|
||||||
if (withEncryption) {
|
if (withEncryption) {
|
||||||
const masterKey_2 = await MasterKey.load(masterKey.id);
|
const masterKey_2 = await MasterKey.load(masterKey.id);
|
||||||
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
|
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
|
||||||
@@ -565,21 +560,21 @@ describe('Synchronizer', function() {
|
|||||||
await Tag.addNote(remoteTag.id, n2.id);
|
await Tag.addNote(remoteTag.id, n2.id);
|
||||||
let noteIds = await Tag.noteIds(tag.id);
|
let noteIds = await Tag.noteIds(tag.id);
|
||||||
expect(noteIds.length).toBe(2);
|
expect(noteIds.length).toBe(2);
|
||||||
await synchronizer().start();
|
context2 = await synchronizer().start({ context: context2 });
|
||||||
|
|
||||||
await switchClient(1);
|
await switchClient(1);
|
||||||
|
|
||||||
await synchronizer().start();
|
context1 = await synchronizer().start({ context: context1 });
|
||||||
let remoteNoteIds = await Tag.noteIds(tag.id);
|
let remoteNoteIds = await Tag.noteIds(tag.id);
|
||||||
expect(remoteNoteIds.length).toBe(2);
|
expect(remoteNoteIds.length).toBe(2);
|
||||||
await Tag.removeNote(tag.id, n1.id);
|
await Tag.removeNote(tag.id, n1.id);
|
||||||
remoteNoteIds = await Tag.noteIds(tag.id);
|
remoteNoteIds = await Tag.noteIds(tag.id);
|
||||||
expect(remoteNoteIds.length).toBe(1);
|
expect(remoteNoteIds.length).toBe(1);
|
||||||
await synchronizer().start();
|
context1 = await synchronizer().start({ context: context1 });
|
||||||
|
|
||||||
await switchClient(2);
|
await switchClient(2);
|
||||||
|
|
||||||
await synchronizer().start();
|
context2 = await synchronizer().start({ context: context2 });
|
||||||
noteIds = await Tag.noteIds(tag.id);
|
noteIds = await Tag.noteIds(tag.id);
|
||||||
expect(noteIds.length).toBe(1);
|
expect(noteIds.length).toBe(1);
|
||||||
expect(remoteNoteIds[0]).toBe(noteIds[0]);
|
expect(remoteNoteIds[0]).toBe(noteIds[0]);
|
||||||
|
@@ -16,6 +16,7 @@ const { FileApi } = require('lib/file-api.js');
|
|||||||
const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
|
const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
|
||||||
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
||||||
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav.js');
|
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav.js');
|
||||||
|
const { FileApiDriverDropbox } = require('lib/file-api-driver-dropbox.js');
|
||||||
const BaseService = require('lib/services/BaseService.js');
|
const BaseService = require('lib/services/BaseService.js');
|
||||||
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
@@ -25,9 +26,11 @@ const SyncTargetMemory = require('lib/SyncTargetMemory.js');
|
|||||||
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
||||||
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||||
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
||||||
|
const SyncTargetDropbox = require('lib/SyncTargetDropbox.js');
|
||||||
const EncryptionService = require('lib/services/EncryptionService.js');
|
const EncryptionService = require('lib/services/EncryptionService.js');
|
||||||
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
|
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
|
||||||
const WebDavApi = require('lib/WebDavApi');
|
const WebDavApi = require('lib/WebDavApi');
|
||||||
|
const DropboxApi = require('lib/DropboxApi');
|
||||||
|
|
||||||
let databases_ = [];
|
let databases_ = [];
|
||||||
let synchronizers_ = [];
|
let synchronizers_ = [];
|
||||||
@@ -51,10 +54,12 @@ SyncTargetRegistry.addClass(SyncTargetMemory);
|
|||||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||||
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
||||||
|
SyncTargetRegistry.addClass(SyncTargetDropbox);
|
||||||
|
|
||||||
// const syncTargetId_ = SyncTargetRegistry.nameToId("nextcloud");
|
// const syncTargetId_ = SyncTargetRegistry.nameToId("nextcloud");
|
||||||
const syncTargetId_ = SyncTargetRegistry.nameToId("memory");
|
const syncTargetId_ = SyncTargetRegistry.nameToId("memory");
|
||||||
//const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
|
//const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
|
||||||
|
// const syncTargetId_ = SyncTargetRegistry.nameToId('dropbox');
|
||||||
const syncDir = __dirname + '/../tests/sync';
|
const syncDir = __dirname + '/../tests/sync';
|
||||||
|
|
||||||
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 100;//400;
|
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 100;//400;
|
||||||
@@ -247,25 +252,15 @@ function fileApi() {
|
|||||||
|
|
||||||
const api = new WebDavApi(options);
|
const api = new WebDavApi(options);
|
||||||
fileApi_ = new FileApi('', new FileApiDriverWebDav(api));
|
fileApi_ = new FileApi('', new FileApiDriverWebDav(api));
|
||||||
|
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('dropbox')) {
|
||||||
|
const api = new DropboxApi();
|
||||||
|
const authTokenPath = __dirname + '/support/dropbox-auth.txt';
|
||||||
|
const authToken = fs.readFileSync(authTokenPath, 'utf8');
|
||||||
|
if (!authToken) throw new Error('Dropbox auth token missing in ' + authTokenPath);
|
||||||
|
api.setAuthToken(authToken);
|
||||||
|
fileApi_ = new FileApi('', new FileApiDriverDropbox(api));
|
||||||
}
|
}
|
||||||
|
|
||||||
// } else if (syncTargetId == Setting.SYNC_TARGET_ONEDRIVE) {
|
|
||||||
// let auth = require('./onedrive-auth.json');
|
|
||||||
// if (!auth) {
|
|
||||||
// const oneDriveApiUtils = new OneDriveApiNodeUtils(oneDriveApi);
|
|
||||||
// auth = await oneDriveApiUtils.oauthDance();
|
|
||||||
// fs.writeFileSync('./onedrive-auth.json', JSON.stringify(auth));
|
|
||||||
// process.exit(1);
|
|
||||||
// } else {
|
|
||||||
// auth = JSON.parse(auth);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // const oneDriveApiUtils = new OneDriveApiNodeUtils(reg.oneDriveApi());
|
|
||||||
// // const auth = await oneDriveApiUtils.oauthDance(this);
|
|
||||||
// // Setting.setValue('sync.3.auth', auth ? JSON.stringify(auth) : null);
|
|
||||||
// // if (!auth) return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
fileApi_.setLogger(logger);
|
fileApi_.setLogger(logger);
|
||||||
fileApi_.setSyncTargetId(syncTargetId_);
|
fileApi_.setSyncTargetId(syncTargetId_);
|
||||||
fileApi_.requestRepeatCount_ = 0;
|
fileApi_.requestRepeatCount_ = 0;
|
||||||
@@ -306,9 +301,10 @@ function asyncTest(callback) {
|
|||||||
await callback();
|
await callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
} finally {
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest };
|
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest };
|
62
ElectronClient/app/gui/DropboxLoginScreen.jsx
Normal file
62
ElectronClient/app/gui/DropboxLoginScreen.jsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const { connect } = require('react-redux');
|
||||||
|
const { reg } = require('lib/registry.js');
|
||||||
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
|
const { Header } = require('./Header.min.js');
|
||||||
|
const { themeStyle } = require('../theme.js');
|
||||||
|
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||||
|
const { _ } = require('lib/locale.js');
|
||||||
|
const Shared = require('lib/components/shared/dropbox-login-shared');
|
||||||
|
|
||||||
|
class DropboxLoginScreenComponent extends React.Component {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.shared_ = new Shared(
|
||||||
|
this,
|
||||||
|
(msg) => bridge().showInfoMessageBox(msg),
|
||||||
|
(msg) => bridge().showErrorMessageBox(msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.shared_.refreshUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const style = this.props.style;
|
||||||
|
const theme = themeStyle(this.props.theme);
|
||||||
|
|
||||||
|
const headerStyle = {
|
||||||
|
width: style.width,
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputStyle = Object.assign({}, theme.inputStyle, { width: 500 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Header style={headerStyle} />
|
||||||
|
<div style={{padding: theme.margin}}>
|
||||||
|
<p style={theme.textStyle}>{_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')}</p>
|
||||||
|
<p style={theme.textStyle}>{_('Step 1: Open this URL in your browser to authorise the application:')}</p>
|
||||||
|
<a style={theme.textStyle} href="#" onClick={this.shared_.loginUrl_click}>{this.state.loginUrl}</a>
|
||||||
|
<p style={theme.textStyle}>{_('Step 2: Enter the code provided by Dropbox:')}</p>
|
||||||
|
<p><input type="text" value={this.state.authCode} onChange={this.shared_.authCodeInput_change} style={inputStyle}/></p>
|
||||||
|
<button disabled={this.state.checkingAuthToken} onClick={this.shared_.submit_click}>{_('Submit')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => {
|
||||||
|
return {
|
||||||
|
theme: state.settings.theme,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropboxLoginScreen = connect(mapStateToProps)(DropboxLoginScreenComponent);
|
||||||
|
|
||||||
|
module.exports = { DropboxLoginScreen };
|
@@ -370,7 +370,7 @@ class NoteTextComponent extends React.Component {
|
|||||||
webviewReady: true,
|
webviewReady: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Setting.value('env') === 'dev') this.webview_.openDevTools();
|
// if (Setting.value('env') === 'dev') this.webview_.openDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
webview_ref(element) {
|
webview_ref(element) {
|
||||||
|
@@ -8,6 +8,7 @@ const Setting = require('lib/models/Setting.js');
|
|||||||
|
|
||||||
const { MainScreen } = require('./MainScreen.min.js');
|
const { MainScreen } = require('./MainScreen.min.js');
|
||||||
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
|
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
|
||||||
|
const { DropboxLoginScreen } = require('./DropboxLoginScreen.min.js');
|
||||||
const { StatusScreen } = require('./StatusScreen.min.js');
|
const { StatusScreen } = require('./StatusScreen.min.js');
|
||||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||||
const { ConfigScreen } = require('./ConfigScreen.min.js');
|
const { ConfigScreen } = require('./ConfigScreen.min.js');
|
||||||
@@ -75,6 +76,7 @@ class RootComponent extends React.Component {
|
|||||||
const screens = {
|
const screens = {
|
||||||
Main: { screen: MainScreen },
|
Main: { screen: MainScreen },
|
||||||
OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') },
|
OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') },
|
||||||
|
DropboxLogin: { screen: DropboxLoginScreen, title: () => _('Dropbox Login') },
|
||||||
Import: { screen: ImportScreen, title: () => _('Import') },
|
Import: { screen: ImportScreen, title: () => _('Import') },
|
||||||
Config: { screen: ConfigScreen, title: () => _('Options') },
|
Config: { screen: ConfigScreen, title: () => _('Options') },
|
||||||
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
|
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
|
||||||
|
2
ElectronClient/app/package-lock.json
generated
2
ElectronClient/app/package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Joplin",
|
"name": "Joplin",
|
||||||
"version": "1.0.79",
|
"version": "1.0.80",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Joplin",
|
"name": "Joplin",
|
||||||
"version": "1.0.79",
|
"version": "1.0.80",
|
||||||
"description": "Joplin for Desktop",
|
"description": "Joplin for Desktop",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@@ -62,6 +62,7 @@ globalStyle.icon = {
|
|||||||
globalStyle.lineInput = {
|
globalStyle.lineInput = {
|
||||||
color: globalStyle.color,
|
color: globalStyle.color,
|
||||||
backgroundColor: globalStyle.backgroundColor,
|
backgroundColor: globalStyle.backgroundColor,
|
||||||
|
fontFamily: globalStyle.fontFamily,
|
||||||
};
|
};
|
||||||
|
|
||||||
globalStyle.textStyle = {
|
globalStyle.textStyle = {
|
||||||
|
@@ -28,7 +28,7 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/
|
|||||||
|
|
||||||
Operating System | Download | Alt. Download
|
Operating System | Download | Alt. Download
|
||||||
-----------------|----------|----------------
|
-----------------|----------|----------------
|
||||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.112/joplin-v1.0.112.apk)
|
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.114/joplin-v1.0.114.apk)
|
||||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeIOS.png'/></a> | -
|
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeIOS.png'/></a> | -
|
||||||
|
|
||||||
## Terminal application
|
## Terminal application
|
||||||
|
@@ -90,8 +90,8 @@ android {
|
|||||||
applicationId "net.cozic.joplin"
|
applicationId "net.cozic.joplin"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 22
|
targetSdkVersion 22
|
||||||
versionCode 2097290
|
versionCode 2097292
|
||||||
versionName "1.0.112"
|
versionName "1.0.114"
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "x86"
|
abiFilters "armeabi-v7a", "x86"
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,7 @@ const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
|||||||
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
||||||
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
||||||
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
|
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
|
||||||
|
const SyncTargetDropbox = require('lib/SyncTargetDropbox.js');
|
||||||
const EncryptionService = require('lib/services/EncryptionService');
|
const EncryptionService = require('lib/services/EncryptionService');
|
||||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||||
const BaseService = require('lib/services/BaseService');
|
const BaseService = require('lib/services/BaseService');
|
||||||
@@ -38,6 +39,7 @@ SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
|||||||
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
||||||
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
||||||
SyncTargetRegistry.addClass(SyncTargetWebDAV);
|
SyncTargetRegistry.addClass(SyncTargetWebDAV);
|
||||||
|
SyncTargetRegistry.addClass(SyncTargetDropbox);
|
||||||
|
|
||||||
class BaseApplication {
|
class BaseApplication {
|
||||||
|
|
||||||
@@ -421,8 +423,14 @@ class BaseApplication {
|
|||||||
if (Setting.value('firstStart')) {
|
if (Setting.value('firstStart')) {
|
||||||
const locale = shim.detectAndSetLocale(Setting);
|
const locale = shim.detectAndSetLocale(Setting);
|
||||||
reg.logger().info('First start: detected locale as ' + locale);
|
reg.logger().info('First start: detected locale as ' + locale);
|
||||||
if (Setting.value('env') === 'dev') Setting.setValue('sync.target', SyncTargetRegistry.nameToId('onedrive_dev'));
|
|
||||||
Setting.setValue('firstStart', 0)
|
if (Setting.value('env') === 'dev') {
|
||||||
|
Setting.setValue('showTrayIcon', 0);
|
||||||
|
Setting.setValue('autoUpdateEnabled', 0);
|
||||||
|
Setting.setValue('sync.interval', 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.setValue('firstStart', 0);
|
||||||
} else {
|
} else {
|
||||||
setLocale(Setting.value('locale'));
|
setLocale(Setting.value('locale'));
|
||||||
}
|
}
|
||||||
|
@@ -30,7 +30,7 @@ class BaseSyncTarget {
|
|||||||
return this.db_;
|
return this.db_;
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated() {
|
async isAuthenticated() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +66,10 @@ class BaseSyncTarget {
|
|||||||
return this.fileApi_;
|
return this.fileApi_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileApiSync() {
|
||||||
|
return this.fileApi_;
|
||||||
|
}
|
||||||
|
|
||||||
// Usually each sync target should create and setup its own file API via initFileApi()
|
// Usually each sync target should create and setup its own file API via initFileApi()
|
||||||
// but for testing purposes it might be convenient to provide it here so that multiple
|
// but for testing purposes it might be convenient to provide it here so that multiple
|
||||||
// clients can share and sync to the same file api (see test-utils.js)
|
// clients can share and sync to the same file api (see test-utils.js)
|
||||||
@@ -109,7 +113,7 @@ class BaseSyncTarget {
|
|||||||
|
|
||||||
async syncStarted() {
|
async syncStarted() {
|
||||||
if (!this.synchronizer_) return false;
|
if (!this.synchronizer_) return false;
|
||||||
if (!this.isAuthenticated()) return false;
|
if (!await this.isAuthenticated()) return false;
|
||||||
const sync = await this.synchronizer();
|
const sync = await this.synchronizer();
|
||||||
return sync.state() != 'idle';
|
return sync.state() != 'idle';
|
||||||
}
|
}
|
||||||
|
214
ReactNativeClient/lib/DropboxApi.js
Normal file
214
ReactNativeClient/lib/DropboxApi.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
const { Logger } = require('lib/logger.js');
|
||||||
|
const { shim } = require('lib/shim.js');
|
||||||
|
const JoplinError = require('lib/JoplinError');
|
||||||
|
const URL = require('url-parse');
|
||||||
|
const { time } = require('lib/time-utils');
|
||||||
|
const EventDispatcher = require('lib/EventDispatcher');
|
||||||
|
|
||||||
|
class DropboxApi {
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
this.logger_ = new Logger();
|
||||||
|
this.options_ = options;
|
||||||
|
this.authToken_ = null;
|
||||||
|
this.dispatcher_ = new EventDispatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId() {
|
||||||
|
return this.options_.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSecret() {
|
||||||
|
return this.options_.secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLogger(l) {
|
||||||
|
this.logger_ = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger() {
|
||||||
|
return this.logger_;
|
||||||
|
}
|
||||||
|
|
||||||
|
authToken() {
|
||||||
|
return this.authToken_; // Without the "Bearer " prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
on(eventName, callback) {
|
||||||
|
return this.dispatcher_.on(eventName, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthToken(v) {
|
||||||
|
this.authToken_ = v;
|
||||||
|
this.dispatcher_.dispatch('authRefreshed', this.authToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
loginUrl() {
|
||||||
|
return 'https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=' + this.clientId();
|
||||||
|
}
|
||||||
|
|
||||||
|
baseUrl(endPointFormat) {
|
||||||
|
if (['content', 'api'].indexOf(endPointFormat) < 0) throw new Error('Invalid end point format: ' + endPointFormat);
|
||||||
|
return 'https://' + endPointFormat + '.dropboxapi.com/2';
|
||||||
|
}
|
||||||
|
|
||||||
|
requestToCurl_(url, options) {
|
||||||
|
let output = [];
|
||||||
|
output.push('curl');
|
||||||
|
if (options.method) output.push('-X ' + options.method);
|
||||||
|
if (options.headers) {
|
||||||
|
for (let n in options.headers) {
|
||||||
|
if (!options.headers.hasOwnProperty(n)) continue;
|
||||||
|
output.push('-H ' + "'" + n + ': ' + options.headers[n] + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.body) output.push('--data ' + '"' + options.body + '"');
|
||||||
|
output.push(url);
|
||||||
|
|
||||||
|
return output.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
async execAuthToken(authCode) {
|
||||||
|
const postData = {
|
||||||
|
code: authCode,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
client_id: this.clientId(),
|
||||||
|
client_secret: this.clientSecret(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var formBody = [];
|
||||||
|
for (var property in postData) {
|
||||||
|
var encodedKey = encodeURIComponent(property);
|
||||||
|
var encodedValue = encodeURIComponent(postData[property]);
|
||||||
|
formBody.push(encodedKey + "=" + encodedValue);
|
||||||
|
}
|
||||||
|
formBody = formBody.join("&");
|
||||||
|
|
||||||
|
const response = await shim.fetch('https://api.dropboxapi.com/oauth2/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||||
|
},
|
||||||
|
body: formBody
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
if (!response.ok) throw new Error(responseText);
|
||||||
|
return JSON.parse(responseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
isTokenError(status, responseText) {
|
||||||
|
if (status === 401) return true;
|
||||||
|
if (responseText.indexOf('OAuth 2 access token is malformed') >= 0) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exec(method, path = '', body = null, headers = null, options = null) {
|
||||||
|
if (headers === null) headers = {};
|
||||||
|
if (options === null) options = {};
|
||||||
|
if (!options.target) options.target = 'string';
|
||||||
|
|
||||||
|
const authToken = this.authToken();
|
||||||
|
|
||||||
|
if (authToken) headers['Authorization'] = 'Bearer ' + authToken;
|
||||||
|
|
||||||
|
const endPointFormat = ['files/upload', 'files/download'].indexOf(path) >= 0 ? 'content' : 'api';
|
||||||
|
|
||||||
|
if (endPointFormat === 'api') {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
if (body && typeof body === 'object') body = JSON.stringify(body);
|
||||||
|
} else {
|
||||||
|
headers['Content-Type'] = 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOptions = {};
|
||||||
|
fetchOptions.headers = headers;
|
||||||
|
fetchOptions.method = method;
|
||||||
|
if (options.path) fetchOptions.path = options.path;
|
||||||
|
if (body) fetchOptions.body = body;
|
||||||
|
|
||||||
|
const url = path.indexOf('https://') === 0 ? path : this.baseUrl(endPointFormat) + '/' + path;
|
||||||
|
|
||||||
|
let tryCount = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
let response = null;
|
||||||
|
|
||||||
|
// console.info(this.requestToCurl_(url, fetchOptions));
|
||||||
|
|
||||||
|
// console.info(method + ' ' + url);
|
||||||
|
|
||||||
|
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
|
||||||
|
response = await shim.uploadBlob(url, fetchOptions);
|
||||||
|
} else if (options.target == 'string') {
|
||||||
|
response = await shim.fetch(url, fetchOptions);
|
||||||
|
} else { // file
|
||||||
|
response = await shim.fetchBlob(url, fetchOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
|
||||||
|
// console.info('Response: ' + responseText);
|
||||||
|
|
||||||
|
let responseJson_ = null;
|
||||||
|
const loadResponseJson = () => {
|
||||||
|
if (!responseText) return null;
|
||||||
|
if (responseJson_) return responseJson_;
|
||||||
|
try {
|
||||||
|
responseJson_ = JSON.parse(responseText);
|
||||||
|
} catch (error) {
|
||||||
|
return { error: responseText };
|
||||||
|
}
|
||||||
|
return responseJson_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
|
||||||
|
const newError = (message) => {
|
||||||
|
const json = loadResponseJson();
|
||||||
|
let code = '';
|
||||||
|
if (json && json.error_summary) {
|
||||||
|
code = json.error_summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
|
||||||
|
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
|
||||||
|
const shortResponseText = (responseText + '').substr(0, 1024);
|
||||||
|
const error = new JoplinError(method + ' ' + path + ': ' + message + ' (' + response.status + '): ' + shortResponseText, code);
|
||||||
|
error.httpStatus = response.status;
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const json = loadResponseJson();
|
||||||
|
if (this.isTokenError(response.status, responseText)) {
|
||||||
|
this.setAuthToken(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When using fetchBlob we only get a string (not xml or json) back
|
||||||
|
if (options.target === 'file') throw newError('fetchBlob error');
|
||||||
|
|
||||||
|
throw newError('Error');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.responseFormat === 'text') return responseText;
|
||||||
|
|
||||||
|
return loadResponseJson();
|
||||||
|
} catch (error) {
|
||||||
|
tryCount++;
|
||||||
|
if (error.code.indexOf('too_many_write_operations') >= 0) {
|
||||||
|
this.logger().warn('too_many_write_operations ' + tryCount);
|
||||||
|
if (tryCount >= 3) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
await time.sleep(tryCount * 2);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DropboxApi;
|
@@ -32,4 +32,4 @@ class EventDispatcher {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { EventDispatcher };
|
module.exports = EventDispatcher;
|
73
ReactNativeClient/lib/SyncTargetDropbox.js
Normal file
73
ReactNativeClient/lib/SyncTargetDropbox.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||||
|
const { _ } = require('lib/locale.js');
|
||||||
|
const DropboxApi = require('lib/DropboxApi');
|
||||||
|
const Setting = require('lib/models/Setting.js');
|
||||||
|
const { parameters } = require('lib/parameters.js');
|
||||||
|
const { FileApi } = require('lib/file-api.js');
|
||||||
|
const { Synchronizer } = require('lib/synchronizer.js');
|
||||||
|
const { FileApiDriverDropbox } = require('lib/file-api-driver-dropbox.js');
|
||||||
|
|
||||||
|
class SyncTargetDropbox extends BaseSyncTarget {
|
||||||
|
|
||||||
|
static id() {
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(db, options = null) {
|
||||||
|
super(db, options);
|
||||||
|
this.api_ = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static targetName() {
|
||||||
|
return 'dropbox';
|
||||||
|
}
|
||||||
|
|
||||||
|
static label() {
|
||||||
|
return _('Dropbox');
|
||||||
|
}
|
||||||
|
|
||||||
|
authRouteName() {
|
||||||
|
return 'DropboxLogin';
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAuthenticated() {
|
||||||
|
const f = await this.fileApi();
|
||||||
|
return !!f.driver().api().authToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
async api() {
|
||||||
|
const fileApi = await this.fileApi();
|
||||||
|
return fileApi.driver().api();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initFileApi() {
|
||||||
|
const params = parameters().dropbox;
|
||||||
|
|
||||||
|
const api = new DropboxApi({
|
||||||
|
id: params.id,
|
||||||
|
secret: params.secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
api.on('authRefreshed', (auth) => {
|
||||||
|
this.logger().info('Saving updated OneDrive auth.');
|
||||||
|
Setting.setValue('sync.' + SyncTargetDropbox.id() + '.auth', auth ? auth : null);
|
||||||
|
});
|
||||||
|
|
||||||
|
const authToken = Setting.value('sync.' + SyncTargetDropbox.id() + '.auth');
|
||||||
|
api.setAuthToken(authToken);
|
||||||
|
|
||||||
|
const appDir = '';
|
||||||
|
const fileApi = new FileApi(appDir, new FileApiDriverDropbox(api));
|
||||||
|
fileApi.setSyncTargetId(SyncTargetDropbox.id());
|
||||||
|
fileApi.setLogger(this.logger());
|
||||||
|
return fileApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initSynchronizer() {
|
||||||
|
if (!(await this.isAuthenticated())) throw new Error('User is not authentified');
|
||||||
|
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SyncTargetDropbox;
|
@@ -19,7 +19,7 @@ class SyncTargetFilesystem extends BaseSyncTarget {
|
|||||||
return _('File system');
|
return _('File system');
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated() {
|
async isAuthenticated() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -19,7 +19,7 @@ class SyncTargetMemory extends BaseSyncTarget {
|
|||||||
return 'Memory';
|
return 'Memory';
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated() {
|
async isAuthenticated() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -28,7 +28,7 @@ class SyncTargetNextcloud extends BaseSyncTarget {
|
|||||||
return _('Nextcloud');
|
return _('Nextcloud');
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated() {
|
async isAuthenticated() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -26,7 +26,7 @@ class SyncTargetOneDrive extends BaseSyncTarget {
|
|||||||
return _('OneDrive');
|
return _('OneDrive');
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated() {
|
async isAuthenticated() {
|
||||||
return this.api().auth();
|
return this.api().auth();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ class SyncTargetOneDrive extends BaseSyncTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initSynchronizer() {
|
async initSynchronizer() {
|
||||||
if (!this.isAuthenticated()) throw new Error('User is not authentified');
|
if (!await this.isAuthenticated()) throw new Error('User is not authentified');
|
||||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -24,7 +24,7 @@ class SyncTargetWebDAV extends BaseSyncTarget {
|
|||||||
return _('WebDAV');
|
return _('WebDAV');
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated() {
|
async isAuthenticated() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,6 +15,7 @@ const globalStyle = {
|
|||||||
dividerColor: "#dddddd",
|
dividerColor: "#dddddd",
|
||||||
selectedColor: '#e5e5e5',
|
selectedColor: '#e5e5e5',
|
||||||
disabledOpacity: 0.2,
|
disabledOpacity: 0.2,
|
||||||
|
colorUrl: '#000CFF',
|
||||||
|
|
||||||
raisedBackgroundColor: "#0080EF",
|
raisedBackgroundColor: "#0080EF",
|
||||||
raisedColor: "#003363",
|
raisedColor: "#003363",
|
||||||
@@ -89,10 +90,17 @@ function addExtraStyles(style) {
|
|||||||
fontSize: style.fontSize,
|
fontSize: style.fontSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
style.urlText = {
|
||||||
|
color: style.colorUrl,
|
||||||
|
fontSize: style.fontSize,
|
||||||
|
};
|
||||||
|
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
function themeStyle(theme) {
|
function themeStyle(theme) {
|
||||||
|
if (!theme) throw new Error('Theme not set');
|
||||||
|
|
||||||
if (themeCache_[theme]) return themeCache_[theme];
|
if (themeCache_[theme]) return themeCache_[theme];
|
||||||
|
|
||||||
let output = Object.assign({}, globalStyle);
|
let output = Object.assign({}, globalStyle);
|
||||||
|
83
ReactNativeClient/lib/components/screens/dropbox-login.js
Normal file
83
ReactNativeClient/lib/components/screens/dropbox-login.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
const React = require('react'); const Component = React.Component;
|
||||||
|
const { View, Button, Text, TextInput, TouchableOpacity, StyleSheet } = require('react-native');
|
||||||
|
const { connect } = require('react-redux');
|
||||||
|
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||||
|
const { _ } = require('lib/locale.js');
|
||||||
|
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||||
|
const DialogBox = require('react-native-dialogbox').default;
|
||||||
|
const { dialogs } = require('lib/dialogs.js');
|
||||||
|
const Shared = require('lib/components/shared/dropbox-login-shared');
|
||||||
|
const { themeStyle } = require('lib/components/global-style.js');
|
||||||
|
|
||||||
|
class DropboxLoginScreenComponent extends BaseScreenComponent {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.styles_ = {};
|
||||||
|
|
||||||
|
this.shared_ = new Shared(
|
||||||
|
this,
|
||||||
|
(msg) => dialogs.info(this, msg),
|
||||||
|
(msg) => dialogs.error(this, msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.shared_.refreshUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
styles() {
|
||||||
|
const themeId = this.props.theme;
|
||||||
|
const theme = themeStyle(themeId);
|
||||||
|
|
||||||
|
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||||
|
this.styles_ = {};
|
||||||
|
|
||||||
|
let styles = {
|
||||||
|
container: {
|
||||||
|
padding: theme.margin,
|
||||||
|
},
|
||||||
|
stepText: Object.assign({}, theme.normalText, { marginBottom: theme.margin }),
|
||||||
|
urlText: Object.assign({}, theme.urlText, { marginBottom: theme.margin }),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.styles_[themeId] = StyleSheet.create(styles);
|
||||||
|
return this.styles_[themeId];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const theme = themeStyle(this.props.theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={this.styles().screen}>
|
||||||
|
<ScreenHeader title={_('Login with Dropbox')}/>
|
||||||
|
|
||||||
|
<View style={this.styles().container}>
|
||||||
|
<Text style={this.styles().stepText}>{_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')}</Text>
|
||||||
|
<Text style={this.styles().stepText}>{_('Step 1: Open this URL in your browser to authorise the application:')}</Text>
|
||||||
|
<View>
|
||||||
|
<TouchableOpacity onPress={this.shared_.loginUrl_click}>
|
||||||
|
<Text style={this.styles().urlText}>{this.state.loginUrl}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<Text style={this.styles().stepText}>{_('Step 2: Enter the code provided by Dropbox:')}</Text>
|
||||||
|
<TextInput value={this.state.authCode} onChangeText={this.shared_.authCodeInput_change} style={theme.lineInput}/>
|
||||||
|
|
||||||
|
<Button disabled={this.state.checkingAuthToken} title={_("Submit")} onPress={this.shared_.submit_click}></Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropboxLoginScreen = connect((state) => {
|
||||||
|
return {
|
||||||
|
theme: state.settings.theme,
|
||||||
|
};
|
||||||
|
})(DropboxLoginScreenComponent)
|
||||||
|
|
||||||
|
module.exports = { DropboxLoginScreen };
|
@@ -0,0 +1,73 @@
|
|||||||
|
const { shim } = require('lib/shim');
|
||||||
|
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||||
|
const { reg } = require('lib/registry.js');
|
||||||
|
const { _ } = require('lib/locale.js');
|
||||||
|
const Setting = require('lib/models/Setting');
|
||||||
|
|
||||||
|
class Shared {
|
||||||
|
|
||||||
|
constructor(comp, showInfoMessageBox, showErrorMessageBox) {
|
||||||
|
this.comp_ = comp;
|
||||||
|
|
||||||
|
this.dropboxApi_ = null;
|
||||||
|
|
||||||
|
this.comp_.state = {
|
||||||
|
loginUrl: '',
|
||||||
|
authCode: '',
|
||||||
|
checkingAuthToken: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.loginUrl_click = () => {
|
||||||
|
if (!this.comp_.state.loginUrl) return;
|
||||||
|
shim.openUrl(this.comp_.state.loginUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.authCodeInput_change = (event) => {
|
||||||
|
this.comp_.setState({
|
||||||
|
authCode: typeof event === 'object' ? event.target.value : event
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submit_click = async () => {
|
||||||
|
this.comp_.setState({ checkingAuthToken: true });
|
||||||
|
|
||||||
|
const api = await this.dropboxApi();
|
||||||
|
try {
|
||||||
|
const response = await api.execAuthToken(this.comp_.state.authCode);
|
||||||
|
|
||||||
|
Setting.setValue('sync.' + this.syncTargetId() + '.auth', response.access_token);
|
||||||
|
api.setAuthToken(response.access_token);
|
||||||
|
await showInfoMessageBox(_('The application has been authorised!'));
|
||||||
|
this.comp_.props.dispatch({ type: 'NAV_BACK' });
|
||||||
|
reg.scheduleSync();
|
||||||
|
} catch (error) {
|
||||||
|
await showErrorMessageBox(_('Could not authorise application:\n\n%s\n\nPlease try again.', error.message));
|
||||||
|
} finally {
|
||||||
|
this.comp_.setState({ checkingAuthToken: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncTargetId() {
|
||||||
|
return SyncTargetRegistry.nameToId('dropbox');
|
||||||
|
}
|
||||||
|
|
||||||
|
async dropboxApi() {
|
||||||
|
if (this.dropboxApi_) return this.dropboxApi_;
|
||||||
|
|
||||||
|
const syncTarget = reg.syncTarget(this.syncTargetId());
|
||||||
|
this.dropboxApi_ = await syncTarget.api();
|
||||||
|
return this.dropboxApi_;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshUrl() {
|
||||||
|
const api = await this.dropboxApi();
|
||||||
|
|
||||||
|
this.comp_.setState({
|
||||||
|
loginUrl: api.loginUrl(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Shared;
|
@@ -36,7 +36,7 @@ shared.synchronize_press = async function(comp) {
|
|||||||
|
|
||||||
const action = comp.props.syncStarted ? 'cancel' : 'start';
|
const action = comp.props.syncStarted ? 'cancel' : 'start';
|
||||||
|
|
||||||
if (!reg.syncTarget().isAuthenticated()) {
|
if (!await reg.syncTarget().isAuthenticated()) {
|
||||||
if (reg.syncTarget().authRouteName()) {
|
if (reg.syncTarget().authRouteName()) {
|
||||||
comp.props.dispatch({
|
comp.props.dispatch({
|
||||||
type: 'NAV_GO',
|
type: 'NAV_GO',
|
||||||
|
@@ -67,6 +67,11 @@ dialogs.error = (parentComponent, message) => {
|
|||||||
return parentComponent.dialogbox.alert(message);
|
return parentComponent.dialogbox.alert(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialogs.info = (parentComponent, message) => {
|
||||||
|
Keyboard.dismiss();
|
||||||
|
return parentComponent.dialogbox.alert(message);
|
||||||
|
}
|
||||||
|
|
||||||
dialogs.DialogBox = DialogBox
|
dialogs.DialogBox = DialogBox
|
||||||
|
|
||||||
module.exports = { dialogs };
|
module.exports = { dialogs };
|
209
ReactNativeClient/lib/file-api-driver-dropbox.js
Normal file
209
ReactNativeClient/lib/file-api-driver-dropbox.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
const { time } = require('lib/time-utils.js');
|
||||||
|
const { shim } = require('lib/shim');
|
||||||
|
const JoplinError = require('lib/JoplinError');
|
||||||
|
const { basicDelta } = require('lib/file-api');
|
||||||
|
|
||||||
|
class FileApiDriverDropbox {
|
||||||
|
|
||||||
|
constructor(api) {
|
||||||
|
this.api_ = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
api() {
|
||||||
|
return this.api_;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestRepeatCount() {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
makePath_(path) {
|
||||||
|
if (!path) return '';
|
||||||
|
return '/' + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async stat(path) {
|
||||||
|
try {
|
||||||
|
const metadata = await this.api().exec('POST', 'files/get_metadata', {
|
||||||
|
path: this.makePath_(path),
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.metadataToStat_(metadata, path);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code.indexOf('not_found') >= 0) {
|
||||||
|
// ignore
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataToStat_(md, path) {
|
||||||
|
const output = {
|
||||||
|
path: path,
|
||||||
|
updated_time: md.server_modified ? new Date(md.server_modified) : new Date(),
|
||||||
|
isDir: md['.tag'] === 'folder',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (md['.tag'] === 'deleted') output.isDeleted = true;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataToStats_(mds) {
|
||||||
|
const output = [];
|
||||||
|
for (let i = 0; i < mds.length; i++) {
|
||||||
|
output.push(this.metadataToStat_(mds[i], mds[i].name));
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTimestamp(path, timestampMs) {
|
||||||
|
throw new Error('Not implemented'); // Not needed anymore
|
||||||
|
}
|
||||||
|
|
||||||
|
async delta(path, options) {
|
||||||
|
const context = options ? options.context : null;
|
||||||
|
let cursor = context ? context.cursor : null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const urlPath = cursor ? 'files/list_folder/continue' : 'files/list_folder';
|
||||||
|
const body = cursor ? { cursor: cursor } : { path: this.makePath_(path), include_deleted: true };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.api().exec('POST', urlPath, body);
|
||||||
|
|
||||||
|
const output = {
|
||||||
|
items: this.metadataToStats_(response.entries),
|
||||||
|
hasMore: response.has_more,
|
||||||
|
context: { cursor: response.cursor },
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
} catch (error) {
|
||||||
|
// If there's an error related to an invalid cursor, clear the cursor and retry.
|
||||||
|
if (cursor) {
|
||||||
|
if (error.httpStatus === 400 || error.code.indexOf('reset') >= 0) {
|
||||||
|
// console.info('Clearing cursor and retrying', error);
|
||||||
|
cursor = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(path, options) {
|
||||||
|
let response = await this.api().exec('POST', 'files/list_folder', {
|
||||||
|
path: this.makePath_(path),
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = this.metadataToStats_(response.entries);
|
||||||
|
|
||||||
|
while (response.has_more) {
|
||||||
|
response = await this.api().exec('POST', 'files/list_folder/continue', {
|
||||||
|
cursor: response.cursor,
|
||||||
|
});
|
||||||
|
|
||||||
|
output = output.concat(this.metadataToStats_(response.entries));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: output,
|
||||||
|
hasMore: false,
|
||||||
|
context: { cursor: response.cursor },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(path, options) {
|
||||||
|
if (!options) options = {};
|
||||||
|
if (!options.responseFormat) options.responseFormat = 'text';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.api().exec('POST', 'files/download', null, {
|
||||||
|
'Dropbox-API-Arg': JSON.stringify({ "path": this.makePath_(path) }),
|
||||||
|
}, options);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code.indexOf('not_found') >= 0) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async mkdir(path) {
|
||||||
|
try {
|
||||||
|
await this.api().exec('POST', 'files/create_folder_v2', {
|
||||||
|
path: this.makePath_(path),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code.indexOf('path/conflict') >= 0) {
|
||||||
|
// Ignore
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(path, content, options = null) {
|
||||||
|
// See https://github.com/facebook/react-native/issues/14445#issuecomment-352965210
|
||||||
|
if (typeof content === 'string') content = shim.Buffer.from(content, 'utf8')
|
||||||
|
|
||||||
|
await this.api().exec('POST', 'files/upload', content, {
|
||||||
|
'Dropbox-API-Arg': JSON.stringify({
|
||||||
|
path: this.makePath_(path),
|
||||||
|
mode: 'overwrite',
|
||||||
|
mute: true, // Don't send a notification to user since there can be many of these updates
|
||||||
|
})}, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(path) {
|
||||||
|
try {
|
||||||
|
await this.api().exec('POST', 'files/delete_v2', {
|
||||||
|
path: this.makePath_(path),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code.indexOf('not_found') >= 0) {
|
||||||
|
// ignore
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async move(oldPath, newPath) {
|
||||||
|
throw new Error('Not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
format() {
|
||||||
|
throw new Error('Not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearRoot() {
|
||||||
|
const entries = await this.list('');
|
||||||
|
const batchDelete = [];
|
||||||
|
for (let i = 0; i < entries.items.length; i++) {
|
||||||
|
batchDelete.push({ path: this.makePath_(entries.items[i].path) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.api().exec('POST', 'files/delete_batch', { entries: batchDelete });
|
||||||
|
const jobId = response.async_job_id;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const check = await this.api().exec('POST', 'files/delete_batch/check', { async_job_id: jobId });
|
||||||
|
if (check['.tag'] === 'complete') break;
|
||||||
|
|
||||||
|
// It returns "failed" if it didn't work but anyway throw an error if it's anything other than complete or in_progress
|
||||||
|
if (check['.tag'] !== 'in_progress') {
|
||||||
|
throw new Error('Batch delete failed? ' + JSON.stringify(check));
|
||||||
|
}
|
||||||
|
await time.sleep(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { FileApiDriverDropbox };
|
@@ -293,6 +293,7 @@ class FileApiDriverWebDav {
|
|||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== 404) throw error;
|
if (error.code !== 404) throw error;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -99,7 +99,7 @@ class Setting extends BaseModel {
|
|||||||
}},
|
}},
|
||||||
'noteVisiblePanes': { value: ['editor', 'viewer'], type: Setting.TYPE_ARRAY, public: false, appTypes: ['desktop'] },
|
'noteVisiblePanes': { value: ['editor', 'viewer'], type: Setting.TYPE_ARRAY, public: false, appTypes: ['desktop'] },
|
||||||
'showAdvancedOptions': { value: false, type: Setting.TYPE_BOOL, public: true, appTypes: ['mobile' ], label: () => _('Show advanced options') },
|
'showAdvancedOptions': { value: false, type: Setting.TYPE_BOOL, public: true, appTypes: ['mobile' ], label: () => _('Show advanced options') },
|
||||||
'sync.target': { value: SyncTargetRegistry.nameToId('onedrive'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: (appType) => { return appType !== 'cli' ? null : _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).') }, options: () => {
|
'sync.target': { value: SyncTargetRegistry.nameToId('dropbox'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: (appType) => { return appType !== 'cli' ? null : _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).') }, options: () => {
|
||||||
return SyncTargetRegistry.idAndLabelPlainObject();
|
return SyncTargetRegistry.idAndLabelPlainObject();
|
||||||
}},
|
}},
|
||||||
|
|
||||||
@@ -121,12 +121,14 @@ class Setting extends BaseModel {
|
|||||||
|
|
||||||
'sync.3.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
'sync.3.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
'sync.4.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
'sync.4.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
|
'sync.7.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
'sync.1.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
'sync.1.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
'sync.2.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
'sync.2.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
'sync.3.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
'sync.3.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
'sync.4.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
'sync.4.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
'sync.5.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
'sync.5.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
'sync.6.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
'sync.6.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
|
'sync.7.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.metadata_;
|
return this.metadata_;
|
||||||
|
@@ -11,6 +11,10 @@ parameters_.dev = {
|
|||||||
id: '606fd4d7-4dfb-4310-b8b7-a47d96aa22b6',
|
id: '606fd4d7-4dfb-4310-b8b7-a47d96aa22b6',
|
||||||
secret: 'qabchuPYL7931$ePDEQ3~_$',
|
secret: 'qabchuPYL7931$ePDEQ3~_$',
|
||||||
},
|
},
|
||||||
|
dropbox: {
|
||||||
|
id: 'cx9li9ur8taq1z7',
|
||||||
|
secret: 'i8f9a1mvx3bijrt',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
parameters_.prod = {
|
parameters_.prod = {
|
||||||
@@ -22,6 +26,10 @@ parameters_.prod = {
|
|||||||
id: '606fd4d7-4dfb-4310-b8b7-a47d96aa22b6',
|
id: '606fd4d7-4dfb-4310-b8b7-a47d96aa22b6',
|
||||||
secret: 'qabchuPYL7931$ePDEQ3~_$',
|
secret: 'qabchuPYL7931$ePDEQ3~_$',
|
||||||
},
|
},
|
||||||
|
dropbox: {
|
||||||
|
id: 'm044w3cvmxhzvop',
|
||||||
|
secret: 'r298deqisz0od56',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function parameters(env = null) {
|
function parameters(env = null) {
|
||||||
|
@@ -70,7 +70,7 @@ reg.scheduleSync = async (delay = null, syncOptions = null) => {
|
|||||||
|
|
||||||
const syncTargetId = Setting.value('sync.target');
|
const syncTargetId = Setting.value('sync.target');
|
||||||
|
|
||||||
if (!reg.syncTarget(syncTargetId).isAuthenticated()) {
|
if (!await reg.syncTarget(syncTargetId).isAuthenticated()) {
|
||||||
reg.logger().info('Synchroniser is missing credentials - manual sync required to authenticate.');
|
reg.logger().info('Synchroniser is missing credentials - manual sync required to authenticate.');
|
||||||
promiseResolve();
|
promiseResolve();
|
||||||
return;
|
return;
|
||||||
|
@@ -111,10 +111,9 @@ function shimInit() {
|
|||||||
const urlParse = require('url').parse;
|
const urlParse = require('url').parse;
|
||||||
|
|
||||||
url = urlParse(url.trim());
|
url = urlParse(url.trim());
|
||||||
|
const method = options.method ? options.method : 'GET';
|
||||||
const http = url.protocol.toLowerCase() == 'http:' ? require('follow-redirects').http : require('follow-redirects').https;
|
const http = url.protocol.toLowerCase() == 'http:' ? require('follow-redirects').http : require('follow-redirects').https;
|
||||||
const headers = options.headers ? options.headers : {};
|
const headers = options.headers ? options.headers : {};
|
||||||
const method = options.method ? options.method : 'GET';
|
|
||||||
if (method != 'GET') throw new Error('Only GET is supported');
|
|
||||||
const filePath = options.path;
|
const filePath = options.path;
|
||||||
|
|
||||||
function makeResponse(response) {
|
function makeResponse(response) {
|
||||||
@@ -143,7 +142,7 @@ function shimInit() {
|
|||||||
// Note: relative paths aren't supported
|
// Note: relative paths aren't supported
|
||||||
const file = fs.createWriteStream(filePath);
|
const file = fs.createWriteStream(filePath);
|
||||||
|
|
||||||
const request = http.get(requestOptions, function(response) {
|
const request = http.request(requestOptions, function(response) {
|
||||||
response.pipe(file);
|
response.pipe(file);
|
||||||
|
|
||||||
file.on('finish', function() {
|
file.on('finish', function() {
|
||||||
@@ -157,6 +156,8 @@ function shimInit() {
|
|||||||
fs.unlink(filePath);
|
fs.unlink(filePath);
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
request.end();
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
fs.unlink(filePath);
|
fs.unlink(filePath);
|
||||||
reject(error);
|
reject(error);
|
||||||
@@ -180,6 +181,13 @@ function shimInit() {
|
|||||||
return Buffer.byteLength(string, 'utf-8');
|
return Buffer.byteLength(string, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shim.Buffer = Buffer;
|
||||||
|
|
||||||
|
shim.openUrl = (url) => {
|
||||||
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
|
bridge().openExternal(url)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { shimInit };
|
module.exports = { shimInit };
|
@@ -6,6 +6,7 @@ const { generateSecureRandom } = require('react-native-securerandom');
|
|||||||
const FsDriverRN = require('lib/fs-driver-rn.js').FsDriverRN;
|
const FsDriverRN = require('lib/fs-driver-rn.js').FsDriverRN;
|
||||||
const urlValidator = require('valid-url');
|
const urlValidator = require('valid-url');
|
||||||
const { Buffer } = require('buffer');
|
const { Buffer } = require('buffer');
|
||||||
|
const { Linking } = require('react-native');
|
||||||
|
|
||||||
function shimInit() {
|
function shimInit() {
|
||||||
shim.Geolocation = GeolocationReact;
|
shim.Geolocation = GeolocationReact;
|
||||||
@@ -116,6 +117,12 @@ function shimInit() {
|
|||||||
shim.stringByteLength = function(string) {
|
shim.stringByteLength = function(string) {
|
||||||
return Buffer.byteLength(string, 'utf-8');
|
return Buffer.byteLength(string, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shim.Buffer = Buffer;
|
||||||
|
|
||||||
|
shim.openUrl = (url) => {
|
||||||
|
Linking.openURL(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { shimInit };
|
module.exports = { shimInit };
|
@@ -85,6 +85,9 @@ shim.fetchRequestCanBeRetried = function(error) {
|
|||||||
// Code: ETIMEDOUT
|
// Code: ETIMEDOUT
|
||||||
if (error.code === 'ETIMEDOUT') return true;
|
if (error.code === 'ETIMEDOUT') return true;
|
||||||
|
|
||||||
|
// ECONNREFUSED is generally temporary
|
||||||
|
if (error.code === 'ECONNREFUSED') return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,5 +132,7 @@ shim.clearInterval = function(id) {
|
|||||||
shim.stringByteLength = function(string) { throw new Error('Not implemented'); }
|
shim.stringByteLength = function(string) { throw new Error('Not implemented'); }
|
||||||
shim.detectAndSetLocale = null;
|
shim.detectAndSetLocale = null;
|
||||||
shim.attachFileToNote = async (note, filePath) => {}
|
shim.attachFileToNote = async (note, filePath) => {}
|
||||||
|
shim.Buffer = null;
|
||||||
|
shim.openUrl = () => { throw new Error('Not implemented'); }
|
||||||
|
|
||||||
module.exports = { shim };
|
module.exports = { shim };
|
@@ -36,6 +36,7 @@ const { WelcomeScreen } = require('lib/components/screens/welcome.js');
|
|||||||
const { SearchScreen } = require('lib/components/screens/search.js');
|
const { SearchScreen } = require('lib/components/screens/search.js');
|
||||||
const { OneDriveLoginScreen } = require('lib/components/screens/onedrive-login.js');
|
const { OneDriveLoginScreen } = require('lib/components/screens/onedrive-login.js');
|
||||||
const { EncryptionConfigScreen } = require('lib/components/screens/encryption-config.js');
|
const { EncryptionConfigScreen } = require('lib/components/screens/encryption-config.js');
|
||||||
|
const { DropboxLoginScreen } = require('lib/components/screens/dropbox-login.js');
|
||||||
const Setting = require('lib/models/Setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { MenuContext } = require('react-native-popup-menu');
|
const { MenuContext } = require('react-native-popup-menu');
|
||||||
const { SideMenu } = require('lib/components/side-menu.js');
|
const { SideMenu } = require('lib/components/side-menu.js');
|
||||||
@@ -55,10 +56,12 @@ const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
|||||||
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
||||||
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
||||||
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
|
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
|
||||||
|
const SyncTargetDropbox = require('lib/SyncTargetDropbox.js');
|
||||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||||
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
||||||
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
||||||
SyncTargetRegistry.addClass(SyncTargetWebDAV);
|
SyncTargetRegistry.addClass(SyncTargetWebDAV);
|
||||||
|
SyncTargetRegistry.addClass(SyncTargetDropbox);
|
||||||
|
|
||||||
// Disabled because not fully working
|
// Disabled because not fully working
|
||||||
//SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
//SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||||
@@ -365,16 +368,6 @@ async function initialize(dispatch) {
|
|||||||
await db.open({ name: 'joplin.sqlite' })
|
await db.open({ name: 'joplin.sqlite' })
|
||||||
} else {
|
} else {
|
||||||
await db.open({ name: 'joplin-68.sqlite' })
|
await db.open({ name: 'joplin-68.sqlite' })
|
||||||
//await db.open({ name: 'joplin-67.sqlite' })
|
|
||||||
|
|
||||||
// await db.exec('DELETE FROM notes');
|
|
||||||
// await db.exec('DELETE FROM folders');
|
|
||||||
// await db.exec('DELETE FROM tags');
|
|
||||||
// await db.exec('DELETE FROM note_tags');
|
|
||||||
// await db.exec('DELETE FROM resources');
|
|
||||||
// await db.exec('DELETE FROM deleted_items');
|
|
||||||
|
|
||||||
// await db.exec('UPDATE notes SET is_conflict = 1 where id like "546f%"');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reg.logger().info('Database is ready.');
|
reg.logger().info('Database is ready.');
|
||||||
@@ -559,6 +552,7 @@ class AppComponent extends React.Component {
|
|||||||
Note: { screen: NoteScreen },
|
Note: { screen: NoteScreen },
|
||||||
Folder: { screen: FolderScreen },
|
Folder: { screen: FolderScreen },
|
||||||
OneDriveLogin: { screen: OneDriveLoginScreen },
|
OneDriveLogin: { screen: OneDriveLoginScreen },
|
||||||
|
DropboxLogin: { screen: DropboxLoginScreen },
|
||||||
EncryptionConfig: { screen: EncryptionConfigScreen },
|
EncryptionConfig: { screen: EncryptionConfigScreen },
|
||||||
Log: { screen: LogScreen },
|
Log: { screen: LogScreen },
|
||||||
Status: { screen: StatusScreen },
|
Status: { screen: StatusScreen },
|
||||||
|
@@ -226,7 +226,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Windows</td>
|
<td>Windows (64-bit only)</td>
|
||||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.0.79/Joplin-Setup-1.0.79.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a></td>
|
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.0.79/Joplin-Setup-1.0.79.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Android</td>
|
<td>Android</td>
|
||||||
<td><a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a></td>
|
<td><a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a></td>
|
||||||
<td>or <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.0.112/joplin-v1.0.112.apk">Download APK File</a></td>
|
<td>or <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.0.114/joplin-v1.0.114.apk">Download APK File</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>iOS</td>
|
<td>iOS</td>
|
||||||
@@ -416,14 +416,14 @@ $$
|
|||||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png" alt=""></td>
|
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png" alt=""></td>
|
||||||
<td>Croatian</td>
|
<td>Croatian</td>
|
||||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po">hr_HR</a></td>
|
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po">hr_HR</a></td>
|
||||||
<td>Hrvoje Mandić <a href="mailto:trbuhom@net.hr">trbuhom@net.hr</a></td>
|
<td>Hrvoje Mandić <a href="mailto:trbuhom@net.hr">trbuhom@net.hr</a></td>
|
||||||
<td>64%</td>
|
<td>64%</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cz.png" alt=""></td>
|
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cz.png" alt=""></td>
|
||||||
<td>Czech</td>
|
<td>Czech</td>
|
||||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po">cs_CZ</a></td>
|
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po">cs_CZ</a></td>
|
||||||
<td>Lukas Helebrandt <a href="mailto:lukas@aiya.cz">lukas@aiya.cz</a></td>
|
<td>Lukas Helebrandt <a href="mailto:lukas@aiya.cz">lukas@aiya.cz</a></td>
|
||||||
<td>99%</td>
|
<td>99%</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -437,7 +437,7 @@ $$
|
|||||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png" alt=""></td>
|
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png" alt=""></td>
|
||||||
<td>Deutsch</td>
|
<td>Deutsch</td>
|
||||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po">de_DE</a></td>
|
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po">de_DE</a></td>
|
||||||
<td>Tobias Grasse <a href="mailto:mail@tobias-grasse.net">mail@tobias-grasse.net</a></td>
|
<td>Tobias Grasse <a href="mailto:mail@tobias-grasse.net">mail@tobias-grasse.net</a></td>
|
||||||
<td>98%</td>
|
<td>98%</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -451,7 +451,7 @@ $$
|
|||||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/es.png" alt=""></td>
|
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/es.png" alt=""></td>
|
||||||
<td>Español</td>
|
<td>Español</td>
|
||||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po">es_ES</a></td>
|
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po">es_ES</a></td>
|
||||||
<td>Fernando Martín <a href="mailto:f@mrtn.es">f@mrtn.es</a></td>
|
<td>Fernando Martín <a href="mailto:f@mrtn.es">f@mrtn.es</a></td>
|
||||||
<td>98%</td>
|
<td>98%</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -479,21 +479,21 @@ $$
|
|||||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/br.png" alt=""></td>
|
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/br.png" alt=""></td>
|
||||||
<td>Português (Brasil)</td>
|
<td>Português (Brasil)</td>
|
||||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po">pt_BR</a></td>
|
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po">pt_BR</a></td>
|
||||||
<td>Renato Nunes Bastos <a href="mailto:rnbastos@gmail.com">rnbastos@gmail.com</a></td>
|
<td>Renato Nunes Bastos <a href="mailto:rnbastos@gmail.com">rnbastos@gmail.com</a></td>
|
||||||
<td>97%</td>
|
<td>97%</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png" alt=""></td>
|
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png" alt=""></td>
|
||||||
<td>Русский</td>
|
<td>Русский</td>
|
||||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po">ru_RU</a></td>
|
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po">ru_RU</a></td>
|
||||||
<td>Artyom Karlov <a href="mailto:artyom.karlov@gmail.com">artyom.karlov@gmail.com</a></td>
|
<td>Artyom Karlov <a href="mailto:artyom.karlov@gmail.com">artyom.karlov@gmail.com</a></td>
|
||||||
<td>98%</td>
|
<td>98%</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png" alt=""></td>
|
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png" alt=""></td>
|
||||||
<td>中文 (简体)</td>
|
<td>中文 (简体)</td>
|
||||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po">zh_CN</a></td>
|
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po">zh_CN</a></td>
|
||||||
<td>RCJacH <a href="mailto:RCJacH@outlook.com">RCJacH@outlook.com</a></td>
|
<td>RCJacH <a href="mailto:RCJacH@outlook.com">RCJacH@outlook.com</a></td>
|
||||||
<td>66%</td>
|
<td>66%</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
Reference in New Issue
Block a user