Merge remote-tracking branch 'origin/develop' into 1996-cli-version

# Conflicts:
#	src/main/java/org/cryptomator/launcher/Cryptomator.java
This commit is contained in:
JaniruTEC
2022-04-12 17:44:13 +02:00
221 changed files with 4488 additions and 2785 deletions

View File

@@ -4,6 +4,8 @@ import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
module org.cryptomator.desktop {
requires static org.jetbrains.annotations;
requires org.cryptomator.cryptofs;
requires org.cryptomator.frontend.dokany;
requires org.cryptomator.frontend.fuse;
@@ -36,6 +38,8 @@ module org.cryptomator.desktop {
opens org.cryptomator.common.settings to com.google.gson;
opens org.cryptomator.launcher to javafx.graphics;
opens org.cryptomator.common to javafx.fxml;
opens org.cryptomator.common.vaults to javafx.fxml;
opens org.cryptomator.ui.addvaultwizard to javafx.fxml;

View File

@@ -89,11 +89,6 @@ public class Environment {
return Boolean.getBoolean("cryptomator.showTrayIcon");
}
@Deprecated // TODO: remove as soon as custom mount path works properly on Win+Fuse
public boolean useExperimentalFuse() {
return Boolean.getBoolean("fuse.experimental");
}
private int getInt(String propertyName, int defaultValue) {
String value = System.getProperty(propertyName);
try {

View File

@@ -0,0 +1,108 @@
package org.cryptomator.common;
import javax.security.auth.Destroyable;
import java.util.Arrays;
/**
* A destroyable CharSequence.
*/
public class Passphrase implements Destroyable, CharSequence {
private final char[] data;
private final int offset;
private final int length;
private boolean destroyed;
/**
* Wraps (doesn't copy) the given data.
*
* @param data The wrapped data. Any changes to this will be reflected in this passphrase
*/
public Passphrase(char[] data) {
this(data, 0, data.length);
}
/**
* Wraps (doesn't copy) a subarray of the given data.
*
* @param data The wrapped data. Any changes to this will be reflected in this passphrase
* @param offset The subarray offset, i.e. the first character of this passphrase
* @param length The subarray length, i.e. the length of this passphrase
*/
public Passphrase(char[] data, int offset, int length) {
if (offset < 0 || length < 0 || offset + length > data.length) {
throw new IndexOutOfBoundsException("[%1$d %1$d + %2$d[ not within [0, %3$d[".formatted(offset, length, data.length));
}
this.data = data;
this.offset = offset;
this.length = length;
}
public static Passphrase copyOf(CharSequence cs) {
char[] result = new char[cs.length()];
for (int i = 0; i < cs.length(); i++) {
result[i] = cs.charAt(i);
}
return new Passphrase(result);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Passphrase that = (Passphrase) o;
// time-constant comparison
int diff = 0;
for (int i = 0; i < length; i++) {
diff |= charAt(i) ^ that.charAt(i);
}
return diff == 0;
}
@Override
public int hashCode() {
// basically Arrays.hashCode, but only for a certain subarray
int result = 1;
for (int i = 0; i < length; i++) {
result = 31 * result + charAt(i);
}
return result;
}
@Override
public String toString() {
return new String(data, offset, length);
}
@Override
public int length() {
return length;
}
@Override
public char charAt(int index) {
if (index < 0 || index >= length) {
throw new IndexOutOfBoundsException("%d not within [0, %d[".formatted(index, length));
}
return data[offset + index];
}
@Override
public Passphrase subSequence(int start, int end) {
if (start < 0 || end < 0 || end > length || start > end) {
throw new IndexOutOfBoundsException("[%d, %d[ not within [0, %d[".formatted(start, end, length));
}
return new Passphrase(Arrays.copyOfRange(data, offset + start, offset + end));
}
@Override
public boolean isDestroyed() {
return destroyed;
}
@Override
public void destroy() {
Arrays.fill(data, offset, offset + length, '\0');
destroyed = true;
}
}

View File

@@ -5,6 +5,9 @@ import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Volume;
import javax.inject.Inject;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
@@ -27,4 +30,13 @@ class CustomDriveLetterChooser implements MountPointChooser {
public Optional<Path> chooseMountPoint(Volume caller) {
return this.vaultSettings.getWinDriveLetter().map(letter -> letter.charAt(0) + ":\\").map(Paths::get);
}
@Override
public boolean prepare(Volume caller, Path driveLetter) throws InvalidMountPointException {
if (!Files.notExists(driveLetter, LinkOption.NOFOLLOW_LINKS)) {
//Drive already exists OR can't be determined
throw new InvalidMountPointException(new FileAlreadyExistsException(driveLetter.toString()));
}
return false;
}
}

View File

@@ -4,6 +4,7 @@ import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Volume;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -11,32 +12,33 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Optional;
class CustomMountPointChooser implements MountPointChooser {
private static final String HIDEAWAY_PREFIX = ".~$";
private static final String HIDEAWAY_SUFFIX = ".tmp";
private static final String WIN_HIDDEN = "dos:hidden";
private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class);
private final VaultSettings vaultSettings;
private final Environment environment;
@Inject
public CustomMountPointChooser(VaultSettings vaultSettings, Environment environment) {
public CustomMountPointChooser(VaultSettings vaultSettings) {
this.vaultSettings = vaultSettings;
this.environment = environment;
}
@Override
public boolean isApplicable(Volume caller) {
//Disable if useExperimentalFuse is required (Win + Fuse), but set to false
return caller.getImplementationType() != VolumeImpl.FUSE || !SystemUtils.IS_OS_WINDOWS || environment.useExperimentalFuse();
return caller.getImplementationType() != VolumeImpl.WEBDAV;
}
@Override
@@ -47,49 +49,102 @@ class CustomMountPointChooser implements MountPointChooser {
@Override
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
switch (caller.getMountPointRequirement()) {
case PARENT_NO_MOUNT_POINT -> prepareParentNoMountPoint(mountPoint);
case EMPTY_MOUNT_POINT -> prepareEmptyMountPoint(mountPoint);
case NONE -> {
//Requirement "NONE" doesn't make any sense here.
//No need to prepare/verify a Mountpoint without requiring one...
return switch (caller.getMountPointRequirement()) {
case PARENT_NO_MOUNT_POINT -> {
prepareParentNoMountPoint(mountPoint);
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
yield true;
}
case EMPTY_MOUNT_POINT -> {
prepareEmptyMountPoint(mountPoint);
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
yield false;
}
case NONE, UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT -> {
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
}
default -> {
//Currently the case for "PARENT_OPT_MOUNT_POINT"
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
}
}
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
return false;
};
}
private void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException {
//This the case on Windows when using FUSE
//See https://github.com/billziss-gh/winfsp/issues/320
Path parent = mountPoint.getParent();
if (!Files.isDirectory(parent)) {
throw new InvalidMountPointException(new NotDirectoryException(parent.toString()));
}
//We must use #notExists() here because notExists =/= !exists (see docs)
if (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) {
//File exists OR can't be determined
throw new InvalidMountPointException(new FileAlreadyExistsException(mountPoint.toString()));
//This is case on Windows when using FUSE
//See https://github.com/billziss-gh/winfsp/issues/320
void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException {
Path hideaway = getHideaway(mountPoint);
var mpExists = Files.exists(mountPoint, LinkOption.NOFOLLOW_LINKS);
var hideExists = Files.exists(hideaway, LinkOption.NOFOLLOW_LINKS);
//TODO: possible improvement by just deleting an _empty_ hideaway
if (mpExists && hideExists) { //both resources exist (whatever type)
throw new InvalidMountPointException(new FileAlreadyExistsException(hideaway.toString()));
} else if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist
throw new InvalidMountPointException(new NoSuchFileException(mountPoint.toString()));
} else if (!mpExists) { //only hideaway exists
checkIsDirectory(hideaway);
LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint);
try {
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS);
}
} catch (IOException e) {
throw new InvalidMountPointException(e);
}
} else { //only mountpoint exists
try {
checkIsDirectory(mountPoint);
checkIsEmpty(mountPoint);
Files.move(mountPoint, hideaway);
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS);
}
} catch (IOException e) {
throw new InvalidMountPointException(e);
}
}
}
private void prepareEmptyMountPoint(Path mountPoint) throws InvalidMountPointException {
//This is the case for Windows when using Dokany and for Linux and Mac
if (!Files.isDirectory(mountPoint)) {
throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString()));
}
try (DirectoryStream<Path> ds = Files.newDirectoryStream(mountPoint)) {
if (ds.iterator().hasNext()) {
throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString()));
}
checkIsDirectory(mountPoint);
try {
checkIsEmpty(mountPoint);
} catch (IOException exception) {
throw new InvalidMountPointException("IOException while checking folder content", exception);
}
}
@Override
public void cleanup(Volume caller, Path mountPoint) {
if (caller.getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT) {
Path hideaway = getHideaway(mountPoint);
try {
Files.move(hideaway, mountPoint);
if (SystemUtils.IS_OS_WINDOWS) {
Files.setAttribute(mountPoint, WIN_HIDDEN, false);
}
} catch (IOException e) {
LOG.error("Unable to clean up mountpoint {} for Winfsp mounting.", mountPoint, e);
}
}
}
private void checkIsDirectory(Path toCheck) throws InvalidMountPointException {
if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) {
throw new InvalidMountPointException(new NotDirectoryException(toCheck.toString()));
}
}
private void checkIsEmpty(Path toCheck) throws InvalidMountPointException, IOException {
try (var dirStream = Files.list(toCheck)) {
if (dirStream.findFirst().isPresent()) {
throw new InvalidMountPointException(new DirectoryNotEmptyException(toCheck.toString()));
}
}
}
//visible for testing
Path getHideaway(Path mountPoint) {
return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX);
}
}

View File

@@ -65,7 +65,7 @@ class TemporaryMountPointChooser implements MountPointChooser {
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
}
default -> {
//Currently the case for "PARENT_OPT_MOUNT_POINT"
//Currently the case for "UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT"
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
}
}

View File

@@ -44,6 +44,7 @@ public class Settings {
public static final String DEFAULT_LICENSE_KEY = "";
public static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false;
public static final String DEFAULT_DISPLAY_CONFIGURATION = "";
public static final String DEFAULT_LANGUAGE = null;
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
@@ -66,6 +67,7 @@ public class Settings {
private final IntegerProperty windowWidth = new SimpleIntegerProperty();
private final IntegerProperty windowHeight = new SimpleIntegerProperty();
private final ObjectProperty<String> displayConfiguration = new SimpleObjectProperty<>(DEFAULT_DISPLAY_CONFIGURATION);
private final StringProperty language = new SimpleStringProperty(DEFAULT_LANGUAGE);
private Consumer<Settings> saveCmd;
@@ -96,6 +98,7 @@ public class Settings {
windowWidth.addListener(this::somethingChanged);
windowHeight.addListener(this::somethingChanged);
displayConfiguration.addListener(this::somethingChanged);
language.addListener(this::somethingChanged);
}
void setSaveCmd(Consumer<Settings> saveCmd) {
@@ -191,4 +194,8 @@ public class Settings {
public ObjectProperty<String> displayConfigurationProperty() {
return displayConfiguration;
}
public StringProperty languageProperty() {
return language;
}
}

View File

@@ -57,6 +57,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
out.name("windowWidth").value((value.windowWidthProperty().get()));
out.name("windowHeight").value((value.windowHeightProperty().get()));
out.name("displayConfiguration").value((value.displayConfigurationProperty().get()));
out.name("language").value((value.languageProperty().get()));
out.endObject();
}
@@ -97,6 +98,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "windowWidth" -> settings.windowWidthProperty().set(in.nextInt());
case "windowHeight" -> settings.windowHeightProperty().set(in.nextInt());
case "displayConfiguration" -> settings.displayConfigurationProperty().set(in.nextString());
case "language" -> settings.languageProperty().set(in.nextString());
default -> {
LOG.warn("Unsupported vault setting found in JSON: " + name);

View File

@@ -3,9 +3,9 @@ package org.cryptomator.common.settings;
import org.apache.commons.lang3.SystemUtils;
public enum UiTheme {
LIGHT("preferences.general.theme.light"), //
DARK("preferences.general.theme.dark"), //
AUTOMATIC("preferences.general.theme.automatic");
LIGHT("preferences.interface.theme.light"), //
DARK("preferences.interface.theme.dark"), //
AUTOMATIC("preferences.interface.theme.automatic");
public static UiTheme[] applicableValues() {
if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS) {

View File

@@ -9,6 +9,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface DefaultMountFlags {
@interface DefaultMountFlags {
}

View File

@@ -86,7 +86,7 @@ public class DokanyVolume extends AbstractVolume {
@Override
public MountPointRequirement getMountPointRequirement() {
return MountPointRequirement.EMPTY_MOUNT_POINT;
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.EMPTY_MOUNT_POINT;
}
public static boolean isSupportedStatic() {

View File

@@ -4,6 +4,7 @@ import com.google.common.collect.Iterators;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.mountpoint.MountPointChooser;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
@@ -28,11 +29,14 @@ public class FuseVolume extends AbstractVolume {
private static final Logger LOG = LoggerFactory.getLogger(FuseVolume.class);
private static final Pattern NON_WHITESPACE_OR_QUOTED = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'"); // Thanks to https://stackoverflow.com/a/366532
private final VaultSettings vaultSettings;
private Mount mount;
@Inject
public FuseVolume(@Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
public FuseVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
super(choosers);
this.vaultSettings = vaultSettings;
}
@Override
@@ -50,7 +54,7 @@ public class FuseVolume extends AbstractVolume {
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
.build();
this.mount = mounter.mount(root, envVars, onExitAction);
} catch ( FuseMountException | FuseNotSupportedException e) {
} catch (FuseMountException | FuseNotSupportedException e) {
throw new VolumeException("Unable to mount Filesystem", e);
}
}
@@ -119,7 +123,10 @@ public class FuseVolume extends AbstractVolume {
@Override
public MountPointRequirement getMountPointRequirement() {
return SystemUtils.IS_OS_WINDOWS ? MountPointRequirement.PARENT_NO_MOUNT_POINT : MountPointRequirement.EMPTY_MOUNT_POINT;
if (!SystemUtils.IS_OS_WINDOWS) {
return MountPointRequirement.EMPTY_MOUNT_POINT;
}
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.PARENT_NO_MOUNT_POINT;
}
public static boolean isSupportedStatic() {

View File

@@ -6,6 +6,11 @@ package org.cryptomator.common.vaults;
*/
public enum MountPointRequirement {
/**
* The Mountpoint needs to be a filesystem root and must not exist.
*/
UNUSED_ROOT_DIR,
/**
* No Mountpoint on the local filesystem required. (e.g. WebDAV)
*/

View File

@@ -0,0 +1,13 @@
package org.cryptomator.launcher;
import java.nio.file.Path;
import java.util.Collection;
public record AppLaunchEvent(AppLaunchEvent.EventType type, Collection<Path> pathsToOpen) {
public enum EventType {
REVEAL_APP,
OPEN_FILE
}
}

View File

@@ -13,44 +13,43 @@ import org.cryptomator.common.ShutdownHook;
import org.cryptomator.ipc.IpcCommunicator;
import org.cryptomator.logging.DebugMode;
import org.cryptomator.logging.LoggerConfiguration;
import org.cryptomator.ui.launcher.UiLauncher;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javafx.application.Application;
import javafx.stage.Stage;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
@Singleton
public class Cryptomator {
private static final long STARTUP_TIME = System.currentTimeMillis();
// DaggerCryptomatorComponent gets generated by Dagger.
// Run Maven and include target/generated-sources/annotations in your IDE.
private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.create();
private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.factory().create(STARTUP_TIME);
private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
private final LoggerConfiguration logConfig;
private final DebugMode debugMode;
private final SupportedLanguages supportedLanguages;
private final Environment env;
private final Lazy<IpcMessageHandler> ipcMessageHandler;
private final CountDownLatch shutdownLatch;
private final ShutdownHook shutdownHook;
private final Lazy<UiLauncher> uiLauncher;
@Inject
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, Lazy<UiLauncher> uiLauncher) {
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, SupportedLanguages supportedLanguages, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, ShutdownHook shutdownHook) {
this.logConfig = logConfig;
this.debugMode = debugMode;
this.supportedLanguages = supportedLanguages;
this.env = env;
this.ipcMessageHandler = ipcMessageHandler;
this.shutdownLatch = shutdownLatch;
this.shutdownHook = shutdownHook;
this.uiLauncher = uiLauncher;
}
public static void main(String[] args) {
@@ -82,8 +81,10 @@ public class Cryptomator {
*/
private int run(String[] args) {
logConfig.init();
LOG.debug("Dagger graph initialized after {}ms", System.currentTimeMillis() - STARTUP_TIME);
LOG.info("Starting Cryptomator {} on {} {} ({})", env.getAppVersion().orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
debugMode.initialize();
supportedLanguages.applyPreferred();
/*
* Attempts to create an IPC connection to a running Cryptomator instance and sends it the given args.
@@ -94,7 +95,7 @@ public class Cryptomator {
communicator.sendHandleLaunchargs(List.of(args));
communicator.sendRevealRunningApp();
LOG.info("Found running application instance. Shutting down...");
return 2;
return 0;
} else {
shutdownHook.runOnShutdown(communicator::closeUnchecked);
var executor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("IPC-%d").build());
@@ -111,21 +112,38 @@ public class Cryptomator {
}
/**
* Launches the JavaFX application and waits until shutdown is requested.
* Launches the JavaFX application, blocking the main thread until shuts down.
*
* @return Nonzero exit code in case of an error.
* @implNote This method blocks until {@link #shutdownLatch} reached zero.
*/
private int runGuiApplication() {
try {
uiLauncher.get().launch();
shutdownLatch.await();
Application.launch(MainApp.class);
LOG.info("UI shut down");
return 0;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
LOG.error("Terminating due to error", e);
return 1;
}
}
public static class MainApp extends Application {
@Override
public void start(Stage primaryStage) {
LOG.info("JavaFX runtime started after {}ms", System.currentTimeMillis() - STARTUP_TIME);
FxApplicationComponent component = CRYPTOMATOR_COMPONENT.fxAppComponentBuilder() //
.fxApplication(this) //
.primaryStage(primaryStage) //
.build();
component.application().start();
}
@Override
public void stop() {
LOG.info("JavaFX application stopped.");
}
}
}

View File

@@ -1,16 +1,25 @@
package org.cryptomator.launcher;
import dagger.BindsInstance;
import dagger.Component;
import org.cryptomator.common.CommonsModule;
import org.cryptomator.logging.LoggerModule;
import org.cryptomator.ui.launcher.UiLauncherModule;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import javax.inject.Named;
import javax.inject.Singleton;
@Singleton
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class, UiLauncherModule.class})
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class})
public interface CryptomatorComponent {
Cryptomator application();
FxApplicationComponent.Builder fxAppComponentBuilder();
@Component.Factory
interface Factory {
CryptomatorComponent create(@BindsInstance @Named("startupTime") long startupTime);
}
}

View File

@@ -2,20 +2,55 @@ package org.cryptomator.launcher;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.PluginClassLoader;
import org.cryptomator.integrations.autostart.AutoStartProvider;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.ResourceBundle;
import java.util.ServiceLoader;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
@Module
@Module(subcomponents = {FxApplicationComponent.class})
class CryptomatorModule {
@Provides
@Singleton
@Named("shutdownLatch")
static CountDownLatch provideShutdownLatch() {
return new CountDownLatch(1);
static ResourceBundle provideLocalization() {
return ResourceBundle.getBundle("i18n.strings");
}
@Provides
@Singleton
@Named("launchEventQueue")
static BlockingQueue<AppLaunchEvent> provideFileOpenRequests() {
return new ArrayBlockingQueue<>(10);
}
// TODO: still needed after integrations-api 1.1.0?
@Provides
@Singleton
static Optional<UiAppearanceProvider> provideAppearanceProvider(PluginClassLoader classLoader) {
return ServiceLoader.load(UiAppearanceProvider.class, classLoader).findFirst();
}
@Provides
@Singleton
static Optional<AutoStartProvider> provideAutostartProvider(PluginClassLoader classLoader) {
return ServiceLoader.load(AutoStartProvider.class, classLoader).findFirst();
}
@Provides
@Singleton
static Optional<TrayIntegrationProvider> provideTrayIntegrationProvider(PluginClassLoader classLoader) {
return ServiceLoader.load(TrayIntegrationProvider.class, classLoader).findFirst();
}
}

View File

@@ -6,7 +6,6 @@
*******************************************************************************/
package org.cryptomator.launcher;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -20,12 +19,10 @@ import java.nio.file.FileSystem;
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.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Collectors;
@Singleton
class FileOpenRequestHandler {

View File

@@ -1,7 +1,6 @@
package org.cryptomator.launcher;
import org.cryptomator.ipc.IpcMessageListener;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@@ -0,0 +1,39 @@
package org.cryptomator.launcher;
import org.cryptomator.common.settings.Settings;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.List;
import java.util.Locale;
@Singleton
public class SupportedLanguages {
private static final Logger LOG = LoggerFactory.getLogger(SupportedLanguages.class);
// these are BCP 47 language codes, not ISO. Note the "-" instead of the "_":
public static final List<String> LANGUAGAE_TAGS = List.of("en", "ar", "bn", "bs", "ca", "cs", "de", "el", "es", "fil", "fr", "gl", "he", //
"hi", "hr", "hu", "id", "it", "ja", "ko", "lv", "mk", "nb", "nl", "nn", "no", "pa", "pl", "pt", "pt-BR", "ro", "ru", "sk", "sr", //
"sr-Latn", "sv", "ta", "te", "th", "tr", "uk", "zh", "zh-HK", "zh-TW");
@Nullable
private final String preferredLanguage;
@Inject
public SupportedLanguages(Settings settings) {
this.preferredLanguage = settings.languageProperty().get();
}
public void applyPreferred() {
if (preferredLanguage == null) {
LOG.debug("Using system locale");
return;
}
var preferredLocale = Locale.forLanguageTag(preferredLanguage);
LOG.debug("Applying preferred locale {}", preferredLocale.getDisplayName(Locale.ENGLISH));
Locale.setDefault(preferredLocale);
}
}

View File

@@ -14,7 +14,7 @@ import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.fxapp.PrimaryStage;
import org.cryptomator.ui.recoverykey.RecoveryKeyDisplayController;
import javax.inject.Named;
@@ -43,12 +43,12 @@ public abstract class AddVaultModule {
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
static Stage provideStage(StageFactory factory, @PrimaryStage Stage primaryStage, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("addvaultwizard.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.initOwner(primaryStage);
return stage;
}

View File

@@ -2,25 +2,24 @@ package org.cryptomator.ui.addvaultwizard;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import javax.inject.Inject;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.Optional;
@AddVaultWizardScoped
public class AddVaultSuccessController implements FxController {
private final FxApplication fxApplication;
private final FxApplicationWindows appWindows;
private final Stage window;
private final ReadOnlyObjectProperty<Vault> vault;
@Inject
AddVaultSuccessController(FxApplication fxApplication, @AddVaultWizardWindow Stage window, @AddVaultWizardWindow ObjectProperty<Vault> vault) {
this.fxApplication = fxApplication;
AddVaultSuccessController(FxApplicationWindows appWindows, @AddVaultWizardWindow Stage window, @AddVaultWizardWindow ObjectProperty<Vault> vault) {
this.appWindows = appWindows;
this.window = window;
this.vault = vault;
}
@@ -28,7 +27,7 @@ public class AddVaultSuccessController implements FxController {
@FXML
public void unlockAndClose() {
close();
fxApplication.startUnlockWorkflow(vault.get(), Optional.of(window));
appWindows.startUnlockWorkflow(vault.get(), window);
}
@FXML

View File

@@ -4,10 +4,10 @@ import dagger.Lazy;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -20,9 +20,6 @@ import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ResourceBundle;
@@ -34,7 +31,7 @@ public class ChooseExistingVaultController implements FxController {
private final Stage window;
private final Lazy<Scene> welcomeScene;
private final Lazy<Scene> successScene;
private final ErrorComponent.Builder errorComponent;
private final FxApplicationWindows appWindows;
private final ObjectProperty<Path> vaultPath;
private final ObjectProperty<Vault> vault;
private final VaultListManager vaultListManager;
@@ -43,11 +40,11 @@ public class ChooseExistingVaultController implements FxController {
private Image screenshot;
@Inject
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle) {
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, FxApplicationWindows appWindows, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle) {
this.window = window;
this.welcomeScene = welcomeScene;
this.successScene = successScene;
this.errorComponent = errorComponent;
this.appWindows = appWindows;
this.vaultPath = vaultPath;
this.vault = vault;
this.vaultListManager = vaultListManager;
@@ -82,7 +79,7 @@ public class ChooseExistingVaultController implements FxController {
window.setScene(successScene.get());
} catch (IOException e) {
LOG.error("Failed to open existing vault.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
appWindows.showErrorWindow(e, window, window.getScene());
}
}
}

View File

@@ -35,14 +35,13 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import java.util.UUID;
@AddVaultWizardScoped
public class CreateNewVaultLocationController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultLocationController.class);
private static final Path DEFAULT_CUSTOM_VAULT_PATH = Paths.get(System.getProperty("user.home"));
private static final String TEMP_FILE_FORMAT = "cryptomator-%s.tmp";
private static final String TEMP_FILE_FORMAT = ".locationTest.cryptomator.tmp";
private final Stage window;
private final Lazy<Scene> chooseNameScene;
@@ -112,7 +111,7 @@ public class CreateNewVaultLocationController implements FxController {
}
private boolean isActuallyWritable(Path p) {
Path tmpFile = p.resolve(String.format(TEMP_FILE_FORMAT, UUID.randomUUID()));
Path tmpFile = p.resolve(TEMP_FILE_FORMAT);
try (var chan = Files.newByteChannel(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) {
return true;
} catch (IOException e) {

View File

@@ -10,12 +10,12 @@ import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.Tasks;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
import org.slf4j.Logger;
@@ -60,7 +60,7 @@ public class CreateNewVaultPasswordController implements FxController {
private final Lazy<Scene> chooseLocationScene;
private final Lazy<Scene> recoveryKeyScene;
private final Lazy<Scene> successScene;
private final ErrorComponent.Builder errorComponent;
private final FxApplicationWindows appWindows;
private final ExecutorService executor;
private final RecoveryKeyFactory recoveryKeyFactory;
private final StringProperty vaultNameProperty;
@@ -83,12 +83,12 @@ public class CreateNewVaultPasswordController implements FxController {
public NewPasswordController newPasswordSceneController;
@Inject
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, ReadmeGenerator readmeGenerator, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, FxApplicationWindows appWindows, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, ReadmeGenerator readmeGenerator, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
this.window = window;
this.chooseLocationScene = chooseLocationScene;
this.recoveryKeyScene = recoveryKeyScene;
this.successScene = successScene;
this.errorComponent = errorComponent;
this.appWindows = appWindows;
this.executor = executor;
this.recoveryKeyFactory = recoveryKeyFactory;
this.vaultNameProperty = vaultName;
@@ -127,7 +127,7 @@ public class CreateNewVaultPasswordController implements FxController {
Files.createDirectory(pathToVault);
} catch (IOException e) {
LOG.error("Failed to create vault directory.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
appWindows.showErrorWindow(e, window, window.getScene());
return;
}
@@ -152,7 +152,7 @@ public class CreateNewVaultPasswordController implements FxController {
window.setScene(recoveryKeyScene.get());
}).onError(IOException.class, e -> {
LOG.error("Failed to initialize vault.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
appWindows.showErrorWindow(e, window, window.getScene());
}).andFinally(() -> {
processing.set(false);
}).runOnce(executor);
@@ -168,7 +168,7 @@ public class CreateNewVaultPasswordController implements FxController {
window.setScene(successScene.get());
}).onError(IOException.class, e -> {
LOG.error("Failed to initialize vault.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
appWindows.showErrorWindow(e, window, window.getScene());
}).andFinally(() -> {
processing.set(false);
}).runOnce(executor);

View File

@@ -8,10 +8,10 @@ import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,7 +26,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.security.SecureRandom;
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@@ -38,9 +37,8 @@ public class ChangePasswordController implements FxController {
private final Stage window;
private final Vault vault;
private final ErrorComponent.Builder errorComponent;
private final FxApplicationWindows appWindows;
private final KeychainManager keychain;
private final SecureRandom csprng;
private final MasterkeyFileAccess masterkeyFileAccess;
public NiceSecurePasswordField oldPasswordField;
@@ -49,12 +47,11 @@ public class ChangePasswordController implements FxController {
public NewPasswordController newPasswordController;
@Inject
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, FxApplicationWindows appWindows, KeychainManager keychain, MasterkeyFileAccess masterkeyFileAccess) {
this.window = window;
this.vault = vault;
this.errorComponent = errorComponent;
this.appWindows = appWindows;
this.keychain = keychain;
this.csprng = csprng;
this.masterkeyFileAccess = masterkeyFileAccess;
}
@@ -95,7 +92,7 @@ public class ChangePasswordController implements FxController {
oldPasswordField.requestFocus();
} catch (IOException | CryptoException e) {
LOG.error("Password change failed. Unable to perform operation.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
appWindows.showErrorWindow(e, window, window.getScene());
}
}

View File

@@ -4,7 +4,6 @@ import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.Nullable;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
@@ -16,34 +15,17 @@ public interface ErrorComponent {
@FxmlScene(FxmlFile.ERROR)
Scene scene();
default void showErrorScene() {
if (Platform.isFxApplicationThread()) {
show();
} else {
Platform.runLater(this::show);
}
}
private void show() {
default Stage show() {
Stage stage = window();
stage.setScene(scene());
stage.show();
return stage;
}
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder cause(Throwable cause);
@BindsInstance
Builder window(Stage window);
@BindsInstance
Builder returnToScene(@Nullable Scene previousScene);
ErrorComponent build();
@Subcomponent.Factory
interface Factory {
ErrorComponent create(@BindsInstance Throwable cause, @BindsInstance Stage window, @BindsInstance @Nullable Scene previousScene);
}
}

View File

@@ -42,7 +42,7 @@ public enum FxmlFile {
this.ressourcePathString = ressourcePathString;
}
String getRessourcePathString() {
public String getRessourcePathString() {
return ressourcePathString;
}
}

View File

@@ -22,6 +22,10 @@ public class FxmlLoaderFactory {
this.resourceBundle = resourceBundle;
}
public static <T extends FxController> FxmlLoaderFactory forController(T controller, Function<Parent, Scene> sceneFactory, ResourceBundle resourceBundle) {
return new FxmlLoaderFactory(Map.of(controller.getClass(), () -> controller), sceneFactory, resourceBundle);
}
/**
* @return A new FXMLLoader instance
*/

View File

@@ -1,23 +1,24 @@
package org.cryptomator.ui.common;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import javax.inject.Inject;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.util.function.Consumer;
@FxApplicationScoped
public class StageFactory {
private final Consumer<Stage> initializer;
public StageFactory(Consumer<Stage> initializer) {
@Inject
public StageFactory(StageInitializer initializer) {
this.initializer = initializer;
}
public Stage create() {
return create(StageStyle.DECORATED);
}
public Stage create(StageStyle stageStyle) {
Stage stage = new Stage(stageStyle);
Stage stage = new Stage(StageStyle.DECORATED);
initializer.accept(stage);
return stage;
}

View File

@@ -0,0 +1,32 @@
package org.cryptomator.ui.common;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import javax.inject.Inject;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import java.util.List;
import java.util.function.Consumer;
/**
* Performs common setup for all stages
*/
@FxApplicationScoped
public class StageInitializer implements Consumer<Stage> {
private final List<Image> windowIcons;
@Inject
public StageInitializer() {
this.windowIcons = SystemUtils.IS_OS_MAC ? List.of() : List.of( //
new Image(StageInitializer.class.getResource("/img/window_icon_32.png").toString()), //
new Image(StageInitializer.class.getResource("/img/window_icon_512.png").toString()) //
);
}
@Override
public void accept(Stage stage) {
stage.getIcons().setAll(windowIcons);
}
}

View File

@@ -1,61 +0,0 @@
package org.cryptomator.ui.common;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UserInteractionLock<E extends Enum<E>> {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private final BooleanProperty awaitingInteraction = new SimpleBooleanProperty();
private final AtomicBoolean interacted = new AtomicBoolean();
private final AtomicReference<E> state;
public UserInteractionLock(E initialValue) {
this.state = new AtomicReference<>(initialValue);
}
public synchronized void reset(E value) {
state.set(value);
interacted.set(false);
}
public void interacted(E result) {
assert Platform.isFxApplicationThread();
lock.lock();
try {
state.set(result);
interacted.set(true);
awaitingInteraction.set(false);
condition.signal();
} finally {
lock.unlock();
}
}
public E awaitInteraction() throws InterruptedException {
assert !Platform.isFxApplicationThread();
lock.lock();
try {
Platform.runLater(() -> awaitingInteraction.set(true));
while (!interacted.get()) {
condition.await();
}
return state.get();
} finally {
lock.unlock();
}
}
public ReadOnlyBooleanProperty awaitingInteraction() {
return awaitingInteraction;
}
}

View File

@@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.concurrent.Task;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
@@ -53,7 +54,9 @@ public class VaultService {
*
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
* @deprecated use {@link org.cryptomator.ui.fxapp.FxApplicationWindows#startLockWorkflow(Vault, Stage)}
*/
@Deprecated
public void lock(Vault vault, boolean forced) {
executorService.execute(createLockTask(vault, forced));
}
@@ -90,7 +93,7 @@ public class VaultService {
* @return Meta-Task that waits until all vaults are locked or fails after the first failure of a subtask
*/
public Task<Collection<Vault>> createLockAllTask(Collection<Vault> vaults, boolean forced) {
List<Task<Vault>> lockTasks = vaults.stream().map(v -> new LockVaultTask(v, forced)).collect(Collectors.toUnmodifiableList());
List<Task<Vault>> lockTasks = vaults.stream().<Task<Vault>>map(v -> new LockVaultTask(v, forced)).toList();
lockTasks.forEach(executorService::execute);
Task<Collection<Vault>> task = new WaitForTasksTask(lockTasks);
String vaultNames = vaults.stream().map(Vault::getDisplayName).collect(Collectors.joining(", "));

View File

@@ -1,5 +1,7 @@
package org.cryptomator.ui.controls;
import org.cryptomator.common.Passphrase;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
@@ -82,7 +84,7 @@ public class NiceSecurePasswordField extends StackPane {
return passwordField.textProperty();
}
public CharSequence getCharacters() {
public Passphrase getCharacters() {
return passwordField.getCharacters();
}

View File

@@ -9,6 +9,7 @@
package org.cryptomator.ui.controls;
import com.google.common.base.Strings;
import org.cryptomator.common.Passphrase;
import javafx.application.Platform;
import javafx.beans.NamedArg;
@@ -28,7 +29,6 @@ import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.TransferMode;
import java.nio.CharBuffer;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Arrays;
@@ -203,8 +203,8 @@ public class SecurePasswordField extends TextField {
* @see #wipe()
*/
@Override
public CharSequence getCharacters() {
return CharBuffer.wrap(content, 0, length);
public Passphrase getCharacters() {
return new Passphrase(content, 0, length);
}
/**

View File

@@ -1,14 +1,14 @@
package org.cryptomator.ui.launcher;
package org.cryptomator.ui.fxapp;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.launcher.AppLaunchEvent;
import org.cryptomator.ui.common.VaultService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javafx.application.Platform;
import java.io.IOException;
import java.nio.file.Path;
@@ -17,22 +17,25 @@ import java.util.concurrent.ExecutorService;
import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_EXT;
@Singleton
// TODO: use message bus
@FxApplicationScoped
class AppLaunchEventHandler {
private static final Logger LOG = LoggerFactory.getLogger(AppLaunchEventHandler.class);
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
private final ExecutorService executorService;
private final FxApplicationStarter fxApplicationStarter;
private final FxApplicationWindows appWindows;
private final VaultListManager vaultListManager;
private final VaultService vaultService;
@Inject
public AppLaunchEventHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExecutorService executorService, FxApplicationStarter fxApplicationStarter, VaultListManager vaultListManager) {
public AppLaunchEventHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExecutorService executorService, FxApplicationWindows appWindows, VaultListManager vaultListManager, VaultService vaultService) {
this.launchEventQueue = launchEventQueue;
this.executorService = executorService;
this.fxApplicationStarter = fxApplicationStarter;
this.appWindows = appWindows;
this.vaultListManager = vaultListManager;
this.vaultService = vaultService;
}
public void startHandlingLaunchEvents() {
@@ -52,14 +55,12 @@ class AppLaunchEventHandler {
}
private void handleLaunchEvent(AppLaunchEvent event) {
switch (event.getType()) {
case REVEAL_APP -> fxApplicationStarter.get().thenAccept(FxApplication::showMainWindow);
case OPEN_FILE -> fxApplicationStarter.get().thenRun(() -> {
Platform.runLater(() -> {
event.getPathsToOpen().forEach(this::addOrRevealVault);
});
switch (event.type()) {
case REVEAL_APP -> appWindows.showMainWindow();
case OPEN_FILE -> Platform.runLater(() -> {
event.pathsToOpen().forEach(this::addOrRevealVault);
});
default -> LOG.warn("Unsupported event type: {}", event.getType());
default -> LOG.warn("Unsupported event type: {}", event.type());
}
}
@@ -75,7 +76,7 @@ class AppLaunchEventHandler {
}
if (v.isUnlocked()) {
fxApplicationStarter.get().thenAccept(app -> app.getVaultService().reveal(v));
vaultService.reveal(v);
}
LOG.debug("Added vault {}", potentialVaultPath);
} catch (IOException e) {

View File

@@ -0,0 +1,26 @@
package org.cryptomator.ui.fxapp;
import org.cryptomator.common.vaults.Vault;
import javax.inject.Inject;
import javafx.collections.ObservableList;
@FxApplicationScoped
public class AutoUnlocker {
private final ObservableList<Vault> vaults;
private final FxApplicationWindows appWindows;
@Inject
public AutoUnlocker(ObservableList<Vault> vaults, FxApplicationWindows appWindows) {
this.vaults = vaults;
this.appWindows = appWindows;
}
public void unlock() {
vaults.stream().filter(Vault::isLocked).filter(v -> v.getVaultSettings().unlockAfterStartup().get()).forEach(v -> {
appWindows.startUnlockWorkflow(v, null);
});
}
}

View File

@@ -0,0 +1,20 @@
package org.cryptomator.ui.fxapp;
import javafx.application.Platform;
import java.awt.desktop.QuitResponse;
record ExitingQuitResponse(QuitResponse delegate) implements QuitResponse {
@Override
public void performQuit() {
Platform.exit();
// TODO wait a moment for javafx to terminate?
delegate.performQuit();
}
@Override
public void cancelQuit() {
delegate.cancelQuit();
}
}

View File

@@ -1,213 +1,79 @@
package org.cryptomator.ui.fxapp;
import dagger.Lazy;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.stage.Stage;
import javafx.stage.Window;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.integrations.uiappearance.Theme;
import org.cryptomator.integrations.uiappearance.UiAppearanceException;
import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.quit.QuitComponent;
import org.cryptomator.ui.unlock.UnlockComponent;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Provider;
import java.awt.desktop.QuitResponse;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.awt.SystemTray;
import java.io.IOException;
import java.io.UncheckedIOException;
@FxApplicationScoped
public class FxApplication extends Application {
public class FxApplication {
private static final Logger LOG = LoggerFactory.getLogger(FxApplication.class);
private final long startupTime;
private final Settings settings;
private final Lazy<MainWindowComponent> mainWindow;
private final Lazy<PreferencesComponent> preferencesWindow;
private final Lazy<QuitComponent> quitWindow;
private final Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider;
private final Provider<LockComponent.Builder> lockWorkflowBuilderProvider;
private final ErrorComponent.Builder errorWindowBuilder;
private final Optional<TrayIntegrationProvider> trayIntegration;
private final Optional<UiAppearanceProvider> appearanceProvider;
private final VaultService vaultService;
private final LicenseHolder licenseHolder;
private final ObservableList<Window> visibleWindows;
private final BooleanBinding hasVisibleWindows;
private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;
private final AppLaunchEventHandler launchEventHandler;
private final Lazy<TrayMenuComponent> trayMenu;
private final FxApplicationWindows appWindows;
private final FxApplicationStyle applicationStyle;
private final FxApplicationTerminator applicationTerminator;
private final AutoUnlocker autoUnlocker;
@Inject
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider, Provider<LockComponent.Builder> lockWorkflowBuilderProvider, Lazy<QuitComponent> quitWindow, ErrorComponent.Builder errorWindowBuilder, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) {
FxApplication(@Named("startupTime") long startupTime, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy<TrayMenuComponent> trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker) {
this.startupTime = startupTime;
this.settings = settings;
this.mainWindow = mainWindow;
this.preferencesWindow = preferencesWindow;
this.unlockWorkflowBuilderProvider = unlockWorkflowBuilderProvider;
this.lockWorkflowBuilderProvider = lockWorkflowBuilderProvider;
this.quitWindow = quitWindow;
this.errorWindowBuilder = errorWindowBuilder;
this.trayIntegration = trayIntegration;
this.appearanceProvider = appearanceProvider;
this.vaultService = vaultService;
this.licenseHolder = licenseHolder;
this.visibleWindows = Stage.getWindows().filtered(Window::isShowing);
this.hasVisibleWindows = Bindings.isNotEmpty(visibleWindows);
this.launchEventHandler = launchEventHandler;
this.trayMenu = trayMenu;
this.appWindows = appWindows;
this.applicationStyle = applicationStyle;
this.applicationTerminator = applicationTerminator;
this.autoUnlocker = autoUnlocker;
}
public void start() {
LOG.trace("FxApplication.start()");
Platform.setImplicitExit(false);
applicationStyle.initialize();
appWindows.initialize();
applicationTerminator.initialize();
hasVisibleWindows.addListener(this::hasVisibleStagesChanged);
settings.theme().addListener(this::appThemeChanged);
loadSelectedStyleSheet(settings.theme().get());
}
@Override
public void start(Stage stage) {
throw new UnsupportedOperationException("Use start() instead.");
}
private void hasVisibleStagesChanged(@SuppressWarnings("unused") ObservableValue<? extends Boolean> observableValue, @SuppressWarnings("unused") boolean oldValue, boolean newValue) {
LOG.debug("has visible stages: {}", newValue);
if (newValue) {
trayIntegration.ifPresent(TrayIntegrationProvider::restoredFromTray);
// init system tray
final boolean hasTrayIcon;
if (SystemTray.isSupported() && settings.showTrayIcon().get()) {
trayMenu.get().initializeTrayIcon();
Platform.setImplicitExit(false); // don't quit when closing all windows
hasTrayIcon = true;
} else {
trayIntegration.ifPresent(TrayIntegrationProvider::minimizedToTray);
hasTrayIcon = false;
}
}
public void showPreferencesWindow(SelectedPreferencesTab selectedTab) {
Platform.runLater(() -> {
preferencesWindow.get().showPreferencesWindow(selectedTab);
LOG.debug("Showing Preferences");
});
}
public CompletionStage<Stage> showMainWindow() {
CompletableFuture<Stage> future = new CompletableFuture<>();
Platform.runLater(() -> {
var win = mainWindow.get().showMainWindow();
LOG.debug("Showing MainWindow");
future.complete(win);
});
return future;
}
public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) {
Platform.runLater(() -> {
if (vault.stateProperty().transition(VaultState.Value.LOCKED, VaultState.Value.PROCESSING)) {
unlockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
LOG.debug("Start unlock workflow for {}", vault.getDisplayName());
} else {
showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to unlock vault in non-locked state.")));
// show main window
appWindows.showMainWindow().thenAccept(stage -> {
if (settings.startHidden().get()) {
if (hasTrayIcon) {
stage.hide();
} else {
stage.setIconified(true);
}
}
LOG.debug("Main window initialized after {}ms", System.currentTimeMillis() - startupTime);
}).exceptionally(error -> {
LOG.error("Failed to show main window", error);
return null;
});
}
public void startLockWorkflow(Vault vault, Optional<Stage> owner) {
Platform.runLater(() -> {
if (vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING)) {
lockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
LOG.debug("Start lock workflow for {}", vault.getDisplayName());
} else {
showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to lock vault in non-unlocked state.")));
}
});
}
public void showQuitWindow(QuitResponse response) {
Platform.runLater(() -> {
quitWindow.get().showQuitWindow(response);
LOG.debug("Showing QuitWindow");
});
}
public VaultService getVaultService() {
return vaultService;
}
private void appThemeChanged(@SuppressWarnings("unused") ObservableValue<? extends UiTheme> observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) {
if (appearanceProvider.isPresent() && oldValue == UiTheme.AUTOMATIC && newValue != UiTheme.AUTOMATIC) {
try {
appearanceProvider.get().removeListener(systemInterfaceThemeListener);
} catch (UiAppearanceException e) {
LOG.error("Failed to disable automatic theme switching.");
}
}
loadSelectedStyleSheet(newValue);
}
private void loadSelectedStyleSheet(UiTheme desiredTheme) {
UiTheme theme = licenseHolder.isValidLicense() ? desiredTheme : UiTheme.LIGHT;
switch (theme) {
case LIGHT -> applyLightTheme();
case DARK -> applyDarkTheme();
case AUTOMATIC -> {
appearanceProvider.ifPresent(appearanceProvider -> {
try {
appearanceProvider.addListener(systemInterfaceThemeListener);
} catch (UiAppearanceException e) {
LOG.error("Failed to enable automatic theme switching.");
}
});
applySystemTheme();
}
}
}
private void systemInterfaceThemeChanged(Theme theme) {
switch (theme) {
case LIGHT -> applyLightTheme();
case DARK -> applyDarkTheme();
}
}
private void applySystemTheme() {
if (appearanceProvider.isPresent()) {
systemInterfaceThemeChanged(appearanceProvider.get().getSystemTheme());
} else {
LOG.warn("No UiAppearanceProvider present, assuming LIGHT theme...");
applyLightTheme();
}
}
private void applyLightTheme() {
Application.setUserAgentStylesheet(getClass().getResource("/css/light_theme.css").toString());
appearanceProvider.ifPresent(appearanceProvider -> {
appearanceProvider.adjustToTheme(Theme.LIGHT);
});
}
private void applyDarkTheme() {
Application.setUserAgentStylesheet(getClass().getResource("/css/dark_theme.css").toString());
appearanceProvider.ifPresent(appearanceProvider -> {
appearanceProvider.adjustToTheme(Theme.DARK);
});
launchEventHandler.startHandlingLaunchEvents();
autoUnlocker.unlock();
}
}

View File

@@ -5,8 +5,12 @@
*******************************************************************************/
package org.cryptomator.ui.fxapp;
import dagger.BindsInstance;
import dagger.Subcomponent;
import javafx.application.Application;
import javafx.stage.Stage;
@FxApplicationScoped
@Subcomponent(modules = FxApplicationModule.class)
public interface FxApplicationComponent {
@@ -16,6 +20,12 @@ public interface FxApplicationComponent {
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder fxApplication(Application application);
@BindsInstance
Builder primaryStage(@PrimaryStage Stage primaryStage);
FxApplicationComponent build();
}

View File

@@ -5,7 +5,6 @@
*******************************************************************************/
package org.cryptomator.ui.fxapp;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import org.apache.commons.lang3.SystemUtils;
@@ -15,67 +14,46 @@ import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.quit.QuitComponent;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.cryptomator.ui.unlock.UnlockComponent;
import javax.inject.Named;
import javafx.application.Application;
import javafx.collections.ObservableSet;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.List;
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, ErrorComponent.class})
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, ErrorComponent.class})
abstract class FxApplicationModule {
@Provides
@Named("windowIcons")
@FxApplicationScoped
static List<Image> provideWindowIcons() {
if (SystemUtils.IS_OS_MAC) {
return Collections.emptyList();
}
try {
return List.of( //
createImageFromResource("/img/window_icon_32.png"), //
createImageFromResource("/img/window_icon_512.png") //
);
} catch (IOException e) {
throw new UncheckedIOException("Failed to load embedded resource.", e);
}
}
@Provides
@FxApplicationScoped
static StageFactory provideStageFactory(@Named("windowIcons") List<Image> windowIcons) {
return new StageFactory(stage -> {
stage.getIcons().addAll(windowIcons);
});
}
private static Image createImageFromResource(String resourceName) throws IOException {
try (InputStream in = FxApplicationModule.class.getResourceAsStream(resourceName)) {
return new Image(in);
}
}
@Binds
abstract Application bindApplication(FxApplication application);
@Provides
@FxApplicationScoped
static TrayMenuComponent provideTrayMenuComponent(TrayMenuComponent.Builder builder) {
return builder.build();
}
@Provides
@FxApplicationScoped
static MainWindowComponent provideMainWindowComponent(MainWindowComponent.Builder builder) {
return builder.build();
}
@Provides
@FxApplicationScoped
static PreferencesComponent providePreferencesComponent(PreferencesComponent.Builder builder) {
return builder.build();
}
@Provides
@FxApplicationScoped
static QuitComponent provideQuitComponent(QuitComponent.Builder builder) {
return builder.build();
}

View File

@@ -0,0 +1,94 @@
package org.cryptomator.ui.fxapp;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.integrations.uiappearance.Theme;
import org.cryptomator.integrations.uiappearance.UiAppearanceException;
import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import java.util.Optional;
@FxApplicationScoped
public class FxApplicationStyle {
private static final Logger LOG = LoggerFactory.getLogger(FxApplicationStyle.class);
private final Settings settings;
private final Optional<UiAppearanceProvider> appearanceProvider;
private final LicenseHolder licenseHolder;
private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;
@Inject
public FxApplicationStyle(Settings settings, Optional<UiAppearanceProvider> appearanceProvider, LicenseHolder licenseHolder){
this.settings = settings;
this.appearanceProvider = appearanceProvider;
this.licenseHolder = licenseHolder;
}
public void initialize() {
settings.theme().addListener(this::appThemeChanged);
loadSelectedStyleSheet(settings.theme().get());
}
private void appThemeChanged(@SuppressWarnings("unused") ObservableValue<? extends UiTheme> observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) {
if (appearanceProvider.isPresent() && oldValue == UiTheme.AUTOMATIC && newValue != UiTheme.AUTOMATIC) {
try {
appearanceProvider.get().removeListener(systemInterfaceThemeListener);
} catch (UiAppearanceException e) {
LOG.error("Failed to disable automatic theme switching.");
}
}
loadSelectedStyleSheet(newValue);
}
private void loadSelectedStyleSheet(UiTheme desiredTheme) {
UiTheme theme = licenseHolder.isValidLicense() ? desiredTheme : UiTheme.LIGHT;
switch (theme) {
case LIGHT -> applyLightTheme();
case DARK -> applyDarkTheme();
case AUTOMATIC -> {
appearanceProvider.ifPresent(provider -> {
try {
provider.addListener(systemInterfaceThemeListener);
} catch (UiAppearanceException e) {
LOG.error("Failed to enable automatic theme switching.");
}
});
applySystemTheme();
}
}
}
private void systemInterfaceThemeChanged(Theme theme) {
switch (theme) {
case LIGHT -> applyLightTheme();
case DARK -> applyDarkTheme();
}
}
private void applySystemTheme() {
if (appearanceProvider.isPresent()) {
systemInterfaceThemeChanged(appearanceProvider.get().getSystemTheme());
} else {
LOG.warn("No UiAppearanceProvider present, assuming LIGHT theme...");
applyLightTheme();
}
}
private void applyLightTheme() {
Application.setUserAgentStylesheet(getClass().getResource("/css/light_theme.css").toString());
appearanceProvider.ifPresent(provider -> provider.adjustToTheme(Theme.LIGHT));
}
private void applyDarkTheme() {
Application.setUserAgentStylesheet(getClass().getResource("/css/dark_theme.css").toString());
appearanceProvider.ifPresent(provider -> provider.adjustToTheme(Theme.DARK));
}
}

View File

@@ -0,0 +1,134 @@
package org.cryptomator.ui.fxapp;
import com.google.common.base.Preconditions;
import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.beans.Observable;
import javafx.collections.ObservableList;
import java.awt.Desktop;
import java.awt.desktop.QuitResponse;
import java.awt.desktop.QuitStrategy;
import java.util.EnumSet;
import java.util.EventObject;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.cryptomator.common.vaults.VaultState.Value.*;
@FxApplicationScoped
public class FxApplicationTerminator {
private static final Set<VaultState.Value> STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR);
private static final Logger LOG = LoggerFactory.getLogger(FxApplicationTerminator.class);
private final ObservableList<Vault> vaults;
private final ShutdownHook shutdownHook;
private final FxApplicationWindows appWindows;
private final AtomicBoolean allowQuitWithoutPrompt = new AtomicBoolean();
@Inject
public FxApplicationTerminator(ObservableList<Vault> vaults, ShutdownHook shutdownHook, FxApplicationWindows appWindows) {
this.vaults = vaults;
this.shutdownHook = shutdownHook;
this.appWindows = appWindows;
}
public void initialize() {
Preconditions.checkState(Desktop.isDesktopSupported(), "java.awt.Desktop not supported");
Desktop desktop = Desktop.getDesktop();
// register quit handler
if (desktop.isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
desktop.setQuitHandler(this::handleQuitRequest);
}
// set quit strategy (cmd+q would call `System.exit(0)` otherwise)
if (desktop.isSupported(Desktop.Action.APP_QUIT_STRATEGY)) {
desktop.setQuitStrategy(QuitStrategy.CLOSE_ALL_WINDOWS);
}
// allow sudden termination?
vaultListChanged(vaults);
vaults.addListener(this::vaultListChanged);
shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults);
}
/**
* Gracefully terminates the application.
*/
public void terminate() {
handleQuitRequest(null, new NoopQuitResponse());
}
private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
boolean allowSuddenTermination = vaults.stream().map(Vault::getState).allMatch(STATES_ALLOWING_TERMINATION::contains);
boolean stateChanged = allowQuitWithoutPrompt.compareAndSet(!allowSuddenTermination, allowSuddenTermination);
Desktop desktop = Desktop.getDesktop();
if (stateChanged && desktop.isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
if (allowSuddenTermination) {
LOG.debug("Enabling sudden termination");
desktop.enableSuddenTermination();
} else {
LOG.debug("Disabling sudden termination");
desktop.disableSuddenTermination();
}
}
}
/**
* Asks the app to quit. If confirmed, the JavaFX application will exit before giving a {@code response}.
*
* @param e ignored
* @param response a quit response that will be {@link ExitingQuitResponse decorated in order to exit the JavaFX application}.
*/
private void handleQuitRequest(@SuppressWarnings("unused") @Nullable EventObject e, QuitResponse response) {
var exitingResponse = new ExitingQuitResponse(response);
if (allowQuitWithoutPrompt.get()) {
exitingResponse.performQuit();
} else {
appWindows.showQuitWindow(exitingResponse);
}
}
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);
} catch (LockNotCompletedException e) {
LOG.error("Failed to lock vault " + vault.getPath(), e);
}
}
}
}
/**
* A dummy QuitResponse that ignores the response.
*
* To be used with {@link #handleQuitRequest(EventObject, QuitResponse)} if the invoking method is not interested in the response.
*/
private static class NoopQuitResponse implements QuitResponse {
@Override
public void performQuit() {
// no-op
}
@Override
public void cancelQuit() {
// no-op
}
}
}

View File

@@ -0,0 +1,155 @@
package org.cryptomator.ui.fxapp;
import com.google.common.base.Preconditions;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.quit.QuitComponent;
import org.cryptomator.ui.unlock.UnlockComponent;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.awt.Desktop;
import java.awt.desktop.AppReopenedListener;
import java.awt.desktop.QuitResponse;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
@FxApplicationScoped
public class FxApplicationWindows {
private static final Logger LOG = LoggerFactory.getLogger(FxApplicationWindows.class);
private final Stage primaryStage;
private final Optional<TrayIntegrationProvider> trayIntegration;
private final Lazy<MainWindowComponent> mainWindow;
private final Lazy<PreferencesComponent> preferencesWindow;
private final Lazy<QuitComponent> quitWindow;
private final UnlockComponent.Factory unlockWorkflowFactory;
private final LockComponent.Factory lockWorkflowFactory;
private final ErrorComponent.Factory errorWindowFactory;
private final ExecutorService executor;
private final FilteredList<Window> visibleWindows;
@Inject
public FxApplicationWindows(@PrimaryStage Stage primaryStage, Optional<TrayIntegrationProvider> trayIntegration, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Lazy<QuitComponent> quitWindow, UnlockComponent.Factory unlockWorkflowFactory, LockComponent.Factory lockWorkflowFactory, ErrorComponent.Factory errorWindowFactory, ExecutorService executor) {
this.primaryStage = primaryStage;
this.trayIntegration = trayIntegration;
this.mainWindow = mainWindow;
this.preferencesWindow = preferencesWindow;
this.quitWindow = quitWindow;
this.unlockWorkflowFactory = unlockWorkflowFactory;
this.lockWorkflowFactory = lockWorkflowFactory;
this.errorWindowFactory = errorWindowFactory;
this.executor = executor;
this.visibleWindows = Window.getWindows().filtered(Window::isShowing);
}
public void initialize() {
Preconditions.checkState(Desktop.isDesktopSupported(), "java.awt.Desktop not supported");
Desktop desktop = Desktop.getDesktop();
// register preferences shortcut
if (desktop.isSupported(Desktop.Action.APP_PREFERENCES)) {
desktop.setPreferencesHandler(evt -> showPreferencesWindow(SelectedPreferencesTab.ANY));
}
// register preferences shortcut
if (desktop.isSupported(Desktop.Action.APP_ABOUT)) {
desktop.setAboutHandler(evt -> showPreferencesWindow(SelectedPreferencesTab.ABOUT));
}
// register app reopen listener
if (desktop.isSupported(Desktop.Action.APP_EVENT_REOPENED)) {
desktop.addAppEventListener((AppReopenedListener) e -> showMainWindow());
}
// observe visible windows
if (trayIntegration.isPresent()) {
visibleWindows.addListener(this::visibleWindowsChanged);
}
}
private void visibleWindowsChanged(ListChangeListener.Change<? extends Window> change) {
int visibleWindows = change.getList().size();
LOG.debug("visible windows: {}", visibleWindows);
if (visibleWindows > 0) {
trayIntegration.ifPresent(TrayIntegrationProvider::restoredFromTray);
} else {
trayIntegration.ifPresent(TrayIntegrationProvider::minimizedToTray);
}
}
public CompletionStage<Stage> showMainWindow() {
return CompletableFuture.supplyAsync(mainWindow.get()::showMainWindow, Platform::runLater).whenComplete(this::reportErrors);
}
public CompletionStage<Stage> showPreferencesWindow(SelectedPreferencesTab selectedTab) {
return CompletableFuture.supplyAsync(() -> preferencesWindow.get().showPreferencesWindow(selectedTab), Platform::runLater).whenComplete(this::reportErrors);
}
public CompletionStage<Stage> showQuitWindow(QuitResponse response) {
return CompletableFuture.supplyAsync(() -> quitWindow.get().showQuitWindow(response), Platform::runLater).whenComplete(this::reportErrors);
}
public CompletionStage<Void> startUnlockWorkflow(Vault vault, @Nullable Stage owner) {
return CompletableFuture.supplyAsync(() -> {
Preconditions.checkState(vault.stateProperty().transition(VaultState.Value.LOCKED, VaultState.Value.PROCESSING), "Vault not locked.");
LOG.debug("Start unlock workflow for {}", vault.getDisplayName());
return unlockWorkflowFactory.create(vault, owner).unlockWorkflow();
}, Platform::runLater) //
.thenCompose(unlockWorkflow -> CompletableFuture.runAsync(unlockWorkflow, executor)) //
.exceptionally(e -> {
showErrorWindow(e, owner == null ? primaryStage : owner, null);
return null;
});
}
public CompletionStage<Void> startLockWorkflow(Vault vault, @Nullable Stage owner) {
return CompletableFuture.supplyAsync(() -> {
Preconditions.checkState(vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING), "Vault not unlocked.");
LOG.debug("Start lock workflow for {}", vault.getDisplayName());
return lockWorkflowFactory.create(vault, owner).lockWorkflow();
}, Platform::runLater) //
.thenCompose(lockWorkflow -> CompletableFuture.runAsync(lockWorkflow, executor)) //
.exceptionally(e -> {
showErrorWindow(e, owner == null ? primaryStage : owner, null);
return null;
});
}
/**
* Displays the generic error scene in the given window.
*
* @param cause The exception to show
* @param window What window to display the scene in
* @param previousScene To what scene to return to when pressing "back". Back button will be hidden, if <code>null</code>
* @return A
*/
public CompletionStage<Stage> showErrorWindow(Throwable cause, Stage window, @Nullable Scene previousScene) {
return CompletableFuture.supplyAsync(() -> errorWindowFactory.create(cause, window, previousScene).show(), Platform::runLater).whenComplete(this::reportErrors);
}
private void reportErrors(@Nullable Stage stage, @Nullable Throwable error) {
if (error != null) {
LOG.error("Failed to display stage", error);
}
}
}

View File

@@ -0,0 +1,14 @@
package org.cryptomator.ui.fxapp;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface PrimaryStage {
}

View File

@@ -1,9 +1,8 @@
package org.cryptomator.ui.health;
import com.google.common.base.Preconditions;
import dagger.Lazy;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,9 +14,7 @@ import javafx.beans.property.ObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.stage.Stage;
@@ -37,7 +34,7 @@ public class CheckListController implements FxController {
private final ObjectProperty<Check> selectedCheck;
private final BooleanBinding mainRunStarted; //TODO: rerunning not considered for now
private final BooleanBinding somethingsRunning;
private final Lazy<ErrorComponent.Builder> errorComponentBuilder;
private final FxApplicationWindows appWindows;
private final IntegerBinding chosenTaskCount;
private final BooleanBinding anyCheckSelected;
private final CheckListCellFactory listCellFactory;
@@ -46,7 +43,7 @@ public class CheckListController implements FxController {
public ListView<Check> checksListView;
@Inject
public CheckListController(@HealthCheckWindow Stage window, List<Check> checks, CheckExecutor checkExecutor, ReportWriter reportWriteTask, ObjectProperty<Check> selectedCheck, Lazy<ErrorComponent.Builder> errorComponentBuilder, CheckListCellFactory listCellFactory) {
public CheckListController(@HealthCheckWindow Stage window, List<Check> checks, CheckExecutor checkExecutor, ReportWriter reportWriteTask, ObjectProperty<Check> selectedCheck, FxApplicationWindows appWindows, CheckListCellFactory listCellFactory) {
this.window = window;
this.checks = FXCollections.observableList(checks, Check::observables);
this.checkExecutor = checkExecutor;
@@ -54,7 +51,7 @@ public class CheckListController implements FxController {
this.chosenChecks = this.checks.filtered(Check::isChosenForExecution);
this.reportWriter = reportWriteTask;
this.selectedCheck = selectedCheck;
this.errorComponentBuilder = errorComponentBuilder;
this.appWindows = appWindows;
this.chosenTaskCount = Bindings.size(this.chosenChecks);
this.mainRunStarted = Bindings.isEmpty(this.checks.filtered(c -> c.getState() == Check.CheckState.RUNNABLE));
this.somethingsRunning = Bindings.isNotEmpty(this.checks.filtered(c -> c.getState() == Check.CheckState.SCHEDULED || c.getState() == Check.CheckState.RUNNING));
@@ -104,7 +101,7 @@ public class CheckListController implements FxController {
reportWriter.writeReport(chosenChecks);
} catch (IOException e) {
LOG.error("Failed to write health check report.", e);
errorComponentBuilder.get().cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
appWindows.showErrorWindow(e, window, window.getScene());
}
}

View File

@@ -7,10 +7,10 @@ import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.VaultConfigLoadException;
import org.cryptomator.cryptofs.VaultKeyInvalidException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.unlock.UnlockCancelledException;
import org.slf4j.Logger;
@@ -40,10 +40,10 @@ public class StartController implements FxController {
private final AtomicReference<Masterkey> masterkeyRef;
private final AtomicReference<VaultConfig> vaultConfigRef;
private final Lazy<Scene> checkScene;
private final Lazy<ErrorComponent.Builder> errorComponent;
private final FxApplicationWindows appWindows;
@Inject
public StartController(@HealthCheckWindow Stage window, @HealthCheckWindow Vault vault, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, Lazy<ErrorComponent.Builder> errorComponent, @Named("unlockWindow") Stage unlockWindow) {
public StartController(@HealthCheckWindow Stage window, @HealthCheckWindow Vault vault, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, FxApplicationWindows appWindows, @Named("unlockWindow") Stage unlockWindow) {
this.window = window;
this.unlockWindow = unlockWindow;
this.vaultConfig = vault.getVaultConfigCache();
@@ -52,7 +52,7 @@ public class StartController implements FxController {
this.masterkeyRef = masterkeyRef;
this.vaultConfigRef = vaultConfigRef;
this.checkScene = checkScene;
this.errorComponent = errorComponent;
this.appWindows = appWindows;
}
@FXML
@@ -106,10 +106,10 @@ public class StartController implements FxController {
// ok
} else if (e instanceof VaultKeyInvalidException) {
LOG.error("Invalid key"); //TODO: specific error screen
errorComponent.get().window(window).cause(e).build().showErrorScene();
appWindows.showErrorWindow(e, window, null);
} else {
LOG.error("Failed to load key.", e);
errorComponent.get().window(window).cause(e).build().showErrorScene();
appWindows.showErrorWindow(e, window, null);
}
}

View File

@@ -26,7 +26,7 @@ abstract class KeyLoadingModule {
@Provides
@KeyLoading
@KeyLoadingScoped
static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map<String, Provider<KeyLoadingStrategy>> strategies) {
static KeyLoadingStrategy provideKeyLoadingStrategy(@KeyLoading Vault vault, Map<String, Provider<KeyLoadingStrategy>> strategies) {
try {
String scheme = vault.getVaultConfigCache().get().getKeyId().getScheme();
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));

View File

@@ -0,0 +1,25 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import dagger.Subcomponent;
import javafx.scene.Scene;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
@ChooseMasterkeyFileScoped
@Subcomponent(modules = {ChooseMasterkeyFileModule.class})
public interface ChooseMasterkeyFileComponent {
@ChooseMasterkeyFileScoped
Scene chooseMasterkeyScene();
@ChooseMasterkeyFileScoped
CompletableFuture<Path> result();
@Subcomponent.Builder
interface Builder {
ChooseMasterkeyFileComponent build();
}
}

View File

@@ -0,0 +1,57 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.io.File;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
@ChooseMasterkeyFileScoped
public class ChooseMasterkeyFileController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(ChooseMasterkeyFileController.class);
private final Stage window;
private final CompletableFuture<Path> result;
private final ResourceBundle resourceBundle;
@Inject
public ChooseMasterkeyFileController(@KeyLoading Stage window, CompletableFuture<Path> result, ResourceBundle resourceBundle) {
this.window = window;
this.result = result;
this.resourceBundle = resourceBundle;
this.window.setOnHiding(this::windowClosed);
}
@FXML
public void cancel() {
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
result.cancel(true);
}
@FXML
public void proceed() {
LOG.trace("proceed()");
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
File masterkeyFile = fileChooser.showOpenDialog(window);
if (masterkeyFile != null) {
LOG.debug("Chose masterkey file: {}", masterkeyFile);
result.complete(masterkeyFile.toPath());
}
}
}

View File

@@ -0,0 +1,29 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import javafx.scene.Scene;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
@Module
interface ChooseMasterkeyFileModule {
@Provides
@ChooseMasterkeyFileScoped
static CompletableFuture<Path> provideResult() {
return new CompletableFuture<>();
}
@Provides
@ChooseMasterkeyFileScoped
static Scene provideChooseMasterkeyScene(ChooseMasterkeyFileController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return FxmlLoaderFactory.forController(controller, sceneFactory, resourceBundle).createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
}
}

View File

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

View File

@@ -1,62 +0,0 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.nio.CharBuffer;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
class MasterkeyFileLoadingFinisher {
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingFinisher.class);
private final Vault vault;
private final Optional<char[]> storedPassword;
private final AtomicReference<char[]> enteredPassword;
private final AtomicBoolean shouldSavePassword;
private final KeychainManager keychain;
@Inject
MasterkeyFileLoadingFinisher(@KeyLoading Vault vault, @Named("savedPassword") Optional<char[]> storedPassword, AtomicReference<char[]> enteredPassword, @Named("savePassword") AtomicBoolean shouldSavePassword, KeychainManager keychain) {
this.vault = vault;
this.storedPassword = storedPassword;
this.enteredPassword = enteredPassword;
this.shouldSavePassword = shouldSavePassword;
this.keychain = keychain;
}
public void cleanup(boolean successfullyUnlocked) {
if (successfullyUnlocked && shouldSavePassword.get()) {
savePasswordToSystemkeychain();
}
wipePassword(storedPassword.orElse(null));
wipePassword(enteredPassword.getAndSet(null));
}
private void savePasswordToSystemkeychain() {
if (keychain.isSupported()) {
try {
keychain.storePassphrase(vault.getId(), vault.getDisplayName(), CharBuffer.wrap(enteredPassword.get()));
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
}
private void wipePassword(char[] pw) {
if (pw != null) {
Arrays.fill(pw, ' ');
}
}
}

View File

@@ -8,54 +8,17 @@ import dagger.multibindings.StringKey;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.nio.file.Path;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {ForgetPasswordComponent.class})
public abstract class MasterkeyFileLoadingModule {
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingModule.class);
public enum PasswordEntry {
PASSWORD_ENTERED,
CANCELED
}
public enum MasterkeyFileProvision {
MASTERKEYFILE_PROVIDED,
CANCELED
}
@Provides
@KeyLoadingScoped
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
return new UserInteractionLock<>(null);
}
@Provides
@KeyLoadingScoped
static UserInteractionLock<MasterkeyFileProvision> provideMasterkeyFileProvisionLock() {
return new UserInteractionLock<>(null);
}
@Module(subcomponents = {ForgetPasswordComponent.class, PassphraseEntryComponent.class, ChooseMasterkeyFileComponent.class})
public interface MasterkeyFileLoadingModule {
@Provides
@Named("savedPassword")
@@ -67,67 +30,12 @@ public abstract class MasterkeyFileLoadingModule {
try {
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
LoggerFactory.getLogger(MasterkeyFileLoadingModule.class).error("Failed to load entry from system keychain.", e);
return Optional.empty();
}
}
}
@Provides
@KeyLoadingScoped
static AtomicReference<Path> provideUserProvidedMasterkeyPath() {
return new AtomicReference<>();
}
@Provides
@KeyLoadingScoped
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicReference<>(storedPassword.orElse(null));
}
@Provides
@Named("savePassword")
@KeyLoadingScoped
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicBoolean(storedPassword.isPresent());
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD)
@KeyLoadingScoped
static Scene provideUnlockScene(@KeyLoading FxmlLoaderFactory fxmlLoaders, @KeyLoading Stage window, @KeyLoading Vault v, ResourceBundle resourceBundle) {
var scene = fxmlLoaders.createScene(FxmlFile.UNLOCK_ENTER_PASSWORD);
scene.windowProperty().addListener((prop, oldVal, newVal) -> {
if (window.equals(newVal)) {
window.setTitle(String.format(resourceBundle.getString("unlock.title"), v.getDisplayName()));
}
});
return scene;
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE)
@KeyLoadingScoped
static Scene provideUnlockSelectMasterkeyFileScene(@KeyLoading FxmlLoaderFactory fxmlLoaders, @KeyLoading Stage window, @KeyLoading Vault v, ResourceBundle resourceBundle) {
var scene = fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
scene.windowProperty().addListener((prop, oldVal, newVal) -> {
if (window.equals(newVal)) {
window.setTitle(String.format(resourceBundle.getString("unlock.chooseMasterkey.title"), v.getDisplayName()));
}
});
return scene;
}
@Binds
@IntoMap
@FxControllerKey(PassphraseEntryController.class)
abstract FxController bindUnlockController(PassphraseEntryController controller);
@Binds
@IntoMap
@FxControllerKey(SelectMasterkeyFileController.class)
abstract FxController bindUnlockSelectMasterkeyFileController(SelectMasterkeyFileController controller);
@Binds
@IntoMap
@KeyLoadingScoped

View File

@@ -1,32 +1,33 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import com.google.common.base.Preconditions;
import dagger.Lazy;
import org.cryptomator.common.Passphrase;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.common.BackupHelper;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.unlock.UnlockCancelledException;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.io.IOException;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
@KeyLoading
public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
@@ -36,28 +37,26 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
private final Vault vault;
private final MasterkeyFileAccess masterkeyFileAccess;
private final Stage window;
private final Lazy<Scene> passphraseEntryScene;
private final Lazy<Scene> selectMasterkeyFileScene;
private final UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock;
private final UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock;
private final AtomicReference<char[]> password;
private final AtomicReference<Path> filePath;
private final MasterkeyFileLoadingFinisher finisher;
private final PassphraseEntryComponent.Builder passphraseEntry;
private final ChooseMasterkeyFileComponent.Builder masterkeyFileChoice;
private final KeychainManager keychain;
private final ResourceBundle resourceBundle;
private boolean wrongPassword;
private Passphrase passphrase;
private boolean savePassphrase;
private boolean wrongPassphrase;
@Inject
public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy<Scene> passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy<Scene> selectMasterkeyFileScene, UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock, UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock, AtomicReference<char[]> password, AtomicReference<Path> filePath, MasterkeyFileLoadingFinisher finisher) {
public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional<char[]> savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain, ResourceBundle resourceBundle) {
this.vault = vault;
this.masterkeyFileAccess = masterkeyFileAccess;
this.window = window;
this.passphraseEntryScene = passphraseEntryScene;
this.selectMasterkeyFileScene = selectMasterkeyFileScene;
this.passwordEntryLock = passwordEntryLock;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
this.password = password;
this.filePath = filePath;
this.finisher = finisher;
this.passphraseEntry = passphraseEntry;
this.masterkeyFileChoice = masterkeyFileChoice;
this.keychain = keychain;
this.resourceBundle = resourceBundle;
this.passphrase = savedPassphrase.map(Passphrase::new).orElse(null);
this.savePassphrase = savedPassphrase.isPresent();
}
@Override
@@ -66,9 +65,11 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
try {
Path filePath = vault.getPath().resolve(keyId.getSchemeSpecificPart());
if (!Files.exists(filePath)) {
filePath = getAlternateMasterkeyFilePath();
filePath = askUserForMasterkeyFilePath();
}
if (passphrase == null) {
askForPassphrase();
}
CharSequence passphrase = getPassphrase();
var masterkey = masterkeyFileAccess.load(filePath, passphrase);
//backup
if (filePath.startsWith(vault.getPath())) {
@@ -90,8 +91,9 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
@Override
public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
if (exception instanceof InvalidPassphraseException) {
this.wrongPassword = true;
password.set(null);
this.wrongPassphrase = true;
passphrase.destroy();
this.passphrase = null;
return true; // reattempting key load
} else {
return false; // nothing we can do
@@ -100,23 +102,29 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
@Override
public void cleanup(boolean unlockedSuccessfully) {
finisher.cleanup(unlockedSuccessfully);
}
private Path getAlternateMasterkeyFilePath() throws UnlockCancelledException, InterruptedException {
if (filePath.get() == null) {
return switch (askUserForMasterkeyFilePath()) {
case MASTERKEYFILE_PROVIDED -> filePath.get();
case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled.");
};
} else {
return filePath.get();
if (unlockedSuccessfully && savePassphrase) {
savePasswordToSystemkeychain(passphrase);
}
if (passphrase != null) {
passphrase.destroy();
}
}
private MasterkeyFileLoadingModule.MasterkeyFileProvision askUserForMasterkeyFilePath() throws InterruptedException {
private void savePasswordToSystemkeychain(Passphrase passphrase) {
if (keychain.isSupported()) {
try {
keychain.storePassphrase(vault.getId(), vault.getDisplayName(), passphrase);
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
}
private Path askUserForMasterkeyFilePath() throws InterruptedException {
var comp = masterkeyFileChoice.build();
Platform.runLater(() -> {
window.setScene(selectMasterkeyFileScene.get());
window.setScene(comp.chooseMasterkeyScene());
window.setTitle(resourceBundle.getString("unlock.chooseMasterkey.title").formatted(vault.getDisplayName()));
window.show();
Window owner = window.getOwner();
if (owner != null) {
@@ -126,24 +134,20 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
window.centerOnScreen();
}
});
return masterkeyFileProvisionLock.awaitInteraction();
}
private CharSequence getPassphrase() throws UnlockCancelledException, InterruptedException {
if (password.get() == null) {
return switch (askForPassphrase()) {
case PASSWORD_ENTERED -> CharBuffer.wrap(password.get());
case CANCELED -> throw new UnlockCancelledException("Password entry cancelled.");
};
} else {
// e.g. pre-filled from keychain or previous unlock attempt
return CharBuffer.wrap(password.get());
try {
return comp.result().get();
} catch (CancellationException e) {
throw new UnlockCancelledException("Choosing masterkey file cancelled.");
} catch (ExecutionException e) {
throw new MasterkeyLoadingFailedException("Failed to select masterkey file.", e);
}
}
private MasterkeyFileLoadingModule.PasswordEntry askForPassphrase() throws InterruptedException {
private void askForPassphrase() throws InterruptedException {
var comp = passphraseEntry.savedPassword(passphrase).build();
Platform.runLater(() -> {
window.setScene(passphraseEntryScene.get());
window.setScene(comp.passphraseEntryScene());
window.setTitle(resourceBundle.getString("unlock.title").formatted(vault.getDisplayName()));
window.show();
Window owner = window.getOwner();
if (owner != null) {
@@ -152,11 +156,19 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
} else {
window.centerOnScreen();
}
if (wrongPassword) {
if (wrongPassphrase) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
try {
var result = comp.result().get();
this.passphrase = result.passphrase();
this.savePassphrase = result.savePassphrase();
} catch (CancellationException e) {
throw new UnlockCancelledException("Password entry cancelled.");
} catch (ExecutionException e) {
throw new MasterkeyLoadingFailedException("Failed to ask for password.", e);
}
}
}

View File

@@ -0,0 +1,31 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.Passphrase;
import javax.inject.Named;
import javafx.scene.Scene;
import java.util.concurrent.CompletableFuture;
@PassphraseEntryScoped
@Subcomponent(modules = {PassphraseEntryModule.class})
public interface PassphraseEntryComponent {
@PassphraseEntryScoped
Scene passphraseEntryScene();
@PassphraseEntryScoped
CompletableFuture<PassphraseEntryResult> result();
@Subcomponent.Builder
interface Builder {
@BindsInstance
PassphraseEntryComponent.Builder savedPassword(@Nullable @Named("savedPassword") Passphrase savedPassword);
PassphraseEntryComponent build();
}
}

View File

@@ -1,16 +1,14 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.common.Passphrase;
import org.cryptomator.ui.common.WeakBindings;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.PasswordEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -21,8 +19,8 @@ import javafx.animation.Interpolator;
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.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
@@ -37,33 +35,27 @@ import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javafx.util.Duration;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.CompletableFuture;
@KeyLoadingScoped
@PassphraseEntryScoped
public class PassphraseEntryController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(PassphraseEntryController.class);
private final Stage window;
private final Vault vault;
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final CompletableFuture<PassphraseEntryResult> result;
private final Passphrase savedPassword;
private final ForgetPasswordComponent.Builder forgetPassword;
private final KeychainManager keychain;
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
private final BooleanBinding userInteractionDisabled;
private final BooleanProperty unlockButtonDisabled;
private final StringBinding vaultName;
private final BooleanProperty unlockInProgress = new SimpleBooleanProperty();
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, unlockInProgress);
private final BooleanProperty unlockButtonDisabled = new SimpleBooleanProperty();
/* FXML */
public NiceSecurePasswordField passwordField;
public CheckBox savePasswordCheckbox;
public FontAwesome5IconView unlockInProgressView;
public ImageView face;
public ImageView leftArm;
public ImageView rightArm;
@@ -72,29 +64,25 @@ public class PassphraseEntryController implements FxController {
public Animation unlockAnimation;
@Inject
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture<PassphraseEntryResult> result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
this.window = window;
this.vault = vault;
this.password = password;
this.savePassword = savePassword;
this.result = result;
this.savedPassword = savedPassword;
this.passwordEntryLock = passwordEntryLock;
this.forgetPassword = forgetPassword;
this.keychain = keychain;
this.unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, passwordEntryLock.awaitingInteraction());
this.userInteractionDisabled = passwordEntryLock.awaitingInteraction().not();
this.unlockButtonDisabled = new SimpleBooleanProperty();
this.vaultName = WeakBindings.bindString(vault.displayNameProperty());
this.window.setOnHiding(this::windowClosed);
window.setOnHiding(this::windowClosed);
result.whenCompleteAsync((r, t) -> unlockInProgress.set(false), Platform::runLater);
}
@FXML
public void initialize() {
savePasswordCheckbox.setSelected(savedPassword.isPresent());
if (password.get() != null) {
passwordField.setPassword(password.get());
if (savedPassword != null) {
savePasswordCheckbox.setSelected(true);
passwordField.setPassword(savedPassword);
}
unlockButtonDisabled.bind(userInteractionDisabled.or(passwordField.textProperty().isEmpty()));
unlockButtonDisabled.bind(unlockInProgress.or(passwordField.textProperty().isEmpty()));
var leftArmTranslation = new Translate(24, 0);
var leftArmRotation = new Rotate(60, 16, 30, 0);
@@ -132,7 +120,7 @@ public class PassphraseEntryController implements FxController {
new KeyFrame(Duration.millis(1000), faceVisible) //
);
passwordEntryLock.awaitingInteraction().addListener(observable -> stopUnlockAnimation());
result.whenCompleteAsync((r, t) -> stopUnlockAnimation());
}
@FXML
@@ -141,26 +129,20 @@ public class PassphraseEntryController implements FxController {
}
private void windowClosed(WindowEvent windowEvent) {
// if not already interacted, mark this workflow as cancelled:
if (passwordEntryLock.awaitingInteraction().get()) {
if(!result.isDone()) {
result.cancel(true);
LOG.debug("Unlock canceled by user.");
passwordEntryLock.interacted(PasswordEntry.CANCELED);
}
}
@FXML
public void unlock() {
LOG.trace("UnlockController.unlock()");
unlockInProgress.set(true);
CharSequence pwFieldContents = passwordField.getCharacters();
char[] newPw = new char[pwFieldContents.length()];
for (int i = 0; i < pwFieldContents.length(); i++) {
newPw[i] = pwFieldContents.charAt(i);
}
char[] oldPw = password.getAndSet(newPw);
if (oldPw != null) {
Arrays.fill(oldPw, ' ');
}
passwordEntryLock.interacted(PasswordEntry.PASSWORD_ENTERED);
Passphrase pw = Passphrase.copyOf(pwFieldContents);
result.complete(new PassphraseEntryResult(pw, savePasswordCheckbox.isSelected()));
startUnlockAnimation();
}
@@ -184,8 +166,7 @@ public class PassphraseEntryController implements FxController {
@FXML
private void didClickSavePasswordCheckbox() {
savePassword.set(savePasswordCheckbox.isSelected());
if (!savePasswordCheckbox.isSelected() && savedPassword.isPresent()) {
if (!savePasswordCheckbox.isSelected() && savedPassword != null) {
forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePasswordCheckbox.setSelected(!forgotten));
}
}
@@ -205,15 +186,15 @@ public class PassphraseEntryController implements FxController {
}
public ContentDisplay getUnlockButtonContentDisplay() {
return passwordEntryLock.awaitingInteraction().get() ? ContentDisplay.TEXT_ONLY : ContentDisplay.LEFT;
return unlockInProgress.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
}
public BooleanBinding userInteractionDisabledProperty() {
return userInteractionDisabled;
public ReadOnlyBooleanProperty userInteractionDisabledProperty() {
return unlockInProgress;
}
public boolean isUserInteractionDisabled() {
return userInteractionDisabled.get();
return unlockInProgress.get();
}
public ReadOnlyBooleanProperty unlockButtonDisabledProperty() {
@@ -227,4 +208,6 @@ public class PassphraseEntryController implements FxController {
public boolean isKeychainAccessAvailable() {
return keychain.isSupported();
}
}

View File

@@ -0,0 +1,28 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import javafx.scene.Scene;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
@Module
interface PassphraseEntryModule {
@Provides
@PassphraseEntryScoped
static CompletableFuture<PassphraseEntryResult> provideResult() {
return new CompletableFuture<>();
}
@Provides
@PassphraseEntryScoped
static Scene provideUnlockScene(PassphraseEntryController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return FxmlLoaderFactory.forController(controller, sceneFactory, resourceBundle).createScene(FxmlFile.UNLOCK_ENTER_PASSWORD);
}
}

View File

@@ -0,0 +1,8 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.Passphrase;
// TODO: change to package-private, as soon as this works for Dagger -.-
public record PassphraseEntryResult(Passphrase passphrase, boolean savePassphrase) {
}

View File

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

View File

@@ -1,67 +0,0 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.MasterkeyFileProvision;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.io.File;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
public class SelectMasterkeyFileController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(SelectMasterkeyFileController.class);
private final Stage window;
private final AtomicReference<Path> masterkeyPath;
private final UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock;
private final ResourceBundle resourceBundle;
@Inject
public SelectMasterkeyFileController(@KeyLoading Stage window, AtomicReference<Path> masterkeyPath, UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock, ResourceBundle resourceBundle) {
this.window = window;
this.masterkeyPath = masterkeyPath;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
this.resourceBundle = resourceBundle;
this.window.setOnHiding(this::windowClosed);
}
@FXML
public void cancel() {
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
// if not already interacted, mark this workflow as cancelled:
if (masterkeyFileProvisionLock.awaitingInteraction().get()) {
LOG.debug("Unlock canceled by user.");
masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELED);
}
}
@FXML
public void proceed() {
LOG.trace("proceed()");
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
File masterkeyFile = fileChooser.showOpenDialog(window);
if (masterkeyFile != null) {
LOG.debug("Chose masterkey file: {}", masterkeyFile);
masterkeyPath.set(masterkeyFile.toPath());
masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.MASTERKEYFILE_PROVIDED);
}
}
}

View File

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

View File

@@ -1,146 +0,0 @@
package org.cryptomator.ui.launcher;
import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.vaults.LockNotCompletedException;
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 javafx.application.Platform;
import javafx.beans.Observable;
import javafx.collections.ObservableList;
import java.awt.Desktop;
import java.awt.EventQueue;
import java.awt.desktop.AboutEvent;
import java.awt.desktop.QuitResponse;
import java.awt.desktop.QuitStrategy;
import java.util.EnumSet;
import java.util.EventObject;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.cryptomator.common.vaults.VaultState.Value.*;
@Singleton
public class AppLifecycleListener {
private static final Logger LOG = LoggerFactory.getLogger(AppLifecycleListener.class);
public static final Set<VaultState.Value> STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR);
private final FxApplicationStarter fxApplicationStarter;
private final CountDownLatch shutdownLatch;
private final ObservableList<Vault> vaults;
private final AtomicBoolean allowQuitWithoutPrompt;
@Inject
AppLifecycleListener(FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, ObservableList<Vault> vaults) {
this.fxApplicationStarter = fxApplicationStarter;
this.shutdownLatch = shutdownLatch;
this.vaults = vaults;
this.allowQuitWithoutPrompt = new AtomicBoolean(true);
vaults.addListener(this::vaultListChanged);
// register preferences shortcut
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_PREFERENCES)) {
Desktop.getDesktop().setPreferencesHandler(this::showPreferencesWindow);
}
// register preferences shortcut
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_ABOUT)) {
Desktop.getDesktop().setAboutHandler(this::showAboutWindow);
}
// register quit handler
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
Desktop.getDesktop().setQuitHandler(this::handleQuitRequest);
}
// set quit strategy (cmd+q would call `System.exit(0)` otherwise)
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_STRATEGY)) {
Desktop.getDesktop().setQuitStrategy(QuitStrategy.CLOSE_ALL_WINDOWS);
}
shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults);
}
/**
* Gracefully terminates the application.
*/
public void quit() {
handleQuitRequest(null, new QuitResponse() {
@Override
public void performQuit() {
// no-op
}
@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().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
originalQuitResponse.performQuit();
}
@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().thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
}
private void showAboutWindow(@SuppressWarnings("unused") AboutEvent aboutEvent) {
fxApplicationStarter.get().thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ABOUT));
}
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);
} catch (LockNotCompletedException e) {
LOG.error("Failed to lock vault " + vault.getPath(), e);
}
}
}
}
}

View File

@@ -1,54 +0,0 @@
package org.cryptomator.ui.launcher;
import dagger.Lazy;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javafx.application.Platform;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
@Singleton
public class FxApplicationStarter {
private static final Logger LOG = LoggerFactory.getLogger(FxApplicationStarter.class);
private final Lazy<FxApplicationComponent> fxAppComponent;
private final ExecutorService executor;
private final AtomicBoolean started;
private final CompletableFuture<FxApplication> future;
@Inject
public FxApplicationStarter(Lazy<FxApplicationComponent> fxAppComponent, ExecutorService executor) {
this.fxAppComponent = fxAppComponent;
this.executor = executor;
this.started = new AtomicBoolean();
this.future = new CompletableFuture<>();
}
public CompletionStage<FxApplication> get() {
if (!started.getAndSet(true)) {
start();
}
return future;
}
private void start() {
executor.submit(() -> {
LOG.debug("Starting JavaFX runtime...");
Platform.startup(() -> {
assert Platform.isFxApplicationThread();
LOG.info("JavaFX Runtime started.");
FxApplication app = fxAppComponent.get().application();
app.start();
future.complete(app);
});
});
}
}

View File

@@ -1,90 +0,0 @@
package org.cryptomator.ui.launcher;
import dagger.Lazy;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javafx.collections.ObservableList;
import java.awt.Desktop;
import java.awt.SystemTray;
import java.awt.desktop.AppReopenedListener;
import java.util.Collection;
import java.util.Optional;
@Singleton
public class UiLauncher {
private static final Logger LOG = LoggerFactory.getLogger(UiLauncher.class);
private final Settings settings;
private final ObservableList<Vault> vaults;
private final Lazy<TrayMenuComponent> trayMenu;
private final FxApplicationStarter fxApplicationStarter;
private final AppLaunchEventHandler launchEventHandler;
private final Optional<TrayIntegrationProvider> trayIntegration;
@Inject
public UiLauncher(Settings settings, ObservableList<Vault> vaults, Lazy<TrayMenuComponent> trayMenu, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<TrayIntegrationProvider> trayIntegration) {
this.settings = settings;
this.vaults = vaults;
this.trayMenu = trayMenu;
this.fxApplicationStarter = fxApplicationStarter;
this.launchEventHandler = launchEventHandler;
this.trayIntegration = trayIntegration;
}
public void launch() {
boolean hidden = settings.startHidden().get();
if (SystemTray.isSupported() && settings.showTrayIcon().get()) {
trayMenu.get().initializeTrayIcon();
launch(true, hidden);
} else {
launch(false, hidden);
}
}
private void launch(boolean withTrayIcon, boolean hidden) {
// start hidden, minimized or normal?
if (withTrayIcon && hidden) {
LOG.debug("Hiding application...");
trayIntegration.ifPresent(TrayIntegrationProvider::minimizedToTray);
} else if (!withTrayIcon && hidden) {
LOG.debug("Minimizing application...");
showMainWindowAsync(true);
} else {
LOG.debug("Showing application...");
showMainWindowAsync(false);
}
// register app reopen listener
Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(false));
// auto unlock
Collection<Vault> vaultsToAutoUnlock = vaults.filtered(this::shouldAttemptAutoUnlock);
if (!vaultsToAutoUnlock.isEmpty()) {
fxApplicationStarter.get().thenAccept(app -> {
for (Vault vault : vaultsToAutoUnlock) {
app.startUnlockWorkflow(vault, Optional.empty());
}
});
}
launchEventHandler.startHandlingLaunchEvents();
}
private boolean shouldAttemptAutoUnlock(Vault vault) {
return vault.isLocked() && vault.getVaultSettings().unlockAfterStartup().get();
}
private void showMainWindowAsync(boolean minimize) {
fxApplicationStarter.get().thenCompose(FxApplication::showMainWindow).thenAccept(win -> win.setIconified(minimize));
}
}

View File

@@ -1,67 +0,0 @@
package org.cryptomator.ui.launcher;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.PluginClassLoader;
import org.cryptomator.integrations.autostart.AutoStartProvider;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.ServiceLoader;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
@Module(subcomponents = {TrayMenuComponent.class, FxApplicationComponent.class})
public abstract class UiLauncherModule {
@Provides
@Singleton
static TrayMenuComponent provideTrayMenuComponent(TrayMenuComponent.Builder builder) {
return builder.build();
}
@Provides
@Singleton
static FxApplicationComponent provideFxApplicationComponent(FxApplicationComponent.Builder builder) {
return builder.build();
}
@Provides
@Singleton
static Optional<UiAppearanceProvider> provideAppearanceProvider(PluginClassLoader classLoader) {
return ServiceLoader.load(UiAppearanceProvider.class, classLoader).findFirst();
}
@Provides
@Singleton
static Optional<AutoStartProvider> provideAutostartProvider(PluginClassLoader classLoader) {
return ServiceLoader.load(AutoStartProvider.class, classLoader).findFirst();
}
@Provides
@Singleton
static Optional<TrayIntegrationProvider> provideTrayIntegrationProvider(PluginClassLoader classLoader) {
return ServiceLoader.load(TrayIntegrationProvider.class, classLoader).findFirst();
}
@Provides
@Singleton
static ResourceBundle provideLocalization() {
return ResourceBundle.getBundle("i18n.strings");
}
@Provides
@Singleton
@Named("launchEventQueue")
static BlockingQueue<AppLaunchEvent> provideFileOpenRequests() {
return new ArrayBlockingQueue<>(10);
}
}

View File

@@ -2,11 +2,11 @@ package org.cryptomator.ui.lock;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.vaults.Vault;
import javax.inject.Named;
import javafx.stage.Stage;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
@@ -25,15 +25,9 @@ public interface LockComponent {
return workflow;
}
@Subcomponent.Builder
interface Builder {
@BindsInstance
LockComponent.Builder vault(@LockWindow Vault vault);
@BindsInstance
LockComponent.Builder owner(@Named("lockWindowOwner") Optional<Stage> owner);
LockComponent build();
@Subcomponent.Factory
interface Factory {
LockComponent create(@BindsInstance @LockWindow Vault vault, @BindsInstance @Named("lockWindowOwner") @Nullable Stage owner);
}
}

View File

@@ -2,56 +2,48 @@ package org.cryptomator.ui.lock;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
@LockScoped
public class LockForcedController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(LockForcedController.class);
private final Stage window;
private final Vault vault;
private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
private final AtomicReference<CompletableFuture<Boolean>> forceRetryDecision;
@Inject
public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock) {
public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, AtomicReference<CompletableFuture<Boolean>> forceRetryDecision) {
this.window = window;
this.vault = vault;
this.forceLockDecisionLock = forceLockDecisionLock;
this.forceRetryDecision = forceRetryDecision;
this.window.setOnHiding(this::windowClosed);
}
@FXML
public void cancel() {
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL);
window.close();
}
@FXML
public void retry() {
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.RETRY);
forceRetryDecision.get().complete(false);
window.close();
}
@FXML
public void force() {
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.FORCE);
forceRetryDecision.get().complete(true);
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
// if not already interacted, set the decision to CANCEL
if (forceLockDecisionLock.awaitingInteraction().get()) {
LOG.debug("Lock canceled in force-lock-phase by user.");
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL);
}
forceRetryDecision.get().cancel(true);
}
// ----- Getter & Setter -----

View File

@@ -6,13 +6,13 @@ import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.common.UserInteractionLock;
import org.jetbrains.annotations.Nullable;
import javax.inject.Named;
import javax.inject.Provider;
@@ -20,22 +20,17 @@ import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
@Module
abstract class LockModule {
enum ForceLockDecision {
CANCEL,
RETRY,
FORCE;
}
@Provides
@LockScoped
static UserInteractionLock<LockModule.ForceLockDecision> provideForceLockDecisionLock() {
return new UserInteractionLock<>(null);
static AtomicReference<CompletableFuture<Boolean>> provideForceRetryDecisionRef() {
return new AtomicReference<>();
}
@Provides
@@ -48,12 +43,12 @@ abstract class LockModule {
@Provides
@LockWindow
@LockScoped
static Stage provideWindow(StageFactory factory, @LockWindow Vault vault, @Named("lockWindowOwner") Optional<Stage> owner) {
static Stage provideWindow(StageFactory factory, @LockWindow Vault vault, @Nullable @Named("lockWindowOwner") Stage owner) {
Stage stage = factory.create();
stage.setTitle(vault.getDisplayName());
stage.setResizable(false);
if (owner.isPresent()) {
stage.initOwner(owner.get());
if (owner != null) {
stage.initOwner(owner);
stage.initModality(Modality.WINDOW_MODAL);
} else {
stage.initModality(Modality.APPLICATION_MODAL);

View File

@@ -5,10 +5,9 @@ import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -18,6 +17,10 @@ import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
/**
* The sequence of actions performed and checked during lock of a vault.
@@ -34,43 +37,48 @@ public class LockWorkflow extends Task<Void> {
private final Stage lockWindow;
private final Vault vault;
private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
private final AtomicReference<CompletableFuture<Boolean>> forceRetryDecision;
private final Lazy<Scene> lockForcedScene;
private final Lazy<Scene> lockFailedScene;
private final ErrorComponent.Builder errorComponent;
private final FxApplicationWindows appWindows;
@Inject
public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene, ErrorComponent.Builder errorComponent) {
public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, AtomicReference<CompletableFuture<Boolean>> forceRetryDecision, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene, FxApplicationWindows appWindows) {
this.lockWindow = lockWindow;
this.vault = vault;
this.forceLockDecisionLock = forceLockDecisionLock;
this.forceRetryDecision = forceRetryDecision;
this.lockForcedScene = lockForcedScene;
this.lockFailedScene = lockFailedScene;
this.errorComponent = errorComponent;
this.appWindows = appWindows;
}
@Override
protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException {
protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException, ExecutionException {
lock(false);
return null;
}
private void lock(boolean forced) throws InterruptedException {
private void lock(boolean forced) throws InterruptedException, ExecutionException {
try {
vault.lock(forced);
} catch (Volume.VolumeException | LockNotCompletedException e) {
LOG.info("Locking {} failed (forced: {}).", vault.getDisplayName(), forced, e);
var decision = askUserForAction();
switch (decision) {
case RETRY -> lock(false);
case FORCE -> lock(true);
case CANCEL -> cancel(false);
}
retryOrCancel();
}
}
private LockModule.ForceLockDecision askUserForAction() throws InterruptedException {
forceLockDecisionLock.reset(null);
private void retryOrCancel() throws ExecutionException, InterruptedException {
try {
boolean forced = askWhetherToUseTheForce().get();
lock(forced);
} catch (CancellationException e) {
cancel(false);
}
}
private CompletableFuture<Boolean> askWhetherToUseTheForce() {
var decision = new CompletableFuture<Boolean>();
forceRetryDecision.set(decision);
// show forcedLock dialogue ...
Platform.runLater(() -> {
lockWindow.setScene(lockForcedScene.get());
@@ -83,8 +91,7 @@ public class LockWorkflow extends Task<Void> {
lockWindow.centerOnScreen();
}
});
// ... and wait for answer
return forceLockDecisionLock.awaitInteraction();
return decision;
}
@Override
@@ -102,7 +109,7 @@ public class LockWorkflow extends Task<Void> {
lockWindow.setScene(lockFailedScene.get());
lockWindow.show();
} else {
errorComponent.cause(throwable).window(lockWindow).build().showErrorScene();
appWindows.showErrorWindow(throwable, lockWindow, null);
}
}

View File

@@ -9,6 +9,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface MainWindow {
@interface MainWindow {
}

View File

@@ -13,6 +13,8 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.common.StageInitializer;
import org.cryptomator.ui.fxapp.PrimaryStage;
import org.cryptomator.ui.health.HealthCheckComponent;
import org.cryptomator.ui.migration.MigrationComponent;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
@@ -34,6 +36,18 @@ import java.util.ResourceBundle;
@Module(subcomponents = {AddVaultWizardComponent.class, HealthCheckComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class, ErrorComponent.class})
abstract class MainWindowModule {
@Provides
@MainWindow
@MainWindowScoped
static Stage provideMainWindow(@PrimaryStage Stage stage, StageInitializer initializer) {
initializer.accept(stage);
stage.setTitle("Cryptomator");
stage.initStyle(StageStyle.UNDECORATED);
stage.setMinWidth(650);
stage.setMinHeight(440);
return stage;
}
@Provides
@MainWindowScoped
static ObjectProperty<Vault> provideSelectedVault() {
@@ -47,22 +61,11 @@ abstract class MainWindowModule {
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@MainWindow
@MainWindowScoped
static Stage provideStage(StageFactory factory) {
Stage stage = factory.create(StageStyle.UNDECORATED);
stage.setMinWidth(650);
stage.setMinHeight(440);
stage.setTitle("Cryptomator");
return stage;
}
@Provides
@MainWindowScoped
@Named("errorWindow")
static Stage provideErrorStage(@MainWindow Stage window, StageFactory factory, ResourceBundle resourceBundle) {
Stage stage = factory.create(StageStyle.DECORATED);
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("main.vaultDetail.error.windowTitle"));
stage.initModality(Modality.APPLICATION_MODAL);
stage.initOwner(window);

View File

@@ -3,9 +3,9 @@ package org.cryptomator.ui.mainwindow;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.FxApplicationTerminator;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.fxapp.UpdateChecker;
import org.cryptomator.ui.launcher.AppLifecycleListener;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.slf4j.Logger;
@@ -25,9 +25,9 @@ public class MainWindowTitleController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(MainWindowTitleController.class);
private final AppLifecycleListener appLifecycle;
private final Stage window;
private final FxApplication application;
private final FxApplicationTerminator terminator;
private final FxApplicationWindows appWindows;
private final boolean trayMenuInitialized;
private final UpdateChecker updateChecker;
private final BooleanBinding updateAvailable;
@@ -40,10 +40,10 @@ public class MainWindowTitleController implements FxController {
private double yOffset;
@Inject
MainWindowTitleController(AppLifecycleListener appLifecycle, @MainWindow Stage window, FxApplication application, TrayMenuComponent trayMenu, UpdateChecker updateChecker, LicenseHolder licenseHolder, Settings settings) {
this.appLifecycle = appLifecycle;
MainWindowTitleController(@MainWindow Stage window, FxApplicationTerminator terminator, FxApplicationWindows appWindows, TrayMenuComponent trayMenu, UpdateChecker updateChecker, LicenseHolder licenseHolder, Settings settings) {
this.window = window;
this.application = application;
this.terminator = terminator;
this.appWindows = appWindows;
this.trayMenuInitialized = trayMenu.isInitialized();
this.updateChecker = updateChecker;
this.updateAvailable = updateChecker.latestVersionProperty().isNotNull();
@@ -96,7 +96,7 @@ public class MainWindowTitleController implements FxController {
if (trayMenuInitialized) {
window.close();
} else {
appLifecycle.quit();
terminator.terminate();
}
}
@@ -107,17 +107,17 @@ public class MainWindowTitleController implements FxController {
@FXML
public void showPreferences() {
application.showPreferencesWindow(SelectedPreferencesTab.ANY);
appWindows.showPreferencesWindow(SelectedPreferencesTab.ANY);
}
@FXML
public void showGeneralPreferences() {
application.showPreferencesWindow(SelectedPreferencesTab.GENERAL);
appWindows.showPreferencesWindow(SelectedPreferencesTab.GENERAL);
}
@FXML
public void showDonationKeyPreferences() {
application.showPreferencesWindow(SelectedPreferencesTab.CONTRIBUTE);
appWindows.showPreferencesWindow(SelectedPreferencesTab.CONTRIBUTE);
}
/* Getter/Setter */

View File

@@ -1,7 +1,6 @@
package org.cryptomator.ui.mainwindow;
import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.Subscription;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.common.Animations;
@@ -9,9 +8,9 @@ import org.cryptomator.ui.common.AutoAnimator;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.fxapp.FxApplication;
import javax.inject.Inject;
import javafx.application.Application;
import javafx.beans.binding.Binding;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
@@ -22,7 +21,7 @@ import javafx.fxml.FXML;
public class VaultDetailController implements FxController {
private final ReadOnlyObjectProperty<Vault> vault;
private final FxApplication application;
private final Application application;
private final Binding<FontAwesome5Icon> glyph;
private final BooleanBinding anyVaultSelected;
@@ -33,7 +32,7 @@ public class VaultDetailController implements FxController {
@Inject
VaultDetailController(ObjectProperty<Vault> vault, FxApplication application) {
VaultDetailController(ObjectProperty<Vault> vault, Application application) {
this.vault = vault;
this.application = application;
this.glyph = EasyBind.select(vault) //

View File

@@ -4,8 +4,7 @@ import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.health.HealthCheckComponent;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
@@ -14,25 +13,23 @@ import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.Optional;
@MainWindowScoped
public class VaultDetailLockedController implements FxController {
private final ReadOnlyObjectProperty<Vault> vault;
private final FxApplication application;
private final FxApplicationWindows appWindows;
private final VaultOptionsComponent.Builder vaultOptionsWindow;
private final KeychainManager keychain;
private final Stage mainWindow;
private final BooleanExpression passwordSaved;
@Inject
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) {
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) {
this.vault = vault;
this.application = application;
this.appWindows = appWindows;
this.vaultOptionsWindow = vaultOptionsWindow;
this.keychain = keychain;
this.mainWindow = mainWindow;
@@ -45,7 +42,7 @@ public class VaultDetailLockedController implements FxController {
@FXML
public void unlock() {
application.startUnlockWorkflow(vault.get(), Optional.of(mainWindow));
appWindows.startUnlockWorkflow(vault.get(), mainWindow);
}
@FXML

View File

@@ -1,15 +1,13 @@
package org.cryptomator.ui.mainwindow;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
@@ -18,21 +16,21 @@ import javafx.stage.Stage;
public class VaultDetailUnknownErrorController implements FxController {
private final ObjectProperty<Vault> vault;
private final ErrorComponent.Builder errorComponentBuilder;
private final FxApplicationWindows appWindows;
private final Stage errorWindow;
private final RemoveVaultComponent.Builder removeVault;
@Inject
public VaultDetailUnknownErrorController(ObjectProperty<Vault> vault, ErrorComponent.Builder errorComponentBuilder, @Named("errorWindow") Stage errorWindow, RemoveVaultComponent.Builder removeVault) {
public VaultDetailUnknownErrorController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, @Named("errorWindow") Stage errorWindow, RemoveVaultComponent.Builder removeVault) {
this.vault = vault;
this.errorComponentBuilder = errorComponentBuilder;
this.appWindows = appWindows;
this.errorWindow = errorWindow;
this.removeVault = removeVault;
}
@FXML
public void showError() {
errorComponentBuilder.window(errorWindow).cause(vault.get().getLastKnownException()).build().showErrorScene();
appWindows.showErrorWindow(vault.get().getLastKnownException(), errorWindow, null);
}
@FXML

View File

@@ -6,7 +6,7 @@ import com.google.common.cache.LoadingCache;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.stats.VaultStatisticsComponent;
import javax.inject.Inject;
@@ -14,22 +14,21 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.Optional;
@MainWindowScoped
public class VaultDetailUnlockedController implements FxController {
private final ReadOnlyObjectProperty<Vault> vault;
private final FxApplication application;
private final FxApplicationWindows appWindows;
private final VaultService vaultService;
private final Stage mainWindow;
private final LoadingCache<Vault, VaultStatisticsComponent> vaultStats;
private final VaultStatisticsComponent.Builder vaultStatsBuilder;
@Inject
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplication application, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) {
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) {
this.vault = vault;
this.application = application;
this.appWindows = appWindows;
this.vaultService = vaultService;
this.mainWindow = mainWindow;
this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats));
@@ -47,7 +46,7 @@ public class VaultDetailUnlockedController implements FxController {
@FXML
public void lock() {
application.startLockWorkflow(vault.get(), Optional.of(mainWindow));
appWindows.startLockWorkflow(vault.get(), mainWindow);
}
@FXML

View File

@@ -7,7 +7,8 @@ import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
@@ -18,7 +19,6 @@ import javafx.beans.property.ObjectProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.EnumSet;
import java.util.Optional;
import static org.cryptomator.common.vaults.VaultState.Value.*;
@@ -27,7 +27,8 @@ public class VaultListContextMenuController implements FxController {
private final ObservableOptionalValue<Vault> selectedVault;
private final Stage mainWindow;
private final FxApplication application;
private final FxApplicationWindows appWindows;
private final VaultService vaultService;
private final KeychainManager keychain;
private final RemoveVaultComponent.Builder removeVault;
private final VaultOptionsComponent.Builder vaultOptionsWindow;
@@ -38,10 +39,11 @@ public class VaultListContextMenuController implements FxController {
private final Binding<Boolean> selectedVaultLockable;
@Inject
VaultListContextMenuController(ObjectProperty<Vault> selectedVault, @MainWindow Stage mainWindow, FxApplication application, KeychainManager keychain, RemoveVaultComponent.Builder removeVault, VaultOptionsComponent.Builder vaultOptionsWindow) {
VaultListContextMenuController(ObjectProperty<Vault> selectedVault, @MainWindow Stage mainWindow, FxApplicationWindows appWindows, VaultService vaultService, KeychainManager keychain, RemoveVaultComponent.Builder removeVault, VaultOptionsComponent.Builder vaultOptionsWindow) {
this.selectedVault = EasyBind.wrapNullable(selectedVault);
this.mainWindow = mainWindow;
this.application = application;
this.appWindows = appWindows;
this.vaultService = vaultService;
this.keychain = keychain;
this.removeVault = removeVault;
this.vaultOptionsWindow = vaultOptionsWindow;
@@ -74,22 +76,20 @@ public class VaultListContextMenuController implements FxController {
@FXML
public void didClickUnlockVault() {
selectedVault.ifValuePresent(v -> {
application.startUnlockWorkflow(v, Optional.of(mainWindow));
appWindows.startUnlockWorkflow(v, mainWindow);
});
}
@FXML
public void didClickLockVault() {
selectedVault.ifValuePresent(v -> {
application.startLockWorkflow(v, Optional.of(mainWindow));
appWindows.startLockWorkflow(v, mainWindow);
});
}
@FXML
public void didClickRevealVault() {
selectedVault.ifValuePresent(v -> {
application.getVaultService().reveal(v);
});
selectedVault.ifValuePresent(vaultService::reveal);
}
// Getter and Setter

View File

@@ -2,9 +2,9 @@ package org.cryptomator.ui.migration;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import javax.inject.Inject;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.stage.Stage;
@@ -12,13 +12,13 @@ public class MigrationImpossibleController implements FxController {
private static final String HELP_URI = "https://docs.cryptomator.org/en/1.5/help/manual-migration/";
private final FxApplication fxApplication;
private final Application application;
private final Stage window;
private final Vault vault;
@Inject
MigrationImpossibleController(FxApplication fxApplication, @MigrationWindow Stage window, @MigrationWindow Vault vault) {
this.fxApplication = fxApplication;
MigrationImpossibleController(Application application, @MigrationWindow Stage window, @MigrationWindow Vault vault) {
this.application = application;
this.window = window;
this.vault = vault;
}
@@ -30,7 +30,7 @@ public class MigrationImpossibleController implements FxController {
@FXML
public void getMigrationHelp() {
fxApplication.getHostServices().showDocument(HELP_URI);
application.getHostServices().showDocument(HELP_URI);
}
/* Getter/Setters */

View File

@@ -6,13 +6,13 @@ import dagger.Provides;
import dagger.multibindings.IntoMap;
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.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.fxapp.PrimaryStage;
import javax.inject.Named;
import javax.inject.Provider;
@@ -37,7 +37,7 @@ abstract class MigrationModule {
@Provides
@MigrationWindow
@MigrationScoped
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
static Stage provideStage(StageFactory factory, @PrimaryStage Stage owner, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("migration.title"));
stage.setResizable(false);

View File

@@ -12,12 +12,12 @@ import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
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.cryptomator.ui.fxapp.FxApplicationWindows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -58,7 +58,7 @@ public class MigrationRunController implements FxController {
private final ScheduledExecutorService scheduler;
private final KeychainManager keychain;
private final ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability;
private final ErrorComponent.Builder errorComponent;
private final FxApplicationWindows appWindows;
private final Lazy<Scene> startScene;
private final Lazy<Scene> successScene;
private final Lazy<Scene> impossibleScene;
@@ -73,14 +73,14 @@ public class MigrationRunController implements FxController {
public NiceSecurePasswordField passwordField;
@Inject
public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, KeychainManager keychain, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy<Scene> capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) Lazy<Scene> impossibleScene, ErrorComponent.Builder errorComponent) {
public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, KeychainManager keychain, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy<Scene> capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) Lazy<Scene> impossibleScene, FxApplicationWindows appWindows) {
this.window = window;
this.vault = vault;
this.executor = executor;
this.scheduler = scheduler;
this.keychain = keychain;
this.missingCapability = missingCapability;
this.errorComponent = errorComponent;
this.appWindows = appWindows;
this.startScene = startScene;
this.successScene = successScene;
this.migrateButtonContentDisplay = Bindings.createObjectBinding(this::getMigrateButtonContentDisplay, vault.stateProperty());
@@ -146,12 +146,12 @@ public class MigrationRunController implements FxController {
}).onError(FileNameTooLongException.class, e -> {
LOG.error("Migration failed because the underlying file system does not support long filenames.", e);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
appWindows.showErrorWindow(e, window, startScene.get());
window.setScene(impossibleScene.get());
}).onError(Exception.class, e -> { // including RuntimeExceptions
LOG.error("Migration failed for technical reasons.", e);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
appWindows.showErrorWindow(e, window, startScene.get());
}).andFinally(() -> {
passwordField.setDisable(false);
progressSyncTask.cancel(true);

View File

@@ -2,25 +2,24 @@ package org.cryptomator.ui.migration;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.fxapp.PrimaryStage;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.Optional;
@MigrationScoped
public class MigrationSuccessController implements FxController {
private final FxApplication fxApplication;
private final FxApplicationWindows appWindows;
private final Stage window;
private final Vault vault;
private final Stage mainWindow;
@Inject
MigrationSuccessController(FxApplication fxApplication, @MigrationWindow Stage window, @MigrationWindow Vault vault, @MainWindow Stage mainWindow) {
this.fxApplication = fxApplication;
MigrationSuccessController(FxApplicationWindows appWindows, @MigrationWindow Stage window, @MigrationWindow Vault vault, @PrimaryStage Stage mainWindow) {
this.appWindows = appWindows;
this.window = window;
this.vault = vault;
this.mainWindow = mainWindow;
@@ -29,7 +28,7 @@ public class MigrationSuccessController implements FxController {
@FXML
public void unlockAndClose() {
close();
fxApplication.startUnlockWorkflow(vault, Optional.of(mainWindow));
appWindows.startUnlockWorkflow(vault, mainWindow);
}
@FXML

View File

@@ -1,34 +1,25 @@
package org.cryptomator.ui.preferences;
import org.cryptomator.common.Environment;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.integrations.autostart.AutoStartProvider;
import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.geometry.NodeOrientation;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
@PreferencesScoped
@@ -38,67 +29,36 @@ public class GeneralPreferencesController implements FxController {
private final Stage window;
private final Settings settings;
private final boolean trayMenuInitialized;
private final boolean trayMenuSupported;
private final Optional<AutoStartProvider> autoStartProvider;
private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
private final LicenseHolder licenseHolder;
private final ResourceBundle resourceBundle;
private final Application application;
private final Environment environment;
private final Set<KeychainAccessProvider> keychainAccessProviders;
private final ErrorComponent.Builder errorComponent;
public ChoiceBox<UiTheme> themeChoiceBox;
private final FxApplicationWindows appWindows;
public ChoiceBox<KeychainAccessProvider> keychainBackendChoiceBox;
public CheckBox showMinimizeButtonCheckbox;
public CheckBox showTrayIconCheckbox;
public CheckBox startHiddenCheckbox;
public CheckBox debugModeCheckbox;
public CheckBox autoStartCheckbox;
public ToggleGroup nodeOrientation;
public RadioButton nodeOrientationLtr;
public RadioButton nodeOrientationRtl;
@Inject
GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, TrayMenuComponent trayMenu, Optional<AutoStartProvider> autoStartProvider, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) {
GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, Optional<AutoStartProvider> autoStartProvider, Set<KeychainAccessProvider> keychainAccessProviders, Application application, Environment environment, FxApplicationWindows appWindows) {
this.window = window;
this.settings = settings;
this.trayMenuInitialized = trayMenu.isInitialized();
this.trayMenuSupported = trayMenu.isSupported();
this.autoStartProvider = autoStartProvider;
this.keychainAccessProviders = keychainAccessProviders;
this.selectedTabProperty = selectedTabProperty;
this.licenseHolder = licenseHolder;
this.resourceBundle = resourceBundle;
this.application = application;
this.environment = environment;
this.errorComponent = errorComponent;
this.appWindows = appWindows;
}
@FXML
public void initialize() {
themeChoiceBox.getItems().addAll(UiTheme.applicableValues());
if (!themeChoiceBox.getItems().contains(settings.theme().get())) {
settings.theme().set(UiTheme.LIGHT);
}
themeChoiceBox.valueProperty().bindBidirectional(settings.theme());
themeChoiceBox.setConverter(new UiThemeConverter(resourceBundle));
showMinimizeButtonCheckbox.selectedProperty().bindBidirectional(settings.showMinimizeButton());
showTrayIconCheckbox.selectedProperty().bindBidirectional(settings.showTrayIcon());
startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden());
debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode());
autoStartProvider.ifPresent(autoStart -> autoStartCheckbox.setSelected(autoStart.isEnabled()));
nodeOrientationLtr.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.LEFT_TO_RIGHT);
nodeOrientationRtl.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.RIGHT_TO_LEFT);
nodeOrientation.selectedToggleProperty().addListener(this::toggleNodeOrientation);
var keychainSettingsConverter = new KeychainProviderClassNameConverter(keychainAccessProviders);
keychainBackendChoiceBox.getItems().addAll(keychainAccessProviders);
keychainBackendChoiceBox.setValue(keychainSettingsConverter.fromString(settings.keychainProvider().get()));
@@ -106,29 +66,10 @@ public class GeneralPreferencesController implements FxController {
Bindings.bindBidirectional(settings.keychainProvider(), keychainBackendChoiceBox.valueProperty(), keychainSettingsConverter);
}
public boolean isTrayMenuInitialized() {
return trayMenuInitialized;
}
public boolean isTrayMenuSupported() {
return trayMenuSupported;
}
public boolean isAutoStartSupported() {
return autoStartProvider.isPresent();
}
private void toggleNodeOrientation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
if (nodeOrientationLtr.equals(newValue)) {
settings.userInterfaceOrientation().set(NodeOrientation.LEFT_TO_RIGHT);
} else if (nodeOrientationRtl.equals(newValue)) {
settings.userInterfaceOrientation().set(NodeOrientation.RIGHT_TO_LEFT);
} else {
LOG.warn("Unexpected toggle option {}", newValue);
}
}
@FXML
public void toggleAutoStart() {
autoStartProvider.ifPresent(autoStart -> {
@@ -142,21 +83,11 @@ public class GeneralPreferencesController implements FxController {
} catch (ToggleAutoStartFailedException e) {
autoStartCheckbox.setSelected(!enableAutoStart); // restore previous state
LOG.error("Failed to toggle autostart.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
appWindows.showErrorWindow(e, window, window.getScene());
}
});
}
public LicenseHolder getLicenseHolder() {
return licenseHolder;
}
@FXML
public void showContributeTab() {
selectedTabProperty.set(SelectedPreferencesTab.CONTRIBUTE);
}
@FXML
public void showLogfileDirectory() {
environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
@@ -164,27 +95,7 @@ public class GeneralPreferencesController implements FxController {
/* Helper classes */
private static class UiThemeConverter extends StringConverter<UiTheme> {
private final ResourceBundle resourceBundle;
UiThemeConverter(ResourceBundle resourceBundle) {
this.resourceBundle = resourceBundle;
}
@Override
public String toString(UiTheme impl) {
return resourceBundle.getString(impl.getDisplayName());
}
@Override
public UiTheme fromString(String string) {
throw new UnsupportedOperationException();
}
}
private class KeychainProviderDisplayNameConverter extends StringConverter<KeychainAccessProvider> {
private static class KeychainProviderDisplayNameConverter extends StringConverter<KeychainAccessProvider> {
@Override
public String toString(KeychainAccessProvider provider) {

View File

@@ -0,0 +1,156 @@
package org.cryptomator.ui.preferences;
import com.google.common.base.Strings;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.launcher.SupportedLanguages;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.geometry.NodeOrientation;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.util.StringConverter;
import java.util.Locale;
import java.util.ResourceBundle;
@PreferencesScoped
public class InterfacePreferencesController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(InterfacePreferencesController.class);
private final Settings settings;
private final boolean trayMenuInitialized;
private final boolean trayMenuSupported;
private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
private final LicenseHolder licenseHolder;
private final ResourceBundle resourceBundle;
public ChoiceBox<UiTheme> themeChoiceBox;
public CheckBox showMinimizeButtonCheckbox;
public CheckBox showTrayIconCheckbox;
public ChoiceBox<String> preferredLanguageChoiceBox;
public ToggleGroup nodeOrientation;
public RadioButton nodeOrientationLtr;
public RadioButton nodeOrientationRtl;
@Inject
InterfacePreferencesController(Settings settings, TrayMenuComponent trayMenu, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ResourceBundle resourceBundle) {
this.settings = settings;
this.trayMenuInitialized = trayMenu.isInitialized();
this.trayMenuSupported = trayMenu.isSupported();
this.selectedTabProperty = selectedTabProperty;
this.licenseHolder = licenseHolder;
this.resourceBundle = resourceBundle;
}
@FXML
public void initialize() {
themeChoiceBox.getItems().addAll(UiTheme.applicableValues());
if (!themeChoiceBox.getItems().contains(settings.theme().get())) {
settings.theme().set(UiTheme.LIGHT);
}
themeChoiceBox.valueProperty().bindBidirectional(settings.theme());
themeChoiceBox.setConverter(new UiThemeConverter(resourceBundle));
showMinimizeButtonCheckbox.selectedProperty().bindBidirectional(settings.showMinimizeButton());
showTrayIconCheckbox.selectedProperty().bindBidirectional(settings.showTrayIcon());
preferredLanguageChoiceBox.getItems().add(null);
preferredLanguageChoiceBox.getItems().addAll(SupportedLanguages.LANGUAGAE_TAGS);
preferredLanguageChoiceBox.valueProperty().bindBidirectional(settings.languageProperty());
preferredLanguageChoiceBox.setConverter(new LanguageTagConverter(resourceBundle));
nodeOrientationLtr.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.LEFT_TO_RIGHT);
nodeOrientationRtl.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.RIGHT_TO_LEFT);
nodeOrientation.selectedToggleProperty().addListener(this::toggleNodeOrientation);
}
public boolean isTrayMenuInitialized() {
return trayMenuInitialized;
}
public boolean isTrayMenuSupported() {
return trayMenuSupported;
}
private void toggleNodeOrientation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
if (nodeOrientationLtr.equals(newValue)) {
settings.userInterfaceOrientation().set(NodeOrientation.LEFT_TO_RIGHT);
} else if (nodeOrientationRtl.equals(newValue)) {
settings.userInterfaceOrientation().set(NodeOrientation.RIGHT_TO_LEFT);
} else {
LOG.warn("Unexpected toggle option {}", newValue);
}
}
public LicenseHolder getLicenseHolder() {
return licenseHolder;
}
@FXML
public void showContributeTab() {
selectedTabProperty.set(SelectedPreferencesTab.CONTRIBUTE);
}
/* Helper classes */
private static class UiThemeConverter extends StringConverter<UiTheme> {
private final ResourceBundle resourceBundle;
UiThemeConverter(ResourceBundle resourceBundle) {
this.resourceBundle = resourceBundle;
}
@Override
public String toString(UiTheme impl) {
return resourceBundle.getString(impl.getDisplayName());
}
@Override
public UiTheme fromString(String string) {
throw new UnsupportedOperationException();
}
}
private static class LanguageTagConverter extends StringConverter<String> {
private final ResourceBundle resourceBundle;
LanguageTagConverter(ResourceBundle resourceBundle) {
this.resourceBundle = resourceBundle;
}
@Override
public String toString(String tag) {
if (tag == null) {
return resourceBundle.getString("preferences.interface.language.auto");
} else {
var locale = Locale.forLanguageTag(tag);
var lang = locale.getDisplayLanguage(locale);
var region = locale.getDisplayCountry(locale);
return lang + (Strings.isNullOrEmpty(region) ? "" : " (" + region + ")");
}
}
@Override
public String fromString(String displayLanguage) {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -24,6 +24,7 @@ public class PreferencesController implements FxController {
private final BooleanBinding updateAvailable;
public TabPane tabPane;
public Tab generalTab;
public Tab interfaceTab;
public Tab volumeTab;
public Tab updatesTab;
public Tab contributeTab;
@@ -50,10 +51,11 @@ public class PreferencesController implements FxController {
private Tab getTabToSelect(SelectedPreferencesTab selectedTab) {
return switch (selectedTab) {
case UPDATES -> updatesTab;
case VOLUME -> volumeTab;
case CONTRIBUTE -> contributeTab;
case GENERAL -> generalTab;
case INTERFACE -> interfaceTab;
case VOLUME -> volumeTab;
case UPDATES -> updatesTab;
case CONTRIBUTE -> contributeTab;
case ABOUT -> aboutTab;
case ANY -> updateAvailable.get() ? updatesTab : generalTab;
};

View File

@@ -64,6 +64,11 @@ abstract class PreferencesModule {
@FxControllerKey(GeneralPreferencesController.class)
abstract FxController bindGeneralPreferencesController(GeneralPreferencesController controller);
@Binds
@IntoMap
@FxControllerKey(InterfacePreferencesController.class)
abstract FxController bindInterfacePreferencesController(InterfacePreferencesController controller);
@Binds
@IntoMap
@FxControllerKey(UpdatesPreferencesController.class)

View File

@@ -11,6 +11,11 @@ public enum SelectedPreferencesTab {
*/
GENERAL,
/**
* Show interface tab
*/
INTERFACE,
/**
* Show volume tab
*/

View File

@@ -5,11 +5,11 @@ import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,18 +33,18 @@ public class RecoveryKeyCreationController implements FxController {
private final ExecutorService executor;
private final RecoveryKeyFactory recoveryKeyFactory;
private final StringProperty recoveryKeyProperty;
private final ErrorComponent.Builder errorComponent;
private final FxApplicationWindows appWindows;
public NiceSecurePasswordField passwordField;
@Inject
public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy<Scene> successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, ErrorComponent.Builder errorComponent) {
public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy<Scene> successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, FxApplicationWindows appWindows) {
this.window = window;
this.successScene = successScene;
this.vault = vault;
this.executor = executor;
this.recoveryKeyFactory = recoveryKeyFactory;
this.recoveryKeyProperty = recoveryKey;
this.errorComponent = errorComponent;
this.appWindows = appWindows;
}
@FXML
@@ -63,7 +63,7 @@ public class RecoveryKeyCreationController implements FxController {
Animations.createShakeWindowAnimation(window).play();
} else {
LOG.error("Creation of recovery key failed.", task.getException());
errorComponent.cause(task.getException()).window(window).returnToScene(window.getScene()).build().showErrorScene();
appWindows.showErrorWindow(task.getException(), window, window.getScene());
}
});
executor.submit(task);

View File

@@ -7,6 +7,7 @@ import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.jetbrains.annotations.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -16,6 +17,7 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.Predicate;
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@@ -102,12 +104,29 @@ public class RecoveryKeyFactory {
* @return <code>true</code> if this seems to be a legitimate recovery key
*/
public boolean validateRecoveryKey(String recoveryKey) {
return validateRecoveryKey(recoveryKey, null);
}
/**
* Checks whether a String is a syntactically correct recovery key with a valid checksum and passes the extended validation.
*
* @param recoveryKey A word sequence which might be a recovery key
* @param extendedValidation Additional verification of the decoded key (optional)
* @return <code>true</code> if this seems to be a legitimate recovery key and passes the extended validation
*/
public boolean validateRecoveryKey(String recoveryKey, @Nullable Predicate<byte[]> extendedValidation) {
byte[] key = new byte[0];
try {
byte[] key = decodeRecoveryKey(recoveryKey);
Arrays.fill(key, (byte) 0x00);
return true;
key = decodeRecoveryKey(recoveryKey);
if (extendedValidation != null) {
return extendedValidation.test(key);
} else {
return true;
}
} catch (IllegalArgumentException e) {
return false;
} finally {
Arrays.fill(key, (byte) 0x00);
}
}

View File

@@ -4,7 +4,9 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
@@ -22,12 +24,25 @@ import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.Map;
import java.util.ResourceBundle;
@Module
abstract class RecoveryKeyModule {
@Provides
@Nullable
@RecoveryKeyWindow
@RecoveryKeyScoped
static VaultConfig.UnverifiedVaultConfig vaultConfig(@RecoveryKeyWindow Vault vault) {
try {
return vault.getVaultConfigCache().get();
} catch (IOException e) {
return null;
}
}
@Provides
@RecoveryKeyWindow
@RecoveryKeyScoped

View File

@@ -3,10 +3,16 @@ package org.cryptomator.ui.recoverykey;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import dagger.Lazy;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.VaultConfigLoadException;
import org.cryptomator.cryptofs.VaultKeyInvalidException;
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 javafx.beans.binding.Bindings;
@@ -24,10 +30,12 @@ import java.util.Optional;
@RecoveryKeyScoped
public class RecoveryKeyRecoverController implements FxController {
private final static CharMatcher ALLOWED_CHARS = CharMatcher.inRange('a', 'z').or(CharMatcher.is(' '));
private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class);
private static final CharMatcher ALLOWED_CHARS = CharMatcher.inRange('a', 'z').or(CharMatcher.is(' '));
private final Stage window;
private final Vault vault;
private final VaultConfig.UnverifiedVaultConfig unverifiedVaultConfig;
private final StringProperty recoveryKey;
private final RecoveryKeyFactory recoveryKeyFactory;
private final BooleanBinding validRecoveryKey;
@@ -37,9 +45,10 @@ public class RecoveryKeyRecoverController implements FxController {
public TextArea textarea;
@Inject
public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene) {
public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow @Nullable VaultConfig.UnverifiedVaultConfig unverifiedVaultConfig, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene) {
this.window = window;
this.vault = vault;
this.unverifiedVaultConfig = unverifiedVaultConfig;
this.recoveryKey = recoveryKey;
this.recoveryKeyFactory = recoveryKeyFactory;
this.validRecoveryKey = Bindings.createBooleanBinding(this::isValidRecoveryKey, recoveryKey);
@@ -96,6 +105,20 @@ public class RecoveryKeyRecoverController implements FxController {
window.setScene(resetPasswordScene.get());
}
private boolean checkKeyAgainstVaultConfig(byte[] key) {
try {
var config = unverifiedVaultConfig.verify(key, unverifiedVaultConfig.allegedVaultVersion());
LOG.info("Provided recovery key matches vault config signature for vault {}", config.getId());
return true;
} catch (VaultKeyInvalidException e) {
LOG.debug("Provided recovery key does not match vault config signature.");
return false;
} catch (VaultConfigLoadException e) {
LOG.error("Failed to parse vault config", e);
return false;
}
}
/* Getter/Setter */
public Vault getVault() {
@@ -107,7 +130,11 @@ public class RecoveryKeyRecoverController implements FxController {
}
public boolean isValidRecoveryKey() {
return recoveryKeyFactory.validateRecoveryKey(recoveryKey.get());
if (unverifiedVaultConfig != null) {
return recoveryKeyFactory.validateRecoveryKey(recoveryKey.get(), this::checkKeyAgainstVaultConfig);
} else {
return recoveryKeyFactory.validateRecoveryKey(recoveryKey.get());
}
}
public TextFormatter getRecoveryKeyTextFormatter() {

View File

@@ -2,11 +2,11 @@ package org.cryptomator.ui.recoverykey;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,19 +31,19 @@ public class RecoveryKeyResetPasswordController implements FxController {
private final ExecutorService executor;
private final StringProperty recoveryKey;
private final Lazy<Scene> recoverScene;
private final ErrorComponent.Builder errorComponent;
private final FxApplicationWindows appWindows;
public NewPasswordController newPasswordController;
@Inject
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverScene, ErrorComponent.Builder errorComponent) {
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverScene, FxApplicationWindows appWindows) {
this.window = window;
this.vault = vault;
this.recoveryKeyFactory = recoveryKeyFactory;
this.executor = executor;
this.recoveryKey = recoveryKey;
this.recoverScene = recoverScene;
this.errorComponent = errorComponent;
this.appWindows = appWindows;
}
@FXML
@@ -64,7 +64,7 @@ public class RecoveryKeyResetPasswordController implements FxController {
});
task.setOnFailed(event -> {
LOG.error("Resetting password failed.", task.getException());
errorComponent.cause(task.getException()).window(window).returnToScene(recoverScene.get()).build().showErrorScene();
appWindows.showErrorWindow(task.getException(), window, recoverScene.get());
});
executor.submit(task);
}

View File

@@ -5,13 +5,13 @@ import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
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.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.fxapp.PrimaryStage;
import javax.inject.Provider;
import javafx.scene.Scene;
@@ -33,12 +33,12 @@ abstract class RemoveVaultModule {
@Provides
@RemoveVaultWindow
@RemoveVaultScoped
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
static Stage provideStage(StageFactory factory, @PrimaryStage Stage primaryStage, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("removeVault.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.initOwner(primaryStage);
return stage;
}

View File

@@ -1,9 +1,9 @@
package org.cryptomator.ui.traymenu;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.launcher.AppLifecycleListener;
import org.cryptomator.ui.launcher.FxApplicationStarter;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.fxapp.FxApplicationTerminator;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import javax.inject.Inject;
@@ -16,7 +16,6 @@ import java.awt.PopupMenu;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.EventObject;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.function.Consumer;
@@ -24,16 +23,18 @@ import java.util.function.Consumer;
class TrayMenuController {
private final ResourceBundle resourceBundle;
private final AppLifecycleListener appLifecycle;
private final FxApplicationStarter fxApplicationStarter;
private final VaultService vaultService;
private final FxApplicationWindows appWindows;
private final FxApplicationTerminator appTerminator;
private final ObservableList<Vault> vaults;
private final PopupMenu menu;
@Inject
TrayMenuController(ResourceBundle resourceBundle, AppLifecycleListener appLifecycle, FxApplicationStarter fxApplicationStarter, ObservableList<Vault> vaults) {
TrayMenuController(ResourceBundle resourceBundle, VaultService vaultService, FxApplicationWindows appWindows, FxApplicationTerminator appTerminator, ObservableList<Vault> vaults) {
this.resourceBundle = resourceBundle;
this.appLifecycle = appLifecycle;
this.fxApplicationStarter = fxApplicationStarter;
this.vaultService = vaultService;
this.appWindows = appWindows;
this.appTerminator = appTerminator;
this.vaults = vaults;
this.menu = new PopupMenu();
}
@@ -91,6 +92,8 @@ class TrayMenuController {
unlockItem.addActionListener(createActionListenerForVault(vault, this::unlockVault));
submenu.add(unlockItem);
} else if (vault.isUnlocked()) {
submenu.setLabel("* ".concat(submenu.getLabel()));
MenuItem lockItem = new MenuItem(resourceBundle.getString("traymenu.vault.lock"));
lockItem.addActionListener(createActionListenerForVault(vault, this::lockVault));
submenu.add(lockItem);
@@ -108,35 +111,31 @@ class TrayMenuController {
}
private void quitApplication(EventObject actionEvent) {
appLifecycle.quit();
appTerminator.terminate();
}
private void unlockVault(Vault vault) {
showMainAppAndThen(app -> app.startUnlockWorkflow(vault, Optional.empty()));
appWindows.startUnlockWorkflow(vault, null);
}
private void lockVault(Vault vault) {
showMainAppAndThen(app -> app.startLockWorkflow(vault, Optional.empty()));
appWindows.startLockWorkflow(vault, null);
}
private void lockAllVaults(ActionEvent actionEvent) {
showMainAppAndThen(app -> app.getVaultService().lockAll(vaults.filtered(Vault::isUnlocked), false));
vaultService.lockAll(vaults.filtered(Vault::isUnlocked), false);
}
private void revealVault(Vault vault) {
showMainAppAndThen(app -> app.getVaultService().reveal(vault));
vaultService.reveal(vault);
}
void showMainWindow(@SuppressWarnings("unused") ActionEvent actionEvent) {
showMainAppAndThen(app -> app.showMainWindow());
appWindows.showMainWindow();
}
private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) {
showMainAppAndThen(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
}
private void showMainAppAndThen(Consumer<FxApplication> action) {
fxApplicationStarter.get().thenAccept(action);
appWindows.showPreferencesWindow(SelectedPreferencesTab.ANY);
}
}

View File

@@ -7,11 +7,11 @@ package org.cryptomator.ui.unlock;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.vaults.Vault;
import javax.inject.Named;
import javafx.stage.Stage;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
@@ -29,16 +29,9 @@ public interface UnlockComponent {
return workflow;
}
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder vault(@UnlockWindow Vault vault);
@BindsInstance
Builder owner(@Named("unlockWindowOwner") Optional<Stage> owner);
UnlockComponent build();
@Subcomponent.Factory
interface Factory {
UnlockComponent create(@BindsInstance @UnlockWindow Vault vault, @BindsInstance @Named("unlockWindowOwner") @Nullable Stage owner);
}
}

View File

@@ -1,5 +1,6 @@
package org.cryptomator.ui.unlock;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
@@ -32,12 +33,24 @@ public class UnlockInvalidMountPointController implements FxController {
return vault.getVaultSettings().getCustomMountPath().orElse("AUTO");
}
public boolean getMustExist() {
MountPointRequirement requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!")).getMountPointRequirement();
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
return requirement == MountPointRequirement.EMPTY_MOUNT_POINT;
public boolean getNotExisting() {
return getMountPointRequirement() == MountPointRequirement.EMPTY_MOUNT_POINT;
}
}
public boolean getExisting() {
return getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT;
}
public boolean getDriveLetterOccupied() {
return getMountPointRequirement() == MountPointRequirement.UNUSED_ROOT_DIR;
}
private MountPointRequirement getMountPointRequirement() {
var requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!")).getMountPointRequirement();
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
assert requirement != MountPointRequirement.UNUSED_ROOT_DIR || SystemUtils.IS_OS_WINDOWS; //Not implemented anywhere, but on Windows
return requirement;
}
}

View File

@@ -14,6 +14,7 @@ import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.jetbrains.annotations.Nullable;
import javax.inject.Named;
import javax.inject.Provider;
@@ -21,7 +22,6 @@ import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module(subcomponents = {KeyLoadingComponent.class})
@@ -37,12 +37,12 @@ abstract class UnlockModule {
@Provides
@UnlockWindow
@UnlockScoped
static Stage provideStage(StageFactory factory, @UnlockWindow Vault vault, @Named("unlockWindowOwner") Optional<Stage> owner) {
static Stage provideStage(StageFactory factory, @UnlockWindow Vault vault, @Nullable @Named("unlockWindowOwner") Stage owner) {
Stage stage = factory.create();
stage.setTitle(vault.getDisplayName());
stage.setResizable(false);
if (owner.isPresent()) {
stage.initOwner(owner.get());
if (owner != null) {
stage.initOwner(owner);
stage.initModality(Modality.WINDOW_MODAL);
} else {
stage.initModality(Modality.APPLICATION_MODAL);

View File

@@ -2,16 +2,17 @@ package org.cryptomator.ui.unlock;
import com.google.common.base.Throwables;
import dagger.Lazy;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume.VolumeException;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -41,17 +42,17 @@ public class UnlockWorkflow extends Task<Boolean> {
private final VaultService vaultService;
private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene;
private final ErrorComponent.Builder errorComponent;
private final FxApplicationWindows appWindows;
private final KeyLoadingStrategy keyLoadingStrategy;
@Inject
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy) {
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy) {
this.window = window;
this.vault = vault;
this.vaultService = vaultService;
this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene;
this.errorComponent = errorComponent;
this.appWindows = appWindows;
this.keyLoadingStrategy = keyLoadingStrategy;
}
@@ -79,9 +80,10 @@ public class UnlockWorkflow extends Task<Boolean> {
}
private void handleInvalidMountPoint(InvalidMountPointException impExc) {
MountPointRequirement requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!", impExc)).getMountPointRequirement();
var requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!", impExc)).getMountPointRequirement();
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
assert requirement != MountPointRequirement.UNUSED_ROOT_DIR || SystemUtils.IS_OS_WINDOWS; //Not implemented anywhere, but on Windows
Throwable cause = impExc.getCause();
// TODO: apply https://openjdk.java.net/jeps/8213076 in future JDK versions
@@ -93,7 +95,11 @@ public class UnlockWorkflow extends Task<Boolean> {
}
showInvalidMountPointScene();
} else if (cause instanceof FileAlreadyExistsException) {
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
if (requirement == MountPointRequirement.UNUSED_ROOT_DIR) {
LOG.error("Unlock failed. Drive Letter already in use: {}", cause.getMessage());
} else {
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
}
showInvalidMountPointScene();
} else if (cause instanceof DirectoryNotEmptyException) {
LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage());
@@ -112,7 +118,7 @@ public class UnlockWorkflow extends Task<Boolean> {
private void handleGenericError(Throwable e) {
LOG.error("Unlock failed for technical reasons.", e);
errorComponent.cause(e).window(window).build().showErrorScene();
appWindows.showErrorWindow(e, window, null);
}
@Override

View File

@@ -11,9 +11,6 @@ import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
@@ -27,23 +24,21 @@ import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.Set;
/**
* TODO: if WebDav is selected on a windows system, custom mount directory is _not_ supported. This is currently not indicated/shown/etc in the ui
*/
@VaultOptionsScoped
public class MountOptionsController implements FxController {
private final Stage window;
private final Vault vault;
private final BooleanProperty osIsWindows = new SimpleBooleanProperty(SystemUtils.IS_OS_WINDOWS);
private final BooleanBinding webDavAndWindows;
private final VolumeImpl usedVolumeImpl;
private final WindowsDriveLetters windowsDriveLetters;
private final ResourceBundle resourceBundle;
public CheckBox readOnlyCheckbox;
public CheckBox customMountFlagsCheckbox;
public TextField mountFlags;
@@ -53,20 +48,13 @@ public class MountOptionsController implements FxController {
public RadioButton mountPointCustomDir;
public ChoiceBox<String> driveLetterSelection;
//FUSE + Windows -> Disable some (experimental) features for the user because they are unstable
//Use argument Dfuse.experimental="true" to override
private final BooleanBinding restrictToStableFuseOnWindows;
@Inject
MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, Settings settings, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, Environment environment) {
this.window = window;
this.vault = vault;
this.webDavAndWindows = settings.preferredVolumeImpl().isEqualTo(VolumeImpl.WEBDAV).and(osIsWindows);
this.usedVolumeImpl = settings.preferredVolumeImpl().get();
this.windowsDriveLetters = windowsDriveLetters;
this.resourceBundle = resourceBundle;
BooleanBinding isFuseOnWindows = settings.preferredVolumeImpl().isEqualTo(VolumeImpl.FUSE).and(osIsWindows);
this.restrictToStableFuseOnWindows = isFuseOnWindows.and(new SimpleBooleanProperty(!environment.useExperimentalFuse())); //Is FUSE on Win and is NOT experimental fuse enabled
}
@FXML
@@ -74,10 +62,11 @@ public class MountOptionsController implements FxController {
// readonly:
readOnlyCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().usesReadOnlyMode());
if (getRestrictToStableFuseOnWindows()) {
//TODO: support this feature on Windows
if (usedVolumeImpl == VolumeImpl.FUSE && isOsWindows()) {
readOnlyCheckbox.setSelected(false); // to prevent invalid states
readOnlyCheckbox.setDisable(true);
}
readOnlyCheckbox.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().or(restrictToStableFuseOnWindows));
// custom mount flags:
mountFlags.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().not());
@@ -95,9 +84,7 @@ public class MountOptionsController implements FxController {
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle));
driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get());
if (vault.getVaultSettings().useCustomMountPath().get()
&& vault.getVaultSettings().getCustomMountPath().isPresent()
&& !getRestrictToStableFuseOnWindows() /* to prevent invalid states */) {
if (vault.getVaultSettings().useCustomMountPath().get() && vault.getVaultSettings().getCustomMountPath().isPresent()) {
mountPoint.selectToggle(mountPointCustomDir);
} else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) {
mountPoint.selectToggle(mountPointWinDriveLetter);
@@ -136,8 +123,11 @@ public class MountOptionsController implements FxController {
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.mountPoint.directoryPickerTitle"));
try {
var initialDir = vault.getVaultSettings().getCustomMountPath().orElse(System.getProperty("user.home"));
directoryChooser.setInitialDirectory(Path.of(initialDir).toFile());
var initialDir = Path.of(vault.getVaultSettings().getCustomMountPath().orElse(System.getProperty("user.home")));
if(Files.exists(initialDir)) {
directoryChooser.setInitialDirectory(initialDir.toFile());
}
} catch (InvalidPathException e) {
// no-op
}
@@ -188,32 +178,28 @@ public class MountOptionsController implements FxController {
// Getter & Setter
public BooleanProperty osIsWindowsProperty() {
return osIsWindows;
public boolean isOsWindows() {
return SystemUtils.IS_OS_WINDOWS;
}
public boolean getOsIsWindows() {
return osIsWindows.get();
public boolean isCustomMountPointSupported() {
return !(usedVolumeImpl == VolumeImpl.WEBDAV && isOsWindows());
}
public BooleanBinding webDavAndWindowsProperty() {
return webDavAndWindows;
}
public boolean isWebDavAndWindows() {
return webDavAndWindows.get();
public boolean isReadOnlySupported() {
return !(usedVolumeImpl == VolumeImpl.FUSE && isOsWindows());
}
public StringProperty customMountPathProperty() {
return vault.getVaultSettings().customMountPath();
}
public boolean isCustomMountOptionsSupported() {
return usedVolumeImpl != VolumeImpl.WEBDAV;
}
public String getCustomMountPath() {
return vault.getVaultSettings().customMountPath().get();
}
public Boolean getRestrictToStableFuseOnWindows() {
return restrictToStableFuseOnWindows.get();
}
}

View File

@@ -13,7 +13,7 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.fxapp.PrimaryStage;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import javax.inject.Provider;
@@ -44,14 +44,14 @@ abstract class VaultOptionsModule {
@Provides
@VaultOptionsWindow
@VaultOptionsScoped
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, @VaultOptionsWindow Vault vault) {
static Stage provideStage(StageFactory factory, @PrimaryStage Stage primaryStage, @VaultOptionsWindow Vault vault) {
Stage stage = factory.create();
stage.setTitle(vault.getDisplayName());
stage.setResizable(true);
stage.setMinWidth(400);
stage.setMinHeight(300);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.initOwner(primaryStage);
return stage;
}

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