Sfoglia il codice sorgente

Merge branch 'feature/802-custom-mount-flags' into develop
fixes #802

Sebastian Stenzel 6 anni fa
parent
commit
b71c6a9d9f

+ 8 - 4
main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java

@@ -10,6 +10,7 @@ package org.cryptomator.common.settings;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
 import org.cryptomator.common.Environment;
 import org.cryptomator.common.LazyInitializer;
 import org.slf4j.Logger;
@@ -29,6 +30,7 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
 import java.nio.file.StandardOpenOption;
 import java.util.Optional;
 import java.util.concurrent.Executors;
@@ -109,12 +111,14 @@ public class SettingsProvider implements Provider<Settings> {
 		LOG.debug("Attempting to save settings to {}", settingsPath);
 		try {
 			Files.createDirectories(settingsPath.getParent());
-			try (OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); //
-					Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
+			Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp");
+			try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE_NEW); //
+				 Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
 				gson.toJson(settings, writer);
-				LOG.info("Settings saved to {}", settingsPath);
 			}
-		} catch (IOException e) {
+			Files.move(tmpPath, settingsPath, StandardCopyOption.REPLACE_EXISTING);
+			LOG.info("Settings saved to {}", settingsPath);
+		} catch (IOException | JsonParseException e) {
 			LOG.error("Failed to save settings.", e);
 		}
 	}

+ 9 - 2
main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java

@@ -20,6 +20,7 @@ import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.Base64;
+import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.UUID;
@@ -34,9 +35,10 @@ public class VaultSettings {
 	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;
+	public static final String DEFAULT_MOUNT_FLAGS = "";
 
 	private final String id;
-	private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
+	private final ObjectProperty<Path> path = new SimpleObjectProperty();
 	private final StringProperty mountName = new SimpleStringProperty();
 	private final StringProperty winDriveLetter = new SimpleStringProperty();
 	private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
@@ -44,6 +46,7 @@ public class VaultSettings {
 	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);
+	private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
 
 	public VaultSettings(String id) {
 		this.id = Objects.requireNonNull(id);
@@ -52,7 +55,7 @@ public class VaultSettings {
 	}
 
 	Observable[] observables() {
-		return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath, usesReadOnlyMode};
+		return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath, usesReadOnlyMode, mountFlags};
 	}
 
 	private void deriveMountNameFromPath(Path path) {
@@ -147,6 +150,10 @@ public class VaultSettings {
 		return usesReadOnlyMode;
 	}
 
+	public StringProperty mountFlags() {
+		return mountFlags;
+	}
+
 	/* Hashcode/Equals */
 
 	@Override

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

@@ -26,8 +26,9 @@ class VaultSettingsJsonAdapter {
 		out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
 		out.name("revealAfterMount").value(value.revealAfterMount().get());
 		out.name("usesIndividualMountPath").value(value.usesIndividualMountPath().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("individualMountPath").value(value.individualMountPath().get());
 		out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
+		out.name("mountFlags").value(value.mountFlags().get());
 		out.endObject();
 	}
 
@@ -41,6 +42,7 @@ class VaultSettingsJsonAdapter {
 		boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT;
 		boolean usesIndividualMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
 		boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
+		String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
 
 		in.beginObject();
 		while (in.hasNext()) {
@@ -73,6 +75,9 @@ class VaultSettingsJsonAdapter {
 				case "usesReadOnlyMode":
 					usesReadOnlyMode = in.nextBoolean();
 					break;
+				case "mountFlags":
+					mountFlags = in.nextString();
+					break;
 				default:
 					LOG.warn("Unsupported vault setting found in JSON: " + name);
 					in.skipValue();
@@ -90,6 +95,7 @@ class VaultSettingsJsonAdapter {
 		vaultSettings.usesIndividualMountPath().set(usesIndividualMountPath);
 		vaultSettings.individualMountPath().set(individualMountPath);
 		vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
+		vaultSettings.mountFlags().set(mountFlags);
 		return vaultSettings;
 	}
 

+ 27 - 1
main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java

@@ -6,12 +6,17 @@
 package org.cryptomator.common.settings;
 
 import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
 import java.io.StringReader;
+import java.io.StringWriter;
 import java.nio.file.Paths;
+import java.util.Arrays;
 
 public class VaultSettingsJsonAdapterTest {
 
@@ -19,7 +24,7 @@ public class VaultSettingsJsonAdapterTest {
 
 	@Test
 	public void testDeserialize() throws IOException {
-		String json = "{\"id\": \"foo\", \"path\": \"/foo/bar\", \"mountName\": \"test\", \"winDriveLetter\": \"X\", \"shouldBeIgnored\": true, \"individualMountPath\": \"/home/test/crypto\"}";
+		String json = "{\"id\": \"foo\", \"path\": \"/foo/bar\", \"mountName\": \"test\", \"winDriveLetter\": \"X\", \"shouldBeIgnored\": true, \"individualMountPath\": \"/home/test/crypto\", \"mountFlags\":\"--foo --bar\"}";
 		JsonReader jsonReader = new JsonReader(new StringReader(json));
 
 		VaultSettings vaultSettings = adapter.read(jsonReader);
@@ -28,6 +33,27 @@ public class VaultSettingsJsonAdapterTest {
 		Assertions.assertEquals("test", vaultSettings.mountName().get());
 		Assertions.assertEquals("X", vaultSettings.winDriveLetter().get());
 		Assertions.assertEquals("/home/test/crypto", vaultSettings.individualMountPath().get());
+		Assertions.assertEquals("--foo --bar", vaultSettings.mountFlags().get());
+
+
+	}
+
+	@Test
+	public void testSerialize() throws IOException {
+		VaultSettings vaultSettings = new VaultSettings("test");
+		vaultSettings.path().set(Paths.get("/foo/bar"));
+		vaultSettings.mountName().set("mountyMcMountFace");
+		vaultSettings.mountFlags().set("--foo --bar");
+
+		StringWriter buf = new StringWriter();
+		JsonWriter jsonWriter = new JsonWriter(buf);
+		adapter.write(jsonWriter, vaultSettings);
+		String result = buf.toString();
+
+		MatcherAssert.assertThat(result, CoreMatchers.containsString("\"id\":\"test\""));
+		MatcherAssert.assertThat(result, CoreMatchers.containsString("\"path\":\"/foo/bar\""));
+		MatcherAssert.assertThat(result, CoreMatchers.containsString("\"mountName\":\"mountyMcMountFace\""));
+		MatcherAssert.assertThat(result, CoreMatchers.containsString("\"mountFlags\":\"--foo --bar\""));
 	}
 
 }

+ 2 - 2
main/pom.xml

@@ -27,8 +27,8 @@
 		<cryptomator.cryptolib.version>1.2.1</cryptomator.cryptolib.version>
 		<cryptomator.cryptofs.version>1.8.5</cryptomator.cryptofs.version>
 		<cryptomator.jni.version>2.0.0</cryptomator.jni.version>
-		<cryptomator.fuse.version>1.1.3</cryptomator.fuse.version>
-		<cryptomator.dokany.version>1.1.8</cryptomator.dokany.version>
+		<cryptomator.fuse.version>1.2.0</cryptomator.fuse.version>
+		<cryptomator.dokany.version>1.1.9</cryptomator.dokany.version>
 		<cryptomator.webdav.version>1.0.10</cryptomator.webdav.version>
 
 		<javafx.version>12</javafx.version>

+ 84 - 54
main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java

@@ -11,6 +11,8 @@ package org.cryptomator.ui.controllers;
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import javafx.application.Application;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
 import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;
 import javafx.fxml.FXML;
@@ -20,13 +22,13 @@ import javafx.scene.control.Button;
 import javafx.scene.control.ButtonType;
 import javafx.scene.control.CheckBox;
 import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.ContentDisplay;
 import javafx.scene.control.Hyperlink;
 import javafx.scene.control.Label;
-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.layout.VBox;
 import javafx.scene.text.Text;
 import javafx.stage.DirectoryChooser;
 import javafx.stage.Stage;
@@ -83,6 +85,7 @@ public class UnlockController implements ViewController {
 	private Vault vault;
 	private Optional<UnlockListener> listener = Optional.empty();
 	private Subscription vaultSubs = Subscription.EMPTY;
+	private BooleanProperty unlocking = new SimpleBooleanProperty();
 
 	@Inject
 	public UnlockController(Application app, @Named("mainWindow") Stage mainWindow, Localization localization, WindowsDriveLetters driveLetters, Optional<KeychainAccess> keychainAccess, Settings settings, ExecutorService executor) {
@@ -113,11 +116,17 @@ public class UnlockController implements ViewController {
 	@FXML
 	private TextField mountName;
 
+	@FXML
+	private CheckBox useCustomMountFlags;
+
+	@FXML
+	private TextField mountFlags;
+
 	@FXML
 	private CheckBox revealAfterMount;
 
 	@FXML
-	private Label winDriveLetterLabel;
+	private CheckBox useCustomWinDriveLetter;
 
 	@FXML
 	private ChoiceBox<Character> winDriveLetter;
@@ -131,20 +140,14 @@ public class UnlockController implements ViewController {
 	@FXML
 	private Label customMountPointLabel;
 
-	@FXML
-	private ProgressIndicator progressIndicator;
-
-	@FXML
-	private Text progressText;
-
 	@FXML
 	private Hyperlink downloadsPageLink;
 
 	@FXML
-	private GridPane advancedOptions;
+	private VBox advancedOptions;
 
 	@FXML
-	private GridPane root;
+	private VBox root;
 
 	@FXML
 	private CheckBox unlockAfterStartup;
@@ -155,25 +158,31 @@ public class UnlockController implements ViewController {
 	@Override
 	public void initialize() {
 		advancedOptions.managedProperty().bind(advancedOptions.visibleProperty());
-		unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty());
+		advancedOptions.disableProperty().bind(unlocking);
+		unlockButton.disableProperty().bind(unlocking.or(passwordField.textProperty().isEmpty()));
 		mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
 		mountName.textProperty().addListener(this::mountNameDidChange);
+		useReadOnlyMode.selectedProperty().addListener(this::useReadOnlyDidChange);
+		useCustomMountFlags.selectedProperty().addListener(this::useCustomMountFlagsDidChange);
+		mountFlags.disableProperty().bind(useCustomMountFlags.selectedProperty().not());
+		mountFlags.textProperty().addListener(this::mountFlagsDidChange);
 		savePassword.setDisable(!keychainAccess.isPresent());
 		unlockAfterStartup.disableProperty().bind(savePassword.disabledProperty().or(savePassword.selectedProperty().not()));
+		downloadsPageLink.visibleProperty().bind(downloadsPageLink.managedProperty());
 
 		customMountPoint.visibleProperty().bind(useCustomMountPoint.selectedProperty());
 		customMountPoint.managedProperty().bind(useCustomMountPoint.selectedProperty());
 		winDriveLetter.setConverter(new WinDriveLetterLabelConverter());
+		winDriveLetter.disableProperty().bind(useCustomWinDriveLetter.selectedProperty().not());
 
 		if (!SystemUtils.IS_OS_WINDOWS) {
-			winDriveLetterLabel.setVisible(false);
-			winDriveLetterLabel.setManaged(false);
+			useCustomWinDriveLetter.setVisible(false);
+			useCustomWinDriveLetter.setManaged(false);
 			winDriveLetter.setVisible(false);
 			winDriveLetter.setManaged(false);
 		}
 	}
 
-
 	@Override
 	public Parent getRoot() {
 		return root;
@@ -198,20 +207,12 @@ public class UnlockController implements ViewController {
 		this.vault = vault;
 		advancedOptions.setVisible(false);
 		advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
-		progressIndicator.setVisible(false);
-		progressText.setText(null);
+		unlockButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
 		state.successMessage().map(localization::getString).ifPresent(messageText::setText);
-		if (SystemUtils.IS_OS_WINDOWS) {
-			winDriveLetter.valueProperty().removeListener(driveLetterChangeListener);
-			winDriveLetter.getItems().clear();
-			winDriveLetter.getItems().add(null);
-			winDriveLetter.getItems().addAll(driveLetters.getAvailableDriveLetters());
-			winDriveLetter.getItems().sort(new WinDriveLetterComparator());
-			winDriveLetter.valueProperty().addListener(driveLetterChangeListener);
-			chooseSelectedDriveLetter();
-		}
-		downloadsPageLink.setVisible(false);
+		downloadsPageLink.setManaged(false);
 		mountName.setText(vault.getMountName());
+		useCustomMountFlags.setSelected(vault.isHavingCustomMountFlags());
+		mountFlags.setText(vault.getMountFlags());
 		savePassword.setSelected(false);
 		// auto-fill pw from keychain:
 		if (keychainAccess.isPresent()) {
@@ -226,6 +227,7 @@ public class UnlockController implements ViewController {
 		VaultSettings vaultSettings = vault.getVaultSettings();
 		unlockAfterStartup.setSelected(savePassword.isSelected() && vaultSettings.unlockAfterStartup().get());
 		revealAfterMount.setSelected(vaultSettings.revealAfterMount().get());
+		useReadOnlyMode.setSelected(vaultSettings.usesReadOnlyMode().get());
 
 		// WEBDAV-dependent controls:
 		if (VolumeImpl.WEBDAV.equals(settings.preferredVolumeImpl().get())) {
@@ -241,26 +243,21 @@ public class UnlockController implements ViewController {
 			}
 		}
 
-		// 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());
-		}
 
 		// OS-dependent controls:
 		if (SystemUtils.IS_OS_WINDOWS) {
+			winDriveLetter.valueProperty().removeListener(driveLetterChangeListener);
+			winDriveLetter.getItems().clear();
+			winDriveLetter.getItems().add(null);
+			winDriveLetter.getItems().addAll(driveLetters.getAvailableDriveLetters());
+			winDriveLetter.getItems().sort(new WinDriveLetterComparator());
+			winDriveLetter.valueProperty().addListener(driveLetterChangeListener);
+			chooseSelectedDriveLetter();
+			
 			winDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
 			winDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
-			winDriveLetterLabel.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
-			winDriveLetterLabel.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
+			useCustomWinDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
+			useCustomWinDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
 		}
 
 		vaultSubs = vaultSubs.and(EasyBind.subscribe(unlockAfterStartup.selectedProperty(), vaultSettings.unlockAfterStartup()::set));
@@ -297,6 +294,7 @@ public class UnlockController implements ViewController {
 	@FXML
 	private void didClickAdvancedOptionsButton() {
 		messageText.setText(null);
+		downloadsPageLink.setManaged(false);
 		advancedOptions.setVisible(!advancedOptions.isVisible());
 		if (advancedOptions.isVisible()) {
 			advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.hide"));
@@ -318,6 +316,30 @@ public class UnlockController implements ViewController {
 		} else {
 			vault.setMountName(newValue);
 		}
+		if (!useCustomMountFlags.isSelected()) {
+			mountFlags.setText(vault.getMountFlags()); // update default flags
+		}
+	}
+
+	private void useReadOnlyDidChange(@SuppressWarnings("unused") ObservableValue<? extends Boolean> property, @SuppressWarnings("unused")Boolean oldValue, Boolean newValue) {
+		vault.getVaultSettings().usesReadOnlyMode().setValue(newValue);
+		if (!useCustomMountFlags.isSelected()) {
+			mountFlags.setText(vault.getMountFlags()); // update default flags
+		}
+	}
+
+
+	private void useCustomMountFlagsDidChange(@SuppressWarnings("unused") ObservableValue<? extends Boolean> property, @SuppressWarnings("unused")Boolean oldValue, Boolean newValue) {
+		if (!newValue) {
+			vault.setMountFlags(VaultSettings.DEFAULT_MOUNT_FLAGS);
+			mountFlags.setText(vault.getMountFlags());
+		}
+	}
+
+	private void mountFlagsDidChange(@SuppressWarnings("unused") ObservableValue<? extends String> property, @SuppressWarnings("unused")String oldValue, String newValue) {
+		if (useCustomMountFlags.isSelected()) {
+			vault.setMountFlags(newValue);
+		}
 	}
 
 	@FXML
@@ -330,6 +352,13 @@ public class UnlockController implements ViewController {
 		}
 	}
 
+	@FXML
+	public void didClickCustomWinDriveLetterCheckbox() {
+		if (!useCustomWinDriveLetter.isSelected()) {
+			winDriveLetter.setValue(null);
+		}
+	}
+
 	/**
 	 * Converts 'C' to "C:" to translate between model and GUI.
 	 */
@@ -379,7 +408,7 @@ public class UnlockController implements ViewController {
 	private void chooseSelectedDriveLetter() {
 		assert SystemUtils.IS_OS_WINDOWS;
 		// if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this:
-		if (driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) {
+		if (vault.getWinDriveLetter() != null && driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) {
 			vault.setWinDriveLetter(null);
 		}
 		final Character letter = vault.getWinDriveLetter();
@@ -428,35 +457,34 @@ public class UnlockController implements ViewController {
 
 	@FXML
 	private void didClickUnlockButton() {
-		advancedOptions.setDisable(true);
-		advancedOptions.setVisible(false);
+		unlocking.set(true);
 		advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
-		progressIndicator.setVisible(true);
+		unlockButton.setContentDisplay(ContentDisplay.LEFT);
 
 		CharSequence password = passwordField.getCharacters();
 		Tasks.create(() -> {
-			progressText.setText(localization.getString("unlock.pendingMessage.unlocking"));
 			vault.unlock(password);
 			if (keychainAccess.isPresent() && savePassword.isSelected()) {
 				keychainAccess.get().storePassphrase(vault.getId(), password);
 			}
 		}).onSuccess(() -> {
-			messageText.setText("");
-			downloadsPageLink.setVisible(false);
+			messageText.setText(null);
+			downloadsPageLink.setManaged(false);
 			listener.ifPresent(lstnr -> lstnr.didUnlock(vault));
 			passwordField.swipe();
 		}).onError(InvalidPassphraseException.class, e -> {
 			messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
+			downloadsPageLink.setManaged(false);
 			passwordField.selectAll();
 			passwordField.requestFocus();
 		}).onError(UnsupportedVaultFormatException.class, e -> {
 			if (e.isVaultOlderThanSoftware()) {
 				// whitespace after localized text used as separator between text and hyperlink
 				messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
-				downloadsPageLink.setVisible(true);
+				downloadsPageLink.setManaged(true);
 			} else if (e.isSoftwareOlderThanVault()) {
 				messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
-				downloadsPageLink.setVisible(true);
+				downloadsPageLink.setManaged(true);
 			} else if (e.getDetectedVersion() == Integer.MAX_VALUE) {
 				messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
 			}
@@ -464,19 +492,21 @@ public class UnlockController implements ViewController {
 			LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage());
 			advancedOptions.setVisible(true);
 			messageText.setText(null);
+			downloadsPageLink.setManaged(false);
 			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);
+			downloadsPageLink.setManaged(false);
 			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"));
+			downloadsPageLink.setManaged(false);
 		}).andFinally(() -> {
-			advancedOptions.setDisable(false);
-			progressIndicator.setVisible(false);
-			progressText.setText(null);
+			unlocking.set(false);
+			unlockButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
 		}).runOnce(executor);
 	}
 

+ 13 - 0
main/ui/src/main/java/org/cryptomator/ui/model/DefaultMountFlags.java

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

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

@@ -44,11 +44,11 @@ public class DokanyVolume implements Volume {
 	}
 
 	@Override
-	public void mount(CryptoFileSystem fs) throws VolumeException, IOException {
+	public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException, IOException {
 		Path mountPath = getMountPoint();
 		String mountName = vaultSettings.mountName().get();
 		try {
-			this.mount = mountFactory.mount(fs.getPath("/"), mountPath, mountName, FS_TYPE_NAME);
+			this.mount = mountFactory.mount(fs.getPath("/"), mountPath, mountName, FS_TYPE_NAME, mountFlags);
 		} 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);

+ 13 - 6
main/ui/src/main/java/org/cryptomator/ui/model/FuseVolume.java

@@ -1,5 +1,6 @@
 package org.cryptomator.ui.model;
 
+import com.google.common.base.Splitter;
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.common.Environment;
 import org.cryptomator.common.settings.VaultSettings;
@@ -9,6 +10,7 @@ import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
 import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
 import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
 import org.cryptomator.frontend.fuse.mount.Mount;
+import org.cryptomator.frontend.fuse.mount.Mounter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -42,7 +44,7 @@ public class FuseVolume implements Volume {
 	}
 
 	@Override
-	public void mount(CryptoFileSystem fs) throws IOException, FuseNotSupportedException, VolumeException {
+	public void mount(CryptoFileSystem fs, String mountFlags) throws IOException, FuseNotSupportedException, VolumeException {
 		Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
 		if (optionalCustomMountPoint.isPresent()) {
 			Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
@@ -53,7 +55,7 @@ public class FuseVolume implements Volume {
 			this.mountPoint = prepareTemporaryMountPoint();
 			LOG.debug("Successfully created mount point: {}", mountPoint);
 		}
-		mount(fs.getPath("/"));
+		mount(fs.getPath("/"), mountFlags);
 	}
 
 	private void checkProvidedMountPoint(Path mountPoint) throws IOException {
@@ -94,18 +96,23 @@ public class FuseVolume implements Volume {
 		throw new VolumeException("Did not find feasible mount point.");
 	}
 
-	private void mount(Path root) throws VolumeException {
+	private void mount(Path root, String mountFlags) throws VolumeException {
 		try {
+			Mounter mounter = FuseMountFactory.getMounter();
 			EnvironmentVariables envVars = EnvironmentVariables.create() //
-					.withMountName(vaultSettings.mountName().getValue()) //
-					.withMountPath(mountPoint) //
+					.withFlags(splitFlags(mountFlags))
+					.withMountPoint(mountPoint) //
 					.build();
-			this.fuseMnt = FuseMountFactory.getMounter().mount(root, envVars);
+			this.fuseMnt = mounter.mount(root, envVars);
 		} catch (CommandFailedException e) {
 			throw new VolumeException("Unable to mount Filesystem", e);
 		}
 	}
 
+	private String[] splitFlags(String str) {
+		return Splitter.on(' ').splitToList(str).toArray(String[]::new);
+	}
+
 	@Override
 	public void reveal() throws VolumeException {
 		try {

+ 13 - 0
main/ui/src/main/java/org/cryptomator/ui/model/PerVault.java

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

+ 26 - 7
main/ui/src/main/java/org/cryptomator/ui/model/Vault.java

@@ -25,7 +25,6 @@ import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
 import org.cryptomator.cryptofs.CryptoFileSystemProvider;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
-import org.cryptomator.ui.model.VaultModule.PerVault;
 import org.fxmisc.easybind.EasyBind;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -44,6 +43,7 @@ import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 
 @PerVault
 public class Vault {
@@ -54,6 +54,7 @@ public class Vault {
 
 	private final VaultSettings vaultSettings;
 	private final Provider<Volume> volumeProvider;
+	private final Supplier<String> defaultMountFlags;
 	private final AtomicReference<CryptoFileSystem> cryptoFileSystem = new AtomicReference<>();
 	private final ObjectProperty<State> state = new SimpleObjectProperty<State>(State.LOCKED);
 
@@ -64,9 +65,10 @@ public class Vault {
 	}
 
 	@Inject
-	Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider) {
+	Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags Supplier<String> defaultMountFlags) {
 		this.vaultSettings = vaultSettings;
 		this.volumeProvider = volumeProvider;
+		this.defaultMountFlags = defaultMountFlags;
 	}
 
 	// ******************************************************************************
@@ -110,7 +112,7 @@ public class Vault {
 			}
 			CryptoFileSystem fs = getCryptoFileSystem(passphrase);
 			volume = volumeProvider.get();
-			volume.mount(fs);
+			volume.mount(fs, getMountFlags());
 			Platform.runLater(() -> {
 				state.set(State.UNLOCKED);
 			});
@@ -241,10 +243,6 @@ public class Vault {
 		}
 	}
 
-	public String getMountName() {
-		return vaultSettings.mountName().get();
-	}
-
 	public String getCustomMountPath() {
 		return vaultSettings.individualMountPath().getValueSafe();
 	}
@@ -253,6 +251,10 @@ public class Vault {
 		vaultSettings.individualMountPath().set(mountPath);
 	}
 
+	public String getMountName() {
+		return vaultSettings.mountName().get();
+	}
+
 	public void setMountName(String mountName) throws IllegalArgumentException {
 		if (StringUtils.isBlank(mountName)) {
 			throw new IllegalArgumentException("mount name is empty");
@@ -261,6 +263,23 @@ public class Vault {
 		}
 	}
 
+	public boolean isHavingCustomMountFlags() {
+		return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
+	}
+
+	public String getMountFlags() {
+		String mountFlags = vaultSettings.mountFlags().get();
+		if (Strings.isNullOrEmpty(mountFlags)) {
+			return defaultMountFlags.get();
+		} else {
+			return mountFlags;
+		}
+	}
+
+	public void setMountFlags(String mountFlags) {
+		vaultSettings.mountFlags().set(mountFlags);
+	}
+
 	public Character getWinDriveLetter() {
 		if (vaultSettings.winDriveLetter().get() == null) {
 			return null;

+ 0 - 1
main/ui/src/main/java/org/cryptomator/ui/model/VaultComponent.java

@@ -7,7 +7,6 @@ package org.cryptomator.ui.model;
 
 import dagger.BindsInstance;
 import org.cryptomator.common.settings.VaultSettings;
-import org.cryptomator.ui.model.VaultModule.PerVault;
 
 import dagger.Subcomponent;
 

+ 94 - 11
main/ui/src/main/java/org/cryptomator/ui/model/VaultModule.java

@@ -7,28 +7,24 @@ package org.cryptomator.ui.model;
 
 import dagger.Module;
 import dagger.Provides;
+import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.common.settings.Settings;
+import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.common.settings.VolumeImpl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.inject.Scope;
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.function.Supplier;
 
 @Module
 public class VaultModule {
 
 	private static final Logger LOG = LoggerFactory.getLogger(VaultModule.class);
 
-	@Scope
-	@Documented
-	@Retention(RetentionPolicy.RUNTIME)
-	@interface PerVault {
-
-	}
-
 	@Provides
 	public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
 		VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
@@ -45,4 +41,91 @@ public class VaultModule {
 		}
 	}
 
+	@Provides
+	@PerVault
+	@DefaultMountFlags
+	public Supplier<String> provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
+		VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
+		switch (preferredImpl) {
+			case FUSE:
+				if (SystemUtils.IS_OS_MAC_OSX) {
+					return () -> getMacFuseDefaultMountFlags(settings, vaultSettings);
+				} else if (SystemUtils.IS_OS_LINUX) {
+					return () -> getLinuxFuseDefaultMountFlags(settings, vaultSettings);
+				}
+			case DOKANY:
+				return () -> getDokanyDefaultMountFlags(settings, vaultSettings);
+			default:
+				return () -> "--flags-supported-on-FUSE-or-DOKANY-only";
+		}
+	}
+
+	// see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
+	private String getMacFuseDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
+		assert SystemUtils.IS_OS_MAC_OSX;
+
+		StringBuilder flags = new StringBuilder();
+		if (vaultSettings.usesReadOnlyMode().get()) {
+			flags.append(" -ordonly");
+		}
+		flags.append(" -ovolname=").append(vaultSettings.mountName().get());
+		flags.append(" -oatomic_o_trunc");
+		flags.append(" -oauto_xattr");
+		flags.append(" -oauto_cache");
+		flags.append(" -omodules=iconv,from_code=UTF-8,to_code=UTF-8-MAC"); // show files names in Unicode NFD encoding
+		flags.append(" -onoappledouble"); // vastly impacts performance for some reason...
+		flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc
+
+		try {
+			Path userHome = Paths.get(System.getProperty("user.home"));
+			int uid = (int) Files.getAttribute(userHome, "unix:uid");
+			int gid = (int) Files.getAttribute(userHome, "unix:gid");
+			flags.append(" -ouid=").append(uid);
+			flags.append(" -ogid=").append(gid);
+		} catch (IOException e) {
+			LOG.error("Could not read uid/gid from USER_HOME", e);
+		}
+
+		return flags.toString();
+	}
+
+	// see https://manpages.debian.org/testing/fuse/mount.fuse.8.en.html
+	private String getLinuxFuseDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
+		assert SystemUtils.IS_OS_LINUX;
+
+		StringBuilder flags = new StringBuilder();
+		if (vaultSettings.usesReadOnlyMode().get()) {
+			flags.append(" -oro");
+		}
+		flags.append(" -oauto_unmount");
+
+		try {
+			Path userHome = Paths.get(System.getProperty("user.home"));
+			int uid = (int) Files.getAttribute(userHome, "unix:uid");
+			int gid = (int) Files.getAttribute(userHome, "unix:gid");
+			flags.append(" -ouid=").append(uid);
+			flags.append(" -ogid=").append(gid);
+		} catch (IOException e) {
+			LOG.error("Could not read uid/gid from USER_HOME", e);
+		}
+
+		return flags.toString();
+	}
+
+	// see https://github.com/cryptomator/dokany-nio-adapter/blob/develop/src/main/java/org/cryptomator/frontend/dokany/MountUtil.java#L30-L34
+	private String getDokanyDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
+		assert SystemUtils.IS_OS_WINDOWS;
+
+		StringBuilder flags = new StringBuilder();
+		flags.append(" --options CURRENT_SESSION");
+		if (vaultSettings.usesReadOnlyMode().get()) {
+			flags.append(",WRITE_PROTECTION");
+		}
+		flags.append(" --threadCount 5");
+		flags.append(" --timeout 10000");
+		flags.append(" --allocation-unit-size 4096");
+		flags.append(" --sector-size 4096");
+		return flags.toString();
+	}
+
 }

+ 1 - 1
main/ui/src/main/java/org/cryptomator/ui/model/Volume.java

@@ -22,7 +22,7 @@ public interface Volume {
 	 * @param fs
 	 * @throws IOException
 	 */
-	void mount(CryptoFileSystem fs) throws IOException, VolumeException;
+	void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException;
 
 	void reveal() throws VolumeException;
 

+ 1 - 2
main/ui/src/main/java/org/cryptomator/ui/model/WebDavVolume.java

@@ -11,7 +11,6 @@ import org.cryptomator.frontend.webdav.servlet.WebDavServletController;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
-
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 
@@ -35,7 +34,7 @@ public class WebDavVolume implements Volume {
 	}
 
 	@Override
-	public void mount(CryptoFileSystem fs) throws VolumeException {
+	public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException {
 		if (server == null) {
 			server = serverProvider.get();
 		}

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

@@ -8,6 +8,8 @@ package org.cryptomator.ui.model;
 import org.apache.commons.lang3.CharUtils;
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.common.FxApplicationScoped;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
 import java.nio.file.FileSystems;
@@ -21,6 +23,7 @@ import java.util.stream.StreamSupport;
 @FxApplicationScoped
 public final class WindowsDriveLetters {
 
+	private static final Logger LOG = LoggerFactory.getLogger(WindowsDriveLetters.class);
 	private static final Set<Character> D_TO_Z;
 
 	static {
@@ -35,10 +38,12 @@ public final class WindowsDriveLetters {
 
 	public Set<Character> getOccupiedDriveLetters() {
 		if (!SystemUtils.IS_OS_WINDOWS) {
-			throw new UnsupportedOperationException("This method is only defined for Windows file systems");
+			LOG.warn("Attempted to get occupied drive letters on non-Windows machine.");
+			return Set.of();
+		} else {
+			Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
+			return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(Collectors.toSet());
 		}
-		Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
-		return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(Collectors.toSet());
 	}
 
 	public Set<Character> getAvailableDriveLetters() {

+ 69 - 86
main/ui/src/main/resources/fxml/unlock.fxml

@@ -17,102 +17,85 @@
 <?import javafx.scene.control.ProgressIndicator?>
 <?import javafx.scene.control.Separator?>
 <?import javafx.scene.control.TextField?>
-<?import javafx.scene.layout.*?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.VBox?>
 <?import javafx.scene.text.Text?>
 <?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">
+<VBox fx:controller="org.cryptomator.ui.controllers.UnlockController" fx:id="root" spacing="12" alignment="BOTTOM_CENTER" xmlns:fx="http://javafx.com/fxml" prefWidth="400">
+
 	<padding>
-		<Insets top="24.0" right="12.0" bottom="24.0" left="12.0" />
+		<Insets top="24" right="12" bottom="24" left="12" />
 	</padding>
 
-	<columnConstraints>
-		<ColumnConstraints percentWidth="38.2"/>
-		<ColumnConstraints percentWidth="61.8"/>
-	</columnConstraints>
+	<!-- Password Field -->
+	<HBox spacing="12" alignment="BASELINE_LEFT">
+		<Label text="%unlock.label.password" HBox.hgrow="NEVER"/>
+		<SecPasswordField fx:id="passwordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" maxWidth="Infinity" HBox.hgrow="ALWAYS"/>
+	</HBox>
+
+	<!-- Unlock Button / Advanced Options Button -->
+	<HBox spacing="12" alignment="CENTER_RIGHT">
+		<Button fx:id="advancedOptionsButton" text="%unlock.button.advancedOptions.show" prefWidth="150.0" onAction="#didClickAdvancedOptionsButton"/>
+		<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" prefWidth="150.0" onAction="#didClickUnlockButton" disable="true" contentDisplay="TEXT_ONLY">
+				<graphic>
+					<ProgressIndicator progress="-1" prefWidth="12" prefHeight="12"/>
+				</graphic>
+		</Button>
+	</HBox>
+
+	<!-- Status Text -->
+	<TextFlow prefWidth="400" textAlignment="LEFT">
+		<children>
+			<Text fx:id="messageText"/>
+			<Hyperlink fx:id="downloadsPageLink" text="%unlock.label.downloadsPageLink" managed="false" onAction="#didClickDownloadsLink"/>
+		</children>
+	</TextFlow>
+
+	<!-- Advanced Options -->
+	<VBox fx:id="advancedOptions" spacing="12" VBox.vgrow="ALWAYS" visible="false">
+
+		<Separator/>
+
+		<!-- Mount Name -->
+		<HBox spacing="12" alignment="BASELINE_LEFT">
+			<Label text="%unlock.label.mountName"/>
+			<TextField fx:id="mountName" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
+		</HBox>
 
-	<children>
-		<!-- Row 0 -->
-		<Label text="%unlock.label.password" GridPane.rowIndex="0" GridPane.columnIndex="0" cacheShape="true" cache="true" />
-		<SecPasswordField fx:id="passwordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
+		<!-- Save Password -->
+		<CheckBox fx:id="savePassword" text="%unlock.label.savePassword" onAction="#didClickSavePasswordCheckbox"/>
 
-		<!-- Row 1 -->
-		<HBox GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" spacing="12.0" alignment="CENTER_RIGHT" cacheShape="true" cache="true">
-			<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>
+		<!-- Auto Unlock -->
+		<CheckBox fx:id="unlockAfterStartup" text="%unlock.label.unlockAfterStartup"/>
 
-		<!-- Row 3 -->
-		<Text fx:id="messageText" cache="true" visible="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2"/>
+		<!-- Reveal Drive -->
+		<CheckBox fx:id="revealAfterMount" text="%unlock.label.revealAfterMount"/>
 
-		<!-- Row 3 -->
-		<GridPane fx:id="advancedOptions" vgap="12.0" hgap="12.0" prefWidth="400.0" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" visible="false" cacheShape="true" cache="true">
+		<!-- Read-Only -->
+		<CheckBox fx:id="useReadOnlyMode" text="%unlock.label.useReadOnlyMode"/>
+
+		<!-- Custom Mount Point -->
+		<CheckBox fx:id="useCustomMountPoint" text="%unlock.label.useOwnMountPath"/>
+		<HBox fx:id="customMountPoint" spacing="6" alignment="BASELINE_LEFT">
 			<padding>
-				<Insets top="24.0" />
+				<Insets left="20.0"/>
 			</padding>
+			<Label HBox.hgrow="ALWAYS" fx:id="customMountPointLabel" textOverrun="LEADING_ELLIPSIS"/>
+			<Button HBox.hgrow="NEVER" minWidth="-Infinity" text="&#xf434;" styleClass="ionicons" onAction="#didClickChooseCustomMountPoint" focusTraversable="true"/>
+		</HBox>
+
+		<!-- Mount Flags -->
+		<HBox spacing="12" alignment="BASELINE_LEFT">
+			<CheckBox fx:id="useCustomMountFlags" text="%unlock.label.useCustomMountFlags"/>
+			<TextField fx:id="mountFlags" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
+		</HBox>
+
+		<!-- Windows Drive Letter -->
+		<HBox spacing="12" alignment="BASELINE_LEFT">
+			<CheckBox fx:id="useCustomWinDriveLetter" text="%unlock.label.winDriveLetter" onAction="#didClickCustomWinDriveLetterCheckbox"/>
+			<ChoiceBox fx:id="winDriveLetter" HBox.hgrow="NEVER" maxWidth="Infinity"/>
+		</HBox>
+	</VBox>
 
-			<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">
-				<Label text="%unlock.label.advancedHeading" style="-fx-background-color: COLOR_BACKGROUND;" cacheShape="true" cache="true">
-					<padding>
-						<Insets left="6.0" right="6.0"/>
-					</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 -->
-			<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" />
-
-			<!-- 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>
-				<Hyperlink fx:id="downloadsPageLink" text="%unlock.label.downloadsPageLink" visible="false" onAction="#didClickDownloadsLink" cacheShape="true" cache="true" />
-			</children>
-		</TextFlow>
-
-		<!-- 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="progressText" cache="true" />
-		</VBox>
-	</children>
-</GridPane>
+</VBox>

+ 2 - 3
main/ui/src/main/resources/localization/en.txt

@@ -74,14 +74,14 @@ upgrade.version5toX.msg=This vault needs to be migrated to a newer format.\nPlea
 unlock.label.password=Password
 unlock.label.savePassword=Save Password
 unlock.label.mountName=Drive Name
+unlock.label.useCustomMountFlags=Custom Mount Flags
 unlock.label.unlockAfterStartup=Auto-Unlock on Start (Experimental)
 unlock.label.revealAfterMount=Reveal Drive
 unlock.label.useReadOnlyMode=Read-Only
 unlock.label.winDriveLetter=Drive Letter
-unlock.label.useOwnMountPath=Use Custom Mount Point
+unlock.label.useOwnMountPath=Custom Mount Point
 unlock.label.chooseMountPath=Choose empty directory…
 unlock.label.downloadsPageLink=All Cryptomator versions
-unlock.label.advancedHeading=Advanced Options
 unlock.button.unlock=Unlock Vault
 unlock.button.advancedOptions.show=More Options
 unlock.button.advancedOptions.hide=Less Options
@@ -89,7 +89,6 @@ unlock.savePassword.delete.confirmation.title=Delete Saved Password
 unlock.savePassword.delete.confirmation.header=Do you really want to delete the saved password of this vault?
 unlock.savePassword.delete.confirmation.content=The saved password of this vault will be immediately deleted from your system keychain. If you'd like to save your password again, you'd have to unlock your vault with the "Save Password" option enabled.
 unlock.choicebox.winDriveLetter.auto=Assign automatically
-unlock.pendingMessage.unlocking=Unlocking vault...
 unlock.errorMessage.wrongPassword=Wrong password
 unlock.errorMessage.unlockFailed=Unlock failed. See log file for details.
 unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware=Unsupported vault. This vault has been created with an older version of Cryptomator.