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