1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-27 20:29:45 +02:00

Compare commits

...

160 Commits

Author SHA1 Message Date
Laurent Cozic
91e337307c Mobile: Upgrade react-native-popup-menu to fix scrolling bug 2017-12-07 23:00:22 +00:00
Laurent Cozic
2855b68ed4 Merge branch 'master' of github.com:laurent22/joplin 2017-12-07 22:56:41 +00:00
Laurent Cozic
45ca6284f9 iOS: Added safe area for iPhone X support 2017-12-07 22:42:40 +00:00
Laurent Cozic
145ee13356 Merge branch 'master' of github.com:laurent22/joplin 2017-12-07 22:42:08 +00:00
Laurent Cozic
6f97747199 Electron: Add support for file system sync 2017-12-07 22:29:02 +00:00
Laurent Cozic
f3751e4ba6 Mobile: Fixed auto-assignment of title for new notes 2017-12-07 21:32:22 +00:00
Laurent Cozic
5cd55cada6 Mobile: Made context menu scrollable 2017-12-07 21:23:41 +00:00
Laurent Cozic
bad4b2ecb8 Electron: Added dialog to export sync status 2017-12-07 21:18:18 +00:00
Laurent Cozic
7aafd63ff3 Update website 2017-12-07 19:38:24 +00:00
Laurent Cozic
3227a13035 Electron release v0.10.37 2017-12-07 18:00:28 +00:00
Laurent Cozic
027f96d100 Electron: Fixes #36: Set installation directory for Windows installer 2017-12-07 18:17:41 +00:00
Laurent Cozic
f242a3c215 Merge branch 'fix-cli' 2017-12-07 17:43:44 +00:00
Laurent Cozic
ad6c347180 CLI: Fixed shell mode 2017-12-07 18:12:46 +00:00
Laurent Cozic
4f0431da55 Merge branch 'master' of github.com:laurent22/joplin 2017-12-07 13:33:26 +00:00
Laurent Cozic
b873fdd029 iOS: Release 2017-12-07 17:46:59 +00:00
Laurent Cozic
c1ff820913 Electron: Fixes #65: Display 'no notebook' message on startup. 2017-12-07 13:16:38 +00:00
Laurent Cozic
7008daf92a All: Improved handling of empty links when importing ENEX files. Fixed minor layout issues in Electron app 2017-12-07 00:57:36 +00:00
Laurent Cozic
ed914c6907 All: Improved rendering of imported ENEX tables 2017-12-07 00:35:15 +00:00
Laurent Cozic
37663bd110 Merge pull request #68 from dbinary/master
adding translate spanish Latin America
2017-12-06 23:59:22 +00:00
Laurent Cozic
f01c6aa8d1 Merge branch 'master' of github.com:laurent22/joplin 2017-12-06 23:56:18 +00:00
Laurent Cozic
8838017830 All: Updated translations 2017-12-06 23:55:22 +00:00
Laurent Cozic
1d6fb8058f Update BUILD.md 2017-12-06 23:34:56 +00:00
Laurent Cozic
bb51729bea Update BUILD.md 2017-12-06 23:34:31 +00:00
Laurent Cozic
507e7e6014 All: Improved ENEX import for web pages that have been saved as notes 2017-12-06 19:29:58 +00:00
Laurent Cozic
f42908b11c Merge branch 'sync-limits' 2017-12-05 17:30:16 +00:00
Laurent Cozic
03ec406627 All: Filter to sync target and refactored so that same code can be used by all clients 2017-12-05 19:21:01 +00:00
Laurent Cozic
c703521b6c All: Handling of unsyncable items 2017-12-05 18:56:39 +00:00
Laurent Cozic
cf97bf9a77 Updated readme 2017-12-05 08:47:48 +00:00
Laurent Cozic
304b9a582f Electron release v0.10.36 2017-12-04 23:57:30 +00:00
Laurent Cozic
4d5c4b1743 Electron: started unsynchronisable items UI 2017-12-04 23:57:13 +00:00
Laurent Cozic
4abe5d07c4 All: Handling of impossible-to-sync items (such as when they are over the size limit of the cloud provider) 2017-12-04 23:38:09 +00:00
Laurent Cozic
f6633e23f5 Minor tweaks 2017-12-04 23:01:22 +00:00
Laurent Cozic
a6d6201ecb All: Improved synchronisation process and saving of models so that reducer can deal with full objects 2017-12-04 22:58:42 +00:00
Luis Pérez
4314c392f6 adding translate spanish Latin America 2017-12-04 13:24:55 -06:00
Laurent Cozic
73e81a54b4 All: Fixed sync issue when target does not have reliable timestamps 2017-12-04 18:39:40 +00:00
Laurent Cozic
ab8c66a361 Updated tools 2017-12-04 18:16:14 +00:00
Laurent Cozic
4b55fefcb1 Cleaned up build tools 2017-12-04 17:55:06 +00:00
Laurent Cozic
0eac8b25e1 Deleted uneeded file 2017-12-03 23:06:02 +00:00
Laurent Cozic
aec556ff7d Delete package-lock.json 2017-12-03 23:03:16 +00:00
Laurent Cozic
110dc29bd4 Merge pull request #57 from mgroth0/patch-1
Add node-gyp dependency to instructions
2017-12-03 21:45:40 +00:00
Laurent Cozic
b1efea1bd9 Update BUILD.md 2017-12-03 21:44:04 +00:00
Laurent Cozic
8671467ed3 Update BUILD.md 2017-12-03 21:43:55 +00:00
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
Matt Groth
485ef1f2c2 Add node-gyp dependency to instructions
I was getting an error on `yarn dist` which I solved with the command `npm install -g node-gyp`.

This was the error:
```
Error: /usr/local/Cellar/node/8.9.1/bin/node exited with code 1
Output:
$ node-pre-gyp install --fallback-to-build
Failed to execute 'node-gyp clean' (Error: spawn node-gyp ENOENT)
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Error output:
node-pre-gyp info it worked if it ends with ok
node-pre-gyp info using node-pre-gyp@0.6.38
node-pre-gyp info using node@8.9.1 | darwin | x64
node-pre-gyp http GET https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v3.1.13/electron-v1.7-darwin-x64.tar.gz
node-pre-gyp http 403 https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v3.1.13/electron-v1.7-darwin-x64.tar.gz
node-pre-gyp ERR! Tried to download(403): https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v3.1.13/electron-v1.7-darwin-x64.tar.gz
node-pre-gyp ERR! Pre-built binaries not found for sqlite3@3.1.13 and electron@1.7.9 (electron-v1.7 ABI) (falling back to source compile with node-gyp)
node-pre-gyp http 403 status code downloading tarball https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v3.1.13/electron-v1.7-darwin-x64.tar.gz
node-pre-gyp ERR! build error
node-pre-gyp ERR! stack Error: Failed to execute 'node-gyp clean' (Error: spawn node-gyp ENOENT)
node-pre-gyp ERR! stack     at ChildProcess.<anonymous> (/Users/matt/joplin/ElectronClient/app/node_modules/sqlite3/node_modules/node-pre-gyp/lib/util/compile.js:77:29)
node-pre-gyp ERR! stack     at emitOne (events.js:116:13)
node-pre-gyp ERR! stack     at ChildProcess.emit (events.js:211:7)
node-pre-gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:196:12)
node-pre-gyp ERR! stack     at onErrorNT (internal/child_process.js:372:16)
node-pre-gyp ERR! stack     at _combinedTickCallback (internal/process/next_tick.js:138:11)
node-pre-gyp ERR! stack     at process._tickCallback (internal/process/next_tick.js:180:9)
node-pre-gyp ERR! System Darwin 16.7.0
node-pre-gyp ERR! command "/usr/local/Cellar/node/8.9.1/bin/node" "/Users/matt/joplin/ElectronClient/app/node_modules/sqlite3/node_modules/.bin/node-pre-gyp" "install" "--fallback-to-build"
node-pre-gyp ERR! cwd /Users/matt/joplin/ElectronClient/app/node_modules/sqlite3
node-pre-gyp ERR! node -v v8.9.1
node-pre-gyp ERR! node-pre-gyp -v v0.6.38
node-pre-gyp ERR! not ok
error Command failed with exit code 1.

    at ChildProcess.childProcess.once.code (/Users/matt/joplin/ElectronClient/app/node_modules/builder-util/src/util.ts:219:14)
    at Object.onceWrapper (events.js:317:30)
    at emitTwo (events.js:126:13)
    at ChildProcess.emit (events.js:214:7)
    at maybeClose (internal/child_process.js:925:16)
    at Socket.stream.socket.on (internal/child_process.js:346:11)
    at emitOne (events.js:116:13)
    at Socket.emit (events.js:211:7)
    at Pipe._handle.close [as _onclose] (net.js:554:12)
From previous event:
    at spawn (/Users/matt/joplin/ElectronClient/app/node_modules/builder-util/src/util.ts:182:3)
    at default.map.concurrency (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/util/yarn.ts:154:7)
    at runCallback (timers.js:789:20)
    at tryOnImmediate (timers.js:751:5)
    at processImmediate [as _immediateCallback] (timers.js:722:5)
From previous event:
    at /Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/util/yarn.ts:152:27
From previous event:
    at rebuild (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/out/util/yarn.js:94:22)
    at /Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/util/yarn.ts:21:11
    at Generator.next (<anonymous>)
    at runCallback (timers.js:789:20)
    at tryOnImmediate (timers.js:751:5)
    at processImmediate [as _immediateCallback] (timers.js:722:5)
From previous event:
    at installOrRebuild (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/out/util/yarn.js:32:21)
    at /Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/packager.ts:388:7
    at Generator.next (<anonymous>)
From previous event:
    at Packager.installAppDependencies (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/out/packager.js:433:11)
    at /Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/packager.ts:284:20
    at Generator.next (<anonymous>)
From previous event:
    at Packager.doBuild (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/out/packager.js:369:11)
    at /Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/packager.ts:236:52
    at Generator.next (<anonymous>)
From previous event:
    at Packager.build (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/out/packager.js:298:11)
    at /Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/builder.ts:277:40
    at Generator.next (<anonymous>)
From previous event:
    at build (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/out/builder.js:63:21)
    at then (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/cli/cli.ts:49:4)
    at runCallback (timers.js:789:20)
    at tryOnImmediate (timers.js:751:5)
    at processImmediate [as _immediateCallback] (timers.js:722:5)
From previous event:
    at Object.args [as handler] (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/cli/cli.ts:49:4)
    at Object.runCommand (/Users/matt/joplin/ElectronClient/app/node_modules/yargs/lib/command.js:228:22)
    at Object.parseArgs [as _parseArgs] (/Users/matt/joplin/ElectronClient/app/node_modules/yargs/yargs.js:1041:24)
    at Object.get [as argv] (/Users/matt/joplin/ElectronClient/app/node_modules/yargs/yargs.js:957:21)
    at Object.<anonymous> (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/cli/cli.ts:43:15)
    at Module._compile (module.js:635:30)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Function.Module.runMain (module.js:676:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3
```
2017-12-01 21:28:13 -05: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
7ac6f39658 Alarm support 2017-09-10 17:56:27 +01:00
155 changed files with 8923 additions and 3920 deletions

3
.gitignore vendored
View File

@@ -34,4 +34,5 @@ sync_staging.sh
_vieux/
_mydocs
.DS_Store
Assets/DownloadBadges*.psd
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

45
BUILD.md Normal file
View File

@@ -0,0 +1,45 @@
# 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. You normally don't need to run this if you haven't updated the translation since the compiled files are on the repository.
## macOS dependencies
brew install yarn node
echo 'export PATH="/usr/local/opt/gettext/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
## Linux and Windows (WSL) dependencies
- Install yarn - https://yarnpkg.com/lang/en/docs/install/
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
# 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 `build.sh`
- Copy the translations to the build directory: `rsync -aP ../ReactNativeClient/locales/ build/locales/`
- Run `run.sh` to start the application for testing.

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,31 +168,42 @@ 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();
}
await doExit();
}
commands() {
if (this.allCommandsLoaded_) return this.commands_;
commands(uiType = null) {
if (!this.allCommandsLoaded_) {
fs.readdirSync(__dirname).forEach((path) => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path)
if (ext != 'js') return;
fs.readdirSync(__dirname).forEach((path) => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path)
if (ext != 'js') return;
let CommandClass = require('./' + path);
let cmd = new CommandClass();
if (!cmd.enabled()) return;
cmd = this.setupCommand(cmd);
this.commands_[cmd.name()] = cmd;
});
let CommandClass = require('./' + path);
let cmd = new CommandClass();
if (!cmd.enabled()) return;
cmd = this.setupCommand(cmd);
this.commands_[cmd.name()] = cmd;
});
this.allCommandsLoaded_ = true;
}
this.allCommandsLoaded_ = true;
if (uiType !== null) {
let temp = [];
for (let n in this.commands_) {
if (!this.commands_.hasOwnProperty(n)) continue;
const c = this.commands_[n];
if (!c.supportsUi(uiType)) continue;
temp[n] = c;
}
return temp;
}
return this.commands_;
}
@@ -246,9 +257,13 @@ class Application extends BaseApplication {
try {
CommandClass = require(__dirname + '/command-' + name + '.js');
} catch (error) {
let e = new Error('No such command: ' + name);
e.type = 'notFound';
throw e;
if (error.message && error.message.indexOf('Cannot find module') >= 0) {
let e = new Error(_('No such command: %s', name));
e.type = 'notFound';
throw e;
} else {
throw error;
}
}
let cmd = new CommandClass();
@@ -306,6 +321,8 @@ class Application extends BaseApplication {
if (argv.length) {
this.gui_ = this.dummyGui();
this.currentFolder_ = await Folder.load(Setting.value('activeFolderId'));
try {
await this.execCommand(argv);
} catch (error) {

View File

@@ -12,6 +12,10 @@ class Command extends BaseCommand {
return _('Exits the application.');
}
compatibleUis() {
return ['gui'];
}
async action(args) {
await app().exit();
}

View File

@@ -18,7 +18,7 @@ class Command extends BaseCommand {
}
allCommands() {
const commands = app().commands();
const commands = app().commands(app().uiType());
let output = [];
for (let n in commands) {
if (!commands.hasOwnProperty(n)) continue;
@@ -69,6 +69,8 @@ class Command extends BaseCommand {
this.stdout('');
this.stdout(_('The possible commands are:'));
this.stdout('');
this.stdout(_('Type `help all` for the complete help of all the commands.'));
this.stdout('');
this.stdout(commandNames.join(', '));
this.stdout('');
this.stdout(_('In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.'));

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

@@ -2,6 +2,7 @@ const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Database } = require('lib/database.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { BaseItem } = require('lib/models/base-item.js');
@@ -12,16 +13,16 @@ class Command extends BaseCommand {
return 'set <note> <name> [value]';
}
enabled() {
return false;
}
description() {
return _('Sets the property <name> of the given <note> to the given [value].');
}
const fields = Note.fields();
const s = [];
for (let i = 0; i < fields.length; i++) {
const f = fields[i];
if (f.name === 'id') continue;
s.push(f.name + ' (' + Database.enumName('fieldType', f.type) + ')');
}
hidden() {
return true;
return _('Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s', s.join(', '));
}
async action(args) {

View File

@@ -16,7 +16,7 @@ class Command extends BaseCommand {
constructor() {
super();
this.syncTarget_ = null;
this.syncTargetId_ = null;
this.releaseLockFn_ = null;
this.oneDriveApiUtils_ = null;
}
@@ -32,7 +32,6 @@ class Command extends BaseCommand {
options() {
return [
['--target <target>', _('Sync to provided target (defaults to sync.target config value)')],
['--random-failures', 'For debugging purposes. Do not use.'],
];
}
@@ -62,6 +61,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 +111,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) => {
@@ -120,16 +139,15 @@ class Command extends BaseCommand {
cliUtils.redrawDone();
this.stdout(msg);
},
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 +174,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

@@ -18,8 +18,8 @@ class Command extends BaseCommand {
return { data: autocompleteFolders };
}
enabled() {
return false;
compatibleUis() {
return ['cli'];
}
async action(args) {

View File

@@ -1,3 +0,0 @@
#/bin/bash
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
NODE_PATH="$CLIENT_DIR/build" node "$CLIENT_DIR/build/build-translation.js" --silent

View File

@@ -1,4 +0,0 @@
#!/bin/bash
set -e
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
"$ROOT_DIR/build.sh" && NODE_PATH="$ROOT_DIR/build" node "$ROOT_DIR/build/build-website.js"

View File

@@ -6,55 +6,4 @@ BUILD_DIR="$ROOT_DIR/build"
rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/"
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
chmod 755 "$BUILD_DIR/main.js"

View File

@@ -1,6 +0,0 @@
#!/bin/bash
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
"$CLIENT_DIR/publish.sh"
npm install -g joplin

1062
CliClient/locales/de_DE.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -100,6 +100,10 @@ msgstr ""
msgid "Cancelling background synchronisation... Please wait."
msgstr ""
#, javascript-format
msgid "No such command: %s"
msgstr ""
#, javascript-format
msgid "The command \"%s\" is only available in GUI mode"
msgstr ""
@@ -508,6 +512,9 @@ msgstr ""
msgid "Tools"
msgstr ""
msgid "Synchronisation status"
msgstr ""
msgid "Options"
msgstr ""
@@ -562,18 +569,30 @@ msgstr ""
msgid "Rename notebook:"
msgstr ""
msgid "Set alarm:"
msgstr ""
msgid "Layout"
msgstr ""
msgid "Some items cannot be synchronised."
msgstr ""
msgid "View them now"
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,16 +603,22 @@ msgstr ""
msgid "Attach file"
msgstr ""
msgid "Set alarm"
msgstr ""
msgid "Refresh"
msgstr ""
msgid "Clear"
msgstr ""
msgid "OneDrive Login"
msgstr ""
msgid "Import"
msgstr ""
msgid "Configuration"
msgid "Synchronisation Status"
msgstr ""
msgid "Delete notebook?"
@@ -628,6 +653,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 +676,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 +752,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 +760,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 +771,12 @@ msgstr ""
msgid "Language"
msgstr ""
msgid "Date format"
msgstr ""
msgid "Time format"
msgstr ""
msgid "Theme"
msgstr ""
@@ -784,10 +810,29 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
msgid "Automatically update the application"
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 "Items that cannot be synchronised"
msgstr ""
#, javascript-format
msgid "\"%s\": \"%s\""
msgstr ""
msgid "Sync status (synced items / total items)"
@@ -816,9 +861,19 @@ 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 ""
@@ -828,6 +883,22 @@ 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 ""
@@ -851,16 +922,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"
@@ -872,7 +940,7 @@ msgstr ""
msgid "Show metadata"
msgstr ""
msgid "View location on map"
msgid "View on map"
msgstr ""
msgid "Delete notebook"

1030
CliClient/locales/es_419.po Normal file

File diff suppressed because it is too large Load Diff

1028
CliClient/locales/es_CR.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -100,6 +100,10 @@ msgstr "o"
msgid "Cancelling background synchronisation... Please wait."
msgstr "Annulation de la synchronisation... Veuillez patienter."
#, fuzzy, javascript-format
msgid "No such command: %s"
msgstr "Commande invalide : \"%s\""
#, javascript-format
msgid "The command \"%s\" is only available in GUI mode"
msgstr ""
@@ -558,6 +562,10 @@ msgstr "Chercher dans toutes les notes"
msgid "Tools"
msgstr "Outils"
#, fuzzy
msgid "Synchronisation status"
msgstr "Cible de la synchronisation"
msgid "Options"
msgstr "Options"
@@ -614,18 +622,32 @@ 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"
#, fuzzy
msgid "Some items cannot be synchronised."
msgstr "Impossible d'initialiser la synchronisation."
msgid "View them now"
msgstr ""
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,17 +659,25 @@ 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"
#, fuzzy
msgid "Synchronisation Status"
msgstr "Cible de la synchronisation"
msgid "Delete notebook?"
msgstr "Supprimer le carnet ?"
@@ -681,6 +711,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 +736,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 +812,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 +822,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 +835,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,12 +874,32 @@ 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"
#, fuzzy
msgid "Automatically update the application"
msgstr "Quitter le logiciel."
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 "Items that cannot be synchronised"
msgstr ""
#, javascript-format
msgid "\"%s\": \"%s\""
msgstr ""
msgid "Sync status (synced items / total items)"
msgstr "Status de la synchronisation (objets synchro. / total)"
@@ -880,11 +927,21 @@ 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"
@@ -894,6 +951,22 @@ 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"
@@ -917,16 +990,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"
@@ -938,8 +1008,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"
@@ -962,6 +1032,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,17 +1104,9 @@ msgstr "Bienvenue"
#~ msgid "Show/Hide the console"
#~ msgstr "Quitter le logiciel."
#, fuzzy
#~ msgid "Last command: %s"
#~ msgstr "Commande invalide : \"%s\""
#~ 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

@@ -100,6 +100,10 @@ msgstr ""
msgid "Cancelling background synchronisation... Please wait."
msgstr ""
#, javascript-format
msgid "No such command: %s"
msgstr ""
#, javascript-format
msgid "The command \"%s\" is only available in GUI mode"
msgstr ""
@@ -508,6 +512,9 @@ msgstr ""
msgid "Tools"
msgstr ""
msgid "Synchronisation status"
msgstr ""
msgid "Options"
msgstr ""
@@ -562,18 +569,30 @@ msgstr ""
msgid "Rename notebook:"
msgstr ""
msgid "Set alarm:"
msgstr ""
msgid "Layout"
msgstr ""
msgid "Some items cannot be synchronised."
msgstr ""
msgid "View them now"
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,16 +603,22 @@ msgstr ""
msgid "Attach file"
msgstr ""
msgid "Set alarm"
msgstr ""
msgid "Refresh"
msgstr ""
msgid "Clear"
msgstr ""
msgid "OneDrive Login"
msgstr ""
msgid "Import"
msgstr ""
msgid "Configuration"
msgid "Synchronisation Status"
msgstr ""
msgid "Delete notebook?"
@@ -628,6 +653,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 +676,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 +752,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 +760,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 +771,12 @@ msgstr ""
msgid "Language"
msgstr ""
msgid "Date format"
msgstr ""
msgid "Time format"
msgstr ""
msgid "Theme"
msgstr ""
@@ -784,10 +810,29 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
msgid "Automatically update the application"
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 "Items that cannot be synchronised"
msgstr ""
#, javascript-format
msgid "\"%s\": \"%s\""
msgstr ""
msgid "Sync status (synced items / total items)"
@@ -816,9 +861,19 @@ 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 ""
@@ -828,6 +883,22 @@ 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 ""
@@ -851,16 +922,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"
@@ -872,7 +940,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.73",
"version": "0.10.79",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -460,16 +460,6 @@
"assert-plus": "1.0.0"
}
},
"gettext-parser": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.3.0.tgz",
"integrity": "sha512-iloxjcw+uTPnQ8DrGICWtqkHNgk3mAiDI77pLmXQCnhM+BxFQXstzTA4zj3EpIYMysRQnnNzHyHzBUEazz80Sw==",
"dev": true,
"requires": {
"encoding": "0.1.12",
"safe-buffer": "5.1.1"
}
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
@@ -878,12 +868,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"mustache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.0.tgz",
"integrity": "sha1-QCj3d4sXcIpImTCm5SrDvKDaQdA=",
"dev": true
},
"nan": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz",

View File

@@ -18,7 +18,7 @@
],
"owner": "Laurent Cozic"
},
"version": "0.10.73",
"version": "0.10.79",
"bin": {
"joplin": "./main.js"
},
@@ -60,9 +60,7 @@
"yargs-parser": "^7.0.0"
},
"devDependencies": {
"gettext-parser": "^1.2.2",
"jasmine": "^2.6.0",
"mustache": "^2.3.0"
"jasmine": "^2.6.0"
},
"scripts": {
"test": "jasmine"

View File

@@ -7,4 +7,4 @@ rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
mkdir -p "$BUILD_DIR/data"
npm test tests-build/synchronizer.js
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)

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,12 +9,13 @@ 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);
});
jasmine.DEFAULT_TIMEOUT_INTERVAL = 9000; // The first test is slow because the database needs to be built
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built
async function allItems() {
let folders = await Folder.all();
@@ -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);
@@ -99,6 +100,7 @@ describe('Synchronizer', function() {
await synchronizer().start();
let all = await allItems();
await localItemsSameAsRemote(all, expect);
done();
@@ -626,5 +628,33 @@ describe('Synchronizer', function() {
done();
});
it('items should skip items that cannot be synced', async (done) => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
const noteId = note1.id;
await synchronizer().start();
let disabledItems = await BaseItem.syncDisabledItems();
expect(disabledItems.length).toBe(0);
await Note.save({ id: noteId, title: "un mod", });
synchronizer().debugFlags_ = ['cannotSync'];
await synchronizer().start();
synchronizer().debugFlags_ = [];
await synchronizer().start(); // Another sync to check that this item is now excluded from sync
await switchClient(2);
await synchronizer().start();
let notes = await Note.all();
expect(notes.length).toBe(1);
expect(notes[0].title).toBe('un');
await switchClient(1);
disabledItems = await BaseItem.syncDisabledItems();
expect(disabledItems.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,16 +33,19 @@ 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('console');
logger.addTarget('file', { path: logDir + '/log.txt' });
logger.setLevel(Logger.LEVEL_DEBUG);
logger.setLevel(Logger.LEVEL_WARN);
BaseItem.loadClass('Note', Note);
BaseItem.loadClass('Folder', Folder);
@@ -121,11 +128,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 +156,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

@@ -15,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;
@@ -132,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);
@@ -222,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'],
@@ -251,6 +259,14 @@ class Application extends BaseApplication {
}, {
label: _('Tools'),
submenu: [{
label: _('Synchronisation status'),
click: () => {
this.dispatch({
type: 'NAV_GO',
routeName: 'Status',
});
}
},{
label: _('Options'),
click: () => {
this.dispatch({
@@ -268,7 +284,7 @@ class Application extends BaseApplication {
}, {
label: _('About Joplin'),
click: () => {
const p = require('./package.json');
const p = packageInfo;
let message = [
p.description,
'',
@@ -283,12 +299,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;
@@ -305,6 +330,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();
}
@@ -346,7 +376,19 @@ class Application extends BaseApplication {
setInterval(() => { runAutoUpdateCheck() }, 2 * 60 * 60 * 1000);
}
reg.scheduleSync();
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,4 +1,5 @@
const { _ } = require('lib/locale.js');
const { dirname } = require('lib/path-utils.js');
const { Logger } = require('lib/logger.js');
class Bridge {
@@ -6,6 +7,7 @@ class Bridge {
constructor(electronWrapper) {
this.electronWrapper_ = electronWrapper;
this.autoUpdateLogger_ = null;
this.lastSelectedPath_ = null;
}
electronApp() {
@@ -37,9 +39,27 @@ class Bridge {
return this.window().setSize(width, height);
}
showSaveDialog(options) {
const {dialog} = require('electron');
if (!options) options = {};
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
const filePath = dialog.showSaveDialog(options);
if (filePath) {
this.lastSelectedPath_ = filePath;
}
return filePath;
}
showOpenDialog(options) {
const {dialog} = require('electron');
return dialog.showOpenDialog(options);
if (!options) options = {};
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
if (!('createDirectory' in options)) options.createDirectory = true;
const filePaths = dialog.showOpenDialog(options);
if (filePaths && filePaths.length) {
this.lastSelectedPath_ = dirname(filePaths[0]);
}
return filePaths;
}
showMessageBox(options) {
@@ -63,6 +83,15 @@ class Bridge {
return result === 0;
}
showInfoMessageBox(message) {
const result = this.showMessageBox({
type: 'info',
message: message,
buttons: [_('OK')],
});
return result === 0;
}
get Menu() {
return require('electron').Menu;
}
@@ -84,7 +113,7 @@ class Bridge {
this.autoUpdateLogger_ = new Logger();
this.autoUpdateLogger_.addTarget('file', { path: logFilePath });
this.autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
this.autoUpdateLogger_.info('checkForUpdatesAndNotify: Intializing...');
this.autoUpdateLogger_.info('checkForUpdatesAndNotify: Initializing...');
this.autoUpdater_ = require("electron-updater").autoUpdater;
this.autoUpdater_.logger = this.autoUpdateLogger_;
}

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

@@ -9,6 +9,18 @@ const { _ } = require('lib/locale.js');
class ConfigScreenComponent extends React.Component {
constructor() {
super();
this.state = {
settings: {},
};
}
componentWillMount() {
this.setState({ settings: this.props.settings });
}
settingToComponent(key, value) {
const theme = themeStyle(this.props.theme);
@@ -28,9 +40,13 @@ class ConfigScreenComponent extends React.Component {
};
const updateSettingValue = (key, value) => {
Setting.setValue(key, value);
const settings = Object.assign({}, this.state.settings);
settings[key] = value;
this.setState({ settings: settings });
}
// 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 +58,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}
@@ -50,22 +66,53 @@ class ConfigScreenComponent extends React.Component {
</div>
);
} else if (md.type === Setting.TYPE_BOOL) {
const onCheckboxClick = (event) => {
updateSettingValue(key, !value)
}
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>
<input id={'setting_checkbox_' + key} type="checkbox" defaultChecked={!!value} onChange={(event) => { onCheckboxClick(event) }}/><label onClick={(event) => { onCheckboxClick(event) }} style={labelStyle} htmlFor={'setting_checkbox_' + key}>{md.label()}</label>
</div>
</div>
);
} else if (md.type === Setting.TYPE_STRING) {
const onTextChange = (event) => {
const settings = Object.assign({}, this.state.settings);
settings[key] = event.target.value;
this.setState({ settings: settings });
}
return (
<div key={key+value} style={rowStyle}>
<div style={labelStyle}><label>{md.label()}</label></div>
<input type="text" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
</div>
);
} else {
console.warn('Type not implemented: ' + key);
}
return output;
}
onSaveClick() {
for (let n in this.state.settings) {
if (!this.state.settings.hasOwnProperty(n)) continue;
Setting.setValue(n, this.state.settings[n]);
}
this.props.dispatch({ type: 'NAV_BACK' });
}
onCancelClick() {
this.props.dispatch({ type: 'NAV_BACK' });
}
render() {
const theme = themeStyle(this.props.theme);
const style = this.props.style;
const settings = this.props.settings;
const settings = this.state.settings;
const headerStyle = {
width: style.width,
@@ -75,15 +122,21 @@ class ConfigScreenComponent extends React.Component {
padding: 10,
};
const buttonStyle = {
display: this.state.settings === this.props.settings ? 'none' : 'inline-block',
marginRight: 10,
}
let settingComps = [];
let keys = Setting.keys(true, 'desktop');
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key === 'sync.target') continue;
if (!(key in settings)) {
console.warn('Missing setting: ' + key);
continue;
}
const md = Setting.settingMetadata(key);
if (md.show && !md.show(settings)) continue;
const comp = this.settingToComponent(key, settings[key]);
if (!comp) continue;
settingComps.push(comp);
@@ -94,6 +147,8 @@ class ConfigScreenComponent extends React.Component {
<Header style={headerStyle} />
<div style={containerStyle}>
{ settingComps }
<button onClick={() => {this.onSaveClick()}} style={buttonStyle}>{_('Save')}</button>
<button onClick={() => {this.onCancelClick()}} style={buttonStyle}>{_('Cancel')}</button>
</div>
</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

@@ -41,19 +41,24 @@ class ImportScreenComponent extends React.Component {
const messages = this.state.messages.slice();
let found = false;
for (let i = 0; i < messages.length; i++) {
if (messages[i].key === key) {
messages[i].text = text;
found = true;
break;
}
}
if (!found) messages.push({ key: key, text: text });
messages.push({ key: key, text: text });
this.setState({ messages: messages });
}
uniqueMessages() {
let output = [];
const messages = this.state.messages.slice();
let foundKeys = [];
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (foundKeys.indexOf(msg.key) >= 0) continue;
foundKeys.push(msg.key);
output.unshift(msg);
}
return output;
}
async doImport() {
const filePath = this.props.filePath;
const folderTitle = await Folder.findUniqueFolderTitle(filename(filePath));
@@ -77,10 +82,9 @@ class ImportScreenComponent extends React.Component {
this.addMessage('progress', lastProgress);
},
onError: (error) => {
const messages = this.state.messages.slice();
let s = error.trace ? error.trace : error.toString();
messages.push({ key: 'error_' + (progressCount++), text: s });
this.addMessage('error_' + (progressCount++), lastProgress);
// Don't display the error directly because most of the time it doesn't matter
// (eg. for weird broken HTML, but the note is still imported)
console.warn('When importing ENEX file', error);
},
}
@@ -95,7 +99,7 @@ class ImportScreenComponent extends React.Component {
render() {
const theme = themeStyle(this.props.theme);
const style = this.props.style;
const messages = this.state.messages;
const messages = this.uniqueMessages();
const messagesStyle = {
padding: 10,

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,69 @@ class MainScreenComponent extends React.Component {
}
}
styles(themeId, width, height, messageBoxVisible) {
const styleKey = themeId + '_' + width + '_' + height + '_' + messageBoxVisible;
if (styleKey === this.styleKey_) return this.styles_;
const theme = themeStyle(themeId);
this.styleKey_ = styleKey;
this.styles_ = {};
this.styles_.header = {
width: width,
};
this.styles_.messageBox = {
width: width,
height: 30,
display: 'flex',
alignItems: 'center',
paddingLeft: 10,
backgroundColor: theme.warningBackgroundColor,
}
const rowHeight = height - theme.headerHeight - (messageBoxVisible ? this.styles_.messageBox.height : 0);
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 messageBoxVisible = this.props.hasDisabledSyncItems;
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, messageBoxVisible);
const theme = themeStyle(this.props.theme);
const headerButtons = [];
@@ -267,20 +330,45 @@ class MainScreenComponent extends React.Component {
},
});
if (!this.promptOnClose_) {
this.promptOnClose_ = (answer, buttonType) => {
return this.state.promptOptions.onClose(answer, buttonType);
}
}
const onViewDisabledItemsClick = () => {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'Status',
});
}
const messageComp = messageBoxVisible ? (
<div style={styles.messageBox}>
<span style={theme.textStyle}>
{_('Some items cannot be synchronised.')} <a href="#" onClick={() => { onViewDisabledItemsClick() }}>{_('View them now')}</a>
</span>
</div>
) : null;
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} />
{messageComp}
<SideBar style={styles.sideBar} />
<NoteList style={styles.noteList} />
<NoteText style={styles.noteText} visiblePanes={this.props.noteVisiblePanes} />
</div>
);
}
@@ -294,6 +382,7 @@ const mapStateToProps = (state) => {
noteVisiblePanes: state.noteVisiblePanes,
folders: state.folders,
notes: state.notes,
hasDisabledSyncItems: state.hasDisabledSyncItems,
};
};

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>
@@ -145,7 +174,7 @@ class NoteListComponent extends React.Component {
}, style);
emptyDivStyle.width = emptyDivStyle.width - padding * 2;
emptyDivStyle.height = emptyDivStyle.height - padding * 2;
return <div style={emptyDivStyle}>{_('No notes in here. Create one by clicking on "New note".')}</div>
return <div style={emptyDivStyle}>{ this.props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}</div>
}
return (
@@ -164,7 +193,8 @@ class NoteListComponent extends React.Component {
const mapStateToProps = (state) => {
return {
notes: state.notes,
selectedNoteId: state.selectedNoteId,
folders: state.folders,
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,6 +58,10 @@ 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() {
@@ -82,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() {
@@ -89,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() {
@@ -109,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
@@ -144,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 });
}
}
@@ -308,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;
@@ -315,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;
@@ -370,7 +431,7 @@ class NoteTextComponent extends React.Component {
height: 30,
boxSizing: 'border-box',
marginTop: 10,
marginBottom: 10,
marginBottom: 0,
display: 'flex',
flexDirection: 'row',
};
@@ -386,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),
@@ -440,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}
@@ -494,6 +581,7 @@ class NoteTextComponent extends React.Component {
{ titleEditor }
{ titleBarMenuButton }
</div>
{ toolbar }
{ editor }
{ viewer }
</div>
@@ -504,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

@@ -8,6 +8,7 @@ const { Setting } = require('lib/models/setting.js');
const { MainScreen } = require('./MainScreen.min.js');
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
const { StatusScreen } = require('./StatusScreen.min.js');
const { ImportScreen } = require('./ImportScreen.min.js');
const { ConfigScreen } = require('./ConfigScreen.min.js');
const { Navigator } = require('./Navigator.min.js');
@@ -74,7 +75,8 @@ 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') },
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
};
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,135 @@
const React = require('react');
const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
const { Setting } = require('lib/models/setting.js');
const { bridge } = require('electron').remote.require('./bridge');
const { Header } = require('./Header.min.js');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const { ReportService } = require('lib/services/report.js');
const fs = require('fs-extra');
class StatusScreenComponent extends React.Component {
constructor() {
super();
this.state = {
report: [],
};
}
componentWillMount() {
this.resfreshScreen();
}
async resfreshScreen() {
const service = new ReportService();
const report = await service.status(Setting.value('sync.target'));
this.setState({ report: report });
}
async exportDebugReportClick() {
const filename = 'syncReport-' + (new Date()).getTime() + '.csv';
const filePath = bridge().showSaveDialog({
title: _('Please select where the sync status should be exported to'),
defaultPath: filename,
});
if (!filePath) return;
const service = new ReportService();
const csv = await service.basicItemList({ format: 'csv' });
await fs.writeFileSync(filePath, csv);
}
render() {
const theme = themeStyle(this.props.theme);
const style = this.props.style;
const headerStyle = {
width: style.width,
};
const containerPadding = 10;
const containerStyle = {
padding: containerPadding,
overflowY: 'auto',
height: style.height - theme.headerHeight - containerPadding * 2,
};
function renderSectionTitleHtml(key, title) {
return <h2 key={'section_' + key} style={theme.h2Style}>{title}</h2>
}
function renderSectionHtml(key, section) {
let itemsHtml = [];
itemsHtml.push(renderSectionTitleHtml(section.title, section.title));
for (let n in section.body) {
if (!section.body.hasOwnProperty(n)) continue;
itemsHtml.push(<div style={theme.textStyle} key={'item_' + n}>{section.body[n]}</div>);
}
return (
<div key={key}>
{itemsHtml}
</div>
);
}
function renderBodyHtml(report) {
let output = [];
let baseStyle = {
paddingLeft: 6,
paddingRight: 6,
paddingTop: 2,
paddingBottom: 2,
flex: 0,
color: theme.color,
fontSize: theme.fontSize,
};
let sectionsHtml = [];
for (let i = 0; i < report.length; i++) {
let section = report[i];
if (!section.body.length) continue;
sectionsHtml.push(renderSectionHtml(i, section));
}
return (
<div>
{sectionsHtml}
</div>
);
}
let body = renderBodyHtml(this.state.report);
return (
<div style={style}>
<Header style={headerStyle} />
<div style={containerStyle}>
<a style={theme.textStyle} onClick={() => this.exportDebugReportClick()}href="#">Export debug report</a>
{body}
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
theme: state.settings.theme,
settings: state.settings,
locale: state.settings.locale,
};
};
const StatusScreen = connect(mapStateToProps)(StatusScreenComponent);
module.exports = { StatusScreen };

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,6 +5,7 @@
<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>

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -35,14 +35,25 @@ Setting.setConstant('appType', 'desktop');
// 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.23",
"version": "0.10.37",
"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",
@@ -25,6 +25,10 @@
"win": {
"icon": "../../Assets/Joplin.ico"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"mac": {
"icon": "../../Assets/macOs.icns",
"asar": false
@@ -60,15 +64,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,27 @@ body, textarea {
overflow: hidden;
}
table {
border-collapse: collapse;
}
table th {
text-align: left;
}
table td, table th {
padding: .5em;
border: 1px solid #ccc;
}
/* 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 +44,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

@@ -25,9 +25,13 @@ const globalStyle = {
selectedColor2: "#5A4D70",
colorError2: "#ff6c6c",
warningBackgroundColor: "#FFD08D",
headerHeight: 35,
headerButtonHPadding: 6,
toolbarHeight: 35,
raisedBackgroundColor: "#0080EF",
raisedColor: "#003363",
raisedHighlightedColor: "#ffffff",
@@ -67,6 +71,9 @@ globalStyle.textStyle2 = Object.assign({}, globalStyle.textStyle, {
color: globalStyle.color2,
});
globalStyle.h2Style = Object.assign({}, globalStyle.textStyle);
globalStyle.h2Style.fontSize *= 1.3;
let themeCache_ = {};
function themeStyle(theme) {

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');
@@ -64,6 +88,6 @@ async function main() {
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

@@ -3,4 +3,4 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$ROOT_DIR"
./build.sh || exit 1
cd "$ROOT_DIR/app"
./node_modules/.bin/electron . --env dev --log-level debug --open-dev-tools "$@"
./node_modules/.bin/electron . --env dev --log-level warn --open-dev-tools "$@"

View File

@@ -1,8 +1,8 @@
# 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.
@@ -12,15 +12,15 @@ Joplin is still under development but is out of Beta and should be suitable for
# Installation
Three types of applications are available: **desktop** (Windows, macOS and Linux), **mobile** (Android and iOS) and for **terminal** (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 | Download
-----------------|--------
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.22/Joplin-Setup-0.10.22.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.22/Joplin-0.10.22.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.22/Joplin-0.10.22-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.37/Joplin-Setup-0.10.37.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.37/Joplin-0.10.37.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.37/Joplin-0.10.37-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
@@ -31,9 +31,18 @@ iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'
## Terminal application
On macOS, Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)), type:
On macOS:
npm install -g joplin
brew install node joplin
On Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)):
**Important:** First, [install Node 8+](https://nodejs.org/en/download/package-manager/). Node 8 is LTS but not yet available everywhere so you might need to manually install it.
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,19 +92,34 @@ 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
- NextCloud support
- All: End to end encryption
- Windows: Tray icon
- Desktop apps: Tag auto-complete
@@ -118,6 +130,7 @@ This translation will apply to both the terminal and the Android application.
- 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.
- 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:
On macOS:
npm install -g joplin
brew install node joplin
On Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)):
**Important:** First, [install Node 8+](https://nodejs.org/en/download/package-manager/). Node 8 is LTS but not yet available everywhere so you might need to manually install it.
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
@@ -128,193 +137,225 @@ Since this is still an actual URL, the terminal will still make it clickable. An
In Markdown, links to resources are represented as a simple ID to the resource. In order to give access to these resources, they will be, like links, converted to local URLs. Clicking this link will then open a browser, which will handle the file - i.e. display the image, open the PDF file, etc.
# Shell mode
Commands can also be used directly from a shell. To view the list of available commands, type `joplin help all`. To reference a note, notebook or tag you can either use the ID (type `joplin ls -l` to view the ID) or by title.
For example, this will create a new note "My note" in the notebook "My notebook":
$ joplin mkbook "My notebook"
$ joplin use "My notebook"
$ joplin mknote "My note"
To view the newly created note:
$ joplin ls -l
fe889 07/12/2017 17:57 My note
Give a new title to the note:
$ joplin set fe889 title "New title"
# Available shortcuts
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

@@ -90,8 +90,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion 16
targetSdkVersion 22
versionCode 66
versionName "0.10.53"
versionCode 78
versionName "0.10.63"
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.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>5</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 {
@@ -56,10 +66,15 @@ class BaseApplication {
}
switchCurrentFolder(folder) {
this.dispatch({
type: 'FOLDER_SELECT',
id: folder ? folder.id : '',
});
if (!this.hasGui()) {
this.currentFolder_ = Object.assign({}, folder);
Setting.setValue('activeFolderId', folder ? folder.id : '');
} else {
this.dispatch({
type: 'FOLDER_SELECT',
id: folder ? folder.id : '',
});
}
}
// Handles the initial flags passed to main script and
@@ -152,9 +167,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;
@@ -220,6 +232,10 @@ class BaseApplication {
return false;
}
uiType() {
return this.hasGui() ? 'gui' : 'cli';
}
generalMiddlewareFn() {
const middleware = store => next => (action) => {
return this.generalMiddleware(store, next, action);
@@ -244,6 +260,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);
}
@@ -259,6 +280,11 @@ class BaseApplication {
}
}
// if (action.type === 'NOTE_DELETE') {
// // Update folders if a note is deleted in case the deleted note was a conflict
// await FoldersScreenUtils.refreshFolders();
// }
if (this.hasGui() && action.type == 'SETTING_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTING_UPDATE_ALL') {
reg.setupRecurrentSync();
}
@@ -279,6 +305,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 +368,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 +386,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,7 @@ 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;
@@ -80,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();
@@ -89,7 +100,7 @@ class MdToHtml {
const href = this.getAttr_(attrs, 'src');
if (!Resource.isResourceUrl(href)) {
return '<span>' + href + '</span><img title="' + htmlentities(title) + '" src="' + href + '"/>';
return '<img title="' + htmlentities(title) + '" src="' + href + '"/>';
}
const resourceId = Resource.urlToId(href);
@@ -121,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);
@@ -139,7 +150,7 @@ class MdToHtml {
const isResourceUrl = Resource.isResourceUrl(href);
if (isResourceUrl && !this.supportsResourceLinks_) {
return ']';
return ')</span>';
} else {
return '</a>';
}
@@ -148,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;
@@ -179,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);
@@ -224,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 + '>');
}
@@ -301,15 +314,19 @@ class MdToHtml {
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;
}

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

@@ -62,11 +62,12 @@ class BaseModel {
return temp;
}
static fieldType(name) {
static fieldType(name, defaultValue = null) {
let fields = this.fields();
for (let i = 0; i < fields.length; i++) {
if (fields[i].name == name) return fields[i].type;
}
if (defaultValue !== null) return defaultValue;
throw new Error('Unknown field: ' + name);
}
@@ -201,11 +202,11 @@ class BaseModel {
let output = {};
let type = null;
for (let n in newModel) {
if (!newModel.hasOwnProperty(n)) continue;
if (n == 'type_') {
type = n;
type = newModel[n];
continue;
}
if (!newModel.hasOwnProperty(n)) continue;
if (!(n in oldModel) || newModel[n] !== oldModel[n]) {
output[n] = newModel[n];
}
@@ -214,6 +215,12 @@ class BaseModel {
return output;
}
static modelsAreSame(oldModel, newModel) {
const diff = this.diffObjects(oldModel, newModel);
delete diff.type_;
return !Object.getOwnPropertyNames(diff).length;
}
static saveQuery(o, options) {
let temp = {}
let fieldNames = this.fieldNames();
@@ -232,6 +239,9 @@ class BaseModel {
o.updated_time = timeNow;
}
// The purpose of user_updated_time is to allow the user to manually set the time of a note (in which case
// options.autoTimestamp will be `false`). However note that if the item is later changed, this timestamp
// will be set again to the current time.
if (options.autoTimestamp && this.hasField('user_updated_time')) {
o.user_updated_time = timeNow;
}
@@ -272,6 +282,18 @@ class BaseModel {
options = this.modOptions(options);
options.isNew = this.isNew(o, options);
// Diff saving is an optimisation which takes a new version of the item and an old one,
// do a diff and save only this diff. IMPORTANT: When using this make sure that both
// models have been normalised using ItemClass.filter()
const isDiffSaving = options && options.oldItem && !options.isNew;
if (isDiffSaving) {
const newObject = BaseModel.diffObjects(options.oldItem, o);
newObject.type_ = o.type_;
newObject.id = o.id;
o = newObject;
}
o = this.filter(o);
let queries = [];
@@ -286,12 +308,21 @@ 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;
if ('user_created_time' in saveQuery.modObject) o.user_created_time = saveQuery.modObject.user_created_time;
o = this.addModelMd(o);
if (isDiffSaving) {
for (let n in options.oldItem) {
if (!options.oldItem.hasOwnProperty(n)) continue;
if (n in o) continue;
o[n] = options.oldItem[n];
}
}
return this.filter(o);
}).catch((error) => {
Log.error('Cannot save model', error);
@@ -322,23 +353,32 @@ class BaseModel {
let output = Object.assign({}, model);
for (let n in output) {
if (!output.hasOwnProperty(n)) continue;
// The SQLite database doesn't have booleans so cast everything to int
if (output[n] === true) output[n] = 1;
if (output[n] === false) output[n] = 0;
if (output[n] === true) {
output[n] = 1;
} else if (output[n] === false) {
output[n] = 0;
} else {
const t = this.fieldType(n, Database.TYPE_UNKNOWN);
if (t === Database.TYPE_INT) {
output[n] = !n ? 0 : parseInt(output[n], 10);
}
}
}
return output;
}
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 +400,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

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

@@ -1,6 +1,6 @@
const React = require('react'); const Component = React.Component;
const { connect } = require('react-redux');
const { Platform, View, Text, Button, StyleSheet, TouchableOpacity, Image } = require('react-native');
const { Platform, View, Text, Button, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions } = require('react-native');
const Icon = require('react-native-vector-icons/Ionicons').default;
const { Log } = require('lib/log.js');
const { BackButtonService } = require('lib/services/back-button.js');
@@ -8,6 +8,8 @@ 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');
@@ -16,6 +18,8 @@ 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
@@ -147,7 +151,6 @@ class ScreenHeaderComponent extends Component {
async backButton_press() {
await BackButtonService.back();
//this.props.dispatch({ type: 'NAV_BACK' });
}
searchButton_press() {
@@ -157,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();
@@ -204,7 +218,7 @@ class ScreenHeaderComponent extends Component {
const itemListCsv = await service.basicItemList({ format: 'csv' });
const filePath = RNFS.ExternalDirectoryPath + '/syncReport-' + (new Date()).getTime() + '.txt';
const finalText = [logItemCsv, itemListCsv].join("\n--------------------------------------------------------------------------------");
const finalText = [logItemCsv, itemListCsv].join("\n================================================================================\n");
await RNFS.writeFile(filePath, finalText);
alert('Debug report exported to ' + filePath);
@@ -256,59 +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_log'} 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_status'} 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 (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.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,
}}
@@ -320,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 {
@@ -330,22 +406,35 @@ 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 windowHeight = Dimensions.get('window').height - 50;
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>
<ScrollView style={{ maxHeight: windowHeight }}>
{ menuOptionComponents }
</ScrollView>
</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>
);
}
@@ -361,8 +450,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();
@@ -29,16 +29,46 @@ shared.saveNoteButton_press = async function(comp) {
}
let isNew = !note.id;
let titleWasAutoAssigned = false;
if (isNew && !note.title) {
note.title = Note.defaultTitle(note);
titleWasAutoAssigned = true;
}
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).
//
// If the title was auto-assigned above, we don't restore
// it from the state because it will be empty there.
if (!titleWasAutoAssigned) 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 +80,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 };

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