Added save password functionality to UI

This commit is contained in:
Sebastian Stenzel
2016-09-02 15:49:09 +02:00
parent 06e526a961
commit ce12af8495
15 changed files with 121 additions and 89 deletions

View File

@@ -14,7 +14,7 @@ public interface KeychainAccess {
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
* @return The stored passphrase for the given key or <code>null</code> if no value for the given key could be found.
*/
CharSequence loadPassphrase(String key);
char[] loadPassphrase(String key);
/**
* Deletes a passphrase with a given key.

View File

@@ -1,7 +1,5 @@
package org.cryptomator.keychain;
import java.nio.CharBuffer;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -29,8 +27,8 @@ class MacSystemKeychainAccess implements KeychainAccessStrategy {
}
@Override
public CharSequence loadPassphrase(String key) {
return CharBuffer.wrap(keychain.loadPassword(key));
public char[] loadPassphrase(String key) {
return keychain.loadPassword(key);
}
@Override

View File

@@ -32,7 +32,7 @@ class WindowsSystemKeychainAccess implements KeychainAccessStrategy {
}
@Override
public CharSequence loadPassphrase(String key) {
public char[] loadPassphrase(String key) {
// TODO Auto-generated method stub
return null;
}

View File

@@ -8,7 +8,7 @@ import dagger.Component;
@Singleton
@Component(modules = KeychainModule.class)
public interface KeychainComponent {
interface KeychainComponent {
Optional<KeychainAccess> keychainAccess();

View File

@@ -5,15 +5,19 @@ import java.util.Map;
class MapKeychainAccess implements KeychainAccessStrategy {
private final Map<String, CharSequence> map = new HashMap<>();
private final Map<String, char[]> map = new HashMap<>();
@Override
public void storePassphrase(String key, CharSequence passphrase) {
map.put(key, passphrase);
char[] pw = new char[passphrase.length()];
for (int i = 0; i < passphrase.length(); i++) {
pw[i] = passphrase.charAt(i);
}
map.put(key, pw);
}
@Override
public CharSequence loadPassphrase(String key) {
public char[] loadPassphrase(String key) {
return map.get(key);
}

View File

@@ -58,7 +58,6 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
@@ -107,7 +106,6 @@
<artifactId>filesystem-stats</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>frontend-api</artifactId>
@@ -118,7 +116,11 @@
<artifactId>frontend-webdav</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>keychain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>

View File

@@ -58,6 +58,10 @@
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>keychain</artifactId>
</dependency>
<!-- CryptoLib -->
<dependency>

View File

@@ -25,6 +25,7 @@ import org.cryptomator.frontend.webdav.WebDavModule;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.cryptomator.jni.JniModule;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.keychain.KeychainModule;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultObjectMapperProvider;
import org.cryptomator.ui.model.Vaults;
@@ -42,7 +43,7 @@ import javafx.application.Application;
import javafx.beans.Observable;
import javafx.stage.Stage;
@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class})
@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class, KeychainModule.class})
class CryptomatorModule {
private static final Logger LOG = LoggerFactory.getLogger(CryptomatorModule.class);

View File

@@ -12,6 +12,7 @@ package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.Objects;
import java.util.Optional;
import javax.inject.Inject;
@@ -31,9 +32,7 @@ import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
@@ -49,9 +48,9 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
private final Application app;
private final PasswordStrengthUtil strengthRater;
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private Optional<ChangePasswordListener> listener = Optional.empty();
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4
private Optional<ChangePasswordListener> listener = Optional.empty();
private Vault vault;
@Inject
public ChangePasswordController(Application app, PasswordStrengthUtil strengthRater, Localization localization) {
@@ -101,7 +100,6 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
BooleanBinding oldPasswordIsEmpty = oldPasswordField.textProperty().isEmpty();
BooleanBinding newPasswordIsEmpty = newPasswordField.textProperty().isEmpty();
BooleanBinding passwordsDiffer = newPasswordField.textProperty().isNotEqualTo(retypePasswordField.textProperty());
EasyBind.subscribe(vault, this::vaultDidChange);
changePasswordButton.disableProperty().bind(oldPasswordIsEmpty.or(newPasswordIsEmpty.or(passwordsDiffer)));
passwordStrength.bind(EasyBind.map(newPasswordField.textProperty(), strengthRater::computeRate));
@@ -118,10 +116,11 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/change_password.fxml");
}
private void vaultDidChange(Vault newVault) {
oldPasswordField.clear();
newPasswordField.clear();
retypePasswordField.clear();
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
oldPasswordField.swipe();
newPasswordField.swipe();
retypePasswordField.swipe();
// trigger "default" change to refresh key bindings:
changePasswordButton.setDefaultButton(false);
changePasswordButton.setDefaultButton(true);
@@ -144,7 +143,7 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
private void didClickChangePasswordButton(ActionEvent event) {
downloadsPageLink.setVisible(false);
try {
vault.get().changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters());
vault.changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters());
messageText.setText(localization.getString("changePassword.infoMessage.success"));
listener.ifPresent(this::invokeListenerLater);
} catch (InvalidPassphraseException e) {

View File

@@ -13,6 +13,7 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.FileAlreadyExistsException;
import java.util.Objects;
import java.util.Optional;
import javax.inject.Inject;
@@ -29,9 +30,7 @@ import org.slf4j.LoggerFactory;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
@@ -44,9 +43,9 @@ public class InitializeController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
private final PasswordStrengthUtil strengthRater;
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private Optional<InitializationListener> listener = Optional.empty();
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4
private Optional<InitializationListener> listener = Optional.empty();
private Vault vault;
@Inject
public InitializeController(Localization localization, PasswordStrengthUtil strengthRater) {
@@ -88,7 +87,6 @@ public class InitializeController extends LocalizedFXMLViewController {
public void initialize() {
BooleanBinding passwordIsEmpty = passwordField.textProperty().isEmpty();
BooleanBinding passwordsDiffer = passwordField.textProperty().isNotEqualTo(retypePasswordField.textProperty());
EasyBind.subscribe(vault, this::vaultDidChange);
okButton.disableProperty().bind(passwordIsEmpty.or(passwordsDiffer));
passwordStrength.bind(EasyBind.map(passwordField.textProperty(), strengthRater::computeRate));
@@ -105,9 +103,10 @@ public class InitializeController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/initialize.fxml");
}
private void vaultDidChange(Vault newVault) {
passwordField.clear();
retypePasswordField.clear();
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
passwordField.swipe();
retypePasswordField.swipe();
// trigger "default" change to refresh key bindings:
okButton.setDefaultButton(false);
okButton.setDefaultButton(true);
@@ -121,7 +120,7 @@ public class InitializeController extends LocalizedFXMLViewController {
protected void initializeVault(ActionEvent event) {
final CharSequence passphrase = passwordField.getCharacters();
try {
vault.get().create(passphrase);
vault.create(passphrase);
listener.ifPresent(this::invokeListenerLater);
} catch (FileAlreadyExistsException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));

View File

@@ -315,7 +315,8 @@ public class MainController extends LocalizedFXMLViewController {
private void showInitializeView() {
final InitializeController ctrl = initializeController.get();
ctrl.vault.bind(selectedVault);
ctrl.loadFxml();
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didInitialize);
activeController.set(ctrl);
}
@@ -326,7 +327,8 @@ public class MainController extends LocalizedFXMLViewController {
private void showUpgradeView() {
final UpgradeController ctrl = upgradeController.get();
ctrl.vault.bind(selectedVault);
ctrl.loadFxml();
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didUpgrade);
activeController.set(ctrl);
}
@@ -337,7 +339,8 @@ public class MainController extends LocalizedFXMLViewController {
private void showUnlockView() {
final UnlockController ctrl = unlockController.get();
ctrl.vault.bind(selectedVault);
ctrl.loadFxml();
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didUnlock);
activeController.set(ctrl);
}
@@ -353,6 +356,7 @@ public class MainController extends LocalizedFXMLViewController {
final UnlockedController ctrl = unlockedVaults.computeIfAbsent(vault, k -> {
return unlockedControllerProvider.get();
});
ctrl.loadFxml();
ctrl.setVault(vault);
ctrl.setListener(this::didLock);
activeController.set(ctrl);
@@ -368,7 +372,8 @@ public class MainController extends LocalizedFXMLViewController {
private void showChangePasswordView() {
final ChangePasswordController ctrl = changePasswordController.get();
ctrl.vault.bind(selectedVault);
ctrl.loadFxml();
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didChangePassword);
activeController.set(ctrl);
}

View File

@@ -9,7 +9,9 @@
package org.cryptomator.ui.controllers;
import java.net.URL;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import javax.inject.Inject;
@@ -22,25 +24,24 @@ import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.frontend.FrontendCreationFailedException;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.webdav.mount.WindowsDriveLetters;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.AsyncTaskService;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dagger.Lazy;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
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;
@@ -61,22 +62,34 @@ public class UnlockController extends LocalizedFXMLViewController {
private final Settings settings;
private final WindowsDriveLetters driveLetters;
private final ChangeListener<Character> driveLetterChangeListener = this::winDriveLetterDidChange;
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private final Optional<KeychainAccess> keychainAccess;
private Vault vault;
private Optional<UnlockListener> listener = Optional.empty();
@Inject
public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy<FrontendFactory> frontendFactory, Settings settings, WindowsDriveLetters driveLetters) {
public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy<FrontendFactory> frontendFactory, Settings settings, WindowsDriveLetters driveLetters,
Optional<KeychainAccess> keychainAccess) {
super(localization);
this.app = app;
this.asyncTaskService = asyncTaskService;
this.frontendFactory = frontendFactory;
this.settings = settings;
this.driveLetters = driveLetters;
this.keychainAccess = keychainAccess;
}
@FXML
private SecPasswordField passwordField;
@FXML
private Button advancedOptionsButton;
@FXML
private Button unlockButton;
@FXML
private CheckBox savePassword;
@FXML
private TextField mountName;
@@ -86,12 +99,6 @@ public class UnlockController extends LocalizedFXMLViewController {
@FXML
private ChoiceBox<Character> winDriveLetter;
@FXML
private Button advancedOptionsButton;
@FXML
private Button unlockButton;
@FXML
private ProgressIndicator progressIndicator;
@@ -107,8 +114,10 @@ public class UnlockController extends LocalizedFXMLViewController {
@Override
public void initialize() {
advancedOptions.managedProperty().bind(advancedOptions.visibleProperty());
unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty());
mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
mountName.textProperty().addListener(this::mountNameDidChange);
savePassword.setDisable(!keychainAccess.isPresent());
if (SystemUtils.IS_OS_WINDOWS) {
winDriveLetter.setConverter(new WinDriveLetterLabelConverter());
} else {
@@ -117,9 +126,6 @@ public class UnlockController extends LocalizedFXMLViewController {
winDriveLetter.setVisible(false);
winDriveLetter.setManaged(false);
}
unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty());
EasyBind.subscribe(vault, this::vaultDidChange);
}
@Override
@@ -127,11 +133,15 @@ public class UnlockController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/unlock.fxml");
}
private void vaultDidChange(Vault newVault) {
if (newVault == null) {
void setVault(Vault vault) {
// trigger "default" change to refresh key bindings:
unlockButton.setDefaultButton(false);
unlockButton.setDefaultButton(true);
if (vault.equals(this.vault)) {
return;
}
passwordField.clear();
this.vault = Objects.requireNonNull(vault);
passwordField.swipe();
advancedOptions.setVisible(false);
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
progressIndicator.setVisible(false);
@@ -145,13 +155,21 @@ public class UnlockController extends LocalizedFXMLViewController {
}
downloadsPageLink.setVisible(false);
messageText.setText(null);
mountName.setText(newVault.getMountName());
mountName.setText(vault.getMountName());
if (SystemUtils.IS_OS_WINDOWS) {
chooseSelectedDriveLetter();
}
// trigger "default" change to refresh key bindings:
unlockButton.setDefaultButton(false);
unlockButton.setDefaultButton(true);
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.selectRange(storedPw.length, storedPw.length);
Arrays.fill(storedPw, ' ');
}
}
}
// ****************************************
@@ -188,14 +206,11 @@ public class UnlockController extends LocalizedFXMLViewController {
}
private void mountNameDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (vault.get() == null) {
return;
}
// newValue is guaranteed to be a-z0-9_, see #filterAlphanumericKeyEvents
if (newValue.isEmpty()) {
mountName.setText(vault.get().getMountName());
mountName.setText(vault.getMountName());
} else {
vault.get().setMountName(newValue);
vault.setMountName(newValue);
}
}
@@ -242,20 +257,17 @@ public class UnlockController extends LocalizedFXMLViewController {
}
private void winDriveLetterDidChange(ObservableValue<? extends Character> property, Character oldValue, Character newValue) {
if (vault.get() == null) {
return;
}
vault.get().setWinDriveLetter(newValue);
vault.setWinDriveLetter(newValue);
settings.save();
}
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.get().getWinDriveLetter())) {
vault.get().setWinDriveLetter(null);
if (driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) {
vault.setWinDriveLetter(null);
}
final Character letter = vault.get().getWinDriveLetter();
final Character letter = vault.getWinDriveLetter();
if (letter == null) {
// first option is known to be 'auto-assign' due to #WinDriveLetterComparator.
this.winDriveLetter.getSelectionModel().selectFirst();
@@ -275,10 +287,10 @@ public class UnlockController extends LocalizedFXMLViewController {
progressIndicator.setVisible(true);
downloadsPageLink.setVisible(false);
CharSequence password = passwordField.getCharacters();
asyncTaskService.asyncTaskOf(() -> this.unlock(vault.get(), password)).run();
asyncTaskService.asyncTaskOf(() -> this.unlock(password)).run();
}
private void unlock(Vault vault, CharSequence password) {
private void unlock(CharSequence password) {
try {
vault.activateFrontend(frontendFactory.get(), settings, password);
vault.reveal();
@@ -286,9 +298,15 @@ public class UnlockController extends LocalizedFXMLViewController {
messageText.setText(null);
listener.ifPresent(lstnr -> lstnr.didUnlock(vault));
});
if (keychainAccess.isPresent() && savePassword.isSelected()) {
keychainAccess.get().storePassphrase(vault.getId(), password);
} else {
passwordField.swipe();
}
} catch (InvalidPassphraseException e) {
Platform.runLater(() -> {
messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
passwordField.selectAll();
passwordField.requestFocus();
});
} catch (UnsupportedVaultFormatException e) {
@@ -307,7 +325,6 @@ public class UnlockController extends LocalizedFXMLViewController {
});
} finally {
Platform.runLater(() -> {
passwordField.swipe();
mountName.setDisable(false);
advancedOptionsButton.setDisable(false);
progressIndicator.setVisible(false);

View File

@@ -27,11 +27,11 @@ import javafx.scene.control.ProgressIndicator;
public class UpgradeController extends LocalizedFXMLViewController {
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
private final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
private final UpgradeStrategies strategies;
private final AsyncTaskService asyncTaskService;
private Optional<UpgradeListener> listener = Optional.empty();
private Vault vault;
@Inject
public UpgradeController(Localization localization, UpgradeStrategies strategies, AsyncTaskService asyncTaskService) {
@@ -67,8 +67,6 @@ public class UpgradeController extends LocalizedFXMLViewController {
BooleanExpression passwordProvided = passwordField.textProperty().isNotEmpty().and(passwordField.disabledProperty().not());
BooleanExpression syncFinished = confirmationCheckbox.selectedProperty();
upgradeButton.disableProperty().bind(passwordProvided.not().or(syncFinished.not()));
EasyBind.subscribe(vault, this::vaultDidChange);
}
@Override
@@ -76,9 +74,10 @@ public class UpgradeController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/upgrade.fxml");
}
private void vaultDidChange(Vault newVault) {
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
errorLabel.setText(null);
strategy.set(strategies.getUpgradeStrategy(newVault));
strategy.set(strategies.getUpgradeStrategy(vault));
// trigger "default" change to refresh key bindings:
upgradeButton.setDefaultButton(false);
upgradeButton.setDefaultButton(true);
@@ -89,7 +88,7 @@ public class UpgradeController extends LocalizedFXMLViewController {
// ****************************************
private String upgradeNotification(UpgradeStrategy instruction) {
return instruction.getNotification(vault.get());
return instruction.getNotification(vault);
}
// ****************************************
@@ -102,15 +101,14 @@ public class UpgradeController extends LocalizedFXMLViewController {
}
private void upgrade(UpgradeStrategy instruction) {
Vault v = Objects.requireNonNull(vault.getValue());
passwordField.setDisable(true);
progressIndicator.setVisible(true);
asyncTaskService //
.asyncTaskOf(() -> {
if (!instruction.isApplicable(v)) {
throw new IllegalStateException("No ugprade needed for " + v.path().getValue());
if (!instruction.isApplicable(vault)) {
throw new IllegalStateException("No ugprade needed for " + vault.path().getValue());
}
instruction.upgrade(v, passwordField.getCharacters());
instruction.upgrade(vault, passwordField.getCharacters());
}) //
.onSuccess(this::showNextUpgrade) //
.onError(UpgradeFailedException.class, e -> {
@@ -125,7 +123,7 @@ public class UpgradeController extends LocalizedFXMLViewController {
private void showNextUpgrade() {
errorLabel.setText(null);
Optional<UpgradeStrategy> nextStrategy = strategies.getUpgradeStrategy(vault.getValue());
Optional<UpgradeStrategy> nextStrategy = strategies.getUpgradeStrategy(vault);
if (nextStrategy.isPresent()) {
strategy.set(nextStrategy);
} else {

View File

@@ -68,12 +68,16 @@
</HBox>
<!-- Row 3.1 -->
<Label text="%unlock.label.mountName" GridPane.rowIndex="1" GridPane.columnIndex="0" cacheShape="true" cache="true" />
<TextField fx:id="mountName" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%unlock.label.savePassword" cacheShape="true" cache="true" />
<CheckBox GridPane.rowIndex="1" GridPane.columnIndex="1" fx:id="savePassword" cacheShape="true" cache="true" />
<!-- Row 3.2 -->
<Label fx:id="winDriveLetterLabel" text="%unlock.label.winDriveLetter" GridPane.rowIndex="2" GridPane.columnIndex="0" cacheShape="true" cache="true" />
<ChoiceBox fx:id="winDriveLetter" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%unlock.label.mountName" cacheShape="true" cache="true" />
<TextField GridPane.rowIndex="2" GridPane.columnIndex="1" fx:id="mountName" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
<!-- Row 3.3 -->
<Label GridPane.rowIndex="3" GridPane.columnIndex="0" fx:id="winDriveLetterLabel" text="%unlock.label.winDriveLetter" cacheShape="true" cache="true" />
<ChoiceBox GridPane.rowIndex="3" GridPane.columnIndex="1" fx:id="winDriveLetter" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
</GridPane>
<!-- Row 4 -->

View File

@@ -49,6 +49,7 @@ upgrade.version3to4.err.io=Migration failed due to an I/O Exception. See log fil
# unlock.fxml
unlock.label.password=Password
unlock.label.savePassword=Save password
unlock.label.mountName=Drive name
unlock.label.winDriveLetter=Drive letter
unlock.label.downloadsPageLink=All Cryptomator versions