1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-30 20:39:46 +02:00

Compare commits

..

130 Commits

Author SHA1 Message Date
Laurent Cozic
7851b6b429 Version update 2017-12-02 15:57:19 +00:00
Laurent Cozic
8eb5f8b74e Electron release v0.10.35 2017-12-02 15:18:23 +00:00
Laurent Cozic
1830ee9fd2 All: Fixed sync issue and database migration issue 2017-12-02 15:18:15 +00:00
Laurent Cozic
dd615d6a8f Update website 2017-12-02 14:50:56 +00:00
Laurent Cozic
08a518db70 Revert to 0.10.31 for now 2017-12-02 14:30:33 +00:00
Laurent Cozic
581372de0b Electron release v0.10.34 2017-12-02 14:26:55 +00:00
Laurent Cozic
fe2c1c197e All: fixed database creation error 2017-12-02 14:26:49 +00:00
Laurent Cozic
9854fddeb2 Electron release v0.10.33 2017-12-02 12:50:52 +00:00
Laurent Cozic
6c3918ebd2 All: Improved Evernote import for blockquotes and sup tags 2017-12-02 12:49:42 +00:00
Laurent Cozic
4af496632f Update website 2017-12-02 11:03:19 +00:00
Laurent Cozic
cd1e7a1083 Electron release v0.10.32 2017-12-02 00:21:29 +00:00
Laurent Cozic
4dce9e9e47 Electron: Allow attaching multiple files 2017-12-01 23:26:08 +00:00
Laurent Cozic
dbeff4fd7d All: Allow attaching files of unknown mime type 2017-12-01 23:15:49 +00:00
Laurent Cozic
aef2e4845d Electron: Fixes #32: Error when manually input alarm date 2017-12-01 21:55:02 +00:00
Laurent Cozic
739c6be476 Update website 2017-12-01 20:06:42 +00:00
Laurent Cozic
ff502670bf Updated Readme, mentioned Homebrew 2017-12-01 20:06:20 +00:00
Laurent Cozic
3894dd1191 Merge branch 'master' of github.com:laurent22/joplin 2017-12-01 19:44:41 +00:00
Laurent Cozic
818537933a Android: Fixes #53: Back button should exit app if nothing to go back to 2017-12-01 19:44:34 +00:00
Laurent Cozic
ec50808ba1 Merge branch 'master' of github.com:laurent22/joplin 2017-12-01 19:15:56 +00:00
Laurent Cozic
33fd8325be Electron: Fix #33: Allow copy and paste in OneDrive login 2017-12-01 19:15:46 +00:00
Laurent Cozic
c72e0a14c0 Electron, Mobile: Fixed header sizes 2017-12-01 19:04:14 +00:00
Laurent Cozic
58bc708014 Merge branch 'master' of github.com:laurent22/joplin 2017-12-01 18:56:43 +00:00
Laurent Cozic
ede1ed8b22 macOS: Allow BACKSPACE instead of DELETE in CLI app 2017-12-01 18:56:35 +00:00
Laurent Cozic
22e39b4434 Merge branch 'master' of github.com:laurent22/joplin 2017-12-01 18:14:06 +00:00
Laurent Cozic
9d984596cc CLI: fixed crash when inputting command without closed quote 2017-12-01 17:51:27 +00:00
Laurent Cozic
fe909f659d All: Added error message for OneDrive for Business 2017-12-01 17:47:18 +00:00
Laurent Cozic
ea120eae91 Update README.md 2017-12-01 15:29:58 +00:00
Laurent Cozic
fa819d25b0 Update README.md 2017-12-01 13:06:26 +00:00
Laurent Cozic
65739a5077 Update website 2017-12-01 09:57:32 +00:00
Laurent Cozic
228d06e27f Electron release v0.10.31 2017-12-01 00:00:33 +00:00
Laurent Cozic
d386b83c53 Merge branch 'master' of github.com:laurent22/joplin 2017-12-01 00:00:26 +00:00
Laurent Cozic
e1b1f31cf1 Electron: #22 Fixes keyboard cursor jumps while typing. 2017-12-01 00:00:18 +00:00
Laurent Cozic
f80b403dfe Create BUILD.md 2017-11-30 23:02:14 +00:00
Laurent Cozic
5af55d7da3 Create issue_template.md 2017-11-30 22:33:18 +00:00
Laurent Cozic
5e6a389f97 Merge branch 'master' of github.com:laurent22/joplin 2017-11-30 21:48:36 +00:00
Laurent Cozic
6f26910243 Android v0.10.61 2017-11-30 21:47:55 +00:00
Laurent Cozic
b2056f9e4d Merge pull request #41 from dflock/patch-1
Minor grammar fix
2017-11-30 21:25:05 +00:00
Duncan Lock
dca4bb204b Minor grammar fix
loss -> lost.
2017-11-30 13:23:04 -08:00
Laurent Cozic
ade39837de Update README.md 2017-11-30 21:18:48 +00:00
Laurent Cozic
2ef2296566 Update website 2017-11-30 20:30:01 +00:00
Laurent Cozic
04ed914894 Electron release v0.10.30 2017-11-30 18:53:35 +00:00
Laurent Cozic
332dd0d859 Merge branch 'alarm-support' 2017-11-30 18:53:26 +00:00
Laurent Cozic
d9a1f7855d Merge branch 'alarm-support' of github.com:laurent22/joplin into alarm-support 2017-11-30 18:53:03 +00:00
Laurent Cozic
3ec59a835c Electron release v0.10.29 2017-11-30 18:44:53 +00:00
Laurent Cozic
620225bb2d Electron: Fixed copy/cut/paste problem in macOS 2017-11-30 18:44:37 +00:00
Laurent Cozic
9d7d469908 Electron: Fixed option page checkboxes 2017-11-30 18:36:26 +00:00
Laurent Cozic
16bf0cf646 All: Added Spanish (Costa Rica) locale 2017-11-30 18:29:10 +00:00
Laurent Cozic
8079106c3e Update websiyte 2017-11-30 01:08:29 +00:00
Laurent Cozic
28143db968 Electron release v0.10.28 2017-11-30 00:25:59 +00:00
Laurent Cozic
7f1a14fa22 All: Fixed note update issue 2017-11-30 00:25:52 +00:00
Laurent Cozic
4de1edda05 Electron release v0.10.27 2017-11-29 23:27:28 +00:00
Laurent Cozic
52f09d2638 Electron: Fixed import of certain images 2017-11-29 23:27:20 +00:00
Laurent Cozic
97b8cad755 Merge branch 'master' of github.com:laurent22/joplin 2017-11-29 23:04:08 +00:00
Laurent Cozic
52cb10dd4e Electron: added toolbar and fixed various state issues 2017-11-29 23:03:10 +00:00
Laurent Cozic
135a8a9273 Update README.md
Mentioned NextCloud support
2017-11-29 19:49:46 +00:00
Laurent Cozic
a346116d5f Update website 2017-11-29 16:03:19 +00:00
Laurent Cozic
934c3c8001 Update readme 2017-11-29 16:02:45 +00:00
Laurent Cozic
565c17df37 Mobile: Improved OneDrive login 2017-11-28 21:49:58 +00:00
Laurent Cozic
5ee9a35f7d Electron release v0.10.26 2017-11-29 15:32:41 +00:00
Laurent Cozic
bd73107853 Electron: Improved parsing of auth token 2017-11-28 21:36:04 +00:00
Laurent Cozic
2e8fe88f53 Don't needlessly reload resources 2017-11-28 21:15:22 +00:00
Laurent Cozic
444c96d5e7 Electron: Fixed scrolling issue 2017-11-28 20:58:07 +00:00
Laurent Cozic
f5ff68b236 Android: test release 2017-11-28 20:35:39 +00:00
Laurent Cozic
d1a83d065a Mobile: Added status info to view active alarms 2017-11-28 20:31:14 +00:00
Laurent Cozic
ddb73c8642 iOS: Fixed notification event 2017-11-28 20:23:22 +00:00
Laurent Cozic
4df73cd82c iOS: Handle foreground notifications 2017-11-28 20:17:34 +00:00
Laurent Cozic
c446e4471d Merge branch 'alarm-support' of github.com:laurent22/joplin into alarm-support 2017-11-28 17:29:31 +00:00
Laurent Cozic
fa22d5bae3 Electron: Fixed getting package info 2017-11-28 19:53:29 +00:00
Laurent Cozic
1a610054d3 Android: Fixed cancelling of notification 2017-11-28 19:49:03 +00:00
Laurent Cozic
11517fa037 iOS: Get notifications to work 2017-11-28 19:36:47 +00:00
Laurent Cozic
67a457b9c5 Android: Got notifications to work 2017-11-28 18:58:04 +00:00
Laurent Cozic
18dc6c826a Electron: Trigger notifications using node-notifier 2017-11-28 18:47:41 +00:00
Laurent Cozic
033d356b56 Removed dependency to marked and update locale 2017-11-28 18:07:49 +00:00
Laurent Cozic
f9f5974267 All: Added option to set date and time format 2017-11-28 18:02:54 +00:00
Laurent Cozic
6e23fead59 Electron: Getting notifications to work 2017-11-28 00:22:38 +00:00
Laurent Cozic
7df6541902 Merge branch 'alarm-support' of github.com:laurent22/joplin into alarm-support 2017-11-27 22:51:03 +00:00
Laurent Cozic
9a40851c77 Electron, Mobile: Created alarm service and drivers 2017-11-27 22:50:46 +00:00
Laurent Cozic
748acdf03f Merge branch 'master' into alarm-support 2017-11-26 21:08:14 +00:00
Laurent Cozic
13d27357a0 Updated Readme 2017-11-26 19:39:12 +00:00
Laurent Cozic
c72caad764 Updated website 2017-11-26 19:38:33 +00:00
Laurent Cozic
2933d09366 Updated Readme 2017-11-26 19:38:05 +00:00
Laurent Cozic
f51ad26db7 Release v0.10.59-android 2017-11-26 19:27:42 +00:00
Laurent Cozic
50ca686727 iOS: Added notification config 2017-11-24 18:27:41 +00:00
Laurent Cozic
3612529e52 iOS: Testing notification 2017-11-24 18:06:45 +00:00
Laurent Cozic
c053146885 iOS: Added notification config 2017-11-24 17:34:52 +00:00
Laurent Cozic
8ccf2ec521 Mobile: Trying RN notification 2017-11-24 20:14:56 +00:00
Laurent Cozic
0428917ea2 iOS: Updated version 2017-11-24 17:28:43 +00:00
Laurent Cozic
914a2554ab All: Updated translations 2017-11-24 19:59:21 +00:00
Laurent Cozic
26bb7dc33b All: Added dev sync target and option to select it in mobile 2017-11-24 19:47:24 +00:00
Laurent Cozic
8e5b0eadd9 All: Clean up 2017-11-24 19:26:49 +00:00
Laurent Cozic
a96b91cfef All: Removed sync target info out of setting class 2017-11-24 19:21:30 +00:00
Laurent Cozic
03251d4c40 All: renamed sync targets 2017-11-24 19:09:15 +00:00
Laurent Cozic
112609c5f1 All: Moving sync target logic to SyncTarget classes 2017-11-24 18:59:16 +00:00
Laurent Cozic
cc7cbc2ecf All: Started moving sync target logic under SyncTarget classes 2017-11-24 18:37:40 +00:00
Laurent Cozic
946ad7c71a All: Started moving sync target logic under SyncTarget classes 2017-11-24 18:06:30 +00:00
Laurent Cozic
60d2b0c763 Merge branch 'master' into alarm-support 2017-11-24 17:09:24 +00:00
Laurent Cozic
d7f3cfd778 All: Started moving sync glue logic to SyncTarget classes 2017-11-23 23:10:55 +00:00
Laurent Cozic
a2ae2c766a Merge branch 'master' of github.com:laurent22/joplin 2017-11-23 19:29:12 +00:00
Laurent Cozic
f52ff730b1 Disable cache 2017-11-23 19:29:03 +00:00
Laurent Cozic
9327a61a36 iOS: maybe bad change 2017-11-23 19:15:44 +00:00
Laurent Cozic
05997908e5 Electron release v0.10.25 2017-11-23 19:05:37 +00:00
Laurent Cozic
2a93dea378 Electron release v0.10.24 2017-11-23 19:04:56 +00:00
Laurent Cozic
fbf7b2cc43 Mobile: Hide irrelevant settings 2017-11-23 18:54:26 +00:00
Laurent Cozic
d8b19f7d08 Mobile: Also do multi-selection from search page 2017-11-23 18:41:35 +00:00
Laurent Cozic
acc4eb5d28 Mobile: Allow selecting, deleting and moving multiple notes 2017-11-23 18:47:51 +00:00
Laurent Cozic
bcd5cd9110 Electron: Hide invalid characters 2017-11-23 18:16:17 +00:00
Laurent Cozic
45a4034816 Android: Automate building for prod 2017-11-23 18:14:38 +00:00
Laurent Cozic
978a08fb06 Trying BuddyBuilder 72 2017-11-22 22:24:20 +00:00
Laurent Cozic
72dc5a6c99 Trying BuddyBuilder 2017-11-22 22:21:27 +00:00
Laurent Cozic
5340fb8af9 Electron: Disable Control+Click 2017-11-22 19:29:49 +00:00
Laurent Cozic
3ce1172c36 Allow changing notebook by drag and dropping notes 2017-11-22 19:20:19 +00:00
Laurent Cozic
e4d48f43d6 Allow multiple selection 2017-11-22 18:35:31 +00:00
Laurent Cozic
3e1ea0eb0a Android icons 2017-11-22 17:37:57 +00:00
Laurent Cozic
d10d6ba7de Merge branch 'master' of github.com:laurent22/joplin 2017-11-21 19:44:10 +00:00
Laurent Cozic
cf832354a2 Updated website 2017-11-21 19:43:09 +00:00
Laurent Cozic
b27519b85a Update appveyor.yml 2017-11-21 19:31:43 +00:00
Laurent Cozic
27af0e69ef Electron release v0.10.23 2017-11-21 18:26:17 +00:00
Laurent Cozic
6283bf6190 Minor fixing on note rendering 2017-11-21 19:47:29 +00:00
Laurent Cozic
48b648e656 Electron: allow disabling auto-updates 2017-11-21 19:37:34 +00:00
Laurent Cozic
367a18db93 Electron: fixed loading of resources, and disable drag and dropping of links in app 2017-11-21 19:31:21 +00:00
Laurent Cozic
f5feb595f6 Updated translation 2017-11-21 19:14:30 +00:00
Laurent Cozic
c6ec0279fc Mobile: Made checkboxes smaller 2017-11-21 19:05:59 +00:00
Laurent Cozic
b0b5488c2e Electron: Better error handling for auto-update 2017-11-21 18:59:32 +00:00
Laurent Cozic
3722012da5 Added command to export debug information from mobile and CLI 2017-11-21 18:48:50 +00:00
Laurent Cozic
c5214b6c44 Core: Fixed potential out-of-sync issue if user cancels while in the middle of delta step 2017-11-21 18:17:50 +00:00
Laurent Cozic
585ccc2b8b Update website 2017-11-20 22:31:29 +00:00
Laurent Cozic
ab8115b89d Added download badges 2017-11-20 22:28:22 +00:00
Laurent Cozic
8d8395c226 Added App Store icon 2017-11-20 20:06:32 +00:00
Laurent Cozic
8dbec0e8f4 Put locales under versioning 2017-11-20 19:14:02 +00:00
Laurent Cozic
7ac6f39658 Alarm support 2017-09-10 17:56:27 +01:00
150 changed files with 5984 additions and 2983 deletions

4
.gitignore vendored
View File

@@ -33,4 +33,6 @@ sync_staging.sh
*.swp
_vieux/
_mydocs
.DS_Store
.DS_Store
Assets/DownloadBadges*.psd
node_modules

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
Assets/Icon-Android-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

38
BUILD.md Normal file
View File

@@ -0,0 +1,38 @@
# General information
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when builing each app.
- The translations are built by running CliClient/build-translation.sh. For this reasons, it's generally better to get the CLI app to build first so that everything is setup correctly.
## macOS dependencies
brew install yarn node xgettext
echo 'export PATH="/usr/local/opt/gettext/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
## Linux and Windows dependencies
- Install yarn - https://yarnpkg.com/lang/en/docs/install/
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
# Building the Electron application
```
cd ElectronClient/app
rsync -a ../../ReactNativeClient/lib/ lib/
npm install
yarn dist
```
If there's an error `while loading shared libraries: libgconf-2.so.4: cannot open shared object file: No such file or directory`, run `sudo apt-get install libgconf-2-4`
That will create the executable file in the `dist` directory.
From `/ElectronClient` you can also run `run.sh` to run the app for testing.
# Building the Mobile application
From `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
# Building the Terminal application
From `/CliClient`, run `npm install` then run `run.sh`. If you get an error about `xgettext`, comment out the command `node build-translation.js --silent` in build.sh

View File

@@ -167,7 +167,7 @@ class AppGui {
});
this.rootWidget_.connect(noteList, (state) => {
return {
selectedNoteId: state.selectedNoteId,
selectedNoteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
items: state.notes,
};
});
@@ -181,7 +181,7 @@ class AppGui {
};
this.rootWidget_.connect(noteText, (state) => {
return {
noteId: state.selectedNoteId,
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
notes: state.notes,
};
});
@@ -195,7 +195,7 @@ class AppGui {
borderRightWidth: 1,
};
this.rootWidget_.connect(noteMetadata, (state) => {
return { noteId: state.selectedNoteId };
return { noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null };
});
noteMetadata.hide();
@@ -321,6 +321,9 @@ class AppGui {
action: async () => {
if (this.widget('folderList').hasFocus) {
const item = this.widget('folderList').selectedJoplinItem;
if (!item) return;
if (item.type_ === BaseModel.TYPE_FOLDER) {
await this.processCommand('rmbook ' + item.id);
} else if (item.type_ === BaseModel.TYPE_TAG) {
@@ -339,6 +342,10 @@ class AppGui {
}
};
shortcuts['BACKSPACE'] = {
alias: 'DELETE',
};
shortcuts[' '] = {
friendlyName: 'SPACE',
description: () => _('Set a to-do as completed / not completed'),
@@ -519,22 +526,22 @@ class AppGui {
return;
}
let note = this.widget('noteList').currentItem;
let folder = this.widget('folderList').currentItem;
let args = splitCommandString(cmd);
try {
let note = this.widget('noteList').currentItem;
let folder = this.widget('folderList').currentItem;
let args = splitCommandString(cmd);
for (let i = 0; i < args.length; i++) {
if (args[i] == '$n') {
args[i] = note ? note.id : '';
} else if (args[i] == '$b') {
args[i] = folder ? folder.id : '';
} else if (args[i] == '$c') {
const item = this.activeListItem();
args[i] = item ? item.id : '';
for (let i = 0; i < args.length; i++) {
if (args[i] == '$n') {
args[i] = note ? note.id : '';
} else if (args[i] == '$b') {
args[i] = folder ? folder.id : '';
} else if (args[i] == '$c') {
const item = this.activeListItem();
args[i] = item ? item.id : '';
}
}
}
try {
await this.app().execCommand(args);
} catch (error) {
this.stdout(error.message);
@@ -765,7 +772,10 @@ class AppGui {
// -------------------------------------------------------------------------
const shortcutKey = this.currentShortcutKeys_.join('');
const cmd = shortcutKey in this.shortcuts_ ? this.shortcuts_[shortcutKey] : null;
let cmd = shortcutKey in this.shortcuts_ ? this.shortcuts_[shortcutKey] : null;
// If this command is an alias to another command, resolve to the actual command
if (cmd && cmd.alias) cmd = this.shortcuts_[cmd.alias];
let processShortcutKeys = !this.app().currentCommand() && cmd;
if (cmd && cmd.canRunAlongOtherCommands) processShortcutKeys = true;

View File

@@ -168,9 +168,9 @@ class Application extends BaseApplication {
await doExit();
}, 5000);
if (await reg.syncStarted()) {
if (await reg.syncTarget().syncStarted()) {
this.stdout(_('Cancelling background synchronisation... Please wait.'));
const sync = await reg.synchronizer(Setting.value('sync.target'));
const sync = await reg.syncTarget().synchronizer();
await sync.cancel();
}

View File

@@ -6,7 +6,7 @@ const processArgs = process.argv.splice(2, process.argv.length);
const silentLog = processArgs.indexOf('--silent') >= 0;
const { basename, dirname } = require('lib/path-utils.js');
const { basename, dirname, filename, fileExtension } = require('lib/path-utils.js');
const fs = require('fs-extra');
const gettextParser = require('gettext-parser');
@@ -68,6 +68,9 @@ function buildLocale(inputFile, outputFile) {
}
async function removePoHeaderDate(filePath) {
// Note: on macOS this will fail because it needs to be 'sed -i ""'
// Solution would be to install gsed, detect it here, and use it in place of sed in macOS
// https://stackoverflow.com/questions/30003570/how-to-use-gnu-sed-on-mac-os-x#34815955
await execCommand('sed -i -e\'/POT-Creation-Date:/d\' "' + filePath + '"');
await execCommand('sed -i -e\'/PO-Revision-Date:/d\' "' + filePath + '"');
}
@@ -110,6 +113,17 @@ function buildIndex(locales) {
return output.join("\n");
}
function availableLocales(defaultLocale) {
const output = [defaultLocale];
fs.readdirSync(cliLocalesDir).forEach((path) => {
if (fileExtension(path) !== 'po') return;
const locale = filename(path);
if (locale === defaultLocale) return;
output.push(locale);
});
return output;
}
async function main() {
let potFilePath = cliLocalesDir + '/joplin.pot';
let jsonLocalesDir = cliDir + '/build/locales';
@@ -131,7 +145,7 @@ async function main() {
fs.mkdirpSync(jsonLocalesDir, 0o755);
let locales = [defaultLocale, 'fr_FR'];
let locales = availableLocales(defaultLocale);
for (let i = 0; i < locales.length; i++) {
const locale = locales[i];
const poFilePäth = cliLocalesDir + '/' + locale + '.po';

View File

@@ -1,7 +1,7 @@
const fs = require('fs-extra');
const { fileExtension, basename, dirname } = require('lib/path-utils.js');
const { _, setLocale, languageCode } = require('lib/locale.js');
const marked = require('lib/marked.js');
const marked = require('marked');
const Mustache = require('mustache');
const headerHtml = `<!doctype html>
@@ -91,6 +91,10 @@ const headerHtml = `<!doctype html>
.cli-screenshot .prompt {
color: #48C2F0;
}
.top-screenshot {
margin-top: 2em;
text-align: center;
}
.header {
position: relative;
padding-left: 2em;
@@ -150,15 +154,29 @@ const headerHtml = `<!doctype html>
width: 100%;
text-align: right;
vertical-align: middle;
padding-top: 3px;
line-height: 0;
}
.twitter-share-button {
.nav-right .share-btn {
display: none;
}
.github-share-button {
.share-btn-github {
display: inline-block;
}
</style>
.nav-right .small-share-btn {
display: none;
}
.nav-right .share-btn-github {
display: inline-block;
}
@media all and (min-width: 400px) {
.nav-right .share-btn {
display: inline-block;
}
.nav-right .small-share-btn {
display: none;
}
}
</style>
</head>
<body>
@@ -179,9 +197,9 @@ const headerHtml = `<!doctype html>
<li class="{{selectedDesktop}}"><a href="{{baseUrl}}/desktop" title="Desktop"><i class="fa fa-desktop"></i></a></li>
</ul>
<div class="nav-right">
<iframe src="https://www.facebook.com/plugins/share_button.php?href=http%3A%2F%2Fjoplin.cozic.net&layout=button&size=small&mobile_iframe=true&width=60&height=20&appId" width="60" height="20" style="border:none;overflow:hidden" scrolling="no" frameborder="0" allowTransparency="true"></iframe>
<a class="twitter-share-button" href="https://twitter.com/intent/tweet?url=http%3A%2F%2Fjoplin.cozic.net">Tweet</a>
<iframe class="github-share-button" src="https://ghbtns.com/github-btn.html?user=laurent22&repo=joplin&type=star&count=true" frameborder="0" scrolling="0" width="80px" height="20px"></iframe>
<iframe class="share-btn" src="https://www.facebook.com/plugins/share_button.php?href=http%3A%2F%2Fjoplin.cozic.net&layout=button&size=small&mobile_iframe=true&width=60&height=20&appId" width="60" height="20" style="border:none;overflow:hidden" scrolling="no" frameborder="0" allowTransparency="true"></iframe>
<iframe class="share-btn" src="https://platform.twitter.com/widgets/tweet_button.html?url=http%3A%2F%2Fjoplin.cozic.net" width="62" height="20" title="Tweet" style="border: 0; overflow: hidden;"></iframe>
<iframe class="share-btn share-btn-github" src="https://ghbtns.com/github-btn.html?user=laurent22&repo=joplin&type=star&count=true" frameborder="0" scrolling="0" width="80px" height="20px"></iframe>
</div>
</div>
</div>
@@ -230,21 +248,6 @@ const footerHtml = `
// `;
const scriptHtml = `
<script>window.twttr = (function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0],
t = window.twttr || {};
if (d.getElementById(id)) return t;
js = d.createElement(s);
js.id = id;
js.src = "https://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
t._e = [];
t.ready = function(f) {
t._e.push(f);
};
return t;
}(document, "script", "twitter-wjs"));</script>
<script>
function stickyHeader() {
if ($(window).scrollTop() > 179) {
@@ -267,6 +270,10 @@ const scriptHtml = `
</script>
`;
// <a href="#" class="small-share-btn" style="background-color: #365899;"><img src="images/ShareFacebook.svg" style=" width: 1.2em; height: 1.2em"/></a>
// <a href="#" class="small-share-btn" style="background-color: #1b95e0;"><img src="images/ShareTwitter.svg" style=" width: 1.2em; height: 1.2em"/></a>
// <a href="#" class="small-share-btn" style="background-color: #eee;"><img src="images/ShareGithub.svg" style=" width: 1.2em; height: 1.2em"/></a>
const rootDir = dirname(dirname(__dirname));
function markdownToHtml(md) {

View File

@@ -0,0 +1,36 @@
const { BaseCommand } = require('./base-command.js');
const { Database } = require('lib/database.js');
const { app } = require('./app.js');
const { Setting } = require('lib/models/setting.js');
const { _ } = require('lib/locale.js');
const { ReportService } = require('lib/services/report.js');
const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'export-sync-status';
}
description() {
return 'Export sync status';
}
hidden() {
return true;
}
async action(args) {
const service = new ReportService();
const csv = await service.basicItemList({ format: 'csv' });
const filePath = Setting.value('profileDir') + '/syncReport-' + (new Date()).getTime() + '.csv';
await fs.writeFileSync(filePath, csv);
this.stdout('Sync status exported to ' + filePath);
app().gui().showConsole();
app().gui().maximizeConsole();
}
}
module.exports = Command;

View File

@@ -2,7 +2,7 @@ const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Folder } = require('lib/models/folder.js');
const { importEnex } = require('import-enex');
const { importEnex } = require('lib/import-enex');
const { filename, basename } = require('lib/path-utils.js');
const { cliUtils } = require('./cli-utils.js');

View File

@@ -16,7 +16,7 @@ class Command extends BaseCommand {
constructor() {
super();
this.syncTarget_ = null;
this.syncTargetId_ = null;
this.releaseLockFn_ = null;
this.oneDriveApiUtils_ = null;
}
@@ -62,6 +62,27 @@ class Command extends BaseCommand {
});
}
async doAuth(syncTargetId) {
const syncTarget = reg.syncTarget(this.syncTargetId_);
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
const auth = await this.oneDriveApiUtils_.oauthDance({
log: (...s) => { return this.stdout(...s); }
});
this.oneDriveApiUtils_ = null;
return auth;
}
cancelAuth() {
if (this.oneDriveApiUtils_) {
this.oneDriveApiUtils_.cancelOAuthDance();
return;
}
}
doingAuth() {
return !!this.oneDriveApiUtils_;
}
async action(args) {
this.releaseLockFn_ = null;
@@ -91,25 +112,24 @@ class Command extends BaseCommand {
};
try {
this.syncTarget_ = Setting.value('sync.target');
if (args.options.target) this.syncTarget_ = args.options.target;
this.syncTargetId_ = Setting.value('sync.target');
if (args.options.target) this.syncTargetId_ = args.options.target;
if (this.syncTarget_ == Setting.SYNC_TARGET_ONEDRIVE && !reg.syncHasAuth(this.syncTarget_)) {
const syncTarget = reg.syncTarget(this.syncTargetId_);
if (!syncTarget.isAuthenticated()) {
app().gui().showConsole();
app().gui().maximizeConsole();
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(reg.oneDriveApi());
const auth = await this.oneDriveApiUtils_.oauthDance({
log: (...s) => { return this.stdout(...s); }
});
this.oneDriveApiUtils_ = null;
Setting.setValue('sync.3.auth', auth ? JSON.stringify(auth) : null);
const auth = await this.doAuth(this.syncTargetId_);
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', auth ? JSON.stringify(auth) : null);
if (!auth) {
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
return cleanUp();
}
}
let sync = await reg.synchronizer(this.syncTarget_);
const sync = await syncTarget.synchronizer();
let options = {
onProgress: (report) => {
@@ -123,13 +143,13 @@ class Command extends BaseCommand {
randomFailures: args.options['random-failures'] === true,
};
this.stdout(_('Synchronisation target: %s (%s)', Setting.enumOptionLabel('sync.target', this.syncTarget_), this.syncTarget_));
this.stdout(_('Synchronisation target: %s (%s)', Setting.enumOptionLabel('sync.target', this.syncTargetId_), this.syncTargetId_));
if (!sync) throw new Error(_('Cannot initialize synchroniser.'));
this.stdout(_('Starting synchronisation...'));
const contextKey = 'sync.' + this.syncTarget_ + '.context';
const contextKey = 'sync.' + this.syncTargetId_ + '.context';
let context = Setting.value(contextKey);
context = context ? JSON.parse(context) : {};
@@ -156,26 +176,28 @@ class Command extends BaseCommand {
}
async cancel() {
if (this.oneDriveApiUtils_) {
this.oneDriveApiUtils_.cancelOAuthDance();
if (this.doingAuth()) {
this.cancelAuth();
return;
}
const target = this.syncTarget_ ? this.syncTarget_ : Setting.value('sync.target');
const syncTargetId = this.syncTargetId_ ? this.syncTargetId_ : Setting.value('sync.target');
cliUtils.redrawDone();
this.stdout(_('Cancelling... Please wait.'));
if (reg.syncHasAuth(target)) {
let sync = await reg.synchronizer(target);
const syncTarget = reg.syncTarget(syncTargetId);
if (syncTarget.isAuthenticated()) {
const sync = await syncTarget.synchronizer();
if (sync) await sync.cancel();
} else {
if (this.releaseLockFn_) this.releaseLockFn_();
this.releaseLockFn_ = null;
}
this.syncTarget_ = null;
this.syncTargetId_ = null;
}
cancellable() {

View File

@@ -8,53 +8,5 @@ rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
cp "$ROOT_DIR/package.json" "$BUILD_DIR"
chmod 755 "$BUILD_DIR/main.js"
cd "$BUILD_DIR"
node build-translation.js --silent
# ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# # require('cache-require-paths');
# mkdir -p "$ROOT_DIR/build"
# rm -f "$ROOT_DIR/app/lib"
# ln -s "$ROOT_DIR/../ReactNativeClient/lib" "$ROOT_DIR/app"
# npm run build || exit 1
# # Files under app/gui are in ES6 already but I cannot get Babel
# # to ignore them, so copy them back to the build directory.
# rsync -a "$ROOT_DIR/app/gui/" "$ROOT_DIR/build/gui/"
# cp "$ROOT_DIR/package.json" "$ROOT_DIR/build"
# chmod 755 "$ROOT_DIR/build/main.js"
# # if [[ ! -f "$ROOT_DIR/package.json.md5" ]]; then
# # "$ROOT_DIR/update-package-md5.sh"
# # fi
# # Add modules on top of main.js:
# # - cache-require-paths to cache require() calls
# # - app-module-path so that lib/something paths can be resolved.
# #PACKAGE_MD5=$(cat "$ROOT_DIR/package.json.md5")
# MAIN_PATH="$ROOT_DIR/build/main.js"
# #LINE_TO_ADD="var osTmpdir = require('os-tmpdir'); process.env.CACHE_REQUIRE_PATHS_FILE = osTmpdir() + '/joplin-module-path-cache-$PACKAGE_MD5'; require('cache-require-paths'); require('app-module-path').addPath(__dirname);"
# LINE_TO_ADD="require('app-module-path').addPath(__dirname);"
# RESULT="$(grep "$LINE_TO_ADD" "$MAIN_PATH")"
# if [[ -z "$RESULT" ]]; then
# echo "Adding extra modules..."
# sed -i "2i $LINE_TO_ADD" "$MAIN_PATH"
# else
# echo "Extra modules already added."
# fi
# NODE_PATH="$ROOT_DIR/build" node "$ROOT_DIR/build/build-translation.js" --silent
# cd "$BUILD_DIR"
# node build-translation.js --silent

View File

@@ -562,18 +562,24 @@ msgstr ""
msgid "Rename notebook:"
msgstr ""
msgid "Set alarm:"
msgstr ""
msgid "Layout"
msgstr ""
msgid "Add or remove tags"
msgstr ""
msgid "Switch between note and to-do"
msgid "Switch between note and to-do type"
msgstr ""
msgid "Delete"
msgstr ""
msgid "Delete notes?"
msgstr ""
msgid "No notes in here. Create one by clicking on \"New note\"."
msgstr ""
@@ -584,18 +590,21 @@ msgstr ""
msgid "Attach file"
msgstr ""
msgid "Set alarm"
msgstr ""
msgid "Refresh"
msgstr ""
msgid "Clear"
msgstr ""
msgid "OneDrive Login"
msgstr ""
msgid "Import"
msgstr ""
msgid "Configuration"
msgstr ""
msgid "Delete notebook?"
msgstr ""
@@ -628,6 +637,15 @@ msgstr ""
msgid "Unknown flag: %s"
msgstr ""
msgid "File system"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -642,8 +660,12 @@ msgid ""
msgstr ""
msgid ""
"Please set the \"sync.2.path\" config value to the desired synchronisation "
"destination."
"Could not synchronize with OneDrive.\n"
"\n"
"This error often happens when using OneDrive for Business, which "
"unfortunately cannot be supported.\n"
"\n"
"Please consider using a regular OneDrive account."
msgstr ""
#, javascript-format
@@ -714,10 +736,6 @@ msgstr ""
msgid "Cannot move note to \"%s\" notebook"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
msgid "File system synchronisation target directory"
msgstr ""
@@ -726,20 +744,6 @@ msgid ""
"See `sync.target`."
msgstr ""
msgid "Synchronisation target"
msgstr ""
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
msgstr ""
msgid "File system"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "Text editor"
msgstr ""
@@ -751,6 +755,12 @@ msgstr ""
msgid "Language"
msgstr ""
msgid "Date format"
msgstr ""
msgid "Time format"
msgstr ""
msgid "Theme"
msgstr ""
@@ -784,9 +794,24 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
msgid "Synchronisation target"
msgstr ""
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
msgid "Sync status (synced items / total items)"
msgstr ""
@@ -813,15 +838,44 @@ msgstr ""
msgid "%s: %d notes"
msgstr ""
msgid "Coming alarms"
msgstr ""
#, javascript-format
msgid "On %s: %s"
msgstr ""
msgid "There are currently no notes. Create one by clicking on the (+) button."
msgstr ""
msgid "Delete these notes?"
msgstr ""
msgid "Log"
msgstr ""
msgid "Status"
msgstr ""
msgid "Export Debug Report"
msgstr ""
msgid "Configuration"
msgstr ""
msgid "Move to notebook..."
msgstr ""
#, javascript-format
msgid "Move %d notes to notebook \"%s\"?"
msgstr ""
msgid "Select date"
msgstr ""
msgid "Confirm"
msgstr ""
msgid "Cancel synchronisation"
msgstr ""
@@ -845,16 +899,13 @@ msgstr ""
msgid "Unsupported image type: %s"
msgstr ""
msgid "Attach image"
msgid "Attach photo"
msgstr ""
msgid "Attach any other file"
msgid "Attach any file"
msgstr ""
msgid "Delete note"
msgstr ""
msgid "Convert to regular note"
msgid "Convert to note"
msgstr ""
msgid "Convert to todo"
@@ -866,7 +917,7 @@ msgstr ""
msgid "Show metadata"
msgstr ""
msgid "View location on map"
msgid "View on map"
msgstr ""
msgid "Delete notebook"

1002
CliClient/locales/es_CR.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -614,18 +614,25 @@ msgstr "Séparez chaque étiquette par une virgule."
msgid "Rename notebook:"
msgstr "Renommer le carnet :"
#, fuzzy
msgid "Set alarm:"
msgstr "Définir ou modifier alarme"
msgid "Layout"
msgstr "Disposition"
msgid "Add or remove tags"
msgstr "Gérer les étiquettes"
msgid "Switch between note and to-do"
msgid "Switch between note and to-do type"
msgstr "Alterner entre note et tâche"
msgid "Delete"
msgstr "Supprimer"
msgid "Delete notes?"
msgstr "Supprimer les notes ?"
msgid "No notes in here. Create one by clicking on \"New note\"."
msgstr ""
"Pas de notes ici. Créez-en une en pressant le bouton \"Nouvelle note\"."
@@ -637,18 +644,22 @@ msgstr "Lien ou message non géré : %s"
msgid "Attach file"
msgstr "Attacher un fichier"
#, fuzzy
msgid "Set alarm"
msgstr "Définir ou modifier alarme"
msgid "Refresh"
msgstr "Rafraîchir"
msgid "Clear"
msgstr "Supprimer"
msgid "OneDrive Login"
msgstr "Connexion OneDrive"
msgid "Import"
msgstr "Importer"
msgid "Configuration"
msgstr "Configuration"
msgid "Delete notebook?"
msgstr "Supprimer le carnet ?"
@@ -681,6 +692,15 @@ msgstr "Utilisation : %s"
msgid "Unknown flag: %s"
msgstr "Paramètre inconnu : %s"
msgid "File system"
msgstr "Système de fichier"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dév (Pour tester uniquement)"
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Paramètre inconnu : %s"
@@ -697,11 +717,13 @@ msgstr ""
"synchronisation à nouveau pour corriger le problème."
msgid ""
"Please set the \"sync.2.path\" config value to the desired synchronisation "
"destination."
"Could not synchronize with OneDrive.\n"
"\n"
"This error often happens when using OneDrive for Business, which "
"unfortunately cannot be supported.\n"
"\n"
"Please consider using a regular OneDrive account."
msgstr ""
"Veuillez attribuer une valeur au paramètre de configuration \"sync.2.path\" "
"pour indiquer le dossier où devra se faire la synchronisation."
#, javascript-format
msgid "Cannot access %s"
@@ -771,10 +793,6 @@ msgstr "Impossible de copier la note vers le carnet \"%s\""
msgid "Cannot move note to \"%s\" notebook"
msgstr "Impossible de déplacer la note vers le carnet \"%s\""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
msgid "File system synchronisation target directory"
msgstr "Cible de la synchronisation sur le disque dur"
@@ -785,22 +803,6 @@ msgstr ""
"Le chemin du répertoire avec lequel synchroniser lorsque la synchronisation "
"par système de fichier est activée. Voir `sync.target`."
msgid "Synchronisation target"
msgstr "Cible de la synchronisation"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
msgstr ""
"La cible avec laquelle synchroniser. Pour synchroniser avec le système de "
"fichier, veuillez spécifier le répertoire avec `sync.2.path`."
msgid "File system"
msgstr "Système de fichier"
msgid "OneDrive"
msgstr "OneDrive"
msgid "Text editor"
msgstr "Éditeur de texte"
@@ -814,6 +816,12 @@ msgstr ""
msgid "Language"
msgstr "Langue"
msgid "Date format"
msgstr "Format de la date"
msgid "Time format"
msgstr "Format de l'heure"
msgid "Theme"
msgstr "Apparence"
@@ -847,9 +855,26 @@ msgstr "%d heure"
msgid "%d hours"
msgstr "%d heures"
msgid "Automatically update the application"
msgstr "Mettre à jour le logiciel automatiquement"
msgid "Show advanced options"
msgstr "Montrer les options avancées"
msgid "Synchronisation target"
msgstr "Cible de la synchronisation"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
msgstr ""
"La cible avec laquelle synchroniser. Pour synchroniser avec le système de "
"fichier, veuillez spécifier le répertoire avec `sync.2.path`."
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
msgid "Sync status (synced items / total items)"
msgstr "Status de la synchronisation (objets synchro. / total)"
@@ -876,17 +901,46 @@ msgstr "Carnets"
msgid "%s: %d notes"
msgstr "%s : %d notes"
msgid "Coming alarms"
msgstr "Alarmes à venir"
#, javascript-format
msgid "On %s: %s"
msgstr "Le %s : %s"
msgid "There are currently no notes. Create one by clicking on the (+) button."
msgstr ""
"Ce carnet ne contient aucune note. Créez-en une en appuyant sur le bouton "
"(+)."
msgid "Delete these notes?"
msgstr "Supprimer ces notes ?"
msgid "Log"
msgstr "Journal"
msgid "Status"
msgstr "État"
msgid "Export Debug Report"
msgstr "Exporter rapport de débogage"
msgid "Configuration"
msgstr "Configuration"
msgid "Move to notebook..."
msgstr "Déplacer la note vers carnet..."
#, javascript-format
msgid "Move %d notes to notebook \"%s\"?"
msgstr "Déplacer %d notes vers carnet \"%s\" ?"
msgid "Select date"
msgstr "Sélectionner date"
msgid "Confirm"
msgstr "Confirmer"
msgid "Cancel synchronisation"
msgstr "Annuler synchronisation"
@@ -910,16 +964,13 @@ msgstr "Ignorer les changements"
msgid "Unsupported image type: %s"
msgstr "Type d'image non géré : %s"
msgid "Attach image"
msgstr "Joindre une image ou photo"
msgid "Attach photo"
msgstr "Attacher une photo"
msgid "Attach any other file"
msgstr "Joindre un fichier"
msgid "Attach any file"
msgstr "Attacher un fichier"
msgid "Delete note"
msgstr "Supprimer la note"
msgid "Convert to regular note"
msgid "Convert to note"
msgstr "Convertir en note"
msgid "Convert to todo"
@@ -931,8 +982,8 @@ msgstr "Cacher les métadonnées"
msgid "Show metadata"
msgstr "Afficher les métadonnées"
msgid "View location on map"
msgstr "Voir l'emplacement sur la carte"
msgid "View on map"
msgstr "Voir emplacement sur carte"
msgid "Delete notebook"
msgstr "Supprimer le carnet"
@@ -955,6 +1006,31 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenue"
#~ msgid "Set or clear alarm:"
#~ msgstr "Définir ou modifier alarme :"
#~ msgid "Set or clear alarm"
#~ msgstr "Définir ou modifier alarme"
#~ msgid "Attach image"
#~ msgstr "Joindre une image ou photo"
#~ msgid "Attach any other file"
#~ msgstr "Joindre un fichier"
#~ msgid "Delete note"
#~ msgstr "Supprimer la note"
#~ msgid "Convert to regular note"
#~ msgstr "Convertir en note"
#~ msgid ""
#~ "Please set the \"sync.2.path\" config value to the desired "
#~ "synchronisation destination."
#~ msgstr ""
#~ "Veuillez attribuer une valeur au paramètre de configuration \"sync.2.path"
#~ "\" pour indiquer le dossier où devra se faire la synchronisation."
#~ msgid "Seach:"
#~ msgstr "Chercher :"
@@ -1009,10 +1085,6 @@ msgstr "Bienvenue"
#~ msgid "Done editing."
#~ msgstr "Edition terminée."
#, fuzzy
#~ msgid "Confirm"
#~ msgstr "Conflits"
#~ msgid "Last error: %s (stacktrace in log)."
#~ msgstr "Dernière erreur : %s (Plus d'information dans le journal d'erreurs)"

View File

@@ -562,18 +562,24 @@ msgstr ""
msgid "Rename notebook:"
msgstr ""
msgid "Set alarm:"
msgstr ""
msgid "Layout"
msgstr ""
msgid "Add or remove tags"
msgstr ""
msgid "Switch between note and to-do"
msgid "Switch between note and to-do type"
msgstr ""
msgid "Delete"
msgstr ""
msgid "Delete notes?"
msgstr ""
msgid "No notes in here. Create one by clicking on \"New note\"."
msgstr ""
@@ -584,18 +590,21 @@ msgstr ""
msgid "Attach file"
msgstr ""
msgid "Set alarm"
msgstr ""
msgid "Refresh"
msgstr ""
msgid "Clear"
msgstr ""
msgid "OneDrive Login"
msgstr ""
msgid "Import"
msgstr ""
msgid "Configuration"
msgstr ""
msgid "Delete notebook?"
msgstr ""
@@ -628,6 +637,15 @@ msgstr ""
msgid "Unknown flag: %s"
msgstr ""
msgid "File system"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -642,8 +660,12 @@ msgid ""
msgstr ""
msgid ""
"Please set the \"sync.2.path\" config value to the desired synchronisation "
"destination."
"Could not synchronize with OneDrive.\n"
"\n"
"This error often happens when using OneDrive for Business, which "
"unfortunately cannot be supported.\n"
"\n"
"Please consider using a regular OneDrive account."
msgstr ""
#, javascript-format
@@ -714,10 +736,6 @@ msgstr ""
msgid "Cannot move note to \"%s\" notebook"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
msgid "File system synchronisation target directory"
msgstr ""
@@ -726,20 +744,6 @@ msgid ""
"See `sync.target`."
msgstr ""
msgid "Synchronisation target"
msgstr ""
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
msgstr ""
msgid "File system"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "Text editor"
msgstr ""
@@ -751,6 +755,12 @@ msgstr ""
msgid "Language"
msgstr ""
msgid "Date format"
msgstr ""
msgid "Time format"
msgstr ""
msgid "Theme"
msgstr ""
@@ -784,9 +794,24 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
msgid "Synchronisation target"
msgstr ""
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
msgid "Sync status (synced items / total items)"
msgstr ""
@@ -813,15 +838,44 @@ msgstr ""
msgid "%s: %d notes"
msgstr ""
msgid "Coming alarms"
msgstr ""
#, javascript-format
msgid "On %s: %s"
msgstr ""
msgid "There are currently no notes. Create one by clicking on the (+) button."
msgstr ""
msgid "Delete these notes?"
msgstr ""
msgid "Log"
msgstr ""
msgid "Status"
msgstr ""
msgid "Export Debug Report"
msgstr ""
msgid "Configuration"
msgstr ""
msgid "Move to notebook..."
msgstr ""
#, javascript-format
msgid "Move %d notes to notebook \"%s\"?"
msgstr ""
msgid "Select date"
msgstr ""
msgid "Confirm"
msgstr ""
msgid "Cancel synchronisation"
msgstr ""
@@ -845,16 +899,13 @@ msgstr ""
msgid "Unsupported image type: %s"
msgstr ""
msgid "Attach image"
msgid "Attach photo"
msgstr ""
msgid "Attach any other file"
msgid "Attach any file"
msgstr ""
msgid "Delete note"
msgstr ""
msgid "Convert to regular note"
msgid "Convert to note"
msgstr ""
msgid "Convert to todo"
@@ -866,7 +917,7 @@ msgstr ""
msgid "Show metadata"
msgstr ""
msgid "View location on map"
msgid "View on map"
msgstr ""
msgid "Delete notebook"

View File

@@ -1,6 +1,6 @@
{
"name": "joplin",
"version": "0.10.71",
"version": "0.10.77",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -797,6 +797,12 @@
"highlight.js": "9.12.0"
}
},
"marked": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz",
"integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=",
"dev": true
},
"md5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",

View File

@@ -18,7 +18,7 @@
],
"owner": "Laurent Cozic"
},
"version": "0.10.71",
"version": "0.10.77",
"bin": {
"joplin": "./main.js"
},
@@ -62,6 +62,7 @@
"devDependencies": {
"gettext-parser": "^1.2.2",
"jasmine": "^2.6.0",
"marked": "^0.3.6",
"mustache": "^2.3.0"
},
"scripts": {

View File

@@ -1,40 +0,0 @@
const { time } = require('lib/time-utils.js');
const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient } = require('test-utils.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { Setting } = require('lib/models/setting.js');
const { BaseItem } = require('lib/models/base-item.js');
const { BaseModel } = require('lib/base-model.js');
process.on('unhandledRejection', (reason, p) => {
console.error('Unhandled promise rejection at: Promise', p, 'reason:', reason);
});
describe('BaseItem', function() {
beforeEach( async (done) => {
await setupDatabaseAndSynchronizer(1);
switchClient(1);
done();
});
it('should create a deleted_items record', async (done) => {
let folder1 = await Folder.save({ title: 'folder1' });
let folder2 = await Folder.save({ title: 'folder2' });
await Folder.delete(folder1.id);
let items = await BaseItem.deletedItems();
expect(items.length).toBe(1);
expect(items[0].item_id).toBe(folder1.id);
expect(items[0].item_type).toBe(folder1.type_);
let folders = await Folder.all();
expect(folders.length).toBe(1);
done();
});
});

View File

@@ -9,6 +9,7 @@ const { Database } = require('lib/database.js');
const { Setting } = require('lib/models/setting.js');
const { BaseItem } = require('lib/models/base-item.js');
const { BaseModel } = require('lib/base-model.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -37,7 +38,7 @@ async function localItemsSameAsRemote(locals, expect) {
expect(!!remote).toBe(true);
if (!remote) continue;
if (syncTargetId() == Setting.SYNC_TARGET_FILESYSTEM) {
if (syncTargetId() == SyncTargetRegistry.nameToId('filesystem')) {
expect(remote.updated_time).toBe(Math.floor(dbItem.updated_time / 1000) * 1000);
} else {
expect(remote.updated_time).toBe(dbItem.updated_time);
@@ -606,5 +607,25 @@ describe('Synchronizer', function() {
done();
});
it('items should be downloaded again when user cancels in the middle of delta operation', async (done) => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
await synchronizer().start();
await switchClient(2);
synchronizer().debugFlags_ = ['cancelDeltaLoop2'];
let context = await synchronizer().start();
let notes = await Note.all();
expect(notes.length).toBe(0);
synchronizer().debugFlags_ = [];
await synchronizer().start({ context: context });
notes = await Note.all();
expect(notes.length).toBe(1);
done();
});
});

View File

@@ -16,6 +16,10 @@ const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
const { time } = require('lib/time-utils.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetMemory = require('lib/SyncTargetMemory.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
let databases_ = [];
let synchronizers_ = [];
@@ -29,12 +33,14 @@ Resource.fsDriver_ = fsDriver;
const logDir = __dirname + '/../tests/logs';
fs.mkdirpSync(logDir, 0o755);
const syncTargetId_ = Setting.SYNC_TARGET_MEMORY;
//const syncTargetId_ = Setting.SYNC_TARGET_FILESYSTEM;
//const syncTargetId_ = Setting.SYNC_TARGET_ONEDRIVE;
SyncTargetRegistry.addClass(SyncTargetMemory);
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
const syncDir = __dirname + '/../tests/sync';
const sleepTime = syncTargetId_ == Setting.SYNC_TARGET_FILESYSTEM ? 1001 : 400;
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400;
const logger = new Logger();
logger.addTarget('file', { path: logDir + '/log.txt' });
@@ -121,11 +127,14 @@ async function setupDatabaseAndSynchronizer(id = null) {
await setupDatabase(id);
if (!synchronizers_[id]) {
synchronizers_[id] = new Synchronizer(db(id), fileApi(), Setting.value('appType'));
synchronizers_[id].setLogger(logger);
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
const syncTarget = new SyncTargetClass(db(id));
syncTarget.setFileApi(fileApi());
syncTarget.setLogger(logger);
synchronizers_[id] = await syncTarget.synchronizer();
}
if (syncTargetId_ == Setting.SYNC_TARGET_FILESYSTEM) {
if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) {
fs.removeSync(syncDir)
fs.mkdirpSync(syncDir, 0o755);
} else {
@@ -146,13 +155,12 @@ function synchronizer(id = null) {
function fileApi() {
if (fileApi_) return fileApi_;
if (syncTargetId_ == Setting.SYNC_TARGET_FILESYSTEM) {
if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) {
fs.removeSync(syncDir)
fs.mkdirpSync(syncDir, 0o755);
fileApi_ = new FileApi(syncDir, new FileApiDriverLocal());
} else if (syncTargetId_ == Setting.SYNC_TARGET_MEMORY) {
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('memory')) {
fileApi_ = new FileApi('/root', new FileApiDriverMemory());
fileApi_.setLogger(logger);
}
// } else if (syncTargetId == Setting.SYNC_TARGET_ONEDRIVE) {
// let auth = require('./onedrive-auth.json');

View File

@@ -1,4 +1,5 @@
app/node_modules/
app/packageInfo.js
dist/
app/lib/
app/gui/*.min.js

View File

@@ -51,7 +51,8 @@ class ElectronAppWrapper {
slashes: true
}))
//if (this.env_ === 'dev') this.win_.webContents.openDevTools();
// Uncomment this to view errors if the application does not start
if (this.env_ === 'dev') this.win_.webContents.openDevTools();
this.win_.on('close', (event) => {
if (this.willQuitApp_ || process.platform !== 'darwin') {

View File

@@ -8,7 +8,6 @@ const { BaseModel } = require('lib/base-model.js');
const { _, setLocale } = require('lib/locale.js');
const os = require('os');
const fs = require('fs-extra');
const { Logger } = require('lib/logger.js');
const { Tag } = require('lib/models/tag.js');
const { reg } = require('lib/registry.js');
const { sprintf } = require('sprintf-js');
@@ -16,6 +15,9 @@ const { JoplinDatabase } = require('lib/joplin-database.js');
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
const { ElectronAppWrapper } = require('./ElectronAppWrapper');
const { defaultState } = require('lib/reducer.js');
const packageInfo = require('./packageInfo.js');
const AlarmService = require('lib/services/AlarmService.js');
const AlarmServiceDriverNode = require('lib/services/AlarmServiceDriverNode');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
@@ -133,7 +135,11 @@ class Application extends BaseApplication {
}
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (!await reg.syncStarted()) reg.scheduleSync();
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync();
}
if (['EVENT_NOTE_ALARM_FIELD_CHANGE', 'NOTE_DELETE'].indexOf(action.type) >= 0) {
await AlarmService.updateNoteNotification(action.id, action.type === 'NOTE_DELETE');
}
const result = await super.generalMiddleware(store, next, action);
@@ -223,21 +229,22 @@ class Application extends BaseApplication {
label: _('Edit'),
submenu: [{
label: _('Copy'),
screens: ['Main'],
screens: ['Main', 'OneDriveLogin'],
role: 'copy',
accelerator: 'CommandOrControl+C',
}, {
label: _('Cut'),
screens: ['Main'],
role: 'copy',
screens: ['Main', 'OneDriveLogin'],
role: 'cut',
accelerator: 'CommandOrControl+X',
}, {
label: _('Paste'),
screens: ['Main'],
role: 'copy',
screens: ['Main', 'OneDriveLogin'],
role: 'paste',
accelerator: 'CommandOrControl+V',
}, {
type: 'separator',
screens: ['Main'],
}, {
label: _('Search in all the notes'),
screens: ['Main'],
@@ -269,7 +276,7 @@ class Application extends BaseApplication {
}, {
label: _('About Joplin'),
click: () => {
const p = require('./package.json');
const p = packageInfo;
let message = [
p.description,
'',
@@ -284,12 +291,21 @@ class Application extends BaseApplication {
},
];
function isEmptyMenu(template) {
for (let i = 0; i < template.length; i++) {
const t = template[i];
if (t.type !== 'separator') return false;
}
return true;
}
function removeUnwantedItems(template, screen) {
let output = [];
for (let i = 0; i < template.length; i++) {
const t = Object.assign({}, template[i]);
if (t.screens && t.screens.indexOf(screen) < 0) continue;
if (t.submenu) t.submenu = removeUnwantedItems(t.submenu, screen);
if (('submenu' in t) && isEmptyMenu(t.submenu)) continue;
output.push(t);
}
return output;
@@ -306,6 +322,11 @@ class Application extends BaseApplication {
async start(argv) {
argv = await super.start(argv);
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
AlarmService.setLogger(reg.logger());
reg.setShowErrorMessageBoxHandler((message) => { bridge().showErrorMessageBox(message) });
if (Setting.value('openDevTools')) {
bridge().window().webContents.openDevTools();
}
@@ -337,13 +358,29 @@ class Application extends BaseApplication {
// but then doesn't install it on exit.
if (shim.isWindows() || shim.isMac()) {
const runAutoUpdateCheck = function() {
bridge().checkForUpdatesAndNotify(Setting.value('profileDir') + '/log-autoupdater.txt');
if (Setting.value('autoUpdateEnabled')) {
bridge().checkForUpdatesAndNotify(Setting.value('profileDir') + '/log-autoupdater.txt');
}
}
setTimeout(() => { runAutoUpdateCheck() }, 5000);
// For those who leave the app always open
setInterval(() => { runAutoUpdateCheck() }, 2 * 60 * 60 * 1000);
}
setTimeout(() => {
AlarmService.garbageCollect();
}, 1000 * 60 * 60);
if (Setting.value('env') === 'dev') {
AlarmService.updateAllNotifications();
} else {
reg.scheduleSync().then(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
AlarmService.updateAllNotifications();
});
}
}
}

View File

@@ -1,10 +1,13 @@
const { _ } = require('lib/locale.js');
const { dirname } = require('lib/path-utils.js');
const { Logger } = require('lib/logger.js');
class Bridge {
constructor(electronWrapper) {
this.electronWrapper_ = electronWrapper;
this.autoUpdateLogger_ = null;
this.lastSelectedPath_ = null;
}
electronApp() {
@@ -38,7 +41,13 @@ class Bridge {
showOpenDialog(options) {
const {dialog} = require('electron');
return dialog.showOpenDialog(options);
if (!options) options = {};
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
const filePaths = dialog.showOpenDialog(options);
if (filePaths && filePaths.length) {
this.lastSelectedPath_ = dirname(filePaths[0]);
}
return filePaths;
}
showMessageBox(options) {
@@ -78,17 +87,21 @@ class Bridge {
return require('electron').shell.openItem(fullPath)
}
checkForUpdatesAndNotify(logFilePath) {
async checkForUpdatesAndNotify(logFilePath) {
if (!this.autoUpdater_) {
const logger = new Logger();
logger.addTarget('file', { path: logFilePath });
logger.setLevel(Logger.LEVEL_DEBUG);
logger.info('checkForUpdatesAndNotify: Intializing...');
this.autoUpdateLogger_ = new Logger();
this.autoUpdateLogger_.addTarget('file', { path: logFilePath });
this.autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
this.autoUpdateLogger_.info('checkForUpdatesAndNotify: Initializing...');
this.autoUpdater_ = require("electron-updater").autoUpdater;
this.autoUpdater_.logger = logger;
this.autoUpdater_.logger = this.autoUpdateLogger_;
}
return this.autoUpdater_.checkForUpdatesAndNotify();
try {
await this.autoUpdater_.checkForUpdatesAndNotify();
} catch (error) {
this.autoUpdateLogger_.error(error);
}
}
}

View File

@@ -0,0 +1,23 @@
const fs = require('fs-extra');
// Electron Builder strip off certain important keys from package.json, which we need, in particular build.appId
// so this script is used to preserve the keys that we need.
const packageInfo = require(__dirname + '/package.json');
let removeKeys = ['scripts', 'devDependencies', 'optionalDependencies', 'dependencies'];
for (let i = 0; i < removeKeys.length; i++) {
delete packageInfo[removeKeys[i]];
}
const appId = packageInfo.build.appId;
delete packageInfo.build;
packageInfo.build = { appId: appId };
let fileContent = "// Auto-generated by compile-package-info.js\n// Do not change directly\nconst packageInfo = " + JSON.stringify(packageInfo, null, 4) + ';';
fileContent += "\n";
fileContent += "module.exports = packageInfo;";
fs.writeFileSync(__dirname + '/packageInfo.js', fileContent);

View File

@@ -0,0 +1,25 @@
const events = require('events');
class EventManager {
constructor() {
this.emitter_ = new events.EventEmitter();
}
on(eventName, callback) {
return this.emitter_.on(eventName, callback);
}
emit(eventName, object = null) {
return this.emitter_.emit(eventName, object);
}
removeListener(eventName, callback) {
return this.emitter_.removeListener(eventName, callback);
}
}
const eventManager = new EventManager();
module.exports = eventManager;

View File

@@ -31,6 +31,8 @@ class ConfigScreenComponent extends React.Component {
Setting.setValue(key, value);
}
// Component key needs to be key+value otherwise it doesn't update when the settings change.
const md = Setting.settingMetadata(key);
if (md.isEnum) {
@@ -42,7 +44,7 @@ class ConfigScreenComponent extends React.Component {
}
return (
<div key={key} style={rowStyle}>
<div key={key+value} style={rowStyle}>
<div style={labelStyle}><label>{md.label()}</label></div>
<select value={value} style={controlStyle} onChange={(event) => { updateSettingValue(key, event.target.value) }}>
{items}
@@ -51,7 +53,7 @@ class ConfigScreenComponent extends React.Component {
);
} else if (md.type === Setting.TYPE_BOOL) {
return (
<div key={key} style={rowStyle}>
<div key={key+value} style={rowStyle}>
<div style={controlStyle}>
<label><input type="checkbox" defaultChecked={!!value} onChange={(event) => { updateSettingValue(key, !!event.target.checked) }}/><span style={labelStyle}> {md.label()}</span></label>
</div>

View File

@@ -41,7 +41,7 @@ class HeaderComponent extends React.Component {
}
render() {
const style = this.props.style;
const style = Object.assign({}, this.props.style);
const theme = themeStyle(this.props.theme);
const showBackButton = this.props.showBackButton === undefined || this.props.showBackButton === true;
style.height = theme.headerHeight;

View File

@@ -15,6 +15,7 @@ const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const layoutUtils = require('lib/layout-utils.js');
const { bridge } = require('electron').remote.require('./bridge');
const eventManager = require('../eventManager');
class MainScreenComponent extends React.Component {
@@ -175,6 +176,43 @@ class MainScreenComponent extends React.Component {
id: searchId,
});
}
this.setState({ promptOptions: null });
}
},
});
} else if (command.name === 'editAlarm') {
const note = await Note.load(command.noteId);
let defaultDate = new Date(Date.now() + 2 * 3600 * 1000);
defaultDate.setMinutes(0);
defaultDate.setSeconds(0);
this.setState({
promptOptions: {
label: _('Set alarm:'),
inputType: 'datetime',
buttons: ['ok', 'cancel', 'clear'],
value: note.todo_due ? new Date(note.todo_due) : defaultDate,
onClose: async (answer, buttonType) => {
let newNote = null;
if (buttonType === 'clear') {
newNote = {
id: note.id,
todo_due: 0,
};
} else if (answer !== null) {
newNote = {
id: note.id,
todo_due: answer.getTime(),
};
}
if (newNote) {
await Note.save(newNote);
eventManager.emit('alarmChange', { noteId: note.id });
}
this.setState({ promptOptions: null });
}
},
@@ -191,44 +229,58 @@ class MainScreenComponent extends React.Component {
}
}
styles(themeId, width, height) {
const styleKey = themeId + '_' + width + '_' + height;
if (styleKey === this.styleKey_) return this.styles_;
const theme = themeStyle(themeId);
this.styleKey_ = styleKey;
this.styles_ = {};
const rowHeight = height - theme.headerHeight;
this.styles_.header = {
width: width,
};
this.styles_.sideBar = {
width: Math.floor(layoutUtils.size(width * .2, 150, 300)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
this.styles_.noteList = {
width: Math.floor(layoutUtils.size(width * .2, 150, 300)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
this.styles_.noteText = {
width: Math.floor(layoutUtils.size(width - this.styles_.sideBar.width - this.styles_.noteList.width, 0)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
this.styles_.prompt = {
width: width,
height: height,
};
return this.styles_;
}
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
const promptOptions = this.state.promptOptions;
const folders = this.props.folders;
const notes = this.props.notes;
const headerStyle = {
width: style.width,
};
const rowHeight = style.height - theme.headerHeight;
const sideBarStyle = {
width: Math.floor(layoutUtils.size(style.width * .2, 150, 300)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
const noteListStyle = {
width: Math.floor(layoutUtils.size(style.width * .2, 150, 300)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
const noteTextStyle = {
width: Math.floor(layoutUtils.size(style.width - sideBarStyle.width - noteListStyle.width, 0)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
const promptStyle = {
width: style.width,
height: style.height,
};
const styles = this.styles(this.props.theme, style.width, style.height);
const headerButtons = [];
@@ -267,20 +319,29 @@ class MainScreenComponent extends React.Component {
},
});
if (!this.promptOnClose_) {
this.promptOnClose_ = (answer, buttonType) => {
return this.state.promptOptions.onClose(answer, buttonType);
}
}
return (
<div style={style}>
<PromptDialog
value={promptOptions && promptOptions.value ? promptOptions.value : ''}
autocomplete={promptOptions && ('autocomplete' in promptOptions) ? promptOptions.autocomplete : null}
defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''}
theme={this.props.theme}
style={promptStyle}
onClose={(answer) => promptOptions.onClose(answer)}
style={styles.prompt}
onClose={this.promptOnClose_}
label={promptOptions ? promptOptions.label : ''}
description={promptOptions ? promptOptions.description : null}
visible={!!this.state.promptOptions} />
<Header style={headerStyle} showBackButton={false} buttons={headerButtons} />
<SideBar style={sideBarStyle} />
<NoteList style={noteListStyle} />
<NoteText style={noteTextStyle} visiblePanes={this.props.noteVisiblePanes} />
visible={!!this.state.promptOptions}
buttons={promptOptions && ('buttons' in promptOptions) ? promptOptions.buttons : null}
inputType={promptOptions && ('inputType' in promptOptions) ? promptOptions.inputType : null} />
<Header style={styles.header} showBackButton={false} buttons={headerButtons} />
<SideBar style={styles.sideBar} />
<NoteList style={styles.noteList} />
<NoteText style={styles.noteText} visiblePanes={this.props.noteVisiblePanes} />
</div>
);
}

View File

@@ -7,6 +7,7 @@ const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const eventManager = require('../eventManager');
class NoteListComponent extends React.Component {
@@ -52,28 +53,31 @@ class NoteListComponent extends React.Component {
}
itemContextMenu(event) {
const noteId = event.target.getAttribute('data-id');
if (!noteId) throw new Error('No data-id on element');
const noteIds = this.props.selectedNoteIds;
if (!noteIds.length) return;
const menu = new Menu()
menu.append(new MenuItem({label: _('Add or remove tags'), click: async () => {
menu.append(new MenuItem({label: _('Add or remove tags'), enabled: noteIds.length === 1, click: async () => {
this.props.dispatch({
type: 'WINDOW_COMMAND',
name: 'setTags',
noteId: noteId,
noteId: noteIds[0],
});
}}));
menu.append(new MenuItem({label: _('Switch between note and to-do'), click: async () => {
const note = await Note.load(noteId);
await Note.save(Note.toggleIsTodo(note));
menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => {
for (let i = 0; i < noteIds.length; i++) {
const note = await Note.load(noteIds[i]);
await Note.save(Note.toggleIsTodo(note));
eventManager.emit('noteTypeToggle', { noteId: note.id });
}
}}));
menu.append(new MenuItem({label: _('Delete'), click: async () => {
const ok = bridge().showConfirmMessageBox(_('Delete note?'));
const ok = bridge().showConfirmMessageBox(noteIds.length > 1 ? _('Delete notes?') : _('Delete note?'));
if (!ok) return;
await Note.delete(noteId);
await Note.batchDelete(noteIds);
}}));
menu.popup(bridge().window());
@@ -81,10 +85,33 @@ class NoteListComponent extends React.Component {
itemRenderer(item, theme, width) {
const onTitleClick = async (event, item) => {
this.props.dispatch({
type: 'NOTE_SELECT',
id: item.id,
});
if (event.ctrlKey) {
event.preventDefault();
this.props.dispatch({
type: 'NOTE_SELECT_TOGGLE',
id: item.id,
});
} else if (event.shiftKey) {
event.preventDefault();
this.props.dispatch({
type: 'NOTE_SELECT_EXTEND',
id: item.id,
});
} else {
this.props.dispatch({
type: 'NOTE_SELECT',
id: item.id,
});
}
}
const onDragStart = (event) => {
const noteIds = this.props.selectedNoteIds;
if (!noteIds.length) return;
event.dataTransfer.setDragImage(new Image(), 1, 1);
event.dataTransfer.clearData();
event.dataTransfer.setData('text/x-jop-note-ids', JSON.stringify(noteIds));
}
const onCheckboxClick = async (event) => {
@@ -94,12 +121,13 @@ class NoteListComponent extends React.Component {
todo_completed: checked ? time.unixMs() : 0,
}
await Note.save(newNote);
eventManager.emit('todoToggle', { noteId: item.id });
}
const hPadding = 10;
let style = Object.assign({ width: width }, this.style().listItem);
if (this.props.selectedNoteId === item.id) style = Object.assign(style, this.style().listItemSelected);
if (this.props.selectedNoteIds.indexOf(item.id) >= 0) style = Object.assign(style, this.style().listItemSelected);
// Setting marginBottom = 1 because it makes the checkbox looks more centered, at least on Windows
// but don't know how it will look in other OSes.
@@ -118,12 +146,13 @@ class NoteListComponent extends React.Component {
return <div key={item.id + '_' + item.todo_completed} style={style}>
{checkbox}
<a
data-id={item.id}
className="list-item"
onContextMenu={(event) => this.itemContextMenu(event)}
href="#"
draggable={true}
style={listItemTitleStyle}
onClick={(event) => { onTitleClick(event, item) }}
onDragStart={(event) => onDragStart(event) }
>
{item.title}
</a>
@@ -164,7 +193,7 @@ class NoteListComponent extends React.Component {
const mapStateToProps = (state) => {
return {
notes: state.notes,
selectedNoteId: state.selectedNoteId,
selectedNoteIds: state.selectedNoteIds,
theme: state.settings.theme,
// uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
};

View File

@@ -1,7 +1,9 @@
const React = require('react');
const { Note } = require('lib/models/note.js');
const { time } = require('lib/time-utils.js');
const { Setting } = require('lib/models/setting.js');
const { IconButton } = require('./IconButton.min.js');
const Toolbar = require('./Toolbar.min.js');
const { connect } = require('react-redux');
const { _ } = require('lib/locale.js');
const { reg } = require('lib/registry.js');
@@ -13,6 +15,7 @@ const AceEditor = require('react-ace').default;
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const { shim } = require('lib/shim.js');
const eventManager = require('../eventManager');
require('brace/mode/markdown');
// https://ace.c9.io/build/kitchen-sink.html
@@ -55,11 +58,18 @@ class NoteTextComponent extends React.Component {
this.restoreScrollTop_ = null;
}
}
this.onAlarmChange_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
this.onNoteTypeToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
this.onTodoToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
}
mdToHtml() {
if (this.mdToHtml_) return this.mdToHtml_;
this.mdToHtml_ = new MdToHtml({ supportsResourceLinks: true });
this.mdToHtml_ = new MdToHtml({
supportsResourceLinks: true,
resourceBaseUrl: 'file://' + Setting.value('resourceDir') + '/',
});
return this.mdToHtml_;
}
@@ -79,6 +89,10 @@ class NoteTextComponent extends React.Component {
});
this.lastLoadedNoteId_ = note ? note.id : null;
eventManager.on('alarmChange', this.onAlarmChange_);
eventManager.on('noteTypeToggle', this.onNoteTypeToggle_);
eventManager.on('todoToggle', this.onTodoToggle_);
}
componentWillUnmount() {
@@ -86,6 +100,10 @@ class NoteTextComponent extends React.Component {
this.mdToHtml_ = null;
this.destroyWebview();
eventManager.removeListener('alarmChange', this.onAlarmChange_);
eventManager.removeListener('noteTypeToggle', this.onNoteTypeToggle_);
eventManager.removeListener('todoToggle', this.onTodoToggle_);
}
async saveIfNeeded() {
@@ -106,13 +124,24 @@ class NoteTextComponent extends React.Component {
}, 500);
}
async reloadNote(props) {
this.mdToHtml_ = null;
async reloadNote(props, options = null) {
if (!options) options = {};
if (!('noReloadIfLocalChanges' in options)) options.noReloadIfLocalChanges = false;
const noteId = props.noteId;
this.lastLoadedNoteId_ = noteId;
const note = noteId ? await Note.load(noteId) : null;
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading
if (!options.noReloadIfLocalChanges && this.isModified()) return;
// If the note hasn't been changed, exit now
if (this.state.note && note) {
let diff = Note.diffObjects(this.state.note, note);
delete diff.type_;
if (!Object.getOwnPropertyNames(diff).length) return;
}
this.mdToHtml_ = null;
// If we are loading nothing (noteId == null), make sure to
// set webviewReady to false too because the webview component
@@ -141,7 +170,7 @@ class NoteTextComponent extends React.Component {
}
if ('syncStarted' in nextProps && !nextProps.syncStarted && !this.isModified()) {
await this.reloadNote(nextProps);
await this.reloadNote(nextProps, { noReloadIfLocalChanges: true });
}
}
@@ -305,6 +334,45 @@ class NoteTextComponent extends React.Component {
this.scheduleSave();
}
async commandAttachFile() {
const noteId = this.props.noteId;
if (!noteId) return;
const filePaths = bridge().showOpenDialog({
properties: ['openFile', 'createDirectory', 'multiSelections'],
});
if (!filePaths || !filePaths.length) return;
await this.saveIfNeeded();
let note = await Note.load(noteId);
for (let i = 0; i < filePaths.length; i++) {
const filePath = filePaths[i];
try {
reg.logger().info('Attaching ' + filePath);
note = await shim.attachFileToNote(note, filePath);
reg.logger().info('File was attached.');
this.setState({
note: Object.assign({}, note),
lastSavedNote: Object.assign({}, note),
});
} catch (error) {
reg.logger().error(error);
}
}
}
commandSetAlarm() {
const noteId = this.props.noteId;
if (!noteId) return;
this.props.dispatch({
type: 'WINDOW_COMMAND',
name: 'editAlarm',
noteId: noteId,
});
}
itemContextMenu(event) {
const noteId = this.props.noteId;
if (!noteId) return;
@@ -312,30 +380,26 @@ class NoteTextComponent extends React.Component {
const menu = new Menu()
menu.append(new MenuItem({label: _('Attach file'), click: async () => {
const filePaths = bridge().showOpenDialog({
properties: ['openFile', 'createDirectory'],
});
if (!filePaths || !filePaths.length) return;
return this.commandAttachFile();
}}));
await this.saveIfNeeded();
const note = await Note.load(noteId);
try {
reg.logger().info('Attaching ' + filePaths[0]);
const newNote = await shim.attachFileToNote(note, filePaths[0]);
reg.logger().info('File was attached.');
this.setState({
note: newNote,
lastSavedNote: Object.assign({}, newNote),
});
} catch (error) {
reg.logger().error(error);
}
menu.append(new MenuItem({label: _('Set alarm'), click: async () => {
return this.commandSetAlarm();
}}));
menu.popup(bridge().window());
}
// shouldComponentUpdate(nextProps, nextState) {
// //console.info('NEXT PROPS', JSON.stringify(nextProps));
// console.info('NEXT STATE ====================');
// for (var n in nextProps) {
// if (!nextProps.hasOwnProperty(n)) continue;
// console.info(n + ' = ' + (nextProps[n] === this.props[n]));
// }
// return true;
// }
render() {
const style = this.props.style;
const note = this.state.note;
@@ -367,7 +431,7 @@ class NoteTextComponent extends React.Component {
height: 30,
boxSizing: 'border-box',
marginTop: 10,
marginBottom: 10,
marginBottom: 0,
display: 'flex',
flexDirection: 'row',
};
@@ -383,7 +447,11 @@ class NoteTextComponent extends React.Component {
marginRight: rootStyle.paddingLeft,
};
const bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop;
const toolbarStyle = {
marginBottom: 10,
};
const bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - toolbarStyle.marginBottom;
const viewerStyle = {
width: Math.floor(innerWidth / 2),
@@ -437,6 +505,28 @@ class NoteTextComponent extends React.Component {
this.webview_.send('setHtml', html);
}
const toolbarItems = [];
toolbarItems.push({
title: _('Attach file'),
iconName: 'fa-paperclip',
onClick: () => { return this.commandAttachFile(); },
});
if (note.is_todo) {
toolbarItems.push({
title: Note.needAlarm(note) ? time.formatMsToLocal(note.todo_due) : _('Set alarm'),
iconName: 'fa-clock-o',
enabled: !note.todo_completed,
onClick: () => { return this.commandSetAlarm(); },
});
}
const toolbar = <Toolbar
style={toolbarStyle}
items={toolbarItems}
/>
const titleEditor = <input
type="text"
style={titleEditorStyle}
@@ -491,6 +581,7 @@ class NoteTextComponent extends React.Component {
{ titleEditor }
{ titleBarMenuButton }
</div>
{ toolbar }
{ editor }
{ viewer }
</div>
@@ -501,7 +592,7 @@ class NoteTextComponent extends React.Component {
const mapStateToProps = (state) => {
return {
noteId: state.selectedNoteId,
noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
folderId: state.selectedFolderId,
itemType: state.selectedItemType,
folders: state.folders,

View File

@@ -41,19 +41,22 @@ class OneDriveLoginScreenComponent extends React.Component {
const url = event.url;
if (this.authCode_) return;
if (url.indexOf(this.redirectUrl() + '?code=') !== 0) return;
let code = url.split('?code=');
this.authCode_ = code[1];
const urlParse = require('url').parse;
const parsedUrl = urlParse(url.trim(), true);
if (!('code' in parsedUrl.query)) return;
this.authCode_ = parsedUrl.query.code;
try {
await reg.oneDriveApi().execTokenRequest(this.authCode_, this.redirectUrl(), true);
await reg.syncTarget().api().execTokenRequest(this.authCode_, this.redirectUrl(), true);
this.props.dispatch({ type: 'NAV_BACK' });
reg.scheduleSync(0);
} catch (error) {
bridge().showMessageBox({
type: 'error',
message: error.message,
message: 'Could not login to OneDrive. Please try again.\n\n' + error.message + "\n\n" + url.match(/.{1,64}/g).join('\n'),
});
}
@@ -62,11 +65,11 @@ class OneDriveLoginScreenComponent extends React.Component {
}
startUrl() {
return reg.oneDriveApi().authCodeUrl(this.redirectUrl());
return reg.syncTarget().api().authCodeUrl(this.redirectUrl());
}
redirectUrl() {
return reg.oneDriveApi().nativeClientRedirectUrl();
return reg.syncTarget().api().nativeClientRedirectUrl();
}
render() {

View File

@@ -1,26 +1,29 @@
const React = require('react');
const { connect } = require('react-redux');
const { _ } = require('lib/locale.js');
const moment = require('moment');
const { themeStyle } = require('../theme.js');
const { time } = require('lib/time-utils.js');
const Datetime = require('react-datetime');
class PromptDialog extends React.Component {
componentWillMount() {
this.setState({
visible: false,
answer: this.props.value ? this.props.value : '',
answer: this.props.defaultValue ? this.props.defaultValue : '',
});
this.focusInput_ = true;
}
componentWillReceiveProps(newProps) {
if ('visible' in newProps) {
if ('visible' in newProps && newProps.visible !== this.props.visible) {
this.setState({ visible: newProps.visible });
if (newProps.visible) this.focusInput_ = true;
}
if ('value' in newProps) {
this.setState({ answer: newProps.value });
if ('defaultValue' in newProps && newProps.defaultValue !== this.props.defaultValue) {
this.setState({ answer: newProps.defaultValue });
}
}
@@ -29,37 +32,43 @@ class PromptDialog extends React.Component {
this.focusInput_ = false;
}
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
styles(themeId, width, height, visible) {
const styleKey = themeId + '_' + width + '_' + height + '_' + visible;
if (styleKey === this.styleKey_) return this.styles_;
const modalLayerStyle = {
const theme = themeStyle(themeId);
this.styleKey_ = styleKey;
this.styles_ = {};
this.styles_.modalLayer = {
zIndex: 9999,
position: 'absolute',
top: 0,
left: 0,
width: style.width,
height: style.height,
width: width,
height: height,
backgroundColor: 'rgba(0,0,0,0.6)',
display: this.state.visible ? 'flex' : 'none',
display: visible ? 'flex' : 'none',
alignItems: 'center',
justifyContent: 'center',
};
const promptDialogStyle = {
this.styles_.promptDialog = {
backgroundColor: 'white',
padding: 16,
display: 'inline-block',
boxShadow: '6px 6px 20px rgba(0,0,0,0.5)',
};
const buttonStyle = {
this.styles_.button = {
minWidth: theme.buttonMinWidth,
minHeight: theme.buttonMinHeight,
marginLeft: 5,
};
const labelStyle = {
this.styles_.label = {
marginRight: 5,
fontSize: theme.fontSize,
color: theme.color,
@@ -67,17 +76,51 @@ class PromptDialog extends React.Component {
verticalAlign: 'top',
};
const inputStyle = {
width: 0.5 * style.width,
this.styles_.input = {
width: 0.5 * width,
maxWidth: 400,
};
const descStyle = Object.assign({}, theme.textStyle, {
this.styles_.desc = Object.assign({}, theme.textStyle, {
marginTop: 10,
});
const onClose = (accept) => {
if (this.props.onClose) this.props.onClose(accept ? this.state.answer : null);
return this.styles_;
}
// shouldComponentUpdate(nextProps, nextState) {
// console.info(JSON.stringify(nextProps)+JSON.stringify(nextState));
// console.info('NEXT PROPS ====================');
// for (var n in nextProps) {
// if (!nextProps.hasOwnProperty(n)) continue;
// console.info(n + ' = ' + (nextProps[n] === this.props[n]));
// }
// console.info('NEXT STATE ====================');
// for (var n in nextState) {
// if (!nextState.hasOwnProperty(n)) continue;
// console.info(n + ' = ' + (nextState[n] === this.state[n]));
// }
// return true;
// }
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
const buttonTypes = this.props.buttons ? this.props.buttons : ['ok', 'cancel'];
const styles = this.styles(this.props.theme, style.width, style.height, this.state.visible);
const onClose = (accept, buttonType) => {
if (this.props.onClose) {
let outputAnswer = this.state.answer;
if (this.props.inputType === 'datetime') {
outputAnswer = anythingToDate(outputAnswer);
}
this.props.onClose(accept ? outputAnswer : null, buttonType);
}
this.setState({ visible: false, answer: '' });
}
@@ -85,6 +128,19 @@ class PromptDialog extends React.Component {
this.setState({ answer: event.target.value });
}
const anythingToDate = (o) => {
if (o && o.toDate) return o.toDate();
if (!o) return null;
let m = moment(o, time.dateTimeFormat());
if (m.isValid()) return m.toDate();
m = moment(o, time.dateFormat());
return m.isValid() ? m.toDate() : null;
}
const onDateTimeChange = (momentObject) => {
this.setState({ answer: momentObject });
}
const onKeyDown = (event) => {
if (event.key === 'Enter') {
onClose(true);
@@ -93,25 +149,43 @@ class PromptDialog extends React.Component {
}
}
const descComp = this.props.description ? <div style={descStyle}>{this.props.description}</div> : null;
const descComp = this.props.description ? <div style={styles.desc}>{this.props.description}</div> : null;
let inputComp = null;
if (this.props.inputType === 'datetime') {
inputComp = <Datetime
value={this.state.answer}
dateFormat={time.dateFormat()}
timeFormat={time.timeFormat()}
onChange={(momentObject) => onDateTimeChange(momentObject)}
/>
} else {
inputComp = <input
style={styles.input}
ref={input => this.answerInput_ = input}
value={this.state.answer}
type="text"
onChange={(event) => onChange(event)}
onKeyDown={(event) => onKeyDown(event)}
/>
}
const buttonComps = [];
if (buttonTypes.indexOf('ok') >= 0) buttonComps.push(<button key="ok" style={styles.button} onClick={() => onClose(true, 'ok')}>{_('OK')}</button>);
if (buttonTypes.indexOf('cancel') >= 0) buttonComps.push(<button key="cancel" style={styles.button} onClick={() => onClose(false, 'cancel')}>{_('Cancel')}</button>);
if (buttonTypes.indexOf('clear') >= 0) buttonComps.push(<button key="clear" style={styles.button} onClick={() => onClose(false, 'clear')}>{_('Clear')}</button>);
return (
<div style={modalLayerStyle}>
<div style={promptDialogStyle}>
<label style={labelStyle}>{this.props.label ? this.props.label : ''}</label>
<div style={styles.modalLayer}>
<div style={styles.promptDialog}>
<label style={styles.label}>{this.props.label ? this.props.label : ''}</label>
<div style={{display: 'inline-block'}}>
<input
style={inputStyle}
ref={input => this.answerInput_ = input}
value={this.state.answer}
type="text"
onChange={(event) => onChange(event)}
onKeyDown={(event) => onKeyDown(event)} />
{inputComp}
{descComp}
</div>
<div style={{ textAlign: 'right', marginTop: 10 }}>
<button style={buttonStyle} onClick={() => onClose(true)}>OK</button>
<button style={buttonStyle} onClick={() => onClose(false)}>Cancel</button>
{buttonComps}
</div>
</div>
</div>

View File

@@ -74,7 +74,7 @@ class RootComponent extends React.Component {
Main: { screen: MainScreen },
OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') },
Import: { screen: ImportScreen, title: () => _('Import') },
Config: { screen: ConfigScreen, title: () => _('Configuration') },
Config: { screen: ConfigScreen, title: () => _('Options') },
};
return (

View File

@@ -4,6 +4,7 @@ const shared = require('lib/components/shared/side-menu-shared.js');
const { Synchronizer } = require('lib/synchronizer.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { Tag } = require('lib/models/tag.js');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('../theme.js');
@@ -164,7 +165,32 @@ class SideBarComponent extends React.Component {
let style = Object.assign({}, this.style().listItem);
if (selected) style = Object.assign(style, this.style().listItemSelected);
if (folder.id === Folder.conflictFolderId()) style = Object.assign(style, this.style().conflictFolder);
return <a className="list-item" href="#" data-id={folder.id} data-type={BaseModel.TYPE_FOLDER} onContextMenu={(event) => this.itemContextMenu(event)} key={folder.id} style={style} onClick={() => {this.folderItem_click(folder)}}>{folder.title}</a>
const onDragOver = (event, folder) => {
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault();
}
const onDrop = async (event, folder) => {
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') < 0) return;
event.preventDefault();
const noteIds = JSON.parse(event.dataTransfer.getData('text/x-jop-note-ids'));
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], folder.id);
}
}
return <a
className="list-item"
onDragOver={(event) => { onDragOver(event, folder) } }
onDrop={(event) => { onDrop(event, folder) } }
href="#"
data-id={folder.id}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={(event) => this.itemContextMenu(event)}
key={folder.id}
style={style} onClick={() => {this.folderItem_click(folder)}}>{folder.title}
</a>
}
tagItem(tag, selected) {

View File

@@ -0,0 +1,58 @@
const React = require('react');
const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const ToolbarButton = require('./ToolbarButton.min.js');
class ToolbarComponent extends React.Component {
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
style.height = theme.toolbarHeight;
style.display = 'flex';
style.flexDirection = 'row';
style.borderBottom = '1px solid ' + theme.dividerColor;
style.boxSizing = 'border-box';
const itemComps = [];
if (this.props.items) {
for (let i = 0; i < this.props.items.length; i++) {
const o = this.props.items[i];
let key = o.iconName ? o.iconName : '';
key += o.title ? o.title : '';
const itemType = !('type' in o) ? 'button' : o.type;
const props = Object.assign({
key: key,
theme: this.props.theme,
}, o);
if (itemType === 'button') {
itemComps.push(<ToolbarButton
{...props}
/>);
} else if (itemType === 'text') {
}
}
}
return (
<div className="editor-toolbar" style={style}>
{ itemComps }
</div>
);
}
}
const mapStateToProps = (state) => {
return { theme: state.settings.theme };
};
const Toolbar = connect(mapStateToProps)(ToolbarComponent);
module.exports = Toolbar;

View File

@@ -0,0 +1,59 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('../theme.js');
class ToolbarButton extends React.Component {
render() {
//const style = this.props.style;
const theme = themeStyle(this.props.theme);
const style = {
height: theme.toolbarHeight,
minWidth: theme.toolbarHeight,
display: 'flex',
alignItems: 'center',
paddingLeft: theme.headerButtonHPadding,
paddingRight: theme.headerButtonHPadding,
color: theme.color,
textDecoration: 'none',
fontFamily: theme.fontFamily,
fontSize: theme.fontSize,
boxSizing: 'border-box',
cursor: 'default',
justifyContent: 'center',
};
let icon = null;
if (this.props.iconName) {
const iconStyle = {
fontSize: Math.round(theme.fontSize * 1.4),
color: theme.color
};
if (this.props.title) iconStyle.marginRight = 5;
icon = <i style={iconStyle} className={"fa " + this.props.iconName}></i>
}
const isEnabled = (!('enabled' in this.props) || this.props.enabled === true);
let classes = ['button'];
if (!isEnabled) classes.push('disabled');
const finalStyle = Object.assign({}, style, {
opacity: isEnabled ? 1 : 0.4,
});
return (
<a
className={classes.join(' ')}
style={finalStyle}
href="#"
onClick={() => { if (isEnabled && this.props.onClick) this.props.onClick() }}
>
{icon}{this.props.title ? this.props.title : ''}
</a>
);
}
}
module.exports = ToolbarButton;

View File

@@ -12,7 +12,7 @@ body {
</style>
<div id="hlScriptContainer"></div>
<div id="content"></div>
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
<script>
const { ipcRenderer } = require('electron');
@@ -71,7 +71,32 @@ function applyHljs(codeElements) {
// / Handle dynamically loading HLJS when a code element is present
// ----------------------------------------------------------------------
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
// so that it is not greater than contentHeight. On the other hand, with percentScroll it is possible to restore
// it at any time knowing that it's not going to be changed because the content height has changed.
// To restore percentScroll the "checkScrollIID" interval is used. It constantly resets the scroll position during
// one second after the content has been updated.
//
// ignoreNextScroll is used to differentiate between scroll event from the users and those that are the result
// of programmatically changing scrollTop. We only want to respond to events initiated by the user.
let percentScroll_ = 0;
let checkScrollIID_ = null;
function setPercentScroll(percent) {
percentScroll_ = percent;
contentElement.scrollTop = percentScroll_ * maxScrollTop();
}
function percentScroll() {
return percentScroll_;
}
function restorePercentScroll() {
setPercentScroll(percentScroll_);
}
ipcRenderer.on('setHtml', (event, html) => {
contentElement.innerHTML = html;
@@ -90,12 +115,37 @@ ipcRenderer.on('setHtml', (event, html) => {
ul.style.listStyleType = 'none';
ul.style.paddingLeft = 0;
}
let previousContentHeight = contentElement.scrollHeight;
let startTime = Date.now();
ignoreNextScrollEvent = true;
restorePercentScroll();
if (!checkScrollIID_) {
checkScrollIID_ = setInterval(() => {
const h = contentElement.scrollHeight;
if (h !== previousContentHeight) {
previousContentHeight = h;
ignoreNextScrollEvent = true;
restorePercentScroll();
}
if (Date.now() - startTime >= 1000) {
clearInterval(checkScrollIID_);
checkScrollIID_ = null;
}
}, 1);
}
});
let ignoreNextScroll = false;
let ignoreNextScrollEvent = false;
ipcRenderer.on('setPercentScroll', (event, percent) => {
ignoreNextScroll = true;
contentElement.scrollTop = percent * maxScrollTop();
if (checkScrollIID_) {
clearInterval(checkScrollIID_);
checkScrollIID_ = null;
}
ignoreNextScrollEvent = true;
setPercentScroll(percent);
});
function maxScrollTop() {
@@ -103,12 +153,14 @@ function maxScrollTop() {
}
contentElement.addEventListener('scroll', function(e) {
if (ignoreNextScroll) {
ignoreNextScroll = false;
if (ignoreNextScrollEvent) {
ignoreNextScrollEvent = false;
return;
}
const m = maxScrollTop();
ipcRenderer.sendToHost('percentScroll', m ? contentElement.scrollTop / m : 0);
const percent = m ? contentElement.scrollTop / m : 0;
setPercentScroll(percent);
ipcRenderer.sendToHost('percentScroll', percent);
});
// Disable drag and drop otherwise it's possible to drop a URL

View File

@@ -5,9 +5,17 @@
<title>Joplin</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="css/font-awesome.min.css">
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
</head>
<body>
<div id="react-root"></div>
<script src="main-html.js"></script>
<style>
/* Disable dragging of links (which are often buttons) */
a:not([draggable=true]), img:not([draggable=true]) {
-webkit-user-drag: none;
user-drag: none;
}
</style>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,5 @@
var locales = {};
locales['en_GB'] = require('./en_GB.json');
locales['es_CR'] = require('./es_CR.json');
locales['fr_FR'] = require('./fr_FR.json');
module.exports = { locales: locales };

View File

@@ -31,11 +31,29 @@ BaseItem.loadClass('NoteTag', NoteTag);
Setting.setConstant('appId', 'net.cozic.joplin-desktop');
Setting.setConstant('appType', 'desktop');
// Disable drag and drop of links inside application (which would
// open it as if the whole app was a browser)
document.addEventListener('dragover', event => event.preventDefault());
document.addEventListener('drop', event => event.preventDefault());
// Disable middle-click (which would open a new browser window, but we don't want this)
document.addEventListener('auxclick', event => event.preventDefault());
// Each link (rendered as a button or list item) has its own custom click event
// so disable the default. In particular this will disable Ctrl+Clicking a link
// which would open a new browser window.
document.addEventListener('click', (event) => event.preventDefault());
shimInit();
app().start(bridge().processArgv()).then(() => {
require('./gui/Root.min.js');
}).catch((error) => {
console.error('Fatal error:');
console.error(error);
// If something goes wrong at this stage we don't have a console or a log file
// so display the error in a message box.
let msg = ['Fatal error:', error.message];
if (error.fileName) msg.push(error.fileName);
if (error.lineNumber) msg.push(error.lineNumber);
if (error.stack) msg.push(error.stack);
bridge().showErrorMessageBox(msg.join('\n'));
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "0.10.22",
"version": "0.10.35",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {
@@ -8,8 +8,8 @@
"pack": "node_modules/.bin/electron-builder --dir",
"dist": "node_modules/.bin/electron-builder",
"publish": "build -p always",
"postinstall": "node compile-jsx.js",
"compile": "node compile-jsx.js"
"postinstall": "node compile-jsx.js && node compile-package-info.js",
"compile": "node compile-jsx.js && node compile-package-info.js"
},
"repository": {
"type": "git",
@@ -60,15 +60,16 @@
"levenshtein": "^1.0.5",
"lodash": "^4.17.4",
"markdown-it": "^8.4.0",
"marked": "^0.3.6",
"md5": "^2.2.1",
"mime": "^2.0.3",
"moment": "^2.19.1",
"node-fetch": "^1.7.3",
"node-notifier": "^5.1.2",
"promise": "^8.0.1",
"query-string": "^5.0.1",
"react": "^16.0.0",
"react-ace": "^5.5.0",
"react-datetime": "^2.11.0",
"react-dom": "^16.0.0",
"react-redux": "^5.0.6",
"redux": "^3.7.2",

View File

@@ -9,6 +9,14 @@ body, textarea {
overflow: hidden;
}
/* By default, the Ice Editor displays invalid characters, such as non-breaking spaces
as red boxes, but since those are actually valid characters and common in imported
Evernote data, we hide them here. */
.ace-chrome .ace_invisible_space {
background-color: transparent !important;
opacity: 0;
}
.note-list .list-item:hover {
background-color: rgba(0,160,255,0.1) !important;
}
@@ -23,18 +31,21 @@ body, textarea {
background-color: #564B6C;
}
.editor-toolbar .button:not(.disabled):hover,
.header .button:not(.disabled):hover {
background-color: rgba(0,160,255,0.1);
border: 1px solid rgba(0,160,255,0.5);
box-sizing: 'border-box';
}
.editor-toolbar .button:not(.disabled):active,
.header .button:not(.disabled):active {
background-color: rgba(0,160,255,0.2);
border: 1px solid rgba(0,160,255,0.7);
box-sizing: 'border-box';
}
.editor-toolbar .button,
.header .button {
border: 1px solid rgba(0,160,255,0);
}

View File

@@ -28,6 +28,8 @@ const globalStyle = {
headerHeight: 35,
headerButtonHPadding: 6,
toolbarHeight: 35,
raisedBackgroundColor: "#0080EF",
raisedColor: "#003363",
raisedHighlightedColor: "#ffffff",

View File

@@ -7,6 +7,14 @@ const request = require('request');
const url = 'https://api.github.com/repos/laurent22/joplin/releases/latest';
const readmePath = __dirname + '/../../README.md';
async function msleep(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});
}
async function gitHubLatestRelease() {
return new Promise((resolve, reject) => {
request.get({
@@ -17,6 +25,7 @@ async function gitHubLatestRelease() {
if (error) {
reject(error);
} else if (response.statusCode !== 200) {
console.warn(data);
reject(new Error('Error HTTP ' + response.statusCode));
} else {
resolve(data);
@@ -48,8 +57,23 @@ function setReadmeContent(content) {
return fs.writeFileSync(readmePath, content);
}
async function main() {
const release = await gitHubLatestRelease();
async function main(argv) {
const waitForVersion = argv.length === 3 ? argv[2] : null;
if (waitForVersion) console.info('Waiting for version ' + waitForVersion + ' to be released before updating readme...');
let release = null;
while (true) {
release = await gitHubLatestRelease();
if (!waitForVersion) break;
if (release.tag_name !== waitForVersion) {
await msleep(60000 * 5);
} else {
console.info('Got version ' + waitForVersion);
break;
}
}
const winUrl = downloadUrl(release, 'windows');
const macOsUrl = downloadUrl(release, 'macos');
@@ -57,13 +81,13 @@ async function main() {
let content = readmeContent();
if (winUrl) content = content.replace(/(\(https:\/\/github.com\/laurent22\/joplin\/releases\/download\/.*?\.exe\))/, '(' + winUrl + ')');
if (macOsUrl) content = content.replace(/(\(https:\/\/github.com\/laurent22\/joplin\/releases\/download\/.*?\.dmg\))/, '(' + macOsUrl + ')');
if (linuxUrl) content = content.replace(/(\(https:\/\/github.com\/laurent22\/joplin\/releases\/download\/.*?\.AppImage\))/, '(' + linuxUrl + ')');
if (winUrl) content = content.replace(/(https:\/\/github.com\/laurent22\/joplin\/releases\/download\/.*?\.exe)/, winUrl);
if (macOsUrl) content = content.replace(/(https:\/\/github.com\/laurent22\/joplin\/releases\/download\/.*?\.dmg)/, macOsUrl);
if (linuxUrl) content = content.replace(/(https:\/\/github.com\/laurent22\/joplin\/releases\/download\/.*?\.AppImage)/, linuxUrl);
setReadmeContent(content);
}
main().catch((error) => {
main(process.argv).catch((error) => {
console.error('Fatal error', error);
});

View File

@@ -9,4 +9,9 @@ git commit -m "Electron release $VERSION"
git tag $VERSION
git push && git push --tags
echo "Create a draft release at: https://github.com/laurent22/joplin/releases/tag/$VERSION"
echo ""
echo "Create a draft release at: https://github.com/laurent22/joplin/releases/tag/$VERSION"
echo ""
echo "Then run:"
echo ""
echo "node $APP_DIR/update-readme-download.js"

View File

@@ -1,47 +0,0 @@
'use strict';
const fs = require('fs-extra');
const https = require('https');
const request = require('request');
const url = 'https://api.github.com/repos/laurent22/joplin/releases/latest';
async function gitHubLatestRelease() {
return new Promise((resolve, reject) => {
request.get({
url: url,
json: true,
headers: {'User-Agent': 'Joplin Readme Updater'}
}, (error, response, data) => {
if (err) {
reject(error);
} else if (response.statusCode !== 200) {
reject(new Error('Error HTTP ' + response.statusCode));
} else {
resolve(data);
}
});
});
}
function downloadUrl(release, os) {
if (!release || !release.assets || !release.assets.length) return null;
for (let i = 0; i < release.assets.length; i++) {
const asset = release.assets[i];
const name = asset.name;
if (name.indexOf('.dmg') > 0 && os === 'macos') return asset.browser_download_url;
}
}
async function main() {
const release = await gitHubLatestRelease();
console.info(downloadUrl(release, 'windows'));
console.info(downloadUrl(release, 'macos'));
console.info(downloadUrl(release, 'linux'));
}
main().catch((error) => {
console.error('Fatal error', error);
});

View File

@@ -1,39 +1,48 @@
# Joplin
Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified with your own text editor.
Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](https://daringfireball.net/projects/markdown/basics).
Notes exported from Evernote via .enex files [can be imported](#importing-notes-from-evernote) into Joplin, including the formatted content (which is converted to markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.).
Notes exported from Evernote via .enex files [can be imported](#importing-notes-from-evernote) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.).
The notes can be [synchronised](#synchronisation) with various targets including the file system (for example with a network directory) or with Microsoft OneDrive. When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.
Joplin is still under development but is out of Beta and should be suitable for every day use. The UI of the terminal client is built on top of the great [terminal-kit](https://github.com/cronvel/terminal-kit) library, the desktop client using [Electron](https://electronjs.org/), and the Android client front end is done using [React Native](https://facebook.github.io/react-native/).
<img src="https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/AllClients.jpg" style="max-width: 100%">
<div class="top-screenshot"><img src="https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/AllClients.jpg" style="max-width: 100%; max-height: 35em;"></div>
# Installation
Three types of applications are available: **desktop** (Windows, macOS and Linux), **mobile** (Android) or for **terminal** emulator (Windows, macOS and Linux). All applications have similar user interfaces and can synchronise with each others.
Three types of applications are available: for the **desktop** (Windows, macOS and Linux), for **mobile** (Android and iOS) and for **terminal** (Windows, macOS and Linux). All applications have similar user interfaces and can synchronise with each others.
## Desktop applications
Operating system | Link
Operating System | Download
-----------------|--------
Windows | [Download Joplin for Windows](https://github.com/laurent22/joplin/releases/download/v0.10.20/Joplin-Setup-0.10.20.exe)
macOS | [Download Joplin for macOS](https://github.com/laurent22/joplin/releases/download/v0.10.20/Joplin-0.10.20.dmg)
Linux | [Download Joplin for Linux](https://github.com/laurent22/joplin/releases/download/v0.10.20/Joplin-0.10.20-x86_64.AppImage)
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.35/Joplin-Setup-0.10.35.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.35/Joplin-0.10.35.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.35/Joplin-0.10.35-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
## Mobile applications
Operating system | Link
Operating System | 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="60px" src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png'/></a>
iOS | Coming soon!
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>
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
On macOS, Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)), type:
**IMPORTANT: Node v8+ is required**
npm install -g joplin
On macOS:
brew install joplin
On Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)), type:
NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin
sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
By default, the application binary will be installed under `~/.joplin-bin`. You may change this directory if needed. Alternatively, if your npm permissions are setup as described [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory) (Option 2) then simply running `npm -g install joplin` would work.
To start it, type `joplin`.
@@ -44,9 +53,9 @@ For usage information, please refer to the full [Joplin Terminal Application Doc
- Desktop, mobile and terminal applications.
- Support notes, to-dos, tags and notebooks.
- Offline first, so the entire data is always available on the device even without an internet connection.
- Ability to synchronise with multiple targets, including the file system and OneDrive (Dropbox is planned).
- Ability to synchronise with multiple targets, including the file system and OneDrive (NextCloud and Dropbox are planned).
- Synchronises to a plain text format, which can be easily manipulated, backed up, or exported to a different format.
- Plain text notes, which are rendered as markdown in the mobile and desktop application.
- Markdown notes, which are rendered with images and formatting in the desktop and mobile applications.
- Tag support
- File attachment support (images are displayed, and other files are linked and can be opened in the relevant application).
- Search functionality.
@@ -59,35 +68,23 @@ Joplin was designed as a replacement for Evernote and so can import complete Eve
- Recognition data - Evernote images, in particular scanned (or photographed) documents have [recognition data](https://en.wikipedia.org/wiki/Optical_character_recognition) associated with them. It is the text that Evernote has been able to recognise in the document. This data is not preserved when the note are imported into Joplin. However, should it become supported in the search tool or other parts of Joplin, it should be possible to regenerate this recognition data since the actual image would still be available.
- Colour, font sizes and faces - Evernote text is stored as HTML and this is converted to Markdown during the import process. For notes that are mostly plain text or with basic formatting (bold, italic, bullet points, links, etc.) this is a lossless conversion, and the note, once rendered back to HTML should be very similar. Tables are also imported and converted to Markdown tables. For very complex notes, some formatting data might be loss - in particular colours, font sizes and font faces will not be imported. The text itself however is always imported in full regardless of formatting.
- Colour, font sizes and faces - Evernote text is stored as HTML and this is converted to Markdown during the import process. For notes that are mostly plain text or with basic formatting (bold, italic, bullet points, links, etc.) this is a lossless conversion, and the note, once rendered back to HTML should be very similar. Tables are also imported and converted to Markdown tables. For very complex notes, some formatting data might be lost - in particular colours, font sizes and font faces will not be imported. The text itself however is always imported in full regardless of formatting.
To import Evernote data, follow these steps:
To import Evernote data, first export your Evernote notebooks to ENEX files as described [here](https://help.evernote.com/hc/en-us/articles/209005557-How-to-back-up-export-and-restore-import-notes-and-notebooks). Then follow these steps:
## Desktop application
On the **desktop application**, open the "File" menu, click "Import Evernote notes" and select your ENEX file. This will open a new screen which will display the import progress. The notes will be imported into a new separate notebook (so that, in case of a mistake, the notes are not mixed up with any existing notes). If needed then can then be moved to a different notebook, or the notebook can be renamed, etc.
* Open the "File" menu and click "Import Evernote notes"
This will open a new screen which will display the import progress. The notes will be imported into a new separate notebook (so that, in case of a mistake, the notes are not mixed up with any existing notes). If needed then can then be moved to a different notebook, or the notebook can be renamed, etc.
## Terminal application
* First, export your Evernote notebooks to ENEX files as described [here](https://help.evernote.com/hc/en-us/articles/209005557-How-to-back-up-export-and-restore-import-notes-and-notebooks).
* In Joplin, in [command-line mode](/terminal#command-line-mode), type `import-enex /path/to/file.enex`. This will import the notes into a new notebook named after the filename.
* Then repeat the process for each notebook that needs to be imported.
On the **terminal application**, in [command-line mode](/terminal#command-line-mode), type `import-enex /path/to/file.enex`. This will import the notes into a new notebook named after the filename.
# Synchronisation
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as OneDrive or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
Currently, synchronisation is possible with OneDrive (by default) or the local filesystem. A Dropbox driver will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
Currently, synchronisation is possible with OneDrive (by default) or the local filesystem. A NextCloud driver, and a Dropbox one will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
## Desktop application
On the **desktop application**, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.
To initiate the synchronisation process, click on the "Synchronise" button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually
## Terminal application
To initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
On the **terminal application**, to initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
*/30 * * * * /path/to/joplin sync
@@ -95,28 +92,45 @@ To initiate the synchronisation process, type `:sync`. You will be asked to foll
Any kind of file can be attached to a note. In Markdown, links to these files are represented as a simple ID to the resource. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application.
# Notifications
On the desktop and mobile apps, an alarm can be associated with any to-do. It will be triggered at the given time by displaying a notification. How the notification will be displayed depends on the operating system since each has a different way to handle this. Please see below for the requirements for the desktop applications:
- **Windows**: >= 8. Make sure the Action Center is enabled on Windows. Task bar balloon for Windows < 8. Growl as fallback. Growl takes precedence over Windows balloons.
- **macOS**: >= 10.8 or Growl if earlier.
- **Linux**: `notify-osd` or `libnotify-bin` installed (Ubuntu should have this by default). Growl otherwise
See [documentation and flow chart for reporter choice](./DECISION_FLOW.md)
On mobile, the alarms will be displayed using the built-in notification system.
If for any reason the notifications do not work, please [open an issue](https://github.com/laurent22/joplin/issues).
# Localisation
Joplin is currently available in English and French. If you would like to contribute a translation, it is quite straightforward, please follow these steps:
Joplin is currently available in English, French and Spanish. If you would like to contribute a translation, it is quite straightforward, please follow these steps:
- [Download Poedit](https://poedit.net/), the translation editor, and install it.
- [Download the file to be translated](https://raw.githubusercontent.com/laurent22/joplin/master/CliClient/locales/joplin.pot).
- In Poedit, open this .pot file, go into the Catalog menu and click Configuration. Change "Country" and "Language" to your own country and language.
- From then you can translate the file. Once it's done, please send the file to [this address](https://raw.githubusercontent.com/laurent22/joplin/master/Assets/Adresse.png) or open a pull request.
- From then you can translate the file. Once it is done, please either [open a pull request](https://github.com/laurent22/joplin/pulls) or send the file to [this address](https://raw.githubusercontent.com/laurent22/joplin/master/Assets/Adresse.png).
This translation will apply to both the terminal and the Android application.
This translation will apply to the three applications - desktop, mobile and terminal.
# Coming features
The two main features that remain to be implemented are:
- iOS application (80% done)
- End to end encryption (to-do)
- NextCloud support
- All: End to end encryption
- Windows: Tray icon
- Desktop apps: Tag auto-complete
- Desktop apps: Dark theme
- Linux: Enable auto-update for desktop app
# Known bugs
- Non-alphabetical characters such as Chinese or Arabic might create glitches in the terminal on Windows. This is a limitation of the current Windows console.
- In the React Native app, changing the notebook of a note sometimes has no effect, due to [this bug](https://github.com/facebook/react-native/issues/15556). Due to [this other bug](https://github.com/facebook/react-native/issues/13351), changing one config value sometimes also set a different one. This is likely to be resolved soon in a future version of React Native.
- Auto-update is not working in the Linux desktop application.
- While the mobile can sync and load tags, it is not currently possible to create new ones. The desktop and terminal apps can create, delete and edit tags.
# License

View File

@@ -10,9 +10,18 @@ The notes can be [synchronised](#synchronisation) with various targets including
# Installation
On macOS, Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)), type:
**IMPORTANT: Node v8+ is required**
npm install -g joplin
On macOS:
brew install joplin
On Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)), type:
NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin
sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
By default, the application binary will be installed under `~/.joplin-bin`. You may change this directory if needed. Alternatively, if your npm permissions are setup as described [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory) (Option 2) then simply running `npm -g install joplin` would work.
To start it, type `joplin`.
@@ -20,7 +29,7 @@ To start it, type `joplin`.
The demo application shows various Wikipedia articles converted to Markdown and organised into notebooks, as well as an example to-do list, in order to test and demonstrate the application. The demo application and its settings will be installed in a separate directory so as not to interfere with any existing Joplin application.
npm install -g demo-joplin
npm install -g demo-joplin
To start it, type `demo-joplin`.
@@ -54,27 +63,27 @@ Shortcut | Description
Create a new note with title "Wednesday's meeting":
mknote "Wednesday's meeting"
mknote "Wednesday's meeting"
Create a new to-do:
mktodo "Buy bread"
mktodo "Buy bread"
Move the currently selected note ($n) to the notebook with title "Personal"
mv $n "Personal"
mv $n "Personal"
Rename the currently selected notebook ($b) to "Something":
ren $b "Something"
ren $b "Something"
Attach a local file to the currently selected note ($n):
ren $n /home/laurent/pictures/Vacation12.jpg
ren $n /home/laurent/pictures/Vacation12.jpg
The configuration can also be changed from command-line mode. For example, to change the current editor to Sublime Text:
config editor "subl -w"
config editor "subl -w"
## Editing a note
@@ -108,7 +117,7 @@ Currently, synchronisation is possible with OneDrive (by default) or the local f
To initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
*/30 * * * * /path/to/joplin sync
*/30 * * * * /path/to/joplin sync
# URLs
@@ -132,189 +141,202 @@ In Markdown, links to resources are represented as a simple ID to the resource.
There are two types of shortcuts: those that manipulate the user interface directly, such as `TAB` to move from one pane to another, and those that are simply shortcuts to actual commands. In a way similar to Vim, these shortcuts are generally a verb followed by an object. For example, typing `mn` ([m]ake [n]ote), is used to create a new note: it will switch the interface to command line mode and pre-fill it with `mknote ""` from where the title of the note can be entered. See below for the full list of shortcuts:
Tab Give focus to next pane
Shift+Tab Give focus to previous pane
: Enter command line mode
ESC Exit command line mode
ENTER Edit the selected note
Ctrl+C Cancel the current command.
Ctrl+D Exit the application.
DELETE Delete the currently selected note or notebook.
SPACE Set a to-do as completed / not completed
tc [t]oggle [c]onsole between maximized/minimized/hidden/visible.
/ Search
tm [t]oggle note [m]etadata.
mn [M]ake a new [n]ote
mt [M]ake a new [t]odo
mb [M]ake a new note[b]ook
yn Copy ([Y]ank) the [n]ote to a notebook.
dn Move the note to a notebook.
Tab Give focus to next pane
Shift+Tab Give focus to previous pane
: Enter command line mode
ESC Exit command line mode
ENTER Edit the selected note
Ctrl+C Cancel the current command.
Ctrl+D Exit the application.
DELETE Delete the currently selected note or notebook.
SPACE Set a to-do as completed / not completed
tc [t]oggle [c]onsole between maximized/minimized/hidden/visible.
/ Search
tm [t]oggle note [m]etadata.
mn [M]ake a new [n]ote
mt [M]ake a new [t]odo
mb [M]ake a new note[b]ook
yn Copy ([Y]ank) the [n]ote to a notebook.
dn Move the note to a notebook.
# Available commands
The following commands are available in [command-line mode](#command-line-mode):
attach <note> <file>
attach <note> <file>
Attaches the given file to the note.
Attaches the given file to the note.
config [name] [value]
config [name] [value]
Gets or sets a config value. If [value] is not provided, it will show the
value of [name]. If neither [name] nor [value] is provided, it will list
the current configuration.
Gets or sets a config value. If [value] is not provided, it will show the
value of [name]. If neither [name] nor [value] is provided, it will list
the current configuration.
-v, --verbose Also displays unset and hidden config variables.
-v, --verbose Also displays unset and hidden config variables.
Possible keys/values:
Possible keys/values:
editor Text editor.
The editor that will be used to open a note. If
none is provided it will try to auto-detect the
default editor.
Type: string.
locale Language.
Type: Enum.
Possible values: en_GB (English), fr_FR (Français).
Default: "en_GB"
sync.2.path File system synchronisation target directory.
The path to synchronise with when file system
synchronisation is enabled. See `sync.target`.
Type: string.
sync.interval Synchronisation interval.
Type: Enum.
Possible values: 0 (Disabled), 300 (5 minutes), 600
(10 minutes), 1800 (30 minutes), 3600 (1 hour),
43200 (12 hours), 86400 (24 hours).
Default: 300
sync.target Synchronisation target.
The target to synchonise to. If synchronising with
the file system, set `sync.2.path` to specify the
target directory.
Type: Enum.
Possible values: 1 (Memory), 2 (File system), 3
(OneDrive).
Default: 3
trackLocation Save geo-location with notes.
Type: bool.
Default: true
uncompletedTodosOnTop Show uncompleted todos on top of the lists.
Type: bool.
Default: true
sync.2.path File system synchronisation target directory.
The path to synchronise with when file system
synchronisation is enabled. See `sync.target`.
Type: string.
editor Text editor.
The editor that will be used to open a note. If
none is provided it will try to auto-detect the
default editor.
Type: string.
locale Language.
Type: Enum.
Possible values: en_GB (English), es_CR (Español),
fr_FR (Français).
Default: "en_GB"
dateFormat Date format.
Type: Enum.
Possible values: DD/MM/YYYY (30/01/2017), DD/MM/YY
(30/01/17), MM/DD/YYYY (01/30/2017), MM/DD/YY
(01/30/17), YYYY-MM-DD (2017-01-30).
Default: "DD/MM/YYYY"
timeFormat Time format.
Type: Enum.
Possible values: HH:mm (20:30), h:mm A (8:30 PM).
Default: "HH:mm"
uncompletedTodosOnTop Show uncompleted todos on top of the lists.
Type: bool.
Default: true
trackLocation Save geo-location with notes.
Type: bool.
Default: true
sync.interval Synchronisation interval.
Type: Enum.
Possible values: 0 (Disabled), 300 (5 minutes), 600
(10 minutes), 1800 (30 minutes), 3600 (1 hour),
43200 (12 hours), 86400 (24 hours).
Default: 300
sync.target Synchronisation target.
The target to synchonise to. If synchronising with
the file system, set `sync.2.path` to specify the
target directory.
Type: Enum.
Possible values: 2 (File system), 3 (OneDrive), 4
(OneDrive Dev (For testing only)).
Default: 3
cp <note> [notebook]
cp <note> [notebook]
Duplicates the notes matching <note> to [notebook]. If no notebook is
specified the note is duplicated in the current notebook.
Duplicates the notes matching <note> to [notebook]. If no notebook is
specified the note is duplicated in the current notebook.
done <note>
done <note>
Marks a to-do as done.
Marks a to-do as done.
edit <note>
edit <note>
Edit note.
Edit note.
exit
exit
Exits the application.
Exits the application.
export <directory>
export <directory>
Exports Joplin data to the given directory. By default, it will export the
complete database including notebooks, notes, tags and resources.
Exports Joplin data to the given directory. By default, it will export the
complete database including notebooks, notes, tags and resources.
--note <note> Exports only the given note.
--notebook <notebook> Exports only the given notebook.
--note <note> Exports only the given note.
--notebook <notebook> Exports only the given notebook.
geoloc <note>
geoloc <note>
Displays a geolocation URL for the note.
Displays a geolocation URL for the note.
help [command]
help [command]
Displays usage information.
Displays usage information.
import-enex <file> [notebook]
import-enex <file> [notebook]
Imports an Evernote notebook file (.enex file).
Imports an Evernote notebook file (.enex file).
-f, --force Do not ask for confirmation.
-f, --force Do not ask for confirmation.
mkbook <new-notebook>
mkbook <new-notebook>
Creates a new notebook.
Creates a new notebook.
mknote <new-note>
mknote <new-note>
Creates a new note.
Creates a new note.
mktodo <new-todo>
mktodo <new-todo>
Creates a new to-do.
Creates a new to-do.
mv <note> [notebook]
mv <note> [notebook]
Moves the notes matching <note> to [notebook].
Moves the notes matching <note> to [notebook].
ren <item> <name>
ren <item> <name>
Renames the given <item> (note or notebook) to <name>.
Renames the given <item> (note or notebook) to <name>.
rmbook <notebook>
rmbook <notebook>
Deletes the given notebook.
Deletes the given notebook.
-f, --force Deletes the notebook without asking for confirmation.
-f, --force Deletes the notebook without asking for confirmation.
rmnote <note-pattern>
rmnote <note-pattern>
Deletes the notes matching <note-pattern>.
Deletes the notes matching <note-pattern>.
-f, --force Deletes the notes without asking for confirmation.
-f, --force Deletes the notes without asking for confirmation.
search <pattern> [notebook]
search <pattern> [notebook]
Searches for the given <pattern> in all the notes.
Searches for the given <pattern> in all the notes.
status
status
Displays summary about the notes and notebooks.
Displays summary about the notes and notebooks.
sync
sync
Synchronises with remote storage.
Synchronises with remote storage.
--target <target> Sync to provided target (defaults to sync.target config
value)
--random-failures For debugging purposes. Do not use.
--target <target> Sync to provided target (defaults to sync.target config
value)
--random-failures For debugging purposes. Do not use.
tag <tag-command> [tag] [note]
tag <tag-command> [tag] [note]
<tag-command> can be "add", "remove" or "list" to assign or remove [tag]
from [note], or to list the notes associated with [tag]. The command `tag
list` can be used to list all the tags.
<tag-command> can be "add", "remove" or "list" to assign or remove [tag]
from [note], or to list the notes associated with [tag]. The command `tag
list` can be used to list all the tags.
todo <todo-command> <note-pattern>
todo <todo-command> <note-pattern>
<todo-command> can either be "toggle" or "clear". Use "toggle" to toggle
the given to-do between completed and uncompleted state (If the target is
a regular note it will be converted to a to-do). Use "clear" to convert
the to-do back to a regular note.
<todo-command> can either be "toggle" or "clear". Use "toggle" to toggle
the given to-do between completed and uncompleted state (If the target is
a regular note it will be converted to a to-do). Use "clear" to convert
the to-do back to a regular note.
undone <note>
undone <note>
Marks a to-do as non-completed.
Marks a to-do as non-completed.
version
version
Displays version information
Displays version information
# License

View File

@@ -53,6 +53,5 @@ fastlane/Preview.html
fastlane/screenshots
# This is generated:
locales
android/build*
android/app/build*

View File

@@ -90,8 +90,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion 16
targetSdkVersion 22
versionCode 64
versionName "0.10.51"
versionCode 77
versionName "0.10.62"
ndk {
abiFilters "armeabi-v7a", "x86"
}
@@ -137,6 +137,7 @@ android {
}
dependencies {
compile project(':react-native-push-notification')
compile project(':react-native-fs')
compile project(':react-native-image-picker')
compile project(':react-native-vector-icons')

View File

@@ -1,37 +1,83 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="net.cozic.joplin"
android:versionCode="2"
android:versionName="0.8.0">
xmlns:tools="http://schemas.android.com/tools"
package="net.cozic.joplin"
android:versionCode="2"
android:versionName="0.8.0">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove"/>
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />
<!-- ==================================== -->
<!-- START react-native-push-notification -->
<!-- ==================================== -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- ================================== -->
<!-- END react-native-push-notification -->
<!-- ================================== -->
<application
android:name=".MainApplication"
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme">
<!-- ==================================== -->
<!-- START react-native-push-notification -->
<!-- ==================================== -->
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="${applicationId}" />
</intent-filter>
</receiver>
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationRegistrationService"/>
<service
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
<!-- ================================== -->
<!-- END react-native-push-notification -->
<!-- ================================== -->
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>

View File

@@ -3,6 +3,7 @@ package net.cozic.joplin;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage;
import com.imagepicker.ImagePickerPackage;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
@@ -31,6 +32,7 @@ public class MainApplication extends Application implements ReactApplication {
return Arrays.<ReactPackage>asList(
new ImageResizerPackage(),
new MainReactPackage(),
new ReactNativePushNotificationPackage(),
new ImagePickerPackage(),
new ReactNativeDocumentPicker(),
new RNFetchBlobPackage(),

View File

@@ -1,4 +1,6 @@
rootProject.name = 'Joplin'
include ':react-native-push-notification'
project(':react-native-push-notification').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-push-notification/android')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
include ':react-native-image-picker'

View File

@@ -0,0 +1,9 @@
@echo off
rem Clear build dir if permission issue:
rem rmdir /S/Q android\app\build
setlocal
node ..\Tools\prepare-android-prod-build.js
cd android
gradlew.bat assembleRelease -PbuildDir=build --console plain

View File

@@ -23,6 +23,7 @@
146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; };
1E71C4672AEC47CE94DFF507 /* Feather.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FD370E24D76E461D960DD85D /* Feather.ttf */; };
350318CF7C9E4BD68821EBE9 /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FF411B45E68B4A8CBCC35777 /* Ionicons.ttf */; };
4DDA31241FC88F2400B5A80D /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DDA310F1FC88EEB00B5A80D /* libRCTPushNotification.a */; };
5AFCE00CC1414FE6BD618F0D /* MaterialIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 15FD7D2C8F0A445BBA807A9D /* MaterialIcons.ttf */; };
5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; };
725A77EC604947A0AFF12C2B /* libRNFetchBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F5E37D05726A4A08B2EE323A /* libRNFetchBlob.a */; };
@@ -101,55 +102,6 @@
remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192;
remoteInfo = React;
};
3DAD3E831DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 2D2A283A1D9B042B00D4039D;
remoteInfo = "RCTImage-tvOS";
};
3DAD3E871DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 2D2A28471D9B043800D4039D;
remoteInfo = "RCTLinking-tvOS";
};
3DAD3E8B1DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 2D2A28541D9B044C00D4039D;
remoteInfo = "RCTNetwork-tvOS";
};
3DAD3E8F1DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 2D2A28611D9B046600D4039D;
remoteInfo = "RCTSettings-tvOS";
};
3DAD3E931DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 2D2A287B1D9B048500D4039D;
remoteInfo = "RCTText-tvOS";
};
3DAD3E981DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 2D2A28881D9B049200D4039D;
remoteInfo = "RCTWebSocket-tvOS";
};
3DAD3EA21DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 2D2A28131D9B038B00D4039D;
remoteInfo = "React-tvOS";
};
3DAD3EA41DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
@@ -157,13 +109,6 @@
remoteGlobalIDString = 3D3C059A1DE3340900C268FA;
remoteInfo = yoga;
};
3DAD3EA61DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 3D3C06751DE3340C00C268FA;
remoteInfo = "yoga-tvOS";
};
3DAD3EA81DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
@@ -171,13 +116,6 @@
remoteGlobalIDString = 3D3CD9251DE5FBEC00167DC4;
remoteInfo = cxxreact;
};
3DAD3EAA1DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 3D3CD9321DE5FBEE00167DC4;
remoteInfo = "cxxreact-tvOS";
};
3DAD3EAC1DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
@@ -185,20 +123,6 @@
remoteGlobalIDString = 3D3CD90B1DE5FBD600167DC4;
remoteInfo = jschelpers;
};
3DAD3EAE1DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 3D3CD9181DE5FBD800167DC4;
remoteInfo = "jschelpers-tvOS";
};
4D2A85971FBCE3AC0028537D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = ADD01A681E09402E00F6D226;
remoteInfo = "RCTBlob-tvOS";
};
4D2A85A91FBCE3AC0028537D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */;
@@ -206,13 +130,6 @@
remoteGlobalIDString = 3DBE0D001F3B181A0099AA32;
remoteInfo = fishhook;
};
4D2A85AB1FBCE3AC0028537D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 3DBE0D0D1F3B181C0099AA32;
remoteInfo = "fishhook-tvOS";
};
4D2A85BA1FBCE3AD0028537D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = DF1C50EBC11E46A3AF87F80A /* RCTImageResizer.xcodeproj */;
@@ -269,13 +186,6 @@
remoteGlobalIDString = 139D7ECE1E25DB7D00323FB7;
remoteInfo = "third-party";
};
4D3A19281FBDDA9400457703 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 3D383D3C1EBD27B6005632C8;
remoteInfo = "third-party-tvOS";
};
4D3A192A1FBDDA9400457703 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
@@ -283,13 +193,6 @@
remoteGlobalIDString = 139D7E881E25C6D100323FB7;
remoteInfo = "double-conversion";
};
4D3A192C1FBDDA9400457703 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 3D383D621EBD27B9005632C8;
remoteInfo = "double-conversion-tvOS";
};
4DA7F80C1FC1DA9C00353191 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = A4716DB8654B431D894F89E1 /* RNImagePicker.xcodeproj */;
@@ -297,6 +200,20 @@
remoteGlobalIDString = 014A3B5C1C6CF33500B6D375;
remoteInfo = RNImagePicker;
};
4DDA310E1FC88EEB00B5A80D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4DDA31011FC88EEA00B5A80D /* RCTPushNotification.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 134814201AA4EA6300B7C361;
remoteInfo = RCTPushNotification;
};
4DDA31101FC88EEB00B5A80D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4DDA31011FC88EEA00B5A80D /* RCTPushNotification.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 3D05745F1DE6004600184BB4;
remoteInfo = "RCTPushNotification-tvOS";
};
5E9157321DD0AC6500FF2AA8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */;
@@ -304,13 +221,6 @@
remoteGlobalIDString = 134814201AA4EA6300B7C361;
remoteInfo = RCTAnimation;
};
5E9157341DD0AC6500FF2AA8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 2D2A28201D9B03D100D4039D;
remoteInfo = "RCTAnimation-tvOS";
};
78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */;
@@ -359,6 +269,7 @@
3FFC0F5EFDC54862B1F998DD /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = "<group>"; };
44A39642217548C8ADA91CBA /* libRNImagePicker.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNImagePicker.a; sourceTree = "<group>"; };
4DA7F7A61FC1196F00353191 /* Joplin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Joplin.entitlements; path = Joplin/Joplin.entitlements; sourceTree = "<group>"; };
4DDA31011FC88EEA00B5A80D /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = "<group>"; };
508DD20D1EA341CA8F730F22 /* libRCTImageResizer.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTImageResizer.a; sourceTree = "<group>"; };
51BCEC3BC28046C8BB19531F /* EvilIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = EvilIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"; sourceTree = "<group>"; };
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = "<group>"; };
@@ -389,6 +300,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4DDA31241FC88F2400B5A80D /* libRCTPushNotification.a in Frameworks */,
ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */,
5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */,
146834051AC3E58100842450 /* libReact.a in Frameworks */,
@@ -436,7 +348,6 @@
isa = PBXGroup;
children = (
00C302C01ABCB91800DB3ED1 /* libRCTImage.a */,
3DAD3E841DF850E9000B6D8A /* libRCTImage-tvOS.a */,
);
name = Products;
sourceTree = "<group>";
@@ -445,7 +356,6 @@
isa = PBXGroup;
children = (
00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */,
3DAD3E8C1DF850E9000B6D8A /* libRCTNetwork-tvOS.a */,
);
name = Products;
sourceTree = "<group>";
@@ -470,7 +380,6 @@
isa = PBXGroup;
children = (
139105C11AF99BAD00B5F7CC /* libRCTSettings.a */,
3DAD3E901DF850E9000B6D8A /* libRCTSettings-tvOS.a */,
);
name = Products;
sourceTree = "<group>";
@@ -479,9 +388,7 @@
isa = PBXGroup;
children = (
139FDEF41B06529B00C62182 /* libRCTWebSocket.a */,
3DAD3E991DF850E9000B6D8A /* libRCTWebSocket-tvOS.a */,
4D2A85AA1FBCE3AC0028537D /* libfishhook.a */,
4D2A85AC1FBCE3AC0028537D /* libfishhook-tvOS.a */,
);
name = Products;
sourceTree = "<group>";
@@ -505,17 +412,11 @@
isa = PBXGroup;
children = (
146834041AC3E56700842450 /* libReact.a */,
3DAD3EA31DF850E9000B6D8A /* libReact.a */,
3DAD3EA51DF850E9000B6D8A /* libyoga.a */,
3DAD3EA71DF850E9000B6D8A /* libyoga.a */,
3DAD3EA91DF850E9000B6D8A /* libcxxreact.a */,
3DAD3EAB1DF850E9000B6D8A /* libcxxreact.a */,
3DAD3EAD1DF850E9000B6D8A /* libjschelpers.a */,
3DAD3EAF1DF850E9000B6D8A /* libjschelpers.a */,
4D3A19271FBDDA9400457703 /* libthird-party.a */,
4D3A19291FBDDA9400457703 /* libthird-party.a */,
4D3A192B1FBDDA9400457703 /* libdouble-conversion.a */,
4D3A192D1FBDDA9400457703 /* libdouble-conversion.a */,
);
name = Products;
sourceTree = "<group>";
@@ -591,11 +492,19 @@
name = Products;
sourceTree = "<group>";
};
4DDA31021FC88EEA00B5A80D /* Products */ = {
isa = PBXGroup;
children = (
4DDA310F1FC88EEB00B5A80D /* libRCTPushNotification.a */,
4DDA31111FC88EEB00B5A80D /* libRCTPushNotification-tvOS.a */,
);
name = Products;
sourceTree = "<group>";
};
5E91572E1DD0AC6500FF2AA8 /* Products */ = {
isa = PBXGroup;
children = (
5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */,
5E9157351DD0AC6500FF2AA8 /* libRCTAnimation.a */,
);
name = Products;
sourceTree = "<group>";
@@ -604,7 +513,6 @@
isa = PBXGroup;
children = (
78C398B91ACF4ADC00677621 /* libRCTLinking.a */,
3DAD3E881DF850E9000B6D8A /* libRCTLinking-tvOS.a */,
);
name = Products;
sourceTree = "<group>";
@@ -630,6 +538,7 @@
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
4DDA31011FC88EEA00B5A80D /* RCTPushNotification.xcodeproj */,
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */,
146833FF1AC3E56700842450 /* React.xcodeproj */,
00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */,
@@ -657,7 +566,6 @@
isa = PBXGroup;
children = (
832341B51AAA6A8300B99B32 /* libRCTText.a */,
3DAD3E941DF850E9000B6D8A /* libRCTText-tvOS.a */,
);
name = Products;
sourceTree = "<group>";
@@ -689,7 +597,6 @@
isa = PBXGroup;
children = (
ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */,
4D2A85981FBCE3AC0028537D /* libRCTBlob-tvOS.a */,
);
name = Products;
sourceTree = "<group>";
@@ -728,6 +635,9 @@
DevelopmentTeam = A9BXAFS6CT;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Push = {
enabled = 1;
};
com.apple.iCloud = {
enabled = 1;
};
@@ -779,6 +689,10 @@
ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */;
ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */;
},
{
ProductGroup = 4DDA31021FC88EEA00B5A80D /* Products */;
ProjectRef = 4DDA31011FC88EEA00B5A80D /* RCTPushNotification.xcodeproj */;
},
{
ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */;
ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */;
@@ -888,55 +802,6 @@
remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3E841DF850E9000B6D8A /* libRCTImage-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libRCTImage-tvOS.a";
remoteRef = 3DAD3E831DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3E881DF850E9000B6D8A /* libRCTLinking-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libRCTLinking-tvOS.a";
remoteRef = 3DAD3E871DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3E8C1DF850E9000B6D8A /* libRCTNetwork-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libRCTNetwork-tvOS.a";
remoteRef = 3DAD3E8B1DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3E901DF850E9000B6D8A /* libRCTSettings-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libRCTSettings-tvOS.a";
remoteRef = 3DAD3E8F1DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3E941DF850E9000B6D8A /* libRCTText-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libRCTText-tvOS.a";
remoteRef = 3DAD3E931DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3E991DF850E9000B6D8A /* libRCTWebSocket-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libRCTWebSocket-tvOS.a";
remoteRef = 3DAD3E981DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3EA31DF850E9000B6D8A /* libReact.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libReact.a;
remoteRef = 3DAD3EA21DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3EA51DF850E9000B6D8A /* libyoga.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -944,13 +809,6 @@
remoteRef = 3DAD3EA41DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3EA71DF850E9000B6D8A /* libyoga.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libyoga.a;
remoteRef = 3DAD3EA61DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3EA91DF850E9000B6D8A /* libcxxreact.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -958,13 +816,6 @@
remoteRef = 3DAD3EA81DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3EAB1DF850E9000B6D8A /* libcxxreact.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libcxxreact.a;
remoteRef = 3DAD3EAA1DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3EAD1DF850E9000B6D8A /* libjschelpers.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -972,20 +823,6 @@
remoteRef = 3DAD3EAC1DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
3DAD3EAF1DF850E9000B6D8A /* libjschelpers.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libjschelpers.a;
remoteRef = 3DAD3EAE1DF850E9000B6D8A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4D2A85981FBCE3AC0028537D /* libRCTBlob-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libRCTBlob-tvOS.a";
remoteRef = 4D2A85971FBCE3AC0028537D /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4D2A85AA1FBCE3AC0028537D /* libfishhook.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -993,13 +830,6 @@
remoteRef = 4D2A85A91FBCE3AC0028537D /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4D2A85AC1FBCE3AC0028537D /* libfishhook-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libfishhook-tvOS.a";
remoteRef = 4D2A85AB1FBCE3AC0028537D /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4D2A85BB1FBCE3AD0028537D /* libRCTImageResizer.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -1056,13 +886,6 @@
remoteRef = 4D3A19261FBDDA9400457703 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4D3A19291FBDDA9400457703 /* libthird-party.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libthird-party.a";
remoteRef = 4D3A19281FBDDA9400457703 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4D3A192B1FBDDA9400457703 /* libdouble-conversion.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -1070,13 +893,6 @@
remoteRef = 4D3A192A1FBDDA9400457703 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4D3A192D1FBDDA9400457703 /* libdouble-conversion.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libdouble-conversion.a";
remoteRef = 4D3A192C1FBDDA9400457703 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4DA7F80D1FC1DA9C00353191 /* libRNImagePicker.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -1084,6 +900,20 @@
remoteRef = 4DA7F80C1FC1DA9C00353191 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4DDA310F1FC88EEB00B5A80D /* libRCTPushNotification.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libRCTPushNotification.a;
remoteRef = 4DDA310E1FC88EEB00B5A80D /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4DDA31111FC88EEB00B5A80D /* libRCTPushNotification-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libRCTPushNotification-tvOS.a";
remoteRef = 4DDA31101FC88EEB00B5A80D /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -1091,13 +921,6 @@
remoteRef = 5E9157321DD0AC6500FF2AA8 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
5E9157351DD0AC6500FF2AA8 /* libRCTAnimation.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libRCTAnimation.a;
remoteRef = 5E9157341DD0AC6500FF2AA8 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -1217,6 +1040,7 @@
PRODUCT_NAME = Joplin;
PROVISIONING_PROFILE = "";
PROVISIONING_PROFILE_SPECIFIER = "";
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -1251,6 +1075,7 @@
PRODUCT_NAME = Joplin;
PROVISIONING_PROFILE = "";
PROVISIONING_PROFILE_SPECIFIER = "";
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;

View File

@@ -1,129 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0820"
version = "1.3">
<BuildAction
parallelizeBuildables = "NO"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D2A28121D9B038B00D4039D"
BuildableName = "libReact.a"
BlueprintName = "React-tvOS"
ReferencedContainer = "container:../node_modules/react-native/React/React.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
BuildableName = "Joplin-tvOS.app"
BlueprintName = "Joplin-tvOS"
ReferencedContainer = "container:Joplin.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E48F1E0B4A5D006451C7"
BuildableName = "Joplin-tvOSTests.xctest"
BlueprintName = "Joplin-tvOSTests"
ReferencedContainer = "container:Joplin.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E48F1E0B4A5D006451C7"
BuildableName = "Joplin-tvOSTests.xctest"
BlueprintName = "Joplin-tvOSTests"
ReferencedContainer = "container:Joplin.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
BuildableName = "Joplin-tvOS.app"
BlueprintName = "Joplin-tvOS"
ReferencedContainer = "container:Joplin.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
BuildableName = "Joplin-tvOS.app"
BlueprintName = "Joplin-tvOS"
ReferencedContainer = "container:Joplin.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
BuildableName = "Joplin-tvOS.app"
BlueprintName = "Joplin-tvOS"
ReferencedContainer = "container:Joplin.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -35,11 +35,11 @@
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForTesting = "NO"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
@@ -57,16 +57,6 @@
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "JoplinTests.xctest"
BlueprintName = "JoplinTests"
ReferencedContainer = "container:Joplin.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference

View File

@@ -11,6 +11,7 @@
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTPushNotificationManager.h>
@implementation AppDelegate
@@ -34,4 +35,11 @@
return YES;
}
// Required to register for notifications
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { [RCTPushNotificationManager didRegisterUserNotificationSettings:notificationSettings]; } // Required for the register event.
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { [RCTPushNotificationManager didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; } // Required for the notification event. You must call the completion handler after handling the remote notification.
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { [RCTPushNotificationManager didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; } // Required for the registrationError event.
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [RCTPushNotificationManager didFailToRegisterForRemoteNotificationsWithError:error]; } // Required for the localNotification event.
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { [RCTPushNotificationManager didReceiveLocalNotification:notification]; }
@end

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -147,7 +147,7 @@
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "icon-1024@1x.png",
"filename" : "AppStore.png",
"scale" : "1x"
}
],
@@ -155,4 +155,4 @@
"version" : 1,
"author" : "xcode"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>0.10.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>3</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
@@ -35,12 +35,12 @@
</dict>
</dict>
</dict>
<key>NSCameraUsageDescription</key>
<string>To allow attaching a photo to a note</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>To add geo-location information to a note. Can be disabled in app.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>To allow attaching images to a note</string>
<key>NSCameraUsageDescription</key>
<string>To allow attaching a photo to a note</string>
<key>UIAppFonts</key>
<array>
<string>Entypo.ttf</string>

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.$(CFBundleIdentifier)</string>

View File

@@ -0,0 +1,9 @@
const ArrayUtils = {};
ArrayUtils.unique = function(array) {
return array.filter(function(elem, index, self) {
return index === self.indexOf(elem);
});
}
module.exports = ArrayUtils;

View File

@@ -14,12 +14,22 @@ const { Logger } = require('lib/logger.js');
const { splitCommandString } = require('lib/string-utils.js');
const { sprintf } = require('sprintf-js');
const { reg } = require('lib/registry.js');
const { time } = require('lib/time-utils.js');
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { fileExtension } = require('lib/path-utils.js');
const { shim } = require('lib/shim.js');
const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const os = require('os');
const fs = require('fs-extra');
const EventEmitter = require('events');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
class BaseApplication {
@@ -152,9 +162,6 @@ class BaseApplication {
if (parentType === 'Folder') {
parentId = state.selectedFolderId;
parentType = BaseModel.TYPE_FOLDER;
} else if (parentType === 'Note') {
parentId = state.selectedNoteId;
parentType = BaseModel.TYPE_NOTE;
} else if (parentType === 'Tag') {
parentId = state.selectedTagId;
parentType = BaseModel.TYPE_TAG;
@@ -244,6 +251,11 @@ class BaseApplication {
await this.refreshNotes(newState);
}
if ((action.type == 'SETTING_UPDATE_ONE' && (action.key == 'dateFormat' || action.key == 'timeFormat')) || (action.type == 'SETTING_UPDATE_ALL')) {
time.setDateFormat(Setting.value('dateFormat'));
time.setTimeFormat(Setting.value('timeFormat'));
}
if (action.type == 'TAG_SELECT' || action.type === 'TAG_DELETE') {
await this.refreshNotes(newState);
}
@@ -279,6 +291,7 @@ class BaseApplication {
BaseModel.dispatch = this.store().dispatch;
FoldersScreenUtils.dispatch = this.store().dispatch;
reg.dispatch = this.store().dispatch;
BaseSyncTarget.dispatch = this.store().dispatch;
}
async readFlagsFromFile(flagPath) {
@@ -341,13 +354,13 @@ class BaseApplication {
this.dbLogger_.setLevel(initArgs.logLevel);
if (Setting.value('env') === 'dev') {
this.dbLogger_.setLevel(Logger.LEVEL_DEBUG);
this.dbLogger_.setLevel(Logger.LEVEL_WARN);
}
this.logger_.info('Profile directory: ' + profileDir);
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
//this.database_.setLogExcludedQueryTypes(['SELECT']);
this.database_.setLogExcludedQueryTypes(['SELECT']);
this.database_.setLogger(this.dbLogger_);
await this.database_.open({ name: profileDir + '/database.sqlite' });
@@ -359,6 +372,7 @@ class BaseApplication {
if (Setting.value('firstStart')) {
const locale = shim.detectAndSetLocale(Setting);
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)
} else {
setLocale(Setting.value('locale'));

View File

@@ -0,0 +1,112 @@
const { reg } = require('lib/registry.js');
class BaseSyncTarget {
constructor(db, options = null) {
this.db_ = db;
this.synchronizer_ = null;
this.initState_ = null;
this.logger_ = null;
this.options_ = options;
}
option(name, defaultValue = null) {
return this.options_ && (name in this.options_) ? this.options_[name] : defaultValue;
}
logger() {
return this.logger_;
}
setLogger(v) {
this.logger_ = v;
}
db() {
return this.db_;
}
isAuthenticated() {
return false;
}
static id() {
throw new Error('id() not implemented');
}
// Note: it cannot be called just "name()" because that's a reserved keyword and
// it would throw an obscure error in React Native.
static targetName() {
throw new Error('targetName() not implemented');
}
static label() {
throw new Error('label() not implemented');
}
async initSynchronizer() {
throw new Error('initSynchronizer() not implemented');
}
async initFileApi() {
throw new Error('initFileApi() not implemented');
}
async fileApi() {
if (this.fileApi_) return this.fileApi_;
this.fileApi_ = await this.initFileApi();
return this.fileApi_;
}
// 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
// clients can share and sync to the same file api (see test-utils.js)
setFileApi(v) {
this.fileApi_ = v;
}
async synchronizer() {
if (this.synchronizer_) return this.synchronizer_;
if (this.initState_ == 'started') {
// Synchronizer is already being initialized, so wait here till it's done.
return new Promise((resolve, reject) => {
const iid = setInterval(() => {
if (this.initState_ == 'ready') {
clearInterval(iid);
resolve(this.synchronizer_);
}
if (this.initState_ == 'error') {
clearInterval(iid);
reject(new Error('Could not initialise synchroniser'));
}
}, 1000);
});
} else {
this.initState_ = 'started';
try {
this.synchronizer_ = await this.initSynchronizer();
this.synchronizer_.setLogger(this.logger());
this.synchronizer_.dispatch = BaseSyncTarget.dispatch;
this.initState_ = 'ready';
return this.synchronizer_;
} catch (error) {
this.initState_ = 'error';
throw error;
}
}
}
async syncStarted() {
if (!this.synchronizer_) return false;
if (!this.isAuthenticated()) return false;
const sync = await this.synchronizer();
return sync.state() != 'idle';
}
}
BaseSyncTarget.dispatch = (action) => {};
module.exports = BaseSyncTarget;

View File

@@ -2,6 +2,7 @@ const MarkdownIt = require('markdown-it');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const { Resource } = require('lib/models/resource.js');
const ModelCache = require('lib/ModelCache');
const { shim } = require('lib/shim.js');
const md5 = require('md5');
@@ -14,6 +15,10 @@ class MdToHtml {
this.loadedResources_ = {};
this.cachedContent_ = null;
this.cachedContentKey_ = null;
this.modelCache_ = new ModelCache();
// Must include last "/"
this.resourceBaseUrl_ = ('resourceBaseUrl' in options) ? options.resourceBaseUrl : null;
}
makeContentKey(resources, body, style, options) {
@@ -77,6 +82,15 @@ class MdToHtml {
this.loadedResources_[id] = {};
const resource = await Resource.load(id);
//const resource = await this.modelCache_.load(Resource, id);
if (!resource) {
// Can happen for example if an image is attached to a note, but the resource hasn't
// been downloaded from the sync target yet.
console.warn('Cannot load resource: ' + id);
return;
}
this.loadedResources_[id] = resource;
if (options.onResourceLoaded) options.onResourceLoaded();
@@ -100,7 +114,8 @@ class MdToHtml {
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (mime == 'image/png' || mime == 'image/jpg' || mime == 'image/jpeg' || mime == 'image/gif') {
const src = './' + Resource.filename(resource);
let src = './' + Resource.filename(resource);
if (this.resourceBaseUrl_ !== null) src = this.resourceBaseUrl_ + src;
let output = '<img title="' + htmlentities(title) + '" src="' + src + '"/>';
return output;
}
@@ -117,7 +132,7 @@ class MdToHtml {
if (isResourceUrl && !this.supportsResourceLinks_) {
// In mobile, links to local resources, such as PDF, etc. currently aren't supported.
// Ideally they should be opened in the user's browser.
return '[Resource not yet supported: '; //+ htmlentities(text) + ']';
return '<span style="opacity: 0.5">(Resource not yet supported: '; //+ htmlentities(text) + ']';
} else {
if (isResourceUrl) {
const resourceId = Resource.pathToId(href);
@@ -135,7 +150,7 @@ class MdToHtml {
const isResourceUrl = Resource.isResourceUrl(href);
if (isResourceUrl && !this.supportsResourceLinks_) {
return ']';
return ')</span>';
} else {
return '</a>';
}
@@ -144,6 +159,7 @@ class MdToHtml {
renderTokens_(tokens, options) {
let output = [];
let previousToken = null;
let anchorAttrs = [];
for (let i = 0; i < tokens.length; i++) {
const t = tokens[i];
const nextToken = i < tokens.length ? tokens[i+1] : null;
@@ -175,6 +191,7 @@ class MdToHtml {
if (openTag) {
if (openTag === 'a') {
anchorAttrs.push(attrs);
output.push(this.renderOpenLink_(attrs, options));
} else {
const attrsHtml = this.renderAttrs_(attrs);
@@ -220,7 +237,7 @@ class MdToHtml {
if (closeTag) {
if (closeTag === 'a') {
output.push(this.renderCloseLink_(attrs, options));
output.push(this.renderCloseLink_(anchorAttrs.pop(), options));
} else {
output.push('</' + closeTag + '>');
}
@@ -234,6 +251,7 @@ class MdToHtml {
render(body, style, options = null) {
if (!options) options = {};
if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage';
if (!options.paddingBottom) options.paddingBottom = '0';
const cacheKey = this.makeContentKey(this.loadedResources_, body, style, options);
if (this.cachedContentKey_ === cacheKey) return this.cachedContent_;
@@ -294,16 +312,21 @@ class MdToHtml {
line-height: ` + style.htmlLineHeight + `;
background-color: ` + style.htmlBackgroundColor + `;
font-family: sans-serif;
padding-bottom: ` + options.paddingBottom + `;
}
p, h1, h2, h3, h4, ul, table {
p, h1, h2, h3, h4, h5, h6, ul, table {
margin-top: 0;
margin-bottom: 14px;
}
h1 {
font-size: 1.2em;
font-size: 1.5em;
font-weight: bold;
}
h2 {
font-size: 1.2em;
font-weight: bold;
}
h3, h4, h5, h6 {
font-size: 1em;
font-weight: bold;
}
@@ -318,15 +341,15 @@ class MdToHtml {
position: relative;
top: .5em;
text-decoration: none;
width: 1.9em;
height: 1.9em;
width: 1.65em; /* Need to cut a bit the right border otherwise the SVG will display a black line */
height: 1.7em;
margin-right: .3em;
background-color: ` + style.htmlColor + `;
/* Awesome Font square-o */
-webkit-mask: url("data:image/svg+xml;utf8,<svg viewBox='0 0 1792 1792' xmlns='http://www.w3.org/2000/svg'><path d='M1312 256h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-832q0-66-47-113t-113-47zm288 160v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z'/></svg>");
}
a.checkbox.tick {
left: .124em; /* square-o and check-square-o aren't exactly aligned so add this extra gap to align them */
left: .1245em; /* square-o and check-square-o aren't exactly aligned so add this extra gap to align them */
/* Awesome Font check-square-o */
-webkit-mask: url("data:image/svg+xml;utf8,<svg viewBox='0 0 1792 1792' xmlns='http://www.w3.org/2000/svg'><path d='M1472 930v318q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q63 0 117 25 15 7 18 23 3 17-9 29l-49 49q-10 10-23 10-3 0-9-2-23-6-45-6h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-254q0-13 9-22l64-64q10-10 23-10 6 0 12 3 20 8 20 29zm231-489l-814 814q-24 24-57 24t-57-24l-430-430q-24-24-24-57t24-57l110-110q24-24 57-24t57 24l263 263 647-647q24-24 57-24t57 24l110 110q24 24 24 57t-24 57z'/></svg>");
}

View File

@@ -0,0 +1,39 @@
class ModelCache {
constructor(maxSize) {
this.cache_ = [];
this.maxSize_ = maxSize;
}
fromCache(ModelClass, id) {
for (let i = 0; i < this.cache_.length; i++) {
const c = this.cache_[i];
if (c.id === id && c.modelType === ModelClass.modelType()) return c
}
return null;
}
cache(ModelClass, id, model) {
if (this.fromCache(ModelClass, model.id)) return;
this.cache_.push({
id: id,
model: model,
modelType: ModelClass.modelType(),
});
if (this.cache_.length > this.maxSize_) {
this.cache_.splice(0, 1);
}
}
async load(ModelClass, id) {
const cached = this.fromCache(ModelClass, id);
if (cached) return cached.model;
const output = await ModelClass.load(id);
this.cache(ModelClass, id, output);
return output;
}
}
module.exports = ModelCache;

View File

@@ -0,0 +1,39 @@
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { _ } = require('lib/locale.js');
const { Setting } = require('lib/models/setting.js');
const { FileApi } = require('lib/file-api.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const { Synchronizer } = require('lib/synchronizer.js');
class SyncTargetFilesystem extends BaseSyncTarget {
static id() {
return 2;
}
static targetName() {
return 'filesystem';
}
static label() {
return _('File system');
}
isAuthenticated() {
return true;
}
async initFileApi() {
const fileApi = new FileApi(Setting.value('sync.2.path'), new FileApiDriverLocal());
fileApi.setLogger(this.logger());
fileApi.setSyncTargetId(SyncTargetFilesystem.id());
return fileApi;
}
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetFilesystem;

View File

@@ -0,0 +1,39 @@
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { _ } = require('lib/locale.js');
const { Setting } = require('lib/models/setting.js');
const { FileApi } = require('lib/file-api.js');
const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
const { Synchronizer } = require('lib/synchronizer.js');
class SyncTargetMemory extends BaseSyncTarget {
static id() {
return 1;
}
static targetName() {
return 'memory';
}
static label() {
return 'Memory';
}
isAuthenticated() {
return true;
}
initFileApi() {
const fileApi = new FileApi('/root', new FileApiDriverMemory());
fileApi.setLogger(this.logger());
fileApi.setSyncTargetId(SyncTargetMemory.id());
return fileApi;
}
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetMemory;

View File

@@ -0,0 +1,85 @@
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { _ } = require('lib/locale.js');
const { OneDriveApi } = require('lib/onedrive-api.js');
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 { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
class SyncTargetOneDrive extends BaseSyncTarget {
constructor(db, options = null) {
super(db, options);
this.api_ = null;
}
static id() {
return 3;
}
static targetName() {
return 'onedrive';
}
static label() {
return _('OneDrive');
}
isAuthenticated() {
return this.api().auth();
}
syncTargetId() {
return SyncTargetOneDrive.id();
}
oneDriveParameters() {
return parameters().oneDrive;
}
api() {
if (this.api_) return this.api_;
const isPublic = Setting.value('appType') != 'cli';
this.api_ = new OneDriveApi(this.oneDriveParameters().id, this.oneDriveParameters().secret, isPublic);
this.api_.setLogger(this.logger());
this.api_.on('authRefreshed', (a) => {
this.logger().info('Saving updated OneDrive auth.');
Setting.setValue('sync.' + this.syncTargetId() + '.auth', a ? JSON.stringify(a) : null);
});
let auth = Setting.value('sync.' + this.syncTargetId() + '.auth');
if (auth) {
try {
auth = JSON.parse(auth);
} catch (error) {
this.logger().warn('Could not parse OneDrive auth token');
this.logger().warn(error);
auth = null;
}
this.api_.setAuth(auth);
}
return this.api_;
}
async initFileApi() {
const appDir = await this.api().appDirectory();
const fileApi = new FileApi(appDir, new FileApiDriverOneDrive(this.api()));
fileApi.setSyncTargetId(this.syncTargetId());
fileApi.setLogger(this.logger());
return fileApi;
}
async initSynchronizer() {
if (!this.isAuthenticated()) throw new Error('User is not authentified');
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetOneDrive;

View File

@@ -0,0 +1,37 @@
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const { _ } = require('lib/locale.js');
const { OneDriveApi } = require('lib/onedrive-api.js');
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 { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
class SyncTargetOneDriveDev extends SyncTargetOneDrive {
static id() {
return 4;
}
static targetName() {
return 'onedrive_dev';
}
static label() {
return _('OneDrive Dev (For testing only)');
}
syncTargetId() {
return SyncTargetOneDriveDev.id();
}
oneDriveParameters() {
return parameters('dev').oneDrive;
}
}
const staticSelf = SyncTargetOneDriveDev;
module.exports = SyncTargetOneDriveDev;

View File

@@ -0,0 +1,39 @@
class SyncTargetRegistry {
static classById(syncTargetId) {
const info = SyncTargetRegistry.reg_[syncTargetId];
if (!info) throw new Error('Invalid id: ' + syncTargetId);
return info.classRef;
}
static addClass(SyncTargetClass) {
this.reg_[SyncTargetClass.id()] = {
id: SyncTargetClass.id(),
name: SyncTargetClass.targetName(),
label: SyncTargetClass.label(),
classRef: SyncTargetClass,
};
}
static nameToId(name) {
for (let n in this.reg_) {
if (!this.reg_.hasOwnProperty(n)) continue;
if (this.reg_[n].name === name) return this.reg_[n].id;
}
throw new Error('Name not found: ' + name);
}
static idAndLabelPlainObject() {
let output = {};
for (let n in this.reg_) {
if (!this.reg_.hasOwnProperty(n)) continue;
output[n] = this.reg_[n].label;
}
return output;
}
}
SyncTargetRegistry.reg_ = {};
module.exports = SyncTargetRegistry;

View File

@@ -286,7 +286,7 @@ class BaseModel {
return this.db().transactionExecBatch(queries).then(() => {
o = Object.assign({}, o);
o.id = modelId;
if (modelId) o.id = modelId;
if ('updated_time' in saveQuery.modObject) o.updated_time = saveQuery.modObject.updated_time;
if ('created_time' in saveQuery.modObject) o.created_time = saveQuery.modObject.created_time;
if ('user_updated_time' in saveQuery.modObject) o.user_updated_time = saveQuery.modObject.user_updated_time;
@@ -331,14 +331,14 @@ class BaseModel {
}
static delete(id, options = null) {
options = this.modOptions(options);
if (!id) throw new Error('Cannot delete object without an ID');
options = this.modOptions(options);
return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id = ?', [id]);
}
static batchDelete(ids, options = null) {
if (!ids.length) return;
options = this.modOptions(options);
if (!ids.length) throw new Error('Cannot delete object without an ID');
return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id IN ("' + ids.join('","') + '")');
}
@@ -360,6 +360,7 @@ BaseModel.TYPE_RESOURCE = 4;
BaseModel.TYPE_TAG = 5;
BaseModel.TYPE_NOTE_TAG = 6;
BaseModel.TYPE_SEARCH = 7;
BaseModel.TYPE_ALARM = 8;
BaseModel.db_ = null;
BaseModel.dispatch = function(o) {};

View File

@@ -87,7 +87,10 @@ class Dropdown extends React.Component {
let headerLabel = '...';
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.value === this.props.selectedValue) headerLabel = item.label;
if (item.value === this.props.selectedValue) {
headerLabel = item.label;
break;
}
}
const closeList = () => {

View File

@@ -48,6 +48,7 @@ class NoteBodyViewer extends Component {
onResourceLoaded: () => {
this.forceUpdate();
},
paddingBottom: '3.8em', // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
};
const html = this.mdToHtml_.render(note ? note.body : '', this.props.webViewStyle, mdOptions);
@@ -79,7 +80,10 @@ class NoteBodyViewer extends Component {
// So we use scalesPageToFix=false on iOS along with that CSS rule.
// `baseUrl` is where the images will be loaded from. So images must use a path relative to resourceDir.
const source = { html: html, baseUrl: 'file://' + Setting.value('resourceDir') + '/' };
const source = {
html: html,
baseUrl: 'file://' + Setting.value('resourceDir') + '/',
};
return (
<View style={style}>

View File

@@ -1,6 +1,6 @@
const React = require('react'); const Component = React.Component;
const { connect } = require('react-redux');
const { ListView, Text, TouchableHighlight, View, StyleSheet } = require('react-native');
const { ListView, Text, TouchableOpacity , View, StyleSheet } = require('react-native');
const { Log } = require('lib/log.js');
const { _ } = require('lib/locale.js');
const { Checkbox } = require('lib/components/checkbox.js');
@@ -41,13 +41,16 @@ class NoteItemComponent extends Component {
paddingRight: theme.marginRight,
paddingTop: theme.itemMarginTop,
paddingBottom: theme.itemMarginBottom,
backgroundColor: theme.backgroundColor,
//backgroundColor: theme.backgroundColor,
},
listItemText: {
flex: 1,
color: theme.color,
fontSize: theme.fontSize,
},
selectionWrapper: {
backgroundColor: theme.backgroundColor,
},
};
styles.listItemWithCheckbox = Object.assign({}, styles.listItem);
@@ -59,6 +62,9 @@ class NoteItemComponent extends Component {
styles.listItemTextWithCheckbox.marginTop = styles.listItem.paddingTop - 1;
styles.listItemTextWithCheckbox.marginBottom = styles.listItem.paddingBottom;
styles.selectionWrapperSelected = Object.assign({}, styles.selectionWrapper);
styles.selectionWrapperSelected.backgroundColor = theme.selectedColor;
this.styles_[this.props.theme] = StyleSheet.create(styles);
return this.styles_[this.props.theme];
}
@@ -76,10 +82,26 @@ class NoteItemComponent extends Component {
onPress() {
if (!this.props.note) return;
if (this.props.noteSelectionEnabled) {
this.props.dispatch({
type: 'NOTE_SELECTION_TOGGLE',
id: this.props.note.id,
});
} else {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'Note',
noteId: this.props.note.id,
});
}
}
onLongPress() {
if (!this.props.note) return;
this.props.dispatch({
type: 'NAV_GO',
routeName: 'Note',
noteId: this.props.note.id,
type: this.props.noteSelectionEnabled ? 'NOTE_SELECTION_TOGGLE' : 'NOTE_SELECTION_START',
id: this.props.note.id,
});
}
@@ -104,19 +126,26 @@ class NoteItemComponent extends Component {
const listItemStyle = isTodo ? this.styles().listItemWithCheckbox : this.styles().listItem;
const listItemTextStyle = isTodo ? this.styles().listItemTextWithCheckbox : this.styles().listItemText;
const rootStyle = isTodo && checkboxChecked ? {opacity: 0.4} : {};
const opacityStyle = isTodo && checkboxChecked ? {opacity: 0.4} : {};
const isSelected = this.props.noteSelectionEnabled && this.props.selectedNoteIds.indexOf(note.id) >= 0;
const selectionWrapperStyle = isSelected ? this.styles().selectionWrapperSelected : this.styles().selectionWrapper;
return (
<TouchableHighlight onPress={() => this.onPress()} underlayColor="#0066FF" style={rootStyle}>
<View style={ listItemStyle }>
<Checkbox
style={checkboxStyle}
checked={checkboxChecked}
onChange={(checked) => this.todoCheckbox_change(checked)}
/>
<Text style={listItemTextStyle}>{note.title}</Text>
<TouchableOpacity onPress={() => this.onPress()} onLongPress={() => this.onLongPress() } activeOpacity={0.5}>
<View style={ selectionWrapperStyle }>
<View style={ opacityStyle }>
<View style={ listItemStyle }>
<Checkbox
style={checkboxStyle}
checked={checkboxChecked}
onChange={(checked) => this.todoCheckbox_change(checked)}
/>
<Text style={listItemTextStyle}>{note.title}</Text>
</View>
</View>
</View>
</TouchableHighlight>
</TouchableOpacity>
);
}
@@ -126,6 +155,8 @@ const NoteItem = connect(
(state) => {
return {
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedNoteIds: state.selectedNoteIds,
};
}
)(NoteItemComponent)

View File

@@ -113,6 +113,7 @@ const NoteList = connect(
items: state.notes,
notesSource: state.notesSource,
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
};
}
)(NoteListComponent)

View File

@@ -4,15 +4,22 @@ const { Platform, View, Text, Button, StyleSheet, TouchableOpacity, Image } = re
const Icon = require('react-native-vector-icons/Ionicons').default;
const { Log } = require('lib/log.js');
const { BackButtonService } = require('lib/services/back-button.js');
const { ReportService } = require('lib/services/report.js');
const { Menu, MenuOptions, MenuOption, MenuTrigger } = require('react-native-popup-menu');
const { _ } = require('lib/locale.js');
const { Setting } = require('lib/models/setting.js');
const { Note } = require('lib/models/note.js');
const { Folder } = require('lib/models/folder.js');
const { FileApi } = require('lib/file-api.js');
const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
const { reg } = require('lib/registry.js');
const { themeStyle } = require('lib/components/global-style.js');
const { ItemList } = require('lib/components/ItemList.js');
const { Dropdown } = require('lib/components/Dropdown.js');
const { time } = require('lib/time-utils');
const RNFS = require('react-native-fs');
const { dialogs } = require('lib/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
// Rather than applying a padding to the whole bar, it is applied to each
// individual component (button, picker, etc.) so that the touchable areas
@@ -144,7 +151,6 @@ class ScreenHeaderComponent extends Component {
async backButton_press() {
await BackButtonService.back();
//this.props.dispatch({ type: 'NAV_BACK' });
}
searchButton_press() {
@@ -154,6 +160,17 @@ class ScreenHeaderComponent extends Component {
});
}
async deleteButton_press() {
// Dialog needs to be displayed as a child of the parent component, otherwise
// it won't be visible within the header component.
const ok = await dialogs.confirm(this.props.parentComponent, _('Delete these notes?'));
if (!ok) return;
const noteIds = this.props.selectedNoteIds;
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
await Note.batchDelete(noteIds);
}
menu_select(value) {
if (typeof(value) == 'function') {
value();
@@ -181,6 +198,32 @@ class ScreenHeaderComponent extends Component {
});
}
async debugReport_press() {
const service = new ReportService();
const logItems = await reg.logger().lastEntries(null);
const logItemRows = [
['Date','Level','Message']
];
for (let i = 0; i < logItems.length; i++) {
const item = logItems[i];
logItemRows.push([
time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss'),
item.level,
item.message
]);
}
const logItemCsv = service.csvCreate(logItemRows);
const itemListCsv = await service.basicItemList({ format: 'csv' });
const filePath = RNFS.ExternalDirectoryPath + '/syncReport-' + (new Date()).getTime() + '.txt';
const finalText = [logItemCsv, itemListCsv].join("\n--------------------------------------------------------------------------------");
await RNFS.writeFile(filePath, finalText);
alert('Debug report exported to ' + filePath);
}
render() {
function sideMenuButton(styles, onPress) {
@@ -227,52 +270,98 @@ class ScreenHeaderComponent extends Component {
);
}
let key = 0;
let menuOptionComponents = [];
for (let i = 0; i < this.props.menuOptions.length; i++) {
let o = this.props.menuOptions[i];
menuOptionComponents.push(
<MenuOption value={o.onPress} key={'menuOption_' + key++} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{o.title}</Text>
</MenuOption>);
function deleteButton(styles, onPress) {
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.iconButton}>
<Icon name='md-trash' style={styles.topIcon} />
</View>
</TouchableOpacity>
);
}
if (this.props.showAdvancedOptions) {
let key = 0;
let menuOptionComponents = [];
if (!this.props.noteSelectionEnabled) {
for (let i = 0; i < this.props.menuOptions.length; i++) {
let o = this.props.menuOptions[i];
if (o.isDivider) {
menuOptionComponents.push(<View key={'menuOption_' + key++} style={this.styles().divider}/>);
} else {
menuOptionComponents.push(
<MenuOption value={o.onPress} key={'menuOption_' + key++} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{o.title}</Text>
</MenuOption>);
}
}
if (this.props.showAdvancedOptions) {
if (menuOptionComponents.length) {
menuOptionComponents.push(<View key={'menuOption_showAdvancedOptions'} style={this.styles().divider}/>);
}
menuOptionComponents.push(
<MenuOption value={() => this.log_press()} key={'menuOption_log'} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Log')}</Text>
</MenuOption>);
menuOptionComponents.push(
<MenuOption value={() => this.status_press()} key={'menuOption_status'} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Status')}</Text>
</MenuOption>);
if (Platform.OS === 'android') {
menuOptionComponents.push(
<MenuOption value={() => this.debugReport_press()} key={'menuOption_debugReport'} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Export Debug Report')}</Text>
</MenuOption>);
}
}
if (menuOptionComponents.length) {
menuOptionComponents.push(<View key={'menuOption_' + key++} style={this.styles().divider}/>);
}
menuOptionComponents.push(
<MenuOption value={() => this.log_press()} key={'menuOption_' + key++} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Log')}</Text>
<MenuOption value={() => this.config_press()} key={'menuOption_config'} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Configuration')}</Text>
</MenuOption>);
} else {
menuOptionComponents.push(
<MenuOption value={() => this.status_press()} key={'menuOption_' + key++} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Status')}</Text>
<MenuOption value={() => this.deleteButton_press()} key={'menuOption_delete'} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Delete')}</Text>
</MenuOption>);
}
if (menuOptionComponents.length) {
menuOptionComponents.push(<View key={'menuOption_' + key++} style={this.styles().divider}/>);
}
menuOptionComponents.push(
<MenuOption value={() => this.config_press()} key={'menuOption_' + key++} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Configuration')}</Text>
</MenuOption>);
const createTitleComponent = () => {
const themeId = Setting.value('theme');
const theme = themeStyle(themeId);
const folderPickerOptions = this.props.folderPickerOptions;
if (folderPickerOptions && folderPickerOptions.enabled) {
const titlePickerItems = (mustSelect) => {
let output = [];
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
for (let i = 0; i < this.props.folders.length; i++) {
let f = this.props.folders[i];
output.push({ label: f.title, value: f.id });
}
output.sort((a, b) => {
if (a.value === null) return -1;
if (b.value === null) return +1;
return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : +1;
});
return output;
}
const p = this.props.titlePicker;
if (p) {
return (
<Dropdown
items={p.items}
items={titlePickerItems(!!folderPickerOptions.mustSelect)}
itemHeight={35}
selectedValue={p.selectedValue}
selectedValue={('selectedFolderId' in folderPickerOptions) ? folderPickerOptions.selectedFolderId : null}
itemListStyle={{
backgroundColor: theme.backgroundColor,
}}
@@ -284,7 +373,30 @@ class ScreenHeaderComponent extends Component {
color: theme.color,
fontSize: theme.fontSize,
}}
onValueChange={(itemValue, itemIndex) => { if (p.onValueChange) p.onValueChange(itemValue, itemIndex); }}
onValueChange={async (folderId, itemIndex) => {
// If onValueChange is specified, use this as a callback, otherwise do the default
// which is to take the selectedNoteIds from the state and move them to the
// chosen folder.
if (folderPickerOptions.onValueChange) {
folderPickerOptions.onValueChange(folderId, itemIndex);
return;
}
if (!folderId) return;
const noteIds = this.props.selectedNoteIds;
if (!noteIds.length) return;
const folder = await Folder.load(folderId);
const ok = noteIds.length > 1 ? await dialogs.confirm(this.props.parentComponent, _('Move %d notes to notebook "%s"?', noteIds.length, folder.title)) : true;
if (!ok) return;
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], folderId);
}
}}
/>
);
} else {
@@ -294,22 +406,32 @@ class ScreenHeaderComponent extends Component {
}
const titleComp = createTitleComponent();
const sideMenuComp = this.props.noteSelectionEnabled ? null : sideMenuButton(this.styles(), () => this.sideMenuButton_press());
const backButtonComp = backButton(this.styles(), () => this.backButton_press(), !this.props.historyCanGoBack);
const searchButtonComp = this.props.noteSelectionEnabled ? null : searchButton(this.styles(), () => this.searchButton_press());
const deleteButtonComp = this.props.noteSelectionEnabled ? deleteButton(this.styles(), () => this.deleteButton_press()) : null;
const menuComp = (
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}>
<MenuTrigger style={{ paddingTop: PADDING_V, paddingBottom: PADDING_V }}>
<Text style={this.styles().contextMenuTrigger}> &#8942;</Text>
</MenuTrigger>
<MenuOptions>
{ menuOptionComponents }
</MenuOptions>
</Menu>
);
return (
<View style={this.styles().container} >
{ sideMenuButton(this.styles(), () => this.sideMenuButton_press()) }
{ backButton(this.styles(), () => this.backButton_press(), !this.props.historyCanGoBack) }
{ sideMenuComp }
{ backButtonComp }
{ saveButton(this.styles(), () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) }
{ titleComp }
{ searchButton(this.styles(), () => this.searchButton_press()) }
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}>
<MenuTrigger style={{ paddingTop: PADDING_V, paddingBottom: PADDING_V }}>
<Text style={this.styles().contextMenuTrigger}> &#8942;</Text>
</MenuTrigger>
<MenuOptions>
{ menuOptionComponents }
</MenuOptions>
</Menu>
{ searchButtonComp }
{ deleteButtonComp }
{ menuComp }
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
</View>
);
}
@@ -325,8 +447,11 @@ const ScreenHeader = connect(
return {
historyCanGoBack: state.historyCanGoBack,
locale: state.settings.locale,
folders: state.folders,
theme: state.settings.theme,
showAdvancedOptions: state.settings.showAdvancedOptions,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedNoteIds: state.selectedNoteIds,
};
}
)(ScreenHeaderComponent)

View File

@@ -145,10 +145,11 @@ class ConfigScreenComponent extends BaseScreenComponent {
render() {
const settings = this.props.settings;
const keys = Setting.keys(true, 'mobile');
let settingComps = [];
for (let key in settings) {
if (key == 'sync.target') continue;
if (!settings.hasOwnProperty(key)) continue;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key == 'sync.target' && !settings.showAdvancedOptions) continue;
if (!Setting.isPublic(key)) continue;
const comp = this.settingToComponent(key, settings[key]);

View File

@@ -12,7 +12,7 @@ const { BackButtonService } = require('lib/services/back-button.js');
const { BaseModel } = require('lib/base-model.js');
const { ActionButton } = require('lib/components/action-button.js');
const Icon = require('react-native-vector-icons/Ionicons').default;
const { fileExtension, basename } = require('lib/path-utils.js');
const { fileExtension, basename, safeFileExtension } = require('lib/path-utils.js');
const mimeUtils = require('lib/mime-utils.js').mime;
const { ScreenHeader } = require('lib/components/screen-header.js');
const { time } = require('lib/time-utils.js');
@@ -30,6 +30,8 @@ const { DocumentPicker, DocumentPickerUtil } = require('react-native-document-pi
const ImageResizer = require('react-native-image-resizer').default;
const shared = require('lib/components/shared/note-screen-shared.js');
const ImagePicker = require('react-native-image-picker');
const AlarmService = require('lib/services/AlarmService.js');
const { SelectDateTimeDialog } = require('lib/components/select-date-time-dialog.js');
class NoteScreenComponent extends BaseScreenComponent {
@@ -48,6 +50,7 @@ class NoteScreenComponent extends BaseScreenComponent {
lastSavedNote: null,
isLoading: true,
titleTextInputHeight: 20,
alarmDialogShown: false,
};
// iOS doesn't support multiline text fields properly so disable it
@@ -292,6 +295,9 @@ class NoteScreenComponent extends BaseScreenComponent {
resource.id = uuid.create();
resource.mime = mimeType;
resource.title = pickerResponse.fileName ? pickerResponse.fileName : _('Untitled');
resource.file_extension = safeFileExtension(fileExtension(pickerResponse.fileName));
if (!resource.mime) resource.mime = 'application/octet-stream';
let targetPath = Resource.fullPath(resource);
@@ -337,6 +343,23 @@ class NoteScreenComponent extends BaseScreenComponent {
shared.toggleIsTodo_onPress(this);
}
setAlarm_onPress() {
this.setState({ alarmDialogShown: true });
}
async onAlarmDialogAccept(date) {
let newNote = Object.assign({}, this.state.note);
newNote.todo_due = date ? date.getTime() : 0;
await this.saveOneProperty('todo_due', date ? date.getTime() : 0);
this.setState({ alarmDialogShown: false });
}
onAlarmDialogReject() {
this.setState({ alarmDialogShown: false });
}
showMetadata_onPress() {
shared.showMetadata_onPress(this);
}
@@ -364,19 +387,21 @@ class NoteScreenComponent extends BaseScreenComponent {
let canAttachPicture = true;
if (Platform.OS === 'android' && Platform.Version < 21) canAttachPicture = false;
if (canAttachPicture) {
output.push({ title: _('Attach image'), onPress: () => { this.attachImage_onPress(); } });
output.push({ title: _('Attach any other file'), onPress: () => { this.attachFile_onPress(); } });
output.push({ title: _('Attach photo'), onPress: () => { this.attachImage_onPress(); } });
output.push({ title: _('Attach any file'), onPress: () => { this.attachFile_onPress(); } });
output.push({ isDivider: true });
}
output.push({ title: _('Delete note'), onPress: () => { this.deleteNote_onPress(); } });
// if (isTodo) {
// let text = note.todo_due ? _('Edit/Clear alarm') : _('Set an alarm');
// output.push({ title: text, onPress: () => { this.setAlarm_onPress(); } });
// }
if (isTodo) {
output.push({ title: _('Set alarm'), onPress: () => { this.setState({ alarmDialogShown: true }) }});;
}
output.push({ title: isTodo ? _('Convert to regular note') : _('Convert to todo'), onPress: () => { this.toggleIsTodo_onPress(); } });
output.push({ title: isTodo ? _('Convert to note') : _('Convert to todo'), onPress: () => { this.toggleIsTodo_onPress(); } });
output.push({ isDivider: true });
if (this.props.showAdvancedOptions) output.push({ title: this.state.showNoteMetadata ? _('Hide metadata') : _('Show metadata'), onPress: () => { this.showMetadata_onPress(); } });
output.push({ title: _('View location on map'), onPress: () => { this.showOnMap_onPress(); } });
output.push({ title: _('View on map'), onPress: () => { this.showOnMap_onPress(); } });
output.push({ isDivider: true });
output.push({ title: _('Delete'), onPress: () => { this.deleteNote_onPress(); } });
return output;
}
@@ -448,15 +473,6 @@ class NoteScreenComponent extends BaseScreenComponent {
return <ActionButton multiStates={true} buttons={buttons} buttonIndex={0} />
}
const titlePickerItems = () => {
let output = [];
for (let i = 0; i < this.props.folders.length; i++) {
let f = this.props.folders[i];
output.push({ label: f.title, value: f.id });
}
return output;
}
const actionButtonComp = renderActionButton();
let showSaveButton = this.state.mode == 'edit' || this.isModified() || this.saveButtonHasBeenShown_;
@@ -487,6 +503,8 @@ class NoteScreenComponent extends BaseScreenComponent {
paddingBottom: 10, // Added for iOS (Not needed for Android??)
}
const dueDate = isTodo && note.todo_due ? new Date(note.todo_due) : null;
const titleComp = (
<View style={titleContainerStyle}>
{ isTodo && <Checkbox style={checkboxStyle} checked={!!Number(note.todo_completed)} onChange={(checked) => { this.todoCheckbox_change(checked) }} /> }
@@ -506,19 +524,10 @@ class NoteScreenComponent extends BaseScreenComponent {
return (
<View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader
titlePicker={{
items: titlePickerItems(),
selectedValue: folder ? folder.id : null,
folderPickerOptions={{
enabled: true,
selectedFolderId: folder ? folder.id : null,
onValueChange: async (itemValue, itemIndex) => {
let note = Object.assign({}, this.state.note);
// RN bug: https://github.com/facebook/react-native/issues/9220
// The Picker fires the onValueChange when the component is initialized
// so we need to check that it has actually changed.
if (note.parent_id == itemValue) return;
reg.logger().info('Moving note: ' + note.parent_id + ' => ' + itemValue);
if (note.id) await Note.moveToFolder(note.id, itemValue);
note.parent_id = itemValue;
@@ -529,7 +538,7 @@ class NoteScreenComponent extends BaseScreenComponent {
note: note,
folder: folder,
});
}
},
}}
menuOptions={this.menuOptions()}
showSaveButton={showSaveButton}
@@ -540,6 +549,14 @@ class NoteScreenComponent extends BaseScreenComponent {
{ bodyComponent }
{ actionButtonComp }
{ this.state.showNoteMetadata && <Text style={this.styles().metadata}>{this.state.noteMetadata}</Text> }
<SelectDateTimeDialog
shown={this.state.alarmDialogShown}
date={dueDate}
onAccept={(date) => this.onAlarmDialogAccept(date) }
onReject={() => this.onAlarmDialogReject() }
/>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
</View>
);
@@ -550,7 +567,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const NoteScreen = connect(
(state) => {
return {
noteId: state.selectedNoteId,
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
folderId: state.selectedFolderId,
itemType: state.selectedItemType,
folders: state.folders,

View File

@@ -142,12 +142,22 @@ class NotesScreenComponent extends BaseScreenComponent {
let title = parent ? parent.title : null;
const addFolderNoteButtons = this.props.selectedFolderId && this.props.selectedFolderId != Folder.conflictFolderId();
const thisComp = this;
const actionButtonComp = this.props.noteSelectionEnabled ? null : <ActionButton addFolderNoteButtons={addFolderNoteButtons} parentFolderId={this.props.selectedFolderId}></ActionButton>
return (
<View style={rootStyle}>
<ScreenHeader title={title} menuOptions={this.menuOptions()} />
<ScreenHeader
title={title}
menuOptions={this.menuOptions()}
parentComponent={thisComp}
folderPickerOptions={{
enabled: this.props.noteSelectionEnabled,
mustSelect: true,
}}
/>
<NoteList style={{flex: 1}}/>
<ActionButton addFolderNoteButtons={addFolderNoteButtons} parentFolderId={this.props.selectedFolderId}></ActionButton>
{ actionButtonComp }
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
</View>
);
@@ -160,6 +170,7 @@ const NotesScreen = connect(
folders: state.folders,
tags: state.tags,
selectedFolderId: state.selectedFolderId,
selectedNoteIds: state.selectedNoteIds,
selectedTagId: state.selectedTagId,
notesParentType: state.notesParentType,
notes: state.notes,
@@ -167,6 +178,7 @@ const NotesScreen = connect(
notesSource: state.notesSource,
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
};
}
)(NotesScreenComponent)

View File

@@ -8,6 +8,7 @@ const { ScreenHeader } = require('lib/components/screen-header.js');
const { reg } = require('lib/registry.js');
const { _ } = require('lib/locale.js');
const { BaseScreenComponent } = require('lib/components/base-screen.js');
const parseUri = require('lib/parseUri');
class OneDriveLoginScreenComponent extends BaseScreenComponent {
@@ -28,11 +29,11 @@ class OneDriveLoginScreenComponent extends BaseScreenComponent {
}
startUrl() {
return reg.oneDriveApi().authCodeUrl(this.redirectUrl());
return reg.syncTarget().api().authCodeUrl(this.redirectUrl());
}
redirectUrl() {
return reg.oneDriveApi().nativeClientRedirectUrl();
return reg.syncTarget().api().nativeClientRedirectUrl();
}
async webview_load(noIdeaWhatThisIs) {
@@ -40,19 +41,19 @@ class OneDriveLoginScreenComponent extends BaseScreenComponent {
// doesn't exist, use this for now. The whole component is completely undocumented
// at the moment so it's likely to change.
const url = noIdeaWhatThisIs.url;
const parsedUrl = parseUri(url);
if (!this.authCode_ && url.indexOf(this.redirectUrl() + '?code=') === 0) {
Log.info('URL: ' + url);
if (!this.authCode_ && parsedUrl && parsedUrl.queryKey && parsedUrl.queryKey.code) {
Log.info('URL: ', url, parsedUrl.queryKey);
let code = url.split('?code=');
this.authCode_ = code[1];
this.authCode_ = parsedUrl.queryKey.code
try {
await reg.oneDriveApi().execTokenRequest(this.authCode_, this.redirectUrl(), true);
await reg.syncTarget().api().execTokenRequest(this.authCode_, this.redirectUrl(), true);
this.props.dispatch({ type: 'NAV_BACK' });
reg.scheduleSync(0);
} catch (error) {
alert(error.message);
alert('Could not login to OneDrive. Please try again\n\n' + error.message + '\n\n' + url);
}
this.authCode_ = null;

View File

@@ -8,6 +8,8 @@ const { Note } = require('lib/models/note.js');
const { NoteItem } = require('lib/components/note-item.js');
const { BaseScreenComponent } = require('lib/components/base-screen.js');
const { themeStyle } = require('lib/components/global-style.js');
const { dialogs } = require('lib/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
class SearchScreenComponent extends BaseScreenComponent {
@@ -139,9 +141,18 @@ class SearchScreenComponent extends BaseScreenComponent {
rootStyle.flex = 0.001; // This is a bit of a hack but it seems to work fine - it makes the component invisible but without unmounting it
}
const thisComponent = this;
return (
<View style={rootStyle}>
<ScreenHeader title={_('Search')}/>
<ScreenHeader
title={_('Search')}
parentComponent={thisComponent}
folderPickerOptions={{
enabled: this.props.noteSelectionEnabled,
mustSelect: true,
}}
/>
<View style={this.styles().body}>
<View style={this.styles().searchContainer}>
<TextInput
@@ -163,6 +174,7 @@ class SearchScreenComponent extends BaseScreenComponent {
renderItem={(event) => <NoteItem note={event.item}/>}
/>
</View>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
</View>
);
}
@@ -174,6 +186,7 @@ const SearchScreen = connect(
return {
query: state.searchQuery,
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
};
}
)(SearchScreenComponent)

View File

@@ -0,0 +1,94 @@
import React, { Component } from 'react';
import { Keyboard, View, Button, Text, StyleSheet, Linking, Image } from 'react-native';
import PopupDialog, { DialogTitle, DialogButton } from 'react-native-popup-dialog';
import DatePicker from 'react-native-datepicker'
import moment from 'moment';
import { _ } from 'lib/locale.js';
class SelectDateTimeDialog extends Component {
constructor() {
super();
this.dialog_ = null;
this.shown_ = false;
this.state = { date: null };
}
componentWillReceiveProps(newProps) {
if (newProps.date != this.state.date) {
this.setState({ date: newProps.date });
}
if ('shown' in newProps) {
this.show(newProps.shown);
}
}
show(doShow = true) {
if (doShow) {
this.dialog_.show();
} else {
this.dialog_.dismiss();
}
this.shown_ = doShow;
}
dismiss() {
this.show(false);
}
dateTimeFormat() {
return "MM/DD/YYYY HH:mm";
}
stringToDate(s) {
return moment(s, this.dateTimeFormat()).toDate();
}
onAccept() {
if (this.props.onAccept) this.props.onAccept(this.state.date);
}
onReject() {
if (this.props.onReject) this.props.onReject();
}
onClear() {
if (this.props.onAccept) this.props.onAccept(null);
}
render() {
const popupActions = [
<DialogButton text="Save alarm" align="center" onPress={() => this.onAccept()} key="saveButton" />,
<DialogButton text="Clear alarm" align="center" onPress={() => this.onClear()} key="clearButton" />,
<DialogButton text="Cancel" align="center" onPress={() => this.onReject()} key="cancelButton" />,
];
return (
<PopupDialog
ref={(dialog) => { this.dialog_ = dialog; }}
dialogTitle={<DialogTitle title={_('Set alarm')} />}
actions={popupActions}
width={0.9}
height={350}
>
<View style={{flex:1, margin: 20, alignItems:'center'}}>
<DatePicker
date={this.state.date}
mode="datetime"
placeholder={_('Select date')}
format={this.dateTimeFormat()}
confirmBtnText={_('Confirm')}
cancelBtnText={_('Cancel')}
onDateChange={(date) => { this.setState({ date: this.stringToDate(date) }); }}
style={{width:300}}
/>
</View>
</PopupDialog>
);
}
}
export { SelectDateTimeDialog };

View File

@@ -13,11 +13,11 @@ shared.noteExists = async function(noteId) {
shared.saveNoteButton_press = async function(comp) {
let note = Object.assign({}, comp.state.note);
// Note has been deleted while user was modifying it. In that, we
// Note has been deleted while user was modifying it. In that case, we
// just save a new note by clearing the note ID.
if (note.id && !(await shared.noteExists(note.id))) delete note.id;
reg.logger().info('Saving note: ', note);
// reg.logger().info('Saving note: ', note);
if (!note.parent_id) {
let folder = await Folder.defaultFolder();
@@ -33,12 +33,37 @@ shared.saveNoteButton_press = async function(comp) {
if (isNew && !note.title) {
note.title = Note.defaultTitle(note);
}
note = await Note.save(note);
// Save only the properties that have changed
let diff = null;
if (!isNew) {
diff = BaseModel.diffObjects(comp.state.lastSavedNote, note);
diff.type_ = note.type_;
diff.id = note.id;
} else {
diff = Object.assign({}, note);
}
const savedNote = await Note.save(diff);
const stateNote = comp.state.note;
// Re-assign any property that might have changed during saving (updated_time, etc.)
note = Object.assign(note, savedNote);
if (stateNote) {
// But we preserve the current title and body because
// the user might have changed them between the time
// saveNoteButton_press was called and the note was
// saved (it's done asynchronously)
note.title = stateNote.title;
note.body = stateNote.body;
}
comp.setState({
lastSavedNote: Object.assign({}, note),
note: note,
});
if (isNew) Note.updateGeolocation(note.id);
comp.refreshNoteMetadata();
}
@@ -50,7 +75,7 @@ shared.saveOneProperty = async function(comp, name, value) {
// just save a new note by clearing the note ID.
if (note.id && !(await shared.noteExists(note.id))) delete note.id;
reg.logger().info('Saving note property: ', note.id, name, value);
// reg.logger().info('Saving note property: ', note.id, name, value);
if (note.id) {
let toSave = { id: note.id };

View File

@@ -36,7 +36,7 @@ shared.synchronize_press = async function(comp) {
const action = comp.props.syncStarted ? 'cancel' : 'start';
if (Setting.value('sync.target') == Setting.SYNC_TARGET_ONEDRIVE && !reg.oneDriveApi().auth()) {
if (!reg.syncTarget().isAuthenticated()) {
comp.props.dispatch({
type: 'NAV_GO',
routeName: 'OneDriveLogin',
@@ -46,7 +46,7 @@ shared.synchronize_press = async function(comp) {
let sync = null;
try {
sync = await reg.synchronizer(Setting.value('sync.target'))
sync = await reg.syncTarget().synchronizer();
} catch (error) {
reg.logger().info('Could not acquire synchroniser:');
reg.logger().info(error);

View File

@@ -67,6 +67,10 @@ class DatabaseDriverNode {
});
}
lastInsertId() {
throw new Error('NOT IMPLEMENTED');
}
}
module.exports = { DatabaseDriverNode };

View File

@@ -2,6 +2,10 @@ const SQLite = require('react-native-sqlite-storage');
class DatabaseDriverReactNative {
constructor() {
this.lastInsertId_ = null;
}
open(options) {
//SQLite.DEBUG(true);
return new Promise((resolve, reject) => {
@@ -45,6 +49,7 @@ class DatabaseDriverReactNative {
exec(sql, params = null) {
return new Promise((resolve, reject) => {
this.db_.executeSql(sql, params, (r) => {
if ('insertId' in r) this.lastInsertId_ = r.insertId;
resolve(r);
}, (error) => {
reject(error);
@@ -52,6 +57,10 @@ class DatabaseDriverReactNative {
});
}
lastInsertId() {
return this.lastInsertId_;
}
}
module.exports = { DatabaseDriverReactNative };

View File

@@ -78,8 +78,10 @@ class Database {
} catch (error) {
if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) {
if (totalWaitTime >= 20000) throw this.sqliteErrorToJsError(error, sql, params);
this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime));
this.logger().warn('Error was: ' + error.toString());
// NOTE: don't put logger statements here because it might log to the database, which
// could result in an error being thrown again.
// this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime));
// this.logger().warn('Error was: ' + error.toString());
await time.msleep(waitTime);
totalWaitTime += waitTime;
waitTime *= 1.5;

View File

@@ -8,7 +8,8 @@ const { Keyboard } = require('react-native');
let dialogs = {};
dialogs.confirm = (parentComponent, message) => {
if (!'dialogbox' in parentComponent) throw new Error('A "dialogbox" component must be defined on the parent component!');
if (!parentComponent) throw new Error('parentComponent is required');
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
return new Promise((resolve, reject) => {
Keyboard.dismiss();
@@ -33,7 +34,8 @@ dialogs.confirm = (parentComponent, message) => {
};
dialogs.pop = (parentComponent, message, buttons) => {
if (!'dialogbox' in parentComponent) throw new Error('A "dialogbox" component must be defined on the parent component!');
if (!parentComponent) throw new Error('parentComponent is required');
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
return new Promise((resolve, reject) => {
Keyboard.dismiss();

View File

@@ -149,6 +149,17 @@ function collapseWhiteSpaceAndAppend(lines, state, text) {
if (!spaceLeft && !spaceRight && text == "") return lines;
if (state.inQuote) {
// Add a ">" at the beginning of the block then at the beginning of each lines. So it turns this:
// "my quote\nsecond line" into this => "> my quote\n> second line"
lines.push('> ');
if (lines.indexOf('\r') >= 0) {
text = text.replace(/\n\r/g, '\n\r> ');
} else {
text = text.replace(/\n/g, '\n> ');
}
}
if (spaceLeft) lines.push(SPACE);
lines.push(text);
if (spaceRight) lines.push(SPACE);
@@ -199,7 +210,7 @@ function isAnchor(n) {
}
function isIgnoredEndTag(n) {
return n=="en-note" || n=="en-todo" || n=="span" || n=="body" || n=="html" || n=="font" || n=="br" || n=='hr' || n=='s' || n == 'tbody';
return n=="en-note" || n=="en-todo" || n=="span" || n=="body" || n=="html" || n=="font" || n=="br" || n=='hr' || n=='s' || n == 'tbody' || n == 'sup';
}
function isListTag(n) {
@@ -212,6 +223,24 @@ function isNewLineOnlyEndTag(n) {
}
function isCodeTag(n) {
// NOTE: This handles "code" tags that were copied and pasted from a browser to Evernote. Evernote also has its own code block, which
// of course is way more complicated and currently not fully supported (the code will be imported and indented properly, but it won't
// have the extra Markdown indentation that identifies the block as code). For reference this is an example of Evernote-style code block:
//
// <div style="-en-codeblock: true; box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;,
// monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius:
// 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position:
// initial initial; background-repeat: initial initial;"><div>function justTesting() {</div><div>&nbsp; &nbsp; &nbsp;someCodeBlock();</div>
// <div>&nbsp; &nbsp; &nbsp;return true;</div><div>}</div></div>
//
// Which in normal HTML would be:
//
// <code>
// function justTesting() {
// someCodeBlock();
// return true;
// }
// <code>
return n == "pre" || n == "code";
}
@@ -230,6 +259,7 @@ function enexXmlToMdArray(stream, resources) {
return new Promise((resolve, reject) => {
let state = {
inCode: false,
inQuote: false,
lists: [],
anchorAttributes: [],
};
@@ -314,6 +344,8 @@ function enexXmlToMdArray(stream, resources) {
section.lines.push("**");
} else if (n == 's') {
// Not supported
} else if (n == 'q') {
section.lines.push('"');
} else if (isAnchor(n)) {
state.anchorAttributes.push(node.attributes);
section.lines.push('[');
@@ -340,7 +372,10 @@ function enexXmlToMdArray(stream, resources) {
section.lines.push(BLOCK_OPEN); section.lines.push("##### ");
} else if (n == "h6") {
section.lines.push(BLOCK_OPEN); section.lines.push("###### ");
} else if (isCodeTag(n)) {
} else if (n == 'blockquote') {
section.lines.push(BLOCK_OPEN);
state.inQuote = true;
} else if (isCodeTag(n, node.attributes)) {
section.lines.push(BLOCK_OPEN);
state.inCode = true;
} else if (n == "br") {
@@ -416,7 +451,7 @@ function enexXmlToMdArray(stream, resources) {
section.lines = addResourceTag(section.lines, resource, node.attributes.alt);
}
}
} else if (n == "span" || n == "font") {
} else if (n == "span" || n == "font" || n == 'sup') {
// Ignore
} else {
console.warn("Unsupported start tag: " + n);
@@ -443,6 +478,11 @@ function enexXmlToMdArray(stream, resources) {
section.lines.push("**");
} else if (isEmTag(n)) {
section.lines.push("*");
} else if (n == 'q') {
section.lines.push('"');
} else if (n == 'blockquote') {
section.lines.push(BLOCK_OPEN);
state.inQuote = false;
} else if (isCodeTag(n)) {
state.inCode = false;
section.lines.push(BLOCK_CLOSE);

Some files were not shown because too many files have changed in this diff Show More