Merge branch 'release/1.5.0-beta2'

This commit is contained in:
Sebastian Stenzel
2020-01-09 17:15:55 +01:00
115 changed files with 3346 additions and 992 deletions

4
.github/stale.yml vendored
View File

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

4
.idea/compiler.xml generated
View File

@@ -31,10 +31,10 @@
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.1.0/kotlinx-metadata-jvm-0.1.0.jar" />
</processorPath>
<module name="commons" />
<module name="keychain" />
<module name="ui" />
<module name="launcher" />
<module name="commons" />
<module name="ui" />
</profile>
</annotationProcessing>
</component>

14
.idea/encodings.xml generated
View File

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

View File

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

View File

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

View File

@@ -28,13 +28,23 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@Module(subcomponents = {VaultComponent.class})
public abstract class CommonsModule {
private static final int NUM_SCHEDULER_THREADS = 4;
@Provides
@Singleton
@Named("licensePublicKey")
static String provideLicensePublicKey() {
// in PEM format without the dash-escaped begin/end lines
return "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB7NfnqiZbg2KTmoflmZ71PbXru7oW" //
+ "fmnV2yv3eDjlDfGruBrqz9TtXBZV/eYWt31xu1osIqaT12lKBvZ511aaAkIBeOEV" //
+ "gwcBIlJr6kUw7NKzeJt7r2rrsOyQoOG2nWc/Of/NBqA3mIZRHk5Aq1YupFdD26QE" //
+ "r0DzRyj4ixPIt38CQB8=";
}
@Provides
@Singleton
@Named("SemVer")
@@ -56,7 +66,7 @@ public abstract class CommonsModule {
@Provides
@Singleton
static ScheduledExecutorService provideScheduledExecutorService(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) {
static ScheduledExecutorService provideScheduledExecutorService(ShutdownHook shutdownHook) {
final AtomicInteger threadNumber = new AtomicInteger(1);
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> {
Thread t = new Thread(r);
@@ -64,7 +74,7 @@ public abstract class CommonsModule {
t.setDaemon(true);
return t;
});
shutdownTaskScheduler.accept(executorService::shutdown);
shutdownHook.runOnShutdown(executorService::shutdown);
return executorService;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,8 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.NodeOrientation;
@@ -35,6 +37,7 @@ public class Settings {
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT;
private static final String DEFAULT_LICENSE_KEY = "";
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
@@ -47,6 +50,7 @@ public class Settings {
private final ObjectProperty<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY);
private Consumer<Settings> saveCmd;
@@ -65,6 +69,7 @@ public class Settings {
preferredVolumeImpl.addListener(this::somethingChanged);
theme.addListener(this::somethingChanged);
userInterfaceOrientation.addListener(this::somethingChanged);
licenseKey.addListener(this::somethingChanged);
}
void setSaveCmd(Consumer<Settings> saveCmd) {
@@ -126,4 +131,8 @@ public class Settings {
public ObjectProperty<NodeOrientation> userInterfaceOrientation() {
return userInterfaceOrientation;
}
public StringProperty licenseKey() {
return licenseKey;
}
}

View File

@@ -38,6 +38,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
out.name("theme").value(value.theme().get().name());
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
out.name("licenseKey").value(value.licenseKey().get());
out.endObject();
}
@@ -90,6 +91,9 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "uiOrientation":
settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
break;
case "licenseKey":
settings.licenseKey().set(in.nextString());
break;
default:
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.0-beta1</version>
<version>1.5.0-beta2</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -23,26 +23,25 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- dependency versions -->
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>1.9.0-rc2</cryptomator.cryptofs.version>
<cryptomator.jni.version>2.2.1</cryptomator.jni.version>
<cryptomator.fuse.version>1.2.1</cryptomator.fuse.version>
<cryptomator.dokany.version>1.1.11</cryptomator.dokany.version>
<cryptomator.jni.version>2.2.2</cryptomator.jni.version>
<cryptomator.fuse.version>1.2.2</cryptomator.fuse.version>
<cryptomator.dokany.version>1.1.12</cryptomator.dokany.version>
<cryptomator.webdav.version>1.0.10</cryptomator.webdav.version>
<!-- 3rd party dependencies -->
<javafx.version>13.0.1</javafx.version>
<commons-lang3.version>3.9</commons-lang3.version>
<jwt.version>3.8.3</jwt.version>
<easybind.version>1.0.3</easybind.version>
<guava.version>28.1-jre</guava.version>
<dagger.version>2.25.2</dagger.version>
<gson.version>2.8.6</gson.version>
<slf4j.version>1.7.29</slf4j.version>
<logback.version>1.2.3</logback.version>
<!-- test dependencies -->
<junit.jupiter.version>5.5.2</junit.jupiter.version>
<mockito.version>3.1.0</mockito.version>
<hamcrest.version>2.2</hamcrest.version>
@@ -156,6 +155,13 @@
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!-- EasyBind -->
<dependency>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.0-beta1</version>
<version>1.5.0-beta2</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
@@ -37,7 +37,7 @@
<dependency>
<groupId>org.fxmisc.easybind</groupId>
<artifactId>easybind</artifactId>
</dependency>
</dependency>
<!-- Google -->
<dependency>
@@ -65,7 +65,7 @@
<dependency>
<groupId>com.nulab-inc</groupId>
<artifactId>zxcvbn</artifactId>
<version>1.2.7</version>
<version>1.3.0</version>
</dependency>
<!-- Logging -->

View File

@@ -15,14 +15,14 @@ import javax.inject.Inject;
import java.nio.file.Path;
@AddVaultWizardScoped
public class AddVaultFailureExisitingController implements FxController {
public class AddVaultFailureExistingController implements FxController {
private final Stage window;
private final Lazy<Scene> previousScene;
private final StringBinding vaultName;
@Inject
AddVaultFailureExisitingController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_EXISTING) Lazy<Scene> previousScene, ObjectProperty<Path> pathOfFailedVault){
AddVaultFailureExistingController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_EXISTING) Lazy<Scene> previousScene, ObjectProperty<Path> pathOfFailedVault){
this.window = window;
this.previousScene = previousScene;
this.vaultName = Bindings.createStringBinding(() -> pathOfFailedVault.get().getFileName().toString(),pathOfFailedVault);

View File

@@ -19,7 +19,10 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.recoverykey.RecoveryKeyDisplayController;
import javax.inject.Named;
import javax.inject.Provider;
@@ -31,6 +34,13 @@ import java.util.ResourceBundle;
@Module
public abstract class AddVaultModule {
@Provides
@AddVaultWizardScoped
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
@@ -150,8 +160,8 @@ public abstract class AddVaultModule {
@Binds
@IntoMap
@FxControllerKey(AddVaultFailureExisitingController.class)
abstract FxController bindAddVaultFailureExistingController(AddVaultFailureExisitingController controller);
@FxControllerKey(AddVaultFailureExistingController.class)
abstract FxController bindAddVaultFailureExistingController(AddVaultFailureExistingController controller);
@Binds
@IntoMap
@@ -168,13 +178,27 @@ public abstract class AddVaultModule {
@FxControllerKey(CreateNewVaultPasswordController.class)
abstract FxController bindCreateNewVaultPasswordController(CreateNewVaultPasswordController controller);
@Binds
@Provides
@IntoMap
@FxControllerKey(AddVaultSuccessController.class)
abstract FxController bindAddVaultSuccessController(AddVaultSuccessController controller);
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
}
@Binds
@IntoMap
@FxControllerKey(CreateNewVaultRecoveryKeyController.class)
abstract FxController bindCreateNewVaultRecoveryKeyController(CreateNewVaultRecoveryKeyController controller);
@Provides
@IntoMap
@FxControllerKey(RecoveryKeyDisplayController.class)
static FxController provideRecoveryKeyDisplayController(@AddVaultWizardWindow Stage window, @Named("vaultName") StringProperty vaultName, @Named("recoveryKey") StringProperty recoveryKey) {
return new RecoveryKeyDisplayController(window, vaultName.get(), recoveryKey.get());
}
@Binds
@IntoMap
@FxControllerKey(AddVaultSuccessController.class)
abstract FxController bindAddVaultSuccessController(AddVaultSuccessController controller);
}

View File

@@ -52,8 +52,10 @@ public class CreateNewVaultLocationController implements FxController {
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
public ToggleGroup predefinedLocationToggler;
public RadioButton iclouddriveRadioButton;
public RadioButton dropboxRadioButton;
public RadioButton gdriveRadioButton;
public RadioButton onedriveRadioButton;
public RadioButton customRadioButton;
@Inject
@@ -91,10 +93,14 @@ public class CreateNewVaultLocationController implements FxController {
}
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
if (dropboxRadioButton.equals(newValue)) {
if (iclouddriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getIclouddriveLocation().resolve(vaultName.get()));
} else if (dropboxRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getDropboxLocation().resolve(vaultName.get()));
} else if (gdriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
} else if (onedriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
} else if (customRadioButton.equals(newValue)) {
vaultPath.set(customVaultPath.resolve(vaultName.get()));
}

View File

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

View File

@@ -1,7 +1,6 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
@@ -10,33 +9,20 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
import javax.inject.Named;
public class CreateNewVaultRecoveryKeyController implements FxController {
private final Stage window;
private final Lazy<Scene> successScene;
private final StringProperty recoveryKeyProperty;
@Inject
CreateNewVaultRecoveryKeyController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, @Named("recoveryKey")StringProperty recoveryKey) {
CreateNewVaultRecoveryKeyController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene) {
this.window = window;
this.successScene = successScene;
this.recoveryKeyProperty = recoveryKey;
}
@FXML
public void next() {
window.setScene(successScene.get());
}
/* Getter/Setter */
public String getRecoveryKey() {
return recoveryKeyProperty.get();
}
public StringProperty recoveryKeyProperty() {
return recoveryKeyProperty;
}
}

View File

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

View File

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

View File

@@ -4,6 +4,8 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Modality;
@@ -14,15 +16,25 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import javax.inject.Named;
import javax.inject.Provider;
import java.nio.CharBuffer;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module
abstract class ChangePasswordModule {
@Provides
@ChangePasswordScoped
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
@Provides
@ChangePasswordWindow
@@ -58,5 +70,12 @@ abstract class ChangePasswordModule {
@IntoMap
@FxControllerKey(ChangePasswordController.class)
abstract FxController bindUnlockController(ChangePasswordController controller);
@Provides
@IntoMap
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
}
}

View File

@@ -18,9 +18,11 @@ public enum FxmlFile {
PREFERENCES("/fxml/preferences.fxml"), //
QUIT("/fxml/quit.fxml"), //
RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), //
RECOVERYKEY_DISPLAY("/fxml/recoverykey_display.fxml"), //
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
UNLOCK("/fxml/unlock.fxml"),
UNLOCK_GENERIC_ERROR("/fxml/unlock_generic_error.fxml"), //
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
VAULT_OPTIONS("/fxml/vault_options.fxml"), //
WRONGFILEALERT("/fxml/wrongfilealert.fxml");

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,190 @@
package org.cryptomator.ui.common;
import javafx.concurrent.Task;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
@FxApplicationScoped
public class VaultService {
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
private final ExecutorService executorService;
@Inject
public VaultService(ExecutorService executorService) {
this.executorService = executorService;
}
public void reveal(Vault vault) {
executorService.execute(createRevealTask(vault));
}
/**
* Creates but doesn't start a reveal task.
*
* @param vault The vault to reveal
*/
public Task<Vault> createRevealTask(Vault vault) {
Task<Vault> task = new RevealVaultTask(vault);
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Locks a vault in a background thread.
*
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
*/
public void lock(Vault vault, boolean forced) {
executorService.execute(createLockTask(vault, forced));
}
/**
* Creates but doesn't start a lock task.
*
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
*/
public Task<Vault> createLockTask(Vault vault, boolean forced) {
Task<Vault> task = new LockVaultTask(vault, forced);
task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to lock " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Locks all given vaults in a background thread.
*
* @param vaults The vaults to lock
* @param forced Whether to attempt a forced lock
*/
public void lockAll(Collection<Vault> vaults, boolean forced) {
executorService.execute(createLockAllTask(vaults, forced));
}
/**
* Creates but doesn't start a lock-all task.
*
* @param vaults The list of vaults to be locked
* @param forced Whether to attempt a forced lock
* @return Meta-Task that waits until all vaults are locked or fails after the first failure of a subtask
*/
public Task<Collection<Vault>> createLockAllTask(Collection<Vault> vaults, boolean forced) {
List<Task<Vault>> lockTasks = vaults.stream().map(v -> new LockVaultTask(v, forced)).collect(Collectors.toUnmodifiableList());
lockTasks.forEach(executorService::execute);
Task<Collection<Vault>> task = new WaitForTasksTask(lockTasks);
String vaultNames = vaults.stream().map(Vault::getDisplayableName).collect(Collectors.joining(", "));
task.setOnSucceeded(evt -> LOG.info("Locked {}", vaultNames));
task.setOnFailed(evt -> LOG.error("Failed to lock vaults " + vaultNames, evt.getSource().getException()));
return task;
}
private static class RevealVaultTask extends Task<Vault> {
private final Vault vault;
/**
* @param vault The vault to lock
*/
public RevealVaultTask(Vault vault) {
this.vault = vault;
}
@Override
protected Vault call() throws Volume.VolumeException {
vault.reveal();
return vault;
}
}
/**
* A task that waits for completion of multiple other tasks
*/
private static class WaitForTasksTask extends Task<Collection<Vault>> {
private final Collection<Task<Vault>> startedTasks;
public WaitForTasksTask(Collection<Task<Vault>> tasks) {
this.startedTasks = List.copyOf(tasks);
}
@Override
protected Collection<Vault> call() throws Exception {
Iterator<Task<Vault>> remainingTasks = startedTasks.iterator();
Collection<Vault> completed = new ArrayList<>();
try {
// wait for all tasks:
while (remainingTasks.hasNext()) {
Vault lockedVault = remainingTasks.next().get();
completed.add(lockedVault);
}
} catch (ExecutionException e) {
// cancel all remaining:
while (remainingTasks.hasNext()) {
remainingTasks.next().cancel(true);
}
throw e;
}
return List.copyOf(completed);
}
}
/**
* A task that locks a vault
*/
private static class LockVaultTask extends Task<Vault> {
private final Vault vault;
private final boolean forced;
/**
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
*/
public LockVaultTask(Vault vault, boolean forced) {
this.vault = vault;
this.forced = forced;
}
@Override
protected Vault call() throws Volume.VolumeException {
vault.lock(forced);
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.LOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.UNLOCKED);
}
}
}

View File

@@ -9,23 +9,29 @@ public enum FontAwesome5Icon {
CHECK("\uF00C"), //
COG("\uF013"), //
COGS("\uF085"), //
COPY("\uF0C5"), //
EXCLAMATION("\uF12A"),
EXCLAMATION_CIRCLE("\uF06A"), //
EXCLAMATION_TRIANGLE("\uF071"), //
EYE("\uF06E"), //
EYE_SLASH("\uF070"), //
FILE_IMPORT("\uF56F"), //
FOLDER_OPEN("\uF07C"), //
HAND_HOLDING_HEART("\uF4BE"), //
HEART("\uF004"), //
HDD("\uF0A0"), //
KEY("\uF084"), //
LINK("\uF0C1"), //
LOCK_ALT("\uF30D"), //
LOCK_OPEN_ALT("\uF3C2"), //
MINUS("\uF068"), //
PLUS("\uF067"), //
PRINT("\uF02F"), //
QUESTION("\uF128"), //
SPARKLES("\uF890"), //
SPINNER("\uF110"), //
SYNC("\uF021"), //
TIMES("\uF00D"), //
USER_CROWN("\uF6A4"), //
WRENCH("\uF0AD"), //
;

View File

@@ -89,7 +89,7 @@ public class NiceSecurePasswordField extends StackPane {
}
public void swipe() {
passwordField.swipe();;
passwordField.swipe();
}
public void selectAll() {

View File

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

View File

@@ -5,8 +5,10 @@
*******************************************************************************/
package org.cryptomator.ui.fxapp;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image;
@@ -44,6 +46,9 @@ abstract class FxApplicationModule {
return Optional.empty();
}
}
@Binds
abstract Application bindApplication(FxApplication application);
@Provides
static MainWindowComponent provideMainWindowComponent(MainWindowComponent.Builder builder) {

View File

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

View File

@@ -68,6 +68,16 @@ abstract class MainWindowModule {
@FxControllerKey(MainWindowController.class)
abstract FxController bindMainWindowController(MainWindowController controller);
@Binds
@IntoMap
@FxControllerKey(MainWindowTitleController.class)
abstract FxController bindMainWindowTitleController(MainWindowTitleController controller);
@Binds
@IntoMap
@FxControllerKey(ResizeController.class)
abstract FxController bindResizeController(ResizeController controller);
@Binds
@IntoMap
@FxControllerKey(VaultListController.class)

View File

@@ -17,22 +17,22 @@ public class MainWindowSceneFactory extends DefaultSceneFactory {
protected static final KeyCodeCombination SHORTCUT_N = new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN);
private final Lazy<MainWindowController> mainWindowController;
private final Lazy<MainWindowTitleController> mainWindowTitleController;
private final Lazy<VaultListController> vaultListController;
@Inject
public MainWindowSceneFactory(Settings settings, Lazy<MainWindowController> mainWindowController, Lazy<VaultListController> vaultListController) {
public MainWindowSceneFactory(Settings settings, Lazy<MainWindowTitleController> mainWindowTitleController, Lazy<VaultListController> vaultListController) {
super(settings);
this.mainWindowController = mainWindowController;
this.mainWindowTitleController = mainWindowTitleController;
this.vaultListController = vaultListController;
}
@Override
protected void setupDefaultAccelerators(Scene scene, Stage stage) {
if (SystemUtils.IS_OS_WINDOWS) {
scene.getAccelerators().put(ALT_F4, mainWindowController.get()::close);
scene.getAccelerators().put(ALT_F4, mainWindowTitleController.get()::close);
} else {
scene.getAccelerators().put(SHORTCUT_W, mainWindowController.get()::close);
scene.getAccelerators().put(SHORTCUT_W, mainWindowTitleController.get()::close);
}
scene.getAccelerators().put(SHORTCUT_N, vaultListController.get()::didClickAddVault);
}

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,18 @@
package org.cryptomator.ui.mainwindow;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
@@ -23,7 +27,6 @@ public class VaultListController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(VaultListController.class);
private final Stage window;
private final ObservableList<Vault> vaults;
private final ObjectProperty<Vault> selectedVault;
private final VaultListCellFactory cellFactory;
@@ -34,8 +37,7 @@ public class VaultListController implements FxController {
public ListView<Vault> vaultList;
@Inject
VaultListController(@MainWindow Stage window, ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) {
this.window = window;
VaultListController(ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) {
this.vaults = vaults;
this.selectedVault = selectedVault;
this.cellFactory = cellFactory;
@@ -43,6 +45,7 @@ public class VaultListController implements FxController {
this.removeVault = removeVault;
this.noVaultSelected = selectedVault.isNull();
this.emptyVaultList = Bindings.isEmpty(vaults);
selectedVault.addListener(this::selectedVaultDidChange);
}
public void initialize() {
@@ -59,6 +62,23 @@ public class VaultListController implements FxController {
});
}
private void selectedVaultDidChange(@SuppressWarnings("unused") ObservableValue<? extends Vault> observableValue, @SuppressWarnings("unused") Vault oldValue, Vault newValue) {
VaultState reportedState = newValue.getState();
switch (reportedState) {
case LOCKED:
case NEEDS_MIGRATION:
case MISSING:
VaultState determinedState = VaultListManager.determineVaultState(newValue.getPath());
newValue.setState(determinedState);
break;
case ERROR:
case UNLOCKED:
case PROCESSING:
default:
// no-op
}
}
@FXML
public void didClickAddVault() {
addVaultWizard.build().showAddVaultWizard();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,8 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
@@ -20,9 +22,15 @@ import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module
@Module(includes = {AutoStartModule.class})
abstract class PreferencesModule {
@Provides
@PreferencesScoped
static ObjectProperty<SelectedPreferencesTab> provideSelectedTabProperty() {
return new SimpleObjectProperty<>(SelectedPreferencesTab.ANY);
}
@Provides
@PreferencesWindow
@PreferencesScoped
@@ -70,4 +78,9 @@ abstract class PreferencesModule {
@FxControllerKey(VolumePreferencesController.class)
abstract FxController bindVolumePreferencesController(VolumePreferencesController controller);
@Binds
@IntoMap
@FxControllerKey(DonationKeyPreferencesController.class)
abstract FxController bindDonationKeyPreferencesController(DonationKeyPreferencesController controller);
}

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ public class RecoveryKeyCreationController implements FxController {
public NiceSecurePasswordField passwordField;
@Inject
public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_DISPLAY) Lazy<Scene> successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey) {
public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy<Scene> successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey) {
this.window = window;
this.successScene = successScene;
this.vault = vault;

View File

@@ -1,26 +1,73 @@
package org.cryptomator.ui.recoverykey;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.print.PageLayout;
import javafx.print.Printer;
import javafx.print.PrinterJob;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.text.Font;
import javafx.scene.text.FontSmoothingType;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
@RecoveryKeyScoped
public class RecoveryKeyDisplayController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyDisplayController.class);
private final Stage window;
private final Vault vault;
private final StringProperty recoveryKeyProperty;
@Inject
public RecoveryKeyDisplayController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey) {
private final String vaultName;
private final String recoveryKey;
public RecoveryKeyDisplayController(Stage window, String vaultName, String recoveryKey) {
this.window = window;
this.vault = vault;
this.recoveryKeyProperty = recoveryKey;
this.vaultName = vaultName;
this.recoveryKey = recoveryKey;
}
@FXML
public void printRecoveryKey() {
// TODO localize
PrinterJob job = PrinterJob.createPrinterJob();
if (job != null && job.showPrintDialog(window)) {
PageLayout pageLayout = job.getJobSettings().getPageLayout();
Text heading = new Text("Cryptomator Recovery Key\n" + vaultName + "\n");
heading.setFont(Font.font("serif", FontWeight.BOLD, 20));
heading.setFontSmoothingType(FontSmoothingType.LCD);
Text key = new Text(recoveryKey);
key.setFont(Font.font("serif", FontWeight.NORMAL, 16));
key.setFontSmoothingType(FontSmoothingType.GRAY);
TextFlow textFlow = new TextFlow();
textFlow.setPrefSize(pageLayout.getPrintableWidth(), pageLayout.getPrintableHeight());
textFlow.getChildren().addAll(heading, key);
textFlow.setLineSpacing(6);
if (job.printPage(textFlow)) {
LOG.info("Recovery key printed.");
job.endJob();
} else {
LOG.warn("Printing recovery key failed.");
}
} else {
LOG.info("Printing recovery key canceled by user.");
}
}
@FXML
public void copyRecoveryKey() {
ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(recoveryKey);
Clipboard.getSystemClipboard().setContent(clipboardContent);
LOG.info("Recovery key copied to clipboard.");
}
@FXML
@@ -30,15 +77,15 @@ public class RecoveryKeyDisplayController implements FxController {
/* Getter/Setter */
public Vault getVault() {
return vault;
}
public ReadOnlyStringProperty recoveryKeyProperty() {
return recoveryKeyProperty;
public boolean isPrinterSupported() {
return Printer.getDefaultPrinter() != null;
}
public String getRecoveryKey() {
return recoveryKeyProperty.get();
return recoveryKey;
}
public String getVaultName() {
return vaultName;
}
}

View File

@@ -10,6 +10,7 @@ import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
@@ -63,10 +64,10 @@ abstract class RecoveryKeyModule {
}
@Provides
@FxmlScene(FxmlFile.RECOVERYKEY_DISPLAY)
@FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS)
@RecoveryKeyScoped
static Scene provideRecoveryKeyDisplayScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_display.fxml");
static Scene provideRecoveryKeySuccessScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_success.fxml");
}
// ------------------
@@ -76,9 +77,16 @@ abstract class RecoveryKeyModule {
@FxControllerKey(RecoveryKeyCreationController.class)
abstract FxController bindRecoveryKeyCreationController(RecoveryKeyCreationController controller);
@Binds
@Provides
@IntoMap
@FxControllerKey(RecoveryKeyDisplayController.class)
abstract FxController bindRecoveryKeyDisplayController(RecoveryKeyDisplayController controller);
static FxController provideRecoveryKeyDisplayController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey) {
return new RecoveryKeyDisplayController(window, vault.getDisplayableName(), recoveryKey.get());
}
@Binds
@IntoMap
@FxControllerKey(RecoveryKeySuccessController.class)
abstract FxController bindRecoveryKeySuccessController(RecoveryKeySuccessController controller);
}

View File

@@ -0,0 +1,24 @@
package org.cryptomator.ui.recoverykey;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
@RecoveryKeyScoped
public class RecoveryKeySuccessController implements FxController {
private final Stage window;
@Inject
public RecoveryKeySuccessController(@RecoveryKeyWindow Stage window) {
this.window = window;
}
@FXML
public void close() {
window.close();
}
}

View File

@@ -3,11 +3,14 @@ package org.cryptomator.ui.traymenu;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.collections.ObservableList;
import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.launcher.FxApplicationStarter;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,17 +40,17 @@ class TrayMenuController {
private final ResourceBundle resourceBundle;
private final FxApplicationStarter fxApplicationStarter;
private final CountDownLatch shutdownLatch;
private final Settings settings;
private final ShutdownHook shutdownHook;
private final ObservableList<Vault> vaults;
private final PopupMenu menu;
private final AtomicBoolean allowSuddenTermination;
@Inject
TrayMenuController(ResourceBundle resourceBundle, FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, Settings settings, ObservableList<Vault> vaults) {
TrayMenuController(ResourceBundle resourceBundle, FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, ObservableList<Vault> vaults) {
this.resourceBundle = resourceBundle;
this.fxApplicationStarter = fxApplicationStarter;
this.shutdownLatch = shutdownLatch;
this.settings = settings;
this.shutdownHook = shutdownHook;
this.vaults = vaults;
this.menu = new PopupMenu();
this.allowSuddenTermination = new AtomicBoolean(true);
@@ -71,6 +74,7 @@ class TrayMenuController {
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
Desktop.getDesktop().setQuitHandler(this::handleQuitRequest);
}
shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults);
// allow sudden termination
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
@@ -112,6 +116,11 @@ class TrayMenuController {
}
menu.addSeparator();
MenuItem lockAllItem = new MenuItem(resourceBundle.getString("traymenu.lockAllVaults"));
lockAllItem.addActionListener(this::lockAllVaults);
lockAllItem.setEnabled(!vaults.filtered(Vault::isUnlocked).isEmpty());
menu.add(lockAllItem);
MenuItem quitApplicationItem = new MenuItem(resourceBundle.getString("traymenu.quitApplication"));
quitApplicationItem.addActionListener(this::quitApplication);
menu.add(quitApplicationItem);
@@ -126,11 +135,11 @@ class TrayMenuController {
submenu.add(unlockItem);
} else if (vault.isUnlocked()) {
MenuItem lockItem = new MenuItem(resourceBundle.getString("traymenu.vault.lock"));
lockItem.setEnabled(false); // TODO add action listener
lockItem.addActionListener(createActionListenerForVault(vault, this::lockVault));
submenu.add(lockItem);
MenuItem revealItem = new MenuItem(resourceBundle.getString("traymenu.vault.reveal"));
revealItem.setEnabled(false); // TODO add action listener
revealItem.addActionListener(createActionListenerForVault(vault, this::revealVault));
submenu.add(revealItem);
}
@@ -145,12 +154,24 @@ class TrayMenuController {
fxApplicationStarter.get(true).thenAccept(app -> app.showUnlockWindow(vault));
}
private void lockVault(Vault vault) {
fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().lock(vault, false));
}
private void lockAllVaults(ActionEvent actionEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().lockAll(vaults.filtered(Vault::isUnlocked), false));
}
private void revealVault(Vault vault) {
fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().reveal(vault));
}
void showMainWindow(@SuppressWarnings("unused") ActionEvent actionEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.showMainWindow());
}
private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) {
fxApplicationStarter.get(true).thenAccept(FxApplication::showPreferencesWindow);
fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
}
private void handleQuitRequest(EventObject e, QuitResponse response) {
@@ -174,4 +195,16 @@ class TrayMenuController {
}
});
}
private void forceUnmountRemainingVaults() {
for (Vault vault : vaults) {
if (vault.isUnlocked()) {
try {
vault.lock(true);
} catch (Volume.VolumeException e) {
LOG.error("Failed to unmount vault " + vault.getPath(), e);
}
}
}
}
}

View File

@@ -7,6 +7,7 @@ import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.WritableValue;
@@ -32,6 +33,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.NotDirectoryException;
import java.util.Arrays;
@@ -49,19 +51,25 @@ public class UnlockController implements FxController {
private final ObjectBinding<ContentDisplay> unlockButtonState;
private final Optional<KeychainAccess> keychainAccess;
private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene;
private final Lazy<Scene> genericErrorScene;
private final ObjectProperty<Exception> genericErrorCause;
private final ForgetPasswordComponent.Builder forgetPassword;
private final BooleanProperty unlockButtonDisabled;
public NiceSecurePasswordField passwordField;
public CheckBox savePassword;
@Inject
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional<KeychainAccess> keychainAccess, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, ForgetPasswordComponent.Builder forgetPassword) {
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional<KeychainAccess> keychainAccess, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, @FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR) Lazy<Scene> genericErrorScene, @Named("genericErrorCause") ObjectProperty<Exception> genericErrorCause, ForgetPasswordComponent.Builder forgetPassword) {
this.window = window;
this.vault = vault;
this.executor = executor;
this.unlockButtonState = Bindings.createObjectBinding(this::getUnlockButtonState, vault.stateProperty());
this.keychainAccess = keychainAccess;
this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene;
this.genericErrorScene = genericErrorScene;
this.genericErrorCause = genericErrorCause;
this.forgetPassword = forgetPassword;
this.unlockButtonDisabled = new SimpleBooleanProperty();
}
@@ -100,17 +108,16 @@ public class UnlockController implements FxController {
shakeWindow();
passwordField.selectAll();
passwordField.requestFocus();
}).onError(UnsupportedVaultFormatException.class, e -> {
// TODO
}).onError(NotDirectoryException.class, e -> {
LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage());
// TODO
window.setScene(invalidMountPointScene.get());
}).onError(DirectoryNotEmptyException.class, e -> {
LOG.error("Unlock failed. Mount point not empty: {}", e.getMessage());
// TODO
window.setScene(invalidMountPointScene.get());
}).onError(Exception.class, e -> { // including RuntimeExceptions
LOG.error("Unlock failed for technical reasons.", e);
// TODO
genericErrorCause.set(e);
window.setScene(genericErrorScene.get());
}).andFinally(() -> {
if (!vault.isUnlocked()) {
vault.setState(VaultState.LOCKED);

View File

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

View File

@@ -0,0 +1,39 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
@UnlockScoped
public class UnlockInvalidMountPointController implements FxController {
private final Stage window;
private final Lazy<Scene> unlockScene;
private final Vault vault;
@Inject
UnlockInvalidMountPointController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @UnlockWindow Vault vault) {
this.window = window;
this.unlockScene = unlockScene;
this.vault = vault;
}
@FXML
public void back() {
window.setScene(unlockScene.get());
}
/* Getter/Setter */
public String getMountPoint() {
return vault.getVaultSettings().getIndividualMountPath().orElse("AUTO");
}
}

View File

@@ -4,6 +4,8 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Modality;
@@ -14,6 +16,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StackTraceController;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import javax.inject.Named;
@@ -44,6 +47,13 @@ abstract class UnlockModule {
return stage;
}
@Provides
@Named("genericErrorCause")
@UnlockScoped
static ObjectProperty<Exception> provideGenericErrorCause() {
return new SimpleObjectProperty<>();
}
@Provides
@FxmlScene(FxmlFile.UNLOCK)
@UnlockScoped
@@ -58,6 +68,21 @@ abstract class UnlockModule {
return fxmlLoaders.createScene("/fxml/unlock_success.fxml");
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT)
@UnlockScoped
static Scene provideInvalidMountPointScene(@UnlockWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/unlock_invalid_mount_point.fxml");
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR)
@UnlockScoped
static Scene provideGenericErrorScene(@UnlockWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/unlock_generic_error.fxml");
}
// ------------------
@@ -71,5 +96,22 @@ abstract class UnlockModule {
@FxControllerKey(UnlockSuccessController.class)
abstract FxController bindUnlockSuccessController(UnlockSuccessController controller);
@Binds
@IntoMap
@FxControllerKey(UnlockInvalidMountPointController.class)
abstract FxController bindUnlockInvalidMountPointController(UnlockInvalidMountPointController controller);
@Binds
@IntoMap
@FxControllerKey(UnlockGenericErrorController.class)
abstract FxController bindUnlockGenericErrorController(UnlockGenericErrorController controller);
@Provides
@IntoMap
@FxControllerKey(StackTraceController.class)
static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty<Exception> errorCause) {
return new StackTraceController(errorCause.get());
}
}

View File

@@ -5,13 +5,13 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.Tasks;
import org.cryptomator.ui.common.VaultService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,14 +26,16 @@ public class UnlockSuccessController implements FxController {
private final Stage window;
private final Vault vault;
private final ExecutorService executor;
private final VaultService vaultService;
private final ObjectProperty<ContentDisplay> revealButtonState;
private final BooleanProperty revealButtonDisabled;
@Inject
public UnlockSuccessController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor) {
public UnlockSuccessController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, VaultService vaultService) {
this.window = window;
this.vault = vault;
this.executor = executor;
this.vaultService = vaultService;
this.revealButtonState = new SimpleObjectProperty<>(ContentDisplay.TEXT_ONLY);
this.revealButtonDisabled = new SimpleBooleanProperty();
}
@@ -49,17 +51,19 @@ public class UnlockSuccessController implements FxController {
LOG.trace("UnlockSuccessController.revealAndClose()");
revealButtonState.set(ContentDisplay.LEFT);
revealButtonDisabled.set(true);
Tasks.create(() -> {
vault.reveal();
}).onSuccess(() -> {
window.close();
}).onError(InvalidPassphraseException.class, e -> {
// TODO
LOG.warn("Reveal failed.", e);
}).andFinally(() -> {
Task<Vault> revealTask = vaultService.createRevealTask(vault);
revealTask.setOnSucceeded(evt -> {
revealButtonState.set(ContentDisplay.TEXT_ONLY);
revealButtonDisabled.set(false);
}).runOnce(executor);
window.close();
});
revealTask.setOnFailed(evt -> {
LOG.warn("Reveal failed.", revealTask.getException());
revealButtonState.set(ContentDisplay.TEXT_ONLY);
revealButtonDisabled.set(false);
});
executor.execute(revealTask);
}
/* Getter/Setter */

View File

@@ -84,12 +84,7 @@ public class MountOptionsController implements FxController {
driveLetterSelection.getItems().addAll(windowsDriveLetters.getAllDriveLetters());
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters));
driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get());
vault.getVaultSettings().usesIndividualMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
vault.getVaultSettings().winDriveLetter().bind( //
Bindings.when(mountPoint.selectedToggleProperty().isEqualTo(mountPointWinDriveLetter)) //
.then(driveLetterSelection.getSelectionModel().selectedItemProperty()) //
.otherwise((String) null) //
);
if (vault.getVaultSettings().usesIndividualMountPath().get()) {
mountPoint.selectToggle(mountPointCustomDir);
} else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) {
@@ -97,6 +92,13 @@ public class MountOptionsController implements FxController {
} else {
mountPoint.selectToggle(mountPointAuto);
}
vault.getVaultSettings().usesIndividualMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
vault.getVaultSettings().winDriveLetter().bind( //
Bindings.when(mountPoint.selectedToggleProperty().isEqualTo(mountPointWinDriveLetter)) //
.then(driveLetterSelection.getSelectionModel().selectedItemProperty()) //
.otherwise((String) null) //
);
}
@FXML
@@ -116,7 +118,7 @@ public class MountOptionsController implements FxController {
@FXML
private void chooseCustomMountPoint() {
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.winDirChooser"));
directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.mountPoint.directoryPickerTitle"));
try {
directoryChooser.setInitialDirectory(Path.of(System.getProperty("user.home")).toFile());
} catch (Exception e) {

View File

@@ -78,6 +78,7 @@
SCROLL_BAR_THUMB_NORMAL: GRAY_3;
SCROLL_BAR_THUMB_HOVER: GRAY_4;
INDICATOR_BG: RED_5;
DRAG_N_DROP_INDICATOR_BG: GRAY_3;
PROGRESS_INDICATOR_BEGIN: GRAY_7;
PROGRESS_INDICATOR_END: GRAY_5;
PROGRESS_BAR_BG: GRAY_2;
@@ -116,6 +117,10 @@
-fx-font-size: 0.8em;
}
.label-extra-small {
-fx-font-size: 0.64em;
}
.text-flow > * {
-fx-fill: TEXT_FILL;
}
@@ -179,20 +184,6 @@
-fx-fill: CONTROL_WHITE_BG_ARMED;
}
.main-window .resizer {
-fx-background-color: linear-gradient(to bottom right,
transparent 50%,
CONTROL_BORDER_NORMAL 51%,
CONTROL_BORDER_NORMAL 60%,
transparent 61%,
transparent 70%,
CONTROL_BORDER_NORMAL 71%,
CONTROL_BORDER_NORMAL 80%,
transparent 81%
);
-fx-cursor: nw_resize;
}
.main-window .update-indicator {
-fx-background-color: PRIMARY_BG, white, INDICATOR_BG;
-fx-background-insets: 0, 1px, 2px;
@@ -201,6 +192,16 @@
-fx-translate-y: 1px;
}
.main-window .drag-n-drop-indicator {
-fx-border-color: DRAG_N_DROP_INDICATOR_BG;
-fx-border-width: 3px;
}
.main-window .drag-n-drop-indicator .drag-n-drop-header {
-fx-background-color: DRAG_N_DROP_INDICATOR_BG;
-fx-padding: 3px;
}
/*******************************************************************************
* *
* TabPane *
@@ -212,34 +213,36 @@
}
.tab-pane > .tab-header-area {
-fx-padding: 6px 12px 0 12px;
-fx-background-color: CONTROL_BORDER_FOCUSED, MAIN_BG;
-fx-background-insets: 0, 0 0 1px 0;
-fx-padding: 0 12px;
-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
-fx-background-insets: 0, 0 0 3px 0;
}
.tab-pane > .tab-header-area > .headers-region > .tab {
-fx-background-color: CONTROL_BORDER_NORMAL, MAIN_BG;
-fx-background-insets: 0 0 1px 0, 1px;
-fx-background-radius: 4px 4px 0 0;
-fx-padding: 0.2em 1em 0.2em 1em;
.tab-pane .tab {
-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
-fx-background-insets: 0, 0 0 3px 0;
-fx-padding: 6px 12px;
}
.tab-pane > .tab-header-area > .headers-region > .tab:selected {
-fx-background-color: CONTROL_BORDER_FOCUSED, MAIN_BG;
-fx-background-insets: 0, 1px 1px 0 1px;
.tab-pane .tab:selected {
-fx-background-color: PRIMARY_BG, CONTROL_PRIMARY_LIGHT_BG_NORMAL;
}
.tab-pane > .tab-header-area > .headers-region > .tab > .tab-container > .tab-label {
-fx-text-fill: TEXT_FILL;
.tab-pane .tab .tab-label {
-fx-text-fill: SECONDARY_BG;
-fx-alignment: CENTER;
}
.tab-pane > .tab-header-area > .headers-region > .tab .glyph-icon {
.tab-pane .tab .glyph-icon {
-fx-fill: SECONDARY_BG;
}
.tab-pane > .tab-header-area > .headers-region > .tab:selected .glyph-icon {
-fx-fill: TEXT_FILL;
.tab-pane .tab:selected .glyph-icon {
-fx-fill: PRIMARY_BG;
}
.tab-pane .tab:selected .tab-label {
-fx-text-fill: TEXT_FILL_PRIMARY;
}
/*******************************************************************************
@@ -672,7 +675,6 @@
******************************************************************************/
.choice-box {
-fx-text-fill: TEXT_FILL;
-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
-fx-background-insets: 0, 1px;
-fx-background-radius: 4px;
@@ -683,6 +685,18 @@
-fx-background-color: CONTROL_BORDER_FOCUSED, CONTROL_BG_NORMAL;
}
.choice-box:disabled {
-fx-background-color: CONTROL_BORDER_DISABLED, CONTROL_BG_DISABLED;
}
.choice-box > .label {
-fx-text-fill: TEXT_FILL;
}
.choice-box:disabled > .label {
-fx-text-fill: TEXT_FILL_SECONDARY;
}
.choice-box > .open-button {
-fx-padding: 0 0 0 0.3em;
}
@@ -694,6 +708,10 @@
-fx-shape: "M 0 0 h 7 l -3.5 4 z";
}
.choice-box:disabled > .open-button > .arrow {
-fx-background-color: transparent, TEXT_FILL_SECONDARY;
}
.choice-box .context-menu {
-fx-translate-x: -1.4em;
}

View File

@@ -78,6 +78,7 @@
SCROLL_BAR_THUMB_NORMAL: GRAY_7;
SCROLL_BAR_THUMB_HOVER: GRAY_6;
INDICATOR_BG: RED_5;
DRAG_N_DROP_INDICATOR_BG: GRAY_5;
PROGRESS_INDICATOR_BEGIN: GRAY_2;
PROGRESS_INDICATOR_END: GRAY_4;
PROGRESS_BAR_BG: GRAY_8;
@@ -116,6 +117,10 @@
-fx-font-size: 0.8em;
}
.label-extra-small {
-fx-font-size: 0.64em;
}
.text-flow > * {
-fx-fill: TEXT_FILL;
}
@@ -179,20 +184,6 @@
-fx-fill: CONTROL_WHITE_BG_ARMED;
}
.main-window .resizer {
-fx-background-color: linear-gradient(to bottom right,
transparent 50%,
CONTROL_BORDER_NORMAL 51%,
CONTROL_BORDER_NORMAL 60%,
transparent 61%,
transparent 70%,
CONTROL_BORDER_NORMAL 71%,
CONTROL_BORDER_NORMAL 80%,
transparent 81%
);
-fx-cursor: nw_resize;
}
.main-window .update-indicator {
-fx-background-color: PRIMARY_BG, white, INDICATOR_BG;
-fx-background-insets: 0, 1px, 2px;
@@ -201,6 +192,16 @@
-fx-translate-y: 1px;
}
.main-window .drag-n-drop-indicator {
-fx-border-color: DRAG_N_DROP_INDICATOR_BG;
-fx-border-width: 3px;
}
.main-window .drag-n-drop-indicator .drag-n-drop-header {
-fx-background-color: DRAG_N_DROP_INDICATOR_BG;
-fx-padding: 3px;
}
/*******************************************************************************
* *
* TabPane *
@@ -212,34 +213,36 @@
}
.tab-pane > .tab-header-area {
-fx-padding: 6px 12px 0 12px;
-fx-background-color: CONTROL_BORDER_FOCUSED, MAIN_BG;
-fx-background-insets: 0, 0 0 1px 0;
-fx-padding: 0 12px;
-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
-fx-background-insets: 0, 0 0 3px 0;
}
.tab-pane > .tab-header-area > .headers-region > .tab {
-fx-background-color: CONTROL_BORDER_NORMAL, MAIN_BG;
-fx-background-insets: 0 0 1px 0, 1px;
-fx-background-radius: 4px 4px 0 0;
-fx-padding: 0.2em 1em 0.2em 1em;
.tab-pane .tab {
-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
-fx-background-insets: 0, 0 0 3px 0;
-fx-padding: 6px 12px;
}
.tab-pane > .tab-header-area > .headers-region > .tab:selected {
-fx-background-color: CONTROL_BORDER_FOCUSED, MAIN_BG;
-fx-background-insets: 0, 1px 1px 0 1px;
.tab-pane .tab:selected {
-fx-background-color: PRIMARY_BG, CONTROL_PRIMARY_LIGHT_BG_NORMAL;
}
.tab-pane > .tab-header-area > .headers-region > .tab > .tab-container > .tab-label {
-fx-text-fill: TEXT_FILL;
.tab-pane .tab .tab-label {
-fx-text-fill: SECONDARY_BG;
-fx-alignment: CENTER;
}
.tab-pane > .tab-header-area > .headers-region > .tab .glyph-icon {
.tab-pane .tab .glyph-icon {
-fx-fill: SECONDARY_BG;
}
.tab-pane > .tab-header-area > .headers-region > .tab:selected .glyph-icon {
-fx-fill: TEXT_FILL;
.tab-pane .tab:selected .glyph-icon {
-fx-fill: PRIMARY_BG;
}
.tab-pane .tab:selected .tab-label {
-fx-text-fill: TEXT_FILL_PRIMARY;
}
/*******************************************************************************
@@ -672,7 +675,6 @@
******************************************************************************/
.choice-box {
-fx-text-fill: TEXT_FILL;
-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
-fx-background-insets: 0, 1px;
-fx-background-radius: 4px;
@@ -683,6 +685,18 @@
-fx-background-color: CONTROL_BORDER_FOCUSED, CONTROL_BG_NORMAL;
}
.choice-box:disabled {
-fx-background-color: CONTROL_BORDER_DISABLED, CONTROL_BG_DISABLED;
}
.choice-box > .label {
-fx-text-fill: TEXT_FILL;
}
.choice-box:disabled > .label {
-fx-text-fill: TEXT_FILL_SECONDARY;
}
.choice-box > .open-button {
-fx-padding: 0 0 0 0.3em;
}
@@ -694,6 +708,10 @@
-fx-shape: "M 0 0 h 7 l -3.5 4 z";
}
.choice-box:disabled > .open-button > .arrow {
-fx-background-color: transparent, TEXT_FILL_SECONDARY;
}
.choice-box .context-menu {
-fx-translate-x: -1.4em;
}

View File

@@ -11,8 +11,8 @@
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.addvaultwizard.ChooseExistingVaultController"
prefWidth="400"
prefHeight="400"
prefWidth="450"
prefHeight="450"
spacing="12"
alignment="CENTER">
<padding>

View File

@@ -11,9 +11,9 @@
<?import org.cryptomator.ui.controls.FormattedLabel?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.addvaultwizard.AddVaultFailureExisitingController"
minWidth="400"
maxWidth="400"
fx:controller="org.cryptomator.ui.addvaultwizard.AddVaultFailureExistingController"
minWidth="450"
maxWidth="450"
minHeight="145"
spacing="12">
<padding>

View File

@@ -14,8 +14,8 @@
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.addvaultwizard.CreateNewVaultLocationController"
prefWidth="400"
prefHeight="400"
prefWidth="450"
prefHeight="450"
spacing="12"
alignment="CENTER_LEFT">
<fx:define>
@@ -29,8 +29,10 @@
<VBox spacing="6">
<Label wrapText="true" text="%addvaultwizard.new.locationInstruction"/>
<RadioButton fx:id="iclouddriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="iCloud Drive" visible="${controller.locationPresets.foundIclouddrive}" managed="${controller.locationPresets.foundIclouddrive}"/>
<RadioButton fx:id="dropboxRadioButton" toggleGroup="${predefinedLocationToggler}" text="Dropbox" visible="${controller.locationPresets.foundDropbox}" managed="${controller.locationPresets.foundDropbox}"/>
<RadioButton fx:id="gdriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="Google Drive" visible="${controller.locationPresets.foundGdrive}" managed="${controller.locationPresets.foundGdrive}"/>
<RadioButton fx:id="onedriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="OneDrive" visible="${controller.locationPresets.foundOnedrive}" managed="${controller.locationPresets.foundOnedrive}"/>
<HBox spacing="12" alignment="CENTER_LEFT">
<RadioButton fx:id="customRadioButton" toggleGroup="${predefinedLocationToggler}" text="%addvaultwizard.new.directoryPickerLabel"/>
<Button contentDisplay="LEFT" text="%addvaultwizard.new.directoryPickerButton" onAction="#chooseCustomVaultPath" disable="${controller.usePresetPath}">

View File

@@ -12,8 +12,8 @@
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.addvaultwizard.CreateNewVaultNameController"
prefWidth="400"
prefHeight="400"
prefWidth="450"
prefHeight="450"
spacing="12"
alignment="CENTER_LEFT">
<padding>

View File

@@ -3,42 +3,34 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.NiceSecurePasswordField?>
<?import org.cryptomator.ui.controls.PasswordStrengthIndicator?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.addvaultwizard.CreateNewVaultPasswordController"
prefWidth="400"
prefHeight="400"
prefWidth="450"
prefHeight="450"
spacing="12"
alignment="CENTER_LEFT">
<fx:define>
<ToggleGroup fx:id="recoveryKeyChoice"/>
</fx:define>
<padding>
<Insets topRightBottomLeft="24"/>
</padding>
<children>
<Region VBox.vgrow="ALWAYS"/>
<fx:include source="/fxml/new_password.fxml"/>
<Region VBox.vgrow="ALWAYS"/>
<VBox spacing="6">
<Label text="%addvaultwizard.new.enterPassword" labelFor="$passwordField"/>
<NiceSecurePasswordField fx:id="passwordField"/>
<PasswordStrengthIndicator spacing="6" prefHeight="6" strength="${controller.passwordStrength}"/>
<Label fx:id="passwordStrengthLabel" styleClass="label-secondary" labelFor="$passwordField" alignment="CENTER_RIGHT" maxWidth="Infinity"/>
</VBox>
<VBox spacing="6">
<Label text="%addvaultwizard.new.reenterPassword" labelFor="$reenterField"/>
<NiceSecurePasswordField fx:id="reenterField"/>
<HBox fx:id="passwordMatchBox" spacing="6" alignment="CENTER_RIGHT">
<FontAwesome5IconView fx:id="checkmark" styleClass="glyph-icon-primary" glyph="CHECK"/>
<FontAwesome5IconView fx:id="cross" styleClass="glyph-icon-red" glyph="TIMES"/>
<Label fx:id="passwordMatchLabel" styleClass="label-secondary" labelFor="$reenterField"/>
</HBox>
<Label text="%addvaultwizard.new.generateRecoveryKeyChoice" wrapText="true"/>
<RadioButton fx:id="showRecoveryKey" toggleGroup="${recoveryKeyChoice}" text="%addvaultwizard.new.generateRecoveryKeyChoice.yes" />
<RadioButton fx:id="skipRecoveryKey" toggleGroup="${recoveryKeyChoice}" text="%addvaultwizard.new.generateRecoveryKeyChoice.no" />
</VBox>
<Region VBox.vgrow="ALWAYS"/>

View File

@@ -3,16 +3,13 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.addvaultwizard.CreateNewVaultRecoveryKeyController"
prefWidth="400"
prefHeight="400"
prefWidth="450"
prefHeight="450"
spacing="12"
alignment="CENTER_LEFT">
<padding>
@@ -21,17 +18,13 @@
<children>
<Region VBox.vgrow="ALWAYS"/>
<Label text="%addvaultwizard.new.recoveryKeyInstruction" wrapText="true"/>
<TextArea editable="false" text="${controller.recoveryKey}" wrapText="true"/>
<CheckBox fx:id="finalConfirmationCheckbox" text="%addvaultwizard.new.recoveryKeySavedCheckbox" wrapText="true"/>
<fx:include source="/fxml/recoverykey_display.fxml"/>
<Region VBox.vgrow="ALWAYS"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+X">
<buttons>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" onAction="#next" defaultButton="true" disable="${!finalConfirmationCheckbox.selected}"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" onAction="#next" defaultButton="true"/>
</buttons>
</ButtonBar>
</children>

View File

@@ -13,8 +13,8 @@
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.addvaultwizard.AddVaultSuccessController"
prefWidth="400"
prefHeight="400"
prefWidth="450"
prefHeight="450"
spacing="12"
alignment="TOP_CENTER">
<padding>
@@ -36,8 +36,8 @@
<ButtonBar buttonMinWidth="120" buttonOrder="+IU">
<buttons>
<Button text="%generic.button.done" ButtonBar.buttonData="FINISH" onAction="#close"/>
<Button text="%addvaultwizard.success.unlockNow" ButtonBar.buttonData="OTHER" onAction="#unlockAndClose" defaultButton="true"/>
<Button text="%generic.button.done" ButtonBar.buttonData="FINISH" onAction="#close" defaultButton="${!controller.vault.locked}"/>
<Button text="%addvaultwizard.success.unlockNow" ButtonBar.buttonData="OTHER" onAction="#unlockAndClose" defaultButton="${controller.vault.locked}" visible="${controller.vault.locked}"/>
</buttons>
</ButtonBar>
</children>

View File

@@ -10,8 +10,8 @@
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.addvaultwizard.AddVaultWelcomeController"
prefWidth="400"
prefHeight="400"
prefWidth="450"
prefHeight="450"
spacing="12"
alignment="TOP_CENTER">
<padding>

View File

@@ -28,22 +28,9 @@
</VBox>
<Region prefHeight="12" VBox.vgrow="NEVER"/>
<fx:include source="/fxml/new_password.fxml"/>
<VBox spacing="6">
<Label labelFor="$newPasswordField" text="%changepassword.enterNewPassword"/>
<NiceSecurePasswordField fx:id="newPasswordField"/>
<PasswordStrengthIndicator prefHeight="6" spacing="6" strength="${controller.passwordStrength}"/>
<Label fx:id="passwordStrengthLabel" styleClass="label-secondary" alignment="CENTER_RIGHT" maxWidth="Infinity"/>
</VBox>
<VBox spacing="6">
<Label labelFor="$reenterPasswordField" text="%changepassword.reenterNewPassword"/>
<NiceSecurePasswordField fx:id="reenterPasswordField"/>
<HBox fx:id="passwordMatchBox" spacing="6" alignment="CENTER_RIGHT">
<FontAwesome5IconView fx:id="checkmark" styleClass="glyph-icon-primary" glyph="CHECK"/>
<FontAwesome5IconView fx:id="cross" styleClass="glyph-icon-red" glyph="TIMES"/>
<Label fx:id="passwordMatchLabel" styleClass="label-secondary" labelFor="$reenterPasswordField"/>
</HBox>
</VBox>
<CheckBox fx:id="finalConfirmationCheckbox" text="%changepassword.finalConfirmation" wrapText="true"/>
<Region VBox.vgrow="ALWAYS"/>

View File

@@ -9,55 +9,32 @@
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Rectangle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.layout.Pane?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:id="root"
fx:controller="org.cryptomator.ui.mainwindow.MainWindowController"
styleClass="main-window">
<HBox styleClass="title" fx:id="titleBar" alignment="CENTER" minHeight="50" maxHeight="50" VBox.vgrow="NEVER" spacing="6">
<padding>
<Insets bottom="6" left="12" right="12" top="6"/>
</padding>
<children>
<Label text="Cryptomator"/>
<Region HBox.hgrow="ALWAYS"/>
<Button contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="#showPreferences" focusTraversable="false">
<graphic>
<StackPane>
<FontAwesome5IconView glyph="COGS"/>
<Region styleClass="update-indicator" visible="${controller.updateAvailable}" StackPane.alignment="TOP_RIGHT" prefWidth="10" prefHeight="10" maxWidth="-Infinity" maxHeight="-Infinity"/>
</StackPane>
</graphic>
<tooltip>
<Tooltip text="%main.preferencesBtn.tooltip"/>
</tooltip>
</Button>
<Button contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="#close" focusTraversable="false">
<graphic>
<FontAwesome5IconView glyph="TIMES"/>
</graphic>
<tooltip>
<Tooltip text="%main.closeBtn.tooltip"/>
</tooltip>
</Button>
</children>
</HBox>
<StackPane VBox.vgrow="ALWAYS">
<SplitPane dividerPositions="0.33" orientation="HORIZONTAL">
<fx:include source="/fxml/vault_list.fxml" SplitPane.resizableWithParent="false"/>
<fx:include source="/fxml/vault_detail.fxml" SplitPane.resizableWithParent="true"/>
</SplitPane>
<Region styleClass="resizer" StackPane.alignment="BOTTOM_RIGHT" fx:id="resizer" prefWidth="10" prefHeight="10" maxWidth="-Infinity" maxHeight="-Infinity"/>
<!-- TODO: use css instead of adding a Rectangle: -->
<Pane fx:id="dragAndDropIndicator">
<StackPane.margin>
<Insets topRightBottomLeft="24"/>
</StackPane.margin>
<Rectangle arcHeight="4" arcWidth="4" fill="gainsboro" strokeType="CENTERED" strokeWidth="3" strokeDashArray="20, 20" strokeLineJoin="ROUND" stroke="black" height="${dragAndDropIndicator.height}"
width="${dragAndDropIndicator.width}"/>
</Pane>
</StackPane>
</VBox>
<StackPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:id="root"
fx:controller="org.cryptomator.ui.mainwindow.MainWindowController"
styleClass="main-window">
<VBox>
<fx:include source="/fxml/main_window_title.fxml" VBox.vgrow="NEVER" />
<StackPane VBox.vgrow="ALWAYS">
<SplitPane dividerPositions="0.33" orientation="HORIZONTAL">
<fx:include source="/fxml/vault_list.fxml" SplitPane.resizableWithParent="false"/>
<fx:include source="/fxml/vault_detail.fxml" SplitPane.resizableWithParent="true"/>
</SplitPane>
<VBox styleClass="drag-n-drop-indicator" visible="${controller.draggingOver}" alignment="TOP_CENTER">
<HBox visible="${!controller.draggingVaultOver}" managed="${!controller.draggingVaultOver}" spacing="6" styleClass="drag-n-drop-header" alignment="CENTER" VBox.vgrow="NEVER">
<FontAwesome5IconView glyph="EXCLAMATION_TRIANGLE"/>
<Label text="%main.dropZone.unknownDragboardContent"/>
</HBox>
<HBox visible="${controller.draggingVaultOver}" managed="${controller.draggingVaultOver}" spacing="6" styleClass="drag-n-drop-header" alignment="CENTER" VBox.vgrow="NEVER">
<FontAwesome5IconView glyph="CHECK"/>
<Label text="%main.dropZone.dropVault"/>
</HBox>
<Region VBox.vgrow="ALWAYS"/>
</VBox>
</StackPane>
</VBox>
<fx:include source="/fxml/main_window_resize.fxml"/>
</StackPane>

View File

@@ -0,0 +1,17 @@
<?import javafx.scene.Cursor?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.Region?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.mainwindow.ResizeController"
nodeOrientation="LEFT_TO_RIGHT"
pickOnBounds="false">
<fx:define>
<Cursor fx:id="nwResize" fx:constant="NW_RESIZE"/>
<Cursor fx:id="neResize" fx:constant="NE_RESIZE"/>
</fx:define>
<Region fx:id="tlResizer" cursor="${nwResize}" prefWidth="10" prefHeight="10" maxWidth="-Infinity" maxHeight="-Infinity" AnchorPane.topAnchor="0" AnchorPane.leftAnchor="0"/>
<Region fx:id="trResizer" cursor="${neResize}" prefWidth="10" prefHeight="10" maxWidth="-Infinity" maxHeight="-Infinity" AnchorPane.topAnchor="0" AnchorPane.rightAnchor="0"/>
<Region fx:id="blResizer" cursor="${neResize}" prefWidth="10" prefHeight="10" maxWidth="-Infinity" maxHeight="-Infinity" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0"/>
<Region fx:id="brResizer" cursor="${nwResize}" prefWidth="10" prefHeight="10" maxWidth="-Infinity" maxHeight="-Infinity" AnchorPane.bottomAnchor="0" AnchorPane.rightAnchor="0"/>
</AnchorPane>

View File

@@ -0,0 +1,55 @@
<?import javafx.scene.layout.StackPane?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.layout.HBox?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<HBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:id="titleBar"
fx:controller="org.cryptomator.ui.mainwindow.MainWindowTitleController"
styleClass="title"
alignment="CENTER"
minHeight="50"
maxHeight="50"
spacing="6">
<padding>
<Insets bottom="6" left="12" right="12" top="6"/>
</padding>
<children>
<Label text="Cryptomator"/>
<Region HBox.hgrow="ALWAYS"/>
<Button contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="#showDonationKeyPreferences" focusTraversable="false" visible="${!controller.licenseHolder.validLicense}">
<graphic>
<StackPane>
<FontAwesome5IconView glyph="EXCLAMATION_CIRCLE" glyphSize="16"/>
<Region styleClass="update-indicator" StackPane.alignment="TOP_RIGHT" prefWidth="10" prefHeight="10" maxWidth="-Infinity" maxHeight="-Infinity"/>
</StackPane>
</graphic>
<tooltip>
<Tooltip text="%main.donationKeyMissing.tooltip"/>
</tooltip>
</Button>
<Button contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="#showPreferences" focusTraversable="false">
<graphic>
<StackPane>
<FontAwesome5IconView glyph="COGS" glyphSize="16"/>
<Region styleClass="update-indicator" visible="${controller.updateAvailable}" StackPane.alignment="TOP_RIGHT" prefWidth="10" prefHeight="10" maxWidth="-Infinity" maxHeight="-Infinity"/>
</StackPane>
</graphic>
<tooltip>
<Tooltip text="%main.preferencesBtn.tooltip"/>
</tooltip>
</Button>
<Button contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="#close" focusTraversable="false">
<graphic>
<FontAwesome5IconView glyph="TIMES" glyphSize="16"/>
</graphic>
<tooltip>
<Tooltip text="%main.closeBtn.tooltip"/>
</tooltip>
</Button>
</children>
</HBox>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.NiceSecurePasswordField?>
<?import org.cryptomator.ui.controls.PasswordStrengthIndicator?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.common.NewPasswordController"
spacing="6"
alignment="CENTER_LEFT">
<fx:define>
<FontAwesome5IconView fx:id="checkmark" styleClass="glyph-icon-primary" glyph="CHECK"/>
<FontAwesome5IconView fx:id="cross" styleClass="glyph-icon-red" glyph="TIMES"/>
</fx:define>
<children>
<Label text="%newPassword.promptText" labelFor="$passwordField"/>
<NiceSecurePasswordField fx:id="passwordField"/>
<PasswordStrengthIndicator spacing="6" prefHeight="6" strength="${controller.passwordStrength}"/>
<Label fx:id="passwordStrengthLabel" styleClass="label-secondary" alignment="CENTER_RIGHT" maxWidth="Infinity"/>
<Region/>
<Label text="%newPassword.reenterPassword" labelFor="$reenterField"/>
<NiceSecurePasswordField fx:id="reenterField"/>
<Label fx:id="passwordMatchLabel" styleClass="label-secondary" alignment="CENTER_RIGHT" maxWidth="Infinity" graphicTextGap="6" contentDisplay="LEFT" />
</children>
</VBox>

View File

@@ -7,7 +7,10 @@
xmlns:fx="http://javafx.com/fxml"
fx:id="tabPane"
fx:controller="org.cryptomator.ui.preferences.PreferencesController"
minWidth="400"
minWidth="-Infinity"
maxWidth="-Infinity"
prefWidth="500"
tabMinWidth="60"
tabClosingPolicy="UNAVAILABLE"
tabDragPolicy="FIXED">
<tabs>
@@ -35,5 +38,13 @@
<fx:include source="/fxml/preferences_updates.fxml"/>
</content>
</Tab>
<Tab fx:id="donationKeyTab" text="%preferences.donationKey">
<graphic>
<FontAwesome5IconView glyph="HEART"/>
</graphic>
<content>
<fx:include source="/fxml/preferences_donationkey.fxml"/>
</content>
</Tab>
</tabs>
</TabPane>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.preferences.DonationKeyPreferencesController"
spacing="18">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<StackPane VBox.vgrow="NEVER" prefHeight="60">
<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.licenseHolder.validLicense}">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="USER_CROWN" glyphSize="24"/>
</StackPane>
<FormattedLabel format="%preferences.donationKey.registeredFor" arg1="${controller.licenseHolder.licenseSubject}" wrapText="true"/>
</HBox>
<HBox spacing="12" alignment="CENTER_LEFT" visible="${!controller.licenseHolder.validLicense}">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="HAND_HOLDING_HEART" glyphSize="24"/>
</StackPane>
<VBox HBox.hgrow="ALWAYS" spacing="6">
<Label text="%preferences.donationKey.noDonationKey" wrapText="true" VBox.vgrow="ALWAYS"/>
<Hyperlink text="%preferences.donationKey.getDonationKey" onAction="#getDonationKey" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="LINK"/>
</graphic>
</Hyperlink>
</VBox>
</HBox>
</StackPane>
<TextArea fx:id="donationKeyField" wrapText="true" VBox.vgrow="ALWAYS" prefRowCount="6"/>
</children>
</VBox>

View File

@@ -21,17 +21,19 @@
<children>
<HBox spacing="6" alignment="CENTER_LEFT">
<Label text="%preferences.general.theme"/>
<ChoiceBox fx:id="themeChoiceBox"/>
<ChoiceBox fx:id="themeChoiceBox" disable="${!controller.licenseHolder.validLicense}"/>
</HBox>
<HBox spacing="6" alignment="CENTER_LEFT">
<Label text="Interface Orientation" HBox.hgrow="NEVER"/>
<RadioButton fx:id="nodeOrientationLtr" text="Left to Right" alignment="CENTER_LEFT" toggleGroup="${nodeOrientation}"/>
<RadioButton fx:id="nodeOrientationRtl" text="Right to Left" alignment="CENTER_RIGHT" toggleGroup="${nodeOrientation}"/>
<Label text="%preferences.general.interfaceOrientation" HBox.hgrow="NEVER"/>
<RadioButton fx:id="nodeOrientationLtr" text="%preferences.general.interfaceOrientation.ltr" alignment="CENTER_LEFT" toggleGroup="${nodeOrientation}"/>
<RadioButton fx:id="nodeOrientationRtl" text="%preferences.general.interfaceOrientation.rtl" alignment="CENTER_RIGHT" toggleGroup="${nodeOrientation}"/>
</HBox>
<CheckBox fx:id="startHiddenCheckbox" text="%preferences.general.startHidden"/>
<CheckBox fx:id="startHiddenCheckbox" text="%preferences.general.startHidden" visible="${controller.trayMenuSupported}" managed="${controller.trayMenuSupported}"/>
<CheckBox fx:id="debugModeCheckbox" text="%preferences.general.debugLogging"/>
<CheckBox fx:id="autoStartCheckbox" text="%preferences.general.autoStart" visible="${controller.autoStartSupported}" managed="${controller.autoStartSupported}" onAction="#toggleAutoStart"/>
</children>
</VBox>

View File

@@ -1,46 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.recoverykey.RecoveryKeyDisplayController"
minWidth="400"
maxWidth="400"
minHeight="145"
maxHeight="180"
minWidth="350"
minHeight="280"
spacing="12"
alignment="TOP_CENTER">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
alignment="TOP_LEFT">
<children>
<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CHECK" glyphSize="24"/>
</StackPane>
<FormattedLabel format="%recoveryKey.display.message" arg1="${controller.vaultName}" wrapText="true"/>
<VBox spacing="6" HBox.hgrow="ALWAYS">
<FormattedLabel format="%recoveryKey.display.message" arg1="${controller.vault.displayableName}" wrapText="true" VBox.vgrow="NEVER"/>
<TextArea editable="false" text="${controller.recoveryKey}" wrapText="true" VBox.vgrow="ALWAYS"/>
</VBox>
</HBox>
<TextArea editable="false" text="${controller.recoveryKey}" wrapText="true" prefRowCount="4" fx:id="textarea"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+R">
<buttons>
<Button text="%generic.button.print" ButtonBar.buttonData="RIGHT" onAction="#printRecoveryKey" visible="${controller.printerSupported}">
<graphic>
<FontAwesome5IconView glyph="PRINT"/>
</graphic>
</Button>
<Button text="%generic.button.copy" ButtonBar.buttonData="RIGHT" onAction="#copyRecoveryKey">
<graphic>
<FontAwesome5IconView glyph="COPY"/>
</graphic>
</Button>
</buttons>
</ButtonBar>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
<buttons>
<Button text="%generic.button.done" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
</buttons>
</ButtonBar>
</VBox>
<Region VBox.vgrow="ALWAYS"/>
<Label text="%recoveryKey.display.StorageHints" wrapText="true"/>
</children>
</VBox>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.Region?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.recoverykey.RecoveryKeySuccessController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_CENTER">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<fx:include source="/fxml/recoverykey_display.fxml"/>
<Region VBox.vgrow="ALWAYS"/>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
<buttons>
<Button text="%generic.button.done" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</VBox>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.common.StackTraceController"
minWidth="300"
spacing="12">
<children>
<HBox spacing="12" VBox.vgrow="ALWAYS">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="EXCLAMATION" glyphSize="24"/>
</StackPane>
<VBox spacing="6" HBox.hgrow="ALWAYS">
<Label text="%generic.error.title" wrapText="true"/>
<Label text="%generic.error.instruction" wrapText="true"/>
</VBox>
</HBox>
<TextArea text="${controller.stackTrace}" prefRowCount="5" editable="false"/>
</children>
</VBox>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.Region?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.unlock.UnlockGenericErrorController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<fx:include source="/fxml/stacktrace.fxml"/>
<Region VBox.vgrow="ALWAYS"/>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="B+U">
<buttons>
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back"/>
<Region ButtonBar.buttonData="OTHER"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</VBox>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.unlock.UnlockInvalidMountPointController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<HBox spacing="12" VBox.vgrow="ALWAYS">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="EXCLAMATION" glyphSize="24"/>
</StackPane>
<VBox spacing="6" HBox.hgrow="ALWAYS">
<FormattedLabel format="%unlock.error.invalidMountPoint" arg1="${controller.mountPoint}" wrapText="true"/>
</VBox>
</HBox>
<Region VBox.vgrow="ALWAYS"/>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="B+U">
<buttons>
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back"/>
<Region ButtonBar.buttonData="OTHER"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</VBox>

View File

@@ -1,10 +1,10 @@
<?import javafx.scene.layout.HBox?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.ThrougputLabel?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
@@ -18,7 +18,7 @@
<FontAwesome5IconView glyph="HDD" glyphSize="24"/>
<VBox spacing="4" alignment="CENTER_LEFT">
<Label text="%main.vaultDetail.revealBtn"/>
<Label styleClass="label-small" text="${controller.vault.accessPoint}" textOverrun="CENTER_ELLIPSIS"/>
<Label styleClass="label-extra-small" text="${controller.vault.accessPoint}" textOverrun="CENTER_ELLIPSIS"/>
</VBox>
</HBox>
</graphic>

View File

@@ -8,6 +8,7 @@
fx:id="tabPane"
fx:controller="org.cryptomator.ui.vaultoptions.VaultOptionsController"
minWidth="400"
tabMinWidth="60"
tabClosingPolicy="UNAVAILABLE"
tabDragPolicy="FIXED">
<tabs>

View File

@@ -10,7 +10,6 @@
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<?import org.cryptomator.ui.controls.AlphanumericTextField?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<VBox xmlns="http://javafx.com/javafx"
@@ -32,35 +31,36 @@
<CheckBox fx:id="readOnlyCheckbox" text="%vaultOptions.mount.readonly"/>
<CheckBox fx:id="customMountFlagsCheckbox" text="%vaultOptions.mount.customMountFlags" onAction="#toggleUseCustomMountFlags"/>
<HBox>
<padding>
<Insets left="25"/>
</padding>
<children>
<TextField fx:id="mountFlags" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
</children>
</HBox>
<Text text="TODO Mount Point"/>
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointAuto" text="TODO Automatically pick a suitable location"/>
<TextField fx:id="mountFlags" HBox.hgrow="ALWAYS" maxWidth="Infinity">
<VBox.margin>
<Insets left="24"/>
</VBox.margin>
</TextField>
<Label text="%vaultOptions.mount.mountPoint">
<VBox.margin>
<Insets top="9"/>
</VBox.margin>
</Label>
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointAuto" text="%vaultOptions.mount.mountPoint.auto"/>
<HBox spacing="6" visible="${controller.osIsWindows}" managed="${controller.osIsWindows}">
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointWinDriveLetter" text="TODO Choose specific drive letter"/>
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointWinDriveLetter" text="%vaultOptions.mount.mountPoint.driveLetter"/>
<ChoiceBox fx:id="driveLetterSelection" disable="${!mountPointWinDriveLetter.selected}"/>
</HBox>
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointCustomDir" text="TODO Choose empty directory"/>
<HBox visible="${mountPointCustomDir.selected}">
<padding>
<Insets left="24"/>
</padding>
<children>
<TextField text="${controller.customMountPath}" HBox.hgrow="ALWAYS" maxWidth="Infinity" disable="true"/>
<Button text="TODO change" onAction="#chooseCustomMountPoint" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="FOLDER_OPEN" glyphSize="15"/>
</graphic>
</Button>
</children>
<HBox spacing="6" alignment="CENTER_LEFT">
<RadioButton toggleGroup="${mountPoint}" fx:id="mountPointCustomDir" text="%vaultOptions.mount.mountPoint.custom"/>
<Button text="%vaultOptions.mount.mountPoint.directoryPickerButton" onAction="#chooseCustomMountPoint" contentDisplay="LEFT" disable="${!mountPointCustomDir.selected}">
<graphic>
<FontAwesome5IconView glyph="FOLDER_OPEN" glyphSize="15"/>
</graphic>
</Button>
</HBox>
<TextField text="${controller.customMountPath}" visible="${mountPointCustomDir.selected}" maxWidth="Infinity" disable="true">
<VBox.margin>
<Insets left="24"/>
</VBox.margin>
</TextField>
</children>
</VBox>

View File

@@ -7,12 +7,18 @@ generic.button.apply=Apply
generic.button.back=Back
generic.button.cancel=Cancel
generic.button.change=Change
generic.button.copy=Copy
generic.button.done=Done
generic.button.next=Next
generic.button.print=Print
## Error
generic.error.title=An unexpected error occured
generic.error.instruction=This should not have happened. Please report the error text below and include a description of what steps did lead to this error.
# Tray Menu
traymenu.showMainWindow=Show
traymenu.showPreferencesWindow=Preferences
traymenu.lockAllVaults=Lock All
traymenu.quitApplication=Quit
traymenu.vault.unlock=Unlock
traymenu.vault.lock=Lock
@@ -34,18 +40,14 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Custom Location
addvaultwizard.new.directoryPickerButton=Choose…
addvaultwizard.new.directoryPickerTitle=Select Directory
addvaultwizard.new.enterPassword=Enter a password for the vault
addvaultwizard.new.fileAlreadyExists=Vault can not be created at this path because some object already exists.
addvaultwizard.new.invalidName=Invalid vault name. Please consider a regular directory name.
addvaultwizard.new.ioException=Selected directory failed a basic test. Make sure to have the appropriate access rights and nothing interferes with Cryptomator.
### Password
addvaultwizard.new.reenterPassword=Confirm the password
addvaultwizard.new.passwordsMatch=Passwords match!
addvaultwizard.new.passwordsDoNotMatch=Passwords do not match
addvaultwizard.new.createVaultBtn=Create Vault
### Recovery Key
addvaultwizard.new.recoveryKeyInstruction=This is your recovery key. Keep it safe, it is your only chance to recover your data if you lose your password.
addvaultwizard.new.recoveryKeySavedCheckbox=Yes, I've made a secure backup of this recovery key
addvaultwizard.new.generateRecoveryKeyChoice=You won't be able to access your data without your password. Do you want a recovery key for the case you lose your password?
addvaultwizard.new.generateRecoveryKeyChoice.yes=Yes please, better safe than sorry
addvaultwizard.new.generateRecoveryKeyChoice.no=No thanks, I will not lose my password
### Information
addvault.new.readme.storageLocation.fileName=WHAT IS THIS DIRECTORY.rtf
addvault.new.readme.storageLocation.1=\\fs40\\qc ⚠️ VAULT FILES ⚠️
@@ -73,10 +75,6 @@ removeVault.confirmBtn=Remove Vault
# Change Password
changepassword.title=Change Password
changepassword.enterOldPassword=Enter the current password for "%s"
changepassword.enterNewPassword=Enter a new password for your vault
changepassword.reenterNewPassword=Confirm the new password
changepassword.passwordsMatch=Passwords match!
changepassword.passwordsDoNotMatch=Passwords do not match
changepassword.finalConfirmation=I understand that I will not be able to recover my data if I forget my password
# Forget Password
@@ -92,6 +90,8 @@ unlock.unlockBtn=Unlock
## Success
unlock.success.message=Unlocked "%s" successfully! Your vault is now accessible.
unlock.success.revealBtn=Reveal Vault
## Invalid Mount Point
unlock.error.invalidMountPoint=Mount point is not an empty directory: %s
# Migration
migration.title=Upgrade Vault
@@ -112,6 +112,10 @@ preferences.general=General
preferences.general.theme=Look & Feel
preferences.general.startHidden=Hide window when starting Cryptomator
preferences.general.debugLogging=Enable debug logging
preferences.general.autoStart=Launch Cryptomator on system start
preferences.general.interfaceOrientation=Interface Orientation
preferences.general.interfaceOrientation.ltr=Left to Right
preferences.general.interfaceOrientation.rtl=Right to Left
## Volume
preferences.volume=Virtual Drive
preferences.volume.type=Volume Type
@@ -123,10 +127,19 @@ preferences.updates.currentVersion=Current Version: %s
preferences.updates.autoUpdateCheck=Check for updates automatically
preferences.updates.checkNowBtn=Check Now
preferences.updates.updateAvailable=Update to version %s available.
## Donation Key
preferences.donationKey=Donation
preferences.donationKey.registeredFor=Registered for %s
preferences.donationKey.noDonationKey=No valid donation key found. It's like a license key but for awesome people using free software. ;-)
preferences.donationKey.getDonationKey=Get a donation key
# Main Window
main.closeBtn.tooltip=Close
main.preferencesBtn.tooltip=Preferences
main.donationKeyMissing.tooltip=Please consider donating
## Drag 'n' Drop
main.dropZone.dropVault=Add this vault
main.dropZone.unknownDragboardContent=If you want to add a vault, drag it to this window
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Click here to add a vault
main.vaultlist.contextMenu.remove=Remove Vault
@@ -166,14 +179,25 @@ vaultOptions.mount.readonly=Read-Only
vaultOptions.mount.driveName=Drive Name
vaultOptions.mount.customMountFlags=Custom Mount Flags
vaultOptions.mount.winDriveLetterOccupied=occupied
vaultOptions.mount.winDirChooser=Pick an empty directory
vaultOptions.mount.mountPoint=Mount Point
vaultOptions.mount.mountPoint.auto=Automatically pick a suitable location
vaultOptions.mount.mountPoint.driveLetter=Use assigned drive letter
vaultOptions.mount.mountPoint.custom=Custom path
vaultOptions.mount.mountPoint.directoryPickerButton=Choose…
vaultOptions.mount.mountPoint.directoryPickerTitle=Pick an empty directory
# Recovery Key
recoveryKey.title=Recovery Key
recoveryKey.enterPassword.prompt=Enter your password to show the recovery key for "%s":
recoveryKey.display.message=Recovery Key for "%s":
recoveryKey.display.message=The following recovery key can be used to restore access to "%s":
recoveryKey.display.StorageHints=Keep it somewhere very secure, e.g.:\n • Store it using a password manager\n • Save it on a USB flash drive\n • Print it on paper
# Misc
# New Password
newPassword.promptText=Enter a new password
newPassword.reenterPassword=Confirm the new password
newPassword.passwordsMatch=Passwords match!
newPassword.passwordsDoNotMatch=Passwords do not match
passwordStrength.messageLabel.tooShort=Use at least %d characters
passwordStrength.messageLabel.0=Very weak
passwordStrength.messageLabel.1=Weak
passwordStrength.messageLabel.2=Fair

View File

@@ -8,6 +8,7 @@ generic.button.cancel=الغاء
generic.button.change=تغيير
generic.button.done=تم
generic.button.next=التالي
## Error
# Tray Menu
traymenu.showMainWindow=اظهار
@@ -33,15 +34,10 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=موقع مخصص
addvaultwizard.new.directoryPickerButton=اختر…
addvaultwizard.new.directoryPickerTitle=اختر القاموس
addvaultwizard.new.enterPassword=أدخل كلمة المرور لهذا الحساب
addvaultwizard.new.fileAlreadyExists=لا يمكن إنشاء مخزن على هذا المسار لأن بعض الملفات موجودة مسبقا.
addvaultwizard.new.invalidName=اسم المخزن غير صالح. يرجى النظر في اسم الدليل المعتاد.
### Password
addvaultwizard.new.reenterPassword=تأكيد كلمة المرور
addvaultwizard.new.passwordsMatch=كلمات المرور متطابقة!
addvaultwizard.new.passwordsDoNotMatch=كلمات المرور غير متطابقة
addvaultwizard.new.createVaultBtn=انشئ حافظة
### Recovery Key
### Information
addvault.new.readme.storageLocation.4=لو انك تحتاج مساعدة, جرب "%s".
## Existing
@@ -52,10 +48,6 @@ addvaultwizard.existing.chooseBtn=اختر…
removeVault.confirmBtn=احذف الحافظة
# Change Password
changepassword.enterNewPassword=ادخل كلمة المرور الجديدة لحافظتك
changepassword.reenterNewPassword=تأكيد كلمة المرور الجديدة
changepassword.passwordsMatch=كلمات المرور متطابقة!
changepassword.passwordsDoNotMatch=كلمات المرور غير متطابقة
# Forget Password
forgetPassword.title=نسيت كلمة المرور
@@ -67,6 +59,7 @@ unlock.savePassword=احفظ كلمة المرور
unlock.unlockBtn=افتح
## Success
unlock.success.revealBtn=افتح الحافظة
## Invalid Mount Point
# Migration
migration.title=ترقية الحافظة
@@ -87,9 +80,11 @@ preferences.volume.webdav.port=منفذ WebDav
preferences.updates=تحديثات
preferences.updates.autoUpdateCheck=تحقق من التحديثات اوتوماتيكيا
preferences.updates.checkNowBtn=تحقق الان
## Donation Key
# Main Window
main.preferencesBtn.tooltip=تفضيلات
## Drag 'n' Drop
## Vault List
main.vaultlist.addVaultBtn=أضِف مخزنًا
## Vault Detail
@@ -108,9 +103,10 @@ main.vaultDetail.migrateButton=ترقية الحافظة
## General
vaultOptions.general=عام
## Mount
vaultOptions.mount.mountPoint.directoryPickerButton=اختر…
# Recovery Key
# Misc
# New Password
# Quit

View File

@@ -6,8 +6,11 @@ generic.button.apply=Použít
generic.button.back=Zpět
generic.button.cancel=Zrušit
generic.button.change=Změnit
generic.button.copy=Kopírovat
generic.button.done=Hotovo
generic.button.next=Další
generic.button.print=Vytisknout
## Error
# Tray Menu
traymenu.showMainWindow=Zobrazit
@@ -33,16 +36,12 @@ addvaultwizard.new.locationPrompt=...
addvaultwizard.new.directoryPickerLabel=Vlastní umístění
addvaultwizard.new.directoryPickerButton=Vybrat...
addvaultwizard.new.directoryPickerTitle=Vyberte adresář
addvaultwizard.new.enterPassword=Zadejte heslo pro trezor
addvaultwizard.new.fileAlreadyExists=Trezor nemůže být vytvořen na tomto umístění, protože stejný objekt již existuje.
addvaultwizard.new.invalidName=Neplatné jméno trezoru. Prosím změňte jméno adresáře.
### Password
addvaultwizard.new.reenterPassword=Potvrďte heslo
addvaultwizard.new.passwordsMatch=Hesla se shodují!
addvaultwizard.new.passwordsDoNotMatch=Hesla se neshodují
addvaultwizard.new.createVaultBtn=Vytvořit trezor
### Recovery Key
addvaultwizard.new.recoveryKeyInstruction=Toto je tvůj obnovovací klíč. Uchovej jej v bezpečí, je to jediná šance, jak obnovit data, pokud ztratíš heslo.
addvaultwizard.new.generateRecoveryKeyChoice.yes=Ano prosím, jistota je jistota
addvaultwizard.new.generateRecoveryKeyChoice.no=Ne, díky, neztratím své heslo
### Information
addvault.new.readme.storageLocation.fileName=CO JE TOTO ZA ADRESÁŘ.rtf
addvault.new.readme.storageLocation.1=\\fs40\\qc ⚠️ OBSAH TREZORU ⚠️
@@ -69,10 +68,6 @@ removeVault.confirmBtn=Odstranit trezor
# Change Password
changepassword.title=Změnit heslo
changepassword.enterOldPassword=Zadejte současné heslo pro "%s"
changepassword.enterNewPassword=Zadejte nové heslo pro váš trezor
changepassword.reenterNewPassword=Potvrď nové heslo
changepassword.passwordsMatch=Hesla se shodují!
changepassword.passwordsDoNotMatch=Hesla se neshodují
changepassword.finalConfirmation=UPOZORNĚNÍ: Pokud heslo zapomenete/ztratíte, nenávratně příjdete o přístup ke svému obsahu!
# Forget Password
@@ -88,6 +83,7 @@ unlock.unlockBtn=Odemknout
## Success
unlock.success.message=Trezor "%s" byl úspěšně odemčen a nyní je dostupný.
unlock.success.revealBtn=Zobrazit trezor
## Invalid Mount Point
# Migration
migration.title=Upgrade trezoru
@@ -119,10 +115,18 @@ preferences.updates.currentVersion=Aktuální verze: %s
preferences.updates.autoUpdateCheck=Automaticky kontrolovat aktualizace
preferences.updates.checkNowBtn=Zkontrolovat nyní
preferences.updates.updateAvailable=Dostupná nová verze %s.
## Donation Key
preferences.donationKey=Příspěvek
preferences.donationKey.registeredFor=Registrováno na: %s
preferences.donationKey.getDonationKey=Získat darovací klíč
# Main Window
main.closeBtn.tooltip=Zavřít
main.preferencesBtn.tooltip=Nastavení
main.donationKeyMissing.tooltip=Prosím, zvažte darování
## Drag 'n' Drop
main.dropZone.dropVault=Přidat tento trezor
main.dropZone.unknownDragboardContent=Pokud chcete přidat trezor, přetáhněte jej do tohoto okna
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Klikněte sem pro přidání nového trezoru
main.vaultlist.contextMenu.remove=Odstranit trezor
@@ -144,6 +148,7 @@ main.vaultDetail.throughput.kbps=%.1f kiB/s
main.vaultDetail.throughput.mbps=%.1f MiB/s
### Needs Migration
main.vaultDetail.migrateButton=Upgrade trezoru
main.vaultDetail.migratePrompt=Váš trezor musí být aktualizován na nový formát, než k němu budete mít přístup
# Wrong File Alert
wrongFileAlert.title=neznámý soubor
@@ -161,14 +166,20 @@ vaultOptions.mount.readonly=Pouze pro čtení
vaultOptions.mount.driveName=Jméno jednotky
vaultOptions.mount.customMountFlags=Vlastní parametry
vaultOptions.mount.winDriveLetterOccupied=obsazené
vaultOptions.mount.winDirChooser=Vyberte prázdný adresář
vaultOptions.mount.mountPoint=Přípojný bod
vaultOptions.mount.mountPoint.auto=Automaticky vybrat vhodné místo
vaultOptions.mount.mountPoint.driveLetter=Použít přiřazené písmeno
vaultOptions.mount.mountPoint.custom=Vlastní cesta
vaultOptions.mount.mountPoint.directoryPickerButton=Vybrat...
vaultOptions.mount.mountPoint.directoryPickerTitle=Vyberte prázdný adresář
# Recovery Key
recoveryKey.title=Klíč k obnově
recoveryKey.enterPassword.prompt=Zadejte své heslo pro zobrazení obnovovacího klíče pro "%s":
recoveryKey.display.message=Klíč k obnově "%s":
recoveryKey.display.message=Následující obnovovací klíč může být použit k obnovení přístupu k "%s":
recoveryKey.display.StorageHints=Uchovejte ho někde velmi bezpečně, např.\n • Uložit pomocí správce hesel\n • Uložit na USB flash disk\n • Vytisknout na papír
# Misc
# New Password
passwordStrength.messageLabel.0=velmi slabé
passwordStrength.messageLabel.1=slabé
passwordStrength.messageLabel.2=dostatečné

View File

@@ -6,12 +6,18 @@ generic.button.apply=Übernehmen
generic.button.back=Zurück
generic.button.cancel=Abbrechen
generic.button.change=Ändern
generic.button.copy=Kopieren
generic.button.done=Fertig
generic.button.next=Weiter
generic.button.print=Drucken
## Error
generic.error.title=Ein unerwarteter Fehler ist aufgetreten
generic.error.instruction=Dies hätte nicht passieren dürfen. Bitte melde den Fehlertext unten und füge eine Beschreibung hinzu, welche Schritte zu diesem Fehler geführt haben.
# Tray Menu
traymenu.showMainWindow=Anzeigen
traymenu.showPreferencesWindow=Einstellungen
traymenu.lockAllVaults=Alle sperren
traymenu.quitApplication=Beenden
traymenu.vault.unlock=Entsperren
traymenu.vault.lock=Sperren
@@ -32,32 +38,28 @@ addvaultwizard.new.locationLabel=Speicherort
addvaultwizard.new.locationPrompt=
addvaultwizard.new.directoryPickerLabel=Eigener Ort
addvaultwizard.new.directoryPickerButton=Durchsuchen 
addvaultwizard.new.directoryPickerTitle=Wähle ein Verzeichnis
addvaultwizard.new.enterPassword=Vergib ein Passwort für den Tresor
addvaultwizard.new.directoryPickerTitle=Verzeichnis auswählen
addvaultwizard.new.fileAlreadyExists=Der Tresor konnte nicht erstellt werden, da der Speicherort bereits belegt ist.
addvaultwizard.new.invalidName=Ungültiger Tresorname. Tresore müssen wie Verzeichnisse benannt werden.
addvaultwizard.new.ioException=Auf das ausgewählte Verzeichnis kann nicht zugegriffen werden. Prüfe die Zugriffsrechte und stelle sicher, dass Cryptomator nicht von einer anderen Anwendung blockiert wird.
### Password
addvaultwizard.new.reenterPassword=Bestätige das Passwort
addvaultwizard.new.passwordsMatch=Passwörter stimmen überein!
addvaultwizard.new.passwordsDoNotMatch=Passwörter stimmen nicht überein
addvaultwizard.new.createVaultBtn=Tresor erstellen
### Recovery Key
addvaultwizard.new.recoveryKeyInstruction=Dies ist dein Wiederherstellungsschlüssel. Bewahre ihn sicher auf. Ohne ihn gibt es keine Möglichkeit deine Daten wiederherzustellen, wenn du dein Kennwort verloren hast.
addvaultwizard.new.recoveryKeySavedCheckbox=Ja, ich habe eine Datensicherung dieses Wiederherstellungsschlüssels durchgeführt
addvaultwizard.new.generateRecoveryKeyChoice=Ohne dieses Passwort kannst du auf deine Daten nicht mehr zugreifen. Möchtest du für den Fall eines Passwortverlusts einen Wiederherstellungsschlüssel erstellen?
addvaultwizard.new.generateRecoveryKeyChoice.yes=Ja bitte, sicher ist sicher
addvaultwizard.new.generateRecoveryKeyChoice.no=Nein danke, ich werde mein Passwort nicht verlieren
### Information
addvault.new.readme.storageLocation.fileName=WAS BEDEUTET DIESER ORDNER.rtf
addvault.new.readme.storageLocation.1=\\fs40\\qc ⚠️ TRESORDATEIEN ⚠️
addvault.new.readme.storageLocation.2=Dies ist der Speicherort deines Tresors. Ändere {\\b KEINE} Dateien in diesem Verzeichnis.
addvault.new.readme.storageLocation.3=Wenn du Dateien mit Cryptomator verschlüsseln möchtest, entsperre den Tresor und verwende das zur Verfügung gestellte Laufwerk.
addvault.new.readme.storageLocation.4=Falls du Hilfe brauchst, versuche %s.
addvault.new.readme.accessLocation.fileName=WILLKOMMEN ZUM TRESOR.rtf
addvault.new.readme.accessLocation.fileName=WILLKOMMEN ZU DEINEM TRESOR.rtf
addvault.new.readme.accessLocation.1=\\fs40\\qc 🔐️ VERSCHLÜSSELTES LAUFWERK 🔐️
addvault.new.readme.accessLocation.2=Dies ist dein Tresorlaufwerk. Alle zu diesem Laufwerk hinzugefügten Dateien werden mit Cryptomator verschlüsselt. Um zu einem späteren Zeitpunkt auf dieses Laufwerk zuzugreifen, entsperre es einfach wieder aus der Cryptomator-Anwendung heraus.
addvault.new.readme.accessLocation.2=Dies ist dein Tresorlaufwerk. Alle zu diesem Laufwerk hinzugefügten Dateien werden von Cryptomator verschlüsselt. Um zu einem späteren Zeitpunkt auf dieses Laufwerk zuzugreifen, entsperre es einfach wieder aus der Cryptomator-Anwendung heraus.
addvault.new.readme.accessLocation.3=Diese Datei kannst du löschen.
## Existing
addvaultwizard.existing.instruction=Wähle die Datei „masterkey.cryptomator“ deines vorhandenen Tresors aus.
addvaultwizard.existing.chooseBtn=Durchsuchen…
addvaultwizard.existing.chooseBtn=Durchsuchen 
addvaultwizard.existing.filePickerTitle=Masterkey-Datei auswählen
addvaultwizard.existing.error=Der Tresor „%s“ ist kein gültiger Tresor und kann nicht hinzugefügt werden. Weitere Informationen findest du im Diagnoseprotokoll.
## Success
@@ -71,11 +73,7 @@ removeVault.confirmBtn=Tresor entfernen
# Change Password
changepassword.title=Password ändern
changepassword.enterOldPassword=Gib dein derzeitiges Passwort für "%s" ein
changepassword.enterNewPassword=Gib ein neues Tresorpasswort ein
changepassword.reenterNewPassword=Bestätige das neue Passwort
changepassword.passwordsMatch=Passwörter stimmen überein!
changepassword.passwordsDoNotMatch=Passwörter stimmen nicht überein
changepassword.enterOldPassword=Gib dein aktuelles Passwort für %s ein
changepassword.finalConfirmation=Mir ist bewusst, dass ich den Tresor ohne das Passwort NICHT mehr öffnen kann
# Forget Password
@@ -85,12 +83,14 @@ forgetPassword.confirmBtn=Passwort vergessen
# Unlock
unlock.title=Tresor entsperren
unlock.passwordPrompt=Gib das Passwort für "%s" ein:
unlock.passwordPrompt=Gib das Passwort für %s ein:
unlock.savePassword=Passwort speichern
unlock.unlockBtn=Entsperren
## Success
unlock.success.message="%s" erfolgreich entsperrt!
unlock.success.message=%s erfolgreich entsperrt! Nun kannst du auf deinen Tresor zugreifen.
unlock.success.revealBtn=Tresor anzeigen
## Invalid Mount Point
unlock.error.invalidMountPoint=Einhängepunkt ist kein leeres Verzeichnis: %s
# Migration
migration.title=Tresor aktualisieren
@@ -111,6 +111,10 @@ preferences.general=Allgemein
preferences.general.theme=Erscheinungsbild
preferences.general.startHidden=Cryptomator im Hintergrund starten
preferences.general.debugLogging=Diagnoseprotokoll aktivieren
preferences.general.autoStart=Cryptomator beim Systemstart starten
preferences.general.interfaceOrientation=Oberflächenausrichtung
preferences.general.interfaceOrientation.ltr=Von links nach rechts
preferences.general.interfaceOrientation.rtl=Von rechts nach links
## Volume
preferences.volume=Tresorlaufwerk
preferences.volume.type=Laufwerkstyp
@@ -122,10 +126,19 @@ preferences.updates.currentVersion=Aktuelle Version: %s
preferences.updates.autoUpdateCheck=Automatisch nach Updates suchen
preferences.updates.checkNowBtn=Jetzt suchen
preferences.updates.updateAvailable=Update auf Version %s verfügbar.
## Donation Key
preferences.donationKey=Spende
preferences.donationKey.registeredFor=Registriert für %s
preferences.donationKey.noDonationKey=Kein gültiger Spendenschlüssel gefunden. Er ist wie ein Lizenzschlüssel, aber für tolle Leute, die freie Software verwenden. ;-)
preferences.donationKey.getDonationKey=Einen Spendenschlüssel erhalten
# Main Window
main.closeBtn.tooltip=Schließen
main.preferencesBtn.tooltip=Einstellungen
main.donationKeyMissing.tooltip=Bitte denke über eine Spende nach
## Drag 'n' Drop
main.dropZone.dropVault=Diesen Tresor hinzufügen
main.dropZone.unknownDragboardContent=Wenn Sie einen Tresor hinzufügen möchten, ziehen Sie ihn in dieses Fenster
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Klicke hier, um einen Tresor hinzuzufügen
main.vaultlist.contextMenu.remove=Tresor entfernen
@@ -152,7 +165,7 @@ main.vaultDetail.migratePrompt=Dein Tresor muss auf ein neues Format aktualisier
# Wrong File Alert
wrongFileAlert.title=Unbekannte Datei
wrongFileAlert.btn=OK
wrongFileAlert.information=Du hast versucht, eine Datei oder einen Ordner hinzuzufügen, der scheinbar kein Cryptomator-Tresor ist. Um Dateien zu verschlüsseln, erstelle und entsperre einen Tresor.
wrongFileAlert.information=Du hast versucht, eine Datei oder einen Ordner hinzuzufügen, der anscheinend kein Cryptomator-Tresor ist. Zum Verschlüsseln deiner Daten musst du zuerst einen Tresor erstellen und diesen dann entsperren.
# Vault Options
## General
@@ -165,14 +178,25 @@ vaultOptions.mount.readonly=Schreibgeschützt
vaultOptions.mount.driveName=Laufwerksname
vaultOptions.mount.customMountFlags=Benutzerdefinierte Mount-Flags
vaultOptions.mount.winDriveLetterOccupied=belegt
vaultOptions.mount.winDirChooser=Wähle ein leeres Verzeichnis
vaultOptions.mount.mountPoint=Einhängepunkt
vaultOptions.mount.mountPoint.auto=Automatisch einen passenden Ort auswählen
vaultOptions.mount.mountPoint.driveLetter=Zugewiesenen Laufwerksbuchstaben verwenden
vaultOptions.mount.mountPoint.custom=Eigener Pfad
vaultOptions.mount.mountPoint.directoryPickerButton=Durchsuchen 
vaultOptions.mount.mountPoint.directoryPickerTitle=Wähle ein leeres Verzeichnis
# Recovery Key
recoveryKey.title=Wiederherstellungsschlüssel
recoveryKey.enterPassword.prompt=Gib dein Kennwort ein, um den Wiederherstellungsschlüssel für „%s“ anzuzeigen:
recoveryKey.display.message=Wiederherstellungsschlüssel für „%s“:
recoveryKey.enterPassword.prompt=Gib dein Passwort ein, um den Wiederherstellungsschlüssel für „%s“ anzuzeigen:
recoveryKey.display.message=Mit folgendem Wiederherstellungsschlüssel kannst du den Zugriff auf „%s“ wiederherstellen:
recoveryKey.display.StorageHints=Bewahre ihn möglichst sicher auf, z. B.\n • in einem Passwortmanager\n • auf einem USB-Speicherstick\n • auf Papier ausgedruckt
# Misc
# New Password
newPassword.promptText=Gib ein neues Passwort ein
newPassword.reenterPassword=Bestätige das neue Passwort
newPassword.passwordsMatch=Passwörter stimmen überein!
newPassword.passwordsDoNotMatch=Passwörter stimmen nicht überein
passwordStrength.messageLabel.tooShort=Verwende mindestens %d Zeichen
passwordStrength.messageLabel.0=Sehr schwach
passwordStrength.messageLabel.1=Schwach
passwordStrength.messageLabel.2=Mittel

View File

@@ -2,6 +2,7 @@
# Generics
## Button
## Error
# Tray Menu
@@ -11,7 +12,6 @@
### Name
### Location
### Password
### Recovery Key
### Information
## Existing
## Success
@@ -24,6 +24,7 @@
# Unlock
## Success
## Invalid Mount Point
# Migration
## Start
@@ -34,8 +35,10 @@
## General
## Volume
## Updates
## Donation Key
# Main Window
## Drag 'n' Drop
## Vault List
## Vault Detail
### Locked
@@ -50,6 +53,6 @@
# Recovery Key
# Misc
# New Password
# Quit

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