Compare commits

...

30 Commits
1.5.2 ... 1.5.4

Author SHA1 Message Date
Sebastian Stenzel
aef33dc864 Merge branch 'release/1.5.4' 2020-05-12 16:26:44 +02:00
Sebastian Stenzel
06d2f2d9e9 Preparing 1.5.4 2020-05-12 16:21:50 +02:00
Sebastian Stenzel
dad0ad76fb New Crowdin translations (#1166)
[ci skip]
2020-05-12 16:20:49 +02:00
Sebastian Stenzel
99fa8e7c8e fixes #1171 (or rather applying a workaround until we get an upstream fix) 2020-05-12 16:18:11 +02:00
Martin Beyer
ab0f175edf Implemented hyperlink in preferences to reveal log files (#1184) 2020-05-12 11:59:22 +02:00
Ralph Plawetzki
8cb9728565 Implement changePassphrase from the secret-service API (#1191)
Fixes #1189
2020-05-12 07:41:10 +02:00
Ralph Plawetzki
d91d27f2a4 Close unlockScene after entering the phrase and unlocking the vault (#1186) 2020-05-12 07:40:38 +02:00
Sebastian Stenzel
b2a6e038ae Updated secret-service lib to 1.0.0
references #1169
2020-05-11 17:34:12 +02:00
Sebastian Stenzel
75f360903c recheck vault state when focusing window
fixes #1190
fixes #1110
fixes #1139
2020-05-11 08:08:15 +02:00
Sebastian Stenzel
79c3137b90 no need to be application-scoped 2020-05-11 07:58:29 +02:00
Sebastian Stenzel
d2189d379c Using switch expressions 2020-05-11 07:47:15 +02:00
Sebastian Stenzel
49aead7323 Merge branch 'feature/refactored-unlock' into develop 2020-05-08 15:08:46 +02:00
Sebastian Stenzel
7bd610563f exception not thrown here
[ci skip]
2020-05-08 09:30:21 +02:00
Sebastian Stenzel
5c1a1ad162 respect choice made in #1083 2020-05-07 16:57:57 +02:00
Sebastian Stenzel
86906d0049 fixes #1083 2020-05-07 16:45:24 +02:00
Sebastian Stenzel
93011dc754 cleanup 2020-05-07 16:33:05 +02:00
Sebastian Stenzel
b084b651af wipe memory when setting a new password 2020-05-07 15:57:56 +02:00
Sebastian Stenzel
fecf9c0423 Fixes #1088 2020-05-07 14:18:09 +02:00
Sebastian Stenzel
117fe78a4a Refactored stage creation 2020-05-07 12:35:01 +02:00
Tobias Hagemann
153d43573a Merge branch 'master' into develop 2020-04-30 16:51:48 +02:00
Tobias Hagemann
9145f5d2f8 Merge branch 'release/1.5.3' 2020-04-30 16:51:01 +02:00
Tobias Hagemann
835ea3b640 preparing 1.5.3 2020-04-30 16:50:16 +02:00
Sebastian Stenzel
1e7eb23d1b New Crowdin translations (#1164)
[ci skip]
2020-04-30 16:48:22 +02:00
Sebastian Stenzel
cfe25d0bf5 updating siv-mode for twice as fast filename encryption/decryption 2020-04-30 16:37:07 +02:00
Sebastian Stenzel
b14939bd77 New Crowdin translations (#1159)
[ci skip]
2020-04-30 16:33:33 +02:00
Tobias Hagemann
26e140ee22 fixed checkmark color if it's selected and disabled 2020-04-30 16:31:25 +02:00
Tobias Hagemann
1c5ecf8c01 centered main.vaultlist.emptyList.onboardingInstruction 2020-04-30 14:52:16 +02:00
Tobias Hagemann
9b528a05b5 localized display name of ui themes, now actually use vaultOptions.mount.winDriveLetterOccupied localizations 2020-04-30 14:42:51 +02:00
Sebastian Stenzel
3a7aa6d64f fixes #1163, fixes #1131 2020-04-30 13:41:18 +02:00
Sebastian Stenzel
55820e47f9 Merge branch 'master' into develop 2020-04-29 16:47:55 +02:00
96 changed files with 1102 additions and 611 deletions

View File

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

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.2</version>
<version>1.5.4</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator Commons</name>

View File

@@ -58,46 +58,22 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "directories":
settings.getDirectories().addAll(readVaultSettingsArray(in));
break;
case "askedForUpdateCheck":
settings.askedForUpdateCheck().set(in.nextBoolean());
break;
case "checkForUpdatesEnabled":
settings.checkForUpdates().set(in.nextBoolean());
break;
case "startHidden":
settings.startHidden().set(in.nextBoolean());
break;
case "port":
settings.port().set(in.nextInt());
break;
case "numTrayNotifications":
settings.numTrayNotifications().set(in.nextInt());
break;
case "preferredGvfsScheme":
settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
break;
case "debugMode":
settings.debugMode().set(in.nextBoolean());
break;
case "preferredVolumeImpl":
settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
break;
case "theme":
settings.theme().set(parseUiTheme(in.nextString()));
break;
case "uiOrientation":
settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
break;
case "licenseKey":
settings.licenseKey().set(in.nextString());
break;
default:
case "directories" -> settings.getDirectories().addAll(readVaultSettingsArray(in));
case "askedForUpdateCheck" -> settings.askedForUpdateCheck().set(in.nextBoolean());
case "checkForUpdatesEnabled" -> settings.checkForUpdates().set(in.nextBoolean());
case "startHidden" -> settings.startHidden().set(in.nextBoolean());
case "port" -> settings.port().set(in.nextInt());
case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt());
case "preferredGvfsScheme" -> settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
case "debugMode" -> settings.debugMode().set(in.nextBoolean());
case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
case "licenseKey" -> settings.licenseKey().set(in.nextString());
default -> {
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
break;
}
}
}
in.endObject();

View File

@@ -1,8 +1,8 @@
package org.cryptomator.common.settings;
public enum UiTheme {
LIGHT("Light"),
DARK("Dark");
LIGHT("preferences.general.theme.light"),
DARK("preferences.general.theme.dark");
// CUSTOM("Custom (%s)");
private String displayName;

View File

@@ -26,7 +26,6 @@ import java.util.Random;
/**
* The settings specific to a single vault.
* TODO: Change the name of individualMountPath and its derivatives to customMountPath
*/
public class VaultSettings {
@@ -36,6 +35,7 @@ public class VaultSettings {
public static final boolean DEFAULT_USES_READONLY_MODE = false;
public static final String DEFAULT_MOUNT_FLAGS = "";
public static final int DEFAULT_FILENAME_LENGTH_LIMIT = -1;
public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
private static final Random RNG = new Random();
@@ -45,11 +45,12 @@ public class VaultSettings {
private final StringProperty winDriveLetter = new SimpleStringProperty();
private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
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 useCustomMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
private final StringProperty customMountPath = new SimpleStringProperty();
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
private final IntegerProperty filenameLengthLimit = new SimpleIntegerProperty(DEFAULT_FILENAME_LENGTH_LIMIT);
private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
public VaultSettings(String id) {
this.id = Objects.requireNonNull(id);
@@ -58,7 +59,7 @@ public class VaultSettings {
}
Observable[] observables() {
return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit};
return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit, actionAfterUnlock};
}
private void deriveMountNameFromPath(Path path) {
@@ -122,17 +123,17 @@ public class VaultSettings {
return revealAfterMount;
}
public BooleanProperty usesIndividualMountPath() {
return usesIndividualMountPath;
public BooleanProperty useCustomMountPath() {
return useCustomMountPath;
}
public StringProperty individualMountPath() {
return individualMountPath;
public StringProperty customMountPath() {
return customMountPath;
}
public Optional<String> getIndividualMountPath() {
if (usesIndividualMountPath.get()) {
return Optional.ofNullable(Strings.emptyToNull(individualMountPath.get()));
public Optional<String> getCustomMountPath() {
if (useCustomMountPath.get()) {
return Optional.ofNullable(Strings.emptyToNull(customMountPath.get()));
} else {
return Optional.empty();
}
@@ -150,6 +151,14 @@ public class VaultSettings {
return filenameLengthLimit;
}
public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
return actionAfterUnlock;
}
public WhenUnlocked getActionAfterUnlock() {
return actionAfterUnlock.get();
}
/* Hashcode/Equals */
@Override

View File

@@ -25,11 +25,12 @@ class VaultSettingsJsonAdapter {
out.name("winDriveLetter").value(value.winDriveLetter().get());
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());
out.name("useCustomMountPath").value(value.useCustomMountPath().get());
out.name("customMountPath").value(value.customMountPath().get());
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
out.name("mountFlags").value(value.mountFlags().get());
out.name("filenameLengthLimit").value(value.filenameLengthLimit().get());
out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name());
out.endObject();
}
@@ -37,56 +38,36 @@ class VaultSettingsJsonAdapter {
String id = null;
String path = null;
String mountName = null;
String individualMountPath = null;
String customMountPath = null;
String winDriveLetter = null;
boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT;
boolean usesIndividualMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
int filenameLengthLimit = VaultSettings.DEFAULT_FILENAME_LENGTH_LIMIT;
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "id":
id = in.nextString();
break;
case "path":
path = in.nextString();
break;
case "mountName":
mountName = in.nextString();
break;
case "winDriveLetter":
winDriveLetter = in.nextString();
break;
case "unlockAfterStartup":
unlockAfterStartup = in.nextBoolean();
break;
case "revealAfterMount":
revealAfterMount = in.nextBoolean();
break;
case "usesIndividualMountPath":
usesIndividualMountPath = in.nextBoolean();
break;
case "individualMountPath":
individualMountPath = in.nextString();
break;
case "usesReadOnlyMode":
usesReadOnlyMode = in.nextBoolean();
break;
case "mountFlags":
mountFlags = in.nextString();
break;
case "filenameLengthLimit":
filenameLengthLimit = in.nextInt();
break;
default:
case "id" -> id = in.nextString();
case "path" -> path = in.nextString();
case "mountName" -> mountName = in.nextString();
case "winDriveLetter" -> winDriveLetter = in.nextString();
case "unlockAfterStartup" -> unlockAfterStartup = in.nextBoolean();
case "revealAfterMount" -> revealAfterMount = in.nextBoolean();
case "usesIndividualMountPath", "useCustomMountPath" -> useCustomMountPath = in.nextBoolean();
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
case "mountFlags" -> mountFlags = in.nextString();
case "filenameLengthLimit" -> filenameLengthLimit = in.nextInt();
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
default -> {
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
break;
}
}
}
in.endObject();
@@ -97,12 +78,22 @@ class VaultSettingsJsonAdapter {
vaultSettings.winDriveLetter().set(winDriveLetter);
vaultSettings.unlockAfterStartup().set(unlockAfterStartup);
vaultSettings.revealAfterMount().set(revealAfterMount);
vaultSettings.usesIndividualMountPath().set(usesIndividualMountPath);
vaultSettings.individualMountPath().set(individualMountPath);
vaultSettings.useCustomMountPath().set(useCustomMountPath);
vaultSettings.customMountPath().set(customMountPath);
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
vaultSettings.mountFlags().set(mountFlags);
vaultSettings.filenameLengthLimit().set(filenameLengthLimit);
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
return vaultSettings;
}
private WhenUnlocked parseActionAfterUnlock(String actionAfterUnlockName) {
try {
return WhenUnlocked.valueOf(actionAfterUnlockName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid action after unlock {}. Defaulting to {}.", actionAfterUnlockName, VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK);
return VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
}
}
}

View File

@@ -1,7 +1,5 @@
package org.cryptomator.common.settings;
import java.util.Arrays;
public enum VolumeImpl {
WEBDAV("WebDAV"),
FUSE("FUSE"),
@@ -17,18 +15,4 @@ public enum VolumeImpl {
return displayName;
}
/**
* Finds a VolumeImpl by display name.
*
* @param displayName Display name of the VolumeImpl
* @return VolumeImpl with the given <code>displayName</code>.
* @throws IllegalArgumentException if not volumeImpl with the given <code>displayName</code> was found.
*/
public static VolumeImpl forDisplayName(String displayName) throws IllegalArgumentException {
return Arrays.stream(values()) //
.filter(impl -> impl.displayName.equals(displayName)) //
.findAny() //
.orElseThrow(IllegalArgumentException::new);
}
}

View File

@@ -1,7 +1,5 @@
package org.cryptomator.common.settings;
import java.util.Arrays;
public enum WebDavUrlScheme {
DAV("dav", "dav:// (Gnome, Nautilus, ...)"),
WEBDAV("webdav", "webdav:// (KDE, Dolphin, ...)");
@@ -20,18 +18,4 @@ public enum WebDavUrlScheme {
public String getDisplayName() {
return displayName;
}
/**
* Finds a WebDavUrlScheme by prefix.
*
* @param prefix Prefix of the WebDavUrlScheme
* @return WebDavUrlScheme with the given <code>prefix</code>.
* @throws IllegalArgumentException if not WebDavUrlScheme with the given <code>prefix</code> was found.
*/
public static WebDavUrlScheme forPrefix(String prefix) throws IllegalArgumentException {
return Arrays.stream(values()) //
.filter(impl -> impl.prefix.equals(prefix)) //
.findAny() //
.orElseThrow(IllegalArgumentException::new);
}
}

View File

@@ -0,0 +1,17 @@
package org.cryptomator.common.settings;
public enum WhenUnlocked {
IGNORE("vaultOptions.general.actionAfterUnlock.ignore"),
REVEAL("vaultOptions.general.actionAfterUnlock.reveal"),
ASK("vaultOptions.general.actionAfterUnlock.ask");
private String displayName;
WhenUnlocked(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@@ -51,7 +51,7 @@ public class DokanyVolume implements Volume {
try {
this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, mountName, FS_TYPE_NAME, mountFlags.strip());
} catch (MountFailedException e) {
if (vaultSettings.getIndividualMountPath().isPresent()) {
if (vaultSettings.getCustomMountPath().isPresent()) {
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
}
throw new VolumeException("Unable to mount Filesystem", e);
@@ -59,7 +59,7 @@ public class DokanyVolume implements Volume {
}
private Path determineMountPoint() throws VolumeException, IOException {
Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
Optional<String> optionalCustomMountPoint = vaultSettings.getCustomMountPath();
if (optionalCustomMountPoint.isPresent()) {
Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
checkProvidedMountPoint(customMountPoint);

View File

@@ -45,7 +45,7 @@ public class FuseVolume implements Volume {
@Override
public void mount(CryptoFileSystem fs, String mountFlags) throws IOException, FuseNotSupportedException, VolumeException {
Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
Optional<String> optionalCustomMountPoint = vaultSettings.getCustomMountPath();
if (optionalCustomMountPoint.isPresent()) {
Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
checkProvidedMountPoint(customMountPoint);

View File

@@ -121,7 +121,7 @@ public class Vault {
}
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException {
if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
if (vaultSettings.useCustomMountPath().get() && Strings.isNullOrEmpty(vaultSettings.customMountPath().get())) {
throw new NotDirectoryException("");
}
CryptoFileSystem fs = getCryptoFileSystem(passphrase);

View File

@@ -92,8 +92,27 @@ public class VaultListManager {
}
return compBuilder.build().vault();
}
public static VaultState redetermineVaultState(Vault vault) {
VaultState previousState = vault.getState();
return switch (previousState) {
case LOCKED, NEEDS_MIGRATION, MISSING -> {
try {
VaultState determinedState = determineVaultState(vault.getPath());
vault.setState(determinedState);
yield determinedState;
} catch (IOException e) {
LOG.warn("Failed to determine vault state for " + vault.getPath(), e);
vault.setState(VaultState.ERROR);
vault.setLastKnownException(e);
yield VaultState.ERROR;
}
}
case ERROR, UNLOCKED, PROCESSING -> previousState;
};
}
public static VaultState determineVaultState(Path pathToVault) throws IOException {
private static VaultState determineVaultState(Path pathToVault) throws IOException {
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
return VaultState.MISSING;
} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {

View File

@@ -16,7 +16,6 @@ import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Paths;
import java.util.Arrays;
public class VaultSettingsJsonAdapterTest {
@@ -32,7 +31,7 @@ public class VaultSettingsJsonAdapterTest {
Assertions.assertEquals(Paths.get("/foo/bar"), vaultSettings.path().get());
Assertions.assertEquals("test", vaultSettings.mountName().get());
Assertions.assertEquals("X", vaultSettings.winDriveLetter().get());
Assertions.assertEquals("/home/test/crypto", vaultSettings.individualMountPath().get());
Assertions.assertEquals("/home/test/crypto", vaultSettings.customMountPath().get());
Assertions.assertEquals("--foo --bar", vaultSettings.mountFlags().get());

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.2</version>
<version>1.5.4</version>
</parent>
<artifactId>keychain</artifactId>
<name>System Keychain Access</name>
@@ -40,7 +40,6 @@
<dependency>
<groupId>de.swiesend</groupId>
<artifactId>secret-service</artifactId>
<version>1.0.0-RC.3</version>
</dependency>
<!-- Logging -->

View File

@@ -28,4 +28,11 @@ public interface KeychainAccess {
*/
void deletePassphrase(String key) throws KeychainAccessException;
/**
* Updates a passphrase with a given key.
*
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
* @param passphrase The secret to be updated in this keychain.
*/
void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException;
}

View File

@@ -48,4 +48,9 @@ public class LinuxSecretServiceKeychainAccess implements KeychainAccessStrategy
public void deletePassphrase(String key) throws KeychainAccessException {
delegate.orElseThrow(IllegalStateException::new).deletePassphrase(key);
}
@Override
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
delegate.orElseThrow(IllegalStateException::new).changePassphrase(key, passphrase);
}
}

View File

@@ -9,6 +9,8 @@ import java.util.Map;
class LinuxSecretServiceKeychainAccessImpl implements KeychainAccessStrategy {
private final String LABEL_FOR_SECRET_IN_KEYRING = "Cryptomator";
@Override
public boolean isSupported() {
try (@SuppressWarnings("unused") SimpleCollection keyring = new SimpleCollection()) {
@@ -24,7 +26,7 @@ class LinuxSecretServiceKeychainAccessImpl implements KeychainAccessStrategy {
try (SimpleCollection keyring = new SimpleCollection()) {
List<String> list = keyring.getItems(createAttributes(key));
if (list == null) {
keyring.createItem("Cryptomator", passphrase, createAttributes(key));
keyring.createItem(LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key));
}
} catch (IOException e) {
throw new KeychainAccessException(e);
@@ -57,6 +59,18 @@ class LinuxSecretServiceKeychainAccessImpl implements KeychainAccessStrategy {
}
}
@Override
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
try (SimpleCollection keyring = new SimpleCollection()) {
List<String> list = keyring.getItems(createAttributes(key));
if (list != null) {
keyring.updateItem(list.get(0), LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key));
}
} catch (IOException e) {
throw new KeychainAccessException(e);
}
}
private Map<String, String> createAttributes(String key) {
Map<String, String> attributes = new HashMap();
attributes.put("Vault", key);

View File

@@ -46,4 +46,9 @@ class MacSystemKeychainAccess implements KeychainAccessStrategy {
keychain().deletePassword(key);
}
@Override
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
storePassphrase(key, passphrase);
}
}

View File

@@ -112,6 +112,11 @@ class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
saveKeychainEntries();
}
@Override
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
storePassphrase(key, passphrase);
}
@Override
public boolean isSupported() {
return SystemUtils.IS_OS_WINDOWS && winFunctions.isPresent() && !keychainPaths.isEmpty();

View File

@@ -31,6 +31,12 @@ class MapKeychainAccess implements KeychainAccessStrategy {
map.remove(key);
}
@Override
public void changePassphrase(String key, CharSequence passphrase) {
map.get(key);
storePassphrase(key, passphrase);
}
@Override
public boolean isSupported() {
return true;

View File

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

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.2</version>
<version>1.5.4</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -24,15 +24,16 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>1.9.9</cryptomator.cryptofs.version>
<cryptomator.cryptofs.version>1.9.10</cryptomator.cryptofs.version>
<cryptomator.jni.version>2.2.2</cryptomator.jni.version>
<cryptomator.fuse.version>1.2.3</cryptomator.fuse.version>
<cryptomator.dokany.version>1.1.14</cryptomator.dokany.version>
<cryptomator.dokany.version>1.1.15</cryptomator.dokany.version>
<cryptomator.webdav.version>1.0.11</cryptomator.webdav.version>
<!-- 3rd party dependencies -->
<javafx.version>14</javafx.version>
<commons-lang3.version>3.9</commons-lang3.version>
<secret-service.version>1.0.0</secret-service.version>
<jwt.version>3.10.2</jwt.version>
<easybind.version>1.0.3</easybind.version>
<guava.version>28.2-jre</guava.version>
@@ -79,6 +80,11 @@
</dependency>
<!-- Cryptomator Libs -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>siv-mode</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
@@ -155,6 +161,13 @@
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!-- Linux System Keychain -->
<dependency>
<groupId>de.swiesend</groupId>
<artifactId>secret-service</artifactId>
<version>${secret-service.version}</version>
</dependency>
<!-- JWT -->
<dependency>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.2</version>
<version>1.5.4</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>

View File

@@ -21,6 +21,7 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.recoverykey.RecoveryKeyDisplayController;
@@ -51,13 +52,12 @@ public abstract class AddVaultModule {
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("addvaultwizard.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -27,7 +27,7 @@ public class AddVaultSuccessController implements FxController {
@FXML
public void unlockAndClose() {
close();
fxApplication.showUnlockWindow(vault.get());
fxApplication.startUnlockWorkflow(vault.get());
}
@FXML

View File

@@ -2,32 +2,28 @@ package org.cryptomator.ui.changepassword;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.util.ResourceBundle;
import java.nio.CharBuffer;
import java.util.Optional;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@@ -40,17 +36,19 @@ public class ChangePasswordController implements FxController {
private final Vault vault;
private final ObjectProperty<CharSequence> newPassword;
private final ErrorComponent.Builder errorComponent;
private final Optional<KeychainAccess> keychain;
public NiceSecurePasswordField oldPasswordField;
public CheckBox finalConfirmationCheckbox;
public Button finishButton;
@Inject
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent) {
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, Optional<KeychainAccess> keychain) {
this.window = window;
this.vault = vault;
this.newPassword = newPassword;
this.errorComponent = errorComponent;
this.keychain = keychain;
}
@FXML
@@ -69,8 +67,9 @@ public class ChangePasswordController implements FxController {
public void finish() {
try {
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
LOG.info("Successful changed password for {}", vault.getDisplayableName());
LOG.info("Successfully changed password for {}", vault.getDisplayableName());
window.close();
updatePasswordInSystemkeychain();
} catch (IOException e) {
LOG.error("IO error occured during password change. Unable to perform operation.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
@@ -81,6 +80,17 @@ public class ChangePasswordController implements FxController {
}
}
private void updatePasswordInSystemkeychain() {
if (keychain.isPresent()) {
try {
keychain.get().changePassphrase(vault.getId(), CharBuffer.wrap(newPassword.get()));
LOG.info("Successfully updated password in system keychain for {}", vault.getDisplayableName());
} catch (KeychainAccessException e) {
LOG.error("Failed to update password in system keychain.", e);
}
}
}
/* Getter/Setter */
public Vault getVault() {

View File

@@ -18,6 +18,7 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Named;
import javax.inject.Provider;
@@ -46,13 +47,12 @@ abstract class ChangePasswordModule {
@Provides
@ChangePasswordWindow
@ChangePasswordScoped
static Stage provideStage(@Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("changepassword.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -0,0 +1,26 @@
package org.cryptomator.ui.common;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.util.function.Consumer;
public class StageFactory {
private final Consumer<Stage> initializer;
public StageFactory(Consumer<Stage> initializer) {
this.initializer = initializer;
}
public Stage create() {
return create(StageStyle.DECORATED);
}
public Stage create(StageStyle stageStyle) {
Stage stage = new Stage(stageStyle);
initializer.accept(stage);
return stage;
}
}

View File

@@ -0,0 +1,51 @@
package org.cryptomator.ui.common;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UserInteractionLock<E extends Enum> {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private final BooleanProperty awaitingInteraction = new SimpleBooleanProperty();
private volatile E state;
public UserInteractionLock(E initialValue) {
state = initialValue;
}
public void interacted(E result) {
assert Platform.isFxApplicationThread();
lock.lock();
try {
state = result;
awaitingInteraction.set(false);
condition.signal();
} finally {
lock.unlock();
}
}
public E awaitInteraction() throws InterruptedException {
assert !Platform.isFxApplicationThread();
lock.lock();
try {
Platform.runLater(() -> awaitingInteraction.set(true));
condition.await();
return state;
} finally {
lock.unlock();
}
}
public ReadOnlyBooleanProperty awaitingInteraction() {
return awaitingInteraction;
}
}

View File

@@ -52,62 +52,6 @@ public class VaultService {
return task;
}
/**
* Attempts to unlock all given vaults in a background thread using passwords stored in the system keychain.
*
* @param vaults The vaults to unlock
* @implNote No-op if no system keychain is present
*/
public void attemptAutoUnlock(Collection<Vault> vaults) {
if (!keychain.isPresent()) {
LOG.debug("No system keychain found. Unable to auto unlock without saved passwords.");
} else {
List<Task<Vault>> unlockTasks = vaults.stream().map(v -> createAutoUnlockTask(v, keychain.get())).collect(Collectors.toList());
Task<Collection<Vault>> runSequentiallyTask = new RunSequentiallyTask(unlockTasks);
executorService.execute(runSequentiallyTask);
}
}
/**
* Creates but doesn't start an auto-unlock task.
*
* @param vault The vault to unlock
* @param keychainAccess The system keychain holding the passphrase for the vault
* @return The task
*/
public Task<Vault> createAutoUnlockTask(Vault vault, KeychainAccess keychainAccess) {
Task<Vault> task = new AutoUnlockVaultTask(vault, keychainAccess);
task.setOnSucceeded(evt -> LOG.info("Auto-unlocked {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to auto-unlock " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Unlocks a vault in a background thread
*
* @param vault The vault to unlock
* @param passphrase The password to use - wipe this param asap
* @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
*/
public void unlock(Vault vault, CharSequence passphrase) {
executorService.execute(createUnlockTask(vault, passphrase));
}
/**
* Creates but doesn't start an unlock task.
*
* @param vault The vault to unlock
* @param passphrase The password to use - wipe this param asap
* @return The task
* @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
*/
public Task<Vault> createUnlockTask(Vault vault, CharSequence passphrase) {
Task<Vault> task = new UnlockVaultTask(vault, passphrase);
task.setOnSucceeded(evt -> LOG.info("Unlocked {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to unlock " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Locks a vault in a background thread.
*
@@ -209,116 +153,6 @@ public class VaultService {
}
}
/**
* A task that runs a list of tasks in their given order
*/
private static class RunSequentiallyTask extends Task<Collection<Vault>> {
private final List<Task<Vault>> tasks;
public RunSequentiallyTask(List<Task<Vault>> tasks) {
this.tasks = List.copyOf(tasks);
}
@Override
protected List<Vault> call() throws ExecutionException, InterruptedException {
List<Vault> completed = new ArrayList<>();
for (Task<Vault> task : tasks) {
task.run();
Vault done = task.get();
completed.add(done);
}
return completed;
}
}
private static class AutoUnlockVaultTask extends Task<Vault> {
private final Vault vault;
private final KeychainAccess keychain;
public AutoUnlockVaultTask(Vault vault, KeychainAccess keychain) {
this.vault = vault;
this.keychain = keychain;
}
@Override
protected Vault call() throws Exception {
char[] storedPw = null;
try {
storedPw = keychain.loadPassphrase(vault.getId());
if (storedPw == null) {
throw new InvalidPassphraseException();
}
vault.unlock(CharBuffer.wrap(storedPw));
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
}
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.UNLOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.LOCKED);
}
}
private static class UnlockVaultTask extends Task<Vault> {
private final Vault vault;
private final CharBuffer passphrase;
/**
* @param vault The vault to unlock
* @param passphrase The password to use - wipe this param asap
* @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
*/
public UnlockVaultTask(Vault vault, CharSequence passphrase) {
this.vault = vault;
this.passphrase = CharBuffer.allocate(passphrase.length());
for (int i = 0; i < passphrase.length(); i++) {
this.passphrase.put(i, passphrase.charAt(i));
}
}
@Override
protected Vault call() throws Exception {
try {
vault.unlock(passphrase);
} finally {
Arrays.fill(passphrase.array(), ' ');
}
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.UNLOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.LOCKED);
}
}
/**
* A task that locks a vault
*/

View File

@@ -94,8 +94,8 @@ public class NiceSecurePasswordField extends StackPane {
passwordField.setPassword(password);
}
public void swipe() {
passwordField.swipe();
public void wipe() {
passwordField.wipe();
}
public void selectAll() {

View File

@@ -40,7 +40,7 @@ import java.util.Arrays;
*/
public class SecurePasswordField extends TextField {
private static final char SWIPE_CHAR = ' ';
private static final char WIPE_CHAR = ' ';
private static final int INITIAL_BUFFER_SIZE = 50;
private static final int GROW_BUFFER_SIZE = 50;
private static final String DEFAULT_PLACEHOLDER = "";
@@ -103,7 +103,7 @@ public class SecurePasswordField extends TextField {
if (e.getCode() == KeyCode.CAPS) {
updateCapsLocked();
} else if (SHORTCUT_BACKSPACE.match(e)) {
swipe();
wipe();
}
}
@@ -189,7 +189,7 @@ public class SecurePasswordField extends TextField {
if (length > content.length) {
char[] newContent = new char[length + GROW_BUFFER_SIZE];
System.arraycopy(content, 0, newContent, 0, content.length);
swipe(content);
wipe(content);
this.content = newContent;
}
}
@@ -201,7 +201,7 @@ public class SecurePasswordField extends TextField {
* @implNote The CharSequence will not copy the backing char[].
* Therefore any mutation to the SecurePasswordField's content will mutate or eventually swipe the returned CharSequence.
* @implSpec The CharSequence is usually in <a href="https://www.unicode.org/glossary/#normalization_form_c">NFC</a> representation (unless NFD-encoded char[] is set via {@link #setPassword(char[])}).
* @see #swipe()
* @see #wipe()
*/
@Override
public CharSequence getCharacters() {
@@ -220,7 +220,7 @@ public class SecurePasswordField extends TextField {
buf[i] = password.charAt(i);
}
setPassword(buf);
Arrays.fill(buf, SWIPE_CHAR);
Arrays.fill(buf, WIPE_CHAR);
}
/**
@@ -231,7 +231,7 @@ public class SecurePasswordField extends TextField {
* @param password
*/
public void setPassword(char[] password) {
swipe();
wipe();
content = Arrays.copyOf(password, password.length);
length = password.length;
@@ -242,14 +242,14 @@ public class SecurePasswordField extends TextField {
/**
* Destroys the stored password by overriding each character with a different character.
*/
public void swipe() {
swipe(content);
public void wipe() {
wipe(content);
length = 0;
setText(null);
}
private void swipe(char[] buffer) {
Arrays.fill(buffer, SWIPE_CHAR);
private void wipe(char[] buffer) {
Arrays.fill(buffer, WIPE_CHAR);
}
/* Observable Properties */

View File

@@ -17,6 +17,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Named;
import javax.inject.Provider;
@@ -37,13 +38,12 @@ abstract class ForgetPasswordModule {
@Provides
@ForgetPasswordWindow
@ForgetPasswordScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons, @Named("forgetPasswordOwner") Stage owner) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle, @Named("forgetPasswordOwner") Stage owner) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("forgetPassword.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -27,6 +27,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Provider;
import java.awt.desktop.QuitResponse;
import java.util.Optional;
@@ -38,24 +39,24 @@ public class FxApplication extends Application {
private final Settings settings;
private final Lazy<MainWindowComponent> mainWindow;
private final Lazy<PreferencesComponent> preferencesWindow;
private final UnlockComponent.Builder unlockWindowBuilder;
private final QuitComponent.Builder quitWindowBuilder;
private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
private final Provider<QuitComponent.Builder> quitWindowBuilderProvider;
private final Optional<MacFunctions> macFunctions;
private final VaultService vaultService;
private final LicenseHolder licenseHolder;
private final ObservableSet<Stage> visibleStages = FXCollections.observableSet();
private final BooleanBinding hasVisibleStages = Bindings.isNotEmpty(visibleStages);
private final BooleanBinding hasVisibleStages;
@Inject
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder) {
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<QuitComponent.Builder> quitWindowBuilderProvider, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder, ObservableSet<Stage> visibleStages) {
this.settings = settings;
this.mainWindow = mainWindow;
this.preferencesWindow = preferencesWindow;
this.unlockWindowBuilder = unlockWindowBuilder;
this.quitWindowBuilder = quitWindowBuilder;
this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
this.quitWindowBuilderProvider = quitWindowBuilderProvider;
this.macFunctions = macFunctions;
this.vaultService = vaultService;
this.licenseHolder = licenseHolder;
this.hasVisibleStages = Bindings.isNotEmpty(visibleStages);
}
public void start() {
@@ -73,11 +74,6 @@ public class FxApplication extends Application {
throw new UnsupportedOperationException("Use start() instead.");
}
private void addVisibleStage(Stage stage) {
visibleStages.add(stage);
stage.setOnHidden(evt -> visibleStages.remove(stage));
}
private void hasVisibleStagesChanged(@SuppressWarnings("unused") ObservableValue<? extends Boolean> observableValue, @SuppressWarnings("unused") boolean oldValue, boolean newValue) {
if (newValue) {
macFunctions.map(MacFunctions::uiState).ifPresent(MacApplicationUiState::transformToForegroundApplication);
@@ -88,32 +84,28 @@ public class FxApplication extends Application {
public void showPreferencesWindow(SelectedPreferencesTab selectedTab) {
Platform.runLater(() -> {
Stage stage = preferencesWindow.get().showPreferencesWindow(selectedTab);
addVisibleStage(stage);
preferencesWindow.get().showPreferencesWindow(selectedTab);
LOG.debug("Showing Preferences");
});
}
public void showMainWindow() {
Platform.runLater(() -> {
Stage stage = mainWindow.get().showMainWindow();
addVisibleStage(stage);
mainWindow.get().showMainWindow();
LOG.debug("Showing MainWindow");
});
}
public void showUnlockWindow(Vault vault) {
public void startUnlockWorkflow(Vault vault) {
Platform.runLater(() -> {
Stage stage = unlockWindowBuilder.vault(vault).build().showUnlockWindow();
addVisibleStage(stage);
unlockWindowBuilderProvider.get().vault(vault).build().startUnlockWorkflow();
LOG.debug("Showing UnlockWindow for {}", vault.getDisplayableName());
});
}
public void showQuitWindow(QuitResponse response) {
Platform.runLater(() -> {
Stage stage = quitWindowBuilder.quitResponse(response).build().showQuitWindow();
addVisibleStage(stage);
quitWindowBuilderProvider.get().quitResponse(response).build().showQuitWindow();
LOG.debug("Showing QuitWindow");
});
}
@@ -129,15 +121,14 @@ public class FxApplication extends Application {
private void loadSelectedStyleSheet(UiTheme desiredTheme) {
UiTheme theme = licenseHolder.isValidLicense() ? desiredTheme : UiTheme.LIGHT;
switch (theme) {
case DARK:
case DARK -> {
Application.setUserAgentStylesheet(getClass().getResource("/css/dark_theme.css").toString());
macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToDarkAqua));
break;
case LIGHT:
default:
}
case LIGHT -> {
Application.setUserAgentStylesheet(getClass().getResource("/css/light_theme.css").toString());
macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToAqua));
break;
}
}
}

View File

@@ -11,10 +11,14 @@ import dagger.Provides;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.quit.QuitComponent;
@@ -32,8 +36,8 @@ abstract class FxApplicationModule {
@Provides
@FxApplicationScoped
static ObjectProperty<Vault> provideSelectedVault() {
return new SimpleObjectProperty<>();
static ObservableSet<Stage> provideVisibleStages() {
return FXCollections.observableSet();
}
@Provides
@@ -43,7 +47,6 @@ abstract class FxApplicationModule {
if (SystemUtils.IS_OS_MAC) {
return Collections.emptyList();
}
try {
return List.of( //
createImageFromResource("/window_icon_32.png"), //
@@ -53,6 +56,21 @@ abstract class FxApplicationModule {
throw new UncheckedIOException("Failed to load embedded resource.", e);
}
}
@Provides
@FxApplicationScoped
static StageFactory provideStageFactory(@Named("windowIcons") List<Image> windowIcons, ObservableSet<Stage> visibleStages) {
return new StageFactory(stage -> {
stage.getIcons().addAll(windowIcons);
stage.showingProperty().addListener((observableValue, wasShowing, isShowing) -> {
if (isShowing) {
visibleStages.add(stage);
} else {
visibleStages.remove(stage);
}
});
});
}
private static Image createImageFromResource(String resourceName) throws IOException {
try (InputStream in = FxApplicationModule.class.getResourceAsStream(resourceName)) {

View File

@@ -52,19 +52,13 @@ class AppLaunchEventHandler {
private void handleLaunchEvent(boolean hasTrayIcon, AppLaunchEvent event) {
switch (event.getType()) {
case REVEAL_APP:
fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow);
break;
case OPEN_FILE:
fxApplicationStarter.get(hasTrayIcon).thenRun(() -> {
case REVEAL_APP -> fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow);
case OPEN_FILE -> fxApplicationStarter.get(hasTrayIcon).thenRun(() -> {
Platform.runLater(() -> {
event.getPathsToOpen().forEach(this::addVault);
});
});
break;
default:
LOG.warn("Unsupported event type: {}", event.getType());
break;
default -> LOG.warn("Unsupported event type: {}", event.getType());
}
}

View File

@@ -64,7 +64,11 @@ public class UiLauncher {
// auto unlock
Collection<Vault> vaultsWithAutoUnlockEnabled = vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get());
if (!vaultsWithAutoUnlockEnabled.isEmpty()) {
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> app.getVaultService().attemptAutoUnlock(vaultsWithAutoUnlockEnabled));
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> {
for (Vault vault : vaultsWithAutoUnlockEnabled){
app.startUnlockWorkflow(vault);
}
});
}
launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);

View File

@@ -1,12 +1,17 @@
package org.cryptomator.ui.mainwindow;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
@@ -28,15 +33,19 @@ public class MainWindowController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(MainWindowController.class);
private final Stage window;
private final VaultListManager vaultListManager;
private final ReadOnlyObjectProperty<Vault> selectedVault;
private final WrongFileAlertComponent.Builder wrongFileAlert;
private final BooleanProperty draggingOver = new SimpleBooleanProperty();
private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
public StackPane root;
@Inject
public MainWindowController(VaultListManager vaultListManager, WrongFileAlertComponent.Builder wrongFileAlert) {
public MainWindowController(@MainWindow Stage window, VaultListManager vaultListManager, ObjectProperty<Vault> selectedVault, WrongFileAlertComponent.Builder wrongFileAlert) {
this.window = window;
this.vaultListManager = vaultListManager;
this.selectedVault = selectedVault;
this.wrongFileAlert = wrongFileAlert;
}
@@ -50,6 +59,14 @@ public class MainWindowController implements FxController {
if (SystemUtils.IS_OS_WINDOWS) {
root.getStyleClass().add("os-windows");
}
window.focusedProperty().addListener(this::mainWindowFocusChanged);
}
private void mainWindowFocusChanged(Observable observable) {
var v = selectedVault.get();
if (v != null) {
VaultListManager.redetermineVaultState(v);
}
}
private void handleDragEvent(DragEvent event) {

View File

@@ -4,16 +4,21 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.cryptomator.ui.migration.MigrationComponent;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
@@ -28,6 +33,12 @@ import java.util.ResourceBundle;
@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, WrongFileAlertComponent.class})
abstract class MainWindowModule {
@Provides
@MainWindowScoped
static ObjectProperty<Vault> provideSelectedVault() {
return new SimpleObjectProperty<>();
}
@Provides
@MainWindow
@MainWindowScoped
@@ -38,22 +49,21 @@ abstract class MainWindowModule {
@Provides
@MainWindow
@MainWindowScoped
static Stage provideStage(@Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage(StageStyle.UNDECORATED);
static Stage provideStage(StageFactory factory) {
Stage stage = factory.create(StageStyle.UNDECORATED);
// TODO: min/max values chosen arbitrarily. We might wanna take a look at the user's resolution...
stage.setMinWidth(650);
stage.setMinHeight(440);
stage.setMaxWidth(1000);
stage.setMaxHeight(700);
stage.setTitle("Cryptomator");
stage.getIcons().addAll(windowIcons);
return stage;
}
@Provides
@FxmlScene(FxmlFile.MAIN_WINDOW)
@MainWindowScoped
static Scene provideMainScene(@MainWindow FXMLLoaderFactory fxmlLoaders, MainWindowController mainWindowController, VaultListController vaultListController) {
static Scene provideMainScene(@MainWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/main_window.fxml");
}

View File

@@ -26,7 +26,7 @@ public class VaultDetailLockedController implements FxController {
@FXML
public void unlock() {
application.showUnlockWindow(vault.get());
application.startUnlockWorkflow(vault.get());
}
@FXML

View File

@@ -64,21 +64,7 @@ public class VaultListController implements FxController {
if (newValue == null) {
return;
}
VaultState reportedState = newValue.getState();
switch (reportedState) {
case LOCKED, NEEDS_MIGRATION, MISSING:
try {
VaultState determinedState = VaultListManager.determineVaultState(newValue.getPath());
newValue.setState(determinedState);
} catch (IOException e) {
LOG.warn("Failed to determine vault state for " + newValue.getPath(), e);
newValue.setState(VaultState.ERROR);
newValue.setLastKnownException(e);
}
break;
case ERROR, UNLOCKED, PROCESSING:
break; // no-op
}
VaultListManager.redetermineVaultState(newValue);
}
@FXML

View File

@@ -17,6 +17,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Named;
@@ -38,13 +39,12 @@ abstract class MigrationModule {
@Provides
@MigrationWindow
@MigrationScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("migration.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -121,7 +121,7 @@ public class MigrationRunController implements FxController {
} else {
LOG.info("Migration of '{}' succeeded.", vault.getDisplayableName());
vault.setState(VaultState.LOCKED);
passwordField.swipe();
passwordField.wipe();
window.setScene(successScene.get());
}
}).onError(InvalidPassphraseException.class, e -> {

View File

@@ -1,8 +1,5 @@
package org.cryptomator.ui.migration;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
@@ -28,7 +25,7 @@ public class MigrationSuccessController implements FxController {
@FXML
public void unlockAndClose() {
close();
fxApplication.showUnlockWindow(vault);
fxApplication.startUnlockWorkflow(vault);
}
@FXML

View File

@@ -12,9 +12,11 @@ import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.util.StringConverter;
import javafx.application.Application;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.Environment;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -22,6 +24,7 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
@PreferencesScoped
@@ -35,6 +38,9 @@ public class GeneralPreferencesController implements FxController {
private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
private final LicenseHolder licenseHolder;
private final ExecutorService executor;
private final ResourceBundle resourceBundle;
private final Application application;
private final Environment environment;
public ChoiceBox<UiTheme> themeChoiceBox;
public CheckBox startHiddenCheckbox;
public CheckBox debugModeCheckbox;
@@ -44,20 +50,23 @@ public class GeneralPreferencesController implements FxController {
public RadioButton nodeOrientationRtl;
@Inject
GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartStrategy> autoStartStrategy, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor) {
GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartStrategy> autoStartStrategy, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment) {
this.settings = settings;
this.trayMenuSupported = trayMenuSupported;
this.autoStartStrategy = autoStartStrategy;
this.selectedTabProperty = selectedTabProperty;
this.licenseHolder = licenseHolder;
this.executor = executor;
this.resourceBundle = resourceBundle;
this.application = application;
this.environment = environment;
}
@FXML
public void initialize() {
themeChoiceBox.getItems().addAll(UiTheme.values());
themeChoiceBox.valueProperty().bindBidirectional(settings.theme());
themeChoiceBox.setConverter(new UiThemeConverter());
themeChoiceBox.setConverter(new UiThemeConverter(resourceBundle));
startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden());
@@ -112,13 +121,24 @@ public class GeneralPreferencesController implements FxController {
selectedTabProperty.set(SelectedPreferencesTab.DONATION_KEY);
}
@FXML
public void showLogfileDirectory(){
environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
}
/* Helper classes */
private static class UiThemeConverter extends StringConverter<UiTheme> {
private final ResourceBundle resourceBundle;
UiThemeConverter(ResourceBundle resourceBundle) {
this.resourceBundle = resourceBundle;
}
@Override
public String toString(UiTheme impl) {
return impl.getDisplayName();
return resourceBundle.getString(impl.getDisplayName());
}
@Override

View File

@@ -15,6 +15,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Named;
import javax.inject.Provider;
@@ -41,11 +42,10 @@ abstract class PreferencesModule {
@Provides
@PreferencesWindow
@PreferencesScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("preferences.title"));
stage.setResizable(false);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -17,6 +17,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Named;
import javax.inject.Provider;
@@ -37,12 +38,11 @@ abstract class QuitModule {
@Provides
@QuitWindow
@QuitScoped
static Stage provideStage(@Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory) {
Stage stage = factory.create();
stage.setMinWidth(300);
stage.setMinHeight(100);
stage.initModality(Modality.APPLICATION_MODAL);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -21,6 +21,7 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Named;
import javax.inject.Provider;
@@ -41,13 +42,12 @@ abstract class RecoveryKeyModule {
@Provides
@RecoveryKeyWindow
@RecoveryKeyScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons, @Named("keyRecoveryOwner") Stage owner) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle, @Named("keyRecoveryOwner") Stage owner) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("recoveryKey.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -17,6 +17,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Named;
@@ -38,13 +39,12 @@ abstract class RemoveVaultModule {
@Provides
@RemoveVaultWindow
@RemoveVaultScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("removeVault.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -103,7 +103,7 @@ class TrayMenuController {
}
private void unlockVault(Vault vault) {
fxApplicationStarter.get(true).thenAccept(app -> app.showUnlockWindow(vault));
fxApplicationStarter.get(true).thenAccept(app -> app.startUnlockWorkflow(vault));
}
private void lockVault(Vault vault) {

View File

@@ -14,21 +14,23 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.common.vaults.Vault;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
@UnlockScoped
@Subcomponent(modules = {UnlockModule.class})
public interface UnlockComponent {
@UnlockWindow
Stage window();
ExecutorService defaultExecutorService();
@FxmlScene(FxmlFile.UNLOCK)
Lazy<Scene> scene();
default Stage showUnlockWindow() {
Stage stage = window();
stage.setScene(scene().get());
stage.show();
return stage;
UnlockWorkflow unlockWorkflow();
default Future<Boolean> startUnlockWorkflow() {
UnlockWorkflow workflow = unlockWorkflow();
defaultExecutorService().submit(workflow);
return workflow;
}
@Subcomponent.Builder

View File

@@ -1,39 +1,30 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.NotDirectoryException;
import javax.inject.Named;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@UnlockScoped
public class UnlockController implements FxController {
@@ -42,124 +33,70 @@ public class UnlockController implements FxController {
private final Stage window;
private final Vault vault;
private final ExecutorService executor;
private final ObjectBinding<ContentDisplay> unlockButtonState;
private final Optional<KeychainAccess> keychainAccess;
private final VaultService vaultService;
private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene;
private final ErrorComponent.Builder errorComponent;
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock;
private final ForgetPasswordComponent.Builder forgetPassword;
private final Optional<KeychainAccess> keychainAccess;
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
private final BooleanBinding userInteractionDisabled;
private final BooleanProperty unlockButtonDisabled;
public NiceSecurePasswordField passwordField;
public CheckBox savePassword;
public CheckBox savePasswordCheckbox;
@Inject
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional<KeychainAccess> keychainAccess, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, ForgetPasswordComponent.Builder forgetPassword) {
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, Optional<KeychainAccess> keychainAccess) {
this.window = window;
this.vault = vault;
this.executor = executor;
this.unlockButtonState = Bindings.createObjectBinding(this::getUnlockButtonState, vault.stateProperty());
this.keychainAccess = keychainAccess;
this.vaultService = vaultService;
this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene;
this.errorComponent = errorComponent;
this.password = password;
this.savePassword = savePassword;
this.savedPassword = savedPassword;
this.passwordEntryLock = passwordEntryLock;
this.forgetPassword = forgetPassword;
this.keychainAccess = keychainAccess;
this.unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, passwordEntryLock.awaitingInteraction());
this.userInteractionDisabled = passwordEntryLock.awaitingInteraction().not();
this.unlockButtonDisabled = new SimpleBooleanProperty();
}
public void initialize() {
if (keychainAccess.isPresent()) {
loadStoredPassword();
} else {
savePassword.setSelected(false);
savePasswordCheckbox.setSelected(savedPassword.isPresent());
if (password.get() != null) {
passwordField.setPassword(password.get());
}
unlockButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.LOCKED).or(passwordField.textProperty().isEmpty()));
unlockButtonDisabled.bind(userInteractionDisabled.or(passwordField.textProperty().isEmpty()));
}
@FXML
public void cancel() {
LOG.debug("Unlock canceled by user.");
window.close();
passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED);
}
@FXML
public void unlock() {
LOG.trace("UnlockController.unlock()");
CharSequence password = passwordField.getCharacters();
Task<Vault> task = vaultService.createUnlockTask(vault, password);
passwordField.setDisable(true);
task.setOnSucceeded(event -> {
passwordField.setDisable(false);
if (keychainAccess.isPresent() && savePassword.isSelected()) {
try {
keychainAccess.get().storePassphrase(vault.getId(), password);
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
passwordField.swipe();
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
window.setScene(successScene.get());
});
task.setOnFailed(event -> {
passwordField.setDisable(false);
if (task.getException() instanceof InvalidPassphraseException) {
Animations.createShakeWindowAnimation(window).play();
passwordField.selectAll();
passwordField.requestFocus();
} else if (task.getException() instanceof NotDirectoryException || task.getException() instanceof DirectoryNotEmptyException) {
LOG.error("Unlock failed. Mount point not an empty directory: {}", task.getException().getMessage());
window.setScene(invalidMountPointScene.get());
} else {
LOG.error("Unlock failed for technical reasons.", task.getException());
errorComponent.cause(task.getException()).window(window).returnToScene(window.getScene()).build().showErrorScene();
}
});
executor.execute(task);
CharSequence pwFieldContents = passwordField.getCharacters();
char[] newPw = new char[pwFieldContents.length()];
for (int i = 0; i < pwFieldContents.length(); i++) {
newPw[i] = pwFieldContents.charAt(i);
}
char[] oldPw = password.getAndSet(newPw);
if (oldPw != null) {
Arrays.fill(oldPw, ' ');
}
passwordEntryLock.interacted(UnlockModule.PasswordEntry.PASSWORD_ENTERED);
}
/* Save Password */
@FXML
private void didClickSavePasswordCheckbox() {
if (!savePassword.isSelected() && hasStoredPassword()) {
forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePassword.setSelected(!forgotten));
}
}
private void loadStoredPassword() {
assert keychainAccess.isPresent();
char[] storedPw = null;
try {
storedPw = keychainAccess.get().loadPassphrase(vault.getId());
if (storedPw != null) {
savePassword.setSelected(true);
passwordField.setPassword(storedPw);
passwordField.selectRange(storedPw.length, storedPw.length);
}
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
}
}
private boolean hasStoredPassword() {
char[] storedPw = null;
try {
storedPw = keychainAccess.get().loadPassphrase(vault.getId());
return storedPw != null;
} catch (KeychainAccessException e) {
return false;
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
savePassword.set(savePasswordCheckbox.isSelected());
if (!savePasswordCheckbox.isSelected() && savedPassword.isPresent()) {
forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePasswordCheckbox.setSelected(!forgotten));
}
}
@@ -169,15 +106,20 @@ public class UnlockController implements FxController {
return vault;
}
public ObjectBinding<ContentDisplay> unlockButtonStateProperty() {
return unlockButtonState;
public ObjectBinding<ContentDisplay> unlockButtonContentDisplayProperty() {
return unlockButtonContentDisplay;
}
public ContentDisplay getUnlockButtonState() {
return switch (vault.getState()) {
case PROCESSING -> ContentDisplay.LEFT;
default -> ContentDisplay.TEXT_ONLY;
};
public ContentDisplay getUnlockButtonContentDisplay() {
return passwordEntryLock.awaitingInteraction().get() ? ContentDisplay.TEXT_ONLY : ContentDisplay.LEFT;
}
public BooleanBinding userInteractionDisabledProperty() {
return userInteractionDisabled;
}
public boolean isUserInteractionDisabled() {
return userInteractionDisabled.get();
}
public ReadOnlyBooleanProperty unlockButtonDisabledProperty() {

View File

@@ -33,7 +33,7 @@ public class UnlockInvalidMountPointController implements FxController {
/* Getter/Setter */
public String getMountPoint() {
return vault.getVaultSettings().getIndividualMountPath().orElse("AUTO");
return vault.getVaultSettings().getCustomMountPath().orElse("AUTO");
}
}

View File

@@ -9,23 +9,73 @@ import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Provider;
import java.nio.CharBuffer;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Module(subcomponents = {ForgetPasswordComponent.class})
abstract class UnlockModule {
private static final Logger LOG = LoggerFactory.getLogger(UnlockModule.class);
public enum PasswordEntry {PASSWORD_ENTERED, CANCELED}
@Provides
@UnlockScoped
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
return new UserInteractionLock<>(null);
}
@Provides
@Named("savedPassword")
@UnlockScoped
static Optional<char[]> provideStoredPassword(Optional<KeychainAccess> keychainAccess, @UnlockWindow Vault vault) {
return keychainAccess.map(k -> {
try {
return k.loadPassphrase(vault.getId());
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
return null;
}
});
}
@Provides
@UnlockScoped
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicReference(storedPassword.orElse(null));
}
@Provides
@Named("savePassword")
@UnlockScoped
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicBoolean(storedPassword.isPresent());
}
@Provides
@UnlockWindow
@UnlockScoped
@@ -36,12 +86,11 @@ abstract class UnlockModule {
@Provides
@UnlockWindow
@UnlockScoped
static Stage provideStage(@UnlockWindow Vault vault, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @UnlockWindow Vault vault) {
Stage stage = factory.create();
stage.setTitle(vault.getDisplayableName());
stage.setResizable(false);
stage.initModality(Modality.APPLICATION_MODAL);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -7,8 +7,10 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import org.cryptomator.common.settings.WhenUnlocked;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;
@@ -29,6 +31,8 @@ public class UnlockSuccessController implements FxController {
private final VaultService vaultService;
private final ObjectProperty<ContentDisplay> revealButtonState;
private final BooleanProperty revealButtonDisabled;
public CheckBox rememberChoiceCheckbox;
@Inject
public UnlockSuccessController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, VaultService vaultService) {
@@ -44,6 +48,9 @@ public class UnlockSuccessController implements FxController {
public void close() {
LOG.trace("UnlockSuccessController.close()");
window.close();
if (rememberChoiceCheckbox.isSelected()) {
vault.getVaultSettings().actionAfterUnlock().setValue(WhenUnlocked.IGNORE);
}
}
@FXML
@@ -64,6 +71,9 @@ public class UnlockSuccessController implements FxController {
revealButtonDisabled.set(false);
});
executor.execute(revealTask);
if (rememberChoiceCheckbox.isSelected()) {
vault.getVaultSettings().actionAfterUnlock().setValue(WhenUnlocked.REVEAL);
}
}
/* Getter/Setter */

View File

@@ -0,0 +1,190 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileSystemException;
import java.nio.file.NotDirectoryException;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* A multi-step task that consists of background activities as well as user interaction.
* <p>
* This class runs the unlock process and controls when to display which UI.
*/
@UnlockScoped
public class UnlockWorkflow extends Task<Boolean> {
private static final Logger LOG = LoggerFactory.getLogger(UnlockWorkflow.class);
private final Stage window;
private final Vault vault;
private final VaultService vaultService;
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final Optional<KeychainAccess> keychain;
private final Lazy<Scene> unlockScene;
private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene;
private final ErrorComponent.Builder errorComponent;
@Inject
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, Optional<KeychainAccess> keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent) {
this.window = window;
this.vault = vault;
this.vaultService = vaultService;
this.password = password;
this.savePassword = savePassword;
this.savedPassword = savedPassword;
this.passwordEntryLock = passwordEntryLock;
this.keychain = keychain;
this.unlockScene = unlockScene;
this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene;
this.errorComponent = errorComponent;
}
@Override
protected Boolean call() throws InterruptedException, IOException, Volume.VolumeException {
try {
if (attemptUnlock()) {
handleSuccess();
return true;
} else {
cancel(false); // set Tasks state to cancelled
return false;
}
} catch (NotDirectoryException | DirectoryNotEmptyException e) {
handleInvalidMountPoint(e);
throw e; // rethrow to trigger correct exception handling in Task
} catch (CryptoException | Volume.VolumeException | IOException e) {
handleGenericError(e);
throw e; // rethrow to trigger correct exception handling in Task
} finally {
wipePassword(password.get());
wipePassword(savedPassword.orElse(null));
}
}
private boolean attemptUnlock() throws InterruptedException, IOException, Volume.VolumeException {
boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
while (proceed) {
try {
vault.unlock(CharBuffer.wrap(password.get()));
return true;
} catch (InvalidPassphraseException e) {
proceed = askForPassword(true) == PasswordEntry.PASSWORD_ENTERED;
}
}
return false;
}
private PasswordEntry askForPassword(boolean animateShake) throws InterruptedException {
Platform.runLater(() -> {
window.setScene(unlockScene.get());
window.show();
if (animateShake) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
}
private void handleSuccess() {
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
if (savePassword.get()) {
savePasswordToSystemkeychain();
}
switch (vault.getVaultSettings().actionAfterUnlock().get()) {
case ASK -> Platform.runLater(() -> {
window.setScene(successScene.get());
window.show();
});
case REVEAL -> {
Platform.runLater(window::close);
vaultService.reveal(vault);
}
case IGNORE -> Platform.runLater(window::close);
}
}
private void savePasswordToSystemkeychain() {
if (keychain.isPresent()) {
try {
keychain.get().storePassphrase(vault.getId(), CharBuffer.wrap(password.get()));
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
}
private void handleInvalidMountPoint(FileSystemException e) {
LOG.error("Unlock failed. Mount point not an empty directory: {}", e.getMessage());
Platform.runLater(() -> {
window.setScene(invalidMountPointScene.get());
});
}
private void handleGenericError(Exception e) {
LOG.error("Unlock failed for technical reasons.", e);
Platform.runLater(() -> {
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
});
}
private void wipePassword(char[] pw) {
if (pw != null) {
Arrays.fill(pw, ' ');
}
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.UNLOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.LOCKED);
}
@Override
protected void cancelled() {
vault.setState(VaultState.LOCKED);
}
}

View File

@@ -2,24 +2,56 @@ package org.cryptomator.ui.vaultoptions;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.util.StringConverter;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.settings.WhenUnlocked;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
import java.util.ResourceBundle;
@VaultOptionsScoped
public class GeneralVaultOptionsController implements FxController {
private final Vault vault;
private final ResourceBundle resourceBundle;
public CheckBox unlockOnStartupCheckbox;
public ChoiceBox<WhenUnlocked> actionAfterUnlockChoiceBox;
@Inject
GeneralVaultOptionsController(@VaultOptionsWindow Vault vault) {
GeneralVaultOptionsController(@VaultOptionsWindow Vault vault, ResourceBundle resourceBundle) {
this.vault = vault;
this.resourceBundle = resourceBundle;
}
@FXML
public void initialize() {
unlockOnStartupCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().unlockAfterStartup());
actionAfterUnlockChoiceBox.getItems().addAll(WhenUnlocked.values());
actionAfterUnlockChoiceBox.valueProperty().bindBidirectional(vault.getVaultSettings().actionAfterUnlock());
actionAfterUnlockChoiceBox.setConverter(new WhenUnlockedConverter(resourceBundle));
}
private static class WhenUnlockedConverter extends StringConverter<WhenUnlocked> {
private final ResourceBundle resourceBundle;
public WhenUnlockedConverter(ResourceBundle resourceBundle) {
this.resourceBundle = resourceBundle;
}
@Override
public String toString(WhenUnlocked obj) {
return resourceBundle.getString(obj.getDisplayName());
}
@Override
public WhenUnlocked fromString(String string) {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -82,10 +82,10 @@ public class MountOptionsController implements FxController {
// mount point options:
mountPoint.selectedToggleProperty().addListener(this::toggleMountPoint);
driveLetterSelection.getItems().addAll(windowsDriveLetters.getAllDriveLetters());
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters));
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle));
driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get());
if (vault.getVaultSettings().usesIndividualMountPath().get()) {
if (vault.getVaultSettings().useCustomMountPath().get()) {
mountPoint.selectToggle(mountPointCustomDir);
} else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) {
mountPoint.selectToggle(mountPointWinDriveLetter);
@@ -93,7 +93,7 @@ public class MountOptionsController implements FxController {
mountPoint.selectToggle(mountPointAuto);
}
vault.getVaultSettings().usesIndividualMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
vault.getVaultSettings().useCustomMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
vault.getVaultSettings().winDriveLetter().bind( //
Bindings.when(mountPoint.selectedToggleProperty().isEqualTo(mountPointWinDriveLetter)) //
.then(driveLetterSelection.getSelectionModel().selectedItemProperty()) //
@@ -126,14 +126,14 @@ public class MountOptionsController implements FxController {
}
File file = directoryChooser.showDialog(window);
if (file != null) {
vault.getVaultSettings().individualMountPath().set(file.getAbsolutePath());
vault.getVaultSettings().customMountPath().set(file.getAbsolutePath());
} else {
vault.getVaultSettings().individualMountPath().set(null);
vault.getVaultSettings().customMountPath().set(null);
}
}
private void toggleMountPoint(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
if (mountPointCustomDir.equals(newValue) && Strings.isNullOrEmpty(vault.getVaultSettings().individualMountPath().get())) {
if (mountPointCustomDir.equals(newValue) && Strings.isNullOrEmpty(vault.getVaultSettings().customMountPath().get())) {
chooseCustomMountPoint();
}
}
@@ -144,15 +144,17 @@ public class MountOptionsController implements FxController {
private static class WinDriveLetterLabelConverter extends StringConverter<String> {
private final Set<String> occupiedDriveLetters;
private final ResourceBundle resourceBundle;
WinDriveLetterLabelConverter(WindowsDriveLetters windowsDriveLetters) {
WinDriveLetterLabelConverter(WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle) {
this.occupiedDriveLetters = windowsDriveLetters.getOccupiedDriveLetters();
this.resourceBundle = resourceBundle;
}
@Override
public String toString(String driveLetter) {
if (occupiedDriveLetters.contains(driveLetter)) {
return driveLetter + ": (occupied)"; // TODO localize?
return driveLetter + ": (" + resourceBundle.getString("vaultOptions.mount.winDriveLetterOccupied") + ")";
} else {
return driveLetter + ":";
}
@@ -184,11 +186,11 @@ public class MountOptionsController implements FxController {
}
public StringProperty customMountPathProperty() {
return vault.getVaultSettings().individualMountPath();
return vault.getVaultSettings().customMountPath();
}
public String getCustomMountPath() {
return vault.getVaultSettings().individualMountPath().get();
return vault.getVaultSettings().customMountPath().get();
}
}

View File

@@ -16,6 +16,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
@@ -38,15 +39,14 @@ abstract class VaultOptionsModule {
@Provides
@VaultOptionsWindow
@VaultOptionsScoped
static Stage provideStage(@MainWindow Stage owner, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, @VaultOptionsWindow Vault vault) {
Stage stage = factory.create();
stage.setTitle(vault.getDisplayableName());
stage.setResizable(true);
stage.setMinWidth(400);
stage.setMinHeight(300);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -14,6 +14,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Named;
@@ -35,13 +36,12 @@ abstract class WrongFileAlertModule {
@Provides
@WrongFileAlertWindow
@WrongFileAlertScoped
static Stage provideStage(@MainWindow Stage mainWindow, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @MainWindow Stage mainWindow, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("wrongFileAlert.title"));
stage.setResizable(false);
stage.initOwner(mainWindow);
stage.initModality(Modality.WINDOW_MODAL);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -631,6 +631,10 @@
-fx-background-color: TEXT_FILL;
}
.check-box:selected:disabled > .box > .mark {
-fx-background-color: TEXT_FILL_MUTED;
}
/*******************************************************************************
* *
* RadioButton *

View File

@@ -630,6 +630,10 @@
-fx-background-color: TEXT_FILL;
}
.check-box:selected:disabled > .box > .mark {
-fx-background-color: TEXT_FILL_MUTED;
}
/*******************************************************************************
* *
* RadioButton *

View File

@@ -34,7 +34,10 @@
<CheckBox fx:id="startHiddenCheckbox" text="%preferences.general.startHidden" visible="${controller.trayMenuSupported}" managed="${controller.trayMenuSupported}"/>
<CheckBox fx:id="debugModeCheckbox" text="%preferences.general.debugLogging"/>
<HBox spacing="6" alignment="CENTER_LEFT">
<CheckBox fx:id="debugModeCheckbox" text="%preferences.general.debugLogging"/>
<Hyperlink styleClass="hyperlink-underline" text="%preferences.general.debugDirectory" onAction="#showLogfileDirectory"/>
</HBox>
<CheckBox fx:id="autoStartCheckbox" text="%preferences.general.autoStart" visible="${controller.autoStartSupported}" managed="${controller.autoStartSupported}" onAction="#toggleAutoStart"/>
</children>

View File

@@ -21,15 +21,15 @@
<children>
<VBox spacing="6">
<FormattedLabel format="%unlock.passwordPrompt" arg1="${controller.vault.displayableName}" wrapText="true"/>
<NiceSecurePasswordField fx:id="passwordField"/>
<CheckBox fx:id="savePassword" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox" disable="${controller.vault.processing}" visible="${controller.keychainAccessAvailable}"/>
<NiceSecurePasswordField fx:id="passwordField" disable="${controller.userInteractionDisabled}"/>
<CheckBox fx:id="savePasswordCheckbox" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox" disable="${controller.userInteractionDisabled}" visible="${controller.keychainAccessAvailable}"/>
</VBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.vault.processing}"/>
<Button text="%unlock.unlockBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#unlock" contentDisplay="${controller.unlockButtonState}" disable="${controller.unlockButtonDisabled}">
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.userInteractionDisabled}"/>
<Button text="%unlock.unlockBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#unlock" contentDisplay="${controller.unlockButtonContentDisplay}" disable="${controller.unlockButtonDisabled}">
<graphic>
<ProgressIndicator progress="-1" prefWidth="12" prefHeight="12"/>
</graphic>

View File

@@ -10,6 +10,7 @@
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.scene.control.CheckBox?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.unlock.UnlockSuccessController"
@@ -26,7 +27,10 @@
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CHECK" glyphSize="24"/>
</StackPane>
<FormattedLabel format="%unlock.success.message" arg1="${controller.vault.displayableName}" wrapText="true" HBox.hgrow="ALWAYS"/>
<VBox spacing="6">
<FormattedLabel format="%unlock.success.message" arg1="${controller.vault.displayableName}" wrapText="true" HBox.hgrow="ALWAYS"/>
<CheckBox text="%unlock.success.rememberChoice" fx:id="rememberChoiceCheckbox"/>
</VBox>
</HBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">

View File

@@ -26,7 +26,7 @@
</ListView>
<VBox visible="${controller.emptyVaultList}" spacing="6" alignment="CENTER">
<Region VBox.vgrow="ALWAYS"/>
<Label VBox.vgrow="NEVER" text="%main.vaultlist.emptyList.onboardingInstruction" wrapText="true"/>
<Label VBox.vgrow="NEVER" text="%main.vaultlist.emptyList.onboardingInstruction" textAlignment="CENTER" wrapText="true"/>
<Arc VBox.vgrow="NEVER" styleClass="onboarding-overlay-arc" type="OPEN" centerX="50" centerY="0" radiusY="100" radiusX="50" startAngle="0" length="-60" strokeWidth="1"/>
</VBox>
</StackPane>

View File

@@ -3,6 +3,9 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.ChoiceBox?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Label?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.vaultoptions.GeneralVaultOptionsController"
@@ -12,5 +15,10 @@
</padding>
<children>
<CheckBox text="%vaultOptions.general.unlockAfterStartup" fx:id="unlockOnStartupCheckbox"/>
<HBox spacing="6" alignment="CENTER_LEFT">
<Label text="%vaultOptions.general.actionAfterUnlock"/>
<ChoiceBox fx:id="actionAfterUnlockChoiceBox"/>
</HBox>
</children>
</VBox>

View File

@@ -96,6 +96,7 @@ unlock.savePassword=Save Password
unlock.unlockBtn=Unlock
## Success
unlock.success.message=Unlocked "%s" successfully! Your vault is now accessible.
unlock.success.rememberChoice=Remember choice, don't show this again
unlock.success.revealBtn=Reveal Vault
## Invalid Mount Point
unlock.error.invalidMountPoint=Mount point is not an empty directory: %s
@@ -129,9 +130,12 @@ preferences.title=Preferences
## General
preferences.general=General
preferences.general.theme=Look & Feel
preferences.general.theme.light=Light
preferences.general.theme.dark=Dark
preferences.general.unlockThemes=Unlock dark mode
preferences.general.startHidden=Hide window when starting Cryptomator
preferences.general.debugLogging=Enable debug logging
preferences.general.debugDirectory=Reveal log files
preferences.general.autoStart=Launch Cryptomator on system start
preferences.general.interfaceOrientation=Interface Orientation
preferences.general.interfaceOrientation.ltr=Left to Right
@@ -204,6 +208,10 @@ wrongFileAlert.link=For further assistance, visit
## General
vaultOptions.general=General
vaultOptions.general.unlockAfterStartup=Unlock vault when starting Cryptomator
vaultOptions.general.actionAfterUnlock=After successful unlock
vaultOptions.general.actionAfterUnlock.ignore=Do nothing
vaultOptions.general.actionAfterUnlock.reveal=Reveal Drive
vaultOptions.general.actionAfterUnlock.ask=Ask
## Mount
vaultOptions.mount=Mounting
vaultOptions.mount.readonly=Read-Only

View File

@@ -95,6 +95,7 @@ unlock.savePassword=احفظ كلمة المرور
unlock.unlockBtn=افتح
## Success
unlock.success.message=تم فتح المخزن "%s" بنجاح! يمكنك الوصول إليه الآن.
unlock.success.rememberChoice=تذكر اختياري ولا تظهر هذا مرة أخرى
unlock.success.revealBtn=افتح الحافظة
## Invalid Mount Point
unlock.error.invalidMountPoint=نقطة الوصول ليست مجلد فارغ: %s
@@ -107,6 +108,7 @@ migration.start.confirm=نعم, محفظتي متزامنة بالكامل
## Run
migration.run.enterPassword=أدخل كلمة المرور لـ "%s"
migration.run.startMigrationBtn=ترقية الحافظة
migration.run.progressHint=قد يستغرق هذا بعض الوقت…
## Sucess
migration.success.nextStepsInstructions=تم ترحيل "%s" بنجاح.\nيمكنك الآن فتح مخزنك.
migration.success.unlockNow=افتح الان
@@ -115,12 +117,20 @@ migration.error.missingFileSystemCapabilities.title=نظام الملفات غي
migration.error.missingFileSystemCapabilities.description=لم تبدأ عملية الترحيل بعد، لأن المخزن الخاص بك موجود على نظام ملفات غير مناسب.
migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=نظام الملفات لا يدعم أسماء الملفات الطويلة.
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=نظام الملفات لا يدعم المسارات الطويلة.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=نظام الملفات لا يُسمح بقراءته.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=نظام الملفات لا يُسمح بأن تتم الكتابة إليه.
## Impossible
migration.impossible.heading=غير قادر على ترحيل المخزن
migration.impossible.reason=لا يمكن ترحيل المخزن تلقائياً لأن موقع التخزين أو نقطة الوصول غير متوافقة.
migration.impossible.moreInfo=لا يزال ممكناً فتح المخزن باستخدام إصدار قديم. للحصول على تعليمات حول كيفية ترحيل المخزن يدوياً، قم بزيارة
# Preferences
preferences.title=تفضيلات
## General
preferences.general=عام
preferences.general.theme=الشكل والمظهر
preferences.general.theme.light=فاتح (أبيض)
preferences.general.theme.dark=مظلم (أسود)
preferences.general.unlockThemes=تفعيل الوضع المظلم
preferences.general.startHidden=إخفاء النافذة عند بدء تشغيل Cryptomator
preferences.general.debugLogging=تمكين سجلات التصحيح
@@ -196,6 +206,10 @@ wrongFileAlert.link=لمزيد من المساعدة، قم بزيارة
## General
vaultOptions.general=عام
vaultOptions.general.unlockAfterStartup=فتح قفل المخزن عند بدء تشغيل Cryptomator
vaultOptions.general.actionAfterUnlock=بعد فتح القفل بنجاح
vaultOptions.general.actionAfterUnlock.ignore=‮لا تفعل شيئاً
vaultOptions.general.actionAfterUnlock.reveal=اظهار القرص
vaultOptions.general.actionAfterUnlock.ask=اسأل
## Mount
vaultOptions.mount=القرص الوهمي
vaultOptions.mount.readonly=للقراءة فقط

View File

@@ -95,6 +95,7 @@ unlock.savePassword=Recorda la contrasenya
unlock.unlockBtn=Desbloqueja
## Success
unlock.success.message="%s" s'ha desbloquejat correctament! Ja es pot accedir a la caixa forta.
unlock.success.rememberChoice=Recorda l'elecció. No ho tornis a mostrar.
unlock.success.revealBtn=Mostra la caixa forta
## Invalid Mount Point
unlock.error.invalidMountPoint=El punt de muntatge no és un directori buit: %s
@@ -107,6 +108,7 @@ migration.start.confirm=Sí, la meua caixa forta està completament sicronitzada
## Run
migration.run.enterPassword=Introduïu la contrasenya per a "%s"
migration.run.startMigrationBtn=Migrar la caixa forta
migration.run.progressHint=Això pot trigar una mica...
## Sucess
migration.success.nextStepsInstructions="%s" s'ha migrat correctament.\nJa podeu desbloquejar la vostra caixa forta.
migration.success.unlockNow=Desbloqueja ara
@@ -117,15 +119,22 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=El sistema d
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=El sistema de fitxers no suporta camins llargs.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=El sistema de fitxers no permet la lectura.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=El sistema de fitxers no permet l'escriptura.
## Impossible
migration.impossible.heading=No s'ha pogut migrar la caixa forta
migration.impossible.reason=La caixa forta no es pot migrar automàticament perquè la ubicació d'enmagatzematge o el punt d'accès no són compatibles.
migration.impossible.moreInfo=La caixa forta es pot obrir amb una versió anterior. Per saber com poder fer la migració de manera manual, visiteu
# Preferences
preferences.title=Preferències
## General
preferences.general=General
preferences.general.theme=Apariència
preferences.general.theme.light=Clar
preferences.general.theme.dark=Fosc
preferences.general.unlockThemes=Desbloqueja el tema fosc
preferences.general.startHidden=Amaga la finestra al iniciar Cryptomator
preferences.general.debugLogging=Habilita el registre de depuració
preferences.general.debugDirectory=Mostra els fitxers de registres
preferences.general.autoStart=Executa Cryptomator en engegar el sistema
preferences.general.interfaceOrientation=Orientació de la interfície
preferences.general.interfaceOrientation.ltr=Esquerra a dreta
@@ -198,6 +207,10 @@ wrongFileAlert.link=Per rebre assistència, visiteu
## General
vaultOptions.general=General
vaultOptions.general.unlockAfterStartup=Desbloqueja la caixa forta al iniciar Cryptomator
vaultOptions.general.actionAfterUnlock=Després d'un desbloqueig correcte
vaultOptions.general.actionAfterUnlock.ignore=No facis res
vaultOptions.general.actionAfterUnlock.reveal=Mostra la unitat
vaultOptions.general.actionAfterUnlock.ask=Pregunta
## Mount
vaultOptions.mount=Muntatge
vaultOptions.mount.readonly=Només lectura

View File

@@ -95,6 +95,7 @@ unlock.savePassword=Uložit heslo
unlock.unlockBtn=Odemknout
## Success
unlock.success.message=Trezor "%s" byl úspěšně odemčen a nyní je dostupný.
unlock.success.rememberChoice=Pamatovat si volbu, nezobrazovat to znovu
unlock.success.revealBtn=Zobrazit trezor
## Invalid Mount Point
unlock.error.invalidMountPoint=Připojovací bod není prázdný adresář: %s
@@ -107,6 +108,7 @@ migration.start.confirm=Ano, můj trezor je plně synchronizován.
## Run
migration.run.enterPassword=Zadejte heslo pro "%s"
migration.run.startMigrationBtn=Migrovat trezor
migration.run.progressHint=Může to chvíli trvat…
## Sucess
migration.success.nextStepsInstructions=Migrace "%s" byla úspěšná.\nNyní můžete svůj trezor odemknout.
migration.success.unlockNow=Odemknout nyní
@@ -117,15 +119,22 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Souborový s
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Souborový systém nepodporuje dlouhé cesty.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Souborový systém neumožňuje čtení.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Souborový systém neumožňuje zápis.
## Impossible
migration.impossible.heading=Trezor není možné zmigrovat
migration.impossible.reason=Trezor nemůže být automaticky zmigrován, protože jeho umístění v úložišti nebo přístupový bod není kompatibilní.
migration.impossible.moreInfo=Trezor může být stále otevřen starší verzí. Pro pokyny, jak ručně migrovat trezor, navštivte
# Preferences
preferences.title=Nastavení
## General
preferences.general=Obecné
preferences.general.theme=Vzhled
preferences.general.theme.light=Světlý
preferences.general.theme.dark=Tmavý
preferences.general.unlockThemes=Odemknout tmavý režim
preferences.general.startHidden=Skrýt okno Cryptomatoru při spuštění
preferences.general.debugLogging=Ladicí režim
preferences.general.debugDirectory=Ukázat logovací soubory
preferences.general.autoStart=Spustit Cryptomator při spuštění systému
preferences.general.interfaceOrientation=Orientace prostředí
preferences.general.interfaceOrientation.ltr=Zleva doprava
@@ -198,6 +207,10 @@ wrongFileAlert.link=Pro další informace navštivte
## General
vaultOptions.general=Obecné
vaultOptions.general.unlockAfterStartup=Odemknout trezor při spuštění Cryptomator
vaultOptions.general.actionAfterUnlock=Po úspěšném odemčení
vaultOptions.general.actionAfterUnlock.ignore=Nedělat nic
vaultOptions.general.actionAfterUnlock.reveal=Zobrazit jednotku
vaultOptions.general.actionAfterUnlock.ask=Zeptat se
## Mount
vaultOptions.mount=Připojení
vaultOptions.mount.readonly=Pouze pro čtení

View File

@@ -95,6 +95,7 @@ unlock.savePassword=Passwort speichern
unlock.unlockBtn=Entsperren
## Success
unlock.success.message=„%s“ erfolgreich entsperrt! Nun kannst du auf deinen Tresor zugreifen.
unlock.success.rememberChoice=Auswahl speichern und nicht mehr anzeigen
unlock.success.revealBtn=Tresor anzeigen
## Invalid Mount Point
unlock.error.invalidMountPoint=Einhängepunkt ist kein leeres Verzeichnis: %s
@@ -107,6 +108,7 @@ migration.start.confirm=Ja, mein Tresor ist vollständig synchronisiert
## Run
migration.run.enterPassword=Gib das Passwort für „%s“ ein
migration.run.startMigrationBtn=Tresor migrieren
migration.run.progressHint=Dies kann einige Zeit dauern …
## Sucess
migration.success.nextStepsInstructions=„%s“ erfolgreich migriert.\nDu kannst deinen Tresor jetzt entsperren.
migration.success.unlockNow=Jetzt entsperren
@@ -117,15 +119,22 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Das Dateisys
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Das Dateisystem unterstützt keine langen Pfadnamen.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Das Dateisystem lässt keine Lesevorgänge zu.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Das Dateisystem lässt keine Schreibvorgänge zu.
## Impossible
migration.impossible.heading=Tresor kann nicht migriert werden
migration.impossible.reason=Der Tresor kann nicht automatisch migriert werden, da sein Speicherort oder Zugangspunkt nicht kompatibel ist.
migration.impossible.moreInfo=Der Tresor kann auch jetzt noch mit einer älteren Version geöffnet werden. Eine Anleitung zum manuellen Migrieren eines Tresors findest du unter
# Preferences
preferences.title=Einstellungen
## General
preferences.general=Allgemein
preferences.general.theme=Erscheinungsbild
preferences.general.theme.light=Hell
preferences.general.theme.dark=Dunkel
preferences.general.unlockThemes=Dunklen Modus freischalten
preferences.general.startHidden=Cryptomator im Hintergrund starten
preferences.general.debugLogging=Diagnoseprotokoll aktivieren
preferences.general.debugDirectory=Protokolldateien anzeigen
preferences.general.autoStart=Cryptomator beim Systemstart starten
preferences.general.interfaceOrientation=Oberflächenausrichtung
preferences.general.interfaceOrientation.ltr=Von links nach rechts
@@ -198,6 +207,10 @@ wrongFileAlert.link=Für weitere Unterstützung besuche
## General
vaultOptions.general=Allgemein
vaultOptions.general.unlockAfterStartup=Tresor beim Start von Cryptomator entsperren
vaultOptions.general.actionAfterUnlock=Nach erfolgreichem Entsperren
vaultOptions.general.actionAfterUnlock.ignore=Nichts tun
vaultOptions.general.actionAfterUnlock.reveal=Laufwerk anzeigen
vaultOptions.general.actionAfterUnlock.ask=Nachfragen
## Mount
vaultOptions.mount=Laufwerk
vaultOptions.mount.readonly=Schreibgeschützt

View File

@@ -31,6 +31,7 @@
## Run
## Sucess
## Missing file system capabilities
## Impossible
# Preferences
## General

View File

@@ -95,6 +95,7 @@ unlock.savePassword=Guardar contraseña
unlock.unlockBtn=Desbloquear
## Success
unlock.success.message=¡"%s" se desbloqueó con éxito! La bóveda ya es accesible.
unlock.success.rememberChoice=Recordar opción y no mostrar de nuevo
unlock.success.revealBtn=Revelar bóveda
## Invalid Mount Point
unlock.error.invalidMountPoint=El punto de montaje no es un directorio vacío: %s
@@ -107,6 +108,7 @@ migration.start.confirm=Sí, mi bóveda está sincronizada
## Run
migration.run.enterPassword=Ingresar la contraseña para "%s"
migration.run.startMigrationBtn=Migrar bóveda
migration.run.progressHint=Esto puede tardar un poco…
## Sucess
migration.success.nextStepsInstructions="%s" se migró con éxito.\nYa se puede desbloquear la bóveda.
migration.success.unlockNow=Desbloquear ahora
@@ -117,12 +119,18 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=El sistema d
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=El sistema de archivos no soporta rutas largas.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=El sistema de archivos no permite la lectura.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=El sistema de archivos no permite la escritura.
## Impossible
migration.impossible.heading=No se pudo migrar la bóveda
migration.impossible.reason=La bóveda no puede migrarse automáticamente porque su ubicación de almacenamiento o punto de acceso es incompatible.
migration.impossible.moreInfo=La bóveda aún se puede abrir con una versión anterior. Para obtener instrucciones en cómo migrar manualmente una bóveda, visite
# Preferences
preferences.title=Preferencias
## General
preferences.general=General
preferences.general.theme=Apariencia
preferences.general.theme.light=Claro
preferences.general.theme.dark=Oscuro
preferences.general.unlockThemes=Desbloquear el modo oscuro
preferences.general.startHidden=Ocultar ventana al iniciar Cryptomator
preferences.general.debugLogging=Habilitar registro de depuración
@@ -198,6 +206,10 @@ wrongFileAlert.link=Para más ayuda, visite
## General
vaultOptions.general=General
vaultOptions.general.unlockAfterStartup=Desbloquear bóveda al iniciar Cryptomator
vaultOptions.general.actionAfterUnlock=Después de desbloquear exitosamente
vaultOptions.general.actionAfterUnlock.ignore=No hacer nada
vaultOptions.general.actionAfterUnlock.reveal=Revelar unidad
vaultOptions.general.actionAfterUnlock.ask=Preguntar
## Mount
vaultOptions.mount=Montaje
vaultOptions.mount.readonly=Sólo lectura

View File

@@ -95,6 +95,7 @@ unlock.savePassword=Enregistrer le mot de passe
unlock.unlockBtn=Déverrouiller
## Success
unlock.success.message=“%s” déverrouillé ! Le contenu de votre coffre est maintenant accessible.
unlock.success.rememberChoice=Se souvenir de mon choix et ne plus me demander
unlock.success.revealBtn=Révéler le coffre
## Invalid Mount Point
unlock.error.invalidMountPoint=Le point de montage n'est pas un répertoire vide : %s
@@ -107,6 +108,7 @@ migration.start.confirm=Ce coffre est bien synchronisé
## Run
migration.run.enterPassword=Entrez le mot de passe pour %s
migration.run.startMigrationBtn=Migrer le coffre
migration.run.progressHint=Ceci peut prendre un certain temps…
## Sucess
migration.success.nextStepsInstructions=“%s” migré.\nVous pouvez à présent déverrouiller ce coffre.
migration.success.unlockNow=Déverouiller
@@ -117,15 +119,22 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Ce système
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Ce système de fichiers ne prend pas en charge les chemins d'accès longs.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Le système de fichiers ne permet pas d'être lu.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Le système de fichiers ne permet pas l'écriture.
## Impossible
migration.impossible.heading=Impossible de migrer le coffre-fort
migration.impossible.reason=Le coffre-fort ne peut pas être migré automatiquement car son emplacement de stockage ou son point d'accès n'est pas compatible.
migration.impossible.moreInfo=Le coffre-fort peut encore être ouvert avec une version plus ancienne. Pour obtenir des instructions sur la façon de migrer manuellement un coffre-fort, visitez
# Preferences
preferences.title=Préférences
## General
preferences.general=Général
preferences.general.theme=Apparence
preferences.general.theme.light=Clair
preferences.general.theme.dark=Sombre
preferences.general.unlockThemes=Débloquer le mode nuit
preferences.general.startHidden=Démarrer Cryptomator en mode caché
preferences.general.debugLogging=Activer les logs debug
preferences.general.debugDirectory=Afficher le relevé
preferences.general.autoStart=Lancer Cryptomator au démarrage du système
preferences.general.interfaceOrientation=Orientation de l'interface
preferences.general.interfaceOrientation.ltr=De gauche à droite
@@ -198,6 +207,10 @@ wrongFileAlert.link=Pour toute aide supplémentaire, visitez
## General
vaultOptions.general=Général
vaultOptions.general.unlockAfterStartup=Déverrouiller le coffre au démarrage
vaultOptions.general.actionAfterUnlock=Après un déverrouillage réussi
vaultOptions.general.actionAfterUnlock.ignore=Ne rien faire
vaultOptions.general.actionAfterUnlock.reveal=Afficher le disque
vaultOptions.general.actionAfterUnlock.ask=Demander
## Mount
vaultOptions.mount=Montage
vaultOptions.mount.readonly=Lecture seule

View File

@@ -63,6 +63,7 @@ migration.title=वाउल्ट को अपग्रेड करें
## Run
## Sucess
## Missing file system capabilities
## Impossible
# Preferences
## General
@@ -91,7 +92,7 @@ main.vaultDetail.migrateButton=वाउल्ट को अपग्रेड
# Wrong File Alert
wrongFileAlert.instruction.1=अपना वाउल्ट खोलें।
wrongFileAlert.link=और मदद लिए, यह जाएं
wrongFileAlert.link=और मदद के लिए, यह जाएं
# Vault Options
## General

View File

@@ -31,6 +31,7 @@
## Run
## Sucess
## Missing file system capabilities
## Impossible
# Preferences
## General

View File

@@ -95,6 +95,7 @@ unlock.savePassword=Salva password
unlock.unlockBtn=Sblocca
## Success
unlock.success.message=Sbloccato "%s" con successo! La tua cassaforte è ora accessibile.
unlock.success.rememberChoice=Ricorda la scelta, non mostrare ancora
unlock.success.revealBtn=Rivela Cassaforte
## Invalid Mount Point
unlock.error.invalidMountPoint=Il punto di mount non è una cartella vuota: %s
@@ -107,6 +108,7 @@ migration.start.confirm=Sì, la mia cassaforte è completamente sincronizzata
## Run
migration.run.enterPassword=Immettere la password per "%s"
migration.run.startMigrationBtn=Migra Cassaforte
migration.run.progressHint=Potrebbe richiedere un po' di tempo…
## Sucess
migration.success.nextStepsInstructions=Migrato "%s" con successo.\nOra puoi sbloccare la tua cassaforte.
migration.success.unlockNow=Sblocca adesso
@@ -117,15 +119,22 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Il file syst
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Il file system non supporta percorsi lunghi.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Il file system non consente di essere letto.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Il file system non consente di essere scritto.
## Impossible
migration.impossible.heading=Impossibile migrare la cassaforte
migration.impossible.reason=La cassaforte non può essere migrata automaticamente perché la sua posizione di archiviazione di accesso non è compatibile.
migration.impossible.moreInfo=La cassaforte può ancora essere aperta con una versione precedente. Per istruzioni su come migrare manualmente una cassaforte, visita
# Preferences
preferences.title=Impostazioni
## General
preferences.general=Generali
preferences.general.theme=Aspetto
preferences.general.theme.light=Chiaro
preferences.general.theme.dark=Scuro
preferences.general.unlockThemes=Sblocca modalità scura
preferences.general.startHidden=Nascondi la finestra all'avvio di Cryptomator
preferences.general.debugLogging=Abilita i registri di debug
preferences.general.debugDirectory=Mostra file log
preferences.general.autoStart=Avvia Cryptomator all'avvio del sistema
preferences.general.interfaceOrientation=Orientamento Interfaccia
preferences.general.interfaceOrientation.ltr=Da Sinistra a Destra
@@ -198,6 +207,10 @@ wrongFileAlert.link=Per ulteriore assistenza, visita
## General
vaultOptions.general=Generali
vaultOptions.general.unlockAfterStartup=Sblocca vault all'avvio di Cryptomator
vaultOptions.general.actionAfterUnlock=Dopo aver sbloccato con successo
vaultOptions.general.actionAfterUnlock.ignore=Non fare nulla
vaultOptions.general.actionAfterUnlock.reveal=Visualizza disco
vaultOptions.general.actionAfterUnlock.ask=Chiedi
## Mount
vaultOptions.mount=Montaggio
vaultOptions.mount.readonly=Sola lettura

View File

@@ -95,6 +95,7 @@ unlock.savePassword=パスワードを保存
unlock.unlockBtn=解錠
## Success
unlock.success.message="%s" の解錠に成功しました! 金庫にアクセス可能です。
unlock.success.rememberChoice=選択を記憶させて、再度表示しない
unlock.success.revealBtn=金庫を表示
## Invalid Mount Point
unlock.error.invalidMountPoint=マウントポイントが空のディレクトリではありません: %s
@@ -107,6 +108,7 @@ migration.start.confirm=はい。同期が完了しています
## Run
migration.run.enterPassword="%s" のパスワードを入力してください
migration.run.startMigrationBtn=金庫を移行
migration.run.progressHint=時間がかかる場合があります...
## Sucess
migration.success.nextStepsInstructions="%s" の移行が成功しました。\n金庫を解錠できるようになりました。
migration.success.unlockNow=今すぐ解錠
@@ -117,15 +119,22 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=ファイル
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=ファイルシステムが長いパスをサポートしていません。
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=ファイルシステムによって読み込みが許可されていません。
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=ファイルシステムによって書き込みが許可されていません。
## Impossible
migration.impossible.heading=金庫の移行に失敗しました
migration.impossible.reason=ストレージ場所またはアクセスポイントに互換性がないため、金庫を自動的に移行できません。
migration.impossible.moreInfo=金庫を古いバージョンで開くことは可能です。手動による金庫の移行方法については次をご覧ください:
# Preferences
preferences.title=設定
## General
preferences.general=基本設定
preferences.general.theme=外見 & 操作性
preferences.general.theme.light=ライト
preferences.general.theme.dark=ダーク
preferences.general.unlockThemes=ダークモードの解錠
preferences.general.startHidden=Cryptomator を開始したときウィンドウを隠す
preferences.general.debugLogging=ログを有効にする
preferences.general.debugDirectory=ログ ファイルを表示
preferences.general.autoStart=システム開始時にCryptomatorを起動する
preferences.general.interfaceOrientation=インターフェイスの向き
preferences.general.interfaceOrientation.ltr=左から右
@@ -198,6 +207,10 @@ wrongFileAlert.link=より詳細な手順については、次のページをご
## General
vaultOptions.general=基本設定
vaultOptions.general.unlockAfterStartup=Cryptomatorの起動時に金庫を解錠する
vaultOptions.general.actionAfterUnlock=解錠に成功したあと
vaultOptions.general.actionAfterUnlock.ignore=何もしない
vaultOptions.general.actionAfterUnlock.reveal=ドライブを表示
vaultOptions.general.actionAfterUnlock.ask=尋ねる
## Mount
vaultOptions.mount=マウント
vaultOptions.mount.readonly=読み取り専用

View File

@@ -95,6 +95,7 @@ unlock.savePassword=비밀번호 저장
unlock.unlockBtn=잠금해제
## Success
unlock.success.message="%s"의 잠금해제가 성공적으로 수행되었습니다! 이제 이 Vault의 접근이 가능합니다.
unlock.success.rememberChoice=선택 기억함, 다시 묻지 않음
unlock.success.revealBtn=Vault 표시
## Invalid Mount Point
unlock.error.invalidMountPoint=구성지점의 디렉터리가 비어있지 않습니다: %s
@@ -107,6 +108,7 @@ migration.start.confirm=네, 이 Vault는 동기화가 완전히 된 상태입
## Run
migration.run.enterPassword="%s"의 비밀번호를 입력하십시요.
migration.run.startMigrationBtn=Vault 마이그레이션
migration.run.progressHint=이 작업은 시간이 조금 소요됩니다.
## Sucess
migration.success.nextStepsInstructions="%s" 의 마이그레이션이 성공적으로 완료되었습니다. 이제 Vault를 잠금해제할 수 있습니다.
migration.success.unlockNow=지금 잠금해제
@@ -115,12 +117,20 @@ migration.error.missingFileSystemCapabilities.title=지원하지 않는 파일
migration.error.missingFileSystemCapabilities.description=Vault가 부적절한 파일시스템에 있기 때문에 마이그레이션이 시작되지 않았습니다.
migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=너무 긴 파일 이름을 파일시스템에서 지원하지 않습니다.
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=너무 긴 경로를 파일시스템에서 지원하지 않습니다.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=파일 시스템이 읽기를 허용하지 않습니다.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=파일 시스템이 쓰기를 허용하지 않습니다.
## Impossible
migration.impossible.heading=Vault를 마이그레이션 할 수 없습니다.
migration.impossible.reason=저장소 위치 또는 접근 지점이 호환되지 않아 Vault를 자동으로 마이그레이션 할 수 없습니다.
migration.impossible.moreInfo=Vault를 이전 버전으로 계속 열수 있습니다. Vault를 직접 마이그레이션 하는 설명을 보시려면, 다음을 방문하십시요.
# Preferences
preferences.title=환경설정
## General
preferences.general=일반
preferences.general.theme=테마설정
preferences.general.theme.light=밝게
preferences.general.theme.dark=어둡게
preferences.general.unlockThemes=다크모드 해제
preferences.general.startHidden=Cryptomator를 시작할 때 창 숨김
preferences.general.debugLogging=디버그 로그기록을 사용하도록 설정
@@ -196,6 +206,10 @@ wrongFileAlert.link=추후 지원을 위해, 다음을 방문하십시요
## General
vaultOptions.general=일반
vaultOptions.general.unlockAfterStartup=Cryptomator를 시작할 때 Vault 잠금 해제
vaultOptions.general.actionAfterUnlock=성공적으로 잠금해제 후
vaultOptions.general.actionAfterUnlock.ignore=아무 것도 하지 않음
vaultOptions.general.actionAfterUnlock.reveal=드라이브 표시
vaultOptions.general.actionAfterUnlock.ask=요청
## Mount
vaultOptions.mount=드라이브 구성
vaultOptions.mount.readonly=읽기 전용

View File

@@ -117,12 +117,15 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Datņu sist
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Datņu sistēma neatbalsta garus ceļu nosaukumus.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Nav atļaujas lasīt no datņu sistēmas.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Nav atļaujas rakstīt datņu sistēmā.
## Impossible
# Preferences
preferences.title=Iestatījumi
## General
preferences.general=Vispārēji
preferences.general.theme=Izskats
preferences.general.theme.light=Gaišs
preferences.general.theme.dark=Tumšs
preferences.general.unlockThemes=Iespējot tumšo režīmu
preferences.general.startHidden=Paslēpt logu, kad startē Cryptomator
preferences.general.debugLogging=Iespējot atkļūdošanas žurnalēšanu
@@ -198,6 +201,7 @@ wrongFileAlert.link=Lai iegūtu turpmāku palīdzību, apmeklējiet
## General
vaultOptions.general=Vispārēji
vaultOptions.general.unlockAfterStartup=Atslēgt glabātuvi startējot Cryptomator
vaultOptions.general.actionAfterUnlock.reveal=Atklāt disku
## Mount
vaultOptions.mount=Montē
vaultOptions.mount.readonly=Tikai lasīt

View File

@@ -95,6 +95,7 @@ unlock.savePassword=Lagre passord
unlock.unlockBtn=Lås opp
## Success
unlock.success.message=Vellykket opplåsning av "%s"! Hvelvet ditt er nå tilgjengelig.
unlock.success.rememberChoice=Husk valget - ikke vis dette igjen
unlock.success.revealBtn=Gjør hvelvet synlig
## Invalid Mount Point
unlock.error.invalidMountPoint=Monteringspunktet er ikke en tom mappe: %s
@@ -107,6 +108,7 @@ migration.start.confirm=Ja, hvelvet mitt er fullstendig synkronisert
## Run
migration.run.enterPassword=Skriv inn passordet for "%s"
migration.run.startMigrationBtn=Overfør hvelv
migration.run.progressHint=Dette kan ta litt tid…
## Sucess
migration.success.nextStepsInstructions=Vellykket overføring av "%s".\nDu kan nå låse opp hvelvet ditt.
migration.success.unlockNow=Lås opp nå
@@ -117,12 +119,18 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Filsystemet
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Filsystemet støtter ikke lange søkestier.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Filsystemet tillater ikke lesing.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Filsystemet tillater ikke å bli skrevet på.
## Impossible
migration.impossible.heading=Kunne ikke overføre hvelvet
migration.impossible.reason=Hvelvet kan ikke overføres automatisk fordi lagringsstedet eller tilgangspunkt ikke er kompatibelt.
migration.impossible.moreInfo=Hvelvet kan fortsatt åpnes hvis du bruker en eldre versjon. For instruksjoner om hvordan man overfører et hvelv, besøk
# Preferences
preferences.title=Innstillinger
## General
preferences.general=Generelt
preferences.general.theme=Grafisk utseende
preferences.general.theme.light=Lys
preferences.general.theme.dark=Mørk
preferences.general.unlockThemes=Lås opp mørk modus
preferences.general.startHidden=Skjul vinduet når du starter Cryptomator
preferences.general.debugLogging=Aktiver loggføring av feilsøk
@@ -198,6 +206,10 @@ wrongFileAlert.link=For ytterligere hjelp, besøk
## General
vaultOptions.general=Generelt
vaultOptions.general.unlockAfterStartup=Lås opp hvelvet når du starter Cryptomator
vaultOptions.general.actionAfterUnlock=Etter vellykket opplåsing
vaultOptions.general.actionAfterUnlock.ignore=Ikke gjør noe
vaultOptions.general.actionAfterUnlock.reveal=Gjør enheten synlig
vaultOptions.general.actionAfterUnlock.ask=Spør
## Mount
vaultOptions.mount=Montering
vaultOptions.mount.readonly=Skrivebeskyttet

View File

@@ -115,6 +115,7 @@ migration.error.missingFileSystemCapabilities.title=Niet ondersteund bestandssys
migration.error.missingFileSystemCapabilities.description=Migratie is niet gestart, omdat uw kluis zich op een niet-adequaat bestandssysteem bevindt.
migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Het bestandssysteem ondersteunt geen lange bestandsnamen.
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Het bestandssysteem ondersteunt geen lange paden.
## Impossible
# Preferences
preferences.title=Voorkeuren
@@ -196,6 +197,7 @@ wrongFileAlert.link=Voor verdere ondersteuning, bezoek
## General
vaultOptions.general=Algemeen
vaultOptions.general.unlockAfterStartup=Ontgrendel kluis bij het starten van Cryptomator
vaultOptions.general.actionAfterUnlock.reveal=Toon Schijf
## Mount
vaultOptions.mount=Aankoppelen
vaultOptions.mount.readonly=Alleen-Lezen

View File

@@ -95,6 +95,7 @@ unlock.savePassword=Lagre passordet
unlock.unlockBtn=Låse opp
## Success
unlock.success.message=Vellykka opplåsning av "%s"! Kvelven din er no tilgjengeleg.
unlock.success.rememberChoice=Hugs valet - ikkje vis dette igjen
unlock.success.revealBtn=Gjer kvelven synleg
## Invalid Mount Point
unlock.error.invalidMountPoint=Monteringspunktet er ikkje ei tom mappe: %s
@@ -107,6 +108,7 @@ migration.start.confirm=Ja, kvelven min er fullstendig synkronisert
## Run
migration.run.enterPassword=Skriv inn passordet for "%s"
migration.run.startMigrationBtn=Migrer kvelv
migration.run.progressHint=Dette kan ta litt tid…
## Sucess
migration.success.nextStepsInstructions=Vellykka migrering av "%s". Du kan no låsa opp kvelven din.
migration.success.unlockNow=Lås opp no
@@ -117,12 +119,18 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Filsystemet
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Filsystemet støttar ikkje lange søkastiar.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Filsystemet tillèt ikkje å bli lese.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Filsystemet tillèt ikkje å bli skrive på.
## Impossible
migration.impossible.heading=Kunne ikkje overføra kvelven
migration.impossible.reason=Kvelven kan ikkje overførast automatisk fordi lagringsstaden eller tilgangspunkt ikkje er kompatibelt.
migration.impossible.moreInfo=Kvelven kan framleis opnast viss du bruker ein eldre versjon. For instruksjonar om korleis ein overfører ein kvelv, besøk
# Preferences
preferences.title=Innstillingar
## General
preferences.general=Generelt
preferences.general.theme=Grafisk utsjånad
preferences.general.theme.light=Lys
preferences.general.theme.dark=Mørk
preferences.general.unlockThemes=Lås opp mørk modus
preferences.general.startHidden=Skjul vindauget når du startar Cryptomator
preferences.general.debugLogging=Aktivar protokollføring av feilsøk
@@ -198,6 +206,10 @@ wrongFileAlert.link=For ytterlegare hjelp, besøk
## General
vaultOptions.general=Generelt
vaultOptions.general.unlockAfterStartup=Lås opp kvelven når du startar Cryptomator
vaultOptions.general.actionAfterUnlock=Etter vellykka opplåsing
vaultOptions.general.actionAfterUnlock.ignore=Ikkje gjer noko
vaultOptions.general.actionAfterUnlock.reveal=Gjer eininga synleg
vaultOptions.general.actionAfterUnlock.ask=Spør
## Mount
vaultOptions.mount=Montering
vaultOptions.mount.readonly=Skriveverna

View File

@@ -31,6 +31,7 @@
## Run
## Sucess
## Missing file system capabilities
## Impossible
# Preferences
## General

View File

@@ -95,6 +95,7 @@ unlock.savePassword=Salvar Senha
unlock.unlockBtn=Desbloquear
## Success
unlock.success.message="%s" foi desbloqueado com sucesso! Seu cofre agora está acessível.
unlock.success.rememberChoice=Lembrar opção escolhida, não mostrar isto novamente
unlock.success.revealBtn=Revelar Cofre
## Invalid Mount Point
unlock.error.invalidMountPoint=O ponto de montagem não é um diretório vazio: %s
@@ -107,6 +108,7 @@ migration.start.confirm=Sim, meu cofre está completamente sincronizado
## Run
migration.run.enterPassword=Digite a senha para "%s"
migration.run.startMigrationBtn=Migrar Cofre
migration.run.progressHint=Isso pode levar algum tempo…
## Sucess
migration.success.nextStepsInstructions="%s" migrado com sucesso.\nVocê agora pode desbloquear este cofre.
migration.success.unlockNow=Desbloquear Agora
@@ -117,12 +119,18 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=O sistema de
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=O sistema de arquivos não suporta caminhos longos.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=O sistema de arquivos não permite leitura.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=O sistema de arquivos não permite ser gravado.
## Impossible
migration.impossible.heading=Não é possível migrar o cofre
migration.impossible.reason=O cofre não pode ser migrado automaticamente porque sua localização de armazenamento ou o ponto de acesso não é compatível.
migration.impossible.moreInfo=O cofre ainda pode ser aberto com uma versão mais antiga. Para instruções sobre como migrar um cofre manualmente, visite
# Preferences
preferences.title=Preferências
## General
preferences.general=Geral
preferences.general.theme=Aparência
preferences.general.theme.light=Claro
preferences.general.theme.dark=Escuro
preferences.general.unlockThemes=Desbloquear o modo escuro
preferences.general.startHidden=Ocultar janela ao iniciar o Cryptomator
preferences.general.debugLogging=Ativar log de debug
@@ -198,6 +206,10 @@ wrongFileAlert.link=Para obter assistência, visite
## General
vaultOptions.general=Geral
vaultOptions.general.unlockAfterStartup=Desbloqueie o cofre ao iniciar o Cryptomator
vaultOptions.general.actionAfterUnlock=Após desbloquear com sucesso
vaultOptions.general.actionAfterUnlock.ignore=Não fazer nada
vaultOptions.general.actionAfterUnlock.reveal=Revelar Volume
vaultOptions.general.actionAfterUnlock.ask=Perguntar
## Mount
vaultOptions.mount=Montagem
vaultOptions.mount.readonly=Somente Leitura

View File

@@ -31,6 +31,7 @@
## Run
## Sucess
## Missing file system capabilities
## Impossible
# Preferences
## General

View File

@@ -14,12 +14,12 @@ generic.button.next=Далее
generic.button.print=Распечатать
## Error
generic.error.title=Произошла непредвиденная ошибка
generic.error.instruction=Этого не должно было произойти. Создайте отчёт об ошибке ниже и опишите, какие шаги к ней привели.
generic.error.instruction=Этого не должно было произойти. Пожалуйста, создайте отчет об ошибке ниже и опишите, какие шаги к ней привели.
# Tray Menu
traymenu.showMainWindow=Показать
traymenu.showPreferencesWindow=Настройки
traymenu.lockAllVaults=Заблокировать всё
traymenu.lockAllVaults=Заблокировать все
traymenu.quitApplication=Выход
traymenu.vault.unlock=Разблокировать
traymenu.vault.lock=Заблокировать
@@ -29,33 +29,33 @@ traymenu.vault.reveal=Показать
addvaultwizard.title=Добавить хранилище
## Welcome
addvaultwizard.welcome.newButton=Создать хранилище
addvaultwizard.welcome.existingButton=Открыть имеющееся хранилище
addvaultwizard.welcome.existingButton=Открыть существующее хранилище
## New
### Name
addvaultwizard.new.nameInstruction=Выберите имя для хранилища
addvaultwizard.new.namePrompt=Имя хранилища
addvaultwizard.new.namePrompt=Название хранилища
### Location
addvaultwizard.new.locationInstruction=Где Cryptomator должен хранить зашифрованные файлы вашего хранилища?
addvaultwizard.new.locationLabel=Место хранения
addvaultwizard.new.locationLabel=Расположение хранилища
addvaultwizard.new.locationPrompt=
addvaultwizard.new.directoryPickerLabel=Своё расположение
addvaultwizard.new.directoryPickerButton=Выбрать…
addvaultwizard.new.directoryPickerTitle=Выберите каталог
addvaultwizard.new.fileAlreadyExists=Хранилище не может быть создано по этому пути, потому что некоторые объекты уже существуют.
addvaultwizard.new.invalidName=Неверное имя хранилища. Укажите корректное имя каталога.
addvaultwizard.new.invalidName=Недопустимое название хранилища. Пожалуйста, выберите корректное название каталога.
### Password
addvaultwizard.new.createVaultBtn=Создать хранилище
addvaultwizard.new.generateRecoveryKeyChoice=Вы не сможете получить доступ к своим данным без пароля. Хотите создать ключ для восстановления на случай потери пароля?
addvaultwizard.new.generateRecoveryKeyChoice.yes=Да, лучше предостеречься, чем потом жалеть
addvaultwizard.new.generateRecoveryKeyChoice.yes=Да, пожалуй, лучше так, чем потом сожалеть
addvaultwizard.new.generateRecoveryKeyChoice.no=Нет, спасибо, я не потеряю свой пароль
### Information
addvault.new.readme.storageLocation.fileName=ВАЖНО.rtf
addvault.new.readme.storageLocation.1=⚠️ ФАЙЛЫ ХРАНИЛИЩА ⚠️
addvault.new.readme.storageLocation.2=Это место, где находится ваше хранилище.
addvault.new.readme.storageLocation.2=Это место расположения вашего хранилища.
addvault.new.readme.storageLocation.3=НЕТ
addvault.new.readme.storageLocation.4=• изменяйте любые файлы в этой папке или
addvault.new.readme.storageLocation.5=• добавляйте в эту папку любые файлы для шифрования.
addvault.new.readme.storageLocation.6=Чтобы зашифровать файлы и просмотреть содержимое хранилища, сделайте следующее:
addvault.new.readme.storageLocation.6=Если вы хотите зашифровать файлы и просмотреть содержимое хранилища, сделайте следующее:
addvault.new.readme.storageLocation.7=1. Добавьте это хранилище в Cryptomator.
addvault.new.readme.storageLocation.8=2. Разблокируйте хранилище в Cryptomator.
addvault.new.readme.storageLocation.9=3. Откройте место доступа, нажав кнопку "Показать".
@@ -63,19 +63,19 @@ addvault.new.readme.storageLocation.10=Если вам нужна помощь,
addvault.new.readme.accessLocation.fileName=ПРИВЕТСТВИЕ.rtf
addvault.new.readme.accessLocation.1=🔐️ ЗАШИФРОВАННЫЙ ТОМ 🔐️
addvault.new.readme.accessLocation.2=Это место доступа к вашему хранилищу.
addvault.new.readme.accessLocation.3=Любые файлы, добавленные в этот том, будут зашифрованы Cryptomator. Вы можете работать с ним как с любым другим диском/папкой. Здесь отображается только расшифрованное содержимое тома, ваши файлы остаются зашифрованными на жёстком диске постоянно.
addvault.new.readme.accessLocation.3=Любые файлы, добавленные в этот том, будут зашифрованы Cryptomator. Вы можете работать с ним, как с любым другим диском/папкой. Здесь отображается только расшифрованное содержимое тома, ваши файлы остаются зашифрованными на жестком диске постоянно.
addvault.new.readme.accessLocation.4=Этот файл можно удалить.
## Existing
addvaultwizard.existing.instruction=Выберите файл "masterkey.cryptomator" из имеющегося хранилища.
addvaultwizard.existing.instruction=Выберите файл "masterkey.cryptomator" от существующего хранилища.
addvaultwizard.existing.chooseBtn=Выбрать…
addvaultwizard.existing.filePickerTitle=Выберите файл мастер-ключа
addvaultwizard.existing.filePickerTitle=Выберите файл MasterKey
## Success
addvaultwizard.success.nextStepsInstructions=Добавлено хранилище "%s".\nДля добавления данных или доступа к содержимому нужно разблокировать хранилище. Его можно разблокировать и позже.
addvaultwizard.success.unlockNow=Разблокировать
# Remove Vault
removeVault.title=Удалить хранилище
removeVault.information=Cryptomator просто забудет это хранилище. Позже вы можете добавить его снова. Зашифрованные файлы не будут удалены с жёсткого диска.
removeVault.information=Cryptomator просто забудет это хранилище. Вы можете добавить его снова позже. Зашифрованные файлы не будут удалены с жесткого диска.
removeVault.confirmBtn=Удалить хранилище
# Change Password
@@ -95,6 +95,7 @@ unlock.savePassword=Сохранить пароль
unlock.unlockBtn=Разблокировать
## Success
unlock.success.message=Разблокировка "%s" успешно выполнена! Доступ в хранилище открыт.
unlock.success.rememberChoice=Запомнить выбор и больше не спрашивать
unlock.success.revealBtn=Показать хранилище
## Invalid Mount Point
unlock.error.invalidMountPoint=Точка монтирования - не пустая папка: %s
@@ -102,32 +103,40 @@ unlock.error.invalidMountPoint=Точка монтирования - не пус
# Migration
migration.title=Обновить хранилище
## Start
migration.start.prompt=Хранилище "%s" нужно преобразовать в более новый формат. Прежде чем продолжить, убедитесь, что нет отложенной синхронизации, которая может повлиять на хранилище.
migration.start.prompt=Ваше хранилище "%s" должно быть обновлено до более нового формата. Перед тем, как продолжить, убедитесь в отсутствии отложенной синхронизации, которая может повлиять на хранилище.
migration.start.confirm=Да, моё хранилище полностью синхронизировано
## Run
migration.run.enterPassword=Введите пароль для "%s"
migration.run.startMigrationBtn=Перенести хранилище
migration.run.progressHint=Это может занять некоторое время…
## Sucess
migration.success.nextStepsInstructions=Перенос "%s" успешно выполнен.\nТеперь можно разблокировать хранилище.
migration.success.nextStepsInstructions="%s" перенесено успешно.\nТеперь вы можете разблокировать хранилище.
migration.success.unlockNow=Разблокировать
## Missing file system capabilities
migration.error.missingFileSystemCapabilities.title=Неподдерживаемая файловая система
migration.error.missingFileSystemCapabilities.description=Миграция не была запущена, так как хранилище находится в ненадлежащей файловой системе.
migration.error.missingFileSystemCapabilities.description=Миграция не была запущена, так как ваше хранилище расположено в неподходящей файловой системе.
migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Файловая система не поддерживает длинные имена файлов.
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Файловая система не поддерживает длинные пути.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Файловая система не разрешает чтение.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Файловая система не разрешает запись.
## Impossible
migration.impossible.heading=Невозможно перенести хранилище
migration.impossible.reason=Хранилище не может быть перенесено автоматически из-за несовместимости его расположения или точки доступа.
migration.impossible.moreInfo=Хранилище по-прежнему можно открыть в более старой версии. Чтобы узнать, как вручную перенести хранилище, посетите
# Preferences
preferences.title=Настройки
## General
preferences.general=Общие
preferences.general.theme=Оформление
preferences.general.unlockThemes=Разблокировать тёмный режим
preferences.general.theme=Внешний вид
preferences.general.theme.light=Светлая
preferences.general.theme.dark=Тёмная
preferences.general.unlockThemes=Разблокировать темный режим
preferences.general.startHidden=Скрывать окно при запуске Cryptomator
preferences.general.debugLogging=Вести журнал отладки
preferences.general.debugLogging=Включить ведение журнала отладки
preferences.general.debugDirectory=Показать файлы журнала
preferences.general.autoStart=Запускать Cryptomator при старте системы
preferences.general.interfaceOrientation=Интерфейс
preferences.general.interfaceOrientation=Ориентация интерфейса
preferences.general.interfaceOrientation.ltr=Слева направо
preferences.general.interfaceOrientation.rtl=Справа налево
## Volume
@@ -139,7 +148,7 @@ preferences.volume.webdav.scheme=Схема WebDAV
preferences.updates=Обновления
preferences.updates.currentVersion=Текущая версия: %s
preferences.updates.autoUpdateCheck=Автоматически проверять наличие обновлений
preferences.updates.checkNowBtn=Проверить
preferences.updates.checkNowBtn=Проверить сейчас
preferences.updates.updateAvailable=Доступно обновление до версии %s.
## Donation Key
preferences.donationKey=Пожертвование
@@ -158,19 +167,19 @@ main.donationKeyMissing.tooltip=Мы будем рады финансовой п
main.dropZone.dropVault=Добавить это хранилище
main.dropZone.unknownDragboardContent=Если вы хотите добавить хранилище, перетащите его в это окно
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Нажмите здесь, чтобы добавить хранилище
main.vaultlist.emptyList.onboardingInstruction=Нажмите здесь для добавления хранилища
main.vaultlist.contextMenu.remove=Удалить хранилище
main.vaultlist.addVaultBtn=Добавить хранилище
## Vault Detail
### Welcome
main.vaultDetail.welcomeOnboarding=Благодарим за выбор Cryptomator для защиты ваших файлов. Если требуется помощь, ознакомьтесь с документацией по началу работы:
main.vaultDetail.welcomeOnboarding=Спасибо, что выбрали Cryptomator для защиты ваших файлов. Если вам нужна помощь, ознакомьтесь с нашими руководствами по началу работы:
### Locked
main.vaultDetail.lockedStatus=ЗАБЛОКИРОВАНО
main.vaultDetail.unlockBtn=Разблокировать
main.vaultDetail.optionsBtn=Параметры хранилища
### Unlocked
main.vaultDetail.unlockedStatus=РАЗБЛОКИРОВАНО
main.vaultDetail.accessLocation=Содержимое хранилища доступно здесь:
main.vaultDetail.accessLocation=Содержимое вашего хранилища доступно здесь:
main.vaultDetail.revealBtn=Показать диск
main.vaultDetail.lockBtn=Заблокировать
main.vaultDetail.bytesPerSecondRead=чтение:
@@ -187,9 +196,9 @@ main.vaultDetail.migratePrompt=Чтобы получить доступ к хр
# Wrong File Alert
wrongFileAlert.title=Как шифровать файлы
wrongFileAlert.header.title=Вы пытались зашифровать эти файлы?
wrongFileAlert.header.lead=Для этого Cryptomator создаёт том в системном диспетчере файлов.
wrongFileAlert.header.lead=Для этого Cryptomator создает том в системном файловом менеджере.
wrongFileAlert.instruction.0=Чтобы зашифровать файлы, выполните следующее:
wrongFileAlert.instruction.1=1. Разблокируйте хранилище.
wrongFileAlert.instruction.1=1. Разблокируйте ваше хранилище.
wrongFileAlert.instruction.2=2. Нажмите кнопку "Показать", чтобы открыть том в диспетчере файлов.
wrongFileAlert.instruction.3=3. Добавьте файлы в этот том.
wrongFileAlert.link=Если нужна помощь, посетите
@@ -198,31 +207,35 @@ wrongFileAlert.link=Если нужна помощь, посетите
## General
vaultOptions.general=Общие
vaultOptions.general.unlockAfterStartup=Разблокировать хранилище при запуске Cryptomator
vaultOptions.general.actionAfterUnlock=После успешной разблокировки
vaultOptions.general.actionAfterUnlock.ignore=Ничего не делать
vaultOptions.general.actionAfterUnlock.reveal=Показать диск
vaultOptions.general.actionAfterUnlock.ask=Спрашивать
## Mount
vaultOptions.mount=Монтирование
vaultOptions.mount.readonly=Только чтение
vaultOptions.mount.driveName=Имя диска
vaultOptions.mount.customMountFlags=Свои флаги монтирования
vaultOptions.mount.customMountFlags=Пользовательские флаги монтирования
vaultOptions.mount.winDriveLetterOccupied=занято
vaultOptions.mount.mountPoint=Точка монтирования
vaultOptions.mount.mountPoint.auto=Автоматически выбирать подходящую
vaultOptions.mount.mountPoint.driveLetter=Использовать назначенную букву диска
vaultOptions.mount.mountPoint.custom=Свой путь
vaultOptions.mount.mountPoint.directoryPickerButton=Выбрать…
vaultOptions.mount.mountPoint.directoryPickerTitle=Выберите пустую папку
vaultOptions.mount.mountPoint.directoryPickerTitle=Выберите пустой каталог
## Master Key
vaultOptions.masterkey=Пароль
vaultOptions.masterkey.changePasswordBtn=Изменить пароль
vaultOptions.masterkey.recoveryKeyExpanation=Ключ восстановления - это единственный способ восстановить доступ к хранилищу при утере пароля.
vaultOptions.masterkey.recoveryKeyExpanation=Ключ восстановления - это единственный способ восстановить доступ к хранилищу в случае утери пароля.
vaultOptions.masterkey.showRecoveryKeyBtn=Показать ключ восстановления
vaultOptions.masterkey.recoverPasswordBtn=Восстановить пароль
# Recovery Key
recoveryKey.title=Ключ восстановления
recoveryKey.enterPassword.prompt=Введите пароль, чтобы показать ключ для "%s":
recoveryKey.enterPassword.prompt=Введите пароль для отображения ключа восстановления для "%s":
recoveryKey.display.message=Следующий ключ восстановления можно использовать для восстановления доступа к "%s":
recoveryKey.display.StorageHints=Храните его в надёжном месте, например:\n • в диспетчере паролей\n • на флеш-накопителе USB\n • распечатанным на бумаге
recoveryKey.recover.prompt=Введите ключ восстановления для "%s":
recoveryKey.display.StorageHints=Храните его в надежном месте, например:\n • в менеджере паролей\n • на флэш-накопителе USB\n • распечатайте на бумаге
recoveryKey.recover.prompt=Введите ваш ключ восстановления для "%s":
recoveryKey.recover.validKey=Это действительный ключ восстановления
recoveryKey.printout.heading=Ключ восстановления Cryptomator\n"%s"\n
@@ -239,5 +252,5 @@ passwordStrength.messageLabel.3=Сильный
passwordStrength.messageLabel.4=Очень сильный
# Quit
quit.prompt=Выйти из приложения? Есть незаблокированные хранилища.
quit.prompt=Выйти из приложения? Есть разблокированные хранилища.
quit.lockAndQuit=Заблокировать и выйти

View File

@@ -77,12 +77,23 @@ removeVault.information=To spôsobí, že Cryptomator iba zabudne na tento trezo
removeVault.confirmBtn=Odstrániť trezor
# Change Password
changepassword.title=Zmeniť heslo
changepassword.enterOldPassword=Zadaj aktuálne heslo pre "%s"
changepassword.finalConfirmation=Chápem, že ak zabudnem svoje heslo, nebudem mať prístup k svojim údajom
# Forget Password
forgetPassword.title=Zabudnuté heslo
forgetPassword.information=Týmto vymažete uložené heslo tohto trezoru z vášho systému kľúčov.
forgetPassword.confirmBtn=Zabudnuté heslo
# Unlock
unlock.title=Odomknúť trezor
unlock.passwordPrompt=Zadajte heslo pre "%s":
unlock.savePassword=Uložiť heslo
unlock.unlockBtn=Odomknúť
## Success
unlock.success.message=Úspešne sa odomkol "%s"! Váš trezor je teraz prístupný.
unlock.success.revealBtn=Odhaliť trezor
## Invalid Mount Point
# Migration
@@ -91,6 +102,7 @@ unlock.unlockBtn=Odomknúť
## Sucess
migration.success.unlockNow=Odomknúť teraz
## Missing file system capabilities
## Impossible
# Preferences
preferences.title=Predvoľby
@@ -101,6 +113,7 @@ preferences.title=Predvoľby
## About
# Main Window
main.closeBtn.tooltip=Zavrieť
main.preferencesBtn.tooltip=Predvoľby
## Drag 'n' Drop
## Vault List
@@ -122,6 +135,7 @@ main.vaultDetail.lockBtn=Uzamknúť
## Mount
vaultOptions.mount.mountPoint.directoryPickerButton=Vybrať…
## Master Key
vaultOptions.masterkey.changePasswordBtn=Zmeniť heslo
# Recovery Key

View File

@@ -95,6 +95,7 @@ unlock.savePassword=Spara lösenord
unlock.unlockBtn=Lås upp
## Success
unlock.success.message="%s" upplåst! Ditt valv är nu åtkomligt.
unlock.success.rememberChoice=Kom ihåg valet, visa inte detta igen
unlock.success.revealBtn=Visa valv
## Invalid Mount Point
unlock.error.invalidMountPoint=Monteringspunkten är inte en tom katalog: %s
@@ -107,6 +108,7 @@ migration.start.confirm=Ja, mitt valv är synkroniserat
## Run
migration.run.enterPassword=Ange lösenordet för "%s"
migration.run.startMigrationBtn=Migrera valv
migration.run.progressHint=Detta kan ta lite tid…
## Sucess
migration.success.nextStepsInstructions="%s" migrerades fullständigt.\nDu kan nu låsa upp ditt valv.
migration.success.unlockNow=Lås upp nu
@@ -117,12 +119,18 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Filsystemet
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Filsystemet har inte stöd för långa filnamn.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Filsystemet tillåter inte att läsas.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Filsystemet tillåter inte att skrivas till.
## Impossible
migration.impossible.heading=Kan inte migrera valv
migration.impossible.reason=Valvet kan inte migreras automatiskt eftersom dess lagringsplats eller åtkomstpunkt inte är kompatibel.
migration.impossible.moreInfo=Valvet kan fortfarande öppnas med en äldre version. För instruktioner om hur du manuellt migrerar ett valv, besök
# Preferences
preferences.title=Inställningar
## General
preferences.general=Allmänt
preferences.general.theme=Utseende
preferences.general.theme.light=Ljust
preferences.general.theme.dark=Mörkt
preferences.general.unlockThemes=Lås upp mörkt läge
preferences.general.startHidden=Dölj fönster när Cryptomator startar
preferences.general.debugLogging=Aktivera loggning för felsökning
@@ -198,6 +206,10 @@ wrongFileAlert.link=För ytterligare hjälp, besök
## General
vaultOptions.general=Allmänt
vaultOptions.general.unlockAfterStartup=Lås upp valvet när Cryptomator startas
vaultOptions.general.actionAfterUnlock=Efter lyckad upplåsning
vaultOptions.general.actionAfterUnlock.ignore=Gör ingenting
vaultOptions.general.actionAfterUnlock.reveal=Visa enhet
vaultOptions.general.actionAfterUnlock.ask=Fråga
## Mount
vaultOptions.mount=Montering
vaultOptions.mount.readonly=Skrivskyddad

View File

@@ -95,6 +95,7 @@ unlock.savePassword=Şifreyi Kaydet
unlock.unlockBtn=Kilidi Aç
## Success
unlock.success.message="%s" 'nin kilidi başarıyla açıldı! Kasanız şimdi erişilebilir durumda.
unlock.success.rememberChoice=Seçimi hatırla, bunu bir daha gösterme
unlock.success.revealBtn=Kasayı Göster
## Invalid Mount Point
unlock.error.invalidMountPoint=Bağlantı noktası boş bir dizin değil: %s
@@ -107,6 +108,7 @@ migration.start.confirm=Evet, kasam tamamen senkronize edildi
## Run
migration.run.enterPassword="%s" için şifre girin
migration.run.startMigrationBtn=Kasayı Taşı
migration.run.progressHint=Bu biraz zaman alabilir…
## Sucess
migration.success.nextStepsInstructions="%s" başarıyla taşındı.\nKasanızın kilidini şimdi kaldırabilirsiniz.
migration.success.unlockNow=Kilidi Şimdi Aç
@@ -117,12 +119,18 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Dosya sistem
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Dosya sistemi, uzun yolları desteklemiyor.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Dosya sistemi okunmaya izin vermiyor.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Dosya sistemi yazılmaya izin vermiyor.
## Impossible
migration.impossible.heading=Kasa taşınamadı
migration.impossible.reason=Depolama konumu veya erişim noktası uyumlu olmadığı için kasa otomatik olarak taşınamıyor.
migration.impossible.moreInfo=Kasa eski bir sürüm ile halen açılabilir. Kasanın nasıl elle taşınabileceğine dair yönlendirme için ziyaret edin
# Preferences
preferences.title=Seçenekler
## General
preferences.general=Genel
preferences.general.theme=Görünüş ve Davranış
preferences.general.theme.light=ık
preferences.general.theme.dark=Koyu
preferences.general.unlockThemes=Koyu modun kilidini aç
preferences.general.startHidden=Cryptomator'ı başlatırken pencereyi gizle
preferences.general.debugLogging=Hata ayıklama günlüğünü etkinleştir
@@ -198,6 +206,10 @@ wrongFileAlert.link=Daha fazla yardım için, ziyaret edin:
## General
vaultOptions.general=Genel
vaultOptions.general.unlockAfterStartup=Cryptomator başlatıldığında kasayı
vaultOptions.general.actionAfterUnlock=Başarılı kilit açmanın ardından
vaultOptions.general.actionAfterUnlock.ignore=Hiçbir şey yapma
vaultOptions.general.actionAfterUnlock.reveal=Sürücüyü Göster
vaultOptions.general.actionAfterUnlock.ask=Sor
## Mount
vaultOptions.mount=Bağlantı
vaultOptions.mount.readonly=Salt-Okunur

View File

@@ -95,6 +95,7 @@ unlock.savePassword=保存密码
unlock.unlockBtn=解锁
## Success
unlock.success.message=已成功解锁 "%s" !您的保险库现在可以访问。
unlock.success.rememberChoice=记住该选择,不要再显示
unlock.success.revealBtn=显示保险库
## Invalid Mount Point
unlock.error.invalidMountPoint=挂载点不是一个空目录: %s
@@ -107,6 +108,7 @@ migration.start.confirm=是的,我的保险库已完全同步
## Run
migration.run.enterPassword=输入 "%s" 的密码
migration.run.startMigrationBtn=迁移保险库
migration.run.progressHint=这可能需要一些时间…
## Sucess
migration.success.nextStepsInstructions=已成功迁移 "%s"\n您现在可以解锁您的保险库。
migration.success.unlockNow=立即解锁
@@ -117,12 +119,18 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=此文件系
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=此文件系统不支持长路径
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=文件系统不允许读取访问
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=文件系统不允许写入
## Impossible
migration.impossible.heading=无法迁移保险库
migration.impossible.reason=此保险库不能自动迁移,因为它的存储位置或接入点不兼容
migration.impossible.moreInfo=此保险库仍可使用旧版本来打开。关于如何手动迁移保险库的操作指南,请访问
# Preferences
preferences.title=首选项
## General
preferences.general=常规​​​​​
preferences.general.theme=界面外观
preferences.general.theme.light=亮色
preferences.general.theme.dark=暗色
preferences.general.unlockThemes=解锁暗黑模式
preferences.general.startHidden=最小化启动 Cryptomator 到系统托盘
preferences.general.debugLogging=启用调试日志
@@ -198,6 +206,10 @@ wrongFileAlert.link=如需进一步帮助,请访问
## General
vaultOptions.general=常规​​​​​
vaultOptions.general.unlockAfterStartup=启动 Cryptomator 时解锁保险库 (此功能需先勾选保存密码)
vaultOptions.general.actionAfterUnlock=解锁成功后的操作
vaultOptions.general.actionAfterUnlock.ignore=什么也不做
vaultOptions.general.actionAfterUnlock.reveal=显示驱动器
vaultOptions.general.actionAfterUnlock.ask=询问
## Mount
vaultOptions.mount=挂载
vaultOptions.mount.readonly=只读

View File

@@ -95,6 +95,7 @@ unlock.savePassword=儲存密碼
unlock.unlockBtn=解鎖
## Success
unlock.success.message=成功解鎖 "%s"!您現在可以存取您的加密檔案庫。
unlock.success.rememberChoice=記得這個決定,不要再顯示
unlock.success.revealBtn=顯示加密檔案庫
## Invalid Mount Point
unlock.error.invalidMountPoint=掛載點不是空資料夾:%s
@@ -107,6 +108,7 @@ migration.start.confirm=是的,我的加密檔案庫已同步完成
## Run
migration.run.enterPassword=輸入 "%s" 的密碼
migration.run.startMigrationBtn=升級加密檔案庫
migration.run.progressHint=這可能需要一點時間…
## Sucess
migration.success.nextStepsInstructions=已成功升級 "%s"。\n您現在可以解鎖您的加密檔案庫了。
migration.success.unlockNow=立即解鎖
@@ -117,12 +119,18 @@ migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=檔案系統
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=檔案系統不支援長路徑名
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=此檔案系統不允許讀取。
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=此檔案系統不允許寫入。
## Impossible
migration.impossible.heading=無法升級加密檔案庫
migration.impossible.reason=此加密檔案庫無法自動升級,因為它的儲存位置並不相容。
migration.impossible.moreInfo=此加密檔案庫仍可被舊版本開啟。如需手動升級加密檔案庫的操作指引,請參照
# Preferences
preferences.title=偏好
## General
preferences.general=一般
preferences.general.theme=外觀
preferences.general.theme.light=亮色
preferences.general.theme.dark=暗色
preferences.general.unlockThemes=解鎖暗色模式
preferences.general.startHidden=啟動 Cryptomator 時隱藏視窗
preferences.general.debugLogging=啟用除錯日誌
@@ -192,12 +200,16 @@ wrongFileAlert.instruction.0=請依下列步驟加密檔案:
wrongFileAlert.instruction.1=1. 解鎖您的加密檔案庫。
wrongFileAlert.instruction.2=2. 點擊「顯示」可在檔案總管中檢視磁區。
wrongFileAlert.instruction.3=3. 把您的檔案加入磁區。
wrongFileAlert.link=如需進一步協助協助,請前往
wrongFileAlert.link=如需進一步協助協助,請參照
# Vault Options
## General
vaultOptions.general=一般
vaultOptions.general.unlockAfterStartup=啟動 Cryptomator 時解鎖加密檔案庫
vaultOptions.general.actionAfterUnlock=成功解鎖後
vaultOptions.general.actionAfterUnlock.ignore=無動作
vaultOptions.general.actionAfterUnlock.reveal=顯示磁碟
vaultOptions.general.actionAfterUnlock.ask=詢問
## Mount
vaultOptions.mount=掛載
vaultOptions.mount.readonly=唯讀

View File

@@ -11,13 +11,15 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see http://www.gnu.org/licenses/.
Cryptomator uses 50 third-party dependencies under the following licenses:
Cryptomator uses 52 third-party dependencies under the following licenses:
Apache License v2.0:
- HKDF-RFC5869 (at.favre.lib:hkdf:1.0.2 - https://github.com/patrickfav/hkdf)
- HKDF-RFC5869 (at.favre.lib:hkdf:1.1.0 - https://github.com/patrickfav/hkdf)
- jffi (com.github.jnr:jffi:1.2.23 - http://github.com/jnr/jffi)
- jnr-a64asm (com.github.jnr:jnr-a64asm:1.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm)
- jnr-constants (com.github.jnr:jnr-constants:0.9.15 - http://github.com/jnr/jnr-constants)
- jnr-enxio (com.github.jnr:jnr-enxio:0.21 - http://github.com/jnr/jnr-enxio)
- jnr-ffi (com.github.jnr:jnr-ffi:2.1.12 - http://github.com/jnr/jnr-ffi)
- jnr-unixsocket (com.github.jnr:jnr-unixsocket:0.23 - http://github.com/jnr/jnr-unixsocket)
- FindBugs-jsr305 (com.google.code.findbugs:jsr305:3.0.2 - http://findbugs.sourceforge.net/)
- Gson (com.google.code.gson:gson:2.8.6 - https://github.com/google/gson/gson)
- Dagger (com.google.dagger:dagger:2.27 - https://github.com/google/dagger)
@@ -68,17 +70,17 @@ Cryptomator uses 50 third-party dependencies under the following licenses:
- javafx-fxml (org.openjfx:javafx-fxml:14 - https://openjdk.java.net/projects/openjfx/javafx-fxml/)
- javafx-graphics (org.openjfx:javafx-graphics:14 - https://openjdk.java.net/projects/openjfx/javafx-graphics/)
LGPL 2.1:
- dbus-java (com.github.hypfvieh:dbus-java:3.0.2 - https://github.com/hypfvieh/dbus-java/dbus-java)
- dbus-java (com.github.hypfvieh:dbus-java:3.2.1 - https://github.com/hypfvieh/dbus-java/dbus-java)
- jnr-posix (com.github.jnr:jnr-posix:3.0.54 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
- Java Native Access (net.java.dev.jna:jna:5.1.0 - https://github.com/java-native-access/jna)
- Java Native Access Platform (net.java.dev.jna:jna-platform:5.1.0 - https://github.com/java-native-access/jna)
MIT License:
- java jwt (com.auth0:java-jwt:3.10.2 - https://github.com/auth0/java-jwt)
- java-utils (com.github.hypfvieh:java-utils:1.0.5 - https://github.com/hypfvieh/java-utils)
- java-utils (com.github.hypfvieh:java-utils:1.0.6 - https://github.com/hypfvieh/java-utils)
- jnr-x86asm (com.github.jnr:jnr-x86asm:1.0.2 - http://github.com/jnr/jnr-x86asm)
- jnr-fuse (com.github.serceman:jnr-fuse:0.5.4 - no url defined)
- zxcvbn4j (com.nulab-inc:zxcvbn:1.3.0 - https://github.com/nulab/zxcvbn4j)
- secret-service (de.swiesend:secret-service:1.0.0-RC.3 - https://github.com/swiesend/secret-service)
- secret-service (de.swiesend:secret-service:1.0.0 - https://github.com/swiesend/secret-service)
- Checker Qual (org.checkerframework:checker-qual:2.10.0 - https://checkerframework.org)
- SLF4J API Module (org.slf4j:slf4j-api:1.7.30 - http://www.slf4j.org)
The BSD 2-Clause License:

View File

@@ -152,7 +152,7 @@ class SecurePasswordFieldTest {
CharSequence result1 = pwField.getCharacters();
Assertions.assertEquals("topSecret", result1.toString());
pwField.swipe();
pwField.wipe();
CharSequence result2 = pwField.getCharacters();
Assertions.assertEquals(" ", result1.toString());
Assertions.assertEquals("", result2.toString());