mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-17 18:21:26 +00:00
Merge remote-tracking branch 'origin/develop' into 1996-cli-version
# Conflicts: # src/main/java/org/cryptomator/launcher/Cryptomator.java
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
108
src/main/java/org/cryptomator/common/Passphrase.java
Normal file
108
src/main/java/org/cryptomator/common/Passphrase.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,6 +9,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
public @interface DefaultMountFlags {
|
||||
@interface DefaultMountFlags {
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
13
src/main/java/org/cryptomator/launcher/AppLaunchEvent.java
Normal file
13
src/main/java/org/cryptomator/launcher/AppLaunchEvent.java
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public enum FxmlFile {
|
||||
this.ressourcePathString = ressourcePathString;
|
||||
}
|
||||
|
||||
String getRessourcePathString() {
|
||||
public String getRessourcePathString() {
|
||||
return ressourcePathString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(", "));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
26
src/main/java/org/cryptomator/ui/fxapp/AutoUnlocker.java
Normal file
26
src/main/java/org/cryptomator/ui/fxapp/AutoUnlocker.java
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
155
src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java
Normal file
155
src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
14
src/main/java/org/cryptomator/ui/fxapp/PrimaryStage.java
Normal file
14
src/main/java/org/cryptomator/ui/fxapp/PrimaryStage.java
Normal 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 {
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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, ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 -----
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
public @interface MainWindow {
|
||||
@interface MainWindow {
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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) //
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -11,6 +11,11 @@ public enum SelectedPreferencesTab {
|
||||
*/
|
||||
GENERAL,
|
||||
|
||||
/**
|
||||
* Show interface tab
|
||||
*/
|
||||
INTERFACE,
|
||||
|
||||
/**
|
||||
* Show volume tab
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user