瀏覽代碼

Merge branch 'develop' into openjdk11

# Conflicts:
#	main/pom.xml
Sebastian Stenzel 6 年之前
父節點
當前提交
ab82874013
共有 53 個文件被更改,包括 963 次插入415 次删除
  1. 8 10
      .gitignore
  2. 1 0
      .idea/.name
  3. 51 0
      .idea/codeStyles/Project.xml
  4. 5 0
      .idea/codeStyles/codeStyleConfig.xml
  5. 34 0
      .idea/compiler.xml
  6. 11 0
      .idea/encodings.xml
  7. 10 0
      .idea/inspectionProfiles/Project_Default.xml
  8. 14 0
      .idea/misc.xml
  9. 6 0
      .idea/vcs.xml
  10. 1 1
      .travis.yml
  11. 0 1
      main/ant-kit/.gitignore
  12. 30 11
      main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java
  13. 11 7
      main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java
  14. 5 7
      main/pom.xml
  15. 13 6
      main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java
  16. 13 5
      main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java
  17. 3 1
      main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java
  18. 111 56
      main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java
  19. 59 21
      main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java
  20. 48 21
      main/ui/src/main/java/org/cryptomator/ui/model/DokanyVolume.java
  21. 75 45
      main/ui/src/main/java/org/cryptomator/ui/model/FuseVolume.java
  22. 0 5
      main/ui/src/main/java/org/cryptomator/ui/model/InvalidSettingsException.java
  23. 27 19
      main/ui/src/main/java/org/cryptomator/ui/model/Vault.java
  24. 4 4
      main/ui/src/main/java/org/cryptomator/ui/model/WindowsDriveLetters.java
  25. 40 36
      main/ui/src/main/resources/fxml/unlock.fxml
  26. 4 7
      main/ui/src/main/resources/fxml/welcome.fxml
  27. 9 5
      main/ui/src/main/resources/localization/ar.txt
  28. 9 5
      main/ui/src/main/resources/localization/bg.txt
  29. 125 0
      main/ui/src/main/resources/localization/ca.txt
  30. 8 4
      main/ui/src/main/resources/localization/cs.txt
  31. 9 5
      main/ui/src/main/resources/localization/da.txt
  32. 9 5
      main/ui/src/main/resources/localization/de.txt
  33. 10 5
      main/ui/src/main/resources/localization/en.txt
  34. 9 4
      main/ui/src/main/resources/localization/es.txt
  35. 8 4
      main/ui/src/main/resources/localization/fr.txt
  36. 10 6
      main/ui/src/main/resources/localization/hu.txt
  37. 9 5
      main/ui/src/main/resources/localization/it.txt
  38. 16 12
      main/ui/src/main/resources/localization/ja.txt
  39. 19 15
      main/ui/src/main/resources/localization/ko.txt
  40. 9 5
      main/ui/src/main/resources/localization/lv.txt
  41. 8 4
      main/ui/src/main/resources/localization/nl.txt
  42. 8 4
      main/ui/src/main/resources/localization/pl.txt
  43. 20 16
      main/ui/src/main/resources/localization/pt.txt
  44. 13 9
      main/ui/src/main/resources/localization/pt_BR.txt
  45. 9 5
      main/ui/src/main/resources/localization/ru.txt
  46. 9 5
      main/ui/src/main/resources/localization/sk.txt
  47. 9 5
      main/ui/src/main/resources/localization/th.txt
  48. 9 5
      main/ui/src/main/resources/localization/tr.txt
  49. 8 4
      main/ui/src/main/resources/localization/uk.txt
  50. 8 4
      main/ui/src/main/resources/localization/zh.txt
  51. 9 5
      main/ui/src/main/resources/localization/zh_HK.txt
  52. 9 5
      main/ui/src/main/resources/localization/zh_TW.txt
  53. 1 1
      main/ui/src/test/java/org/cryptomator/ui/l10n/LocalizationTest.java

+ 8 - 10
.gitignore

@@ -9,15 +9,13 @@
 .settings
 .project
 .classpath
-target/
-test-output/
 
-# IntelliJ Settings Files #
-.idea/
-out/
-.idea_modules/
-*.iws
-*.iml
+# Maven #
+target/
 
-# Temporary file created by test launcher
-main/launcher/.ipcPort.tmp
+# IntelliJ Settings Files (https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems) #
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
+.idea/**/libraries/
+*.iml

+ 1 - 0
.idea/.name

@@ -0,0 +1 @@
+Cryptomator

+ 51 - 0
.idea/codeStyles/Project.xml

@@ -0,0 +1,51 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <option name="OTHER_INDENT_OPTIONS">
+      <value>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </value>
+    </option>
+    <option name="LINE_SEPARATOR" value="&#10;" />
+    <option name="RIGHT_MARGIN" value="220" />
+    <option name="FORMATTER_TAGS_ENABLED" value="true" />
+    <JavaCodeStyleSettings>
+      <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="30" />
+      <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="10" />
+      <option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
+        <value />
+      </option>
+      <option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
+      <option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
+    </JavaCodeStyleSettings>
+    <codeStyleSettings language="Groovy">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="HTML">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="JAVA">
+      <option name="KEEP_LINE_BREAKS" value="false" />
+      <option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
+      <option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
+      <option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
+      <option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="JSON">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="XML">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+  </code_scheme>
+</component>

+ 5 - 0
.idea/codeStyles/codeStyleConfig.xml

@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+  </state>
+</component>

+ 34 - 0
.idea/compiler.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <annotationProcessing>
+      <profile name="Annotation profile for Cryptomator" enabled="true">
+        <sourceOutputDir name="target/generated-sources/annotations" />
+        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
+        <outputRelativeToContentRoot value="true" />
+        <processorPath useClasspath="false">
+          <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.20/dagger-compiler-2.20.jar" />
+          <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.20/dagger-2.20.jar" />
+          <entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
+          <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.20/dagger-producers-2.20.jar" />
+          <entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/25.0-jre/guava-25.0-jre.jar" />
+          <entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.jar" />
+          <entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.3/checker-compat-qual-2.5.3.jar" />
+          <entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.1.3/error_prone_annotations-2.1.3.jar" />
+          <entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar" />
+          <entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.14/animal-sniffer-annotations-1.14.jar" />
+          <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.20/dagger-spi-2.20.jar" />
+          <entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" />
+          <entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
+          <entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar" />
+          <entry name="$MAVEN_REPOSITORY$/javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar" />
+        </processorPath>
+        <module name="commons" />
+        <module name="keychain" />
+        <module name="launcher" />
+        <module name="uber-jar" />
+        <module name="ui" />
+      </profile>
+    </annotationProcessing>
+  </component>
+</project>

+ 11 - 0
.idea/encodings.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding" addBOMForNewFiles="with NO BOM">
+    <file url="file://$PROJECT_DIR$/main" 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/uber-jar" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/main/ui" charset="UTF-8" />
+  </component>
+</project>

+ 10 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,10 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
+      <option name="processCode" value="true" />
+      <option name="processLiterals" value="true" />
+      <option name="processComments" value="true" />
+    </inspection_tool>
+  </profile>
+</component>

+ 14 - 0
.idea/misc.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="MavenProjectsManager">
+    <option name="originalFiles">
+      <list>
+        <option value="$PROJECT_DIR$/main/pom.xml" />
+      </list>
+    </option>
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_10" project-jdk-name="10" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/out" />
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 1 - 1
.travis.yml

@@ -7,7 +7,7 @@ cache:
   - $HOME/.m2
 env:
   global:
-    - secure: "lV9OwUbHMrMpLUH1CY+Z4puLDdFXytudyPlG1eGRsesdpuG6KM3uQVz6uAtf6lrU8DRbMM/T7ML+PmvQ4UoPPYLdLxESLLBat2qUPOIVBOhTSlCc7I0DmGy04CSvkeMy8dPaQC0ukgNiR7zwoNzfcpGRN/U9S8tziDruuHoZSrg=" # BINTRAY_API_KEY
+    - secure: "HftEaabMmWn5GwKFKksUkOcelc3Mn7xazwAEy+4d4gL1+F8VhID/6DCK7nas+afUymWnxTano8Rv4Ci5MWryNkNkTH+FUPWmF3xWezc3hajSyS7RB92IZ8VPetl4Fo8UI1WwM5apDEaugalPxkIf8a7N+lpG5X/Gpumwzo3Be3w=" # BINTRAY_API_KEY
     - secure: "oWFgRTVP6lyTa7qVxlvkpm20MtVc3BtmsNXQJS6bfg2A0o/iCQMNx7OD59BaafCLGRKvCcJVESiC8FlSylVMS7CDSyYu0gg70NUiIuHp4NBM5inFWYCy/PdQsCTzr5uvNG+rMFQpMFRaCV0FrfM3tLondcVkhsHL68l93Xoexx4=" # CODACY_PROJECT_TOKEN
     - secure: "zJxgytA2Ks5Xzv+7kUaUq+EBFNQw9Qec63lcMJVuXVWczjL16nKW1EzzV515ag+OWL46z3lEPForDhufw0VtFnNmaX68jkO0mp01eLrHApc1llN2Y/U8GBXfNNazN4+Kom4H+z/AO+wJr8EsKMMUczCdQ3APgd9uVI0hzXw/Z3M=" # GITHUB_API_KEY
 addons:

+ 0 - 1
main/ant-kit/.gitignore

@@ -1 +0,0 @@
-/target/

+ 30 - 11
main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java

@@ -5,16 +5,7 @@
  *******************************************************************************/
 package org.cryptomator.common.settings;
 
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Path;
-import java.util.Base64;
-import java.util.Objects;
-import java.util.UUID;
-
-import org.apache.commons.lang3.StringUtils;
-import org.fxmisc.easybind.EasyBind;
-
+import com.google.common.base.Strings;
 import javafx.beans.Observable;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.ObjectProperty;
@@ -22,12 +13,27 @@ import javafx.beans.property.SimpleBooleanProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.property.SimpleStringProperty;
 import javafx.beans.property.StringProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.fxmisc.easybind.EasyBind;
 
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * The settings specific to a single vault.
+ * TODO: Change the name of individualMountPath and its derivatives to customMountPath
+ */
 public class VaultSettings {
 
 	public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false;
 	public static final boolean DEFAULT_REAVEAL_AFTER_MOUNT = true;
 	public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
+	public static final boolean DEFAULT_USES_READONLY_MODE = false;
 
 	private final String id;
 	private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
@@ -37,6 +43,7 @@ public class VaultSettings {
 	private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REAVEAL_AFTER_MOUNT);
 	private final BooleanProperty usesIndividualMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
 	private final StringProperty individualMountPath = new SimpleStringProperty();
+	private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
 
 	public VaultSettings(String id) {
 		this.id = Objects.requireNonNull(id);
@@ -45,7 +52,7 @@ public class VaultSettings {
 	}
 
 	Observable[] observables() {
-		return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath};
+		return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath, usesReadOnlyMode};
 	}
 
 	private void deriveMountNameFromPath(Path path) {
@@ -128,6 +135,18 @@ public class VaultSettings {
 		return individualMountPath;
 	}
 
+	public Optional<String> getIndividualMountPath() {
+		if (usesIndividualMountPath.get()) {
+			return Optional.ofNullable(Strings.emptyToNull(individualMountPath.get()));
+		} else {
+			return Optional.empty();
+		}
+	}
+
+	public BooleanProperty usesReadOnlyMode() {
+		return usesReadOnlyMode;
+	}
+
 	/* Hashcode/Equals */
 
 	@Override

+ 11 - 7
main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java

@@ -5,14 +5,13 @@
  *******************************************************************************/
 package org.cryptomator.common.settings;
 
-import java.io.IOException;
-import java.nio.file.Paths;
-
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.nio.file.Paths;
 
 class VaultSettingsJsonAdapter {
 
@@ -27,8 +26,8 @@ class VaultSettingsJsonAdapter {
 		out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
 		out.name("revealAfterMount").value(value.revealAfterMount().get());
 		out.name("usesIndividualMountPath").value(value.usesIndividualMountPath().get());
-		//TODO: should this always be written? ( because it could contain metadata, which the user does not want to save!)
-		out.name("individualMountPath").value(value.individualMountPath().get());
+		out.name("individualMountPath").value(value.individualMountPath().get());    //TODO: should this always be written? ( because it could contain metadata, which the user may not want to save!)
+		out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
 		out.endObject();
 	}
 
@@ -41,6 +40,7 @@ class VaultSettingsJsonAdapter {
 		boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
 		boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT;
 		boolean usesIndividualMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
+		boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
 
 		in.beginObject();
 		while (in.hasNext()) {
@@ -70,6 +70,9 @@ class VaultSettingsJsonAdapter {
 				case "individualMountPath":
 					individualMountPath = in.nextString();
 					break;
+				case "usesReadOnlyMode":
+					usesReadOnlyMode = in.nextBoolean();
+					break;
 				default:
 					LOG.warn("Unsupported vault setting found in JSON: " + name);
 					in.skipValue();
@@ -85,6 +88,7 @@ class VaultSettingsJsonAdapter {
 		vaultSettings.revealAfterMount().set(revealAfterMount);
 		vaultSettings.usesIndividualMountPath().set(usesIndividualMountPath);
 		vaultSettings.individualMountPath().set(individualMountPath);
+		vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
 		return vaultSettings;
 	}
 

+ 5 - 7
main/pom.xml

@@ -25,11 +25,11 @@
 
 		<!-- dependency versions -->
 		<cryptomator.cryptolib.version>1.2.1</cryptomator.cryptolib.version>
-		<cryptomator.cryptofs.version>1.6.1</cryptomator.cryptofs.version>
+		<cryptomator.cryptofs.version>1.7.0</cryptomator.cryptofs.version>
 		<cryptomator.jni.version>2.0.0</cryptomator.jni.version>
-		<cryptomator.fuse.version>1.0.2</cryptomator.fuse.version>
-		<cryptomator.dokany.version>1.0.0</cryptomator.dokany.version>
-		<cryptomator.webdav.version>1.0.5</cryptomator.webdav.version>
+		<cryptomator.fuse.version>1.1.0</cryptomator.fuse.version>
+		<cryptomator.dokany.version>1.1.3</cryptomator.dokany.version>
+		<cryptomator.webdav.version>1.0.6</cryptomator.webdav.version>
 
 		<javafx.version>11</javafx.version>
 
@@ -39,7 +39,7 @@
 		<easybind.version>1.0.3</easybind.version>
 
 		<guava.version>27.0-jre</guava.version>
-		<dagger.version>2.19</dagger.version>
+		<dagger.version>2.20</dagger.version>
 		<gson.version>2.8.5</gson.version>
 
 		<slf4j.version>1.7.25</slf4j.version>
@@ -331,8 +331,6 @@
 				<artifactId>maven-compiler-plugin</artifactId>
 				<version>3.8.0</version>
 				<configuration>
-					<source>11</source>
-					<target>11</target>
 					<release>11</release>
 					<annotationProcessorPaths>
 						<path>

+ 13 - 6
main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java

@@ -16,6 +16,7 @@ import java.util.Optional;
 
 import javax.inject.Inject;
 
+import javafx.beans.Observable;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
 import org.cryptomator.ui.controls.SecPasswordField;
@@ -48,7 +49,7 @@ public class ChangePasswordController implements ViewController {
 	private final Application app;
 	private final PasswordStrengthUtil strengthRater;
 	private final Localization localization;
-	private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4
+	private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); // 0-4
 	private Optional<ChangePasswordListener> listener = Optional.empty();
 	private Vault vault;
 
@@ -100,11 +101,9 @@ public class ChangePasswordController implements ViewController {
 
 	@Override
 	public void initialize() {
-		BooleanBinding oldPasswordIsEmpty = oldPasswordField.textProperty().isEmpty();
-		BooleanBinding newPasswordIsEmpty = newPasswordField.textProperty().isEmpty();
-		BooleanBinding passwordsDiffer = newPasswordField.textProperty().isNotEqualTo(retypePasswordField.textProperty());
-		changePasswordButton.disableProperty().bind(oldPasswordIsEmpty.or(newPasswordIsEmpty.or(passwordsDiffer)));
-		passwordStrength.bind(EasyBind.map(newPasswordField.textProperty(), strengthRater::computeRate));
+		oldPasswordField.textProperty().addListener(this::passwordsChanged);
+		newPasswordField.textProperty().addListener(this::passwordsChanged);
+		retypePasswordField.textProperty().addListener(this::passwordsChanged);
 
 		passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor));
 		passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor));
@@ -114,6 +113,14 @@ public class ChangePasswordController implements ViewController {
 		passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
 	}
 
+	private void passwordsChanged(Observable observable) {
+		boolean oldPasswordEmpty = oldPasswordField.getCharacters().length() == 0;
+		boolean newPasswordEmpty = newPasswordField.getCharacters().length() == 0;
+		boolean passwordsEqual = newPasswordField.getCharacters().equals(retypePasswordField.getCharacters());
+		changePasswordButton.setDisable(oldPasswordEmpty || newPasswordEmpty || !passwordsEqual);
+		passwordStrength.set(strengthRater.computeRate(newPasswordField.getCharacters().toString()));
+	}
+
 	@Override
 	public Parent getRoot() {
 		return root;

+ 13 - 5
main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java

@@ -16,6 +16,9 @@ import java.util.Optional;
 
 import javax.inject.Inject;
 
+import javafx.beans.Observable;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.value.ObservableIntegerValue;
 import org.cryptomator.ui.controls.SecPasswordField;
 import org.cryptomator.ui.l10n.Localization;
 import org.cryptomator.ui.model.Vault;
@@ -42,7 +45,7 @@ public class InitializeController implements ViewController {
 
 	private final Localization localization;
 	private final PasswordStrengthUtil strengthRater;
-	private ObservableValue<Integer> passwordStrength; // 0-4
+	private IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); // strengths: 0-4
 	private Optional<InitializationListener> listener = Optional.empty();
 	private Vault vault;
 
@@ -87,10 +90,8 @@ public class InitializeController implements ViewController {
 
 	@Override
 	public void initialize() {
-		BooleanBinding passwordIsEmpty = passwordField.textProperty().isEmpty();
-		BooleanBinding passwordsDiffer = passwordField.textProperty().isNotEqualTo(retypePasswordField.textProperty());
-		okButton.disableProperty().bind(passwordIsEmpty.or(passwordsDiffer));
-		passwordStrength = EasyBind.map(passwordField.textProperty(), strengthRater::computeRate);
+		passwordField.textProperty().addListener(this::passwordsChanged);
+		retypePasswordField.textProperty().addListener(this::passwordsChanged);
 
 		passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor));
 		passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor));
@@ -100,6 +101,13 @@ public class InitializeController implements ViewController {
 		passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
 	}
 
+	private void passwordsChanged(Observable observable) {
+		boolean passwordsEmpty = passwordField.getCharacters().length() == 0;
+		boolean passwordsEqual = passwordField.getCharacters().equals(retypePasswordField.getCharacters());
+		okButton.setDisable(passwordsEmpty || !passwordsEqual);
+		passwordStrength.set(strengthRater.computeRate(passwordField.getCharacters().toString()));
+	}
+
 	@Override
 	public Parent getRoot() {
 		return root;

+ 3 - 1
main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java

@@ -222,7 +222,7 @@ public class MainController implements ViewController {
 			ButtonType forceShutdownButtonType = new ButtonType(localization.getString("main.gracefulShutdown.button.forceShutdown"));
 			Alert gracefulShutdownDialog = DialogBuilderUtil.buildGracefulShutdownDialog(
 					localization.getString("main.gracefulShutdown.dialog.title"), localization.getString("main.gracefulShutdown.dialog.header"), localization.getString("main.gracefulShutdown.dialog.content"),
-					forceShutdownButtonType, forceShutdownButtonType, tryAgainButtonType);
+					forceShutdownButtonType, ButtonType.CANCEL, forceShutdownButtonType, tryAgainButtonType);
 
 			Optional<ButtonType> choice = gracefulShutdownDialog.showAndWait();
 			choice.ifPresent(btnType -> {
@@ -230,6 +230,8 @@ public class MainController implements ViewController {
 					gracefulShutdown();
 				} else if (forceShutdownButtonType.equals(btnType)) {
 					Platform.runLater(Platform::exit);
+				} else {
+					return;
 				}
 			});
 		} else {

+ 111 - 56
main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java

@@ -8,13 +8,6 @@
  ******************************************************************************/
 package org.cryptomator.ui.controllers;
 
-import javax.inject.Inject;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.concurrent.ExecutorService;
-
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import javafx.application.Application;
@@ -34,7 +27,10 @@ import javafx.scene.control.ProgressIndicator;
 import javafx.scene.control.TextField;
 import javafx.scene.input.KeyEvent;
 import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
 import javafx.scene.text.Text;
+import javafx.stage.DirectoryChooser;
+import javafx.stage.Stage;
 import javafx.util.StringConverter;
 import org.apache.commons.lang3.CharUtils;
 import org.apache.commons.lang3.SystemUtils;
@@ -43,9 +39,7 @@ import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.common.settings.VolumeImpl;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
-import org.cryptomator.frontend.webdav.ServerLifecycleException;
 import org.cryptomator.keychain.KeychainAccess;
-import org.cryptomator.ui.model.InvalidSettingsException;
 import org.cryptomator.ui.controls.SecPasswordField;
 import org.cryptomator.ui.l10n.Localization;
 import org.cryptomator.ui.model.Vault;
@@ -57,6 +51,19 @@ import org.fxmisc.easybind.Subscription;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.inject.Inject;
+import javax.inject.Named;
+import java.io.File;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+
 public class UnlockController implements ViewController {
 
 	private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
@@ -67,6 +74,7 @@ public class UnlockController implements ViewController {
 			.precomputed();
 
 	private final Application app;
+	private final Stage mainWindow;
 	private final Localization localization;
 	private final WindowsDriveLetters driveLetters;
 	private final ChangeListener<Character> driveLetterChangeListener = this::winDriveLetterDidChange;
@@ -78,8 +86,9 @@ public class UnlockController implements ViewController {
 	private Subscription vaultSubs = Subscription.EMPTY;
 
 	@Inject
-	public UnlockController(Application app, Localization localization, WindowsDriveLetters driveLetters, Optional<KeychainAccess> keychainAccess, Settings settings, ExecutorService executor) {
+	public UnlockController(Application app, @Named("mainWindow") Stage mainWindow, Localization localization, WindowsDriveLetters driveLetters, Optional<KeychainAccess> keychainAccess, Settings settings, ExecutorService executor) {
 		this.app = app;
+		this.mainWindow = mainWindow;
 		this.localization = localization;
 		this.driveLetters = driveLetters;
 		this.keychainAccess = keychainAccess;
@@ -115,13 +124,13 @@ public class UnlockController implements ViewController {
 	private ChoiceBox<Character> winDriveLetter;
 
 	@FXML
-	private CheckBox useOwnMountPath;
+	private CheckBox useCustomMountPoint;
 
 	@FXML
-	private Label mountPathLabel;
+	private HBox customMountPoint;
 
 	@FXML
-	private TextField mountPath;
+	private Label customMountPointLabel;
 
 	@FXML
 	private ProgressIndicator progressIndicator;
@@ -141,6 +150,9 @@ public class UnlockController implements ViewController {
 	@FXML
 	private CheckBox unlockAfterStartup;
 
+	@FXML
+	private CheckBox useReadOnlyMode;
+
 	@Override
 	public void initialize() {
 		advancedOptions.managedProperty().bind(advancedOptions.visibleProperty());
@@ -150,28 +162,15 @@ public class UnlockController implements ViewController {
 		savePassword.setDisable(!keychainAccess.isPresent());
 		unlockAfterStartup.disableProperty().bind(savePassword.disabledProperty().or(savePassword.selectedProperty().not()));
 
-		mountPathLabel.visibleProperty().bind(useOwnMountPath.selectedProperty());
-		mountPath.visibleProperty().bind(useOwnMountPath.selectedProperty());
-		mountPath.managedProperty().bind(useOwnMountPath.selectedProperty());
-		mountPath.textProperty().addListener(this::mountPathDidChange);
+		customMountPoint.visibleProperty().bind(useCustomMountPoint.selectedProperty());
+		customMountPoint.managedProperty().bind(useCustomMountPoint.selectedProperty());
+		winDriveLetter.setConverter(new WinDriveLetterLabelConverter());
 
-		if (SystemUtils.IS_OS_WINDOWS) {
-			winDriveLetter.setConverter(new WinDriveLetterLabelConverter());
-			useOwnMountPath.setVisible(false);
-			useOwnMountPath.setManaged(false);
-			mountPathLabel.setManaged(false);
-			//dirty cheat
-			mountPath.setMouseTransparent(true);
-		} else {
+		if (!SystemUtils.IS_OS_WINDOWS) {
 			winDriveLetterLabel.setVisible(false);
 			winDriveLetterLabel.setManaged(false);
 			winDriveLetter.setVisible(false);
 			winDriveLetter.setManaged(false);
-			if (VolumeImpl.WEBDAV.equals(settings.preferredVolumeImpl().get())) {
-				useOwnMountPath.setVisible(false);
-				useOwnMountPath.setManaged(false);
-				mountPathLabel.setManaged(false);
-			}
 		}
 	}
 
@@ -210,20 +209,18 @@ public class UnlockController implements ViewController {
 			winDriveLetter.getItems().addAll(driveLetters.getAvailableDriveLetters());
 			winDriveLetter.getItems().sort(new WinDriveLetterComparator());
 			winDriveLetter.valueProperty().addListener(driveLetterChangeListener);
+			chooseSelectedDriveLetter();
 		}
 		downloadsPageLink.setVisible(false);
 		messageText.setText(null);
 		mountName.setText(vault.getMountName());
-		if (SystemUtils.IS_OS_WINDOWS) {
-			chooseSelectedDriveLetter();
-		}
 		savePassword.setSelected(false);
 		// auto-fill pw from keychain:
 		if (keychainAccess.isPresent()) {
 			char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId());
 			if (storedPw != null) {
 				savePassword.setSelected(true);
-				passwordField.setText(new String(storedPw));
+				passwordField.setPassword(storedPw);
 				passwordField.selectRange(storedPw.length, storedPw.length);
 				Arrays.fill(storedPw, ' ');
 			}
@@ -231,15 +228,59 @@ public class UnlockController implements ViewController {
 		VaultSettings vaultSettings = vault.getVaultSettings();
 		unlockAfterStartup.setSelected(savePassword.isSelected() && vaultSettings.unlockAfterStartup().get());
 		revealAfterMount.setSelected(vaultSettings.revealAfterMount().get());
-		useOwnMountPath.setSelected(vaultSettings.usesIndividualMountPath().get());
 
-		vaultSubs = vaultSubs.and(EasyBind.subscribe(unlockAfterStartup.selectedProperty(), vaultSettings.unlockAfterStartup()::set));
-		vaultSubs = vaultSubs.and(EasyBind.subscribe(revealAfterMount.selectedProperty(), vaultSettings.revealAfterMount()::set));
-		vaultSubs = vaultSubs.and(EasyBind.subscribe(useOwnMountPath.selectedProperty(), vaultSettings.usesIndividualMountPath()::set));
+		// WEBDAV-dependent controls:
+		if (VolumeImpl.WEBDAV.equals(settings.preferredVolumeImpl().get())) {
+			useCustomMountPoint.setVisible(false);
+			useCustomMountPoint.setManaged(false);
+		} else {
+			useCustomMountPoint.setVisible(true);
+			useCustomMountPoint.setSelected(vaultSettings.usesIndividualMountPath().get());
+			if (Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
+				customMountPointLabel.setText(localization.getString("unlock.label.chooseMountPath"));
+			} else {
+				customMountPointLabel.setText(displayablePath(vaultSettings.individualMountPath().getValueSafe()));
+			}
+		}
 
+		// DOKANY-dependent controls:
+		if (VolumeImpl.DOKANY.equals(settings.preferredVolumeImpl().get())) {
+			winDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
+			winDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
+			winDriveLetterLabel.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
+			winDriveLetterLabel.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
+			// readonly not yet supported by dokany
+			useReadOnlyMode.setSelected(false);
+			useReadOnlyMode.setVisible(false);
+			useReadOnlyMode.setManaged(false);
+		} else {
+			useReadOnlyMode.setSelected(vaultSettings.usesReadOnlyMode().get());
+		}
 
-		mountPath.textProperty().setValue(vaultSettings.individualMountPath().getValueSafe());
+		// OS-dependent controls:
+		if (SystemUtils.IS_OS_WINDOWS) {
+			winDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
+			winDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
+			winDriveLetterLabel.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
+			winDriveLetterLabel.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
+		}
 
+		vaultSubs = vaultSubs.and(EasyBind.subscribe(unlockAfterStartup.selectedProperty(), vaultSettings.unlockAfterStartup()::set));
+		vaultSubs = vaultSubs.and(EasyBind.subscribe(revealAfterMount.selectedProperty(), vaultSettings.revealAfterMount()::set));
+		vaultSubs = vaultSubs.and(EasyBind.subscribe(useCustomMountPoint.selectedProperty(), vaultSettings.usesIndividualMountPath()::set));
+		vaultSubs = vaultSubs.and(EasyBind.subscribe(useReadOnlyMode.selectedProperty(), vaultSettings.usesReadOnlyMode()::set));
+	}
+
+	private String displayablePath(String path) {
+		Path homeDir = Paths.get(SystemUtils.USER_HOME);
+		Path p = Paths.get(path);
+		if (p.startsWith(homeDir)) {
+			Path relativePath = homeDir.relativize(p);
+			String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
+			return homePrefix + relativePath.toString();
+		} else {
+			return p.toString();
+		}
 	}
 
 	// ****************************************
@@ -281,8 +322,13 @@ public class UnlockController implements ViewController {
 		}
 	}
 
-	private void mountPathDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
-		vault.setIndividualMountPath(newValue);
+	public void didClickChooseCustomMountPoint(ActionEvent actionEvent) {
+		DirectoryChooser dirChooser = new DirectoryChooser();
+		File file = dirChooser.showDialog(mainWindow);
+		if (file != null) {
+			customMountPointLabel.setText(displayablePath(file.toString()));
+			vault.setCustomMountPath(file.toString());
+		}
 	}
 
 	/**
@@ -295,7 +341,7 @@ public class UnlockController implements ViewController {
 			if (letter == null) {
 				return localization.getString("unlock.choicebox.winDriveLetter.auto");
 			} else {
-				return Character.toString(letter) + ":";
+				return letter + ":";
 			}
 		}
 
@@ -389,6 +435,7 @@ public class UnlockController implements ViewController {
 
 		CharSequence password = passwordField.getCharacters();
 		Tasks.create(() -> {
+			messageText.setText(localization.getString("unlock.pendingMessage.unlocking"));
 			vault.unlock(password);
 			if (keychainAccess.isPresent() && savePassword.isSelected()) {
 				keychainAccess.get().storePassphrase(vault.getId(), password);
@@ -397,10 +444,6 @@ public class UnlockController implements ViewController {
 			messageText.setText(null);
 			downloadsPageLink.setVisible(false);
 			listener.ifPresent(lstnr -> lstnr.didUnlock(vault));
-		}).onError(InvalidSettingsException.class, e -> {
-			messageText.setText(localization.getString("unlock.errorMessage.invalidMountPath"));
-			advancedOptions.setVisible(true);
-			mountPath.setStyle("-fx-border-color: red;");
 		}).onError(InvalidPassphraseException.class, e -> {
 			messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
 			passwordField.selectAll();
@@ -416,10 +459,17 @@ public class UnlockController implements ViewController {
 			} else if (e.getDetectedVersion() == Integer.MAX_VALUE) {
 				messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
 			}
-		}).onError(ServerLifecycleException.class, e -> {
-			LOG.error("Unlock failed for technical reasons.", e);
-			messageText.setText(localization.getString("unlock.errorMessage.unlockFailed"));
-		}).onError(Exception.class, e -> {
+		}).onError(NotDirectoryException.class, e -> {
+			LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage());
+			advancedOptions.setVisible(true);
+			messageText.setText(null);
+			showUnlockFailedErrorDialog("unlock.failedDialog.content.mountPathNonExisting");
+		}).onError(DirectoryNotEmptyException.class, e -> {
+			LOG.error("Unlock failed. Mount point not empty: {}", e.getMessage());
+			advancedOptions.setVisible(true);
+			messageText.setText(null);
+			showUnlockFailedErrorDialog("unlock.failedDialog.content.mountPathNotEmpty");
+		}).onError(Exception.class, e -> { // including RuntimeExceptions
 			LOG.error("Unlock failed for technical reasons.", e);
 			messageText.setText(localization.getString("unlock.errorMessage.unlockFailed"));
 		}).andFinally(() -> {
@@ -428,12 +478,17 @@ public class UnlockController implements ViewController {
 			}
 			advancedOptions.setDisable(false);
 			progressIndicator.setVisible(false);
-			if (advancedOptions.isVisible()) { //dirty programming, but otherwise the focus is wrong
-				mountPath.requestFocus();
-			}
 		}).runOnce(executor);
 	}
 
+	private void showUnlockFailedErrorDialog(String localizableContentKey) {
+		String title = localization.getString("unlock.failedDialog.title");
+		String header = localization.getString("unlock.failedDialog.header");
+		String content = localization.getString(localizableContentKey);
+		Alert alert = DialogBuilderUtil.buildErrorDialog(title, header, content, ButtonType.OK);
+		alert.show();
+	}
+
 	/* callback */
 
 	public void setListener(UnlockListener listener) {
@@ -449,9 +504,9 @@ public class UnlockController implements ViewController {
 	/* state */
 
 	public enum State {
-		UNLOCKING(null),
-		INITIALIZED("unlock.successLabel.vaultCreated"),
-		PASSWORD_CHANGED("unlock.successLabel.passwordChanged"),
+		UNLOCKING(null), //
+		INITIALIZED("unlock.successLabel.vaultCreated"), //
+		PASSWORD_CHANGED("unlock.successLabel.passwordChanged"), //
 		UPGRADED("unlock.successLabel.upgraded");
 
 		private Optional<String> successMessage;

+ 59 - 21
main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java

@@ -2,25 +2,35 @@
  * Copyright (c) 2014, 2017 Sebastian Stenzel
  * All rights reserved.
  * This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
- * 
+ *
  * Contributors:
  *     Sebastian Stenzel - initial API and implementation
  ******************************************************************************/
 package org.cryptomator.ui.controls;
 
-import java.util.Arrays;
-
+import com.google.common.base.Strings;
 import javafx.scene.control.PasswordField;
 import javafx.scene.input.DragEvent;
 import javafx.scene.input.Dragboard;
 import javafx.scene.input.TransferMode;
 
+import java.nio.CharBuffer;
+import java.util.Arrays;
+
 /**
- * Compromise in security. While the text can be swiped, any access to the {@link #getText()} method will create a copy of the String in the heap.
+ * Patched PasswordField that doesn't create String copies of the password in memory. Instead the password is stored in a char[] that can be swiped.
+ *
+ * @implNote Since {@link #setText(String)} is final, we can not override its behaviour. For that reason you should not use the {@link #textProperty()} for anything else than display purposes.
  */
 public class SecPasswordField extends PasswordField {
 
 	private static final char SWIPE_CHAR = ' ';
+	private static final int INITIAL_BUFFER_SIZE = 50;
+	private static final int GROW_BUFFER_SIZE = 50;
+	private static final String PLACEHOLDER = "*";
+
+	private char[] content = new char[INITIAL_BUFFER_SIZE];
+	private int length = 0;
 
 	public SecPasswordField() {
 		this.onDragOverProperty().set(this::handleDragOver);
@@ -43,26 +53,54 @@ public class SecPasswordField extends PasswordField {
 		event.consume();
 	}
 
+	@Override
+	public void replaceText(int start, int end, String text) {
+		int removed = end - start;
+		int added = text.length();
+		this.length += added - removed;
+		growContentIfNeeded();
+		text.getChars(0, text.length(), content, start);
+
+		String placeholderString = Strings.repeat(PLACEHOLDER, text.length());
+		super.replaceText(start, end, placeholderString);
+	}
+
+	private void growContentIfNeeded() {
+		if (this.length > content.length) {
+			char[] newContent = new char[content.length + GROW_BUFFER_SIZE];
+			System.arraycopy(content, 0, newContent, 0, content.length);
+			swipe();
+			this.content = newContent;
+		}
+	}
+
+	/**
+	 * Creates a CharSequence by wrapping the password characters.
+	 *
+	 * @return A character sequence backed by the SecPasswordField's buffer (not a copy).
+	 * @implNote The CharSequence will not copy the backing char[].
+	 * Therefore any mutation to the SecPasswordField's content will mutate or eventually swipe the returned CharSequence.
+	 * @see #swipe()
+	 */
+	@Override
+	public CharSequence getCharacters() {
+		return CharBuffer.wrap(content, 0, length);
+	}
+
+	public void setPassword(char[] password) {
+		swipe();
+		content = Arrays.copyOf(password, password.length);
+		length = password.length;
+
+		String placeholderString = Strings.repeat(PLACEHOLDER, password.length);
+		setText(placeholderString);
+	}
+
 	/**
-	 * {@link #getContent()} uses a StringBuilder, which in turn is backed by a char[].
-	 * The delete operation of AbstractStringBuilder closes the gap, that forms by deleting chars, by moving up the following chars.
-	 * <br/>
-	 * Imagine the following example with <code>pass</code> being the password, <code>x</code> being the swipe char and <code>'</code> being the offset of the char array:
-	 * <ol>
-	 * <li>Append filling chars to the end of the password: <code>passxxxx'</code></li>
-	 * <li>Delete first 4 chars. Internal implementation will then copy subsequent chars to the position, where the deletion occured: <code>xxxx'xxxx</code></li>
-	 * <li>Delete first 4 chars again, as we appended 4 chars in step 1: <code>'xxxxxx</code></li>
-	 * </ol>
+	 * Destroys the stored password by overriding each character with a different character.
 	 */
 	public void swipe() {
-		final int pwLength = this.getContent().length();
-		final char[] fillingChars = new char[pwLength];
-		Arrays.fill(fillingChars, SWIPE_CHAR);
-		this.getContent().insert(pwLength, new String(fillingChars), false);
-		this.getContent().delete(0, pwLength, true);
-		this.getContent().delete(0, pwLength, true);
-		// previous text has now been overwritten. but we still need to update the text to trigger some property bindings:
-		this.clear();
+		Arrays.fill(content, SWIPE_CHAR);
 	}
 
 }

+ 48 - 21
main/ui/src/main/java/org/cryptomator/ui/model/DokanyVolume.java

@@ -1,19 +1,29 @@
 package org.cryptomator.ui.model;
 
-import javax.inject.Inject;
-import java.nio.file.Paths;
-import java.util.Set;
-import java.util.concurrent.ExecutorService;
-
-import com.google.common.collect.Sets;
+import com.google.common.base.Strings;
 import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.cryptofs.CryptoFileSystem;
 import org.cryptomator.frontend.dokany.Mount;
 import org.cryptomator.frontend.dokany.MountFactory;
 import org.cryptomator.frontend.dokany.MountFailedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
 
 public class DokanyVolume implements Volume {
 
+	private static final Logger LOG = LoggerFactory.getLogger(DokanyVolume.class);
+
 	private static final String FS_TYPE_NAME = "Cryptomator File System";
 
 	private final VaultSettings vaultSettings;
@@ -28,34 +38,51 @@ public class DokanyVolume implements Volume {
 		this.windowsDriveLetters = windowsDriveLetters;
 	}
 
-
 	@Override
 	public boolean isSupported() {
 		return DokanyVolume.isSupportedStatic();
 	}
 
-	//TODO: Drive letter 'A' as mount point is invalid in dokany. maybe we should do already here something against it
 	@Override
-	public void mount(CryptoFileSystem fs) throws VolumeException {
-		char driveLetter;
-		if (!vaultSettings.winDriveLetter().getValueSafe().equals("")) {
-			driveLetter = vaultSettings.winDriveLetter().get().charAt(0);
+	public void mount(CryptoFileSystem fs) throws VolumeException, IOException {
+		Path mountPath = getMountPoint();
+		String mountName = vaultSettings.mountName().get();
+		try {
+			this.mount = mountFactory.mount(fs.getPath("/"), mountPath, mountName, FS_TYPE_NAME);
+		} catch (MountFailedException e) {
+			if (vaultSettings.getIndividualMountPath().isPresent()) {
+				LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPath);
+			}
+			throw new VolumeException("Unable to mount Filesystem", e);
+		}
+	}
+
+	private Path getMountPoint() throws VolumeException, IOException {
+		Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
+		if (optionalCustomMountPoint.isPresent()) {
+			Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
+			checkProvidedMountPoint(customMountPoint);
+			return customMountPoint;
+		} else if (!Strings.isNullOrEmpty(vaultSettings.winDriveLetter().get())) {
+			return Paths.get(vaultSettings.winDriveLetter().get().charAt(0) + ":\\");
 		} else {
 			//auto assign drive letter
 			if (!windowsDriveLetters.getAvailableDriveLetters().isEmpty()) {
-				//this is a temporary fix for 'A' being an invalid drive letter
-				Set<Character> availableLettersWithoutA = Sets.difference(windowsDriveLetters.getAvailableDriveLetters(), Set.of('A'));
-				driveLetter = availableLettersWithoutA.iterator().next();
-//				driveLetter = windowsDriveLetters.getAvailableDriveLetters().iterator().next();
+				return Paths.get(windowsDriveLetters.getAvailableDriveLetters().iterator().next() + ":\\");
 			} else {
 				throw new VolumeException("No free drive letter available.");
 			}
 		}
-		String mountName = vaultSettings.mountName().get();
-		try {
-			this.mount = mountFactory.mount(fs.getPath("/"), Paths.get(driveLetter + ":\\") , mountName, FS_TYPE_NAME);
-		} catch (MountFailedException e) {
-			throw new VolumeException("Unable to mount Filesystem", e);
+	}
+
+	private void checkProvidedMountPoint(Path mountPoint) throws IOException {
+		if (!Files.isDirectory(mountPoint)) {
+			throw new NotDirectoryException(mountPoint.toString());
+		}
+		try (DirectoryStream<Path> ds = Files.newDirectoryStream(mountPoint)) {
+			if (ds.iterator().hasNext()) {
+				throw new DirectoryNotEmptyException(mountPoint.toString());
+			}
 		}
 	}
 

+ 75 - 45
main/ui/src/main/java/org/cryptomator/ui/model/FuseVolume.java

@@ -1,14 +1,5 @@
 package org.cryptomator.ui.model;
 
-import java.io.IOException;
-import java.nio.file.DirectoryNotEmptyException;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-import javax.inject.Inject;
-
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.cryptofs.CryptoFileSystem;
@@ -20,65 +11,86 @@ import org.cryptomator.frontend.fuse.mount.Mount;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.inject.Inject;
+import java.io.IOException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+
 public class FuseVolume implements Volume {
 
 	private static final Logger LOG = LoggerFactory.getLogger(FuseVolume.class);
 
-	/**
-	 * TODO: dont use fixed Strings and rather set them in some system environment variables in the cryptomator installer and load those!
-	 */
+	// TODO: dont use fixed Strings and rather set them in some system environment variables in the cryptomator installer and load those!
 	private static final String DEFAULT_MOUNTROOTPATH_MAC = System.getProperty("user.home") + "/Library/Application Support/Cryptomator";
 	private static final String DEFAULT_MOUNTROOTPATH_LINUX = System.getProperty("user.home") + "/.Cryptomator";
+	private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
 
 	private final VaultSettings vaultSettings;
 
 	private Mount fuseMnt;
-	private Path mountPath;
-	private boolean extraDirCreated;
+	private Path mountPoint;
+	private boolean createdTemporaryMountPoint;
 
 	@Inject
 	public FuseVolume(VaultSettings vaultSettings) {
 		this.vaultSettings = vaultSettings;
-		this.extraDirCreated = false;
 	}
 
 	@Override
 	public void mount(CryptoFileSystem fs) throws IOException, FuseNotSupportedException, VolumeException {
-		String mountPath;
-		if (vaultSettings.usesIndividualMountPath().get()) {
-			//specific path given
-			mountPath = vaultSettings.individualMountPath().get();
+		Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
+		if (optionalCustomMountPoint.isPresent()) {
+			Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
+			checkProvidedMountPoint(customMountPoint);
+			this.mountPoint = customMountPoint;
+			this.createdTemporaryMountPoint = false;
+			LOG.debug("Successfully checked custom mount point: {}", mountPoint);
 		} else {
-			//choose default path & create extra directory
-			mountPath = createDirIfNotExist(SystemUtils.IS_OS_MAC ? DEFAULT_MOUNTROOTPATH_MAC : DEFAULT_MOUNTROOTPATH_LINUX, vaultSettings.mountName().get());
-			extraDirCreated = true;
+			this.mountPoint = createTemporaryMountPoint();
+			this.createdTemporaryMountPoint = true;
+			LOG.debug("Successfully created mount point: {}", mountPoint);
 		}
-		this.mountPath = Paths.get(mountPath).toAbsolutePath();
 		mount(fs.getPath("/"));
 	}
 
-	private String createDirIfNotExist(String prefix, String dirName) throws IOException {
-		Path p = Paths.get(prefix, dirName + vaultSettings.getId());
-		if (Files.isDirectory(p)) {
-			try (DirectoryStream<Path> emptyCheck = Files.newDirectoryStream(p)) {
-				if (emptyCheck.iterator().hasNext()) {
-					throw new DirectoryNotEmptyException("Mount point is not empty.");
-				} else {
-					LOG.info("Directory already exists and is empty. Using it as mount point.");
-					return p.toString();
-				}
+	private void checkProvidedMountPoint(Path mountPoint) throws IOException {
+		if (!Files.isDirectory(mountPoint)) {
+			throw new NotDirectoryException(mountPoint.toString());
+		}
+		try (DirectoryStream<Path> ds = Files.newDirectoryStream(mountPoint)) {
+			if (ds.iterator().hasNext()) {
+				throw new DirectoryNotEmptyException(mountPoint.toString());
 			}
-		} else {
-			Files.createDirectory(p);
-			return p.toString();
 		}
 	}
 
+	private Path createTemporaryMountPoint() throws IOException {
+		Path parent = Paths.get(SystemUtils.IS_OS_MAC ? DEFAULT_MOUNTROOTPATH_MAC : DEFAULT_MOUNTROOTPATH_LINUX);
+		String basename = vaultSettings.getId();
+		for (int i = 0; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
+			try {
+				Path mountPath = parent.resolve(basename + "_" + i);
+				Files.createDirectory(mountPath);
+				return mountPath;
+			} catch (FileAlreadyExistsException e) {
+				continue;
+			}
+		}
+		LOG.error("Failed to create mount path at {}/{}_x. Giving up after {} attempts.", parent, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
+		throw new FileAlreadyExistsException(parent.toString() + "/" + basename);
+	}
+
 	private void mount(Path root) throws VolumeException {
 		try {
-			EnvironmentVariables envVars = EnvironmentVariables.create()
-					.withMountName(vaultSettings.mountName().getValue())
-					.withMountPath(mountPath)
+			EnvironmentVariables envVars = EnvironmentVariables.create() //
+					.withMountName(vaultSettings.mountName().getValue()) //
+					.withMountPath(mountPoint) //
 					.build();
 			this.fuseMnt = FuseMountFactory.getMounter().mount(root, envVars);
 		} catch (CommandFailedException e) {
@@ -91,27 +103,45 @@ public class FuseVolume implements Volume {
 		try {
 			fuseMnt.revealInFileManager();
 		} catch (CommandFailedException e) {
-			LOG.info("Revealing the vault in file manger failed: " + e.getMessage());
+			LOG.debug("Revealing the vault in file manger failed: " + e.getMessage());
+			throw new VolumeException(e);
+		}
+	}
+
+	@Override
+	public boolean supportsForcedUnmount() {
+		return true;
+	}
+
+	@Override
+	public synchronized void unmountForced() throws VolumeException {
+		try {
+			fuseMnt.unmountForced();
+			fuseMnt.close();
+		} catch (CommandFailedException e) {
 			throw new VolumeException(e);
 		}
+		deleteTemporaryMountPoint();
 	}
 
 	@Override
 	public synchronized void unmount() throws VolumeException {
 		try {
+			fuseMnt.unmount();
 			fuseMnt.close();
 		} catch (CommandFailedException e) {
 			throw new VolumeException(e);
 		}
-		cleanup();
+		deleteTemporaryMountPoint();
 	}
 
-	private void cleanup() {
-		if (extraDirCreated) {
+	private void deleteTemporaryMountPoint() {
+		if (createdTemporaryMountPoint) {
 			try {
-				Files.delete(mountPath);
+				Files.delete(mountPoint);
+				LOG.debug("Successfully deleted mount point: {}", mountPoint);
 			} catch (IOException e) {
-				LOG.warn("Could not delete mounting directory:" + e.getMessage());
+				LOG.warn("Could not delete mount point: {}", e.getMessage());
 			}
 		}
 	}

+ 0 - 5
main/ui/src/main/java/org/cryptomator/ui/model/InvalidSettingsException.java

@@ -1,5 +0,0 @@
-package org.cryptomator.ui.model;
-
-public class InvalidSettingsException extends RuntimeException {
-
-}

+ 27 - 19
main/ui/src/main/java/org/cryptomator/ui/model/Vault.java

@@ -8,19 +8,7 @@
  *******************************************************************************/
 package org.cryptomator.ui.model;
 
-import java.io.IOException;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.Files;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
-
-import javax.inject.Inject;
-import javax.inject.Provider;
-
+import com.google.common.base.Strings;
 import javafx.application.Platform;
 import javafx.beans.Observable;
 import javafx.beans.binding.Binding;
@@ -34,6 +22,7 @@ import org.cryptomator.common.settings.Settings;
 import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.cryptofs.CryptoFileSystem;
 import org.cryptomator.cryptofs.CryptoFileSystemProperties;
+import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
 import org.cryptomator.cryptofs.CryptoFileSystemProvider;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
@@ -42,6 +31,21 @@ import org.fxmisc.easybind.EasyBind;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.inject.Inject;
+import javax.inject.Provider;
+import java.io.IOException;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
+
 @PerVault
 public class Vault {
 
@@ -78,9 +82,13 @@ public class Vault {
 	}
 
 	private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
+		List<FileSystemFlags> flags = new ArrayList<>();
+		if (vaultSettings.usesReadOnlyMode().get()) {
+			flags.add(FileSystemFlags.READONLY);
+		}
 		CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
 				.withPassphrase(passphrase) //
-				.withFlags() //
+				.withFlags(flags) //
 				.withMasterkeyFilename(MASTERKEY_FILENAME) //
 				.build();
 		return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
@@ -98,11 +106,11 @@ public class Vault {
 		CryptoFileSystemProvider.changePassphrase(getPath(), MASTERKEY_FILENAME, oldPassphrase, newPassphrase);
 	}
 
-	public synchronized void unlock(CharSequence passphrase) throws InvalidSettingsException, CryptoException, IOException, Volume.VolumeException {
+	public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException {
 		Platform.runLater(() -> state.set(State.PROCESSING));
 		try {
-			if (vaultSettings.usesIndividualMountPath().and(vaultSettings.individualMountPath().isEmpty()).get()) {
-				throw new InvalidSettingsException();
+			if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
+				throw new NotDirectoryException("");
 			}
 			CryptoFileSystem fs = getCryptoFileSystem(passphrase);
 			volume = volumeProvider.get();
@@ -241,11 +249,11 @@ public class Vault {
 		return vaultSettings.mountName().get();
 	}
 
-	public String getIndividualMountPath() {
+	public String getCustomMountPath() {
 		return vaultSettings.individualMountPath().getValueSafe();
 	}
 
-	public void setIndividualMountPath(String mountPath) {
+	public void setCustomMountPath(String mountPath) {
 		vaultSettings.individualMountPath().set(mountPath);
 	}
 

+ 4 - 4
main/ui/src/main/java/org/cryptomator/ui/model/WindowsDriveLetters.java

@@ -22,11 +22,11 @@ import org.apache.commons.lang3.SystemUtils;
 @Singleton
 public final class WindowsDriveLetters {
 
-	private static final Set<Character> A_TO_Z;
+	private static final Set<Character> D_TO_Z;
 
 	static {
-		try (IntStream stream = IntStream.rangeClosed('A', 'Z')) {
-			A_TO_Z = stream.mapToObj(i -> (char) i).collect(Collectors.toSet());
+		try (IntStream stream = IntStream.rangeClosed('D', 'Z')) {
+			D_TO_Z = stream.mapToObj(i -> (char) i).collect(Collectors.toSet());
 		}
 	}
 
@@ -45,7 +45,7 @@ public final class WindowsDriveLetters {
 	public Set<Character> getAvailableDriveLetters() {
 		Set<Character> occupiedDriveLetters = getOccupiedDriveLetters();
 		Predicate<Character> isOccupiedDriveLetter = occupiedDriveLetters::contains;
-		return A_TO_Z.stream().filter(isOccupiedDriveLetter.negate()).collect(Collectors.toSet());
+		return D_TO_Z.stream().filter(isOccupiedDriveLetter.negate()).collect(Collectors.toSet());
 	}
 
 }

+ 40 - 36
main/ui/src/main/resources/fxml/unlock.fxml

@@ -7,29 +7,25 @@
   Contributors:
       Sebastian Stenzel - initial API and implementation
 -->
-<?import java.net.URL?>
-<?import java.lang.String?>
-<?import org.cryptomator.ui.controls.SecPasswordField?>
+
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.CheckBox?>
+<?import javafx.scene.control.ChoiceBox?>
+<?import javafx.scene.control.Hyperlink?>
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.control.ProgressIndicator?>
-<?import javafx.scene.control.CheckBox?>
-<?import javafx.scene.control.Button?>
-<?import javafx.scene.layout.GridPane?>
-<?import javafx.geometry.Insets?>
-<?import javafx.scene.layout.ColumnConstraints?>
+<?import javafx.scene.control.Separator?>
 <?import javafx.scene.control.TextField?>
-<?import javafx.scene.text.TextFlow?>
-<?import javafx.scene.control.Hyperlink?>
+<?import javafx.scene.layout.*?>
 <?import javafx.scene.text.Text?>
-<?import javafx.scene.layout.HBox?>
-<?import javafx.scene.control.Separator?>
-<?import javafx.scene.control.ChoiceBox?>
-
+<?import javafx.scene.text.TextFlow?>
+<?import org.cryptomator.ui.controls.SecPasswordField?>
 <GridPane fx:controller="org.cryptomator.ui.controllers.UnlockController" fx:id="root" vgap="12.0" hgap="12.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml" cacheShape="true" cache="true">
 	<padding>
 		<Insets top="24.0" right="12.0" bottom="24.0" left="12.0" />
 	</padding>
-	
+
 	<columnConstraints>
 		<ColumnConstraints percentWidth="38.2"/>
 		<ColumnConstraints percentWidth="61.8"/>
@@ -45,7 +41,7 @@
 			<Button fx:id="advancedOptionsButton" text="%unlock.button.advancedOptions.show" prefWidth="150.0" onAction="#didClickAdvancedOptionsButton" cacheShape="true" cache="true" />
 			<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" prefWidth="150.0" onAction="#didClickUnlockButton" disable="true" cacheShape="true" cache="true" />
 		</HBox>
-		
+
 		<!-- Row 3 -->
 		<Label fx:id="successMessage" cacheShape="true" cache="true" visible="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2"/>
 
@@ -54,12 +50,12 @@
 			<padding>
 				<Insets top="24.0" />
 			</padding>
-			
+
 			<columnConstraints>
 				<ColumnConstraints percentWidth="38.2"/>
 				<ColumnConstraints percentWidth="61.8"/>
 			</columnConstraints>
-			
+
 			<!-- Row 3.0 -->
 			<Separator GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true"/>
 			<HBox alignment="CENTER" prefWidth="400.0" GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true">
@@ -69,46 +65,54 @@
 					</padding>
 				</Label>
 			</HBox>
-			
+
 			<!-- Row 3.1 -->
 			<CheckBox GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="2" fx:id="savePassword" text="%unlock.label.savePassword" onAction="#didClickSavePasswordCheckbox" cacheShape="true" cache="true" />
-			
+
 			<!-- Row 3.2 -->
 			<CheckBox GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" fx:id="unlockAfterStartup" text="%unlock.label.unlockAfterStartup" cacheShape="true" cache="true" />
 
 			<!-- Row 3.3 -->
 			<Label GridPane.rowIndex="3" GridPane.columnIndex="0" text="%unlock.label.mountName"  cacheShape="true" cache="true" />
 			<TextField GridPane.rowIndex="3" GridPane.columnIndex="1" fx:id="mountName" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
-			
+
 			<!-- Row 3.4 -->
 			<CheckBox GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" fx:id="revealAfterMount" text="%unlock.label.revealAfterMount" cacheShape="true" cache="true" />
-			
-			<!-- Row 3.5 Alt1 -->
-			<Label GridPane.rowIndex="5" GridPane.columnIndex="0" fx:id="winDriveLetterLabel" text="%unlock.label.winDriveLetter" cacheShape="true" cache="true" />
-			<ChoiceBox GridPane.rowIndex="5" GridPane.columnIndex="1" fx:id="winDriveLetter" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
 
-			<!-- Row 3.5 Alt2 -->
-			<CheckBox GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" fx:id="useOwnMountPath" text="%unlock.label.useOwnMountPath" cacheShape="true" cache="true" />
+			<!-- Row 3.5 -->
+			<CheckBox GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" fx:id="useReadOnlyMode" text="%unlock.label.useReadOnlyMode" cacheShape="true" cache="true" />
+
+			<!-- Row 3.6 -->
+			<CheckBox GridPane.rowIndex="6" GridPane.columnIndex="0" GridPane.columnSpan="2" fx:id="useCustomMountPoint" text="%unlock.label.useOwnMountPath" cacheShape="true" cache="true" />
 
-			<Label GridPane.rowIndex="6" GridPane.columnIndex="0" fx:id="mountPathLabel" text="%unlock.label.mountPath" cacheShape="true" cache="true" />
-			<TextField GridPane.rowIndex="6" GridPane.columnIndex="1" fx:id="mountPath" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
+			<!-- Row 3.7 Alt1 -->
+			<Label GridPane.rowIndex="7" GridPane.columnIndex="0" fx:id="winDriveLetterLabel" text="%unlock.label.winDriveLetter" cacheShape="true" cache="true" />
+			<ChoiceBox GridPane.rowIndex="7" GridPane.columnIndex="1" fx:id="winDriveLetter" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
 
+			<!-- Row 3.7 Alt2 -->
+			<HBox fx:id="customMountPoint" GridPane.rowIndex="7" GridPane.columnIndex="0" GridPane.columnSpan="2" spacing="6" alignment="BASELINE_LEFT" cacheShape="true" cache="true">
+				<padding>
+					<Insets left="20.0" />
+				</padding>
+				<Label HBox.hgrow="ALWAYS" fx:id="customMountPointLabel" textOverrun="LEADING_ELLIPSIS" cacheShape="true" cache="true" />
+				<Button HBox.hgrow="NEVER" minWidth="-Infinity" text="&#xf434;" styleClass="ionicons" onAction="#didClickChooseCustomMountPoint" focusTraversable="true" cacheShape="true" cache="true" />
+			</HBox>
 		</GridPane>
-		
+
 		<!-- Row 4 -->
 		<TextFlow GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true">
 			<GridPane.margin>
 				<Insets top="24.0"/>
 			</GridPane.margin>
 			<children>
-				<Text fx:id="messageText" cache="true" />
 				<Hyperlink fx:id="downloadsPageLink" text="%unlock.label.downloadsPageLink" visible="false" onAction="#didClickDownloadsLink" cacheShape="true" cache="true" />
 			</children>
 		</TextFlow>
-		
-		<!-- Row 5-->
-		<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" cacheShape="true" cache="true" cacheHint="SPEED" />
+
+		<!-- Row 5 -->
+		<VBox GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" spacing="12.0" alignment="CENTER" cacheShape="true" cache="true">
+			<ProgressIndicator progress="-1" fx:id="progressIndicator" cacheShape="true" cache="true" cacheHint="SPEED" />
+			<Text fx:id="messageText" cache="true" />
+		</VBox>
 	</children>
 </GridPane>
-
-

+ 4 - 7
main/ui/src/main/resources/fxml/welcome.fxml

@@ -7,31 +7,28 @@
   Contributors:
       Sebastian Stenzel - initial API and implementation
 -->
-<?import java.net.URL?>
 <?import javafx.scene.image.ImageView?>
 <?import javafx.scene.image.Image?>
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.control.Hyperlink?>
-<?import javafx.scene.control.CheckBox?>
 <?import javafx.scene.layout.HBox?>
 <?import javafx.scene.layout.VBox?>
 <?import javafx.scene.control.ProgressIndicator?>
-<?import javafx.scene.control.Button?>
 
 <VBox fx:controller="org.cryptomator.ui.controllers.WelcomeController" fx:id="root" prefWidth="400.0" prefHeight="400.0" spacing="24.0" alignment="CENTER" xmlns:fx="http://javafx.com/fxml" cacheShape="true" cache="true">
-	
+
 	<VBox fx:id="checkForUpdatesContainer" spacing="6.0" alignment="CENTER" cacheShape="true" cache="true" prefHeight="64.0">
 		<HBox alignment="CENTER" spacing="5.0" cacheShape="true" cache="true">
 			<Label fx:id="checkForUpdatesStatus" cacheShape="true" cache="true" />
-			<ProgressIndicator fx:id="checkForUpdatesIndicator" progress="-1" prefWidth="15.0" prefHeight="15.0" cacheShape="true" cache="true" cacheHint="SPEED" />
+			<ProgressIndicator fx:id="checkForUpdatesIndicator" progress="-1" prefWidth="15.0" prefHeight="15.0" visible="false" cacheShape="true" cache="true" cacheHint="SPEED" />
 		</HBox>
 		<Hyperlink wrapText="true" textAlignment="CENTER" fx:id="updateLink" onAction="#didClickUpdateLink" cacheShape="true" cache="true" disable="true" />
 	</VBox>
-	
+
 	<ImageView fitHeight="200.0" preserveRatio="true" smooth="true" cache="true" style="-fx-background-color: green;">
 		<Image url="/bot_welcome.png"/>
 	</ImageView>
-	
+
 	<VBox prefHeight="64.0"/>
 
 </VBox>

文件差異過大導致無法顯示
+ 9 - 5
main/ui/src/main/resources/localization/ar.txt


文件差異過大導致無法顯示
+ 9 - 5
main/ui/src/main/resources/localization/bg.txt


+ 125 - 0
main/ui/src/main/resources/localization/ca.txt

@@ -0,0 +1,125 @@
+app.name = Cryptomator
+# main.fxml
+main.emptyListInstructions = Feu click ací per afegir una caixa forta
+main.directoryList.contextMenu.remove = Elimina de la llista
+main.directoryList.contextMenu.changePassword = Canvia la contrasenya
+main.addDirectory.contextMenu.new = Crea una caixa forta nova
+main.addDirectory.contextMenu.open = Obri una caixa forta existent
+# welcome.fxml
+welcome.checkForUpdates.label.currentlyChecking = Comprovant actualitzacions
+welcome.newVersionMessage = La versió %1$s és disponible per descarregar.\nLa versió actual és %2$s.
+# initialize.fxml
+initialize.label.password = Contrasenya
+initialize.label.retypePassword = Torneu a escriure la contrasenya
+initialize.button.ok = Crea una caixa forta
+initialize.messageLabel.alreadyInitialized = La caixa forta ja està inicialitzada
+initialize.messageLabel.initializationFailed = No s'ha pogut inicialitzar la caixa forta. Consulteu l'arxiu de registre per a més informació.
+# notfound.fxml
+notfound.label = No s'ha trobat la caixa forta. S'ha mogut a altre lloc?
+# upgrade.fxml
+upgrade.button = Actualitza la caixa forta
+upgrade.version3dropBundleExtension.msg = Esta caixa forta es deu actualitzar a un format més modern.\nEs va a canviar el nom de "%1$s" a "%2$s".\nAssegureu-vos de que la sincronització ha acabat abans d'iniciar el procés.
+upgrade.version3dropBundleExtension.err.alreadyExists = Error en la migració automàtica.\n"%s" ja existeix.
+# unlock.fxml
+unlock.label.password = Contrasenya
+unlock.label.mountName = Nom de la unitat
+unlock.label.winDriveLetter = Lletra de la unitat
+unlock.label.downloadsPageLink = Totes les versions de Cryptomator
+unlock.label.advancedHeading = Opcions avançades
+unlock.button.unlock = Debloqueja la caixa forta
+unlock.button.advancedOptions.show = Més opcions
+unlock.button.advancedOptions.hide = Menys opcions
+unlock.choicebox.winDriveLetter.auto = Assigna automàticament
+unlock.errorMessage.wrongPassword = Contrasenya incorrecta
+unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware = La caixa forta no és compatible. Aquesta caixa forta s'ha creat amb una versió anterior de Cryptomator.
+unlock.errorMessage.unsupportedVersion.softwareOlderThanVault = La caixa forta no és compatible. Aquesta caixa forta s'ha creat amb una versió més nova de Cryptomator.
+unlock.messageLabel.startServerFailed = S'ha produït un error en iniciar el servidor WebDAV.
+# change_password.fxml
+changePassword.label.oldPassword = Contrasenya antiga
+changePassword.label.newPassword = Contrasenya nova
+changePassword.label.retypePassword = Torneu a escriure la contrasenya
+changePassword.label.downloadsPageLink = Totes les versions de Cryptomator
+changePassword.button.change = Canvia la contrasenya
+changePassword.errorMessage.wrongPassword = Contrasenya incorrecta
+changePassword.errorMessage.decryptionFailed = Ha fallat el desencriptatge
+# unlocked.fxml
+unlocked.button.lock = Bloqueja la caixa forta
+unlocked.moreOptions.reveal = Mostra la unitat
+unlocked.label.revealFailed = L'ordre ha fallat
+unlocked.label.unmountFailed = Error al expulsar la unidad
+unlocked.label.statsEncrypted = xifrat
+unlocked.label.statsDecrypted = desxifrat
+unlocked.ioGraph.yAxis.label = Velocitat de transferència de dades (MiB/s)
+# settings.fxml
+settings.version.label = Versió %s
+settings.checkForUpdates.label = Comprova si hi ha actualitzacions
+settings.requiresRestartLabel = És necessari reiniciar * Cryptomator
+# tray icon
+tray.menu.open = Obri
+tray.menu.quit = Surt
+tray.infoMsg.title = Encara s'està executant
+tray.infoMsg.msg = Cryptomator encara està executant-se. Sortiu des de la icona de la safata.
+tray.infoMsg.msg.osx = Cryptomator encara està executant-se. Sortiu des de la icona de la barra de menú
+initialize.messageLabel.passwordStrength.0 = Molt dèbil
+initialize.messageLabel.passwordStrength.1 = Dèbil
+initialize.messageLabel.passwordStrength.2 = Acceptable
+initialize.messageLabel.passwordStrength.3 = Forta
+initialize.messageLabel.passwordStrength.4 = Molt forta
+initialize.label.doNotForget = IMPORTANT\: No hi ha manera de recuperar les dades si oblideu la contrasenya.
+main.directoryList.remove.confirmation.title = Suprimeix la caixa forta
+main.directoryList.remove.confirmation.header = ¿Esteu segur que voleu suprimir aquesta caixa forta?
+main.directoryList.remove.confirmation.content = La caixa forta només es suprimeix de la llista. Per tal de eliminar-la permanentment esborreu la caixa forta del vostre sistema de fitxers.
+upgrade.version3to4.msg = S'ha de migrar la caixa forta a un format més nou.\nS'actualitzaran els noms xifrats de les carpetes.\nAssegureu-vos que la sincronització ha acabat abans de continuar.
+upgrade.version3to4.err.io = Error en la migració degut a una excepció de E/S. Comproveu el registre per veure'n els detalls.\n
+# upgrade.fxml
+upgrade.confirmation.label = Sí, m'he assegurat que la sincronització hagi acabat
+unlock.label.savePassword = Desa la contrasenya
+unlock.errorMessage.unauthenticVersionMac = No s'ha pogut autenticar la versió de MAC.
+unlocked.label.mountFailed = Ha fallat el muntatge de la unitat
+unlock.savePassword.delete.confirmation.title = Elimina la contrasenya desada
+unlock.savePassword.delete.confirmation.header = Esteu segur que voleu eliminar la contrasenya desada d'aquesta unitat?
+unlock.savePassword.delete.confirmation.content = La contrasenya desada d'aquesta caixa forta va a ser eliminada inmediatament del clauer del seu sistema. Si voleu tornar a desar la contrasenya haureu de tornar a desbloquejar la vostra caixa forta i activar l'opció "Desa la contrasenya".
+settings.debugMode.label = Mode de depuració *
+upgrade.version3dropBundleExtension.title = Actualitza la caixa forta a la versió 3 (Drop Bundle Extension)
+upgrade.version3to4.title = Actualitza la caixa forta de la versió 3 a la 4
+upgrade.version4to5.title = Actualitza la caixa forta de la versió 4 a la 5
+upgrade.version4to5.msg = S'ha de migrar la caixa forta a un format més nou.\nS'actualitzaran els fitxers xifrats.\nAssegureu-vos que la sincronització ha acabat abans de continuar.\n\nNota\: la data de modificació de tots els fitxers es canviarà a la data/hora del procés.
+upgrade.version4to5.err.io = La migració ha fallat a causa d'una excepció d'E/S. Comproveu el registre per veure'n els detalls.
+unlock.label.revealAfterMount = Mostra la unitat
+unlocked.lock.force.confirmation.title = Ha fallat el bloqueig de %1$s
+unlocked.lock.force.confirmation.header = Voleu forçar el bloqueig?
+unlocked.lock.force.confirmation.content = Això pot ser perquè altres programes encara estan accedint als fitxers de la caixa forta o perquè s'ha produït un altre problema.\n\nEls programes què encara estan accedint als fitxers poden funcionar incorrectament i les dades què aquests programes no hagin escrit es poden perdre.
+unlock.label.unlockAfterStartup = Desbloqueig automàtic al iniciar (experimental)
+unlock.errorMessage.unlockFailed = Ha fallat el desbloqueig. Comproveu el registre per veure'n els detalls.
+upgrade.version5toX.title = Actualització de la versió de la caixa forta
+upgrade.version5toX.msg = S'ha de migrar la caixa forta a un format més nou.\nAssegureu-vos que la sincronització ha acabat abans de continuar.
+main.createVault.nonEmptyDir.title = Ha fallat la creació de la caixa forta
+main.createVault.nonEmptyDir.header = El directori seleccionat no és buit
+main.createVault.nonEmptyDir.content = Hi ha fitxers (possibement ocults) al directori seleccionat. Només es pot crear una caixa forta a un directori buit.
+settings.webdav.port.label = Port WebDAV
+settings.webdav.port.prompt = 0 \= Tria automàticament
+settings.webdav.port.apply = Aplica
+settings.webdav.prefGvfsScheme.label = Esquema de WebDAV
+settings.volume.label = Tipus de volum preferit
+settings.volume.webdav = WebDAV
+settings.volume.fuse = FUSE
+unlock.successLabel.vaultCreated = La caixa forta s'ha creat correctament.
+unlock.successLabel.passwordChanged = La contrasenya s'ha canviat correctament.
+unlock.successLabel.upgraded = Cryptomator s'ha actualitzat correctament.
+unlock.label.useOwnMountPath = Utilitza un punt de muntatge personalitzat
+welcome.askForUpdateCheck.dialog.title = Comprovació d'actualizacions
+welcome.askForUpdateCheck.dialog.header = Activo la comprovació automàtica d'actualitzacions?
+welcome.askForUpdateCheck.dialog.content = Recomanat\: Activa la comprovació d'actualitzacions per assegurar-vos que sempre teniu la darrera versió de Cryptomator instal·lada amb totes les actualitzacions de seguretat.\n\nPodeu canviar aquesta opció des de la configuració en qualsevol moment.
+settings.volume.dokany = Dokany
+main.gracefulShutdown.dialog.title = Ha fallat el bloqueig de la caixa(es) forta(es)
+main.gracefulShutdown.dialog.header = La caixa(es) forta(es) és(són) en ús
+main.gracefulShutdown.dialog.content = Hi ha programes encara estan utilitzant una caixa forta o més d'una. Tanqueu-los per permetre que Cryptomator es tanqui correctament i, a continuació, torneu-ho a intentar.\n\nSi això no funciona, es pot forçar l'aturada de Cryptomator tot i que no es recomana, donç pot comportar pèrdua de dades.
+main.gracefulShutdown.button.tryAgain = Torna-ho a intentar
+main.gracefulShutdown.button.forceShutdown = Força l'aturada
+unlock.pendingMessage.unlocking = La caixa forta s'està desbloquejant...
+unlock.failedDialog.title = Unlock failed
+unlock.failedDialog.header = Unlock failed
+unlock.failedDialog.content.mountPathNonExisting = Mount point does not exist.
+unlock.failedDialog.content.mountPathNotEmpty = Mount point is not empty.
+unlock.label.useReadOnlyMode = Read-Only
+unlock.label.chooseMountPath = Choose empty directory…

文件差異過大導致無法顯示
+ 8 - 4
main/ui/src/main/resources/localization/cs.txt


文件差異過大導致無法顯示
+ 9 - 5
main/ui/src/main/resources/localization/da.txt


文件差異過大導致無法顯示
+ 9 - 5
main/ui/src/main/resources/localization/de.txt


文件差異過大導致無法顯示
+ 10 - 5
main/ui/src/main/resources/localization/en.txt


文件差異過大導致無法顯示
+ 9 - 4
main/ui/src/main/resources/localization/es.txt


文件差異過大導致無法顯示
+ 8 - 4
main/ui/src/main/resources/localization/fr.txt


文件差異過大導致無法顯示
+ 10 - 6
main/ui/src/main/resources/localization/hu.txt


文件差異過大導致無法顯示
+ 9 - 5
main/ui/src/main/resources/localization/it.txt


文件差異過大導致無法顯示
+ 16 - 12
main/ui/src/main/resources/localization/ja.txt


文件差異過大導致無法顯示
+ 19 - 15
main/ui/src/main/resources/localization/ko.txt


文件差異過大導致無法顯示
+ 9 - 5
main/ui/src/main/resources/localization/lv.txt


文件差異過大導致無法顯示
+ 8 - 4
main/ui/src/main/resources/localization/nl.txt


文件差異過大導致無法顯示
+ 8 - 4
main/ui/src/main/resources/localization/pl.txt


文件差異過大導致無法顯示
+ 20 - 16
main/ui/src/main/resources/localization/pt.txt


文件差異過大導致無法顯示
+ 13 - 9
main/ui/src/main/resources/localization/pt_BR.txt


文件差異過大導致無法顯示
+ 9 - 5
main/ui/src/main/resources/localization/ru.txt


文件差異過大導致無法顯示
+ 9 - 5
main/ui/src/main/resources/localization/sk.txt


文件差異過大導致無法顯示
+ 9 - 5
main/ui/src/main/resources/localization/th.txt


文件差異過大導致無法顯示
+ 9 - 5
main/ui/src/main/resources/localization/tr.txt


文件差異過大導致無法顯示
+ 8 - 4
main/ui/src/main/resources/localization/uk.txt


+ 8 - 4
main/ui/src/main/resources/localization/zh.txt

@@ -97,8 +97,6 @@ upgrade.version5toX.msg = 此资料库需要升级至最新版本,\n请确保
 main.createVault.nonEmptyDir.title = 创建资料库失败
 main.createVault.nonEmptyDir.header = 选择的目录不为空
 main.createVault.nonEmptyDir.content = 选择的目录含有文件(可能是隐藏的)。资料库只能创建在空目录
-unlock.label.mountPath = 挂载路径
-unlock.label.mountPathButton = 设定
 settings.webdav.port.label = webDav端口
 settings.webdav.port.prompt = 0\=自动选择WebDav端口
 settings.webdav.port.apply = 设定
@@ -114,9 +112,15 @@ welcome.askForUpdateCheck.dialog.title = 检查更新
 welcome.askForUpdateCheck.dialog.header = 启用更新检查?
 welcome.askForUpdateCheck.dialog.content = 启用检查更新,Cryptomator将从Cryptomator服务器获取当前版本,并在有新版本时提示。\n我们建议您启用更新检查,以确保安装了最新版的Cryptomator,并安装了所有安全补丁。如果您未启用更新检查,则需要您自己到https\://cryptomator.org/downloads/ 检查并下载最新版本。\n你可以随时在设置中更改此设置。
 settings.volume.dokany = Dokany
-unlock.errorMessage.invalidMountPath = 未设置单独的挂载路径
 main.gracefulShutdown.dialog.title = Locking vault(s) failed
 main.gracefulShutdown.dialog.header = Vault(s) in use
 main.gracefulShutdown.dialog.content = One or more vaults are still in use by other programs. Please close them to allow Cryptomator to shut down properly, then try again.\n\nIf this doesn't work, Cryptomator can shut down forcefully, but this can incur data loss and is not recommended.
 main.gracefulShutdown.button.tryAgain = Try again
-main.gracefulShutdown.button.forceShutdown = Force shutdown
+main.gracefulShutdown.button.forceShutdown = Force shutdown
+unlock.pendingMessage.unlocking = Unlocking vault...
+unlock.failedDialog.title = Unlock failed
+unlock.failedDialog.header = Unlock failed
+unlock.failedDialog.content.mountPathNonExisting = Mount point does not exist.
+unlock.failedDialog.content.mountPathNotEmpty = Mount point is not empty.
+unlock.label.useReadOnlyMode = Read-Only
+unlock.label.chooseMountPath = Choose empty directory…

文件差異過大導致無法顯示
+ 9 - 5
main/ui/src/main/resources/localization/zh_HK.txt


文件差異過大導致無法顯示
+ 9 - 5
main/ui/src/main/resources/localization/zh_TW.txt


+ 1 - 1
main/ui/src/test/java/org/cryptomator/ui/l10n/LocalizationTest.java

@@ -31,7 +31,7 @@ public class LocalizationTest {
 	private static final Logger LOG = LoggerFactory.getLogger(LocalizationTest.class);
 	private static final String RESOURCE_FOLDER_PATH = "/localization/";
 	private static final String REF_FILE_NAME = "en.txt";
-	private static final String[] LANG_FILE_NAMES = {"ar.txt", "bg.txt", "cs.txt", "da.txt", "de.txt", "es.txt", "fr.txt", "hu.txt", "it.txt", "ja.txt", //
+	private static final String[] LANG_FILE_NAMES = {"ar.txt", "bg.txt", "ca.txt", "cs.txt", "da.txt", "de.txt", "es.txt", "fr.txt", "hu.txt", "it.txt", "ja.txt", //
 			"ko.txt", "lv.txt", "nl.txt", "pl.txt", "pt.txt", "pt_BR.txt", "ru.txt", "sk.txt", "th.txt", "tr.txt", "uk.txt", "zh_HK.txt", "zh_TW.txt", "zh.txt"};
 
 	/*