Compare commits

...

352 Commits

Author SHA1 Message Date
Sebastian Stenzel
c6d88f1dc4 Merge branch 'release/1.5.0-beta3' 2020-02-27 10:02:37 +01:00
Sebastian Stenzel
4a2d71405d preparing 1.5.0-beta3 2020-02-27 09:58:37 +01:00
Sebastian Stenzel
f98c3aa5ca New Crowdin translations (#1064)
[ci skip]
2020-02-27 09:54:48 +01:00
Sebastian Stenzel
7d389b561d use shorter names for rtf files to make sure the name doesn't get collapsed in the file explorer 2020-02-27 09:40:52 +01:00
Sebastian Stenzel
7056d35254 refer to docs.cryptomator.org instead of google.com in README file 2020-02-27 09:39:03 +01:00
Sebastian Stenzel
6a8234ddd9 fixes #1046 2020-02-26 16:09:08 +01:00
Tobias Hagemann
3d3eed0ed2 updated logo and removed unused assets 2020-02-21 12:08:58 +01:00
Sebastian Stenzel
e479b35846 typo
[ci skip]
2020-02-20 17:43:19 +01:00
Sebastian Stenzel
83c7421a96 renamed keys
[ci skip]
2020-02-20 17:19:56 +01:00
Sebastian Stenzel
116091f2e0 New Crowdin translations (#1041)
* New translations
[ci skip]
2020-02-20 17:17:49 +01:00
Sebastian Stenzel
ce11017609 Moved masterkey-related vault options to its own tab
Also made remaining vault recovery labels localizable
2020-02-20 16:27:21 +01:00
Sebastian Stenzel
caa8c84d8a Can now use a recovery key to reset a vault's password 2020-02-20 15:31:22 +01:00
Sebastian Stenzel
e14fc56b37 fixed autocompletion for pasting text 2020-02-20 14:09:32 +01:00
Sebastian Stenzel
0d29e56948 Implemented word auto-completion for recovery key entry field 2020-02-20 11:10:42 +01:00
Sebastian Stenzel
d2a27c782d No longer warn about this method invocation 2020-02-20 11:09:52 +01:00
Sebastian Stenzel
864e92ea59 updated funding.yml 2020-02-20 09:51:43 +01:00
Sebastian Stenzel
97dfe9a1d4 fixed unit test 2020-02-20 00:28:48 +01:00
Sebastian Stenzel
d7edfd13a7 Began vault recovery wizard 2020-02-19 17:27:07 +01:00
Sebastian Stenzel
35207de7cc fixes #1056 2020-02-17 22:36:13 +01:00
Sebastian Stenzel
adf0e3720d dedup 2020-02-17 20:34:20 +01:00
Sebastian Stenzel
f73c1889b7 Show generic error screen in case of exceptions during migration (references #1056) 2020-02-17 20:23:19 +01:00
Sebastian Stenzel
18ff1d2898 fixes #986, fixes #1062 2020-02-17 18:45:31 +01:00
Sebastian Stenzel
c6e2fa11a4 updated cryptofs to 1.9.2, fixes #1057, references #1056 2020-02-17 18:14:08 +01:00
Sebastian Stenzel
3fac8b7cd6 updated dependencies 2020-02-17 18:12:58 +01:00
Sebastian Stenzel
f62faa72ce (Auto)Unlock via VaultService (fixes #1044) 2020-02-13 23:50:26 +01:00
Sebastian Stenzel
57256d0733 began implementation of new auto unlocker (references #1044) 2020-02-13 17:16:09 +01:00
Sebastian Stenzel
e1f44fb48a App lifecycle fixes 2020-02-13 16:29:49 +01:00
Sebastian Stenzel
62676d5a83 minor ui state tweaks
[ci skip]
2020-02-13 14:32:23 +01:00
Armin Schrenk
7755256956 further additions to #1060 2020-02-12 16:19:42 +01:00
Armin Schrenk
e1cf8546b7 adding recover vault dialogue stub 2020-02-12 15:02:06 +01:00
Tobias Hagemann
e40837da2c updated primary_d2 color (somehow there was a small discrepancy in the corporate design manual) [ci skip] 2020-01-31 13:25:11 +01:00
Tobias Hagemann
e7c3c0ab53 updated color palette 2020-01-30 15:33:54 +01:00
Sebastian Stenzel
2336b9c622 write vault migration events to additional upgrade log (references #1058) 2020-01-30 09:24:46 +01:00
Sebastian Stenzel
4e2122a64f Merge pull request #1050 from zhouer/close_request
Handle close request event for main window, fixes #1040
2020-01-16 12:22:56 +01:00
Sebastian Stenzel
833bb085e6 Using disting ExecutorService and ScheduledExecutorService since the latter doesn't scale beyond its pre-defined number of core threads
fixes #1051
2020-01-15 13:07:19 +01:00
En-Jan Chou
1ec9b0507d Prevent window close if user choose cancel in quit window 2020-01-14 17:12:36 -05:00
En-Jan Chou
b6a4f70ec9 Handle close request event for main window
When receiving close request event, main window should quit program
if there is no system tray available, and just like the close button
was clicked. The window close request event happens when user close
window by pressing alt-f4 (Gnome) or when receiving WM_DELETE_WINDOW
xwindow event. Current behavior is just close the window and let the
program keep running, and there is no way to quit program without
system tray.
2020-01-14 16:54:47 -05:00
Sebastian Stenzel
eb047e29a0 jcenter requires https now
[ci skip]
2020-01-14 22:06:01 +01:00
Sebastian Stenzel
5dc1d9b2e0 Fail fast (ammendment for #1047) 2020-01-14 21:46:52 +01:00
Sebastian Stenzel
362f3eac63 Certain app lifecycle event handlers are now now longer @TrayMenuScoped but rather @Singleton, so they get set up even if there is no TrayIcon (references #1035) 2020-01-14 21:43:45 +01:00
Sebastian Stenzel
f0b26c60c4 Merge pull request #1045 from zhouer/minimize
Add minimize button, fixes #1035
2020-01-14 20:59:10 +01:00
Sebastian Stenzel
e827d86967 Merge pull request #1047 from zhouer/icons
Add multiple icons and let system choose the best size
2020-01-14 20:54:18 +01:00
Tobias Hagemann
04533c0338 downgrading font awesome from pro to free 2020-01-14 16:57:13 +01:00
En-Jan Chou
e148e39fef Rollback TrayMenuController to FxApplication code moving 2020-01-13 03:48:34 -05:00
En-Jan Chou
cb97905c98 Use static emptyList and close InputStream 2020-01-12 21:17:56 -05:00
En-Jan Chou
7ec7cecbfd Add multiple icons and let system choose the best size 2020-01-12 13:42:17 -05:00
En-Jan Chou
3369401f1c Add minimize button and tray menu refactoring
* Add minimize button and change close button behavior
  * close main window (minimize to tray) if system tray available,
    quit application otherwise.
  * show minimize button if system tray unavailable
* Move some codes from TrayMenuController to FxApplication, includes:
  * Desktop integrations (shortcut, quit handlers...)
  * vaults change listener for sudden termination
  * public method showPreferenceWindow change to showPerferenceTab
    due to name conflict
  * public method quitApplication for both main window and system tray
  * shutdown hook for unmounting vaults on shutdown
* Add a new i18n string: main.minimizeBtn.tooltip
2020-01-12 05:29:56 -05:00
Sebastian Stenzel
09b9cfe027 onboarding text in empty vault list is now centered 2020-01-10 16:12:00 +01:00
Sebastian Stenzel
e94ac3720b onboarding text in empty vault list is now centered 2020-01-10 14:46:25 +01:00
Sebastian Stenzel
4414d60026 Merge branch 'master' into develop 2020-01-09 17:16:40 +01:00
Sebastian Stenzel
a6e680e32d Merge branch 'release/1.5.0-beta2' 2020-01-09 17:15:55 +01:00
Sebastian Stenzel
d60a024c63 preparing 1.5.0-beta2 2020-01-09 16:50:38 +01:00
Sebastian Stenzel
cc1b536656 New Crowdin translations (#1039)
[ci skip]
2020-01-09 16:49:39 +01:00
Sebastian Stenzel
85e773abef enable themes for donators 2020-01-09 16:34:31 +01:00
Sebastian Stenzel
4690bc56aa fixes #1038 2020-01-09 16:04:46 +01:00
Sebastian Stenzel
292c2676fd fixes #980 2020-01-09 15:44:25 +01:00
Sebastian Stenzel
f7c6604c7c fixes #1029 2020-01-09 15:16:13 +01:00
Sebastian Stenzel
22310eb957 updated donation key store uri 2020-01-09 14:38:23 +01:00
Sebastian Stenzel
6d17d1298b removed unused fields 2020-01-09 13:03:22 +01:00
Sebastian Stenzel
e90a680620 Split up MainWindowController, as it grew too big. The title bar is now handled by MainWindowTitleController
(@zhouer add the minimize button for #1035 to this new controller and fxml file)
2020-01-09 12:18:24 +01:00
Sebastian Stenzel
35d0ccfe89 allow resizing main window on all four corners
fixes #995
2020-01-09 12:06:12 +01:00
Tobias Hagemann
62cc454367 updated license public key 2020-01-09 10:35:27 +01:00
Tobias Hagemann
8bb277fe56 added iCloud Drive and OneDrive location presets 2020-01-09 10:27:56 +01:00
Sebastian Stenzel
67485b76af Make sure that adding a vault located at ~/foo is noop when a vault located at /home/user/foo already exists
fixes #1032
2020-01-08 16:50:08 +01:00
Sebastian Stenzel
270e60d7cf updated logging
[ci skip]
2020-01-08 15:41:26 +01:00
Sebastian Stenzel
11ffec862e Added autostart support for windows
fixes #418
2020-01-07 17:56:05 +01:00
Sebastian Stenzel
d37e0dd5e1 Determine autostart state async (#418) 2020-01-07 17:55:36 +01:00
Sebastian Stenzel
960c3665e0 Localizations 2020-01-06 12:54:51 +01:00
Sebastian Stenzel
3396c79c38 updated intellij config
[ci skip]
2020-01-06 12:45:01 +01:00
Sebastian Stenzel
cc1517f1b8 Added checkbox for auto start on macOS (#418) 2020-01-06 12:37:36 +01:00
Sebastian Stenzel
4f3ff55ae7 updated stale config
[ci skip]
2020-01-06 09:48:18 +01:00
Sebastian Stenzel
f51362e95e cleanup
[ci skip]
2020-01-06 09:46:14 +01:00
Sebastian Stenzel
415dfece58 sames as in 9b6145ce:
We only want the user to trigger the listener
2020-01-06 09:46:01 +01:00
Sebastian Stenzel
3bdb5353da New Crowdin translations (#1015)
[ci skip]
2020-01-04 22:11:48 +01:00
Sebastian Stenzel
6ddf1b59ef Merge pull request #1031 from zhouer/hide_start_hidden
Hide startHidden setting when system tray is not supported
2019-12-29 14:18:35 +01:00
En-Jan Chou
ef88f11c4f Hide startHidden setting when system tray is not supported
For platform not supporting system tray, users will have no chance to show
window and modify settings back again if the window is hidden at startup.
2019-12-26 09:40:33 -05:00
Sebastian Stenzel
ec800c5439 Enforce minimum password length (amendment to #1018) 2019-12-18 15:31:52 +01:00
Sebastian Stenzel
a2f3f5d254 fixes #1018, references #979 2019-12-18 14:58:36 +01:00
Sebastian Stenzel
f475f70adf Updated zxcvbn4j to 1.3.0, which allows us to swipe the password from memory even after measuring its strength 2019-12-18 14:40:04 +01:00
Sebastian Stenzel
93c3da66da Merge pull request #1024 from zhouer/fix_mount_point
Initialize radio button before binding settings to it
2019-12-18 12:16:12 +01:00
En-Jan Chou
9b6145ce3f Initialize radio button before binding settings to it
The usesIndividualMountPath setting will be cleared if it bind to
an uninitialized radio button. This patch sets radio button with
current settings before binding the setting to the radio button.
2019-12-18 02:39:03 -05:00
Sebastian Stenzel
148eed172a Deduplicated UI code
Made "Enter Password" and "Reenter Password" a reusable component that is included in its original places
preparation for #1018
2019-12-17 17:35:16 +01:00
Sebastian Stenzel
c97f146964 enabled "lock all" from tray menu (#297, #863) 2019-12-16 14:44:14 +01:00
Sebastian Stenzel
a1034f5663 Refactored "lock all" task 2019-12-16 14:43:33 +01:00
Sebastian Stenzel
938b351f33 enabled "reveal" from tray menu (#297) 2019-12-16 13:10:43 +01:00
Sebastian Stenzel
d870ecbcdb moved reveal() to VaultService 2019-12-16 13:03:28 +01:00
Sebastian Stenzel
5dff2126c4 cleanup of new VaultService 2019-12-16 11:22:30 +01:00
Sebastian Stenzel
41a6cc15e8 run "lock all" service on shared executorservice 2019-12-16 10:32:35 +01:00
Sebastian Stenzel
f21b30c009 removed unused field 2019-12-16 10:31:04 +01:00
Sebastian Stenzel
d69b63acc3 enabled "lock" from tray menu (#297) 2019-12-11 15:57:39 +01:00
Sebastian Stenzel
f95b2baad5 "lock" deduplication 2019-12-11 15:57:14 +01:00
Sebastian Stenzel
1a4d1fffb3 shutdown logback manually 2019-12-11 13:53:32 +01:00
Sebastian Stenzel
ac536ba125 Some shutdown hook refactorings for #980 2019-12-04 17:50:30 +01:00
Sebastian Stenzel
362b225d66 Fixes #939 2019-11-27 16:39:40 +01:00
Sebastian Stenzel
7d0bdc1a63 Show stack trace in case of unexpected exceptions 2019-11-27 15:44:53 +01:00
Sebastian Stenzel
94e3b21e44 Typo
[ci skip]
2019-11-27 13:20:44 +01:00
Sebastian Stenzel
23f89c1dc9 Reevaluate vault state when selecting vault 2019-11-26 17:21:28 +01:00
Sebastian Stenzel
ce9c2f2c7a removed unused import
[ci skip]
2019-11-26 17:21:08 +01:00
Tobias Hagemann
44e97ab046 smaller text size for path in reveal button [ci skip] 2019-11-26 17:02:13 +01:00
Sebastian Stenzel
5d0a1fd49f tabpane styling
[ci skip]
2019-11-26 16:39:32 +01:00
Sebastian Stenzel
ffc1d8dc1e simplified 2019-11-22 14:23:55 +01:00
Sebastian Stenzel
54fca93bba deduplicated code 2019-11-22 14:21:51 +01:00
Sebastian Stenzel
c281687910 added copy/print buttons to recovery key dialog 2019-11-22 13:43:40 +01:00
Sebastian Stenzel
6229d7abbe Updated password and recovery key instructions 2019-11-22 13:10:50 +01:00
Sebastian Stenzel
01644eddb6 increase wizard dimensions
[ci skip]
2019-11-21 16:58:05 +01:00
Sebastian Stenzel
6e30df3796 Let the user choose whether a recovery key should be shown during vault creation 2019-11-21 16:57:43 +01:00
Sebastian Stenzel
f87fa319ff renamed class 2019-11-20 17:14:31 +01:00
Sebastian Stenzel
9b019726bb show badge for missing license key in window title 2019-11-20 17:12:11 +01:00
Sebastian Stenzel
cceafb76ed New Crowdin translations
New translations strings.properties
[ci skip]
2019-11-20 17:09:01 +01:00
Tobias Hagemann
39e2994c69 updated drag-n-drop-indicator styling 2019-11-20 16:53:02 +01:00
Sebastian Stenzel
1ddfcc3219 reworked drag'n'drop l&f 2019-11-20 15:03:03 +01:00
Tobias Hagemann
687f11596e added disabled styling to choice-box 2019-11-19 21:52:52 +01:00
Sebastian Stenzel
b445f614fb Made mount point settings localizable
(and some fxml cleanup)
[ci skip]
2019-11-19 21:39:00 +01:00
Sebastian Stenzel
c6fc1d93a0 save donation key to settings 2019-11-19 21:20:00 +01:00
Sebastian Stenzel
28d58922e3 TextArea needs a prefRowCount, otherwise it attempts to use all available space, displacing other elements.
fixes #1012
2019-11-18 10:11:20 +01:00
Sebastian Stenzel
b884ea7ddc added registration key to preferences 2019-11-15 15:56:35 +01:00
Sebastian Stenzel
1717e20b61 Add JWT verifier 2019-11-14 17:02:41 +01:00
Sebastian Stenzel
6222f96b01 Merge branch 'master' into develop 2019-11-12 13:40:34 +01:00
Sebastian Stenzel
88f83c5cdb Merge branch 'release/1.5.0-beta1' 2019-11-12 13:33:08 +01:00
Sebastian Stenzel
c8059bef78 bumped version
[ci skip]
2019-11-12 13:32:51 +01:00
Sebastian Stenzel
638b731cf8 always set version to SNAPSHOT-{commit} for untagged CI builds 2019-11-12 13:23:05 +01:00
Sebastian Stenzel
b2b6e304ed updated CI config 2019-11-12 13:13:58 +01:00
Sebastian Stenzel
0748709f40 Updated dependencies 2019-11-11 16:42:56 +01:00
Sebastian Stenzel
d8ee446d37 New Crowdin translations (#987)
* New translations strings.properties
[ci skip]
2019-11-11 16:36:48 +01:00
Sebastian Stenzel
9932185ccc fixed typo
kudos to our translation team
[ci skip]
2019-10-31 22:10:37 +01:00
Sebastian Stenzel
77afd4688a ide settings
[ci skip]
2019-10-31 22:10:09 +01:00
Sebastian Stenzel
9a5ef3f6ff updated cryptofs to 1.9.0-rc1 2019-10-31 01:29:30 +01:00
Sebastian Stenzel
c99f23a4c7 Merge branch 'support/1.4.x' into develop
# Conflicts:
#	main/buildkit/pom.xml
#	main/commons/pom.xml
#	main/keychain/pom.xml
#	main/launcher/pom.xml
#	main/pom.xml
#	main/ui/pom.xml
2019-10-31 01:11:02 +01:00
Sebastian Stenzel
274c3886f5 Merge branch 'hotfix/1.4.17' into support/1.4.x 2019-10-31 01:01:24 +01:00
Sebastian Stenzel
95db29158c fixes #997 2019-10-31 00:54:49 +01:00
Sebastian Stenzel
872680a737 updated dependencies, removed commons-io 2019-10-31 00:54:37 +01:00
Sebastian Stenzel
0ba12f6301 bumped version to 1.4.17 2019-10-31 00:54:09 +01:00
Armin Schrenk
248df9da51 TODOs 2019-10-23 14:23:53 +02:00
Armin Schrenk
702408d488 phrasing 2019-10-23 14:23:44 +02:00
Armin Schrenk
dd1506f17a windows: fixing that already used drive letters are not shown as such in drop down menu 2019-10-23 13:07:41 +02:00
Sebastian Stenzel
7fb5c741ad get rid of some legacy code with too much if/else 2019-10-18 14:34:48 +02:00
Sebastian Stenzel
aa61ab2b6e allow changing of user interface orientation via settings 2019-10-18 12:51:41 +02:00
Armin Schrenk
7a1e20d732 showing custom mountPath if selected 2019-10-16 16:58:50 +02:00
Armin Schrenk
c1a5e187b6 localization 2019-10-16 16:49:21 +02:00
Armin Schrenk
57d3f788e6 further improving windows mount options 2019-10-16 16:41:28 +02:00
Armin Schrenk
ca73c3ad90 Updating WinDriveLetters since Dokany be installed in version 1.3.x 2019-10-16 15:57:08 +02:00
Armin Schrenk
aec367dcc7 adding TODO 2019-10-16 15:49:17 +02:00
Armin Schrenk
56e7c13cb1 including windows mount option for selecting mount point (without functionality) 2019-10-16 14:32:10 +02:00
Sebastian Stenzel
3b4f384bfd there shouldn't be a "."
[ci skip]
2019-10-14 10:51:16 +02:00
Sebastian Stenzel
d1a20da7e0 Localization [ci skip] 2019-10-14 10:22:23 +02:00
Tobias Hagemann
4b3533f717 say my name [ci skip] 2019-10-14 00:31:49 +02:00
Sebastian Stenzel
6535adef44 Merge branch 'master' into develop 2019-10-13 15:42:09 +02:00
Sebastian Stenzel
f69cde1469 Merge branch 'release/1.5.0-alpha2' 2019-10-13 15:41:27 +02:00
Sebastian Stenzel
59643b762f Preparing 1.5.0-alpha2 2019-10-13 15:36:52 +02:00
Sebastian Stenzel
ec69c1411b Merge commit 'ad28d4510d8b99db02c1883f1061817b658f3407' into develop
# Conflicts:
#	main/ui/src/main/resources/i18n/strings_de.properties
2019-10-13 15:35:00 +02:00
Sebastian Stenzel
ad28d4510d New translations strings.properties (German)
[ci skip]
2019-10-13 15:26:53 +02:00
Sebastian Stenzel
111e500928 only show migration's progressbar when there actually is any progress 2019-10-13 15:20:38 +02:00
Sebastian Stenzel
b5cb129ff0 updated to lastest cryptofs beta 2019-10-13 14:46:25 +02:00
Sebastian Stenzel
8a1586e5e8 New translations strings.properties (Japanese)
[ci skip]
2019-10-12 08:15:37 +02:00
Sebastian Stenzel
e1959211de New translations strings.properties (French)
[ci skip]
2019-10-11 22:50:43 +02:00
Sebastian Stenzel
53494fa141 New translations strings.properties (French)
[ci skip]
2019-10-11 22:08:38 +02:00
Sebastian Stenzel
7fdab3a2ab New translations strings.properties (French)
[ci skip]
2019-10-11 21:27:17 +02:00
Sebastian Stenzel
cb749c2fba New translations strings.properties (French)
[ci skip]
2019-10-11 19:58:32 +02:00
Sebastian Stenzel
453efe9998 New translations strings.properties (French)
[ci skip]
2019-10-11 19:26:49 +02:00
Sebastian Stenzel
2948b78cbe showing the window shouldn't be a decision made by the vault list controller.
when opening a .cryptomator file, the handler for open file requests is responsible for showing the main window.
2019-10-11 15:48:28 +02:00
Sebastian Stenzel
5a9f993df8 updated cryptofs to fix a migration problem 2019-10-11 13:23:19 +02:00
Sebastian Stenzel
4936cc76d0 New translations strings.properties (Korean)
[ci skip]
2019-10-11 00:45:55 +02:00
Sebastian Stenzel
4a15467ff5 New translations strings.properties (Italian)
[ci skip]
2019-10-10 22:41:06 +02:00
Sebastian Stenzel
bb09b32885 New translations strings.properties (French)
[ci skip]
2019-10-10 18:57:37 +02:00
Sebastian Stenzel
4c9372747c New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-10 12:59:48 +02:00
Sebastian Stenzel
1c04c22617 New translations strings.properties (Japanese)
[ci skip]
2019-10-10 10:18:14 +02:00
Sebastian Stenzel
675146d20c New translations strings.properties (Japanese)
[ci skip]
2019-10-10 09:48:14 +02:00
Sebastian Stenzel
56ee6af9de New translations strings.properties (Russian)
[ci skip]
2019-10-10 08:32:02 +02:00
Sebastian Stenzel
926a0b3717 New translations strings.properties (Spanish)
[ci skip]
2019-10-10 06:26:05 +02:00
Sebastian Stenzel
eae28ce76d New translations strings.properties (Spanish)
[ci skip]
2019-10-10 05:57:57 +02:00
Sebastian Stenzel
99c34539e6 New translations strings.properties (German)
[ci skip]
2019-10-09 21:28:50 +02:00
Sebastian Stenzel
0dfafdd874 New translations strings.properties (German)
[ci skip]
2019-10-09 21:02:41 +02:00
Sebastian Stenzel
a8cb40831e New translations strings.properties (Dutch)
[ci skip]
2019-10-09 20:21:07 +02:00
Sebastian Stenzel
3b1bed9345 New translations strings.properties (Swedish)
[ci skip]
2019-10-09 19:29:01 +02:00
Sebastian Stenzel
f18b81bdb7 New translations strings.properties (Spanish)
[ci skip]
2019-10-09 19:01:38 +02:00
Armin Schrenk
4a61fe372e enhancing error handling in storage location selection when creating a new vault 2019-10-09 18:24:23 +02:00
Sebastian Stenzel
0bd0543d10 New translations strings.properties (Russian)
[ci skip]
2019-10-09 17:49:00 +02:00
Sebastian Stenzel
06a3a04840 New translations strings.properties (Italian)
[ci skip]
2019-10-09 17:48:58 +02:00
Sebastian Stenzel
3eb379b1e9 New translations strings.properties (Swedish)
[ci skip]
2019-10-09 17:07:27 +02:00
Sebastian Stenzel
52e7707f81 New translations strings.properties (Greek)
[ci skip]
2019-10-09 17:07:25 +02:00
Sebastian Stenzel
28d2424cd5 New translations strings.properties (Japanese)
[ci skip]
2019-10-09 17:07:23 +02:00
Sebastian Stenzel
187e9f17fc New translations strings.properties (Korean)
[ci skip]
2019-10-09 17:07:22 +02:00
Sebastian Stenzel
b9776a1017 New translations strings.properties (Croatian)
[ci skip]
2019-10-09 17:07:21 +02:00
Sebastian Stenzel
3fbf28eea9 New translations strings.properties (Portuguese)
[ci skip]
2019-10-09 17:07:19 +02:00
Sebastian Stenzel
99e7ec7dc2 New translations strings.properties (Turkish)
[ci skip]
2019-10-09 17:07:18 +02:00
Sebastian Stenzel
a9b4512ce6 New translations strings.properties (Dutch)
[ci skip]
2019-10-09 17:07:16 +02:00
Sebastian Stenzel
feb9ee238d New translations strings.properties (French)
[ci skip]
2019-10-09 17:07:14 +02:00
Sebastian Stenzel
748f7ca889 New translations strings.properties (Czech)
[ci skip]
2019-10-09 17:07:12 +02:00
Sebastian Stenzel
87a469f264 New translations strings.properties (Spanish)
[ci skip]
2019-10-09 17:07:11 +02:00
Sebastian Stenzel
59e0175d65 New translations strings.properties (Russian)
[ci skip]
2019-10-09 17:07:09 +02:00
Sebastian Stenzel
7595f5317d New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-09 17:07:07 +02:00
Sebastian Stenzel
3ba4e87f1d New translations strings.properties (Italian)
[ci skip]
2019-10-09 17:07:06 +02:00
Sebastian Stenzel
0a7aec26cc New translations strings.properties (German)
[ci skip]
2019-10-09 17:07:04 +02:00
Armin Schrenk
d70b7e12ef add TODO 2019-10-09 16:48:20 +02:00
Armin Schrenk
54d2591391 adding error screen for adding existing vault 2019-10-09 16:48:07 +02:00
Armin Schrenk
4f70695ceb reducing possible mistake when adding new controller to module 2019-10-09 16:45:50 +02:00
Sebastian Stenzel
8f0a151018 New translations strings.properties (Spanish)
[ci skip]
2019-10-09 16:24:33 +02:00
Sebastian Stenzel
cd64460e62 added shake animation 2019-10-09 16:15:58 +02:00
Sebastian Stenzel
db3c0622c6 New translations strings.properties (Swedish)
[ci skip]
2019-10-09 15:55:05 +02:00
Sebastian Stenzel
ac0092b178 New translations strings.properties (Korean)
[ci skip]
2019-10-09 15:55:02 +02:00
Sebastian Stenzel
fab32e9e93 New translations strings.properties (Russian)
[ci skip]
2019-10-09 15:55:00 +02:00
Sebastian Stenzel
3d1129b0f3 New translations strings.properties (Italian)
[ci skip]
2019-10-09 15:54:57 +02:00
Sebastian Stenzel
89b2ff74f3 New translations strings.properties (German)
[ci skip]
2019-10-09 15:54:54 +02:00
Sebastian Stenzel
a43e8fc461 New translations strings.properties (Swedish)
[ci skip]
2019-10-09 15:21:38 +02:00
Sebastian Stenzel
98d3fddfcc New translations strings.properties (Greek)
[ci skip]
2019-10-09 15:21:36 +02:00
Sebastian Stenzel
ff36fff091 New translations strings.properties (Japanese)
[ci skip]
2019-10-09 15:21:35 +02:00
Sebastian Stenzel
7d02108c8b New translations strings.properties (Korean)
[ci skip]
2019-10-09 15:21:33 +02:00
Sebastian Stenzel
593f37d5fe New translations strings.properties (Croatian)
[ci skip]
2019-10-09 15:21:32 +02:00
Sebastian Stenzel
7abe9c627b New translations strings.properties (Portuguese)
[ci skip]
2019-10-09 15:21:30 +02:00
Sebastian Stenzel
a0923c3c9b New translations strings.properties (Turkish)
[ci skip]
2019-10-09 15:21:29 +02:00
Sebastian Stenzel
439042ab95 New translations strings.properties (Dutch)
[ci skip]
2019-10-09 15:21:28 +02:00
Sebastian Stenzel
113e505c52 New translations strings.properties (French)
[ci skip]
2019-10-09 15:21:26 +02:00
Sebastian Stenzel
f01e0ae194 New translations strings.properties (Czech)
[ci skip]
2019-10-09 15:21:25 +02:00
Sebastian Stenzel
5540cef257 New translations strings.properties (Spanish)
[ci skip]
2019-10-09 15:21:23 +02:00
Sebastian Stenzel
ab16ee493f New translations strings.properties (Russian)
[ci skip]
2019-10-09 15:21:21 +02:00
Sebastian Stenzel
b1a1d1029c New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-09 15:21:20 +02:00
Sebastian Stenzel
a46b4aa768 New translations strings.properties (Italian)
[ci skip]
2019-10-09 15:21:18 +02:00
Sebastian Stenzel
245fe4b525 New translations strings.properties (German)
[ci skip]
2019-10-09 15:21:17 +02:00
Sebastian Stenzel
c5a9926652 Added localizable labels to recovery key display window 2019-10-09 15:15:13 +02:00
Sebastian Stenzel
7032862a65 Added basic CSS for TextArea 2019-10-09 15:01:43 +02:00
Sebastian Stenzel
2012229c46 fixed tests 2019-10-09 14:14:38 +02:00
Sebastian Stenzel
a6672ddbfc oxfordTop5000WordsSortedByLength.stream().filter(Pattern.compile("[a-z]{2,8}").asMatchPredicate()).distinct().limit(4096) 2019-10-09 14:04:28 +02:00
Sebastian Stenzel
8dd2147638 AppleScriptEngineFactory no longer exists since Java 9 (see https://bugs.openjdk.java.net/browse/JDK-8143404)
Formerly used in TrayIconUtil.java, which no longer exists either
2019-10-09 11:30:01 +02:00
Sebastian Stenzel
7a29a1b680 New translations strings.properties (Swedish)
[ci skip]
2019-10-09 08:48:14 +02:00
Sebastian Stenzel
f4983f7862 New translations strings.properties (Spanish)
[ci skip]
2019-10-08 23:46:34 +02:00
Sebastian Stenzel
488bd1087a New translations strings.properties (German)
[ci skip]
2019-10-08 22:23:21 +02:00
Sebastian Stenzel
d557136295 New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-08 21:42:26 +02:00
Sebastian Stenzel
87f4b639a0 New translations strings.properties (Italian)
[ci skip]
2019-10-08 20:38:54 +02:00
Sebastian Stenzel
dbb9379a2e New translations strings.properties (Swedish)
[ci skip]
2019-10-08 20:10:14 +02:00
Sebastian Stenzel
fa86ae68ea Removed old 1.4.x localizations 2019-10-08 20:03:02 +02:00
Sebastian Stenzel
f82fddc8fe Made recovery key creation a two-step wizard 2019-10-08 19:18:49 +02:00
Sebastian Stenzel
e6f0b321cb New translations strings.properties (German)
[ci skip]
2019-10-08 18:36:38 +02:00
Sebastian Stenzel
41fef58450 New translations strings.properties (Dutch)
[ci skip]
2019-10-08 18:01:08 +02:00
Sebastian Stenzel
268026629d New translations strings.properties (Spanish)
[ci skip]
2019-10-08 18:01:07 +02:00
Sebastian Stenzel
b95fa1868a New translations strings.properties (German)
[ci skip]
2019-10-08 18:01:06 +02:00
Sebastian Stenzel
d2b7376e37 New translations strings.properties (Korean)
[ci skip]
2019-10-08 17:19:31 +02:00
Sebastian Stenzel
8d6eac63e2 New translations strings.properties (Turkish)
[ci skip]
2019-10-08 17:19:29 +02:00
Sebastian Stenzel
b8fc2dcb64 New translations strings.properties (Russian)
[ci skip]
2019-10-08 17:19:27 +02:00
Sebastian Stenzel
671f934934 New translations strings.properties (Swedish)
[ci skip]
2019-10-08 16:28:17 +02:00
Sebastian Stenzel
58ff6554fc New translations strings.properties (Japanese)
[ci skip]
2019-10-08 16:28:14 +02:00
Sebastian Stenzel
cad6b221d9 New translations strings.properties (Korean)
[ci skip]
2019-10-08 16:28:13 +02:00
Sebastian Stenzel
c02d5a9794 New translations strings.properties (Turkish)
[ci skip]
2019-10-08 16:28:09 +02:00
Sebastian Stenzel
aac71277e3 New translations strings.properties (Dutch)
[ci skip]
2019-10-08 16:28:07 +02:00
Sebastian Stenzel
aa61be7bf5 New translations strings.properties (French)
[ci skip]
2019-10-08 16:28:05 +02:00
Sebastian Stenzel
3a4f796c79 New translations strings.properties (Czech)
[ci skip]
2019-10-08 16:28:04 +02:00
Sebastian Stenzel
f070cdc12d New translations strings.properties (Spanish)
[ci skip]
2019-10-08 16:28:02 +02:00
Sebastian Stenzel
e14911d4d5 New translations strings.properties (Russian)
[ci skip]
2019-10-08 16:28:00 +02:00
Sebastian Stenzel
aa666ad025 New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-08 16:27:59 +02:00
Sebastian Stenzel
cd55c2666f New translations strings.properties (Italian)
[ci skip]
2019-10-08 16:27:57 +02:00
Sebastian Stenzel
0dc6032209 New translations strings.properties (German)
[ci skip]
2019-10-08 16:27:56 +02:00
Sebastian Stenzel
f7630c28d6 Generate recovery key during vault creation 2019-10-08 16:26:57 +02:00
Sebastian Stenzel
08d9beb6b8 Externalized logic of recovery key creation to reusable utility class 2019-10-08 14:58:55 +02:00
Sebastian Stenzel
5808239416 Refactored shortcut handlers 2019-10-08 12:46:10 +02:00
Sebastian Stenzel
e2f400340b added test for issue #979 2019-10-08 11:16:13 +02:00
Sebastian Stenzel
5854b24c44 updated badge link 2019-10-08 11:16:07 +02:00
Tobias Hagemann
7adaf44fb3 added progress bar 2019-10-08 01:06:50 +02:00
Sebastian Stenzel
0bd74056a0 New translations strings.properties (Swedish)
[ci skip]
2019-10-07 13:42:39 +02:00
Sebastian Stenzel
bbeea7a2ee New translations strings.properties (Swedish)
[ci skip]
2019-10-06 17:55:46 +02:00
Sebastian Stenzel
e3548737f1 New translations strings.properties (Russian)
[ci skip]
2019-10-06 17:55:44 +02:00
Sebastian Stenzel
75d7656824 New translations strings.properties (Swedish)
[ci skip]
2019-10-06 17:26:01 +02:00
Sebastian Stenzel
c292f18915 New translations strings.properties (Russian)
[ci skip]
2019-10-06 17:25:59 +02:00
Sebastian Stenzel
9636e7c700 New translations strings.properties (Swedish)
[ci skip]
2019-10-06 16:56:15 +02:00
Sebastian Stenzel
eb8f7840cc New translations strings.properties (Korean)
[ci skip]
2019-10-06 11:55:39 +02:00
Sebastian Stenzel
180d79f49e New translations strings.properties (Korean)
[ci skip]
2019-10-06 11:25:49 +02:00
Sebastian Stenzel
29cb1c96b1 New translations strings.properties (Swedish)
[ci skip]
2019-10-06 10:26:28 +02:00
Sebastian Stenzel
f61432fc52 New translations strings.properties (French)
[ci skip]
2019-10-05 12:16:15 +02:00
Sebastian Stenzel
8e7dbf4640 New translations strings.properties (French)
[ci skip]
2019-10-05 11:50:53 +02:00
Sebastian Stenzel
d2528faf3a New translations strings.properties (French)
[ci skip]
2019-10-05 10:07:47 +02:00
Sebastian Stenzel
c1a3fe66ef New translations strings.properties (Japanese)
[ci skip]
2019-10-05 09:36:19 +02:00
Sebastian Stenzel
ecb4c114a2 New translations strings.properties (French)
[ci skip]
2019-10-05 09:36:17 +02:00
Sebastian Stenzel
1ecaf5ae6e New translations strings.properties (Spanish)
[ci skip]
2019-10-05 00:55:54 +02:00
Sebastian Stenzel
e88b7f00b3 New translations strings.properties (Spanish)
[ci skip]
2019-10-05 00:22:30 +02:00
Sebastian Stenzel
91aaabc7fb New translations strings.properties (French)
[ci skip]
2019-10-04 23:45:54 +02:00
Sebastian Stenzel
7414c29593 New translations strings.properties (French)
[ci skip]
2019-10-04 23:17:06 +02:00
Sebastian Stenzel
79ae5b7bff New translations strings.properties (French)
[ci skip]
2019-10-04 15:26:28 +02:00
Sebastian Stenzel
486a3a07f5 New translations strings.properties (Japanese)
[ci skip]
2019-10-04 14:50:00 +02:00
Sebastian Stenzel
02d44eb0e7 New translations strings.properties (Japanese)
[ci skip]
2019-10-04 13:56:22 +02:00
Sebastian Stenzel
3aff7bd956 New translations strings.properties (Japanese)
[ci skip]
2019-10-04 13:20:39 +02:00
Sebastian Stenzel
cd6a6cc55a New translations strings.properties (Korean)
[ci skip]
2019-10-04 07:36:24 +02:00
Sebastian Stenzel
d0a30a1779 New translations strings.properties (Korean)
[ci skip]
2019-10-04 03:26:03 +02:00
Sebastian Stenzel
a5fb30d7c7 New translations strings.properties (Japanese)
[ci skip]
2019-10-04 01:55:05 +02:00
Sebastian Stenzel
ba4e813a35 New translations strings.properties (French)
[ci skip]
2019-10-04 01:16:25 +02:00
Sebastian Stenzel
f6d4caee07 New translations strings.properties (Japanese)
[ci skip]
2019-10-04 00:46:37 +02:00
Sebastian Stenzel
d3a00e726f New translations strings.properties (French)
[ci skip]
2019-10-04 00:46:35 +02:00
Sebastian Stenzel
d71c0695f4 New translations strings.properties (French)
[ci skip]
2019-10-04 00:19:26 +02:00
Sebastian Stenzel
394c91c50c New translations strings.properties (Czech)
[ci skip]
2019-10-03 18:06:06 +02:00
Sebastian Stenzel
71c35eaf4c New translations strings.properties (Greek)
[ci skip]
2019-10-03 15:44:58 +02:00
Sebastian Stenzel
70790d0e6a New translations strings.properties (Japanese)
[ci skip]
2019-10-03 14:08:44 +02:00
Sebastian Stenzel
8fbc00a417 New translations strings.properties (Japanese)
[ci skip]
2019-10-03 13:41:10 +02:00
Sebastian Stenzel
080bf7f540 New translations strings.properties (Japanese)
[ci skip]
2019-10-03 13:08:50 +02:00
Sebastian Stenzel
9d988294f3 New translations strings.properties (Czech)
[ci skip]
2019-10-03 12:30:27 +02:00
Sebastian Stenzel
a712013507 New translations strings.properties (Czech)
[ci skip]
2019-10-03 12:00:54 +02:00
Sebastian Stenzel
0aab22975a New translations strings.properties (Czech)
[ci skip]
2019-10-03 11:25:33 +02:00
Sebastian Stenzel
fde6555e4d New translations strings.properties (Czech)
[ci skip]
2019-10-03 10:55:07 +02:00
Sebastian Stenzel
748e76e187 New translations strings.properties (Czech)
[ci skip]
2019-10-03 10:24:17 +02:00
Sebastian Stenzel
55a627f718 New translations strings.properties (Czech)
[ci skip]
2019-10-03 08:58:08 +02:00
Sebastian Stenzel
a19cbff12f New translations strings.properties (Czech)
[ci skip]
2019-10-03 08:27:39 +02:00
Sebastian Stenzel
d5e098a6cd New translations strings.properties (Czech)
[ci skip]
2019-10-03 08:00:05 +02:00
Sebastian Stenzel
3f9fc28e3c New translations strings.properties (Spanish)
[ci skip]
2019-10-02 17:59:29 +02:00
Sebastian Stenzel
0bf4081d71 New translations strings.properties (Spanish)
[ci skip]
2019-10-02 17:29:19 +02:00
Sebastian Stenzel
de3e392aa3 New translations strings.properties (Czech)
[ci skip]
2019-10-02 16:48:00 +02:00
Sebastian Stenzel
44a5f51c93 New translations strings.properties (Korean)
[ci skip]
2019-10-02 14:34:11 +02:00
Sebastian Stenzel
9c8ea3c2e5 New translations strings.properties (Croatian)
[ci skip]
2019-10-02 14:34:10 +02:00
Sebastian Stenzel
4208d1e036 New translations strings.properties (Portuguese)
[ci skip]
2019-10-02 14:34:08 +02:00
Sebastian Stenzel
154198fd5c New translations strings.properties (Turkish)
[ci skip]
2019-10-02 14:34:06 +02:00
Sebastian Stenzel
0ad0d79a54 New translations strings.properties (Dutch)
[ci skip]
2019-10-02 14:34:05 +02:00
Sebastian Stenzel
966538e47b New translations strings.properties (French)
[ci skip]
2019-10-02 14:34:03 +02:00
Sebastian Stenzel
181a1e7248 New translations strings.properties (Czech)
[ci skip]
2019-10-02 14:34:01 +02:00
Sebastian Stenzel
1a13e03a08 New translations strings.properties (Spanish)
[ci skip]
2019-10-02 14:33:59 +02:00
Sebastian Stenzel
0fa0568f24 New translations strings.properties (Russian)
[ci skip]
2019-10-02 14:33:58 +02:00
Sebastian Stenzel
d7e76bee6d New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-02 14:33:56 +02:00
Sebastian Stenzel
77dcf4da62 New translations strings.properties (Italian)
[ci skip]
2019-10-02 14:33:55 +02:00
Sebastian Stenzel
6eb0384fa6 New translations strings.properties (German)
[ci skip]
2019-10-02 14:33:54 +02:00
Armin Schrenk
5af5a6c5b0 enhancing fxml loading to add local specific stylesheets, for #754 2019-10-02 14:27:10 +02:00
Armin Schrenk
0e4c833be0 loaading dosis-bold font via @font-face in css 2019-10-02 13:39:38 +02:00
Sebastian Stenzel
a2e4a2c78a New translations strings.properties (Korean)
[ci skip]
2019-10-02 13:07:55 +02:00
Sebastian Stenzel
f4ab2b590f New translations strings.properties (Korean)
[ci skip]
2019-10-02 12:32:27 +02:00
Sebastian Stenzel
80b3d7ab6b New translations strings.properties (Korean)
[ci skip]
2019-10-02 10:54:40 +02:00
Sebastian Stenzel
b23fbd4507 New translations strings.properties (Korean)
[ci skip]
2019-10-02 10:29:20 +02:00
Sebastian Stenzel
5b56ff6d55 New translations strings.properties (Korean)
[ci skip]
2019-10-02 09:58:16 +02:00
Sebastian Stenzel
143b691c61 New translations strings.properties (Korean)
[ci skip]
2019-10-02 09:31:50 +02:00
Sebastian Stenzel
88e4c88a87 New translations strings.properties (Croatian)
[ci skip]
2019-10-01 22:37:58 +02:00
Sebastian Stenzel
c9e06ad2d2 New translations strings.properties (Turkish)
[ci skip]
2019-10-01 22:07:35 +02:00
Sebastian Stenzel
bd814f729d New translations strings.properties (Italian)
[ci skip]
2019-10-01 22:07:33 +02:00
Sebastian Stenzel
8c8b50b83a New translations strings.properties (Portuguese)
[ci skip]
2019-10-01 21:39:03 +02:00
Sebastian Stenzel
e9f4790b40 New translations strings.properties (Turkish)
[ci skip]
2019-10-01 21:39:01 +02:00
Sebastian Stenzel
ac1149b873 New translations strings.properties (Dutch)
[ci skip]
2019-10-01 21:39:00 +02:00
Sebastian Stenzel
51b6f828ed New translations strings.properties (Italian)
[ci skip]
2019-10-01 21:38:59 +02:00
Sebastian Stenzel
b97a99d4e2 New translations strings.properties (Turkish)
[ci skip]
2019-10-01 21:09:42 +02:00
Sebastian Stenzel
6ed661aa13 New translations strings.properties (Dutch)
[ci skip]
2019-10-01 20:44:03 +02:00
Sebastian Stenzel
f3d46ed767 New translations strings.properties (Italian)
[ci skip]
2019-10-01 20:44:02 +02:00
Sebastian Stenzel
3bde55c4df New translations strings.properties (Dutch)
[ci skip]
2019-10-01 20:08:57 +02:00
Sebastian Stenzel
dd591ec258 New translations strings.properties (Spanish)
[ci skip]
2019-10-01 20:08:56 +02:00
Sebastian Stenzel
4ce035b554 New translations strings.properties (French)
[ci skip]
2019-10-01 18:38:52 +02:00
Sebastian Stenzel
cdc451fdca New translations strings.properties (Czech)
[ci skip]
2019-10-01 18:38:50 +02:00
Sebastian Stenzel
0680d16708 New translations strings.properties (Italian)
[ci skip]
2019-10-01 18:09:13 +02:00
Sebastian Stenzel
1f1792522e New translations strings.properties (German)
[ci skip]
2019-10-01 18:09:12 +02:00
Sebastian Stenzel
db53a7a3dc New translations strings.properties (Russian)
[ci skip]
2019-10-01 17:18:13 +02:00
Sebastian Stenzel
9362196d4c New translations strings.properties (Spanish)
[ci skip]
2019-10-01 16:51:17 +02:00
Sebastian Stenzel
527d387640 New translations strings.properties (Russian)
[ci skip]
2019-10-01 16:51:16 +02:00
Sebastian Stenzel
5f9dda4bbb New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-01 16:51:14 +02:00
Sebastian Stenzel
6a991f3ab5 New translations strings.properties (Italian)
[ci skip]
2019-10-01 16:51:13 +02:00
Sebastian Stenzel
4c023f792c New translations strings.properties (German)
[ci skip]
2019-10-01 15:27:40 +02:00
Sebastian Stenzel
6b5f7d37ca more crowdin fine tuning
[ci skip]
2019-10-01 15:26:18 +02:00
Sebastian Stenzel
87158b1e7a Update Crowdin configuration file 2019-10-01 13:53:37 +02:00
Sebastian Stenzel
0b132b7d10 updaten OpenJFX to version 13 2019-10-01 13:04:44 +02:00
Sebastian Stenzel
cd4cb70896 show progress during vault migration (ugly prototype) 2019-09-27 21:44:24 +02:00
Sebastian Stenzel
633470b0d6 reduced visibility of scope annotation 2019-09-27 21:43:56 +02:00
Sebastian Stenzel
1930090044 Added first prototyp for recovery key generation 2019-09-27 21:43:42 +02:00
Sebastian Stenzel
ccefb3613e renamed file 2019-09-27 20:01:02 +02:00
Sebastian Stenzel
9e79350b9e added the strongest avenger 😜
[ci skip]
2019-09-27 10:56:44 +02:00
Sebastian Stenzel
9092c2325a Merge branch 'master' into develop 2019-09-19 11:23:58 +02:00
226 changed files with 12649 additions and 9600 deletions

5
.crowdin.yml Normal file
View File

@@ -0,0 +1,5 @@
commit_message: "[ci skip]"
escape_special_characters: 0
files:
- source: /main/ui/src/main/resources/i18n/strings.properties
translation: /main/ui/src/main/resources/i18n/strings_%two_letters_code%.properties

2
.github/FUNDING.yml vendored
View File

@@ -1,6 +1,6 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
github: [cryptomator] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username

4
.github/stale.yml vendored
View File

@@ -1,12 +1,14 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 90
daysUntilStale: 180
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 30
# Issues with these labels will never be considered stale
exemptLabels:
- type:security-issue # never close automatically
- type:feature-request # never close automatically
- state:awaiting-response # handled by different bot
- state:blocked
- state:confirmed
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Label to use when marking an issue as stale

28
.idea/compiler.xml generated
View File

@@ -7,26 +7,34 @@
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.22.1/dagger-compiler-2.22.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.22.1/dagger-2.22.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.26/dagger-compiler-2.26.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.26/dagger-2.26.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.22.1/dagger-producers-2.22.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/25.0-jre/guava-25.0-jre.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.3/checker-compat-qual-2.5.3.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.1.3/error_prone_annotations-2.1.3.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.26/dagger-producers-2.26.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/27.1-jre/guava-27.1-jre.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.1/jsr305-3.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-qual/2.5.2/checker-qual-2.5.2.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.2.0/error_prone_annotations-2.2.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.14/animal-sniffer-annotations-1.14.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.22.1/dagger-spi-2.22.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.17/animal-sniffer-annotations-1.17.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.3/checker-compat-qual-2.5.3.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.26/dagger-spi-2.26.jar" />
<entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/net/ltgt/gradle/incap/incap/0.2/incap-0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.3.50/kotlin-stdlib-1.3.50.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.50/kotlin-stdlib-common-1.3.50.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.1.0/kotlinx-metadata-jvm-0.1.0.jar" />
</processorPath>
<module name="keychain" />
<module name="launcher" />
<module name="commons" />
<module name="ui" />
<module name="launcher" />
</profile>
</annotationProcessing>
</component>

14
.idea/encodings.xml generated
View File

@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM">
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
<file url="file://$PROJECT_DIR$/main" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/buildkit" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/commons" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/keychain" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/launcher" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/ui" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/commons/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/keychain/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/keychain/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/launcher/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/ui/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/ui/src/main/resources" charset="UTF-8" />
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

View File

@@ -1,4 +1,4 @@
dist: xenial
dist: bionic
language: java
sudo: false
jdk:
@@ -17,24 +17,25 @@ addons:
- haveged
install:
- curl -o $HOME/.m2/settings.xml https://gist.githubusercontent.com/cryptobot/cf5fbd909c4782aaeeeb7c7f4a1a43da/raw/e60ee486e34ee0c79f89f947abe2c83b4290c6bb/settings.xml
- mvn -fmain/pom.xml clean install -DskipTests org.codehaus.mojo:versions-maven-plugin:help dependency:go-offline -Pcoverage,release # "clean install" needed until we can exclude artifacts currently in the reactor, see https://maven.apache.org/plugins/maven-dependency-plugin/go-offline-mojo.html#excludeReactor and https://issues.apache.org/jira/browse/MDEP-568
- mvn -fmain/pom.xml clean install -DskipTests -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN org.codehaus.mojo:versions-maven-plugin:help dependency:go-offline -Pcoverage,release # "clean install" needed until we can exclude artifacts currently in the reactor, see https://maven.apache.org/plugins/maven-dependency-plugin/go-offline-mojo.html#excludeReactor and https://issues.apache.org/jira/browse/MDEP-568
before_script:
- |
if [[ -n "$TRAVIS_TAG" ]]; then
mvn -fmain/pom.xml org.codehaus.mojo:versions-maven-plugin:set -DnewVersion=$TRAVIS_TAG
else
mvn -fmain/pom.xml org.codehaus.mojo:versions-maven-plugin:set -DnewVersion=SNAPSHOT-$(echo $TRAVIS_COMMIT | head -c7)
fi
script:
- mvn --update-snapshots -fmain/pom.xml clean test verify -Pcoverage
after_success:
- curl -o ~/codacy-coverage-reporter.jar https://oss.sonatype.org/service/local/repositories/releases/content/com/codacy/codacy-coverage-reporter/4.0.2/codacy-coverage-reporter-4.0.2-assembly.jar
- curl -o ~/codacy-coverage-reporter.jar https://oss.sonatype.org/service/local/repositories/releases/content/com/codacy/codacy-coverage-reporter/6.0.7/codacy-coverage-reporter-6.0.7-assembly.jar
- $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/commons/target/site/jacoco/jacoco.xml --partial
- $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/keychain/target/site/jacoco/jacoco.xml --partial
- $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/ui/target/site/jacoco/jacoco.xml --partial
- $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/launcher/target/site/jacoco/jacoco.xml --partial
- $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar final
before_deploy:
- |
if [[ -n "$TRAVIS_TAG" ]]; then
mvn -fmain/pom.xml org.codehaus.mojo:versions-maven-plugin:set -DnewVersion=$TRAVIS_TAG
elif [[ $TRAVIS_BRANCH == "develop" ]] && [[ $TRAVIS_PULL_REQUEST == "false" ]]; then
mvn -fmain/pom.xml org.codehaus.mojo:versions-maven-plugin:set -DnewVersion=SNAPSHOT-$(echo $TRAVIS_COMMIT | head -c7)
fi
- mvn -fmain/pom.xml clean package -Prelease -DskipTests
- mvn -fmain/pom.xml package -Prelease -DskipTests
- export TODAY=`date +'%Y-%m-%d'`; envsubst '$TRAVIS_TAG $TODAY' < .travis-deploy-release.tmpl.json > .travis-deploy-release.json
deploy:
- provider: bintray # SNAPSHOTS

View File

@@ -4,7 +4,7 @@
[![Known Vulnerabilities](https://snyk.io/test/github/cryptomator/cryptomator/badge.svg?targetFile=main%2Fpom.xml)](https://snyk.io/test/github/cryptomator/cryptomator?targetFile=main%2Fpom.xml)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/2a0adf3cec6a4143b91035d3924178f1)](https://www.codacy.com/app/cryptomator/cryptomator?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=cryptomator/cryptomator&amp;utm_campaign=Badge_Grade)
[![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator)
[![POEditor](https://img.shields.io/badge/POEditor-Help%20Translate-blue.svg?style=flat)](https://poeditor.com/join/project/bHwbvJmx0E)
[![Crowdin](https://badges.crowdin.net/cryptomator/localized.svg)](https://translate.cryptomator.org/)
[![Latest Release](https://img.shields.io/github/release/cryptomator/cryptomator.svg)](https://github.com/cryptomator/cryptomator/releases/latest)
[![Community](https://img.shields.io/badge/help-Community-orange.svg)](https://community.cryptomator.org)

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.0-alpha1</version>
<version>1.5.0-beta3</version>
</parent>
<artifactId>buildkit</artifactId>
<packaging>pom</packaging>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.0-alpha1</version>
<version>1.5.0-beta3</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator Commons</name>
@@ -48,6 +48,12 @@
<artifactId>easybind</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
</dependency>
<!-- Google -->
<dependency>
<groupId>com.google.guava</groupId>

View File

@@ -5,7 +5,6 @@
*******************************************************************************/
package org.cryptomator.common;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import javafx.beans.binding.Binding;
@@ -19,6 +18,8 @@ import org.cryptomator.common.vaults.VaultComponent;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Singleton;
@@ -27,13 +28,29 @@ import java.util.Comparator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@Module(subcomponents = {VaultComponent.class})
public abstract class CommonsModule {
private static final int NUM_SCHEDULER_THREADS = 4;
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
private static final int NUM_SCHEDULER_THREADS = 2;
private static final int NUM_CORE_BG_THREADS = 6;
private static final long BG_THREAD_KEEPALIVE_SECONDS = 60l;
@Provides
@Singleton
@Named("licensePublicKey")
static String provideLicensePublicKey() {
// in PEM format without the dash-escaped begin/end lines
return "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB7NfnqiZbg2KTmoflmZ71PbXru7oW" //
+ "fmnV2yv3eDjlDfGruBrqz9TtXBZV/eYWt31xu1osIqaT12lKBvZ511aaAkIBeOEV" //
+ "gwcBIlJr6kUw7NKzeJt7r2rrsOyQoOG2nWc/Of/NBqA3mIZRHk5Aq1YupFdD26QE" //
+ "r0DzRyj4ixPIt38CQB8=";
}
@Provides
@Singleton
@@ -56,21 +73,41 @@ public abstract class CommonsModule {
@Provides
@Singleton
static ScheduledExecutorService provideScheduledExecutorService(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) {
static ScheduledExecutorService provideScheduledExecutorService(ShutdownHook shutdownHook) {
final AtomicInteger threadNumber = new AtomicInteger(1);
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> {
String name = String.format("App Scheduled Executor %02d", threadNumber.getAndIncrement());
Thread t = new Thread(r);
t.setName("Background Thread " + threadNumber.getAndIncrement());
t.setName(name);
t.setUncaughtExceptionHandler(CommonsModule::handleUncaughtExceptionInBackgroundThread);
t.setDaemon(true);
LOG.debug("Starting {}", t.getName());
return t;
});
shutdownTaskScheduler.accept(executorService::shutdown);
shutdownHook.runOnShutdown(executorService::shutdown);
return executorService;
}
@Binds
@Provides
@Singleton
abstract ExecutorService bindExecutorService(ScheduledExecutorService executor);
static ExecutorService provideExecutorService(ShutdownHook shutdownHook) {
final AtomicInteger threadNumber = new AtomicInteger(1);
ExecutorService executorService = new ThreadPoolExecutor(NUM_CORE_BG_THREADS, Integer.MAX_VALUE, BG_THREAD_KEEPALIVE_SECONDS, TimeUnit.SECONDS, new SynchronousQueue<>(), r -> {
String name = String.format("App Background Thread %03d", threadNumber.getAndIncrement());
Thread t = new Thread(r);
t.setName(name);
t.setUncaughtExceptionHandler(CommonsModule::handleUncaughtExceptionInBackgroundThread);
t.setDaemon(true);
LOG.debug("Starting {}", t.getName());
return t;
});
shutdownHook.runOnShutdown(executorService::shutdown);
return executorService;
}
private static void handleUncaughtExceptionInBackgroundThread(Thread thread, Throwable throwable) {
LOG.error("Uncaught exception in " + thread.getName(), throwable);
}
@Provides
@Singleton

View File

@@ -25,6 +25,7 @@ public class Environment {
private static final Path RELATIVE_HOME_DIR = Paths.get("~");
private static final Path ABSOLUTE_HOME_DIR = Paths.get(USER_HOME);
private static final char PATH_LIST_SEP = ':';
private static final int DEFAULT_MIN_PW_LENGTH = 8;
@Inject
public Environment() {
@@ -37,6 +38,7 @@ public class Environment {
LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath"));
LOG.debug("cryptomator.logDir: {}", System.getProperty("cryptomator.logDir"));
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
}
public boolean useCustomLogbackConfig() {
@@ -63,6 +65,19 @@ public class Environment {
return getPath("cryptomator.mountPointsDir").map(this::replaceHomeDir);
}
public int getMinPwLength() {
return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH);
}
private int getInt(String propertyName, int defaultValue) {
String value = System.getProperty(propertyName);
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) { // includes "null" values
return defaultValue;
}
}
private Optional<Path> getPath(String propertyName) {
String value = System.getProperty(propertyName);
return Optional.ofNullable(value).map(Paths::get);

View File

@@ -0,0 +1,56 @@
package org.cryptomator.common;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.google.common.io.BaseEncoding;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Optional;
@Singleton
class LicenseChecker {
private final JWTVerifier verifier;
@Inject
public LicenseChecker(@Named("licensePublicKey") String pemEncodedPublicKey) {
Algorithm algorithm = Algorithm.ECDSA512(decodePublicKey(pemEncodedPublicKey), null);
this.verifier = JWT.require(algorithm).build();
}
private static ECPublicKey decodePublicKey(String pemEncodedPublicKey) {
try {
byte[] keyBytes = BaseEncoding.base64().decode(pemEncodedPublicKey);
PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes));
if (key instanceof ECPublicKey) {
return (ECPublicKey) key;
} else {
throw new IllegalStateException("Key not an EC public key.");
}
} catch (InvalidKeySpecException e) {
throw new IllegalArgumentException("Invalid license public key", e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
public Optional<DecodedJWT> check(String licenseKey) {
try {
return Optional.of(verifier.verify(licenseKey));
} catch (JWTVerificationException exception) {
return Optional.empty();
}
}
}

View File

@@ -0,0 +1,79 @@
package org.cryptomator.common;
import com.auth0.jwt.interfaces.DecodedJWT;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.cryptomator.common.settings.Settings;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class LicenseHolder {
private final Settings settings;
private final LicenseChecker licenseChecker;
private final ObjectProperty<DecodedJWT> validJwtClaims;
private final StringBinding licenseSubject;
private final BooleanBinding validLicenseProperty;
@Inject
public LicenseHolder(LicenseChecker licenseChecker, Settings settings) {
this.settings = settings;
this.licenseChecker = licenseChecker;
this.validJwtClaims = new SimpleObjectProperty<>();
this.licenseSubject = Bindings.createStringBinding(this::getLicenseSubject, validJwtClaims);
this.validLicenseProperty = validJwtClaims.isNotNull();
Optional<DecodedJWT> claims = licenseChecker.check(settings.licenseKey().get());
validJwtClaims.set(claims.orElse(null));
}
public boolean validateAndStoreLicense(String licenseKey) {
Optional<DecodedJWT> claims = licenseChecker.check(licenseKey);
validJwtClaims.set(claims.orElse(null));
if (claims.isPresent()) {
settings.licenseKey().set(licenseKey);
return true;
} else {
return false;
}
}
/* Observable Properties */
public Optional<String> getLicenseKey() {
DecodedJWT claims = validJwtClaims.get();
if (claims != null) {
return Optional.of(claims.getToken());
} else {
return Optional.empty();
}
}
public StringBinding licenseSubjectProperty() {
return licenseSubject;
}
public String getLicenseSubject() {
DecodedJWT claims = validJwtClaims.get();
if (claims != null) {
return claims.getSubject();
} else {
return null;
}
}
public BooleanBinding validLicenseProperty() {
return validLicenseProperty;
}
public boolean isValidLicense() {
return validLicenseProperty.get();
}
}

View File

@@ -1,28 +0,0 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common;
import java.util.Optional;
import java.util.function.Function;
public final class Optionals {
private Optionals() {
}
/**
* Returns a function that is equivalent to the input function but immediately gets the value of the returned optional when invoked.
*
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
* @param function An {@code Optional}-bearing input function {@code Function<Foo, Optional<Bar>>}
* @return A {@code Function<Foo, Bar>}, that may throw a NoSuchElementException, if the original function returns an empty optional.
*/
public static <T, R> Function<T, R> unwrap(Function<T, Optional<R>> function) {
return t -> function.apply(t).get();
}
}

View File

@@ -0,0 +1,96 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common;
import com.google.common.util.concurrent.Runnables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Queue;
import java.util.concurrent.PriorityBlockingQueue;
@Singleton
public class ShutdownHook extends Thread {
private static final int PRIO_VERY_LAST = Integer.MIN_VALUE;
public static final int PRIO_LAST = PRIO_VERY_LAST + 1;
public static final int PRIO_DEFAULT = 0;
public static final int PRIO_FIRST = Integer.MAX_VALUE;
private static final Logger LOG = LoggerFactory.getLogger(ShutdownHook.class);
private static final OrderedTask POISON = new OrderedTask(PRIO_VERY_LAST, Runnables.doNothing());
private final Queue<OrderedTask> tasks = new PriorityBlockingQueue<>();
@Inject
ShutdownHook() {
super(null, null, "ShutdownTasks", 0);
Runtime.getRuntime().addShutdownHook(this);
LOG.debug("Registered shutdown hook.");
}
@Override
public void run() {
LOG.debug("Running graceful shutdown tasks...");
tasks.add(POISON);
Runnable task;
while ((task = tasks.remove()) != POISON) {
try {
task.run();
} catch (RuntimeException e) {
LOG.error("Exception while shutting down.", e);
}
}
}
/**
* Schedules a task to be run during shutdown with default order
*
* @param task The task to be scheduled
*/
public void runOnShutdown(Runnable task) {
runOnShutdown(PRIO_DEFAULT, task);
}
/**
* Schedules a task to be run with the given priority
*
* @param priority Tasks with high priority will be run before task with lower priority
* @param task The task to be scheduled
*/
public void runOnShutdown(int priority, Runnable task) {
tasks.add(new OrderedTask(priority, task));
}
private static class OrderedTask implements Comparable<OrderedTask>, Runnable {
private final int priority;
private final Runnable task;
public OrderedTask(int priority, Runnable task) {
this.priority = priority;
this.task = task;
}
@Override
public int compareTo(OrderedTask other) {
// overflow-safe signum impl:
if (this.priority > other.priority) {
return -1; // higher prio -> this before other
} else if (this.priority < other.priority) {
return +1; // lower prio -> other before this
} else {
return 0; // same prio
}
}
@Override
public void run() {
task.run();
}
}
}

View File

@@ -15,8 +15,11 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.NodeOrientation;
import java.util.function.Consumer;
@@ -30,9 +33,11 @@ public class Settings {
public static final int DEFAULT_PORT = 42427;
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
public static final WebDavUrlScheme DEFAULT_GVFS_SCHEME = WebDavUrlScheme.DAV;
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
public static final boolean DEFAULT_DEBUG_MODE = false;
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT;
private static final String DEFAULT_LICENSE_KEY = "";
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
@@ -44,6 +49,8 @@ public class Settings {
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
private final ObjectProperty<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY);
private Consumer<Settings> saveCmd;
@@ -61,6 +68,8 @@ public class Settings {
debugMode.addListener(this::somethingChanged);
preferredVolumeImpl.addListener(this::somethingChanged);
theme.addListener(this::somethingChanged);
userInterfaceOrientation.addListener(this::somethingChanged);
licenseKey.addListener(this::somethingChanged);
}
void setSaveCmd(Consumer<Settings> saveCmd) {
@@ -118,4 +127,12 @@ public class Settings {
public ObjectProperty<UiTheme> theme() {
return theme;
}
public ObjectProperty<NodeOrientation> userInterfaceOrientation() {
return userInterfaceOrientation;
}
public StringProperty licenseKey() {
return licenseKey;
}
}

View File

@@ -9,6 +9,7 @@ import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import javafx.geometry.NodeOrientation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,6 +37,8 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
out.name("debugMode").value(value.debugMode().get());
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
out.name("theme").value(value.theme().get().name());
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
out.name("licenseKey").value(value.licenseKey().get());
out.endObject();
}
@@ -85,6 +88,12 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "theme":
settings.theme().set(parseUiTheme(in.nextString()));
break;
case "uiOrientation":
settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
break;
case "licenseKey":
settings.licenseKey().set(in.nextString());
break;
default:
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
@@ -109,7 +118,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
try {
return WebDavUrlScheme.valueOf(webDavUrlSchemeName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid volume type {}. Defaulting to {}.", webDavUrlSchemeName, Settings.DEFAULT_GVFS_SCHEME);
LOG.warn("Invalid WebDAV url scheme {}. Defaulting to {}.", webDavUrlSchemeName, Settings.DEFAULT_GVFS_SCHEME);
return Settings.DEFAULT_GVFS_SCHEME;
}
}
@@ -118,11 +127,20 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
try {
return UiTheme.valueOf(uiThemeName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid volume type {}. Defaulting to {}.", uiThemeName, Settings.DEFAULT_THEME);
LOG.warn("Invalid ui theme {}. Defaulting to {}.", uiThemeName, Settings.DEFAULT_THEME);
return Settings.DEFAULT_THEME;
}
}
private NodeOrientation parseUiOrientation(String uiOrientationName) {
try {
return NodeOrientation.valueOf(uiOrientationName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid ui orientation {}. Defaulting to {}.", uiOrientationName, Settings.DEFAULT_USER_INTERFACE_ORIENTATION);
return Settings.DEFAULT_USER_INTERFACE_ORIENTATION;
}
}
private List<VaultSettings> readVaultSettingsArray(JsonReader in) throws IOException {
List<VaultSettings> result = new ArrayList<>();
in.beginArray();

View File

@@ -32,7 +32,6 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -46,16 +45,17 @@ public class SettingsProvider implements Supplier<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
private static final long SAVE_DELAY_MS = 1000;
private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
private final AtomicReference<Settings> settings = new AtomicReference<>();
private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter();
private final Environment env;
private final ScheduledExecutorService scheduler;
private final Gson gson;
@Inject
public SettingsProvider(Environment env) {
public SettingsProvider(Environment env, ScheduledExecutorService scheduler) {
this.env = env;
this.scheduler = scheduler;
this.gson = new GsonBuilder() //
.setPrettyPrinting().setLenient().disableHtmlEscaping() //
.registerTypeAdapter(Settings.class, settingsJsonAdapter) //
@@ -98,7 +98,7 @@ public class SettingsProvider implements Supplier<Settings> {
final Optional<Path> settingsPath = env.getSettingsPath().findFirst(); // alway save to preferred (first) path
settingsPath.ifPresent(path -> {
Runnable saveCommand = () -> this.save(settings, path);
ScheduledFuture<?> scheduledTask = saveScheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
ScheduledFuture<?> scheduledTask = scheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
if (previouslyScheduledTask != null) {
previouslyScheduledTask.cancel(false);

View File

@@ -69,8 +69,9 @@ public class DokanyVolume implements Volume {
} else {
//auto assign drive letter
if (!windowsDriveLetters.getAvailableDriveLetters().isEmpty()) {
return windowsDriveLetters.getAvailableDriveLetters().iterator().next();
return Path.of(windowsDriveLetters.getAvailableDriveLetters().iterator().next() + ":\\");
} else {
//TODO: Error Handling
throw new VolumeException("No free drive letter available.");
}
}

View File

@@ -18,6 +18,7 @@ import org.cryptomator.cryptofs.migration.Migrators;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Collection;
@@ -67,7 +68,13 @@ public class VaultListManager {
}
private Optional<Vault> get(Path vaultPath) {
return vaultList.stream().filter(v -> v.getPath().equals(vaultPath)).findAny();
return vaultList.stream().filter(v -> {
try {
return Files.isSameFile(vaultPath, v.getPath());
} catch (IOException e) {
return false;
}
}).findAny();
}
private Vault create(VaultSettings vaultSettings) {
@@ -76,7 +83,7 @@ public class VaultListManager {
return comp.vault();
}
private VaultState determineVaultState(Path pathToVault) {
public static VaultState determineVaultState(Path pathToVault) {
try {
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
return VaultState.MISSING;

View File

@@ -5,6 +5,7 @@
*******************************************************************************/
package org.cryptomator.common.vaults;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -14,7 +15,6 @@ import javax.inject.Singleton;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;
@@ -23,11 +23,11 @@ import java.util.stream.StreamSupport;
public final class WindowsDriveLetters {
private static final Logger LOG = LoggerFactory.getLogger(WindowsDriveLetters.class);
private static final Set<Path> D_TO_Z;
private static final Set<String> A_TO_Z;
static {
try (IntStream stream = IntStream.rangeClosed('D', 'Z')) {
D_TO_Z = stream.mapToObj(i -> Path.of(((char) i)+":\\")).collect(Collectors.toSet());
try (IntStream stream = IntStream.rangeClosed('A', 'Z')) {
A_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(Collectors.toSet());
}
}
@@ -35,20 +35,21 @@ public final class WindowsDriveLetters {
public WindowsDriveLetters() {
}
public Set<Path> getOccupiedDriveLetters() {
public Set<String> getAllDriveLetters() {
return A_TO_Z;
}
public Set<String> getOccupiedDriveLetters() {
if (!SystemUtils.IS_OS_WINDOWS) {
LOG.warn("Attempted to get occupied drive letters on non-Windows machine.");
return Set.of();
} else {
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
return StreamSupport.stream(rootDirs.spliterator(), false).collect(Collectors.toSet());
return StreamSupport.stream(rootDirs.spliterator(), false).map(p -> p.toString().substring(0,1)).collect(Collectors.toSet());
}
}
public Set<Path> getAvailableDriveLetters() {
Set<Path> occupiedDriveLetters = getOccupiedDriveLetters();
Predicate<Path> isOccupiedDriveLetter = occupiedDriveLetters::contains;
return D_TO_Z.stream().filter(isOccupiedDriveLetter.negate()).collect(Collectors.toSet());
public Set<String> getAvailableDriveLetters() {
return Sets.difference(A_TO_Z, getOccupiedDriveLetters());
}
}

View File

@@ -0,0 +1,62 @@
package org.cryptomator.common;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.cryptomator.common.LicenseChecker;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Optional;
class LicenseCheckerTest {
private static final String PUBLIC_KEY = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ" //
+ "PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47" //
+ "6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM" //
+ "Al8G7CqwoJOsW7Kddns=";
private LicenseChecker licenseChecker;
@BeforeEach
public void setup() {
licenseChecker = new LicenseChecker(PUBLIC_KEY);
}
@Test
public void testCheckValidLicense() {
String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
Optional<DecodedJWT> decoded = licenseChecker.check(license);
Assertions.assertTrue(decoded.isPresent());
Assertions.assertEquals("cryptobot@example.com", decoded.get().getSubject());
}
@Test
public void testCheckInvalidLicenseHeader() {
String license = "EyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
Optional<DecodedJWT> decoded = licenseChecker.check(license);
Assertions.assertFalse(decoded.isPresent());
}
@Test
public void testCheckInvalidLicensePayload() {
String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.EyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
Optional<DecodedJWT> decoded = licenseChecker.check(license);
Assertions.assertFalse(decoded.isPresent());
}
@Test
public void testCheckInvalidLicenseSignature() {
String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.aQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
Optional<DecodedJWT> decoded = licenseChecker.check(license);
Assertions.assertFalse(decoded.isPresent());
}
}

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.0-alpha1</version>
<version>1.5.0-beta3</version>
</parent>
<artifactId>keychain</artifactId>
<name>System Keychain Access</name>

View File

@@ -11,6 +11,7 @@ import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;
import org.cryptomator.common.JniModule;
import javax.inject.Singleton;
import java.util.Optional;
import java.util.Set;
@@ -24,6 +25,7 @@ public class KeychainModule {
}
@Provides
@Singleton
public Optional<KeychainAccess> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst();
}

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.0-alpha1</version>
<version>1.5.0-beta3</version>
</parent>
<artifactId>launcher</artifactId>
<name>Cryptomator Launcher</name>

View File

@@ -1,48 +0,0 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.launcher;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
class CleanShutdownPerformer extends Thread {
private static final Logger LOG = LoggerFactory.getLogger(CleanShutdownPerformer.class);
private final ConcurrentMap<Runnable, Boolean> tasks = new ConcurrentHashMap<>();
@Inject
CleanShutdownPerformer() {
super(null, null, "ShutdownTasks", 0);
}
@Override
public void run() {
LOG.debug("Running graceful shutdown tasks...");
tasks.keySet().forEach(r -> {
try {
r.run();
} catch (RuntimeException e) {
LOG.error("Exception while shutting down.", e);
}
});
tasks.clear();
}
void scheduleShutdownTask(Runnable task) {
tasks.put(task, Boolean.TRUE);
}
void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(this);
}
}

View File

@@ -32,17 +32,15 @@ public class Cryptomator {
private final IpcFactory ipcFactory;
private final Optional<String> applicationVersion;
private final CountDownLatch shutdownLatch;
private final CleanShutdownPerformer shutdownPerformer;
private final UiLauncher uiLauncher;
@Inject
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, CleanShutdownPerformer shutdownPerformer, UiLauncher uiLauncher) {
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, UiLauncher uiLauncher) {
this.logConfig = logConfig;
this.debugMode = debugMode;
this.ipcFactory = ipcFactory;
this.applicationVersion = applicationVersion;
this.shutdownLatch = shutdownLatch;
this.shutdownPerformer = shutdownPerformer;
this.uiLauncher = uiLauncher;
}
@@ -90,7 +88,6 @@ public class Cryptomator {
*/
private int runGuiApplication() {
try {
shutdownPerformer.registerShutdownHook();
uiLauncher.launch();
shutdownLatch.await();
LOG.info("UI shut down");

View File

@@ -7,18 +7,10 @@ import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
@Module
class CryptomatorModule {
@Provides
@Singleton
@Named("shutdownTaskScheduler")
Consumer<Runnable> provideShutdownTaskScheduler(CleanShutdownPerformer shutdownPerformer) {
return shutdownPerformer::scheduleShutdownTask;
}
@Provides
@Singleton
@Named("shutdownLatch")

View File

@@ -21,8 +21,10 @@ import java.nio.file.FileSystems;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Singleton
@@ -40,7 +42,7 @@ class FileOpenRequestHandler {
}
private void openFiles(OpenFilesEvent evt) {
Stream<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath);
Collection<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath).collect(Collectors.toList());
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}
@@ -51,16 +53,18 @@ class FileOpenRequestHandler {
// visible for testing
void handleLaunchArgs(FileSystem fs, String[] args) {
Stream<Path> pathsToOpen = Arrays.stream(args).map(str -> {
Collection<Path> pathsToOpen = Arrays.stream(args).map(str -> {
try {
return fs.getPath(str);
} catch (InvalidPathException e) {
LOG.trace("Argument not a valid path: {}", str);
return null;
}
}).filter(Objects::nonNull);
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}).filter(Objects::nonNull).collect(Collectors.toList());
if (!pathsToOpen.isEmpty()) {
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}
}

View File

@@ -8,6 +8,7 @@ import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Stream;
@@ -27,7 +28,7 @@ class IpcProtocolImpl implements IpcProtocol {
@Override
public void revealRunningApp() {
launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Stream.empty()));
launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Collections.emptyList()));
}
@Override

View File

@@ -5,9 +5,8 @@ import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.hook.DelayingShutdownHook;
import ch.qos.logback.core.util.Duration;
import org.cryptomator.common.Environment;
import org.cryptomator.common.ShutdownHook;
import javax.inject.Inject;
import javax.inject.Named;
@@ -16,26 +15,27 @@ import java.util.Map;
@Singleton
public class LoggerConfiguration {
private static final double SHUTDOWN_DELAY_MS = 100;
private final LoggerContext context;
private final Environment environment;
private final Appender<ILoggingEvent> stdout;
private final Appender<ILoggingEvent> upgrade;
private final Appender<ILoggingEvent> file;
private final ShutdownHook shutdownHook;
@Inject
LoggerConfiguration(LoggerContext context, //
Environment environment, //
@Named("stdoutAppender") Appender<ILoggingEvent> stdout, //
@Named("upgradeAppender") Appender<ILoggingEvent> upgrade, //
@Named("fileAppender") Appender<ILoggingEvent> file) {
@Named("fileAppender") Appender<ILoggingEvent> file, //
ShutdownHook shutdownHook) {
this.context = context;
this.environment = environment;
this.stdout = stdout;
this.upgrade = upgrade;
this.file = file;
this.shutdownHook = shutdownHook;
}
public void init() {
@@ -55,16 +55,15 @@ public class LoggerConfiguration {
}
// configure upgrade logger:
Logger upgrades = context.getLogger("org.cryptomator.ui.model.upgrade");
Logger upgrades = context.getLogger("org.cryptomator.cryptofs.migration");
upgrades.setLevel(Level.DEBUG);
upgrades.addAppender(stdout);
upgrades.addAppender(upgrade);
upgrades.addAppender(file);
upgrades.setAdditive(false);
// add shutdown hook
DelayingShutdownHook shutdownHook = new DelayingShutdownHook();
shutdownHook.setContext(context);
shutdownHook.setDelay(Duration.buildByMilliseconds(SHUTDOWN_DELAY_MS));
shutdownHook.runOnShutdown(ShutdownHook.PRIO_LAST, context::stop);
}
}

View File

@@ -15,16 +15,14 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class FileOpenRequestHandlerTest {
@@ -39,32 +37,30 @@ public class FileOpenRequestHandlerTest {
@Test
@DisplayName("./cryptomator.exe foo bar")
public void testOpenArgsWithCorrectPaths() throws IOException {
public void testOpenArgsWithCorrectPaths() {
inTest.handleLaunchArgs(new String[]{"foo", "bar"});
AppLaunchEvent evt = queue.poll();
Assertions.assertNotNull(evt);
List<Path> paths = evt.getPathsToOpen().collect(Collectors.toList());
Collection<Path> paths = evt.getPathsToOpen();
MatcherAssert.assertThat(paths, CoreMatchers.hasItems(Paths.get("foo"), Paths.get("bar")));
}
@Test
@DisplayName("./cryptomator.exe foo (with 'foo' being an invalid path)")
public void testOpenArgsWithIncorrectPaths() throws IOException {
public void testOpenArgsWithIncorrectPaths() {
FileSystem fs = Mockito.mock(FileSystem.class);
Mockito.when(fs.getPath("foo")).thenThrow(new InvalidPathException("foo", "foo is not a path"));
inTest.handleLaunchArgs(fs, new String[]{"foo"});
AppLaunchEvent evt = queue.poll();
Assertions.assertNotNull(evt);
List<Path> paths = evt.getPathsToOpen().collect(Collectors.toList());
Assertions.assertTrue(paths.isEmpty());
Assertions.assertNull(evt);
}
@Test
@DisplayName("./cryptomator.exe foo (with full event queue)")
public void testOpenArgsWithFullQueue() throws IOException {
queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Stream.empty()));
public void testOpenArgsWithFullQueue() {
queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Collections.emptyList()));
Assumptions.assumeTrue(queue.remainingCapacity() == 0);
inTest.handleLaunchArgs(new String[]{"foo"});

View File

@@ -3,13 +3,13 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.0-alpha1</version>
<version>1.5.0-beta3</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
<organization>
<name>cryptomator.org</name>
<url>http://cryptomator.org</url>
<url>https://cryptomator.org</url>
</organization>
<developers>
@@ -23,37 +23,34 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- dependency versions -->
<cryptomator.cryptolib.version>1.2.2</cryptomator.cryptolib.version>
<cryptomator.cryptofs.version>1.9.0-beta1</cryptomator.cryptofs.version>
<cryptomator.jni.version>2.2.1</cryptomator.jni.version>
<cryptomator.fuse.version>1.2.0</cryptomator.fuse.version>
<cryptomator.dokany.version>1.1.11</cryptomator.dokany.version>
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>1.9.3</cryptomator.cryptofs.version>
<cryptomator.jni.version>2.2.2</cryptomator.jni.version>
<cryptomator.fuse.version>1.2.2</cryptomator.fuse.version>
<cryptomator.dokany.version>1.1.12</cryptomator.dokany.version>
<cryptomator.webdav.version>1.0.10</cryptomator.webdav.version>
<javafx.version>12</javafx.version>
<commons-io.version>2.6</commons-io.version>
<commons-lang3.version>3.8.1</commons-lang3.version>
<!-- 3rd party dependencies -->
<javafx.version>14-ea+8</javafx.version>
<commons-lang3.version>3.9</commons-lang3.version>
<jwt.version>3.8.3</jwt.version>
<easybind.version>1.0.3</easybind.version>
<guava.version>27.1-jre</guava.version>
<dagger.version>2.22.1</dagger.version>
<gson.version>2.8.5</gson.version>
<slf4j.version>1.7.26</slf4j.version>
<guava.version>28.1-jre</guava.version>
<dagger.version>2.26</dagger.version>
<gson.version>2.8.6</gson.version>
<slf4j.version>1.7.29</slf4j.version>
<logback.version>1.2.3</logback.version>
<junit.jupiter.version>5.4.2</junit.jupiter.version>
<mockito.version>2.27.0</mockito.version>
<hamcrest.version>2.1</hamcrest.version>
<!-- test dependencies -->
<junit.jupiter.version>5.6.0</junit.jupiter.version>
<mockito.version>3.2.4</mockito.version>
<hamcrest.version>2.2</hamcrest.version>
</properties>
<repositories>
<repository>
<id>jcenter</id>
<url>http://jcenter.bintray.com</url>
<url>https://jcenter.bintray.com</url>
</repository>
</repositories>
@@ -82,11 +79,6 @@
</dependency>
<!-- Cryptomator Libs -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
<version>${cryptomator.cryptolib.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
@@ -158,16 +150,18 @@
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!-- EasyBind -->
<dependency>
@@ -273,7 +267,7 @@
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.0</version>
<version>3.1.1</version>
<executions>
<execution>
<id>copy-libs</id>
@@ -302,7 +296,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
<version>0.8.5</version>
<executions>
<execution>
<id>prepare-agent</id>
@@ -329,7 +323,7 @@
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<version>3.8.1</version>
<configuration>
<release>11</release>
<annotationProcessorPaths>
@@ -344,7 +338,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
<version>2.22.2</version>
</plugin>
</plugins>
</build>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.0-alpha1</version>
<version>1.5.0-beta3</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
@@ -22,10 +22,6 @@
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
</dependency>
<!-- JavaFx -->
<dependency>
@@ -41,7 +37,7 @@
<dependency>
<groupId>org.fxmisc.easybind</groupId>
<artifactId>easybind</artifactId>
</dependency>
</dependency>
<!-- Google -->
<dependency>
@@ -54,10 +50,6 @@
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
@@ -73,7 +65,7 @@
<dependency>
<groupId>com.nulab-inc</groupId>
<artifactId>zxcvbn</artifactId>
<version>1.2.7</version>
<version>1.3.0</version>
</dependency>
<!-- Logging -->

View File

@@ -0,0 +1,51 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
import java.nio.file.Path;
@AddVaultWizardScoped
public class AddVaultFailureExistingController implements FxController {
private final Stage window;
private final Lazy<Scene> previousScene;
private final StringBinding vaultName;
@Inject
AddVaultFailureExistingController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_EXISTING) Lazy<Scene> previousScene, ObjectProperty<Path> pathOfFailedVault){
this.window = window;
this.previousScene = previousScene;
this.vaultName = Bindings.createStringBinding(() -> pathOfFailedVault.get().getFileName().toString(),pathOfFailedVault);
}
@FXML
public void close(){
window.close();
}
@FXML
public void back(){
window.setScene(previousScene.get());
}
// Getter & Setter
public StringBinding vaultNameProperty(){
return vaultName;
}
public String getVaultName(){
return vaultName.get();
}
}

View File

@@ -1,62 +1,63 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Binds;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.IntoSet;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.recoverykey.RecoveryKeyDisplayController;
import javax.inject.Named;
import javax.inject.Provider;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
@Module
public abstract class AddVaultModule {
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, resourceBundle);
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon, @AddVaultWizardWindow Lazy<Map<KeyCodeCombination, Runnable>> accelerators) {
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("addvaultwizard.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.sceneProperty().addListener(observable -> {
stage.getScene().getAccelerators().putAll(accelerators.get());
});
windowIcon.ifPresent(stage.getIcons()::add);
stage.getIcons().addAll(windowIcons);
return stage;
}
@@ -67,6 +68,7 @@ public abstract class AddVaultModule {
}
@Provides
@Named("vaultName")
@AddVaultWizardScoped
static StringProperty provideVaultName() {
return new SimpleStringProperty("");
@@ -79,24 +81,11 @@ public abstract class AddVaultModule {
return new SimpleObjectProperty<>();
}
// ------------------
@Provides
@AddVaultWizardWindow
@Named("recoveryKey")
@AddVaultWizardScoped
static Map<KeyCodeCombination, Runnable> provideDefaultAccellerators(@AddVaultWizardWindow Set<Map.Entry<KeyCombination, Runnable>> accelerators) {
return Map.ofEntries(accelerators.toArray(Map.Entry[]::new));
}
@Provides
@IntoSet
@AddVaultWizardWindow
static Map.Entry<KeyCombination, Runnable> provideCloseWindowShortcut(@AddVaultWizardWindow Stage window) {
if (SystemUtils.IS_OS_WINDOWS) {
return Map.entry(new KeyCodeCombination(KeyCode.F4, KeyCombination.ALT_DOWN), window::close);
} else {
return Map.entry(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN), window::close);
}
static StringProperty provideRecoveryKey() {
return new SimpleStringProperty();
}
// ------------------
@@ -104,48 +93,57 @@ public abstract class AddVaultModule {
@Provides
@FxmlScene(FxmlFile.ADDVAULT_WELCOME)
@AddVaultWizardScoped
static Scene provideWelcomeScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
Scene scene = fxmlLoaders.createScene("/fxml/addvault_welcome.fxml");
KeyCombination cmdW = new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN);
scene.getAccelerators().put(cmdW, window::close);
return scene;
static Scene provideWelcomeScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_WELCOME.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_EXISTING)
@AddVaultWizardScoped
static Scene provideChooseExistingVaultScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
return fxmlLoaders.createScene("/fxml/addvault_existing.fxml");
static Scene provideChooseExistingVaultScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_EXISTING.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_EXISTING_ERROR)
@AddVaultWizardScoped
static Scene provideChooseExistingVaultErrorScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_EXISTING_ERROR.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_NAME)
@AddVaultWizardScoped
static Scene provideCreateNewVaultNameScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
return fxmlLoaders.createScene("/fxml/addvault_new_name.fxml");
static Scene provideCreateNewVaultNameScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_NAME.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION)
@AddVaultWizardScoped
static Scene provideCreateNewVaultLocationScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
return fxmlLoaders.createScene("/fxml/addvault_new_location.fxml");
static Scene provideCreateNewVaultLocationScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_LOCATION.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD)
@AddVaultWizardScoped
static Scene provideCreateNewVaultPasswordScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
return fxmlLoaders.createScene("/fxml/addvault_new_password.fxml");
static Scene provideCreateNewVaultPasswordScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_PASSWORD.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY)
@AddVaultWizardScoped
static Scene provideCreateNewVaultRecoveryKeyScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_SUCCESS)
@AddVaultWizardScoped
static Scene provideCreateNewVaultSuccessScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
return fxmlLoaders.createScene("/fxml/addvault_success.fxml");
static Scene provideCreateNewVaultSuccessScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_SUCCESS.getRessourcePathString());
}
// ------------------
@@ -160,6 +158,11 @@ public abstract class AddVaultModule {
@FxControllerKey(ChooseExistingVaultController.class)
abstract FxController bindChooseExistingVaultController(ChooseExistingVaultController controller);
@Binds
@IntoMap
@FxControllerKey(AddVaultFailureExistingController.class)
abstract FxController bindAddVaultFailureExistingController(AddVaultFailureExistingController controller);
@Binds
@IntoMap
@FxControllerKey(CreateNewVaultNameController.class)
@@ -175,6 +178,25 @@ public abstract class AddVaultModule {
@FxControllerKey(CreateNewVaultPasswordController.class)
abstract FxController bindCreateNewVaultPasswordController(CreateNewVaultPasswordController controller);
@Provides
@IntoMap
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
}
@Binds
@IntoMap
@FxControllerKey(CreateNewVaultRecoveryKeyController.class)
abstract FxController bindCreateNewVaultRecoveryKeyController(CreateNewVaultRecoveryKeyController controller);
@Provides
@IntoMap
@FxControllerKey(RecoveryKeyDisplayController.class)
static FxController provideRecoveryKeyDisplayController(@AddVaultWizardWindow Stage window, @Named("vaultName") StringProperty vaultName, @Named("recoveryKey") StringProperty recoveryKey, ResourceBundle localization) {
return new RecoveryKeyDisplayController(window, vaultName.get(), recoveryKey.get(), localization);
}
@Binds
@IntoMap
@FxControllerKey(AddVaultSuccessController.class)

View File

@@ -28,16 +28,18 @@ public class ChooseExistingVaultController implements FxController {
private final Stage window;
private final Lazy<Scene> welcomeScene;
private final Lazy<Scene> successScene;
private final Lazy<Scene> errorScene;
private final ObjectProperty<Path> vaultPath;
private final ObjectProperty<Vault> vault;
private final VaultListManager vaultListManager;
private final ResourceBundle resourceBundle;
@Inject
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle) {
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.ADDVAULT_EXISTING_ERROR) Lazy<Scene> errorScene, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle) {
this.window = window;
this.welcomeScene = welcomeScene;
this.successScene = successScene;
this.errorScene = errorScene;
this.vaultPath = vaultPath;
this.vault = vault;
this.vaultListManager = vaultListManager;
@@ -51,20 +53,19 @@ public class ChooseExistingVaultController implements FxController {
@FXML
public void chooseFileAndNext() {
//TODO: error handling & cannot unlock added vault
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("addvaultwizard.existing.filePickerTitle"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
File file = fileChooser.showOpenDialog(window);
if (file != null) {
vaultPath.setValue(file.toPath().toAbsolutePath().getParent());
File masterkeyFile = fileChooser.showOpenDialog(window);
if (masterkeyFile != null) {
vaultPath.setValue(masterkeyFile.toPath().toAbsolutePath().getParent());
try {
Vault newVault = vaultListManager.add(vaultPath.get());
vault.set(newVault);
window.setScene(successScene.get());
} catch (NoSuchFileException e) {
LOG.error("Nope", e);
// TODO
LOG.error("Failed to open existing vault.", e);
window.setScene(errorScene.get());
}
}
}

View File

@@ -23,12 +23,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.File;
import java.io.IOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ResourceBundle;
@@ -53,12 +52,14 @@ public class CreateNewVaultLocationController implements FxController {
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
public ToggleGroup predefinedLocationToggler;
public RadioButton iclouddriveRadioButton;
public RadioButton dropboxRadioButton;
public RadioButton gdriveRadioButton;
public RadioButton onedriveRadioButton;
public RadioButton customRadioButton;
@Inject
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, StringProperty vaultName, ResourceBundle resourceBundle) {
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
this.window = window;
this.chooseNameScene = chooseNameScene;
this.choosePasswordScene = choosePasswordScene;
@@ -92,10 +93,14 @@ public class CreateNewVaultLocationController implements FxController {
}
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
if (dropboxRadioButton.equals(newValue)) {
if (iclouddriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getIclouddriveLocation().resolve(vaultName.get()));
} else if (dropboxRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getDropboxLocation().resolve(vaultName.get()));
} else if (gdriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
} else if (onedriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
} else if (customRadioButton.equals(newValue)) {
vaultPath.set(customVaultPath.resolve(vaultName.get()));
}
@@ -115,14 +120,12 @@ public class CreateNewVaultLocationController implements FxController {
Files.delete(createdDir); // assert: dir exists and is empty
window.setScene(choosePasswordScene.get());
} catch (FileAlreadyExistsException e) {
LOG.warn("Can not use already existing vault path: {}", vaultPath.get());
LOG.warn("Can not use already existing vault path {}", vaultPath.get());
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
} catch (NoSuchFileException | DirectoryNotEmptyException e) {
LOG.error("Failed to delete recently created directory.", e);
// TODO show generic error text for unexpected exception
} catch (IOException e) {
LOG.warn("Can not create vault at path: {}", vaultPath.get());
// TODO show generic error text for unexpected exception
LOG.warn("Thrown Exception:", e);
warningText.set(resourceBundle.getString("addvaultwizard.new.ioException"));
}
}

View File

@@ -16,6 +16,7 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
import javax.inject.Named;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.regex.Pattern;
@@ -36,7 +37,7 @@ public class CreateNewVaultNameController implements FxController {
private final StringBinding warningText;
@Inject
CreateNewVaultNameController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, ObjectProperty<Path> vaultPath, StringProperty vaultName, ResourceBundle resourceBundle) {
CreateNewVaultNameController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
this.window = window;
this.welcomeScene = welcomeScene;
this.chooseLocationScene = chooseLocationScene;

View File

@@ -5,17 +5,14 @@ import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
@@ -25,14 +22,12 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.Tasks;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.channels.WritableByteChannel;
@@ -56,43 +51,42 @@ public class CreateNewVaultPasswordController implements FxController {
private final Stage window;
private final Lazy<Scene> chooseLocationScene;
private final Lazy<Scene> recoveryKeyScene;
private final Lazy<Scene> successScene;
private final ExecutorService executor;
private final StringProperty vaultName;
private final ObjectProperty<Path> vaultPath;
private final ObjectProperty<Vault> vault;
private final RecoveryKeyFactory recoveryKeyFactory;
private final StringProperty vaultNameProperty;
private final ObjectProperty<Path> vaultPathProperty;
private final ObjectProperty<Vault> vaultProperty;
private final StringProperty recoveryKeyProperty;
private final VaultListManager vaultListManager;
private final ResourceBundle resourceBundle;
private final PasswordStrengthUtil strengthRater;
private final ObjectProperty<CharSequence> password;
private final ReadmeGenerator readmeGenerator;
private final IntegerProperty passwordStrength;
private final BooleanProperty processing;
private final BooleanProperty readyToCreateVault;
private final ObjectBinding<ContentDisplay> createVaultButtonState;
public NiceSecurePasswordField passwordField;
public NiceSecurePasswordField reenterField;
public Label passwordStrengthLabel;
public HBox passwordMatchBox;
public FontAwesome5IconView checkmark;
public FontAwesome5IconView cross;
public Label passwordMatchLabel;
public CheckBox finalConfirmationCheckbox;
public ToggleGroup recoveryKeyChoice;
public Toggle showRecoveryKey;
public Toggle skipRecoveryKey;
@Inject
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ExecutorService executor, StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ReadmeGenerator readmeGenerator) {
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty<CharSequence> password, ReadmeGenerator readmeGenerator) {
this.window = window;
this.chooseLocationScene = chooseLocationScene;
this.recoveryKeyScene = recoveryKeyScene;
this.successScene = successScene;
this.executor = executor;
this.vaultName = vaultName;
this.vaultPath = vaultPath;
this.vault = vault;
this.recoveryKeyFactory = recoveryKeyFactory;
this.vaultNameProperty = vaultName;
this.vaultPathProperty = vaultPath;
this.vaultProperty = vault;
this.recoveryKeyProperty = recoveryKey;
this.vaultListManager = vaultListManager;
this.resourceBundle = resourceBundle;
this.strengthRater = strengthRater;
this.password = password;
this.readmeGenerator = readmeGenerator;
this.passwordStrength = new SimpleIntegerProperty(-1);
this.processing = new SimpleBooleanProperty();
this.readyToCreateVault = new SimpleBooleanProperty();
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
@@ -100,22 +94,8 @@ public class CreateNewVaultPasswordController implements FxController {
@FXML
public void initialize() {
//binds the actual strength value to the rating of the password util
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters().toString()), passwordField.textProperty()));
//binding indicating if the passwords not match
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(() -> CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0, passwordField.textProperty(), reenterField.textProperty());
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
readyToCreateVault.bind(reenterFieldNotEmpty.and(passwordsMatch).and(finalConfirmationCheckbox.selectedProperty()).and(processing.not()));
//make match indicator invisible when passwords do not match or one is empty
passwordMatchBox.visibleProperty().bind(reenterFieldNotEmpty);
checkmark.visibleProperty().bind(passwordsMatch.and(reenterFieldNotEmpty));
checkmark.managedProperty().bind(checkmark.visibleProperty());
cross.visibleProperty().bind(passwordsMatch.not().and(reenterFieldNotEmpty));
cross.managedProperty().bind(cross.visibleProperty());
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("addvaultwizard.new.passwordsMatch")).otherwise(resourceBundle.getString("addvaultwizard.new.passwordsDoNotMatch")));
//bindsings for the password strength indicator
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
BooleanBinding isValidNewPassword = Bindings.createBooleanBinding(() -> password.get() != null && password.get().length() > 0, password);
readyToCreateVault.bind(isValidNewPassword.and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
}
@FXML
@@ -125,8 +105,8 @@ public class CreateNewVaultPasswordController implements FxController {
@FXML
public void next() {
Path pathToVault = vaultPath.get();
Path pathToVault = vaultPathProperty.get();
try {
Files.createDirectory(pathToVault);
} catch (FileAlreadyExistsException e) {
@@ -137,11 +117,41 @@ public class CreateNewVaultPasswordController implements FxController {
LOG.error("", e);
}
if (showRecoveryKey.equals(recoveryKeyChoice.getSelectedToggle())) {
showRecoveryKeyScene();
} else if (skipRecoveryKey.equals(recoveryKeyChoice.getSelectedToggle())) {
showSuccessScene();
} else {
throw new IllegalStateException("Unexpected toggle state");
}
}
private void showRecoveryKeyScene() {
Path pathToVault = vaultPathProperty.get();
processing.set(true);
Tasks.create(() -> {
initializeVault(pathToVault, passwordField.getCharacters());
initializeVault(pathToVault, password.get());
return recoveryKeyFactory.createRecoveryKey(pathToVault, password.get());
}).onSuccess(recoveryKey -> {
initializationSucceeded(pathToVault);
recoveryKeyProperty.set(recoveryKey);
window.setScene(recoveryKeyScene.get());
}).onError(IOException.class, e -> {
// TODO show generic error screen
LOG.error("", e);
}).andFinally(() -> {
processing.set(false);
}).runOnce(executor);
}
private void showSuccessScene() {
Path pathToVault = vaultPathProperty.get();
processing.set(true);
Tasks.create(() -> {
initializeVault(pathToVault, password.get());
}).onSuccess(() -> {
initializationSucceeded(pathToVault);
window.setScene(successScene.get());
}).onError(IOException.class, e -> {
// TODO show generic error screen
LOG.error("", e);
@@ -170,12 +180,11 @@ public class CreateNewVaultPasswordController implements FxController {
}
LOG.info("Created vault at {}", path);
}
private void initializationSucceeded(Path pathToVault) {
try {
Vault newVault = vaultListManager.add(pathToVault);
vault.set(newVault);
window.setScene(successScene.get());
vaultProperty.set(newVault);
} catch (NoSuchFileException e) {
throw new UncheckedIOException(e);
}
@@ -184,19 +193,11 @@ public class CreateNewVaultPasswordController implements FxController {
/* Getter/Setter */
public String getVaultName() {
return vaultName.get();
return vaultNameProperty.get();
}
public StringProperty vaultNameProperty() {
return vaultName;
}
public IntegerProperty passwordStrengthProperty() {
return passwordStrength;
}
public int getPasswordStrength() {
return passwordStrength.get();
return vaultNameProperty;
}
public BooleanProperty readyToCreateVaultProperty() {

View File

@@ -0,0 +1,28 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
public class CreateNewVaultRecoveryKeyController implements FxController {
private final Stage window;
private final Lazy<Scene> successScene;
@Inject
CreateNewVaultRecoveryKeyController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene) {
this.window = window;
this.successScene = successScene;
}
@FXML
public void next() {
window.setScene(successScene.get());
}
}

View File

@@ -13,20 +13,30 @@ import java.nio.file.Paths;
public class LocationPresets {
private static final String USER_HOME = System.getProperty("user.home");
private static final String[] ICLOUDDRIVE_LOCATIONS = {"~/Library/Mobile Documents/iCloud~com~setolabs~Cryptomator/Documents"};
private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"};
private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"};
private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"};
private final ReadOnlyObjectProperty<Path> iclouddriveLocation;
private final ReadOnlyObjectProperty<Path> dropboxLocation;
private final ReadOnlyObjectProperty<Path> gdriveLocation;
private final ReadOnlyObjectProperty<Path> onedriveLocation;
private final BooleanBinding foundIclouddrive;
private final BooleanBinding foundDropbox;
private final BooleanBinding foundGdrive;
private final BooleanBinding foundOnedrive;
@Inject
public LocationPresets() {
this.iclouddriveLocation = new SimpleObjectProperty<>(existingWritablePath(ICLOUDDRIVE_LOCATIONS));
this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS));
this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS));
this.onedriveLocation = new SimpleObjectProperty<>(existingWritablePath(ONEDRIVE_LOCATIONS));
this.foundIclouddrive = iclouddriveLocation.isNotNull();
this.foundDropbox = dropboxLocation.isNotNull();
this.foundGdrive = gdriveLocation.isNotNull();
this.foundOnedrive = onedriveLocation.isNotNull();
}
private static Path existingWritablePath(String... candidates) {
@@ -49,6 +59,22 @@ public class LocationPresets {
/* Observables */
public ReadOnlyObjectProperty<Path> iclouddriveLocationProperty() {
return iclouddriveLocation;
}
public Path getIclouddriveLocation() {
return iclouddriveLocation.get();
}
public BooleanBinding foundIclouddriveProperty() {
return foundIclouddrive;
}
public boolean isFoundIclouddrive() {
return foundIclouddrive.get();
}
public ReadOnlyObjectProperty<Path> dropboxLocationProperty() {
return dropboxLocation;
}
@@ -73,7 +99,7 @@ public class LocationPresets {
return gdriveLocation.get();
}
public BooleanBinding froundGdriveProperty() {
public BooleanBinding foundGdriveProperty() {
return foundGdrive;
}
@@ -81,4 +107,20 @@ public class LocationPresets {
return foundGdrive.get();
}
public ReadOnlyObjectProperty<Path> onedriveLocationProperty() {
return onedriveLocation;
}
public Path getOnedriveLocation() {
return onedriveLocation.get();
}
public BooleanBinding foundOnedriveProperty() {
return foundOnedrive;
}
public boolean isFoundOnedrive() {
return foundOnedrive.get();
}
}

View File

@@ -10,7 +10,7 @@ public class ReadmeGenerator {
// specs: https://web.archive.org/web/20190708132914/http://www.kleinlercher.at/tools/Windows_Protocols/Word2007RTFSpec9.pdf
private static final String RTF_HEADER = "{\\rtf1\\fbidis\\ansi\\uc0\\fs32\n";
private static final String RTF_FOOTER = "}";
private static final String HELP_URL = "{\\field{\\*\\fldinst HYPERLINK \"http://www.google.com/\"}{\\fldrslt google.com}}";
private static final String HELP_URL = "{\\field{\\*\\fldinst HYPERLINK \"http://docs.cryptoamtor.org/\"}{\\fldrslt docs.cryptoamtor.org}}";
private final ResourceBundle resourceBundle;

View File

@@ -3,6 +3,7 @@ package org.cryptomator.ui.changepassword;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
@@ -22,6 +23,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.util.ResourceBundle;
@@ -33,47 +35,24 @@ public class ChangePasswordController implements FxController {
private final Stage window;
private final Vault vault;
private final ResourceBundle resourceBundle;
private final PasswordStrengthUtil strengthRater;
private final IntegerProperty passwordStrength;
private final ObjectProperty<CharSequence> newPassword;
public NiceSecurePasswordField oldPasswordField;
public NiceSecurePasswordField newPasswordField;
public NiceSecurePasswordField reenterPasswordField;
public Label passwordStrengthLabel;
public HBox passwordMatchBox;
public FontAwesome5IconView checkmark;
public FontAwesome5IconView cross;
public Label passwordMatchLabel;
public CheckBox finalConfirmationCheckbox;
public Button finishButton;
@Inject
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword) {
this.window = window;
this.vault = vault;
this.resourceBundle = resourceBundle;
this.strengthRater = strengthRater;
this.passwordStrength = new SimpleIntegerProperty(-1);
this.newPassword = newPassword;
}
@FXML
public void initialize() {
//binds the actual strength value to the rating of the password util
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(newPasswordField.getCharacters().toString()), newPasswordField.textProperty()));
//binding indicating if the passwords not match
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(() -> CharSequence.compare(newPasswordField.getCharacters(), reenterPasswordField.getCharacters()) == 0, newPasswordField.textProperty(), reenterPasswordField.textProperty());
BooleanBinding reenterFieldNotEmpty = reenterPasswordField.textProperty().isNotEmpty();
//disable the finish button when passwords do not match or one is empty
finishButton.disableProperty().bind(reenterFieldNotEmpty.not().or(passwordsMatch.not()).or(finalConfirmationCheckbox.selectedProperty().not()));
//make match indicator invisible when passwords do not match or one is empty
passwordMatchBox.visibleProperty().bind(reenterFieldNotEmpty);
checkmark.visibleProperty().bind(passwordsMatch.and(reenterFieldNotEmpty));
checkmark.managedProperty().bind(checkmark.visibleProperty());
cross.visibleProperty().bind(passwordsMatch.not().and(reenterFieldNotEmpty));
cross.managedProperty().bind(cross.visibleProperty());
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("changepassword.passwordsMatch")).otherwise(resourceBundle.getString("changepassword.passwordsDoNotMatch")));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
BooleanBinding hasNotConfirmedCheckbox = finalConfirmationCheckbox.selectedProperty().not();
BooleanBinding isInvalidNewPassword = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword);
finishButton.disableProperty().bind(hasNotConfirmedCheckbox.or(isInvalidNewPassword));
}
@FXML
@@ -84,15 +63,15 @@ public class ChangePasswordController implements FxController {
@FXML
public void finish() {
try {
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPasswordField.getCharacters());
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
LOG.info("Successful changed password for {}", vault.getDisplayableName());
window.close();
} catch (IOException e) {
//TODO
// TODO show generic error screen
LOG.error("IO error occured during password change. Unable to perform operation.", e);
e.printStackTrace();
} catch (InvalidPassphraseException e) {
//TODO
// TODO shake
LOG.info("Wrong old password.");
}
}
@@ -102,12 +81,5 @@ public class ChangePasswordController implements FxController {
public Vault getVault() {
return vault;
}
public IntegerProperty passwordStrengthProperty() {
return passwordStrength;
}
public int getPasswordStrength() {
return passwordStrength.get();
}
}

View File

@@ -4,42 +4,55 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import javax.inject.Named;
import javax.inject.Provider;
import java.nio.CharBuffer;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module
abstract class ChangePasswordModule {
@Provides
@ChangePasswordWindow
@ChangePasswordScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, resourceBundle);
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
@Provides
@ChangePasswordWindow
@ChangePasswordScoped
static Stage provideStage(@Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon) {
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@ChangePasswordWindow
@ChangePasswordScoped
static Stage provideStage(@Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("changepassword.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
windowIcon.ifPresent(stage.getIcons()::add);
stage.getIcons().addAll(windowIcons);
return stage;
}
@@ -57,5 +70,12 @@ abstract class ChangePasswordModule {
@IntoMap
@FxControllerKey(ChangePasswordController.class)
abstract FxController bindUnlockController(ChangePasswordController controller);
@Provides
@IntoMap
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
}
}

View File

@@ -0,0 +1,36 @@
package org.cryptomator.ui.common;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.value.WritableValue;
import javafx.stage.Window;
import javafx.util.Duration;
public class Animations {
public static Timeline createShakeWindowAnimation(Window window) {
WritableValue<Double> writableWindowX = new WritableValue<>() {
@Override
public Double getValue() {
return window.getX();
}
@Override
public void setValue(Double value) {
window.setX(value);
}
};
return new Timeline( //
new KeyFrame(Duration.ZERO, new KeyValue(writableWindowX, window.getX())), //
new KeyFrame(new Duration(100), new KeyValue(writableWindowX, window.getX() - 22.0)), //
new KeyFrame(new Duration(200), new KeyValue(writableWindowX, window.getX() + 18.0)), //
new KeyFrame(new Duration(300), new KeyValue(writableWindowX, window.getX() - 14.0)), //
new KeyFrame(new Duration(400), new KeyValue(writableWindowX, window.getX() + 10.0)), //
new KeyFrame(new Duration(500), new KeyValue(writableWindowX, window.getX() - 6.0)), //
new KeyFrame(new Duration(600), new KeyValue(writableWindowX, window.getX() + 2.0)), //
new KeyFrame(new Duration(700), new KeyValue(writableWindowX, window.getX())) //
);
}
}

View File

@@ -0,0 +1,59 @@
package org.cryptomator.ui.common;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.stage.Stage;
import javafx.stage.Window;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import javax.inject.Inject;
import java.util.function.Function;
@FxApplicationScoped
public class DefaultSceneFactory implements Function<Parent, Scene> {
protected static final KeyCodeCombination ALT_F4 = new KeyCodeCombination(KeyCode.F4, KeyCombination.ALT_DOWN);
protected static final KeyCodeCombination SHORTCUT_W = new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN);
protected final Settings settings;
@Inject
public DefaultSceneFactory(Settings settings) {
this.settings = settings;
}
@Override
public Scene apply(Parent root) {
Scene scene = new Scene(root);
configureRoot(root);
configureScene(scene);
return scene;
}
protected void configureRoot(Parent root) {
root.nodeOrientationProperty().bind(settings.userInterfaceOrientation());
}
protected void configureScene(Scene scene) {
scene.windowProperty().addListener(observable -> {
Window window = scene.getWindow();
if (window instanceof Stage) {
setupDefaultAccelerators(scene, (Stage) window);
}
});
}
protected void setupDefaultAccelerators(Scene scene, Stage stage) {
if (SystemUtils.IS_OS_WINDOWS) {
scene.getAccelerators().put(ALT_F4, stage::close);
} else {
scene.getAccelerators().put(SHORTCUT_W, stage::close);
}
}
}

View File

@@ -1,24 +1,28 @@
package org.cryptomator.ui.common;
import com.google.common.base.Splitter;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javax.inject.Provider;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.function.Function;
public class FXMLLoaderFactory {
private final Map<Class<? extends FxController>, Provider<FxController>> factories;
private final Map<Class<? extends FxController>, Provider<FxController>> controllerFactories;
private final Function<Parent, Scene> sceneFactory;
private final ResourceBundle resourceBundle;
public FXMLLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
this.factories = factories;
public FXMLLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> controllerFactories, Function<Parent, Scene> sceneFactory, ResourceBundle resourceBundle) {
this.controllerFactories = controllerFactories;
this.sceneFactory = sceneFactory;
this.resourceBundle = resourceBundle;
}
@@ -34,6 +38,7 @@ public class FXMLLoaderFactory {
/**
* Loads the FXML given fxml resource in a new FXMLLoader instance.
*
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
* @return The FXMLLoader used to load the file
* @throws IOException if an error occurs while loading the FXML file
@@ -48,6 +53,7 @@ public class FXMLLoaderFactory {
/**
* {@link #load(String) Loads} the FXML file and creates a new Scene containing the loaded ui.
*
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
* @throws UncheckedIOException wrapping any IOException thrown by {@link #load(String)).
*/
@@ -59,14 +65,16 @@ public class FXMLLoaderFactory {
throw new UncheckedIOException("Failed to load " + fxmlResourceName, e);
}
Parent root = loader.getRoot();
return new Scene(root);
List<String> addtionalStyleSheets = Splitter.on(',').omitEmptyStrings().splitToList(resourceBundle.getString("additionalStyleSheets"));
addtionalStyleSheets.forEach(styleSheet -> root.getStylesheets().add("/css/" + styleSheet));
return sceneFactory.apply(root);
}
private FxController constructController(Class<?> aClass) {
if (!factories.containsKey(aClass)) {
if (!controllerFactories.containsKey(aClass)) {
throw new IllegalArgumentException("ViewController not registered: " + aClass);
} else {
return factories.get(aClass).get();
return controllerFactories.get(aClass).get();
}
}
}

View File

@@ -3,27 +3,41 @@ package org.cryptomator.ui.common;
public enum FxmlFile {
ADDVAULT_WELCOME("/fxml/addvault_welcome.fxml"), //
ADDVAULT_EXISTING("/fxml/addvault_existing.fxml"), //
ADDVAULT_EXISTING_ERROR("/fxml/addvault_existing_error.fxml"),
ADDVAULT_NEW_NAME("/fxml/addvault_new_name.fxml"), //
ADDVAULT_NEW_LOCATION("/fxml/addvault_new_location.fxml"), //
ADDVAULT_NEW_PASSWORD("/fxml/addvault_new_password.fxml"), //
ADDVAULT_NEW_RECOVERYKEY("/fxml/addvault_new_recoverykey.fxml"), //
ADDVAULT_SUCCESS("/fxml/addvault_success.fxml"), //
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
MAIN_WINDOW("/fxml/main_window.fxml"), //
MIGRATION_CAPABILITY_ERROR("/fxml/migration_capability_error.fxml"), //
MIGRATION_GENERIC_ERROR("/fxml/migration_generic_error.fxml"), //
MIGRATION_RUN("/fxml/migration_run.fxml"), //
MIGRATION_START("/fxml/migration_start.fxml"), //
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
PREFERENCES("/fxml/preferences.fxml"), //
QUIT("/fxml/quit.fxml"), //
RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), //
RECOVERYKEY_RECOVER("/fxml/recoverykey_recover.fxml"), //
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
UNLOCK("/fxml/unlock2.fxml"), // TODO rename
UNLOCK("/fxml/unlock.fxml"),
UNLOCK_GENERIC_ERROR("/fxml/unlock_generic_error.fxml"), //
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
VAULT_OPTIONS("/fxml/vault_options.fxml"), //
WRONGFILEALERT("/fxml/wrongfilealert.fxml");
private final String filename;
private final String ressourcePathString;
FxmlFile(String filename) {
this.filename = filename;
FxmlFile(String ressourcePathString) {
this.ressourcePathString = ressourcePathString;
}
public String getRessourcePathString(){
return ressourcePathString;
}
}

View File

@@ -0,0 +1,74 @@
package org.cryptomator.ui.common;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.fxmisc.easybind.EasyBind;
import java.util.ResourceBundle;
public class NewPasswordController implements FxController {
private final ResourceBundle resourceBundle;
private final PasswordStrengthUtil strengthRater;
private final ObjectProperty<CharSequence> password;
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1);
public NiceSecurePasswordField passwordField;
public NiceSecurePasswordField reenterField;
public Label passwordStrengthLabel;
public Label passwordMatchLabel;
public FontAwesome5IconView checkmark;
public FontAwesome5IconView cross;
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ObjectProperty<CharSequence> password) {
this.resourceBundle = resourceBundle;
this.strengthRater = strengthRater;
this.password = password;
}
@FXML
public void initialize() {
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::hasSamePasswordInBothFields, passwordField.textProperty(), reenterField.textProperty());
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters()), passwordField.textProperty()));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty);
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(checkmark).otherwise(cross));
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("newPassword.passwordsMatch")).otherwise(resourceBundle.getString("newPassword.passwordsDoNotMatch")));
passwordField.textProperty().addListener(this::passwordsDidChange);
reenterField.textProperty().addListener(this::passwordsDidChange);
}
private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
if (hasSamePasswordInBothFields() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
password.set(passwordField.getCharacters());
} else {
password.set("");
}
}
private boolean hasSamePasswordInBothFields() {
return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0;
}
/* Getter/Setter */
public IntegerProperty passwordStrengthProperty() {
return passwordStrength;
}
public int getPasswordStrength() {
return passwordStrength.get();
}
}

View File

@@ -8,12 +8,11 @@
*******************************************************************************/
package org.cryptomator.ui.common;
import com.google.common.base.Strings;
import com.nulabinc.zxcvbn.Zxcvbn;
import org.cryptomator.common.Environment;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
@@ -22,31 +21,37 @@ public class PasswordStrengthUtil {
private static final int PW_TRUNC_LEN = 100; // truncate very long passwords, since zxcvbn memory and runtime depends vastly on the length
private static final String RESSOURCE_PREFIX = "passwordStrength.messageLabel.";
private static final List<String> SANITIZED_INPUTS = List.of("cryptomator");
private final Zxcvbn zxcvbn;
private final List<String> sanitizedInputs;
private final ResourceBundle resourceBundle;
private final int minPwLength;
private final Zxcvbn zxcvbn;
@Inject
public PasswordStrengthUtil(ResourceBundle resourceBundle) {
public PasswordStrengthUtil(ResourceBundle resourceBundle, Environment environment) {
this.resourceBundle = resourceBundle;
this.minPwLength = environment.getMinPwLength();
this.zxcvbn = new Zxcvbn();
this.sanitizedInputs = new ArrayList<>();
this.sanitizedInputs.add("cryptomator");
}
public int computeRate(String password) {
if (Strings.isNullOrEmpty(password)) {
public boolean fulfillsMinimumRequirements(CharSequence password) {
return password.length() >= minPwLength;
}
public int computeRate(CharSequence password) {
if (password == null || password.length() < minPwLength) {
return -1;
} else {
int numCharsToRate = Math.min(PW_TRUNC_LEN, password.length());
return zxcvbn.measure(password.substring(0, numCharsToRate), sanitizedInputs).getScore();
return zxcvbn.measure(password.subSequence(0, numCharsToRate), SANITIZED_INPUTS).getScore();
}
}
public String getStrengthDescription(Number score) {
if (resourceBundle.containsKey(RESSOURCE_PREFIX + score.intValue())) {
return resourceBundle.getString("passwordStrength.messageLabel." + score.intValue());
if (score.intValue() == -1) {
return String.format(resourceBundle.getString(RESSOURCE_PREFIX + "tooShort"), minPwLength);
} else if (resourceBundle.containsKey(RESSOURCE_PREFIX + score.intValue())) {
return resourceBundle.getString(RESSOURCE_PREFIX + score.intValue());
} else {
return "";
}

View File

@@ -0,0 +1,28 @@
package org.cryptomator.ui.common;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
public class StackTraceController implements FxController {
private final String stackTrace;
public StackTraceController(Throwable cause) {
this.stackTrace = provideStackTrace(cause);
}
private static String provideStackTrace(Throwable cause) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
cause.printStackTrace(new PrintStream(baos));
return baos.toString(StandardCharsets.UTF_8);
}
/* Getter/Setter */
public String getStackTrace() {
return stackTrace;
}
}

View File

@@ -73,21 +73,21 @@ public class Tasks {
return new TaskImpl<>(callable, successHandler, errorHandlers, finallyHandler);
}
public Task<T> runOnce(ExecutorService executorService) {
public Task<T> runOnce(ExecutorService executor) {
Task<T> task = build();
executorService.submit(task);
executor.submit(task);
return task;
}
public Task<T> scheduleOnce(ScheduledExecutorService executorService, long delay, TimeUnit unit) {
public Task<T> scheduleOnce(ScheduledExecutorService scheduler, long delay, TimeUnit unit) {
Task<T> task = build();
executorService.schedule(task, delay, unit);
scheduler.schedule(task, delay, unit);
return task;
}
public ScheduledService<T> schedulePeriodically(ExecutorService executorService, Duration initialDelay, Duration period) {
public ScheduledService<T> schedulePeriodically(ExecutorService executor, Duration initialDelay, Duration period) {
ScheduledService<T> service = new RestartingService<>(this::build);
service.setExecutor(executorService);
service.setExecutor(executor);
service.setDelay(initialDelay);
service.setPeriod(period);
Platform.runLater(service::start);

View File

@@ -0,0 +1,350 @@
package org.cryptomator.ui.common;
import javafx.concurrent.Task;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
@FxApplicationScoped
public class VaultService {
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
private final ExecutorService executorService;
private final Optional<KeychainAccess> keychain;
@Inject
public VaultService(ExecutorService executorService, Optional<KeychainAccess> keychain) {
this.executorService = executorService;
this.keychain = keychain;
}
public void reveal(Vault vault) {
executorService.execute(createRevealTask(vault));
}
/**
* Creates but doesn't start a reveal task.
*
* @param vault The vault to reveal
*/
public Task<Vault> createRevealTask(Vault vault) {
Task<Vault> task = new RevealVaultTask(vault);
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Attempts to unlock all given vaults in a background thread using passwords stored in the system keychain.
*
* @param vaults The vaults to unlock
* @implNote No-op if no system keychain is present
*/
public void attemptAutoUnlock(Collection<Vault> vaults) {
if (!keychain.isPresent()) {
LOG.debug("No system keychain found. Unable to auto unlock without saved passwords.");
} else {
for (Vault vault : vaults) {
attemptAutoUnlock(vault, keychain.get());
}
}
}
/**
* Unlocks a vault in a background thread using a stored passphrase
*
* @param vault The vault to unlock
* @param keychainAccess The system keychain holding the passphrase for the vault
*/
public void attemptAutoUnlock(Vault vault, KeychainAccess keychainAccess) {
executorService.execute(createAutoUnlockTask(vault, keychainAccess));
}
/**
* Creates but doesn't start an auto-unlock task.
*
* @param vault The vault to unlock
* @param keychainAccess The system keychain holding the passphrase for the vault
* @return The task
*/
public Task<Vault> createAutoUnlockTask(Vault vault, KeychainAccess keychainAccess) {
Task<Vault> task = new AutoUnlockVaultTask(vault, keychainAccess);
task.setOnSucceeded(evt -> LOG.info("Auto-unlocked {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to auto-unlock " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Unlocks a vault in a background thread
*
* @param vault The vault to unlock
* @param passphrase The password to use - wipe this param asap
* @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
*/
public void unlock(Vault vault, CharSequence passphrase) {
executorService.execute(createUnlockTask(vault, passphrase));
}
/**
* Creates but doesn't start an unlock task.
*
* @param vault The vault to unlock
* @param passphrase The password to use - wipe this param asap
* @return The task
* @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
*/
public Task<Vault> createUnlockTask(Vault vault, CharSequence passphrase) {
Task<Vault> task = new UnlockVaultTask(vault, passphrase);
task.setOnSucceeded(evt -> LOG.info("Unlocked {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to unlock " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Locks a vault in a background thread.
*
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
*/
public void lock(Vault vault, boolean forced) {
executorService.execute(createLockTask(vault, forced));
}
/**
* Creates but doesn't start a lock task.
*
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
* @return The task
*/
public Task<Vault> createLockTask(Vault vault, boolean forced) {
Task<Vault> task = new LockVaultTask(vault, forced);
task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to lock " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Locks all given vaults in a background thread.
*
* @param vaults The vaults to lock
* @param forced Whether to attempt a forced lock
*/
public void lockAll(Collection<Vault> vaults, boolean forced) {
executorService.execute(createLockAllTask(vaults, forced));
}
/**
* Creates but doesn't start a lock-all task.
*
* @param vaults The list of vaults to be locked
* @param forced Whether to attempt a forced lock
* @return Meta-Task that waits until all vaults are locked or fails after the first failure of a subtask
*/
public Task<Collection<Vault>> createLockAllTask(Collection<Vault> vaults, boolean forced) {
List<Task<Vault>> lockTasks = vaults.stream().map(v -> new LockVaultTask(v, forced)).collect(Collectors.toUnmodifiableList());
lockTasks.forEach(executorService::execute);
Task<Collection<Vault>> task = new WaitForTasksTask(lockTasks);
String vaultNames = vaults.stream().map(Vault::getDisplayableName).collect(Collectors.joining(", "));
task.setOnSucceeded(evt -> LOG.info("Locked {}", vaultNames));
task.setOnFailed(evt -> LOG.error("Failed to lock vaults " + vaultNames, evt.getSource().getException()));
return task;
}
private static class RevealVaultTask extends Task<Vault> {
private final Vault vault;
/**
* @param vault The vault to lock
*/
public RevealVaultTask(Vault vault) {
this.vault = vault;
}
@Override
protected Vault call() throws Volume.VolumeException {
vault.reveal();
return vault;
}
}
/**
* A task that waits for completion of multiple other tasks
*/
private static class WaitForTasksTask extends Task<Collection<Vault>> {
private final Collection<Task<Vault>> startedTasks;
public WaitForTasksTask(Collection<Task<Vault>> tasks) {
this.startedTasks = List.copyOf(tasks);
}
@Override
protected Collection<Vault> call() throws Exception {
Iterator<Task<Vault>> remainingTasks = startedTasks.iterator();
Collection<Vault> completed = new ArrayList<>();
try {
// wait for all tasks:
while (remainingTasks.hasNext()) {
Vault lockedVault = remainingTasks.next().get();
completed.add(lockedVault);
}
} catch (ExecutionException e) {
// cancel all remaining:
while (remainingTasks.hasNext()) {
remainingTasks.next().cancel(true);
}
throw e;
}
return List.copyOf(completed);
}
}
private static class AutoUnlockVaultTask extends Task<Vault> {
private final Vault vault;
private final KeychainAccess keychain;
public AutoUnlockVaultTask(Vault vault, KeychainAccess keychain) {
this.vault = vault;
this.keychain = keychain;
}
@Override
protected Vault call() throws Exception {
char[] storedPw = null;
try {
storedPw = keychain.loadPassphrase(vault.getId());
if (storedPw == null) {
throw new InvalidPassphraseException();
}
vault.unlock(CharBuffer.wrap(storedPw));
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
}
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.UNLOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.LOCKED);
}
}
private static class UnlockVaultTask extends Task<Vault> {
private final Vault vault;
private final CharBuffer passphrase;
/**
* @param vault The vault to unlock
* @param passphrase The password to use - wipe this param asap
* @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
*/
public UnlockVaultTask(Vault vault, CharSequence passphrase) {
this.vault = vault;
this.passphrase = CharBuffer.allocate(passphrase.length());
for (int i = 0; i < passphrase.length(); i++) {
this.passphrase.put(i, passphrase.charAt(i));
}
}
@Override
protected Vault call() throws Exception {
try {
vault.unlock(passphrase);
} finally {
Arrays.fill(passphrase.array(), ' ');
}
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.UNLOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.LOCKED);
}
}
/**
* A task that locks a vault
*/
private static class LockVaultTask extends Task<Vault> {
private final Vault vault;
private final boolean forced;
/**
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
*/
public LockVaultTask(Vault vault, boolean forced) {
this.vault = vault;
this.forced = forced;
}
@Override
protected Vault call() throws Volume.VolumeException {
vault.lock(forced);
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.LOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.UNLOCKED);
}
}
}

View File

@@ -5,27 +5,35 @@ package org.cryptomator.ui.controls;
*/
public enum FontAwesome5Icon {
ANCHOR("\uF13D"), //
ARROW_ALT_UP("\uF357"), //
ARROW_UP("\uF062"), //
CHECK("\uF00C"), //
COG("\uF013"), //
COGS("\uF085"), //
COPY("\uF0C5"), //
CROWN("\uF521"), //
EXCLAMATION("\uF12A"), //
EXCLAMATION_CIRCLE("\uF06A"), //
EXCLAMATION_TRIANGLE("\uF071"), //
EYE("\uF06E"), //
EYE_SLASH("\uF070"), //
FILE_IMPORT("\uF56F"), //
FOLDER_OPEN("\uF07C"), //
HAND_HOLDING_HEART("\uF4BE"), //
HEART("\uF004"), //
HDD("\uF0A0"), //
KEY("\uF084"), //
LOCK_ALT("\uF30D"), //
LOCK_OPEN_ALT("\uF3C2"), //
MINUS("\uF068"), //
LINK("\uF0C1"), //
LOCK("\uF023"), //
LOCK_OPEN("\uF3C1"), //
MAGIC("\uF0D0"), //
PLUS("\uF067"), //
PRINT("\uF02F"), //
QUESTION("\uF128"), //
SPARKLES("\uF890"), //
SPINNER("\uF110"), //
SYNC("\uF021"), //
TIMES("\uF00D"), //
WRENCH("\uF0AD"), //
WINDOW_MINIMIZE("\uF2D1"), //
;
private final String unicode;

View File

@@ -18,7 +18,7 @@ public class FontAwesome5IconView extends Text {
private static final FontAwesome5Icon DEFAULT_GLYPH = FontAwesome5Icon.ANCHOR;
private static final double DEFAULT_GLYPH_SIZE = 12.0;
private static final String FONT_PATH = "/css/fontawesome5-pro-solid.otf";
private static final String FONT_PATH = "/css/fontawesome5-free-solid.otf";
private static final Font FONT;
private ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);

View File

@@ -32,7 +32,7 @@ public class NiceSecurePasswordField extends StackPane {
iconContainer.getStyleClass().add(ICONS_STLYE_CLASS);
StackPane.setAlignment(iconContainer, Pos.CENTER_RIGHT);
capsLockedIcon.setGlyph(FontAwesome5Icon.ARROW_ALT_UP);
capsLockedIcon.setGlyph(FontAwesome5Icon.ARROW_UP);
capsLockedIcon.setGlyphSize(ICON_SIZE);
capsLockedIcon.visibleProperty().bind(passwordField.capsLockedProperty());
capsLockedIcon.managedProperty().bind(passwordField.capsLockedProperty());
@@ -67,6 +67,10 @@ public class NiceSecurePasswordField extends StackPane {
public void requestFocus() {
passwordField.requestFocus();
}
public String getText() {
return passwordField.getText();
}
public StringProperty textProperty() {
return passwordField.textProperty();
@@ -76,12 +80,16 @@ public class NiceSecurePasswordField extends StackPane {
return passwordField.getCharacters();
}
public void setPassword(CharSequence password) {
passwordField.setPassword(password);
}
public void setPassword(char[] password) {
passwordField.setPassword(password);
}
public void swipe() {
passwordField.swipe();;
passwordField.swipe();
}
public void selectAll() {

View File

@@ -11,6 +11,7 @@ import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
@@ -19,8 +20,8 @@ import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Named;
import javax.inject.Provider;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module
@@ -29,30 +30,23 @@ abstract class ForgetPasswordModule {
@Provides
@ForgetPasswordWindow
@ForgetPasswordScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, resourceBundle);
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@ForgetPasswordWindow
@ForgetPasswordScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon, @Named("forgetPasswordOwner") Stage owner) {
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons, @Named("forgetPasswordOwner") Stage owner) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("forgetPassword.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
windowIcon.ifPresent(stage.getIcons()::add);
stage.getIcons().addAll(windowIcons);
return stage;
}
@Provides
@FxmlScene(FxmlFile.FORGET_PASSWORD)
@ForgetPasswordScoped
static Scene provideForgetPasswordScene(@ForgetPasswordWindow FXMLLoaderFactory fxmlLoaders, @ForgetPasswordWindow Stage window) {
return fxmlLoaders.createScene("/fxml/forget_password.fxml");
}
@Provides
@ForgetPasswordWindow
@ForgetPasswordScoped
@@ -64,6 +58,15 @@ abstract class ForgetPasswordModule {
@ForgetPasswordWindow
@ForgetPasswordScoped
abstract ReadOnlyBooleanProperty bindReadOnlyConfirmedProperty(@ForgetPasswordWindow BooleanProperty confirmedProperty);
// ------------------
@Provides
@FxmlScene(FxmlFile.FORGET_PASSWORD)
@ForgetPasswordScoped
static Scene provideForgetPasswordScene(@ForgetPasswordWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/forget_password.fxml");
}
// ------------------

View File

@@ -8,6 +8,6 @@ import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface ForgetPasswordScoped {
@interface ForgetPasswordScoped {
}

View File

@@ -9,6 +9,7 @@ import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.stage.Stage;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.vaults.Vault;
@@ -16,8 +17,10 @@ import org.cryptomator.jni.JniException;
import org.cryptomator.jni.MacApplicationUiAppearance;
import org.cryptomator.jni.MacApplicationUiState;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.quit.QuitComponent;
import org.cryptomator.ui.unlock.UnlockComponent;
import org.slf4j.Logger;
@@ -26,8 +29,6 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.awt.desktop.QuitResponse;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@FxApplicationScoped
public class FxApplication extends Application {
@@ -40,17 +41,21 @@ public class FxApplication extends Application {
private final UnlockComponent.Builder unlockWindowBuilder;
private final QuitComponent.Builder quitWindowBuilder;
private final Optional<MacFunctions> macFunctions;
private final VaultService vaultService;
private final LicenseHolder licenseHolder;
private final ObservableSet<Stage> visibleStages = FXCollections.observableSet();
private final BooleanBinding hasVisibleStages = Bindings.isNotEmpty(visibleStages);
@Inject
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional<MacFunctions> macFunctions) {
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder) {
this.settings = settings;
this.mainWindow = mainWindow;
this.preferencesWindow = preferencesWindow;
this.unlockWindowBuilder = unlockWindowBuilder;
this.quitWindowBuilder = quitWindowBuilder;
this.macFunctions = macFunctions;
this.vaultService = vaultService;
this.licenseHolder = licenseHolder;
}
public void start() {
@@ -81,9 +86,9 @@ public class FxApplication extends Application {
}
}
public void showPreferencesWindow() {
public void showPreferencesWindow(SelectedPreferencesTab selectedTab) {
Platform.runLater(() -> {
Stage stage = preferencesWindow.get().showPreferencesWindow();
Stage stage = preferencesWindow.get().showPreferencesWindow(selectedTab);
addVisibleStage(stage);
LOG.debug("Showing Preferences");
});
@@ -113,11 +118,16 @@ public class FxApplication extends Application {
});
}
public VaultService getVaultService() {
return vaultService;
}
private void themeChanged(@SuppressWarnings("unused") ObservableValue<? extends UiTheme> observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) {
loadSelectedStyleSheet(newValue);
}
private void loadSelectedStyleSheet(UiTheme theme) {
private void loadSelectedStyleSheet(UiTheme desiredTheme) {
UiTheme theme = licenseHolder.isValidLicense() ? desiredTheme : UiTheme.LIGHT;
switch (theme) {
// case CUSTOM:
// // TODO

View File

@@ -5,8 +5,10 @@
*******************************************************************************/
package org.cryptomator.ui.fxapp;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image;
@@ -20,7 +22,9 @@ import org.cryptomator.ui.unlock.UnlockComponent;
import javax.inject.Named;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.List;
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, QuitComponent.class})
abstract class FxApplicationModule {
@@ -32,19 +36,32 @@ abstract class FxApplicationModule {
}
@Provides
@Named("windowIcon")
@Named("windowIcons")
@FxApplicationScoped
static Optional<Image> provideWindowIcon() {
static List<Image> provideWindowIcons() {
if (SystemUtils.IS_OS_MAC) {
return Optional.empty();
return Collections.emptyList();
}
try (InputStream in = FxApplicationModule.class.getResourceAsStream("/window_icon_32.png")) { // TODO: use some higher res depending on display?
return Optional.of(new Image(in));
try {
return List.of( //
createImageFromResource("/window_icon_32.png"), //
createImageFromResource("/window_icon_512.png") //
);
} catch (IOException e) {
return Optional.empty();
throw new UncheckedIOException("Failed to load embedded resource.", e);
}
}
private static Image createImageFromResource(String resourceName) throws IOException {
try (InputStream in = FxApplicationModule.class.getResourceAsStream(resourceName)) {
return new Image(in);
}
}
@Binds
abstract Application bindApplication(FxApplication application);
@Provides
static MainWindowComponent provideMainWindowComponent(MainWindowComponent.Builder builder) {
return builder.build();

View File

@@ -1,94 +0,0 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.l10n;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Objects;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
@FxApplicationScoped
public class Localization extends ResourceBundle {
private static final Logger LOG = LoggerFactory.getLogger(Localization.class);
private static final String LOCALIZATION_DEFAULT_FILE = "/localization/en.txt";
private static final String LOCALIZATION_FILENAME_FMT = "/localization/%s.txt";
private final ResourceBundle fallback;
private final ResourceBundle localized;
@Inject
public Localization() {
try {
this.fallback = Objects.requireNonNull(loadLocalizationFile(LOCALIZATION_DEFAULT_FILE));
LOG.debug("Loaded localization default file: {}", LOCALIZATION_DEFAULT_FILE);
String language = Locale.getDefault().getLanguage();
String region = Locale.getDefault().getCountry();
LOG.debug("Detected language \"{}\" and region \"{}\"", language, region);
ResourceBundle localizationBundle = null;
if (StringUtils.isNotEmpty(language) && StringUtils.isNotEmpty(region)) {
String file = String.format(LOCALIZATION_FILENAME_FMT, language + "_" + region);
LOG.trace("Attempting to load localization from: {}", file);
localizationBundle = loadLocalizationFile(file);
}
if (StringUtils.isNotEmpty(language) && localizationBundle == null) {
String file = String.format(LOCALIZATION_FILENAME_FMT, language);
LOG.trace("Attempting to load localization from: {}", file);
localizationBundle = loadLocalizationFile(file);
}
if (localizationBundle == null) {
LOG.debug("No localization found. Falling back to default language.");
localizationBundle = this.fallback;
}
this.localized = Objects.requireNonNull(localizationBundle);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
// returns null if no resource for given path
private static ResourceBundle loadLocalizationFile(String resourcePath) throws IOException {
try (InputStream in = Localization.class.getResourceAsStream(resourcePath)) {
if (in != null) {
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
return new PropertyResourceBundle(reader);
} else {
return null;
}
}
}
@Override
protected Object handleGetObject(String key) {
return localized.containsKey(key) ? localized.getObject(key) : fallback.getObject(key);
}
@Override
public Enumeration<String> getKeys() {
Collection<String> keys = Sets.union(localized.keySet(), fallback.keySet());
return Collections.enumeration(keys);
}
}

View File

@@ -1,16 +1,17 @@
package org.cryptomator.ui.launcher;
import java.nio.file.Path;
import java.util.Collection;
import java.util.stream.Stream;
public class AppLaunchEvent {
private final Stream<Path> pathsToOpen;
private final EventType type;
private final Collection<Path> pathsToOpen;
public enum EventType {REVEAL_APP, OPEN_FILE}
public AppLaunchEvent(EventType type, Stream<Path> pathsToOpen) {
public AppLaunchEvent(EventType type, Collection<Path> pathsToOpen) {
this.type = type;
this.pathsToOpen = pathsToOpen;
}
@@ -19,7 +20,7 @@ public class AppLaunchEvent {
return type;
}
public Stream<Path> getPathsToOpen() {
public Collection<Path> getPathsToOpen() {
return pathsToOpen;
}
}

View File

@@ -0,0 +1,125 @@
package org.cryptomator.ui.launcher;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.collections.ObservableList;
import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.awt.Desktop;
import java.awt.EventQueue;
import java.awt.desktop.QuitResponse;
import java.util.EnumSet;
import java.util.EventObject;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
@Singleton
public class AppLifecycleListener {
private static final Logger LOG = LoggerFactory.getLogger(AppLifecycleListener.class);
public static final Set<VaultState> STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR);
private final FxApplicationStarter fxApplicationStarter;
private final CountDownLatch shutdownLatch;
private final ObservableList<Vault> vaults;
private final AtomicBoolean allowQuitWithoutPrompt;
@Inject
AppLifecycleListener(FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, ObservableList<Vault> vaults) {
this.fxApplicationStarter = fxApplicationStarter;
this.shutdownLatch = shutdownLatch;
this.vaults = vaults;
this.allowQuitWithoutPrompt = new AtomicBoolean(true);
vaults.addListener(this::vaultListChanged);
// register preferences shortcut
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_PREFERENCES)) {
Desktop.getDesktop().setPreferencesHandler(this::showPreferencesWindow);
}
// register quit handler
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
Desktop.getDesktop().setQuitHandler(this::handleQuitRequest);
}
shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults);
}
/**
* Gracefully terminates the application.
*/
public void quit() {
handleQuitRequest(null, new QuitResponse() {
@Override
public void performQuit() {
System.exit(0);
}
@Override
public void cancelQuit() {
// no-op
}
});
}
private void handleQuitRequest(@SuppressWarnings("unused") EventObject e, QuitResponse response) {
QuitResponse decoratedQuitResponse = decorateQuitResponse(response);
if (allowQuitWithoutPrompt.get()) {
decoratedQuitResponse.performQuit();
} else {
fxApplicationStarter.get(true).thenAccept(app -> app.showQuitWindow(decoratedQuitResponse));
}
}
private QuitResponse decorateQuitResponse(QuitResponse originalQuitResponse) {
return new QuitResponse() {
@Override
public void performQuit() {
Platform.exit(); // will be no-op, if JavaFX never started.
shutdownLatch.countDown(); // main thread is waiting for this latch
EventQueue.invokeLater(originalQuitResponse::performQuit); // this will eventually call System.exit(0)
}
@Override
public void cancelQuit() {
originalQuitResponse.cancelQuit();
}
};
}
private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
assert Platform.isFxApplicationThread();
boolean allVaultsAllowTermination = vaults.stream().map(Vault::getState).allMatch(STATES_ALLOWING_TERMINATION::contains);
boolean suddenTerminationChanged = allowQuitWithoutPrompt.compareAndSet(!allVaultsAllowTermination, allVaultsAllowTermination);
if (suddenTerminationChanged) {
LOG.debug("Allow quitting without prompt: {}", allVaultsAllowTermination);
}
}
private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
}
private void forceUnmountRemainingVaults() {
for (Vault vault : vaults) {
if (vault.isUnlocked()) {
try {
vault.lock(true);
} catch (Volume.VolumeException e) {
LOG.error("Failed to unmount vault " + vault.getPath(), e);
}
}
}
}
}

View File

@@ -1,6 +1,8 @@
package org.cryptomator.ui.launcher;
import javafx.collections.ObservableList;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.jni.JniException;
import org.cryptomator.jni.MacApplicationUiState;
import org.cryptomator.jni.MacFunctions;
@@ -13,9 +15,8 @@ import javax.inject.Inject;
import javax.inject.Singleton;
import java.awt.Desktop;
import java.awt.SystemTray;
import java.awt.desktop.AppReopenedEvent;
import java.awt.desktop.AppReopenedListener;
import java.awt.desktop.SystemEventListener;
import java.util.Collection;
import java.util.Optional;
@Singleton
@@ -24,14 +25,16 @@ public class UiLauncher {
private static final Logger LOG = LoggerFactory.getLogger(UiLauncher.class);
private final Settings settings;
private final ObservableList<Vault> vaults;
private final TrayMenuComponent.Builder trayComponent;
private final FxApplicationStarter fxApplicationStarter;
private final AppLaunchEventHandler launchEventHandler;
private final Optional<MacFunctions> macFunctions;
@Inject
public UiLauncher(Settings settings, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<MacFunctions> macFunctions) {
public UiLauncher(Settings settings, ObservableList<Vault> vaults, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<MacFunctions> macFunctions) {
this.settings = settings;
this.vaults = vaults;
this.trayComponent = trayComponent;
this.fxApplicationStarter = fxApplicationStarter;
this.launchEventHandler = launchEventHandler;
@@ -48,7 +51,7 @@ public class UiLauncher {
}
// show window on start?
if (settings.startHidden().get()) {
if (hasTrayIcon && settings.startHidden().get()) {
LOG.debug("Hiding application...");
macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToAgentApplication));
} else {
@@ -58,6 +61,12 @@ public class UiLauncher {
// register app reopen listener
Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(hasTrayIcon));
// auto unlock
Collection<Vault> vaultsWithAutoUnlockEnabled = vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get());
if (!vaultsWithAutoUnlockEnabled.isEmpty()) {
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> app.getVaultService().attemptAutoUnlock(vaultsWithAutoUnlockEnabled));
}
launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);
}

View File

@@ -1,62 +1,39 @@
package org.cryptomator.ui.mainwindow;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import javafx.scene.layout.StackPane;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FontLoader;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.UpdateChecker;
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@MainWindowScoped
public class MainWindowController implements FxController {
private static final String TITLE_FONT = "/css/dosis-bold.ttf";
private static final Logger LOG = LoggerFactory.getLogger(MainWindowController.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private final Stage window;
private final FxApplication application;
private final boolean minimizeToSysTray;
private final UpdateChecker updateChecker;
private final BooleanBinding updateAvailable;
private final VaultListManager vaultListManager;
private final WrongFileAlertComponent.Builder wrongFileAlert;
public HBox titleBar;
public VBox root;
public Pane dragAndDropIndicator;
public Region resizer;
private double xOffset;
private double yOffset;
private final BooleanProperty draggingOver = new SimpleBooleanProperty();
private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
public StackPane root;
@Inject
public MainWindowController(@MainWindow Stage window, FxApplication application, @Named("trayMenuSupported") boolean minimizeToSysTray, UpdateChecker updateChecker, VaultListManager vaultListManager, WrongFileAlertComponent.Builder wrongFileAlert) {
this.window = window;
this.application = application;
this.minimizeToSysTray = minimizeToSysTray;
this.updateChecker = updateChecker;
this.updateAvailable = updateChecker.latestVersionProperty().isNotNull();
public MainWindowController(VaultListManager vaultListManager, WrongFileAlertComponent.Builder wrongFileAlert) {
this.vaultListManager = vaultListManager;
this.wrongFileAlert = wrongFileAlert;
}
@@ -64,86 +41,70 @@ public class MainWindowController implements FxController {
@FXML
public void initialize() {
LOG.debug("init MainWindowController");
loadFont(TITLE_FONT);
titleBar.setOnMousePressed(event -> {
xOffset = event.getSceneX();
yOffset = event.getSceneY();
});
titleBar.setOnMouseDragged(event -> {
window.setX(event.getScreenX() - xOffset);
window.setY(event.getScreenY() - yOffset);
});
resizer.setOnMouseDragged(event -> {
// we know for a fact that window is borderless. i.e. the scene starts at 0/0 of the window.
window.setWidth(event.getSceneX());
window.setHeight(event.getSceneY());
});
updateChecker.automaticallyCheckForUpdatesIfEnabled();
dragAndDropIndicator.setVisible(false);
root.setOnDragOver(event -> {
if (event.getGestureSource() != root && event.getDragboard().hasFiles()) {
/* allow for both copying and moving, whatever user chooses */
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
dragAndDropIndicator.setVisible(true);
}
event.consume();
});
root.setOnDragExited(event -> dragAndDropIndicator.setVisible(false));
root.setOnDragDropped(event -> {
if (event.getGestureSource() != root && event.getDragboard().hasFiles()) {
/* allow for both copying and moving, whatever user chooses */
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
Collection<Vault> vaultPaths = event.getDragboard().getFiles().stream().map(File::toPath).flatMap(this::addVault).collect(Collectors.toSet());
if (vaultPaths.isEmpty()) {
wrongFileAlert.build().showWrongFileAlertWindow();
}
}
event.consume();
});
root.setOnDragEntered(this::handleDragEvent);
root.setOnDragOver(this::handleDragEvent);
root.setOnDragDropped(this::handleDragEvent);
root.setOnDragExited(this::handleDragEvent);
}
private Stream<Vault> addVault(Path pathToVault) {
private void handleDragEvent(DragEvent event) {
if (DragEvent.DRAG_ENTERED.equals(event.getEventType()) && event.getGestureSource() == null) {
draggingOver.set(true);
} else if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.ANY);
draggingVaultOver.set(event.getDragboard().getFiles().stream().map(File::toPath).anyMatch(this::containsVault));
} else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
Set<Path> vaultPaths = event.getDragboard().getFiles().stream().map(File::toPath).filter(this::containsVault).collect(Collectors.toSet());
if (vaultPaths.isEmpty()) {
wrongFileAlert.build().showWrongFileAlertWindow();
} else {
vaultPaths.forEach(this::addVault);
}
event.setDropCompleted(!vaultPaths.isEmpty());
event.consume();
} else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) {
draggingOver.set(false);
draggingVaultOver.set(false);
}
}
private boolean containsVault(Path path) {
if (path.getFileName().toString().equals(MASTERKEY_FILENAME)) {
return true;
} else if (Files.isDirectory(path) && Files.exists(path.resolve(MASTERKEY_FILENAME))) {
return true;
} else {
return false;
}
}
private void addVault(Path pathToVault) {
try {
if (pathToVault.getFileName().toString().equals(MASTERKEY_FILENAME)) {
return Stream.of(vaultListManager.add(pathToVault.getParent()));
vaultListManager.add(pathToVault.getParent());
} else {
return Stream.of(vaultListManager.add(pathToVault));
vaultListManager.add(pathToVault);
}
} catch (NoSuchFileException e) {
LOG.debug("Not a vault: {}", pathToVault);
}
return Stream.empty();
}
private void loadFont(String resourcePath) {
try {
FontLoader.load(resourcePath);
} catch (FontLoader.FontLoaderException e) {
LOG.warn("Error loading font from path: " + resourcePath, e);
}
}
@FXML
public void close() {
if (minimizeToSysTray) {
window.close();
} else {
window.setIconified(true);
}
}
@FXML
public void showPreferences() {
application.showPreferencesWindow();
}
/* Getter/Setter */
public BooleanBinding updateAvailableProperty() {
return updateAvailable;
public BooleanProperty draggingOverProperty() {
return draggingOver;
}
public boolean isUpdateAvailable() {
return updateAvailable.get();
public boolean isDraggingOver() {
return draggingOver.get();
}
public BooleanProperty draggingVaultOverProperty() {
return draggingVaultOver;
}
public boolean isDraggingVaultOver() {
return draggingVaultOver.get();
}
}

View File

@@ -12,6 +12,7 @@ import javafx.scene.input.KeyCombination;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
@@ -24,8 +25,8 @@ import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
import javax.inject.Named;
import javax.inject.Provider;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, WrongFileAlertComponent.class})
@@ -34,14 +35,14 @@ abstract class MainWindowModule {
@Provides
@MainWindow
@MainWindowScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, resourceBundle);
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, MainWindowSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@MainWindow
@MainWindowScoped
static Stage provideStage(@Named("windowIcon") Optional<Image> windowIcon) {
static Stage provideStage(@Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage(StageStyle.UNDECORATED);
// TODO: min/max values chosen arbitrarily. We might wanna take a look at the user's resolution...
stage.setMinWidth(650);
@@ -49,7 +50,7 @@ abstract class MainWindowModule {
stage.setMaxWidth(1000);
stage.setMaxHeight(700);
stage.setTitle("Cryptomator");
windowIcon.ifPresent(stage.getIcons()::add);
stage.getIcons().addAll(windowIcons);
return stage;
}
@@ -57,15 +58,7 @@ abstract class MainWindowModule {
@FxmlScene(FxmlFile.MAIN_WINDOW)
@MainWindowScoped
static Scene provideMainScene(@MainWindow FXMLLoaderFactory fxmlLoaders, MainWindowController mainWindowController, VaultListController vaultListController) {
Scene scene = fxmlLoaders.createScene("/fxml/main_window.fxml");
// still not perfect... cant't we have a global menubar via the AWT tray app?
KeyCombination cmdN = new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN);
KeyCombination cmdW = new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN);
scene.getAccelerators().put(cmdN, vaultListController::didClickAddVault);
scene.getAccelerators().put(cmdW, mainWindowController::close);
return scene;
return fxmlLoaders.createScene("/fxml/main_window.fxml");
}
// ------------------
@@ -75,6 +68,16 @@ abstract class MainWindowModule {
@FxControllerKey(MainWindowController.class)
abstract FxController bindMainWindowController(MainWindowController controller);
@Binds
@IntoMap
@FxControllerKey(MainWindowTitleController.class)
abstract FxController bindMainWindowTitleController(MainWindowTitleController controller);
@Binds
@IntoMap
@FxControllerKey(ResizeController.class)
abstract FxController bindResizeController(ResizeController controller);
@Binds
@IntoMap
@FxControllerKey(VaultListController.class)

View File

@@ -0,0 +1,39 @@
package org.cryptomator.ui.mainwindow;
import dagger.Lazy;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.ui.common.DefaultSceneFactory;
import javax.inject.Inject;
@MainWindowScoped
public class MainWindowSceneFactory extends DefaultSceneFactory {
protected static final KeyCodeCombination SHORTCUT_N = new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN);
private final Lazy<MainWindowTitleController> mainWindowTitleController;
private final Lazy<VaultListController> vaultListController;
@Inject
public MainWindowSceneFactory(Settings settings, Lazy<MainWindowTitleController> mainWindowTitleController, Lazy<VaultListController> vaultListController) {
super(settings);
this.mainWindowTitleController = mainWindowTitleController;
this.vaultListController = vaultListController;
}
@Override
protected void setupDefaultAccelerators(Scene scene, Stage stage) {
if (SystemUtils.IS_OS_WINDOWS) {
scene.getAccelerators().put(ALT_F4, mainWindowTitleController.get()::close);
} else {
scene.getAccelerators().put(SHORTCUT_W, mainWindowTitleController.get()::close);
}
scene.getAccelerators().put(SHORTCUT_N, vaultListController.get()::didClickAddVault);
}
}

View File

@@ -0,0 +1,107 @@
package org.cryptomator.ui.mainwindow;
import javafx.beans.binding.BooleanBinding;
import javafx.fxml.FXML;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.UpdateChecker;
import org.cryptomator.ui.launcher.AppLifecycleListener;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
@MainWindowScoped
public class MainWindowTitleController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(MainWindowTitleController.class);
public HBox titleBar;
private final AppLifecycleListener appLifecycle;
private final Stage window;
private final FxApplication application;
private final boolean minimizeToSysTray;
private final UpdateChecker updateChecker;
private final BooleanBinding updateAvailable;
private final LicenseHolder licenseHolder;
private double xOffset;
private double yOffset;
@Inject
MainWindowTitleController(AppLifecycleListener appLifecycle, @MainWindow Stage window, FxApplication application, @Named("trayMenuSupported") boolean minimizeToSysTray, UpdateChecker updateChecker, LicenseHolder licenseHolder) {
this.appLifecycle = appLifecycle;
this.window = window;
this.application = application;
this.minimizeToSysTray = minimizeToSysTray;
this.updateChecker = updateChecker;
this.updateAvailable = updateChecker.latestVersionProperty().isNotNull();
this.licenseHolder = licenseHolder;
}
@FXML
public void initialize() {
LOG.debug("init MainWindowTitleController");
updateChecker.automaticallyCheckForUpdatesIfEnabled();
titleBar.setOnMousePressed(event -> {
xOffset = event.getSceneX();
yOffset = event.getSceneY();
});
titleBar.setOnMouseDragged(event -> {
window.setX(event.getScreenX() - xOffset);
window.setY(event.getScreenY() - yOffset);
});
window.setOnCloseRequest(event -> {
close();
event.consume();
});
}
@FXML
public void close() {
if (minimizeToSysTray) {
window.close();
} else {
appLifecycle.quit();
}
}
@FXML
public void minimize() {
window.setIconified(true);
}
@FXML
public void showPreferences() {
application.showPreferencesWindow(SelectedPreferencesTab.ANY);
}
@FXML
public void showDonationKeyPreferences() {
application.showPreferencesWindow(SelectedPreferencesTab.DONATION_KEY);
}
/* Getter/Setter */
public LicenseHolder getLicenseHolder() {
return licenseHolder;
}
public BooleanBinding updateAvailableProperty() {
return updateAvailable;
}
public boolean isUpdateAvailable() {
return updateAvailable.get();
}
public boolean isMinimizeToSysTray() {
return minimizeToSysTray;
}
}

View File

@@ -0,0 +1,102 @@
package org.cryptomator.ui.mainwindow;
import javafx.fxml.FXML;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
@MainWindow
public class ResizeController implements FxController {
private final Stage window;
public Region tlResizer;
public Region trResizer;
public Region blResizer;
public Region brResizer;
private double origX, origY, origW, origH;
@Inject
ResizeController(@MainWindow Stage window) {
this.window = window;
// TODO inject settings and save current position and size
}
@FXML
public void initialize() {
tlResizer.setOnMousePressed(this::startResize);
trResizer.setOnMousePressed(this::startResize);
blResizer.setOnMousePressed(this::startResize);
brResizer.setOnMousePressed(this::startResize);
tlResizer.setOnMouseDragged(this::resizeTopLeft);
trResizer.setOnMouseDragged(this::resizeTopRight);
blResizer.setOnMouseDragged(this::resizeBottomLeft);
brResizer.setOnMouseDragged(this::resizeBottomRight);
}
private void startResize(MouseEvent evt) {
origX = window.getX();
origY = window.getY();
origW = window.getWidth();
origH = window.getHeight();
}
private void resizeTopLeft(MouseEvent evt) {
resizeTop(evt);
resizeLeft(evt);
}
private void resizeTopRight(MouseEvent evt) {
resizeTop(evt);
resizeRight(evt);
}
private void resizeBottomLeft(MouseEvent evt) {
resizeBottom(evt);
resizeLeft(evt);
}
private void resizeBottomRight(MouseEvent evt) {
resizeBottom(evt);
resizeRight(evt);
}
private void resizeTop(MouseEvent evt) {
double newY = evt.getScreenY();
double dy = newY - origY;
double newH = origH - dy;
if (newH < window.getMaxHeight() && newH > window.getMinHeight()) {
window.setY(newY);
window.setHeight(newH);
}
}
private void resizeLeft(MouseEvent evt) {
double newX = evt.getScreenX();
double dx = newX - origX;
double newW = origW - dx;
if (newW < window.getMaxWidth() && newW > window.getMinWidth()) {
window.setX(newX);
window.setWidth(newW);
}
}
private void resizeBottom(MouseEvent evt) {
double newH = evt.getSceneY();
if (newH < window.getMaxHeight() && newH > window.getMinHeight()) {
window.setHeight(newH);
}
}
private void resizeRight(MouseEvent evt) {
double newW = evt.getSceneX();
if (newW < window.getMaxWidth() && newW > window.getMinWidth()) {
window.setWidth(newW);
}
}
}

View File

@@ -33,11 +33,11 @@ public class VaultDetailController implements FxController {
private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
switch (state) {
case LOCKED:
return FontAwesome5Icon.LOCK_ALT;
return FontAwesome5Icon.LOCK;
case PROCESSING:
return FontAwesome5Icon.SPINNER;
case UNLOCKED:
return FontAwesome5Icon.LOCK_OPEN_ALT;
return FontAwesome5Icon.LOCK_OPEN;
default:
return FontAwesome5Icon.EXCLAMATION_TRIANGLE;
}

View File

@@ -4,53 +4,32 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.Tasks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.cryptomator.ui.common.VaultService;
import javax.inject.Inject;
import java.util.concurrent.ExecutorService;
@MainWindowScoped
public class VaultDetailUnlockedController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(VaultDetailUnlockedController.class);
private final ReadOnlyObjectProperty<Vault> vault;
private final ExecutorService executor;
private final VaultService vaultService;
@Inject
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, ExecutorService executor) {
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, VaultService vaultService) {
this.vault = vault;
this.executor = executor;
this.vaultService = vaultService;
}
@FXML
public void revealAccessLocation() {
try {
vault.get().reveal();
} catch (Volume.VolumeException e) {
LOG.error("Failed to reveal vault.", e);
}
vaultService.reveal(vault.get());
}
@FXML
public void lock() {
Vault v = vault.get();
v.setState(VaultState.PROCESSING);
Tasks.create(() -> {
v.lock(false);
}).onSuccess(() -> {
LOG.trace("Regular unmount succeeded.");
v.setState(VaultState.LOCKED);
}).onError(Exception.class, e -> {
v.setState(VaultState.UNLOCKED);
LOG.error("Regular unmount failed.", e);
// TODO
}).runOnce(executor);
vaultService.lock(vault.get(), false);
// TODO count lock attempts, and allow forced lock
}
/* Getter/Setter */

View File

@@ -25,11 +25,11 @@ public class VaultListCellController implements FxController {
private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
switch (state) {
case LOCKED:
return FontAwesome5Icon.LOCK_ALT;
return FontAwesome5Icon.LOCK;
case PROCESSING:
return FontAwesome5Icon.SPINNER;
case UNLOCKED:
return FontAwesome5Icon.LOCK_OPEN_ALT;
return FontAwesome5Icon.LOCK_OPEN;
default:
return FontAwesome5Icon.EXCLAMATION_TRIANGLE;
}

View File

@@ -1,14 +1,18 @@
package org.cryptomator.ui.mainwindow;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
@@ -17,12 +21,12 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
//TODO: Add check if a vault in the list is invalid and add notification & controller
@MainWindowScoped
public class VaultListController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(VaultListController.class);
private final Stage window;
private final ObservableList<Vault> vaults;
private final ObjectProperty<Vault> selectedVault;
private final VaultListCellFactory cellFactory;
@@ -33,8 +37,7 @@ public class VaultListController implements FxController {
public ListView<Vault> vaultList;
@Inject
VaultListController(@MainWindow Stage window, ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) {
this.window = window;
VaultListController(ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) {
this.vaults = vaults;
this.selectedVault = selectedVault;
this.cellFactory = cellFactory;
@@ -42,6 +45,7 @@ public class VaultListController implements FxController {
this.removeVault = removeVault;
this.noVaultSelected = selectedVault.isNull();
this.emptyVaultList = Bindings.isEmpty(vaults);
selectedVault.addListener(this::selectedVaultDidChange);
}
public void initialize() {
@@ -53,15 +57,28 @@ public class VaultListController implements FxController {
if (c.wasAdded()) {
Vault anyAddedVault = c.getAddedSubList().get(0);
vaultList.getSelectionModel().select(anyAddedVault);
window.setIconified(false);
window.show();
window.toFront();
window.requestFocus(); // TODO: this beeps on macOS if there is a modal child window...
}
}
});
}
private void selectedVaultDidChange(@SuppressWarnings("unused") ObservableValue<? extends Vault> observableValue, @SuppressWarnings("unused") Vault oldValue, Vault newValue) {
VaultState reportedState = newValue.getState();
switch (reportedState) {
case LOCKED:
case NEEDS_MIGRATION:
case MISSING:
VaultState determinedState = VaultListManager.determineVaultState(newValue.getPath());
newValue.setState(determinedState);
break;
case ERROR:
case UNLOCKED:
case PROCESSING:
default:
// no-op
}
}
@FXML
public void didClickAddVault() {
addVaultWizard.build().showAddVaultWizard();

View File

@@ -0,0 +1,57 @@
package org.cryptomator.ui.migration;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.ResourceBundle;
@MigrationScoped
public class MigrationCapabilityErrorController implements FxController {
private final Stage window;
private final ResourceBundle localization;
private final Lazy<Scene> startScene;
private final StringBinding missingCapabilityDescription;
private final ReadOnlyObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability;
@Inject
MigrationCapabilityErrorController(@MigrationWindow Stage window, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, ResourceBundle localization, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene) {
this.window = window;
this.missingCapability = missingCapability;
this.localization = localization;
this.startScene = startScene;
this.missingCapabilityDescription = Bindings.createStringBinding(this::getMissingCapabilityDescription, missingCapability);
}
@FXML
public void back() {
window.setScene(startScene.get());
}
/* Getters */
public StringBinding missingCapabilityDescriptionProperty() {
return missingCapabilityDescription;
}
public String getMissingCapabilityDescription() {
FileSystemCapabilityChecker.Capability c = missingCapability.get();
if (c != null) {
return localization.getString("migration.error.missingFileSystemCapabilities.reason." + c.name());
} else {
return null;
}
}
}

View File

@@ -0,0 +1,29 @@
package org.cryptomator.ui.migration;
import dagger.Lazy;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
@MigrationScoped
public class MigrationGenericErrorController implements FxController {
private final Stage window;
private final Lazy<Scene> startScene;
@Inject
MigrationGenericErrorController(@MigrationWindow Stage window, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene) {
this.window = window;
this.startScene = startScene;
}
@FXML
public void back() {
window.setScene(startScene.get());
}
}

View File

@@ -1,32 +1,30 @@
package org.cryptomator.ui.migration;
import dagger.Binds;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.IntoSet;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StackTraceController;
import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Named;
import javax.inject.Provider;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
@Module
abstract class MigrationModule {
@@ -34,33 +32,37 @@ abstract class MigrationModule {
@Provides
@MigrationWindow
@MigrationScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, resourceBundle);
}
@Provides
@MigrationWindow
@MigrationScoped
static Map<KeyCodeCombination, Runnable> provideDefaultAccellerators(@MigrationWindow Set<Map.Entry<KeyCombination, Runnable>> accelerators) {
return Map.ofEntries(accelerators.toArray(Map.Entry[]::new));
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@MigrationWindow
@MigrationScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon, @MigrationWindow Lazy<Map<KeyCodeCombination, Runnable>> accelerators) {
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("migration.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.sceneProperty().addListener(observable -> {
stage.getScene().getAccelerators().putAll(accelerators.get());
});
windowIcon.ifPresent(stage.getIcons()::add);
stage.getIcons().addAll(windowIcons);
return stage;
}
@Provides
@Named("genericErrorCause")
@MigrationScoped
static ObjectProperty<Throwable> provideGenericErrorCause() {
return new SimpleObjectProperty<>();
}
@Provides
@Named("capabilityErrorCause")
@MigrationScoped
static ObjectProperty<FileSystemCapabilityChecker.Capability> provideCapabilityErrorCause() {
return new SimpleObjectProperty<>();
}
@Provides
@FxmlScene(FxmlFile.MIGRATION_START)
@MigrationScoped
@@ -82,19 +84,21 @@ abstract class MigrationModule {
return fxmlLoaders.createScene("/fxml/migration_success.fxml");
}
// ------------------
@Provides
@FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR)
@MigrationScoped
static Scene provideMigrationCapabilityErrorScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_capability_error.fxml");
}
@Provides
@IntoSet
@MigrationWindow
static Map.Entry<KeyCombination, Runnable> provideCloseWindowShortcut(@MigrationWindow Stage window) {
if (SystemUtils.IS_OS_WINDOWS) {
return Map.entry(new KeyCodeCombination(KeyCode.F4, KeyCombination.ALT_DOWN), window::close);
} else {
return Map.entry(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN), window::close);
}
@FxmlScene(FxmlFile.MIGRATION_GENERIC_ERROR)
@MigrationScoped
static Scene provideMigrationGenericErrorScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_generic_error.fxml");
}
// ------------------
@Binds
@@ -112,4 +116,21 @@ abstract class MigrationModule {
@FxControllerKey(MigrationSuccessController.class)
abstract FxController bindMigrationSuccessController(MigrationSuccessController controller);
@Binds
@IntoMap
@FxControllerKey(MigrationCapabilityErrorController.class)
abstract FxController bindMigrationCapabilityErrorController(MigrationCapabilityErrorController controller);
@Binds
@IntoMap
@FxControllerKey(MigrationGenericErrorController.class)
abstract FxController bindMigrationGenericErrorController(MigrationGenericErrorController controller);
@Provides
@IntoMap
@FxControllerKey(StackTraceController.class)
static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty<Throwable> errorCause) {
return new StackTraceController(errorCause.get());
}
}

View File

@@ -1,26 +1,28 @@
package org.cryptomator.ui.migration;
import dagger.Lazy;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.WritableValue;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptofs.migration.Migrators;
import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException;
import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
@@ -30,36 +32,54 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@MigrationScoped
public class MigrationRunController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(MigrationRunController.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private static final long MIGRATION_PROGRESS_UPDATE_MILLIS = 50;
private final Stage window;
private final Vault vault;
private final ExecutorService executor;
private final ScheduledExecutorService scheduler;
private final Optional<KeychainAccess> keychainAccess;
private final ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability;
private final ObjectProperty<Throwable> errorCause;
private final Lazy<Scene> startScene;
private final Lazy<Scene> successScene;
private final ObjectBinding<ContentDisplay> migrateButtonContentDisplay;
private final Lazy<Scene> capabilityErrorScene;
private final Lazy<Scene> genericErrorScene;
private final BooleanProperty migrationButtonDisabled;
private final DoubleProperty migrationProgress;
private volatile double volatileMigrationProgress = -1.0;
public NiceSecurePasswordField passwordField;
@Inject
public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, Optional<KeychainAccess> keychainAccess, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene) {
public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, Optional<KeychainAccess> keychainAccess, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, @Named("genericErrorCause") ObjectProperty<Throwable> errorCause, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy<Scene> capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_GENERIC_ERROR) Lazy<Scene> genericErrorScene) {
this.window = window;
this.vault = vault;
this.executor = executor;
this.scheduler = scheduler;
this.keychainAccess = keychainAccess;
this.missingCapability = missingCapability;
this.errorCause = errorCause;
this.startScene = startScene;
this.successScene = successScene;
this.migrateButtonContentDisplay = Bindings.createObjectBinding(this::getMigrateButtonContentDisplay, vault.stateProperty());
this.capabilityErrorScene = capabilityErrorScene;
this.genericErrorScene = genericErrorScene;
this.migrationButtonDisabled = new SimpleBooleanProperty();
this.migrationProgress = new SimpleDoubleProperty(volatileMigrationProgress);
}
public void initialize() {
@@ -79,35 +99,62 @@ public class MigrationRunController implements FxController {
LOG.info("Migrating vault {}", vault.getPath());
CharSequence password = passwordField.getCharacters();
vault.setState(VaultState.PROCESSING);
ScheduledFuture<?> progressSyncTask = scheduler.scheduleAtFixedRate(() -> {
Platform.runLater(() -> {
migrationProgress.set(volatileMigrationProgress);
});
}, 0, MIGRATION_PROGRESS_UPDATE_MILLIS, TimeUnit.MILLISECONDS);
Tasks.create(() -> {
Migrators migrators = Migrators.get();
migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password);
migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password, this::migrationProgressChanged);
return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME);
}).onSuccess(needsAnotherMigration -> {
if (needsAnotherMigration) {
LOG.info("Migration of '{}' succeeded, but another migration is required.", vault.getDisplayableName());
vault.setState(VaultState.NEEDS_MIGRATION);
} else {
LOG.info("Migration of '{}' succeeded.", vault.getDisplayableName());
vault.setState(VaultState.LOCKED);
passwordField.swipe();
LOG.info("Migration of '{}' succeeded.", vault.getDisplayableName());
window.setScene(successScene.get());
}
}).onError(InvalidPassphraseException.class, e -> {
shakeWindow();
Animations.createShakeWindowAnimation(window).play();
passwordField.selectAll();
passwordField.requestFocus();
vault.setState(VaultState.NEEDS_MIGRATION);
}).onError(NoApplicableMigratorException.class, e -> {
LOG.error("Can not migrate vault.", e);
}).onError(FileSystemCapabilityChecker.MissingCapabilityException.class, e -> {
LOG.error("Underlying file system not supported.", e);
vault.setState(VaultState.ERROR);
// TODO show specific error screen
missingCapability.set(e.getMissingCapability());
window.setScene(capabilityErrorScene.get());
}).onError(Exception.class, e -> { // including RuntimeExceptions
LOG.error("Migration failed for technical reasons.", e);
vault.setState(VaultState.ERROR);
// TODO show generic error screen
vault.setState(VaultState.NEEDS_MIGRATION);
errorCause.set(e);
window.setScene(genericErrorScene.get());
}).andFinally(() -> {
progressSyncTask.cancel(true);
}).runOnce(executor);
}
// Called by a background task. We can not directly modify observable properties from here
private void migrationProgressChanged(MigrationProgressListener.ProgressState state, double progress) {
switch (state) {
case INITIALIZING:
volatileMigrationProgress = -1.0;
break;
case MIGRATING:
volatileMigrationProgress = progress;
break;
case FINALIZING:
volatileMigrationProgress = 1.0;
break;
default:
throw new IllegalStateException("Unexpted state " + state);
}
}
private void loadStoredPassword() {
assert keychainAccess.isPresent();
char[] storedPw = null;
@@ -126,33 +173,6 @@ public class MigrationRunController implements FxController {
}
}
/* Animations */
private void shakeWindow() {
WritableValue<Double> writableWindowX = new WritableValue<>() {
@Override
public Double getValue() {
return window.getX();
}
@Override
public void setValue(Double value) {
window.setX(value);
}
};
Timeline timeline = new Timeline( //
new KeyFrame(Duration.ZERO, new KeyValue(writableWindowX, window.getX())), //
new KeyFrame(new Duration(100), new KeyValue(writableWindowX, window.getX() - 22.0)), //
new KeyFrame(new Duration(200), new KeyValue(writableWindowX, window.getX() + 18.0)), //
new KeyFrame(new Duration(300), new KeyValue(writableWindowX, window.getX() - 14.0)), //
new KeyFrame(new Duration(400), new KeyValue(writableWindowX, window.getX() + 10.0)), //
new KeyFrame(new Duration(500), new KeyValue(writableWindowX, window.getX() - 6.0)), //
new KeyFrame(new Duration(600), new KeyValue(writableWindowX, window.getX() + 2.0)), //
new KeyFrame(new Duration(700), new KeyValue(writableWindowX, window.getX())) //
);
timeline.play();
}
/* Getter/Setter */
public Vault getVault() {
@@ -180,4 +200,12 @@ public class MigrationRunController implements FxController {
}
}
public ReadOnlyDoubleProperty migrationProgressProperty() {
return migrationProgress;
}
public double getMigrationProgress() {
return migrationProgress.get();
}
}

View File

@@ -0,0 +1,43 @@
package org.cryptomator.ui.preferences;
import org.cryptomator.jni.MacFunctions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
class AutoStartMacStrategy implements AutoStartStrategy {
private static final Logger LOG = LoggerFactory.getLogger(AutoStartMacStrategy.class);
private final MacFunctions macFunctions;
public AutoStartMacStrategy(MacFunctions macFunctions) {
this.macFunctions = macFunctions;
}
@Override
public CompletionStage<Boolean> isAutoStartEnabled() {
boolean enabled = macFunctions.launchServices().isLoginItemEnabled();
return CompletableFuture.completedFuture(enabled);
}
@Override
public void enableAutoStart() throws TogglingAutoStartFailedException {
if (macFunctions.launchServices().enableLoginItem()) {
LOG.debug("Added login item.");
} else {
throw new TogglingAutoStartFailedException("Failed to add login item.");
}
}
@Override
public void disableAutoStart() throws TogglingAutoStartFailedException {
if (macFunctions.launchServices().disableLoginItem()) {
LOG.debug("Removed login item.");
} else {
throw new TogglingAutoStartFailedException("Failed to remove login item.");
}
}
}

View File

@@ -0,0 +1,26 @@
package org.cryptomator.ui.preferences;
import dagger.Module;
import dagger.Provides;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.jni.MacFunctions;
import java.util.Optional;
@Module
abstract class AutoStartModule {
@Provides
@PreferencesScoped
public static Optional<AutoStartStrategy> provideAutoStartStrategy(Optional<MacFunctions> macFunctions) {
if (SystemUtils.IS_OS_MAC_OSX && macFunctions.isPresent()) {
return Optional.of(new AutoStartMacStrategy(macFunctions.get()));
} else if (SystemUtils.IS_OS_WINDOWS) {
Optional<String> exeName = ProcessHandle.current().info().command();
return exeName.map(AutoStartWinStrategy::new);
} else {
return Optional.empty();
}
}
}

View File

@@ -0,0 +1,24 @@
package org.cryptomator.ui.preferences;
import java.util.concurrent.CompletionStage;
public interface AutoStartStrategy {
CompletionStage<Boolean> isAutoStartEnabled();
void enableAutoStart() throws TogglingAutoStartFailedException;
void disableAutoStart() throws TogglingAutoStartFailedException;
class TogglingAutoStartFailedException extends Exception {
public TogglingAutoStartFailedException(String message) {
super(message);
}
public TogglingAutoStartFailedException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@@ -0,0 +1,91 @@
package org.cryptomator.ui.preferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
class AutoStartWinStrategy implements AutoStartStrategy {
private static final Logger LOG = LoggerFactory.getLogger(AutoStartWinStrategy.class);
private static final String HKCU_AUTOSTART_KEY = "\"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\"";
private static final String AUTOSTART_VALUE = "Cryptomator";
private final String exePath;
public AutoStartWinStrategy(String exePath) {
this.exePath = exePath;
}
@Override
public CompletionStage<Boolean> isAutoStartEnabled() {
ProcessBuilder regQuery = new ProcessBuilder("reg", "query", HKCU_AUTOSTART_KEY, //
"/v", AUTOSTART_VALUE);
try {
Process proc = regQuery.start();
return proc.onExit().thenApply(p -> p.exitValue() == 0);
} catch (IOException e) {
LOG.warn("Failed to query {} from registry key {}", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY);
return CompletableFuture.completedFuture(false);
}
}
@Override
public void enableAutoStart() throws TogglingAutoStartFailedException {
ProcessBuilder regAdd = new ProcessBuilder("reg", "add", HKCU_AUTOSTART_KEY, //
"/v", AUTOSTART_VALUE, //
"/t", "REG_SZ", //
"/d", "\"" + exePath + "\"", //
"/f");
String command = regAdd.command().stream().collect(Collectors.joining(" "));
try {
Process proc = regAdd.start();
boolean finishedInTime = waitForProcess(proc, 5, TimeUnit.SECONDS);
if (finishedInTime) {
LOG.debug("Added {} to registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY);
} else {
throw new TogglingAutoStartFailedException("Adding registry value failed.");
}
} catch (IOException e) {
throw new TogglingAutoStartFailedException("Adding registry value failed. " + command, e);
}
}
@Override
public void disableAutoStart() throws TogglingAutoStartFailedException {
ProcessBuilder regRemove = new ProcessBuilder("reg", "delete", HKCU_AUTOSTART_KEY, //
"/v", AUTOSTART_VALUE, //
"/f");
String command = regRemove.command().stream().collect(Collectors.joining(" "));
try {
Process proc = regRemove.start();
boolean finishedInTime = waitForProcess(proc, 5, TimeUnit.SECONDS);
if (finishedInTime) {
LOG.debug("Removed {} from registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY);
} else {
throw new TogglingAutoStartFailedException("Removing registry value failed.");
}
} catch (IOException e) {
throw new TogglingAutoStartFailedException("Removing registry value failed. " + command, e);
}
}
private static boolean waitForProcess(Process proc, int timeout, TimeUnit timeUnit) {
boolean finishedInTime = false;
try {
finishedInTime = proc.waitFor(timeout, timeUnit);
} catch (InterruptedException e) {
LOG.error("Timeout while reading registry", e);
Thread.currentThread().interrupt();
} finally {
if (!finishedInTime) {
proc.destroyForcibly();
}
}
return finishedInTime;
}
}

View File

@@ -0,0 +1,45 @@
package org.cryptomator.ui.preferences;
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.TextArea;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
@PreferencesScoped
public class DonationKeyPreferencesController implements FxController {
private static final String DONATION_URI = "https://store.cryptomator.org/desktop";
private final Application application;
private final LicenseHolder licenseHolder;
public TextArea donationKeyField;
@Inject
DonationKeyPreferencesController(Application application, LicenseHolder licenseHolder) {
this.application = application;
this.licenseHolder = licenseHolder;
}
@FXML
public void initialize() {
donationKeyField.setText(licenseHolder.getLicenseKey().orElse(null));
donationKeyField.textProperty().addListener(this::registrationKeyChanged);
}
private void registrationKeyChanged(@SuppressWarnings("unused") ObservableValue<? extends String> observable, @SuppressWarnings("unused") String oldValue, String newValue) {
licenseHolder.validateAndStoreLicense(newValue);
}
@FXML
public void getDonationKey() {
application.getHostServices().showDocument(DONATION_URI);
}
public LicenseHolder getLicenseHolder() {
return licenseHolder;
}
}

View File

@@ -1,27 +1,56 @@
package org.cryptomator.ui.preferences;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.geometry.NodeOrientation;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.util.StringConverter;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
@PreferencesScoped
public class GeneralPreferencesController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(GeneralPreferencesController.class);
private final Settings settings;
private final boolean trayMenuSupported;
private final Optional<AutoStartStrategy> autoStartStrategy;
private final LicenseHolder licenseHolder;
private final ExecutorService executor;
public ChoiceBox<UiTheme> themeChoiceBox;
public CheckBox startHiddenCheckbox;
public CheckBox debugModeCheckbox;
public CheckBox autoStartCheckbox;
public ToggleGroup nodeOrientation;
public RadioButton nodeOrientationLtr;
public RadioButton nodeOrientationRtl;
@Inject
GeneralPreferencesController(Settings settings) {
GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartStrategy> autoStartStrategy, LicenseHolder licenseHolder, ExecutorService executor) {
this.settings = settings;
this.trayMenuSupported = trayMenuSupported;
this.autoStartStrategy = autoStartStrategy;
this.licenseHolder = licenseHolder;
this.executor = executor;
}
@FXML
public void initialize() {
themeChoiceBox.getItems().addAll(UiTheme.values());
themeChoiceBox.valueProperty().bindBidirectional(settings.theme());
@@ -30,6 +59,48 @@ public class GeneralPreferencesController implements FxController {
startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden());
debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode());
autoStartStrategy.ifPresent(autoStart -> {
autoStart.isAutoStartEnabled().thenAccept(enabled -> {
Platform.runLater(() -> autoStartCheckbox.setSelected(enabled));
});
});
nodeOrientationLtr.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.LEFT_TO_RIGHT);
nodeOrientationRtl.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.RIGHT_TO_LEFT);
nodeOrientation.selectedToggleProperty().addListener(this::toggleNodeOrientation);
}
public boolean isTrayMenuSupported() {
return this.trayMenuSupported;
}
public boolean isAutoStartSupported() {
return autoStartStrategy.isPresent();
}
private void toggleNodeOrientation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
if (nodeOrientationLtr.equals(newValue)) {
settings.userInterfaceOrientation().set(NodeOrientation.LEFT_TO_RIGHT);
} else if (nodeOrientationRtl.equals(newValue)) {
settings.userInterfaceOrientation().set(NodeOrientation.RIGHT_TO_LEFT);
} else {
LOG.warn("Unexpected toggle option {}", newValue);
}
}
@FXML
public void toggleAutoStart() {
autoStartStrategy.ifPresent(autoStart -> {
boolean enableAutoStart = autoStartCheckbox.isSelected();
Task<Void> toggleTask = new ToggleAutoStartTask(autoStart, enableAutoStart);
toggleTask.setOnFailed(evt -> autoStartCheckbox.setSelected(!enableAutoStart)); // restore previous state
executor.execute(toggleTask);
});
}
public LicenseHolder getLicenseHolder() {
return licenseHolder;
}
/* Helper classes */
@@ -47,4 +118,25 @@ public class GeneralPreferencesController implements FxController {
}
}
private static class ToggleAutoStartTask extends Task<Void> {
private final AutoStartStrategy autoStart;
private final boolean enable;
public ToggleAutoStartTask(AutoStartStrategy autoStart, boolean enable) {
this.autoStart = autoStart;
this.enable = enable;
}
@Override
protected Void call() throws Exception {
if (enable) {
autoStart.enableAutoStart();
} else {
autoStart.disableAutoStart();
}
return null;
}
}
}

View File

@@ -7,9 +7,9 @@ package org.cryptomator.ui.preferences;
import dagger.Lazy;
import dagger.Subcomponent;
import javafx.beans.property.ObjectProperty;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
@@ -23,7 +23,10 @@ public interface PreferencesComponent {
@FxmlScene(FxmlFile.PREFERENCES)
Lazy<Scene> scene();
default Stage showPreferencesWindow() {
ObjectProperty<SelectedPreferencesTab> selectedTabProperty();
default Stage showPreferencesWindow(SelectedPreferencesTab selectedTab) {
selectedTabProperty().set(selectedTab);
Stage stage = window();
stage.setScene(scene().get());
stage.show();

View File

@@ -1,6 +1,9 @@
package org.cryptomator.ui.preferences;
import javafx.beans.Observable;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
@@ -15,26 +18,50 @@ import javax.inject.Inject;
public class PreferencesController implements FxController {
private final Stage window;
private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
private final BooleanBinding updateAvailable;
public TabPane tabPane;
public Tab generalTab;
public Tab volumeTab;
public Tab updatesTab;
public Tab donationKeyTab;
@Inject
public PreferencesController(@PreferencesWindow Stage window, UpdateChecker updateChecker) {
public PreferencesController(@PreferencesWindow Stage window, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, UpdateChecker updateChecker) {
this.window = window;
this.selectedTabProperty = selectedTabProperty;
this.updateAvailable = updateChecker.latestVersionProperty().isNotNull();
}
@FXML
public void initialize() {
window.setOnShowing(this::windowWillAppear);
selectedTabProperty.addListener(observable -> this.selectChosenTab());
}
private void windowWillAppear(@SuppressWarnings("unused") WindowEvent windowEvent) {
if (updateAvailable.get()) {
tabPane.getSelectionModel().select(updatesTab);
private void selectChosenTab() {
Tab toBeSelected = getTabToSelect(selectedTabProperty.get());
tabPane.getSelectionModel().select(toBeSelected);
}
private Tab getTabToSelect(SelectedPreferencesTab selectedTab) {
switch (selectedTab) {
case UPDATES:
return updatesTab;
case VOLUME:
return volumeTab;
case DONATION_KEY:
return donationKeyTab;
case GENERAL:
return generalTab;
case ANY:
default:
return updateAvailable.get() ? updatesTab : generalTab;
}
}
private void windowWillAppear(@SuppressWarnings("unused") WindowEvent windowEvent) {
selectChosenTab();
}
}

View File

@@ -4,12 +4,12 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.stage.Stage;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
@@ -18,41 +18,42 @@ import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Named;
import javax.inject.Provider;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module
@Module(includes = {AutoStartModule.class})
abstract class PreferencesModule {
@Provides
@PreferencesWindow
@PreferencesScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, resourceBundle);
static ObjectProperty<SelectedPreferencesTab> provideSelectedTabProperty() {
return new SimpleObjectProperty<>(SelectedPreferencesTab.ANY);
}
@Provides
@PreferencesWindow
@PreferencesScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon) {
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@PreferencesWindow
@PreferencesScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("preferences.title"));
stage.setResizable(false);
windowIcon.ifPresent(stage.getIcons()::add);
stage.getIcons().addAll(windowIcons);
return stage;
}
@Provides
@FxmlScene(FxmlFile.PREFERENCES)
@PreferencesScoped
static Scene providePreferencesScene(@PreferencesWindow FXMLLoaderFactory fxmlLoaders, @PreferencesWindow Stage window) {
Scene scene = fxmlLoaders.createScene("/fxml/preferences.fxml");
KeyCombination cmdW = new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN);
scene.getAccelerators().put(cmdW, window::close);
return scene;
static Scene providePreferencesScene(@PreferencesWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/preferences.fxml");
}
// ------------------
@@ -77,4 +78,9 @@ abstract class PreferencesModule {
@FxControllerKey(VolumePreferencesController.class)
abstract FxController bindVolumePreferencesController(VolumePreferencesController controller);
@Binds
@IntoMap
@FxControllerKey(DonationKeyPreferencesController.class)
abstract FxController bindDonationKeyPreferencesController(DonationKeyPreferencesController controller);
}

View File

@@ -0,0 +1,28 @@
package org.cryptomator.ui.preferences;
public enum SelectedPreferencesTab {
/**
* Let the controller decide which tab to show.
*/
ANY,
/**
* Show general tab
*/
GENERAL,
/**
* Show volume tab
*/
VOLUME,
/**
* Show updates tab
*/
UPDATES,
/**
* Show donation key tab
*/
DONATION_KEY,
}

View File

@@ -16,6 +16,9 @@ import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
/**
* TODO: if WebDAV is selected under Windows, show warning that specific mount options (like selecting a directory as mount point) are _not_ supported
*/
@PreferencesScoped
public class VolumePreferencesController implements FxController {

View File

@@ -1,26 +1,22 @@
package org.cryptomator.ui.quit;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.awt.desktop.QuitResponse;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
@QuitScoped
public class QuitController implements FxController {
@@ -30,15 +26,17 @@ public class QuitController implements FxController {
private final Stage window;
private final QuitResponse response;
private final ObservableList<Vault> unlockedVaults;
private final ExecutorService executor;
private final ExecutorService executorService;
private final VaultService vaultService;
public Button lockAndQuitButton;
@Inject
QuitController(@QuitWindow Stage window, QuitResponse response, ObservableList<Vault> vaults, ExecutorService executor) {
QuitController(@QuitWindow Stage window, QuitResponse response, ObservableList<Vault> vaults, ExecutorService executorService, VaultService vaultService) {
this.window = window;
this.response = response;
this.unlockedVaults = vaults.filtered(Vault::isUnlocked);
this.executor = executor;
this.executorService = executorService;
this.vaultService = vaultService;
}
@FXML
@@ -52,84 +50,24 @@ public class QuitController implements FxController {
public void lockAndQuit() {
lockAndQuitButton.setDisable(true);
lockAndQuitButton.setContentDisplay(ContentDisplay.LEFT);
Iterator<Vault> toBeLocked = List.copyOf(unlockedVaults).iterator();
ScheduledService<Void> lockAllService = new LockAllVaultsService(executor, toBeLocked);
lockAllService.setOnSucceeded(evt -> {
if (!toBeLocked.hasNext()) {
Task<Collection<Vault>> lockAllTask = vaultService.createLockAllTask(unlockedVaults, false);
lockAllTask.setOnSucceeded(evt -> {
LOG.info("Locked {}", lockAllTask.getValue().stream().map(Vault::getDisplayableName).collect(Collectors.joining(", ")));
if (unlockedVaults.isEmpty()) {
window.close();
response.performQuit();
}
});
lockAllService.setOnFailed(evt -> {
lockAllTask.setOnFailed(evt -> {
LOG.warn("Locking failed", lockAllTask.getException());
lockAndQuitButton.setDisable(false);
lockAndQuitButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
// TODO: show force lock or force quit scene (and DO NOT cancelQuit() here!)
// see https://github.com/cryptomator/cryptomator/blob/1.4.16/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java#L151-L163
response.cancelQuit();
});
lockAllService.start();
executorService.execute(lockAllTask);
}
/**
* @param vault The vault to lock
* @return Task that tries to lock the given vault gracefully.
*/
private Task<Void> createGracefulLockTask(Vault vault) {
Task task = new Task<Void>() {
@Override
protected Void call() throws Volume.VolumeException {
vault.lock(false);
LOG.info("Locked {}", vault.getDisplayableName());
return null;
}
};
task.setOnScheduled(evt -> {
vault.setState(VaultState.PROCESSING);
});
task.setOnSucceeded(evt -> {
vault.setState(VaultState.LOCKED);
});
task.setOnFailed(evt -> {
LOG.warn("Failed to lock vault", vault);
});
return task;
}
/**
* @return Task that succeeds immediately
*/
private Task<Void> createNoopTask() {
return new Task<>() {
@Override
protected Void call() {
return null;
}
};
}
private class LockAllVaultsService extends ScheduledService<Void> {
private final Iterator<Vault> vaultsToLock;
public LockAllVaultsService(Executor executor, Iterator<Vault> vaultsToLock) {
this.vaultsToLock = vaultsToLock;
setExecutor(executor);
setRestartOnFailure(false);
}
@Override
protected Task<Void> createTask() {
assert Platform.isFxApplicationThread();
if (vaultsToLock.hasNext()) {
return createGracefulLockTask(vaultsToLock.next());
} else {
// This should be unreachable code, since vaultsToLock is only accessed on the FX App Thread.
// But if quitting the application takes longer for any reason, this service should shut down properly
reset();
return createNoopTask();
}
}
}
}

View File

@@ -11,6 +11,7 @@ import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
@@ -19,8 +20,8 @@ import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Named;
import javax.inject.Provider;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module
@@ -29,32 +30,27 @@ abstract class QuitModule {
@Provides
@QuitWindow
@QuitScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, resourceBundle);
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@QuitWindow
@QuitScoped
static Stage provideStage(@Named("windowIcon") Optional<Image> windowIcon) {
static Stage provideStage(@Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
stage.setMinWidth(300);
stage.setMinHeight(100);
stage.initModality(Modality.APPLICATION_MODAL);
windowIcon.ifPresent(stage.getIcons()::add);
stage.getIcons().addAll(windowIcons);
return stage;
}
@Provides
@FxmlScene(FxmlFile.QUIT)
@QuitScoped
static Scene provideUnlockScene(@QuitWindow FXMLLoaderFactory fxmlLoaders, @QuitWindow Stage window) {
Scene scene = fxmlLoaders.createScene("/fxml/quit.fxml");
KeyCombination cmdW = new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN);
scene.getAccelerators().put(cmdW, window::close);
return scene;
static Scene provideUnlockScene(@QuitWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/quit.fxml");
}
// ------------------

View File

@@ -0,0 +1,69 @@
package org.cryptomator.ui.recoverykey;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class AutoCompleter {
private final List<String> dictionary;
public AutoCompleter(Collection<String> dictionary) {
this.dictionary = unmodifiableSortedRandomAccessList(dictionary);
}
private static <T extends Comparable<T>> List<T> unmodifiableSortedRandomAccessList(Collection<T> items) {
List<T> result = new ArrayList<>(items);
Collections.sort(result);
return Collections.unmodifiableList(result);
}
public Optional<String> autocomplete(String prefix) {
if (Strings.isNullOrEmpty(prefix)) {
return Optional.empty();
}
int potentialMatchIdx = findIndexOfLexicographicallyPreceeding(0, dictionary.size(), prefix);
if (potentialMatchIdx < dictionary.size()) {
String potentialMatch = dictionary.get(potentialMatchIdx);
return potentialMatch.startsWith(prefix) ? Optional.of(potentialMatch) : Optional.empty();
} else {
return Optional.empty();
}
}
/**
* Find the index of the first word in {@link #dictionary} that starts with a given prefix.
*
* This method performs an "unsuccessful" binary search (it doesn't return when encountering an exact match).
* Instead it continues searching in the left half (which includes the exact match) until only one element is left.
*
* If the dictionary doesn't contain a word "left" of the given prefix, this method returns an invalid index, though.
*
* @param begin Index of first element (inclusive)
* @param end Index of last element (exclusive)
* @param prefix
* @return index between [0, dictLen], i.e. index can exceed the upper bounds of {@link #dictionary}.
*/
private int findIndexOfLexicographicallyPreceeding(int begin, int end, String prefix) {
if (begin >= end) {
return begin; // this is usually where a binary search ends "unsuccessful"
}
int mid = (begin + end) / 2;
String word = dictionary.get(mid);
if (prefix.compareTo(word) <= 0) { // prefix preceeds or matches word
// proceed in left half
assert mid < end;
return findIndexOfLexicographicallyPreceeding(0, mid, prefix);
} else {
// proceed in right half
assert mid >= begin;
return findIndexOfLexicographicallyPreceeding(mid + 1, end, prefix);
}
}
}

View File

@@ -0,0 +1,55 @@
package org.cryptomator.ui.recoverykey;
import dagger.BindsInstance;
import dagger.Lazy;
import dagger.Subcomponent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.annotation.Nullable;
import javax.inject.Named;
import java.util.Optional;
@RecoveryKeyScoped
@Subcomponent(modules = {RecoveryKeyModule.class})
public interface RecoveryKeyComponent {
@RecoveryKeyWindow
Stage window();
@FxmlScene(FxmlFile.RECOVERYKEY_CREATE)
Lazy<Scene> creationScene();
@FxmlScene(FxmlFile.RECOVERYKEY_RECOVER)
Lazy<Scene> recoverScene();
default void showRecoveryKeyCreationWindow() {
Stage stage = window();
stage.setScene(creationScene().get());
stage.sizeToScene();
stage.show();
}
default void showRecoveryKeyRecoverWindow() {
Stage stage = window();
stage.setScene(recoverScene().get());
stage.sizeToScene();
stage.show();
}
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder vault(@RecoveryKeyWindow Vault vault);
@BindsInstance
Builder owner(@Named("keyRecoveryOwner") Stage owner);
RecoveryKeyComponent build();
}
}

View File

@@ -0,0 +1,86 @@
package org.cryptomator.ui.recoverykey;
import dagger.Lazy;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
@RecoveryKeyScoped
public class RecoveryKeyCreationController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class);
private final Stage window;
private final Lazy<Scene> successScene;
private final Vault vault;
private final ExecutorService executor;
private final RecoveryKeyFactory recoveryKeyFactory;
private final StringProperty recoveryKeyProperty;
public NiceSecurePasswordField passwordField;
@Inject
public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy<Scene> successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey) {
this.window = window;
this.successScene = successScene;
this.vault = vault;
this.executor = executor;
this.recoveryKeyFactory = recoveryKeyFactory;
this.recoveryKeyProperty = recoveryKey;
}
@FXML
public void createRecoveryKey() {
Task<String> task = new RecoveryKeyCreationTask();
task.setOnScheduled(event -> {
LOG.debug("Creating recovery key for {}.", vault.getDisplayablePath());
});
task.setOnSucceeded(event -> {
String recoveryKey = task.getValue();
recoveryKeyProperty.set(recoveryKey);
window.setScene(successScene.get());
});
task.setOnFailed(event -> {
if (task.getException() instanceof InvalidPassphraseException) {
Animations.createShakeWindowAnimation(window).play();
} else {
LOG.error("Creation of recovery key failed.", task.getException());
}
});
executor.submit(task);
}
@FXML
public void close() {
window.close();
}
private class RecoveryKeyCreationTask extends Task<String> {
@Override
protected String call() throws IOException {
return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
}
}
/* Getter/Setter */
public Vault getVault() {
return vault;
}
}

View File

@@ -0,0 +1,99 @@
package org.cryptomator.ui.recoverykey;
import javafx.fxml.FXML;
import javafx.print.PageLayout;
import javafx.print.Printer;
import javafx.print.PrinterJob;
import javafx.scene.control.Button;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.text.Font;
import javafx.scene.text.FontSmoothingType;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ResourceBundle;
public class RecoveryKeyDisplayController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyDisplayController.class);
private final Stage window;
private final String vaultName;
private final String recoveryKey;
private final ResourceBundle localization;
public Button copyButton;
public RecoveryKeyDisplayController(Stage window, String vaultName, String recoveryKey, ResourceBundle localization) {
this.window = window;
this.vaultName = vaultName;
this.recoveryKey = recoveryKey;
this.localization = localization;
}
@FXML
public void printRecoveryKey() {
// TODO localize
PrinterJob job = PrinterJob.createPrinterJob();
if (job != null && job.showPrintDialog(window)) {
PageLayout pageLayout = job.getJobSettings().getPageLayout();
Text heading = new Text("Cryptomator Recovery Key\n" + vaultName + "\n");
heading.setFont(Font.font("serif", FontWeight.BOLD, 20));
heading.setFontSmoothingType(FontSmoothingType.LCD);
Text key = new Text(recoveryKey);
key.setFont(Font.font("serif", FontWeight.NORMAL, 16));
key.setFontSmoothingType(FontSmoothingType.GRAY);
TextFlow textFlow = new TextFlow();
textFlow.setPrefSize(pageLayout.getPrintableWidth(), pageLayout.getPrintableHeight());
textFlow.getChildren().addAll(heading, key);
textFlow.setLineSpacing(6);
if (job.printPage(textFlow)) {
LOG.info("Recovery key printed.");
job.endJob();
} else {
LOG.warn("Printing recovery key failed.");
}
} else {
LOG.info("Printing recovery key canceled by user.");
}
}
@FXML
public void copyRecoveryKey() {
ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(recoveryKey);
Clipboard.getSystemClipboard().setContent(clipboardContent);
LOG.info("Recovery key copied to clipboard.");
copyButton.setText(localization.getString("generic.button.copied"));
}
@FXML
public void close() {
window.close();
}
/* Getter/Setter */
public boolean isPrinterSupported() {
return Printer.getDefaultPrinter() != null;
}
public String getRecoveryKey() {
return recoveryKey;
}
public String getVaultName() {
return vaultName;
}
}

View File

@@ -0,0 +1,111 @@
package org.cryptomator.ui.recoverykey;
import com.google.common.base.Preconditions;
import com.google.common.hash.Hashing;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
@Singleton
public class RecoveryKeyFactory {
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private static final byte[] PEPPER = new byte[0];
private final WordEncoder wordEncoder;
@Inject
public RecoveryKeyFactory(WordEncoder wordEncoder) {
this.wordEncoder = wordEncoder;
}
public Collection<String> getDictionary() {
return wordEncoder.getWords();
}
/**
* @param vaultPath Path to the storage location of a vault
* @param password The vault's password
* @return The recovery key of the vault at the given path
* @throws IOException If the masterkey file could not be read
* @throws InvalidPassphraseException If the provided password is wrong
* @apiNote This is a long-running operation and should be invoked in a background thread
*/
public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException {
byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vaultPath, MASTERKEY_FILENAME, PEPPER, password);
try {
return createRecoveryKey(rawKey);
} finally {
Arrays.fill(rawKey, (byte) 0x00);
}
}
// visible for testing
String createRecoveryKey(byte[] rawKey) {
Preconditions.checkArgument(rawKey.length == 64, "key should be 64 bytes");
byte[] paddedKey = Arrays.copyOf(rawKey, 66);
try {
// copy 16 most significant bits of CRC32(rawKey) to the end of paddedKey:
Hashing.crc32().hashBytes(rawKey).writeBytesTo(paddedKey, 64, 2);
return wordEncoder.encodePadded(paddedKey);
} finally {
Arrays.fill(paddedKey, (byte) 0x00);
}
}
/**
* Creates a completely new masterkey using a recovery key.
* @param vaultPath Path to the storage location of a vault
* @param recoveryKey A recovery key for this vault
* @param newPassword The new password used to encrypt the keys
* @throws IOException If the masterkey file could not be written
* @throws IllegalArgumentException If the recoveryKey is invalid
* @apiNote This is a long-running operation and should be invoked in a background thread
*/
public void resetPasswordWithRecoveryKey(Path vaultPath, String recoveryKey, CharSequence newPassword) throws IOException, IllegalArgumentException {
final byte[] rawKey = decodeRecoveryKey(recoveryKey);
try {
CryptoFileSystemProvider.restoreRawKey(vaultPath, MASTERKEY_FILENAME, rawKey, PEPPER, newPassword);
} finally {
Arrays.fill(rawKey, (byte) 0x00);
}
}
/**
* Checks whether a String is a syntactically correct recovery key with a valid checksum
* @param recoveryKey A word sequence which might be a recovery key
* @return <code>true</code> if this seems to be a legitimate recovery key
*/
public boolean validateRecoveryKey(String recoveryKey) {
try {
byte[] key = decodeRecoveryKey(recoveryKey);
Arrays.fill(key, (byte) 0x00);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
private byte[] decodeRecoveryKey(String recoveryKey) throws IllegalArgumentException {
byte[] paddedKey = new byte[0];
try {
paddedKey = wordEncoder.decode(recoveryKey);
Preconditions.checkArgument(paddedKey.length == 66, "Recovery key doesn't consist of 66 bytes.");
byte[] rawKey = Arrays.copyOf(paddedKey, 64);
byte[] expectedCrc16 = Arrays.copyOfRange(paddedKey, 64, 66);
byte[] actualCrc32 = Hashing.crc32().hashBytes(rawKey).asBytes();
byte[] actualCrc16 = Arrays.copyOf(actualCrc32, 2);
Preconditions.checkArgument(Arrays.equals(expectedCrc16, actualCrc16), "Recovery key has invalid CRC.");
return rawKey;
} finally {
Arrays.fill(paddedKey, (byte) 0x00);
}
}
}

View File

@@ -0,0 +1,135 @@
package org.cryptomator.ui.recoverykey;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import javax.inject.Named;
import javax.inject.Provider;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
@Module
abstract class RecoveryKeyModule {
@Provides
@RecoveryKeyWindow
@RecoveryKeyScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@RecoveryKeyWindow
@RecoveryKeyScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons, @Named("keyRecoveryOwner") Stage owner) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("recoveryKey.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}
@Provides
@RecoveryKeyWindow
@RecoveryKeyScoped
static StringProperty provideRecoveryKeyProperty() {
return new SimpleStringProperty();
}
@Provides
@RecoveryKeyScoped
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
// ------------------
@Provides
@FxmlScene(FxmlFile.RECOVERYKEY_CREATE)
@RecoveryKeyScoped
static Scene provideRecoveryKeyCreationScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_create.fxml");
}
@Provides
@FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS)
@RecoveryKeyScoped
static Scene provideRecoveryKeySuccessScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_success.fxml");
}
@Provides
@FxmlScene(FxmlFile.RECOVERYKEY_RECOVER)
@RecoveryKeyScoped
static Scene provideRecoveryKeyRecoverScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_recover.fxml");
}
@Provides
@FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD)
@RecoveryKeyScoped
static Scene provideRecoveryKeyResetPasswordScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_reset_password.fxml");
}
// ------------------
@Binds
@IntoMap
@FxControllerKey(RecoveryKeyCreationController.class)
abstract FxController bindRecoveryKeyCreationController(RecoveryKeyCreationController controller);
@Provides
@IntoMap
@FxControllerKey(RecoveryKeyDisplayController.class)
static FxController provideRecoveryKeyDisplayController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey, ResourceBundle localization) {
return new RecoveryKeyDisplayController(window, vault.getDisplayableName(), recoveryKey.get(), localization);
}
@Binds
@IntoMap
@FxControllerKey(RecoveryKeyRecoverController.class)
abstract FxController provideRecoveryKeyRecoverController(RecoveryKeyRecoverController controller);
@Binds
@IntoMap
@FxControllerKey(RecoveryKeySuccessController.class)
abstract FxController bindRecoveryKeySuccessController(RecoveryKeySuccessController controller);
@Binds
@IntoMap
@FxControllerKey(RecoveryKeyResetPasswordController.class)
abstract FxController bindRecoveryKeyResetPasswordController(RecoveryKeyResetPasswordController controller);
@Provides
@IntoMap
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
}
}

View File

@@ -0,0 +1,116 @@
package org.cryptomator.ui.recoverykey;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
import java.util.Optional;
@RecoveryKeyScoped
public class RecoveryKeyRecoverController implements FxController {
private final static CharMatcher ALLOWED_CHARS = CharMatcher.inRange('a', 'z').or(CharMatcher.is(' '));
private final Stage window;
private final Vault vault;
private final StringProperty recoveryKey;
private final RecoveryKeyFactory recoveryKeyFactory;
private final BooleanBinding validRecoveryKey;
private final Lazy<Scene> resetPasswordScene;
private final AutoCompleter autoCompleter;
public TextArea textarea;
@Inject
public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene) {
this.window = window;
this.vault = vault;
this.recoveryKey = recoveryKey;
this.recoveryKeyFactory = recoveryKeyFactory;
this.validRecoveryKey = Bindings.createBooleanBinding(this::isValidRecoveryKey, recoveryKey);
this.resetPasswordScene = resetPasswordScene;
this.autoCompleter = new AutoCompleter(recoveryKeyFactory.getDictionary());
}
@FXML
public void initialize() {
recoveryKey.bind(textarea.textProperty());
}
private TextFormatter.Change filterTextChange(TextFormatter.Change change) {
if (Strings.isNullOrEmpty(change.getText())) {
// pass-through caret/selection changes that don't affect the text
return change;
}
if (!ALLOWED_CHARS.matchesAllOf(change.getText())) {
return null; // reject change
}
String text = change.getControlNewText();
int caretPos = change.getCaretPosition();
if (caretPos == text.length() || text.charAt(caretPos) == ' ') { // are we at the end of a word?
int beginOfWord = Math.max(text.substring(0, caretPos).lastIndexOf(' ') + 1, 0);
String currentWord = text.substring(beginOfWord, caretPos);
Optional<String> suggestion = autoCompleter.autocomplete(currentWord);
if (suggestion.isPresent()) {
String completion = suggestion.get().substring(currentWord.length());
change.setText(change.getText() + completion);
change.setAnchor(caretPos + completion.length());
}
}
return change;
}
@FXML
public void onKeyPressed(KeyEvent keyEvent) {
if (keyEvent.getCode() == KeyCode.TAB && textarea.getAnchor() > textarea.getCaretPosition()) {
// apply autocompletion:
int pos = textarea.getAnchor();
textarea.insertText(pos, " ");
textarea.positionCaret(pos + 1);
}
}
@FXML
public void close() {
window.close();
}
@FXML
public void recover() {
window.setScene(resetPasswordScene.get());
}
/* Getter/Setter */
public Vault getVault() {
return vault;
}
public BooleanBinding validRecoveryKeyProperty() {
return validRecoveryKey;
}
public boolean isValidRecoveryKey() {
return recoveryKeyFactory.validateRecoveryKey(recoveryKey.get());
}
public TextFormatter getRecoveryKeyTextFormatter() {
return new TextFormatter<>(this::filterTextChange);
}
}

View File

@@ -0,0 +1,94 @@
package org.cryptomator.ui.recoverykey;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
@RecoveryKeyScoped
public class RecoveryKeyResetPasswordController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyResetPasswordController.class);
private final Stage window;
private final Vault vault;
private final RecoveryKeyFactory recoveryKeyFactory;
private final ExecutorService executor;
private final StringProperty recoveryKey;
private final ObjectProperty<CharSequence> newPassword;
private final Lazy<Scene> recoverScene;
private final BooleanBinding invalidNewPassword;
@Inject
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @Named("newPassword")ObjectProperty<CharSequence> newPassword, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverScene) {
this.window = window;
this.vault = vault;
this.recoveryKeyFactory = recoveryKeyFactory;
this.executor = executor;
this.recoveryKey = recoveryKey;
this.newPassword = newPassword;
this.recoverScene = recoverScene;
this.invalidNewPassword = Bindings.createBooleanBinding(this::isInvalidNewPassword, newPassword);
}
@FXML
public void back() {
window.setScene(recoverScene.get());
}
@FXML
public void done() {
Task<Void> task = new ResetPasswordTask();
task.setOnScheduled(event -> {
LOG.debug("Using recovery key to reset password for {}.", vault.getDisplayablePath());
});
task.setOnSucceeded(event -> {
LOG.info("Used recovery key to reset password for {}.", vault.getDisplayablePath());
// TODO show success screen
window.close();
});
task.setOnFailed(event -> {
// TODO show generic error screen
LOG.error("Creation of recovery key failed.", task.getException());
});
executor.submit(task);
}
private class ResetPasswordTask extends Task<Void> {
@Override
protected Void call() throws IOException, IllegalArgumentException {
recoveryKeyFactory.resetPasswordWithRecoveryKey(vault.getPath(), recoveryKey.get(), newPassword.get());
return null;
}
}
/* Getter/Setter */
public BooleanBinding invalidNewPasswordProperty() {
return invalidNewPassword;
}
public boolean isInvalidNewPassword() {
return newPassword.get() == null || newPassword.get().length() == 0;
}
}

View File

@@ -0,0 +1,13 @@
package org.cryptomator.ui.recoverykey;
import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface RecoveryKeyScoped {
}

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