diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index b351fb381..e55829ac9 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,6 +1,6 @@
# These are supported funding model platforms
-github: [overheadhunter, tobihagemann] # 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
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 1119f53ee..89ecef4de 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -7,20 +7,20 @@
-
-
+
+
-
+
-
+
-
+
@@ -32,9 +32,9 @@
-
+
diff --git a/main/buildkit/pom.xml b/main/buildkit/pom.xml
index c08e82d9f..070e34c6a 100644
--- a/main/buildkit/pom.xml
+++ b/main/buildkit/pom.xml
@@ -4,7 +4,7 @@
org.cryptomator
main
- 1.5.0-beta2
+ 1.5.0-beta3
buildkit
pom
diff --git a/main/commons/pom.xml b/main/commons/pom.xml
index 5a00af424..3c75ea10f 100644
--- a/main/commons/pom.xml
+++ b/main/commons/pom.xml
@@ -4,7 +4,7 @@
org.cryptomator
main
- 1.5.0-beta2
+ 1.5.0-beta3
commons
Cryptomator Commons
diff --git a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java
index 795dc6dcd..bac0114da 100644
--- a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java
+++ b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java
@@ -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,12 +28,18 @@ 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;
@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
@@ -69,18 +76,38 @@ public abstract class CommonsModule {
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;
});
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
diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
index 9d3510dac..7bb7aefa0 100644
--- a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
+++ b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
@@ -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 {
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> scheduledSaveCmd = new AtomicReference<>();
private final AtomicReference 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 {
final Optional 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);
diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java b/main/commons/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java
index 6f1edfec2..c873e9f3f 100644
--- a/main/commons/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java
+++ b/main/commons/src/main/java/org/cryptomator/common/vaults/WindowsDriveLetters.java
@@ -41,7 +41,6 @@ public final class WindowsDriveLetters {
public Set getOccupiedDriveLetters() {
if (!SystemUtils.IS_OS_WINDOWS) {
- LOG.warn("Attempted to get occupied drive letters on non-Windows machine.");
return Set.of();
} else {
Iterable rootDirs = FileSystems.getDefault().getRootDirectories();
diff --git a/main/keychain/pom.xml b/main/keychain/pom.xml
index 568dfca7f..7b8157808 100644
--- a/main/keychain/pom.xml
+++ b/main/keychain/pom.xml
@@ -4,7 +4,7 @@
org.cryptomator
main
- 1.5.0-beta2
+ 1.5.0-beta3
keychain
System Keychain Access
diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java
index fd622e299..d36c8e4b5 100644
--- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java
+++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java
@@ -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 provideSupportedKeychain(Set keychainAccessStrategies) {
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst();
}
diff --git a/main/launcher/pom.xml b/main/launcher/pom.xml
index 9338d9943..cddee8417 100644
--- a/main/launcher/pom.xml
+++ b/main/launcher/pom.xml
@@ -4,7 +4,7 @@
org.cryptomator
main
- 1.5.0-beta2
+ 1.5.0-beta3
launcher
Cryptomator Launcher
diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java b/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java
index b0ea9d08b..cabcaf087 100644
--- a/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java
+++ b/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java
@@ -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 pathsToOpen = evt.getFiles().stream().map(File::toPath);
+ Collection 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 pathsToOpen = Arrays.stream(args).map(str -> {
+ Collection 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);
+ }
}
diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java b/main/launcher/src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java
index 4f6e8cacb..57f21f5e8 100644
--- a/main/launcher/src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java
+++ b/main/launcher/src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java
@@ -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
diff --git a/main/launcher/src/main/java/org/cryptomator/logging/LoggerConfiguration.java b/main/launcher/src/main/java/org/cryptomator/logging/LoggerConfiguration.java
index cb3bf6ff1..ce9109c88 100644
--- a/main/launcher/src/main/java/org/cryptomator/logging/LoggerConfiguration.java
+++ b/main/launcher/src/main/java/org/cryptomator/logging/LoggerConfiguration.java
@@ -55,10 +55,11 @@ 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
diff --git a/main/launcher/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java b/main/launcher/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java
index b89f3ed82..b0ed4b307 100644
--- a/main/launcher/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java
+++ b/main/launcher/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java
@@ -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 paths = evt.getPathsToOpen().collect(Collectors.toList());
+ Collection 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 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"});
diff --git a/main/pom.xml b/main/pom.xml
index 386bf7b7b..a414c44e2 100644
--- a/main/pom.xml
+++ b/main/pom.xml
@@ -3,13 +3,13 @@
4.0.0
org.cryptomator
main
- 1.5.0-beta2
+ 1.5.0-beta3
pom
Cryptomator
cryptomator.org
- http://cryptomator.org
+ https://cryptomator.org
@@ -24,33 +24,33 @@
UTF-8
- 1.9.0-rc2
+ 1.9.3
2.2.2
1.2.2
1.1.12
1.0.10
- 13.0.1
+ 14-ea+8
3.9
3.8.3
1.0.3
28.1-jre
- 2.25.2
+ 2.26
2.8.6
1.7.29
1.2.3
- 5.5.2
- 3.1.0
+ 5.6.0
+ 3.2.4
2.2
jcenter
- http://jcenter.bintray.com
+ https://jcenter.bintray.com
diff --git a/main/ui/pom.xml b/main/ui/pom.xml
index afe40303b..d79d8a017 100644
--- a/main/ui/pom.xml
+++ b/main/ui/pom.xml
@@ -4,7 +4,7 @@
org.cryptomator
main
- 1.5.0-beta2
+ 1.5.0-beta3
ui
Cryptomator GUI
diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java
index aafa97f5e..4e5143d8c 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java
@@ -27,8 +27,8 @@ 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;
@Module
@@ -51,13 +51,13 @@ public abstract class AddVaultModule {
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
- static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional windowIcon) {
+ static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List windowIcons) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("addvaultwizard.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
- windowIcon.ifPresent(stage.getIcons()::add);
+ stage.getIcons().addAll(windowIcons);
return stage;
}
@@ -193,8 +193,8 @@ public abstract class AddVaultModule {
@Provides
@IntoMap
@FxControllerKey(RecoveryKeyDisplayController.class)
- static FxController provideRecoveryKeyDisplayController(@AddVaultWizardWindow Stage window, @Named("vaultName") StringProperty vaultName, @Named("recoveryKey") StringProperty recoveryKey) {
- return new RecoveryKeyDisplayController(window, vaultName.get(), recoveryKey.get());
+ 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
diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java
index 543e84573..19a5107e7 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java
@@ -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;
diff --git a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java
index fcef4a7ec..41e0ede1d 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java
@@ -22,8 +22,8 @@ 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
@@ -46,13 +46,13 @@ abstract class ChangePasswordModule {
@Provides
@ChangePasswordWindow
@ChangePasswordScoped
- static Stage provideStage(@Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional windowIcon) {
+ static Stage provideStage(@Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List 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;
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/Animations.java b/main/ui/src/main/java/org/cryptomator/ui/common/Animations.java
new file mode 100644
index 000000000..d9c1ed19f
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/common/Animations.java
@@ -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 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())) //
+ );
+ }
+
+}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java
index e6f449ccb..998bea98e 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java
@@ -12,12 +12,16 @@ public enum FxmlFile {
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/unlock.fxml"),
diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/StackTraceController.java b/main/ui/src/main/java/org/cryptomator/ui/common/StackTraceController.java
index accab1b89..f83614dc1 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/common/StackTraceController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/common/StackTraceController.java
@@ -8,11 +8,11 @@ public class StackTraceController implements FxController {
private final String stackTrace;
- public StackTraceController(Exception cause) {
+ public StackTraceController(Throwable cause) {
this.stackTrace = provideStackTrace(cause);
}
- static String provideStackTrace(Exception cause) {
+ private static String provideStackTrace(Throwable cause) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
cause.printStackTrace(new PrintStream(baos));
return baos.toString(StandardCharsets.UTF_8);
diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/Tasks.java b/main/ui/src/main/java/org/cryptomator/ui/common/Tasks.java
index cb6ab4644..a7a61d862 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/common/Tasks.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/common/Tasks.java
@@ -73,21 +73,21 @@ public class Tasks {
return new TaskImpl<>(callable, successHandler, errorHandlers, finallyHandler);
}
- public Task runOnce(ExecutorService executorService) {
+ public Task runOnce(ExecutorService executor) {
Task task = build();
- executorService.submit(task);
+ executor.submit(task);
return task;
}
- public Task scheduleOnce(ScheduledExecutorService executorService, long delay, TimeUnit unit) {
+ public Task scheduleOnce(ScheduledExecutorService scheduler, long delay, TimeUnit unit) {
Task task = build();
- executorService.schedule(task, delay, unit);
+ scheduler.schedule(task, delay, unit);
return task;
}
- public ScheduledService schedulePeriodically(ExecutorService executorService, Duration initialDelay, Duration period) {
+ public ScheduledService schedulePeriodically(ExecutorService executor, Duration initialDelay, Duration period) {
ScheduledService service = new RestartingService<>(this::build);
- service.setExecutor(executorService);
+ service.setExecutor(executor);
service.setDelay(initialDelay);
service.setPeriod(period);
Platform.runLater(service::start);
diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java
index ec2030fd2..68e1fecb0 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java
@@ -4,15 +4,20 @@ 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;
@@ -23,10 +28,12 @@ public class VaultService {
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
private final ExecutorService executorService;
+ private final Optional keychain;
@Inject
- public VaultService(ExecutorService executorService) {
+ public VaultService(ExecutorService executorService, Optional keychain) {
this.executorService = executorService;
+ this.keychain = keychain;
}
public void reveal(Vault vault) {
@@ -45,6 +52,72 @@ public class VaultService {
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 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 createAutoUnlockTask(Vault vault, KeychainAccess keychainAccess) {
+ Task 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 createUnlockTask(Vault vault, CharSequence passphrase) {
+ Task 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.
*
@@ -60,6 +133,7 @@ public class VaultService {
*
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
+ * @return The task
*/
public Task createLockTask(Vault vault, boolean forced) {
Task task = new LockVaultTask(vault, forced);
@@ -145,6 +219,93 @@ public class VaultService {
}
}
+ private static class AutoUnlockVaultTask extends Task {
+
+ 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 {
+
+ 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
*/
@@ -186,5 +347,4 @@ public class VaultService {
}
-
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
index 742d29438..97d20ecdb 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
@@ -5,12 +5,13 @@ 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"), //
- EXCLAMATION("\uF12A"),
+ CROWN("\uF521"), //
+ EXCLAMATION("\uF12A"), //
EXCLAMATION_CIRCLE("\uF06A"), //
EXCLAMATION_TRIANGLE("\uF071"), //
EYE("\uF06E"), //
@@ -22,17 +23,17 @@ public enum FontAwesome5Icon {
HDD("\uF0A0"), //
KEY("\uF084"), //
LINK("\uF0C1"), //
- LOCK_ALT("\uF30D"), //
- LOCK_OPEN_ALT("\uF3C2"), //
+ LOCK("\uF023"), //
+ LOCK_OPEN("\uF3C1"), //
+ MAGIC("\uF0D0"), //
PLUS("\uF067"), //
PRINT("\uF02F"), //
QUESTION("\uF128"), //
- SPARKLES("\uF890"), //
SPINNER("\uF110"), //
SYNC("\uF021"), //
TIMES("\uF00D"), //
- USER_CROWN("\uF6A4"), //
WRENCH("\uF0AD"), //
+ WINDOW_MINIMIZE("\uF2D1"), //
;
private final String unicode;
diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java
index a0c6618fa..6d131da27 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java
@@ -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 glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java b/main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java
index 0a50f5322..d23951a4b 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java
@@ -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());
diff --git a/main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordModule.java b/main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordModule.java
index b06f8dec0..a6d45fa41 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordModule.java
@@ -20,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
@@ -37,13 +37,13 @@ abstract class ForgetPasswordModule {
@Provides
@ForgetPasswordWindow
@ForgetPasswordScoped
- static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional windowIcon, @Named("forgetPasswordOwner") Stage owner) {
+ static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List 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;
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
index a51470489..5a12822b2 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
@@ -22,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 {
@@ -34,19 +36,29 @@ abstract class FxApplicationModule {
}
@Provides
- @Named("windowIcon")
+ @Named("windowIcons")
@FxApplicationScoped
- static Optional provideWindowIcon() {
+ static List 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);
diff --git a/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEvent.java b/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEvent.java
index 7fc7fcfe6..a7334302c 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEvent.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEvent.java
@@ -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 pathsToOpen;
private final EventType type;
+ private final Collection pathsToOpen;
public enum EventType {REVEAL_APP, OPEN_FILE}
- public AppLaunchEvent(EventType type, Stream pathsToOpen) {
+ public AppLaunchEvent(EventType type, Collection pathsToOpen) {
this.type = type;
this.pathsToOpen = pathsToOpen;
}
@@ -19,7 +20,7 @@ public class AppLaunchEvent {
return type;
}
- public Stream getPathsToOpen() {
+ public Collection getPathsToOpen() {
return pathsToOpen;
}
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java b/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java
new file mode 100644
index 000000000..bb53e8768
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java
@@ -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 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 vaults;
+ private final AtomicBoolean allowQuitWithoutPrompt;
+
+ @Inject
+ AppLifecycleListener(FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, ObservableList 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);
+ }
+ }
+ }
+ }
+
+}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java b/main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java
index 538d69602..f44071d86 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java
@@ -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 vaults;
private final TrayMenuComponent.Builder trayComponent;
private final FxApplicationStarter fxApplicationStarter;
private final AppLaunchEventHandler launchEventHandler;
private final Optional macFunctions;
@Inject
- public UiLauncher(Settings settings, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional macFunctions) {
+ public UiLauncher(Settings settings, ObservableList vaults, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional 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 vaultsWithAutoUnlockEnabled = vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get());
+ if (!vaultsWithAutoUnlockEnabled.isEmpty()) {
+ fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> app.getVaultService().attemptAutoUnlock(vaultsWithAutoUnlockEnabled));
+ }
+
launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java
index 8fb9dd1a4..9a058ea85 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java
@@ -25,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})
@@ -42,7 +42,7 @@ abstract class MainWindowModule {
@Provides
@MainWindow
@MainWindowScoped
- static Stage provideStage(@Named("windowIcon") Optional windowIcon) {
+ static Stage provideStage(@Named("windowIcons") List 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);
@@ -50,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;
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java
index 49bd905f5..53044e00a 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java
@@ -5,12 +5,11 @@ import javafx.fxml.FXML;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import org.cryptomator.common.LicenseHolder;
-import org.cryptomator.common.vaults.VaultListManager;
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.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -24,6 +23,7 @@ public class MainWindowTitleController implements FxController {
public HBox titleBar;
+ private final AppLifecycleListener appLifecycle;
private final Stage window;
private final FxApplication application;
private final boolean minimizeToSysTray;
@@ -35,7 +35,8 @@ public class MainWindowTitleController implements FxController {
private double yOffset;
@Inject
- MainWindowTitleController(@MainWindow Stage window, FxApplication application, @Named("trayMenuSupported") boolean minimizeToSysTray, UpdateChecker updateChecker, LicenseHolder licenseHolder) {
+ 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;
@@ -56,6 +57,10 @@ public class MainWindowTitleController implements FxController {
window.setX(event.getScreenX() - xOffset);
window.setY(event.getScreenY() - yOffset);
});
+ window.setOnCloseRequest(event -> {
+ close();
+ event.consume();
+ });
}
@FXML
@@ -63,10 +68,15 @@ public class MainWindowTitleController implements FxController {
if (minimizeToSysTray) {
window.close();
} else {
- window.setIconified(true);
+ appLifecycle.quit();
}
}
+ @FXML
+ public void minimize() {
+ window.setIconified(true);
+ }
+
@FXML
public void showPreferences() {
application.showPreferencesWindow(SelectedPreferencesTab.ANY);
@@ -91,5 +101,7 @@ public class MainWindowTitleController implements FxController {
return updateAvailable.get();
}
-
+ public boolean isMinimizeToSysTray() {
+ return minimizeToSysTray;
+ }
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java
index dc77d04b5..7f25bdf62 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java
@@ -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;
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java
index 1575c128f..ec553c329 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java
@@ -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;
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationCapabilityErrorController.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationCapabilityErrorController.java
new file mode 100644
index 000000000..e6c31b7a0
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationCapabilityErrorController.java
@@ -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 startScene;
+ private final StringBinding missingCapabilityDescription;
+ private final ReadOnlyObjectProperty missingCapability;
+
+ @Inject
+ MigrationCapabilityErrorController(@MigrationWindow Stage window, @Named("capabilityErrorCause") ObjectProperty missingCapability, ResourceBundle localization, @FxmlScene(FxmlFile.MIGRATION_START) Lazy 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;
+ }
+ }
+}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationGenericErrorController.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationGenericErrorController.java
new file mode 100644
index 000000000..3b6121c79
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationGenericErrorController.java
@@ -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 startScene;
+
+ @Inject
+ MigrationGenericErrorController(@MigrationWindow Stage window, @FxmlScene(FxmlFile.MIGRATION_START) Lazy startScene) {
+ this.window = window;
+ this.startScene = startScene;
+ }
+
+ @FXML
+ public void back() {
+ window.setScene(startScene.get());
+ }
+}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationModule.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationModule.java
index 11d13fa81..7404088e3 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationModule.java
@@ -4,22 +4,26 @@ 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.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;
@Module
@@ -35,16 +39,30 @@ abstract class MigrationModule {
@Provides
@MigrationWindow
@MigrationScoped
- static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional windowIcon) {
+ static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List windowIcons) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("migration.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
- windowIcon.ifPresent(stage.getIcons()::add);
+ stage.getIcons().addAll(windowIcons);
return stage;
}
+ @Provides
+ @Named("genericErrorCause")
+ @MigrationScoped
+ static ObjectProperty provideGenericErrorCause() {
+ return new SimpleObjectProperty<>();
+ }
+
+ @Provides
+ @Named("capabilityErrorCause")
+ @MigrationScoped
+ static ObjectProperty provideCapabilityErrorCause() {
+ return new SimpleObjectProperty<>();
+ }
+
@Provides
@FxmlScene(FxmlFile.MIGRATION_START)
@MigrationScoped
@@ -66,6 +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
+ @FxmlScene(FxmlFile.MIGRATION_GENERIC_ERROR)
+ @MigrationScoped
+ static Scene provideMigrationGenericErrorScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) {
+ return fxmlLoaders.createScene("/fxml/migration_generic_error.fxml");
+ }
+
+
// ------------------
@Binds
@@ -83,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 errorCause) {
+ return new StackTraceController(errorCause.get());
+ }
+
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java
index 181876d2f..81c8f90f7 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java
@@ -1,32 +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.property.SimpleDoubleProperty;
-import javafx.beans.value.WritableValue;
-import javafx.concurrent.ScheduledService;
-import javafx.concurrent.Task;
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.MigrationProgressListener;
-import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException;
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;
@@ -36,44 +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 Duration MIGRATION_PROGRESS_UPDATE_INTERVAL = Duration.millis(25);
+ 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;
+ private final ObjectProperty missingCapability;
+ private final ObjectProperty errorCause;
private final Lazy startScene;
private final Lazy successScene;
private final ObjectBinding migrateButtonContentDisplay;
+ private final Lazy capabilityErrorScene;
+ private final Lazy genericErrorScene;
private final BooleanProperty migrationButtonDisabled;
private final DoubleProperty migrationProgress;
- private final ScheduledService migrationProgressObservationService;
private volatile double volatileMigrationProgress = -1.0;
public NiceSecurePasswordField passwordField;
@Inject
- public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, Optional keychainAccess, @FxmlScene(FxmlFile.MIGRATION_START) Lazy startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy successScene) {
+ public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, Optional keychainAccess, @Named("capabilityErrorCause") ObjectProperty missingCapability, @Named("genericErrorCause") ObjectProperty errorCause, @FxmlScene(FxmlFile.MIGRATION_START) Lazy startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_GENERIC_ERROR) Lazy 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);
- this.migrationProgressObservationService = new MigrationProgressObservationService();
- migrationProgressObservationService.setExecutor(executor);
- migrationProgressObservationService.setPeriod(MIGRATION_PROGRESS_UPDATE_INTERVAL);
}
public void initialize() {
@@ -93,36 +99,42 @@ public class MigrationRunController implements FxController {
LOG.info("Migrating vault {}", vault.getPath());
CharSequence password = passwordField.getCharacters();
vault.setState(VaultState.PROCESSING);
- migrationProgressObservationService.start();
+ 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, this::migrationProgressChanged);
return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME);
}).onSuccess(needsAnotherMigration -> {
- LOG.info("Migration of '{}' succeeded.", vault.getDisplayableName());
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();
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(() -> {
- migrationProgressObservationService.cancel();
- migrationProgressObservationService.reset();
+ progressSyncTask.cancel(true);
}).runOnce(executor);
}
@@ -161,54 +173,6 @@ public class MigrationRunController implements FxController {
}
}
- // Sets migrationProgress to volatileMigrationProgress at its configured interval
- private class MigrationProgressObservationService extends ScheduledService {
-
- @Override
- protected Task createTask() {
- return new Task<>() {
- @Override
- protected Double call() {
- return volatileMigrationProgress;
- }
- };
- }
-
- @Override
- protected void succeeded() {
- assert getValue() != null;
- migrationProgress.set(getValue());
- super.succeeded();
- }
- }
-
- /* Animations */
-
- private void shakeWindow() {
- WritableValue 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() {
diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java
index 8b76364ef..4a6051cbd 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java
@@ -18,8 +18,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(includes = {AutoStartModule.class})
@@ -41,11 +41,11 @@ abstract class PreferencesModule {
@Provides
@PreferencesWindow
@PreferencesScoped
- static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional windowIcon) {
+ static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List 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;
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java b/main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java
index 5383a0f5e..afdcb78a9 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java
@@ -20,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
@@ -37,12 +37,12 @@ abstract class QuitModule {
@Provides
@QuitWindow
@QuitScoped
- static Stage provideStage(@Named("windowIcon") Optional windowIcon) {
+ static Stage provideStage(@Named("windowIcons") List 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;
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/AutoCompleter.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/AutoCompleter.java
new file mode 100644
index 000000000..4ca6d58ce
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/AutoCompleter.java
@@ -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 dictionary;
+
+ public AutoCompleter(Collection dictionary) {
+ this.dictionary = unmodifiableSortedRandomAccessList(dictionary);
+ }
+
+ private static > List unmodifiableSortedRandomAccessList(Collection items) {
+ List result = new ArrayList<>(items);
+ Collections.sort(result);
+ return Collections.unmodifiableList(result);
+ }
+
+ public Optional 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);
+ }
+ }
+
+}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java
index 25fa319b4..b9ab0fe37 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java
@@ -21,11 +21,21 @@ public interface RecoveryKeyComponent {
Stage window();
@FxmlScene(FxmlFile.RECOVERYKEY_CREATE)
- Lazy scene();
+ Lazy creationScene();
+
+ @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER)
+ Lazy recoverScene();
default void showRecoveryKeyCreationWindow() {
Stage stage = window();
- stage.setScene(scene().get());
+ stage.setScene(creationScene().get());
+ stage.sizeToScene();
+ stage.show();
+ }
+
+ default void showRecoveryKeyRecoverWindow() {
+ Stage stage = window();
+ stage.setScene(recoverScene().get());
stage.sizeToScene();
stage.show();
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java
index 945256f9e..6bb6a9f80 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java
@@ -1,27 +1,21 @@
package org.cryptomator.ui.recoverykey;
import dagger.Lazy;
-import javafx.animation.KeyFrame;
-import javafx.animation.KeyValue;
-import javafx.animation.Timeline;
-import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.StringProperty;
-import javafx.beans.value.WritableValue;
+import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
-import javafx.util.Duration;
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.common.Tasks;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.Nullable;
import javax.inject.Inject;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
@@ -48,19 +42,26 @@ public class RecoveryKeyCreationController implements FxController {
this.recoveryKeyFactory = recoveryKeyFactory;
this.recoveryKeyProperty = recoveryKey;
}
-
+
@FXML
public void createRecoveryKey() {
- Tasks.create(() -> {
- return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
- }).onSuccess(result -> {
- recoveryKeyProperty.set(result);
+ Task 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());
- }).onError(IOException.class, e -> {
- LOG.error("Creation of recovery key failed.", e);
- }).onError(InvalidPassphraseException.class, e -> {
- shakeWindow();
- }).runOnce(executor);
+ });
+ 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
@@ -68,31 +69,13 @@ public class RecoveryKeyCreationController implements FxController {
window.close();
}
- /* Animations */
+ private class RecoveryKeyCreationTask extends Task {
- private void shakeWindow() {
- WritableValue writableWindowX = new WritableValue<>() {
- @Override
- public Double getValue() {
- return window.getX();
- }
+ @Override
+ protected String call() throws IOException {
+ return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
+ }
- @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 */
diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyDisplayController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyDisplayController.java
index 2b61ddf85..19b32add2 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyDisplayController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyDisplayController.java
@@ -4,6 +4,7 @@ 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;
@@ -16,6 +17,8 @@ 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);
@@ -23,11 +26,14 @@ public class RecoveryKeyDisplayController implements FxController {
private final Stage window;
private final String vaultName;
private final String recoveryKey;
-
- public RecoveryKeyDisplayController(Stage window, String vaultName, 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
@@ -68,6 +74,8 @@ public class RecoveryKeyDisplayController implements FxController {
clipboardContent.putString(recoveryKey);
Clipboard.getSystemClipboard().setContent(clipboardContent);
LOG.info("Recovery key copied to clipboard.");
+
+ copyButton.setText(localization.getString("generic.button.copied"));
}
@FXML
diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java
index 00e4002e2..25374a7f2 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java
@@ -10,11 +10,13 @@ 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;
@@ -22,6 +24,10 @@ public class RecoveryKeyFactory {
public RecoveryKeyFactory(WordEncoder wordEncoder) {
this.wordEncoder = wordEncoder;
}
+
+ public Collection getDictionary() {
+ return wordEncoder.getWords();
+ }
/**
* @param vaultPath Path to the storage location of a vault
@@ -32,7 +38,7 @@ public class RecoveryKeyFactory {
* @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, new byte[0], password);
+ byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vaultPath, MASTERKEY_FILENAME, PEPPER, password);
try {
return createRecoveryKey(rawKey);
} finally {
@@ -53,26 +59,53 @@ public class RecoveryKeyFactory {
}
}
+ /**
+ * 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 true if this seems to be a legitimate recovery key
*/
public boolean validateRecoveryKey(String recoveryKey) {
- final byte[] paddedKey;
try {
- paddedKey = wordEncoder.decode(recoveryKey);
+ byte[] key = decodeRecoveryKey(recoveryKey);
+ Arrays.fill(key, (byte) 0x00);
+ return true;
} catch (IllegalArgumentException e) {
return false;
}
- if (paddedKey.length != 66) {
- 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);
}
- 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);
- return Arrays.equals(expectedCrc16, actualCrc16);
}
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java
index 80f89c749..3926f14ae 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java
@@ -4,6 +4,8 @@ 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;
@@ -17,11 +19,13 @@ 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.Optional;
import java.util.ResourceBundle;
@Module
@@ -37,13 +41,13 @@ abstract class RecoveryKeyModule {
@Provides
@RecoveryKeyWindow
@RecoveryKeyScoped
- static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional windowIcon, @Named("keyRecoveryOwner") Stage owner) {
+ static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List 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);
- windowIcon.ifPresent(stage.getIcons()::add);
+ stage.getIcons().addAll(windowIcons);
return stage;
}
@@ -53,7 +57,15 @@ abstract class RecoveryKeyModule {
static StringProperty provideRecoveryKeyProperty() {
return new SimpleStringProperty();
}
-
+
+ @Provides
+ @RecoveryKeyScoped
+ @Named("newPassword")
+ static ObjectProperty provideNewPasswordProperty() {
+ return new SimpleObjectProperty<>("");
+ }
+
+
// ------------------
@Provides
@@ -70,6 +82,20 @@ abstract class RecoveryKeyModule {
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
@@ -80,13 +106,30 @@ abstract class RecoveryKeyModule {
@Provides
@IntoMap
@FxControllerKey(RecoveryKeyDisplayController.class)
- static FxController provideRecoveryKeyDisplayController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey) {
- return new RecoveryKeyDisplayController(window, vault.getDisplayableName(), recoveryKey.get());
+ 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 password) {
+ return new NewPasswordController(resourceBundle, strengthRater, password);
+ }
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java
new file mode 100644
index 000000000..8b9e1cdfb
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java
@@ -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 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 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 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);
+ }
+}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java
new file mode 100644
index 000000000..7bda40364
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java
@@ -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 newPassword;
+ private final Lazy recoverScene;
+ private final BooleanBinding invalidNewPassword;
+
+ @Inject
+ public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @Named("newPassword")ObjectProperty newPassword, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy 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 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 {
+
+ @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;
+ }
+}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/WordEncoder.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/WordEncoder.java
index b06d06902..d5e7e667c 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/WordEncoder.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/WordEncoder.java
@@ -2,6 +2,7 @@ package org.cryptomator.ui.recoverykey;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -11,6 +12,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -31,6 +33,10 @@ class WordEncoder {
this(DEFAULT_WORD_FILE);
}
+ public List getWords() {
+ return words;
+ }
+
public WordEncoder(String wordFile) {
try (InputStream in = getClass().getResourceAsStream(wordFile); //
Reader reader = new InputStreamReader(in, StandardCharsets.US_ASCII.newDecoder()); //
@@ -78,7 +84,7 @@ class WordEncoder {
* @throws IllegalArgumentException If the encoded string doesn't consist of a multiple of two words or one of the words is unknown to this encoder.
*/
public byte[] decode(String encoded) {
- List splitted = Splitter.on(DELIMITER).omitEmptyStrings().splitToList(encoded);
+ List splitted = Splitter.on(DELIMITER).omitEmptyStrings().splitToList(Strings.nullToEmpty(encoded));
Preconditions.checkArgument(splitted.size() % 2 == 0, "%s needs to be a multiple of two words", encoded);
byte[] result = new byte[splitted.size() / 2 * 3];
for (int i = 0; i < splitted.size(); i+=2) {
diff --git a/main/ui/src/main/java/org/cryptomator/ui/removevault/RemoveVaultModule.java b/main/ui/src/main/java/org/cryptomator/ui/removevault/RemoveVaultModule.java
index 965b6c6e8..2bf44b106 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/removevault/RemoveVaultModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/removevault/RemoveVaultModule.java
@@ -21,8 +21,8 @@ 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;
@Module
@@ -38,13 +38,13 @@ abstract class RemoveVaultModule {
@Provides
@RemoveVaultWindow
@RemoveVaultScoped
- static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional windowIcon) {
+ static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List windowIcons) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("removeVault.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
- windowIcon.ifPresent(stage.getIcons()::add);
+ stage.getIcons().addAll(windowIcons);
return stage;
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java
index bef3c441c..55735f5b1 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java
@@ -3,57 +3,37 @@ package org.cryptomator.ui.traymenu;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.collections.ObservableList;
-import org.cryptomator.common.ShutdownHook;
-import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.common.vaults.VaultState;
-import org.cryptomator.common.vaults.Volume;
-import org.cryptomator.ui.fxapp.FxApplication;
+import org.cryptomator.ui.launcher.AppLifecycleListener;
import org.cryptomator.ui.launcher.FxApplicationStarter;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import javax.inject.Inject;
-import javax.inject.Named;
-import java.awt.Desktop;
import java.awt.Menu;
import java.awt.MenuItem;
import java.awt.PopupMenu;
-import java.awt.desktop.QuitResponse;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
-import java.util.EnumSet;
import java.util.EventObject;
import java.util.ResourceBundle;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
@TrayMenuScoped
class TrayMenuController {
- private static final Logger LOG = LoggerFactory.getLogger(TrayMenuController.class);
- public static final Set STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR);
-
private final ResourceBundle resourceBundle;
+ private final AppLifecycleListener appLifecycle;
private final FxApplicationStarter fxApplicationStarter;
- private final CountDownLatch shutdownLatch;
- private final ShutdownHook shutdownHook;
private final ObservableList vaults;
private final PopupMenu menu;
- private final AtomicBoolean allowSuddenTermination;
@Inject
- TrayMenuController(ResourceBundle resourceBundle, FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, ObservableList vaults) {
+ TrayMenuController(ResourceBundle resourceBundle, AppLifecycleListener appLifecycle, FxApplicationStarter fxApplicationStarter, ObservableList vaults) {
this.resourceBundle = resourceBundle;
+ this.appLifecycle = appLifecycle;
this.fxApplicationStarter = fxApplicationStarter;
- this.shutdownLatch = shutdownLatch;
- this.shutdownHook = shutdownHook;
this.vaults = vaults;
this.menu = new PopupMenu();
- this.allowSuddenTermination = new AtomicBoolean(true);
}
public PopupMenu getMenu() {
@@ -62,40 +42,12 @@ class TrayMenuController {
public void initTrayMenu() {
vaults.addListener(this::vaultListChanged);
-
rebuildMenu();
-
- // 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);
-
- // allow sudden termination
- if (Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
- Desktop.getDesktop().enableSuddenTermination();
- }
}
private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
assert Platform.isFxApplicationThread();
rebuildMenu();
- boolean allVaultsAllowTermination = vaults.stream().map(Vault::getState).allMatch(STATES_ALLOWING_TERMINATION::contains);
- boolean suddenTerminationChanged = allowSuddenTermination.compareAndSet(!allVaultsAllowTermination, allVaultsAllowTermination);
- if (suddenTerminationChanged && Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
- if (allVaultsAllowTermination) {
- Desktop.getDesktop().enableSuddenTermination();
- LOG.debug("sudden termination enabled");
- } else {
- Desktop.getDesktop().disableSuddenTermination();
- LOG.debug("sudden termination disabled");
- }
- }
}
private void rebuildMenu() {
@@ -174,37 +126,8 @@ class TrayMenuController {
fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
}
- private void handleQuitRequest(EventObject e, QuitResponse response) {
- if (allowSuddenTermination.get()) {
- response.performQuit(); // really?
- } else {
- fxApplicationStarter.get(true).thenAccept(app -> app.showQuitWindow(response));
- }
- }
-
private void quitApplication(EventObject actionEvent) {
- handleQuitRequest(actionEvent, new QuitResponse() {
- @Override
- public void performQuit() {
- shutdownLatch.countDown();
- }
-
- @Override
- public void cancelQuit() {
- // no-op
- }
- });
+ appLifecycle.quit();
}
- 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);
- }
- }
- }
- }
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java
index 837dc827f..257f185f7 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java
@@ -1,32 +1,28 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
-import javafx.animation.KeyFrame;
-import javafx.animation.KeyValue;
-import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.value.WritableValue;
+import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
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.cryptolib.api.InvalidPassphraseException;
-import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
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;
-import org.cryptomator.ui.common.Tasks;
+import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.slf4j.Logger;
@@ -50,22 +46,24 @@ public class UnlockController implements FxController {
private final ExecutorService executor;
private final ObjectBinding unlockButtonState;
private final Optional keychainAccess;
+ private final VaultService vaultService;
private final Lazy successScene;
private final Lazy invalidMountPointScene;
private final Lazy genericErrorScene;
- private final ObjectProperty genericErrorCause;
+ private final ObjectProperty genericErrorCause;
private final ForgetPasswordComponent.Builder forgetPassword;
private final BooleanProperty unlockButtonDisabled;
public NiceSecurePasswordField passwordField;
public CheckBox savePassword;
@Inject
- public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional keychainAccess, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, @FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR) Lazy genericErrorScene, @Named("genericErrorCause") ObjectProperty genericErrorCause, ForgetPasswordComponent.Builder forgetPassword) {
+ public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional keychainAccess, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, @FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR) Lazy genericErrorScene, @Named("genericErrorCause") ObjectProperty genericErrorCause, ForgetPasswordComponent.Builder forgetPassword) {
this.window = window;
this.vault = vault;
this.executor = executor;
this.unlockButtonState = Bindings.createObjectBinding(this::getUnlockButtonState, vault.stateProperty());
this.keychainAccess = keychainAccess;
+ this.vaultService = vaultService;
this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene;
this.genericErrorScene = genericErrorScene;
@@ -93,36 +91,36 @@ public class UnlockController implements FxController {
public void unlock() {
LOG.trace("UnlockController.unlock()");
CharSequence password = passwordField.getCharacters();
- vault.setState(VaultState.PROCESSING);
- Tasks.create(() -> {
- vault.unlock(password);
+
+ Task task = vaultService.createUnlockTask(vault, password);
+ task.setOnSucceeded(event -> {
if (keychainAccess.isPresent() && savePassword.isSelected()) {
- keychainAccess.get().storePassphrase(vault.getId(), password);
+ try {
+ keychainAccess.get().storePassphrase(vault.getId(), password);
+ } catch (KeychainAccessException e) {
+ LOG.error("Failed to store passphrase in system keychain.", e);
+ }
}
- }).onSuccess(() -> {
- vault.setState(VaultState.UNLOCKED);
passwordField.swipe();
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
window.setScene(successScene.get());
- }).onError(InvalidPassphraseException.class, e -> {
- shakeWindow();
- passwordField.selectAll();
- passwordField.requestFocus();
- }).onError(NotDirectoryException.class, e -> {
- LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage());
- window.setScene(invalidMountPointScene.get());
- }).onError(DirectoryNotEmptyException.class, e -> {
- LOG.error("Unlock failed. Mount point not empty: {}", e.getMessage());
- window.setScene(invalidMountPointScene.get());
- }).onError(Exception.class, e -> { // including RuntimeExceptions
- LOG.error("Unlock failed for technical reasons.", e);
- genericErrorCause.set(e);
- window.setScene(genericErrorScene.get());
- }).andFinally(() -> {
- if (!vault.isUnlocked()) {
- vault.setState(VaultState.LOCKED);
+ });
+ task.setOnFailed(event -> {
+ if (task.getException() instanceof InvalidPassphraseException) {
+ Animations.createShakeWindowAnimation(window).play();
+ passwordField.selectAll();
+ passwordField.requestFocus();
+ } else if (task.getException() instanceof NotDirectoryException
+ || task.getException() instanceof DirectoryNotEmptyException) {
+ LOG.error("Unlock failed. Mount point not an empty directory: {}", task.getException().getMessage());
+ window.setScene(invalidMountPointScene.get());
+ } else {
+ LOG.error("Unlock failed for technical reasons.", task.getException());
+ genericErrorCause.set(task.getException());
+ window.setScene(genericErrorScene.get());
}
- }).runOnce(executor);
+ });
+ executor.execute(task);
}
/* Save Password */
@@ -167,33 +165,6 @@ public class UnlockController implements FxController {
}
}
- /* Animations */
-
- private void shakeWindow() {
- WritableValue 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() {
diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
index 69488010f..d06c9daed 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
@@ -21,8 +21,8 @@ import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
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 = {ForgetPasswordComponent.class})
@@ -38,19 +38,19 @@ abstract class UnlockModule {
@Provides
@UnlockWindow
@UnlockScoped
- static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional windowIcon) {
+ static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List windowIcons) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("unlock.title"));
stage.setResizable(false);
stage.initModality(Modality.APPLICATION_MODAL);
- windowIcon.ifPresent(stage.getIcons()::add);
+ stage.getIcons().addAll(windowIcons);
return stage;
}
@Provides
@Named("genericErrorCause")
@UnlockScoped
- static ObjectProperty provideGenericErrorCause() {
+ static ObjectProperty provideGenericErrorCause() {
return new SimpleObjectProperty<>();
}
@@ -109,7 +109,7 @@ abstract class UnlockModule {
@Provides
@IntoMap
@FxControllerKey(StackTraceController.class)
- static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty errorCause) {
+ static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty errorCause) {
return new StackTraceController(errorCause.get());
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java
index 5e68480a8..2507997c9 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java
@@ -1,11 +1,9 @@
package org.cryptomator.ui.vaultoptions;
import javafx.fxml.FXML;
-import javafx.stage.Stage;
+import javafx.scene.control.CheckBox;
import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.ui.changepassword.ChangePasswordComponent;
import org.cryptomator.ui.common.FxController;
-import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import javax.inject.Inject;
@@ -13,26 +11,15 @@ import javax.inject.Inject;
public class GeneralVaultOptionsController implements FxController {
private final Vault vault;
- private final Stage window;
- private final ChangePasswordComponent.Builder changePasswordWindow;
- private final RecoveryKeyComponent.Builder recoveryKeyWindow;
+ public CheckBox unlockOnStartupCheckbox;
@Inject
- GeneralVaultOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Builder recoveryKeyWindow) {
+ GeneralVaultOptionsController(@VaultOptionsWindow Vault vault) {
this.vault = vault;
- this.window = window;
- this.changePasswordWindow = changePasswordWindow;
- this.recoveryKeyWindow = recoveryKeyWindow;
}
@FXML
- public void changePassword() {
- changePasswordWindow.vault(vault).owner(window).build().showChangePasswordWindow();
+ public void initialize() {
+ unlockOnStartupCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().unlockAfterStartup());
}
-
- @FXML
- public void showRecoveryKey() {
- recoveryKeyWindow.vault(vault).owner(window).build().showRecoveryKeyCreationWindow();
- }
-
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java
new file mode 100644
index 000000000..d03ac4e31
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java
@@ -0,0 +1,42 @@
+package org.cryptomator.ui.vaultoptions;
+
+import javafx.fxml.FXML;
+import javafx.stage.Stage;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.ui.changepassword.ChangePasswordComponent;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
+
+import javax.inject.Inject;
+
+@VaultOptionsScoped
+public class MasterkeyOptionsController implements FxController {
+
+ private final Vault vault;
+ private final Stage window;
+ private final ChangePasswordComponent.Builder changePasswordWindow;
+ private final RecoveryKeyComponent.Builder recoveryKeyWindow;
+
+ @Inject
+ MasterkeyOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Builder recoveryKeyWindow) {
+ this.vault = vault;
+ this.window = window;
+ this.changePasswordWindow = changePasswordWindow;
+ this.recoveryKeyWindow = recoveryKeyWindow;
+ }
+
+ @FXML
+ public void changePassword() {
+ changePasswordWindow.vault(vault).owner(window).build().showChangePasswordWindow();
+ }
+
+ @FXML
+ public void showRecoveryKey() {
+ recoveryKeyWindow.vault(vault).owner(window).build().showRecoveryKeyCreationWindow();
+ }
+
+ @FXML
+ public void showRecoverVaultDialogue() {
+ recoveryKeyWindow.vault(vault).owner(window).build().showRecoveryKeyRecoverWindow();
+ }
+}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java
index 09185729a..639f5b36b 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java
@@ -21,8 +21,8 @@ import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
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 = {ChangePasswordComponent.class, RecoveryKeyComponent.class})
@@ -38,13 +38,15 @@ abstract class VaultOptionsModule {
@Provides
@VaultOptionsWindow
@VaultOptionsScoped
- static Stage provideStage(@MainWindow Stage owner, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle, @Named("windowIcon") Optional windowIcon) {
+ static Stage provideStage(@MainWindow Stage owner, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle, @Named("windowIcons") List windowIcons) {
Stage stage = new Stage();
stage.setTitle(vault.getDisplayableName());
- stage.setResizable(false);
+ stage.setResizable(true);
+ stage.setMinWidth(400);
+ stage.setMinHeight(300);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
- windowIcon.ifPresent(stage.getIcons()::add);
+ stage.getIcons().addAll(windowIcons);
return stage;
}
@@ -72,4 +74,9 @@ abstract class VaultOptionsModule {
@FxControllerKey(MountOptionsController.class)
abstract FxController bindMountOptionsController(MountOptionsController controller);
+ @Binds
+ @IntoMap
+ @FxControllerKey(MasterkeyOptionsController.class)
+ abstract FxController bindMasterkeyOptionsController(MasterkeyOptionsController controller);
+
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertModule.java b/main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertModule.java
index c0f2786dc..0f64a802e 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertModule.java
@@ -17,8 +17,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
@@ -34,12 +34,12 @@ abstract class WrongFileAlertModule {
@Provides
@WrongFileAlertWindow
@WrongFileAlertScoped
- static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional windowIcon) {
+ static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List windowIcons) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("wrongFileAlert.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
- windowIcon.ifPresent(stage.getIcons()::add);
+ stage.getIcons().addAll(windowIcons);
return stage;
}
diff --git a/main/ui/src/main/resources/bot.png b/main/ui/src/main/resources/bot.png
new file mode 100644
index 000000000..c02c60d7b
Binary files /dev/null and b/main/ui/src/main/resources/bot.png differ
diff --git a/main/ui/src/main/resources/bot@2x.png b/main/ui/src/main/resources/bot@2x.png
new file mode 100644
index 000000000..6192beae5
Binary files /dev/null and b/main/ui/src/main/resources/bot@2x.png differ
diff --git a/main/ui/src/main/resources/bot_welcome.png b/main/ui/src/main/resources/bot_welcome.png
deleted file mode 100644
index a429621d9..000000000
Binary files a/main/ui/src/main/resources/bot_welcome.png and /dev/null differ
diff --git a/main/ui/src/main/resources/bot_welcome@2x.png b/main/ui/src/main/resources/bot_welcome@2x.png
deleted file mode 100644
index d6e201c20..000000000
Binary files a/main/ui/src/main/resources/bot_welcome@2x.png and /dev/null differ
diff --git a/main/ui/src/main/resources/css/dark_theme.css b/main/ui/src/main/resources/css/dark_theme.css
index 7ad3a1da5..4a31930f8 100644
--- a/main/ui/src/main/resources/css/dark_theme.css
+++ b/main/ui/src/main/resources/css/dark_theme.css
@@ -17,7 +17,7 @@
}
@font-face {
- src: url('dosis-bold.ttf');
+ src: url('quicksand-bold.ttf');
}
/*******************************************************************************
@@ -27,39 +27,37 @@
******************************************************************************/
.root {
- GREEN_0: #373B30;
- GREEN_1: #384D14;
- GREEN_2: #476611;
- GREEN_3: #598016;
- GREEN_4: #699917;
- GREEN_5: #79B01A;
- GREEN_6: #91C734;
- GREEN_7: #B9E070;
- GREEN_8: #D7E7BA;
- GREEN_9: #F3F5F0;
+ PRIMARY_D2: #2D4D2E;
+ PRIMARY_D1: #407F41;
+ PRIMARY: #49B04A;
+ PRIMARY_L1: #66CC68;
+ PRIMARY_L2: #EBF5EB;
- GRAY_0: #222222;
- GRAY_1: #3B3B3B;
- GRAY_2: #515151;
- GRAY_3: #626262;
- GRAY_4: #7E7E7E;
- GRAY_5: #9E9E9E;
- GRAY_6: #B1B1B1;
- GRAY_7: #CFCFCF;
- GRAY_8: #E1E1E1;
- GRAY_9: #F7F7F7;
+ SECONDARY: #008A7B;
+
+ GRAY_0: #1F2122;
+ GRAY_1: #35393B;
+ GRAY_2: #494E51;
+ GRAY_3: #585E62;
+ GRAY_4: #71797E;
+ GRAY_5: #8E989E;
+ GRAY_6: #9FAAB1;
+ GRAY_7: #BEC9CF;
+ GRAY_8: #D3DCE1;
+ GRAY_9: #EDF3F7;
RED_5: #E74C3C;
ORANGE_5: #E67E22;
YELLOW_5: #F1C40F;
- PRIMARY_BG: GREEN_3;
- SECONDARY_BG: GRAY_3;
MAIN_BG: GRAY_1;
TEXT_FILL: GRAY_9;
- TEXT_FILL_PRIMARY: GREEN_5;
- TEXT_FILL_SECONDARY: GRAY_5;
- TEXT_FILL_WHITE: white;
+ TEXT_FILL_HIGHLIGHTED: PRIMARY;
+ TEXT_FILL_MUTED: GRAY_5;
+
+ TITLE_BG: linear-gradient(to bottom, GRAY_2, GRAY_1);
+ TITLE_TEXT_FILL: PRIMARY;
+
CONTROL_BORDER_NORMAL: GRAY_3;
CONTROL_BORDER_FOCUSED: GRAY_5;
CONTROL_BORDER_DISABLED: GRAY_2;
@@ -67,27 +65,20 @@
CONTROL_BG_HOVER: GRAY_1;
CONTROL_BG_ARMED: GRAY_2;
CONTROL_BG_DISABLED: GRAY_1;
- CONTROL_PRIMARY_BORDER_NORMAL: GREEN_5;
- CONTROL_PRIMARY_BORDER_FOCUSED: GREEN_7;
- CONTROL_PRIMARY_BORDER_DISABLED: GREEN_3;
- CONTROL_PRIMARY_BG_NORMAL: GREEN_3;
- CONTROL_PRIMARY_BG_ARMED: GREEN_4;
- CONTROL_PRIMARY_BG_DISABLED: GREEN_2;
- CONTROL_PRIMARY_LIGHT_BG_NORMAL: GREEN_0;
- CONTROL_WHITE_BG_ARMED: GRAY_8;
+ CONTROL_BG_SELECTED: GRAY_1;
+
+ CONTROL_PRIMARY_BORDER_NORMAL: PRIMARY;
+ CONTROL_PRIMARY_BORDER_ARMED: PRIMARY_L1;
+ CONTROL_PRIMARY_BORDER_FOCUSED: SECONDARY;
+ CONTROL_PRIMARY_BG_NORMAL: PRIMARY;
+ CONTROL_PRIMARY_BG_ARMED: PRIMARY_L1;
+
SCROLL_BAR_THUMB_NORMAL: GRAY_3;
SCROLL_BAR_THUMB_HOVER: GRAY_4;
- INDICATOR_BG: RED_5;
- DRAG_N_DROP_INDICATOR_BG: GRAY_3;
+
PROGRESS_INDICATOR_BEGIN: GRAY_7;
PROGRESS_INDICATOR_END: GRAY_5;
PROGRESS_BAR_BG: GRAY_2;
- PASSWORD_STRENGTH_INDICATOR_BG: GRAY_3;
- PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_0: RED_5;
- PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_1: ORANGE_5;
- PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_2: YELLOW_5;
- PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_3: GREEN_6;
- PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_4: GREEN_5;
-fx-background-color: MAIN_BG;
-fx-text-fill: TEXT_FILL;
@@ -105,7 +96,7 @@
}
.label-secondary {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
}
.label-large {
@@ -136,11 +127,11 @@
}
.glyph-icon-primary {
- -fx-fill: PRIMARY_BG;
+ -fx-fill: PRIMARY;
}
-.glyph-icon-secondary {
- -fx-fill: SECONDARY_BG;
+.glyph-icon-muted {
+ -fx-fill: TEXT_FILL_MUTED;
}
.glyph-icon-white {
@@ -158,15 +149,16 @@
******************************************************************************/
.main-window .title {
- -fx-background-color: PRIMARY_BG;
+ -fx-background-color: CONTROL_BORDER_NORMAL, TITLE_BG;
+ -fx-background-insets: 0, 0 0 1px 0;
}
.main-window .title .label {
- -fx-font-family: 'Dosis';
- -fx-font-size: 21px;
+ -fx-font-family: 'Quicksand';
+ -fx-font-size: 16px;
-fx-font-style: normal;
-fx-font-weight: 700;
- -fx-text-fill: white;
+ -fx-text-fill: TITLE_TEXT_FILL;
}
.main-window .title .button {
@@ -181,24 +173,23 @@
}
.main-window .title .button:armed .glyph-icon {
- -fx-fill: CONTROL_WHITE_BG_ARMED;
+ -fx-fill: GRAY_8;
}
.main-window .update-indicator {
- -fx-background-color: PRIMARY_BG, white, INDICATOR_BG;
- -fx-background-insets: 0, 1px, 2px;
- -fx-background-radius: 6px, 5px, 4px;
- -fx-translate-x: -1px;
- -fx-translate-y: 1px;
+ -fx-background-color: white, RED_5;
+ -fx-background-insets: 1px, 2px;
+ -fx-background-radius: 6px, 5px;
+ -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 2, 0, 0, 0);
}
.main-window .drag-n-drop-indicator {
- -fx-border-color: DRAG_N_DROP_INDICATOR_BG;
+ -fx-border-color: SECONDARY;
-fx-border-width: 3px;
}
.main-window .drag-n-drop-indicator .drag-n-drop-header {
- -fx-background-color: DRAG_N_DROP_INDICATOR_BG;
+ -fx-background-color: SECONDARY;
-fx-padding: 3px;
}
@@ -225,24 +216,24 @@
}
.tab-pane .tab:selected {
- -fx-background-color: PRIMARY_BG, CONTROL_PRIMARY_LIGHT_BG_NORMAL;
+ -fx-background-color: PRIMARY, CONTROL_BG_SELECTED;
}
.tab-pane .tab .tab-label {
- -fx-text-fill: SECONDARY_BG;
+ -fx-text-fill: TEXT_FILL_MUTED;
-fx-alignment: CENTER;
}
.tab-pane .tab .glyph-icon {
- -fx-fill: SECONDARY_BG;
+ -fx-fill: TEXT_FILL_MUTED;
}
.tab-pane .tab:selected .glyph-icon {
- -fx-fill: PRIMARY_BG;
+ -fx-fill: PRIMARY;
}
.tab-pane .tab:selected .tab-label {
- -fx-text-fill: TEXT_FILL_PRIMARY;
+ -fx-text-fill: TEXT_FILL_HIGHLIGHTED;
}
/*******************************************************************************
@@ -271,16 +262,16 @@
}
.list-view:focused .list-cell:selected {
- -fx-background-color: PRIMARY_BG, CONTROL_PRIMARY_LIGHT_BG_NORMAL;
+ -fx-background-color: PRIMARY, CONTROL_BG_SELECTED;
-fx-background-insets: 0, 0 0 0 3px;
}
.list-cell:selected {
- -fx-background-color: CONTROL_PRIMARY_LIGHT_BG_NORMAL;
+ -fx-background-color: CONTROL_BG_SELECTED;
}
.list-cell .glyph-icon {
- -fx-fill: SECONDARY_BG;
+ -fx-fill: TEXT_FILL_MUTED;
}
.list-cell .header-label {
@@ -289,16 +280,16 @@
}
.list-cell .detail-label {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
-fx-font-size: 0.8em;
}
.list-cell:selected .glyph-icon {
- -fx-fill: PRIMARY_BG;
+ -fx-fill: PRIMARY;
}
.list-cell:selected .header-label {
- -fx-text-fill: TEXT_FILL_PRIMARY;
+ -fx-text-fill: TEXT_FILL_HIGHLIGHTED;
}
.list-cell.drop-above {
@@ -379,13 +370,13 @@
}
.badge-primary {
- -fx-text-fill: TEXT_FILL_WHITE;
- -fx-background-color: PRIMARY_BG;
+ -fx-text-fill: white;
+ -fx-background-color: PRIMARY;
}
.badge-secondary {
- -fx-text-fill: TEXT_FILL_WHITE;
- -fx-background-color: SECONDARY_BG;
+ -fx-text-fill: white;
+ -fx-background-color: SECONDARY;
}
/*******************************************************************************
@@ -395,27 +386,27 @@
******************************************************************************/
.password-strength-indicator .segment {
- -fx-background-color: PASSWORD_STRENGTH_INDICATOR_BG;
+ -fx-background-color: CONTROL_BORDER_NORMAL;
}
.password-strength-indicator.strength-0 .segment.active {
- -fx-background-color: PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_0;
+ -fx-background-color: RED_5;
}
.password-strength-indicator.strength-1 .segment.active {
- -fx-background-color: PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_1;
+ -fx-background-color: ORANGE_5;
}
.password-strength-indicator.strength-2 .segment.active {
- -fx-background-color: PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_2;
+ -fx-background-color: YELLOW_5;
}
.password-strength-indicator.strength-3 .segment.active {
- -fx-background-color: PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_3;
+ -fx-background-color: PRIMARY;
}
.password-strength-indicator.strength-4 .segment.active {
- -fx-background-color: PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_4;
+ -fx-background-color: PRIMARY_D1;
}
/*******************************************************************************
@@ -442,8 +433,8 @@
.text-input {
-fx-cursor: text;
-fx-text-fill: TEXT_FILL;
- -fx-highlight-fill: PRIMARY_BG;
- -fx-prompt-text-fill: TEXT_FILL_SECONDARY;
+ -fx-highlight-fill: PRIMARY;
+ -fx-prompt-text-fill: TEXT_FILL_MUTED;
-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
-fx-background-insets: 0, 1px;
-fx-background-radius: 4px;
@@ -455,7 +446,7 @@
}
.text-input:disabled {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
-fx-background-color: CONTROL_BORDER_DISABLED, CONTROL_BG_DISABLED;
}
@@ -487,7 +478,7 @@
}
.text-input:disabled {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
-fx-background-color: CONTROL_BORDER_DISABLED, CONTROL_BG_DISABLED;
}
@@ -499,8 +490,8 @@
-fx-padding: 0.2em 0.5em 0.2em 0.5em;
-fx-cursor: text;
-fx-text-fill: TEXT_FILL;
- -fx-highlight-fill: PRIMARY_BG;
- -fx-prompt-text-fill: TEXT_FILL_SECONDARY;
+ -fx-highlight-fill: PRIMARY;
+ -fx-prompt-text-fill: TEXT_FILL_MUTED;
-fx-background-color: null;
}
@@ -529,7 +520,7 @@
}
.button:default {
- -fx-text-fill: TEXT_FILL_WHITE;
+ -fx-text-fill: white;
-fx-background-color: CONTROL_PRIMARY_BORDER_NORMAL, CONTROL_PRIMARY_BG_NORMAL;
}
@@ -538,25 +529,25 @@
}
.button:default:armed {
- -fx-background-color: CONTROL_PRIMARY_BORDER_NORMAL, CONTROL_PRIMARY_BG_ARMED;
+ -fx-background-color: CONTROL_PRIMARY_BORDER_ARMED, CONTROL_PRIMARY_BG_ARMED;
}
.button:disabled,
.button:default:disabled {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
-fx-background-color: CONTROL_BORDER_DISABLED, CONTROL_BG_DISABLED;
}
.button:disabled .glyph-icon {
- -fx-fill: TEXT_FILL_SECONDARY;
+ -fx-fill: TEXT_FILL_MUTED;
}
.button:default .glyph-icon {
- -fx-fill: TEXT_FILL_WHITE;
+ -fx-fill: white;
}
.button:default .label {
- -fx-text-fill: TEXT_FILL_WHITE;
+ -fx-text-fill: white;
}
.button-large {
@@ -577,7 +568,7 @@
}
.hyperlink.hyperlink-secondary {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
}
.hyperlink-hover-icon {
@@ -601,7 +592,7 @@
}
.check-box:disabled {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
}
.check-box > .box {
@@ -694,7 +685,7 @@
}
.choice-box:disabled > .label {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
}
.choice-box > .open-button {
@@ -709,7 +700,7 @@
}
.choice-box:disabled > .open-button > .arrow {
- -fx-background-color: transparent, TEXT_FILL_SECONDARY;
+ -fx-background-color: transparent, TEXT_FILL_MUTED;
}
.choice-box .context-menu {
@@ -758,7 +749,7 @@
}
.menu-item:disabled > .label {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
}
.radio-menu-item:checked > .left-container > .radio {
diff --git a/main/ui/src/main/resources/css/dosis-bold.ttf b/main/ui/src/main/resources/css/dosis-bold.ttf
deleted file mode 100644
index d5e938e24..000000000
Binary files a/main/ui/src/main/resources/css/dosis-bold.ttf and /dev/null differ
diff --git a/main/ui/src/main/resources/css/fontawesome5-free-solid.otf b/main/ui/src/main/resources/css/fontawesome5-free-solid.otf
new file mode 100644
index 000000000..7f130607c
Binary files /dev/null and b/main/ui/src/main/resources/css/fontawesome5-free-solid.otf differ
diff --git a/main/ui/src/main/resources/css/fontawesome5-pro-solid.otf b/main/ui/src/main/resources/css/fontawesome5-pro-solid.otf
deleted file mode 100644
index 66ce2e49f..000000000
Binary files a/main/ui/src/main/resources/css/fontawesome5-pro-solid.otf and /dev/null differ
diff --git a/main/ui/src/main/resources/css/light_theme.css b/main/ui/src/main/resources/css/light_theme.css
index a09d3a908..64e4a527b 100644
--- a/main/ui/src/main/resources/css/light_theme.css
+++ b/main/ui/src/main/resources/css/light_theme.css
@@ -17,7 +17,7 @@
}
@font-face {
- src: url('dosis-bold.ttf');
+ src: url('quicksand-bold.ttf');
}
/*******************************************************************************
@@ -27,16 +27,13 @@
******************************************************************************/
.root {
- GREEN_0: #373B30;
- GREEN_1: #384D14;
- GREEN_2: #476611;
- GREEN_3: #598016;
- GREEN_4: #699917;
- GREEN_5: #79B01A;
- GREEN_6: #91C734;
- GREEN_7: #B9E070;
- GREEN_8: #D7E7BA;
- GREEN_9: #F3F5F0;
+ PRIMARY_D2: #2D4D2E;
+ PRIMARY_D1: #407F41;
+ PRIMARY: #49B04A;
+ PRIMARY_L1: #66CC68;
+ PRIMARY_L2: #EBF5EB;
+
+ SECONDARY: #008A7B;
GRAY_0: #222222;
GRAY_1: #3B3B3B;
@@ -53,13 +50,14 @@
ORANGE_5: #E67E22;
YELLOW_5: #F1C40F;
- PRIMARY_BG: GREEN_5;
- SECONDARY_BG: GRAY_5;
MAIN_BG: GRAY_9;
TEXT_FILL: GRAY_0;
- TEXT_FILL_PRIMARY: GREEN_4;
- TEXT_FILL_SECONDARY: GRAY_4;
- TEXT_FILL_WHITE: white;
+ TEXT_FILL_HIGHLIGHTED: PRIMARY;
+ TEXT_FILL_MUTED: GRAY_5;
+
+ TITLE_BG: PRIMARY;
+ TITLE_TEXT_FILL: white;
+
CONTROL_BORDER_NORMAL: GRAY_7;
CONTROL_BORDER_FOCUSED: GRAY_5;
CONTROL_BORDER_DISABLED: GRAY_8;
@@ -67,27 +65,20 @@
CONTROL_BG_HOVER: GRAY_9;
CONTROL_BG_ARMED: GRAY_8;
CONTROL_BG_DISABLED: GRAY_9;
- CONTROL_PRIMARY_BORDER_NORMAL: GREEN_3;
- CONTROL_PRIMARY_BORDER_FOCUSED: GREEN_1;
- CONTROL_PRIMARY_BORDER_DISABLED: GREEN_5;
- CONTROL_PRIMARY_BG_NORMAL: GREEN_5;
- CONTROL_PRIMARY_BG_ARMED: GREEN_4;
- CONTROL_PRIMARY_BG_DISABLED: GREEN_6;
- CONTROL_PRIMARY_LIGHT_BG_NORMAL: GREEN_9;
- CONTROL_WHITE_BG_ARMED: GRAY_8;
+ CONTROL_BG_SELECTED: PRIMARY_L2;
+
+ CONTROL_PRIMARY_BORDER_NORMAL: PRIMARY_D1;
+ CONTROL_PRIMARY_BORDER_ARMED: PRIMARY_D2;
+ CONTROL_PRIMARY_BORDER_FOCUSED: SECONDARY;
+ CONTROL_PRIMARY_BG_NORMAL: PRIMARY;
+ CONTROL_PRIMARY_BG_ARMED: PRIMARY_D1;
+
SCROLL_BAR_THUMB_NORMAL: GRAY_7;
SCROLL_BAR_THUMB_HOVER: GRAY_6;
- INDICATOR_BG: RED_5;
- DRAG_N_DROP_INDICATOR_BG: GRAY_5;
+
PROGRESS_INDICATOR_BEGIN: GRAY_2;
PROGRESS_INDICATOR_END: GRAY_4;
PROGRESS_BAR_BG: GRAY_8;
- PASSWORD_STRENGTH_INDICATOR_BG: GRAY_5;
- PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_0: RED_5;
- PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_1: ORANGE_5;
- PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_2: YELLOW_5;
- PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_3: GREEN_6;
- PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_4: GREEN_5;
-fx-background-color: MAIN_BG;
-fx-text-fill: TEXT_FILL;
@@ -105,7 +96,7 @@
}
.label-secondary {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
}
.label-large {
@@ -136,11 +127,11 @@
}
.glyph-icon-primary {
- -fx-fill: PRIMARY_BG;
+ -fx-fill: PRIMARY;
}
-.glyph-icon-secondary {
- -fx-fill: SECONDARY_BG;
+.glyph-icon-muted {
+ -fx-fill: TEXT_FILL_MUTED;
}
.glyph-icon-white {
@@ -158,15 +149,15 @@
******************************************************************************/
.main-window .title {
- -fx-background-color: PRIMARY_BG;
+ -fx-background-color: TITLE_BG;
}
.main-window .title .label {
- -fx-font-family: 'Dosis';
- -fx-font-size: 21px;
+ -fx-font-family: 'Quicksand';
+ -fx-font-size: 16px;
-fx-font-style: normal;
-fx-font-weight: 700;
- -fx-text-fill: white;
+ -fx-text-fill: TITLE_TEXT_FILL;
}
.main-window .title .button {
@@ -181,24 +172,23 @@
}
.main-window .title .button:armed .glyph-icon {
- -fx-fill: CONTROL_WHITE_BG_ARMED;
+ -fx-fill: GRAY_8;
}
.main-window .update-indicator {
- -fx-background-color: PRIMARY_BG, white, INDICATOR_BG;
- -fx-background-insets: 0, 1px, 2px;
- -fx-background-radius: 6px, 5px, 4px;
- -fx-translate-x: -1px;
- -fx-translate-y: 1px;
+ -fx-background-color: white, RED_5;
+ -fx-background-insets: 1px, 2px;
+ -fx-background-radius: 6px, 5px;
+ -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 2, 0, 0, 0);
}
.main-window .drag-n-drop-indicator {
- -fx-border-color: DRAG_N_DROP_INDICATOR_BG;
+ -fx-border-color: SECONDARY;
-fx-border-width: 3px;
}
.main-window .drag-n-drop-indicator .drag-n-drop-header {
- -fx-background-color: DRAG_N_DROP_INDICATOR_BG;
+ -fx-background-color: SECONDARY;
-fx-padding: 3px;
}
@@ -225,24 +215,24 @@
}
.tab-pane .tab:selected {
- -fx-background-color: PRIMARY_BG, CONTROL_PRIMARY_LIGHT_BG_NORMAL;
+ -fx-background-color: PRIMARY, CONTROL_BG_SELECTED;
}
.tab-pane .tab .tab-label {
- -fx-text-fill: SECONDARY_BG;
+ -fx-text-fill: TEXT_FILL_MUTED;
-fx-alignment: CENTER;
}
.tab-pane .tab .glyph-icon {
- -fx-fill: SECONDARY_BG;
+ -fx-fill: TEXT_FILL_MUTED;
}
.tab-pane .tab:selected .glyph-icon {
- -fx-fill: PRIMARY_BG;
+ -fx-fill: PRIMARY;
}
.tab-pane .tab:selected .tab-label {
- -fx-text-fill: TEXT_FILL_PRIMARY;
+ -fx-text-fill: TEXT_FILL_HIGHLIGHTED;
}
/*******************************************************************************
@@ -271,16 +261,16 @@
}
.list-view:focused .list-cell:selected {
- -fx-background-color: PRIMARY_BG, CONTROL_PRIMARY_LIGHT_BG_NORMAL;
+ -fx-background-color: PRIMARY, CONTROL_BG_SELECTED;
-fx-background-insets: 0, 0 0 0 3px;
}
.list-cell:selected {
- -fx-background-color: CONTROL_PRIMARY_LIGHT_BG_NORMAL;
+ -fx-background-color: CONTROL_BG_SELECTED;
}
.list-cell .glyph-icon {
- -fx-fill: SECONDARY_BG;
+ -fx-fill: TEXT_FILL_MUTED;
}
.list-cell .header-label {
@@ -289,16 +279,16 @@
}
.list-cell .detail-label {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
-fx-font-size: 0.8em;
}
.list-cell:selected .glyph-icon {
- -fx-fill: PRIMARY_BG;
+ -fx-fill: PRIMARY;
}
.list-cell:selected .header-label {
- -fx-text-fill: TEXT_FILL_PRIMARY;
+ -fx-text-fill: TEXT_FILL_HIGHLIGHTED;
}
.list-cell.drop-above {
@@ -379,13 +369,13 @@
}
.badge-primary {
- -fx-text-fill: TEXT_FILL_WHITE;
- -fx-background-color: PRIMARY_BG;
+ -fx-text-fill: white;
+ -fx-background-color: PRIMARY;
}
.badge-secondary {
- -fx-text-fill: TEXT_FILL_WHITE;
- -fx-background-color: SECONDARY_BG;
+ -fx-text-fill: white;
+ -fx-background-color: SECONDARY;
}
/*******************************************************************************
@@ -395,27 +385,27 @@
******************************************************************************/
.password-strength-indicator .segment {
- -fx-background-color: PASSWORD_STRENGTH_INDICATOR_BG;
+ -fx-background-color: CONTROL_BORDER_NORMAL;
}
.password-strength-indicator.strength-0 .segment.active {
- -fx-background-color: PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_0;
+ -fx-background-color: RED_5;
}
.password-strength-indicator.strength-1 .segment.active {
- -fx-background-color: PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_1;
+ -fx-background-color: ORANGE_5;
}
.password-strength-indicator.strength-2 .segment.active {
- -fx-background-color: PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_2;
+ -fx-background-color: YELLOW_5;
}
.password-strength-indicator.strength-3 .segment.active {
- -fx-background-color: PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_3;
+ -fx-background-color: PRIMARY;
}
.password-strength-indicator.strength-4 .segment.active {
- -fx-background-color: PASSWORD_STRENGTH_INDICATOR_BG_STRENGTH_4;
+ -fx-background-color: PRIMARY_D1;
}
/*******************************************************************************
@@ -442,8 +432,8 @@
.text-input {
-fx-cursor: text;
-fx-text-fill: TEXT_FILL;
- -fx-highlight-fill: PRIMARY_BG;
- -fx-prompt-text-fill: TEXT_FILL_SECONDARY;
+ -fx-highlight-fill: PRIMARY;
+ -fx-prompt-text-fill: TEXT_FILL_MUTED;
-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
-fx-background-insets: 0, 1px;
-fx-background-radius: 4px;
@@ -455,7 +445,7 @@
}
.text-input:disabled {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
-fx-background-color: CONTROL_BORDER_DISABLED, CONTROL_BG_DISABLED;
}
@@ -487,7 +477,7 @@
}
.text-input:disabled {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
-fx-background-color: CONTROL_BORDER_DISABLED, CONTROL_BG_DISABLED;
}
@@ -499,8 +489,8 @@
-fx-padding: 0.2em 0.5em 0.2em 0.5em;
-fx-cursor: text;
-fx-text-fill: TEXT_FILL;
- -fx-highlight-fill: PRIMARY_BG;
- -fx-prompt-text-fill: TEXT_FILL_SECONDARY;
+ -fx-highlight-fill: PRIMARY;
+ -fx-prompt-text-fill: TEXT_FILL_MUTED;
-fx-background-color: null;
}
@@ -529,7 +519,7 @@
}
.button:default {
- -fx-text-fill: TEXT_FILL_WHITE;
+ -fx-text-fill: white;
-fx-background-color: CONTROL_PRIMARY_BORDER_NORMAL, CONTROL_PRIMARY_BG_NORMAL;
}
@@ -538,25 +528,25 @@
}
.button:default:armed {
- -fx-background-color: CONTROL_PRIMARY_BORDER_NORMAL, CONTROL_PRIMARY_BG_ARMED;
+ -fx-background-color: CONTROL_PRIMARY_BORDER_ARMED, CONTROL_PRIMARY_BG_ARMED;
}
.button:disabled,
.button:default:disabled {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
-fx-background-color: CONTROL_BORDER_DISABLED, CONTROL_BG_DISABLED;
}
.button:disabled .glyph-icon {
- -fx-fill: TEXT_FILL_SECONDARY;
+ -fx-fill: TEXT_FILL_MUTED;
}
.button:default .glyph-icon {
- -fx-fill: TEXT_FILL_WHITE;
+ -fx-fill: white;
}
.button:default .label {
- -fx-text-fill: TEXT_FILL_WHITE;
+ -fx-text-fill: white;
}
.button-large {
@@ -577,7 +567,7 @@
}
.hyperlink.hyperlink-secondary {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
}
.hyperlink-hover-icon {
@@ -601,7 +591,7 @@
}
.check-box:disabled {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
}
.check-box > .box {
@@ -694,7 +684,7 @@
}
.choice-box:disabled > .label {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
}
.choice-box > .open-button {
@@ -709,7 +699,7 @@
}
.choice-box:disabled > .open-button > .arrow {
- -fx-background-color: transparent, TEXT_FILL_SECONDARY;
+ -fx-background-color: transparent, TEXT_FILL_MUTED;
}
.choice-box .context-menu {
@@ -758,7 +748,7 @@
}
.menu-item:disabled > .label {
- -fx-text-fill: TEXT_FILL_SECONDARY;
+ -fx-text-fill: TEXT_FILL_MUTED;
}
.radio-menu-item:checked > .left-container > .radio {
diff --git a/main/ui/src/main/resources/css/quicksand-bold.ttf b/main/ui/src/main/resources/css/quicksand-bold.ttf
new file mode 100644
index 000000000..f538c6fa3
Binary files /dev/null and b/main/ui/src/main/resources/css/quicksand-bold.ttf differ
diff --git a/main/ui/src/main/resources/fxml/addvault_welcome.fxml b/main/ui/src/main/resources/fxml/addvault_welcome.fxml
index 06911a9f8..faa974931 100644
--- a/main/ui/src/main/resources/fxml/addvault_welcome.fxml
+++ b/main/ui/src/main/resources/fxml/addvault_welcome.fxml
@@ -21,7 +21,7 @@
-
+
@@ -29,7 +29,7 @@