mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 16:51:28 +00:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c45455cb2 | ||
|
|
411aca8695 | ||
|
|
3de69021df | ||
|
|
8418038736 | ||
|
|
69f8c46f15 | ||
|
|
25195fffe2 | ||
|
|
0e784a6ffc | ||
|
|
351f96fa8b | ||
|
|
703fb4fb51 | ||
|
|
a975df6f8c | ||
|
|
3bd2a2f911 | ||
|
|
0091c401df | ||
|
|
4ab7bfaa20 | ||
|
|
7e0ffb43a6 | ||
|
|
d3063c8117 | ||
|
|
cd1ed088c3 | ||
|
|
e831debb94 | ||
|
|
8d15a6612f | ||
|
|
330b1cbf7d | ||
|
|
c925727269 | ||
|
|
ac2dc06bd6 | ||
|
|
80b83adb53 | ||
|
|
ddc31ac2bb | ||
|
|
eb7c5d0b2b | ||
|
|
e5ce3cb62d | ||
|
|
d3a6964d8f | ||
|
|
d80605532f | ||
|
|
8d5fb14b50 | ||
|
|
570286c7df | ||
|
|
11e3ee8d90 | ||
|
|
0664c670f7 | ||
|
|
f36f9d412c | ||
|
|
bd734f1960 | ||
|
|
675a0b1a73 | ||
|
|
a2816277bf | ||
|
|
e37f1f914b | ||
|
|
adf7694308 | ||
|
|
c13449c6ad | ||
|
|
f72035210c | ||
|
|
89ef5238ea | ||
|
|
8198f66c1f | ||
|
|
eb5aa4ee44 | ||
|
|
adb09a0efe | ||
|
|
241eb8bed5 | ||
|
|
842a0d6ff3 | ||
|
|
3200917df2 | ||
|
|
12dcf0647d | ||
|
|
22859c9ffa | ||
|
|
29182156df | ||
|
|
357d63f398 | ||
|
|
e594bf208d | ||
|
|
2f0de3520a | ||
|
|
8def68eb02 | ||
|
|
aef33dc864 | ||
|
|
06d2f2d9e9 | ||
|
|
dad0ad76fb | ||
|
|
99fa8e7c8e | ||
|
|
ab0f175edf | ||
|
|
8cb9728565 | ||
|
|
d91d27f2a4 | ||
|
|
b2a6e038ae | ||
|
|
75f360903c | ||
|
|
79c3137b90 | ||
|
|
d2189d379c | ||
|
|
49aead7323 | ||
|
|
7bd610563f | ||
|
|
5c1a1ad162 | ||
|
|
86906d0049 | ||
|
|
93011dc754 | ||
|
|
b084b651af | ||
|
|
fecf9c0423 | ||
|
|
117fe78a4a | ||
|
|
153d43573a | ||
|
|
9145f5d2f8 | ||
|
|
835ea3b640 | ||
|
|
1e7eb23d1b | ||
|
|
cfe25d0bf5 | ||
|
|
b14939bd77 | ||
|
|
26e140ee22 | ||
|
|
1c5ecf8c01 | ||
|
|
9b528a05b5 | ||
|
|
3a7aa6d64f | ||
|
|
55820e47f9 |
4
.github/ISSUE_TEMPLATE/bug.md
vendored
4
.github/ISSUE_TEMPLATE/bug.md
vendored
@@ -23,7 +23,7 @@ Of course, we also expect you to search for existing similar issues first! ;) ht
|
||||
|
||||
* Operating system and version: [Windows/macOS/Linux + Version]
|
||||
* Cryptomator version: [Shown in the settings]
|
||||
* Drive: [Dokany/FUSE/WebDAV]
|
||||
* Volume type: [Dokany/FUSE/WebDAV, shown in the settings]
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
@@ -58,4 +58,4 @@ Log file location:
|
||||
- macOS: ~/Library/Logs/Cryptomator
|
||||
- Linux: ~/.local/share/Cryptomator/logs
|
||||
|
||||
-->
|
||||
-->
|
||||
|
||||
28
.idea/compiler.xml
generated
28
.idea/compiler.xml
generated
@@ -7,34 +7,26 @@
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<processorPath useClasspath="false">
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.27/dagger-compiler-2.27.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.27/dagger-2.27.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.22/dagger-compiler-2.22.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.22/dagger-2.22.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.27/dagger-producers-2.27.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/27.1-jre/guava-27.1-jre.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.1/jsr305-3.0.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-qual/2.5.2/checker-qual-2.5.2.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.2.0/error_prone_annotations-2.2.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.17/animal-sniffer-annotations-1.17.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.22/dagger-producers-2.22.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/25.0-jre/guava-25.0-jre.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.3/checker-compat-qual-2.5.3.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.27/dagger-spi-2.27.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.1.3/error_prone_annotations-2.1.3.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.14/animal-sniffer-annotations-1.14.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.22/dagger-spi-2.22.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/net/ltgt/gradle/incap/incap/0.2/incap-0.2.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.3.50/kotlin-stdlib-1.3.50.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.50/kotlin-stdlib-common-1.3.50.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.1.0/kotlinx-metadata-jvm-0.1.0.jar" />
|
||||
</processorPath>
|
||||
<module name="keychain" />
|
||||
<module name="launcher" />
|
||||
<module name="commons" />
|
||||
<module name="ui" />
|
||||
<module name="launcher" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.5.2</version>
|
||||
<version>1.5.7</version>
|
||||
</parent>
|
||||
<artifactId>buildkit</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.5.2</version>
|
||||
<version>1.5.7</version>
|
||||
</parent>
|
||||
<artifactId>commons</artifactId>
|
||||
<name>Cryptomator Commons</name>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
public enum UiTheme {
|
||||
LIGHT("Light"),
|
||||
DARK("Dark");
|
||||
// CUSTOM("Custom (%s)");
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
private String displayName;
|
||||
public enum UiTheme {
|
||||
LIGHT("preferences.general.theme.light"), //
|
||||
DARK("preferences.general.theme.dark"), //
|
||||
AUTOMATIC("preferences.general.theme.automatic");
|
||||
|
||||
public static UiTheme[] applicableValues() {
|
||||
if (SystemUtils.IS_OS_MAC) {
|
||||
return values();
|
||||
} else {
|
||||
return new UiTheme[]{LIGHT, DARK};
|
||||
}
|
||||
}
|
||||
|
||||
private final String displayName;
|
||||
|
||||
UiTheme(String displayName) {
|
||||
this.displayName = displayName;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -35,9 +34,11 @@ public class VaultSettings {
|
||||
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
|
||||
public static final boolean DEFAULT_USES_READONLY_MODE = false;
|
||||
public static final String DEFAULT_MOUNT_FLAGS = "";
|
||||
public static final String DEFAULT_MOUNT_NAME = "Vault";
|
||||
public static final int DEFAULT_FILENAME_LENGTH_LIMIT = -1;
|
||||
|
||||
private static final Random RNG = new Random();
|
||||
public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
|
||||
|
||||
private static final Random RNG = new Random();
|
||||
|
||||
private final String id;
|
||||
private final ObjectProperty<Path> path = new SimpleObjectProperty();
|
||||
@@ -45,25 +46,36 @@ 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);
|
||||
|
||||
EasyBind.subscribe(path, this::deriveMountNameFromPath);
|
||||
EasyBind.subscribe(path, this::deriveMountNameFromPathOrUseDefault);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (path != null && StringUtils.isBlank(mountName.get())) {
|
||||
mountName.set(normalizeMountName(path.getFileName().toString()));
|
||||
private void deriveMountNameFromPathOrUseDefault(Path newPath) {
|
||||
final boolean mountNameSet = !StringUtils.isBlank(mountName.get());
|
||||
final boolean dirnameExists = (newPath != null) && (newPath.getFileName() != null);
|
||||
|
||||
if (!mountNameSet && dirnameExists) {
|
||||
mountName.set(normalizeMountName(newPath.getFileName().toString()));
|
||||
} else if (!mountNameSet && !dirnameExists) {
|
||||
mountName.set(DEFAULT_MOUNT_NAME + id);
|
||||
} else if (mountNameSet && dirnameExists) {
|
||||
if (mountName.get().equals(DEFAULT_MOUNT_NAME + id)) {
|
||||
//this is okay, since this function is only executed if the path changes (aka, the vault is created or added)
|
||||
mountName.set(newPath.getFileName().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,17 +134,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();
|
||||
}
|
||||
@@ -145,11 +157,19 @@ public class VaultSettings {
|
||||
public StringProperty mountFlags() {
|
||||
return mountFlags;
|
||||
}
|
||||
|
||||
|
||||
public IntegerProperty filenameLengthLimit() {
|
||||
return filenameLengthLimit;
|
||||
}
|
||||
|
||||
public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
|
||||
return actionAfterUnlock;
|
||||
}
|
||||
|
||||
public WhenUnlocked getActionAfterUnlock() {
|
||||
return actionAfterUnlock.get();
|
||||
}
|
||||
|
||||
/* Hashcode/Equals */
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.5.2</version>
|
||||
<version>1.5.7</version>
|
||||
</parent>
|
||||
<artifactId>keychain</artifactId>
|
||||
<name>System Keychain Access</name>
|
||||
@@ -15,16 +15,27 @@
|
||||
<artifactId>commons</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JavaFx -->
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-base</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-graphics</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Google -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Google -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
@@ -40,7 +51,6 @@
|
||||
<dependency>
|
||||
<groupId>de.swiesend</groupId>
|
||||
<artifactId>secret-service</artifactId>
|
||||
<version>1.0.0-RC.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
public interface KeychainAccess {
|
||||
|
||||
/**
|
||||
* Associates a passphrase with a given key.
|
||||
*
|
||||
* @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}.
|
||||
* @param passphrase The secret to store in this keychain.
|
||||
*/
|
||||
void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException;
|
||||
|
||||
/**
|
||||
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
|
||||
* @return The stored passphrase for the given key or <code>null</code> if no value for the given key could be found.
|
||||
*/
|
||||
char[] loadPassphrase(String key) throws KeychainAccessException;
|
||||
|
||||
/**
|
||||
* Deletes a passphrase with a given key.
|
||||
*
|
||||
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
|
||||
*/
|
||||
void deletePassphrase(String key) throws KeychainAccessException;
|
||||
|
||||
}
|
||||
@@ -5,7 +5,36 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
interface KeychainAccessStrategy extends KeychainAccess {
|
||||
interface KeychainAccessStrategy {
|
||||
|
||||
/**
|
||||
* Associates a passphrase with a given key.
|
||||
*
|
||||
* @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}.
|
||||
* @param passphrase The secret to store in this keychain.
|
||||
*/
|
||||
void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException;
|
||||
|
||||
/**
|
||||
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
|
||||
* @return The stored passphrase for the given key or <code>null</code> if no value for the given key could be found.
|
||||
*/
|
||||
char[] loadPassphrase(String key) throws KeychainAccessException;
|
||||
|
||||
/**
|
||||
* Deletes a passphrase with a given key.
|
||||
*
|
||||
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
|
||||
*/
|
||||
void deletePassphrase(String key) throws KeychainAccessException;
|
||||
|
||||
/**
|
||||
* Updates a passphrase with a given key. Noop, if there is no item for the 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;
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if this KeychainAccessStrategy works on the current machine.
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class KeychainManager implements KeychainAccessStrategy {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(KeychainManager.class);
|
||||
|
||||
private final KeychainAccessStrategy keychain;
|
||||
private LoadingCache<String, BooleanProperty> passphraseStoredProperties;
|
||||
|
||||
KeychainManager(KeychainAccessStrategy keychain) {
|
||||
assert keychain.isSupported();
|
||||
this.keychain = keychain;
|
||||
this.passphraseStoredProperties = CacheBuilder.newBuilder() //
|
||||
.weakValues() //
|
||||
.build(CacheLoader.from(this::createStoredPassphraseProperty));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
|
||||
keychain.storePassphrase(key, passphrase);
|
||||
setPassphraseStored(key, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] loadPassphrase(String key) throws KeychainAccessException {
|
||||
char[] passphrase = keychain.loadPassphrase(key);
|
||||
setPassphraseStored(key, passphrase != null);
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePassphrase(String key) throws KeychainAccessException {
|
||||
keychain.deletePassphrase(key);
|
||||
setPassphraseStored(key, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
|
||||
keychain.changePassphrase(key, passphrase);
|
||||
setPassphraseStored(key, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the keychain knows a passphrase for the given key.
|
||||
* <p>
|
||||
* Expensive operation. If possible, use {@link #getPassphraseStoredProperty(String)} instead.
|
||||
*
|
||||
* @param key The key to look up
|
||||
* @return <code>true</code> if a password for <code>key</code> is stored.
|
||||
* @throws KeychainAccessException
|
||||
*/
|
||||
public boolean isPassphraseStored(String key) throws KeychainAccessException {
|
||||
char[] storedPw = null;
|
||||
try {
|
||||
storedPw = keychain.loadPassphrase(key);
|
||||
return storedPw != null;
|
||||
} finally {
|
||||
if (storedPw != null) {
|
||||
Arrays.fill(storedPw, ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setPassphraseStored(String key, boolean value) {
|
||||
BooleanProperty property = passphraseStoredProperties.getIfPresent(key);
|
||||
if (property != null) {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
property.set(value);
|
||||
} else {
|
||||
LOG.warn("");
|
||||
Platform.runLater(() -> property.set(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable property for use in the UI that tells whether a passphrase is stored for the given key.
|
||||
* <p>
|
||||
* Assuming that this process is the only process modifying Cryptomator-related items in the system keychain, this
|
||||
* property stays in memory in an attempt to avoid unnecessary calls to the system keychain. Note that due to this
|
||||
* fact the value stored in the returned property is not 100% reliable. Code defensively!
|
||||
*
|
||||
* @param key The key to look up
|
||||
* @return An observable property which is <code>true</code> when it almost certain that a password for <code>key</code> is stored.
|
||||
* @see #isPassphraseStored(String)
|
||||
*/
|
||||
public ReadOnlyBooleanProperty getPassphraseStoredProperty(String key) {
|
||||
return passphraseStoredProperties.getUnchecked(key);
|
||||
}
|
||||
|
||||
private BooleanProperty createStoredPassphraseProperty(String key) {
|
||||
try {
|
||||
LOG.warn("LOAD"); // TODO remove
|
||||
return new SimpleBooleanProperty(isPassphraseStored(key));
|
||||
} catch (KeychainAccessException e) {
|
||||
return new SimpleBooleanProperty(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,10 +5,10 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.ElementsIntoSet;
|
||||
import dagger.multibindings.IntoSet;
|
||||
import org.cryptomator.common.JniModule;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
@@ -16,18 +16,30 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@Module(includes = {JniModule.class})
|
||||
public class KeychainModule {
|
||||
public abstract class KeychainModule {
|
||||
|
||||
@Provides
|
||||
@ElementsIntoSet
|
||||
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain, LinuxSecretServiceKeychainAccess linKeychain) {
|
||||
return Sets.newHashSet(macKeychain, winKeychain, linKeychain);
|
||||
}
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract KeychainAccessStrategy bindMacSystemKeychainAccess(MacSystemKeychainAccess keychainAccessStrategy);
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract KeychainAccessStrategy bindWindowsProtectedKeychainAccess(WindowsProtectedKeychainAccess keychainAccessStrategy);
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract KeychainAccessStrategy bindLinuxSecretServiceKeychainAccess(LinuxSecretServiceKeychainAccess keychainAccessStrategy);
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public Optional<KeychainAccess> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
|
||||
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst();
|
||||
static Optional<KeychainAccessStrategy> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
|
||||
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).findFirst();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static Optional<KeychainManager> provideKeychainManager(Optional<KeychainAccessStrategy> keychainAccess) {
|
||||
return keychainAccess.map(KeychainManager::new);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package org.cryptomator.keychain;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A facade to LinuxSecretServiceKeychainAccessImpl that doesn't depend on libraries that are unavailable on Mac and Windows.
|
||||
*/
|
||||
@Singleton
|
||||
public class LinuxSecretServiceKeychainAccess implements KeychainAccessStrategy {
|
||||
|
||||
// the actual implementation is hidden in this delegate object which is loaded via reflection,
|
||||
@@ -48,4 +50,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,11 +8,13 @@ package org.cryptomator.keychain;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.jni.MacFunctions;
|
||||
import org.cryptomator.jni.MacKeychainAccess;
|
||||
|
||||
@Singleton
|
||||
class MacSystemKeychainAccess implements KeychainAccessStrategy {
|
||||
|
||||
private final Optional<MacFunctions> macFunctions;
|
||||
@@ -46,4 +48,11 @@ class MacSystemKeychainAccess implements KeychainAccessStrategy {
|
||||
keychain().deletePassword(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassphrase(String key, CharSequence passphrase) {
|
||||
if (keychain().deletePassword(key)) {
|
||||
keychain().storePassword(key, passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -50,6 +51,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
@Singleton
|
||||
class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WindowsProtectedKeychainAccess.class);
|
||||
@@ -112,6 +114,14 @@ class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
|
||||
saveKeychainEntries();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassphrase(String key, CharSequence passphrase) {
|
||||
loadKeychainEntriesIfNeeded();
|
||||
if (keychainEntries.remove(key) != null) {
|
||||
storePassphrase(key, passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return SystemUtils.IS_OS_WINDOWS && winFunctions.isPresent() && !keychainPaths.isEmpty();
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
|
||||
class KeychainManagerTest {
|
||||
|
||||
@Test
|
||||
public void testStoreAndLoad() throws KeychainAccessException {
|
||||
KeychainManager keychainManager = new KeychainManager(new MapKeychainAccess());
|
||||
keychainManager.storePassphrase("test", "asd");
|
||||
Assertions.assertArrayEquals("asd".toCharArray(), keychainManager.loadPassphrase("test"));
|
||||
}
|
||||
|
||||
@Nested
|
||||
public static class WhenObservingProperties {
|
||||
|
||||
@BeforeAll
|
||||
public static void startup() throws InterruptedException {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Platform.startup(latch::countDown);
|
||||
latch.await(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPropertyChangesWhenStoringPassword() throws KeychainAccessException, InterruptedException {
|
||||
KeychainManager keychainManager = new KeychainManager(new MapKeychainAccess());
|
||||
ReadOnlyBooleanProperty property = keychainManager.getPassphraseStoredProperty("test");
|
||||
Assertions.assertEquals(false, property.get());
|
||||
|
||||
keychainManager.storePassphrase("test", "bar");
|
||||
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Platform.runLater(() -> {
|
||||
result.set(property.get());
|
||||
latch.countDown();
|
||||
});
|
||||
latch.await(1, TimeUnit.SECONDS);
|
||||
Assertions.assertEquals(true, result.get());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class KeychainModuleTest {
|
||||
|
||||
@Test
|
||||
public void testGetKeychain() throws KeychainAccessException {
|
||||
Optional<KeychainAccess> keychainAccess = DaggerTestKeychainComponent.builder().keychainModule(new TestKeychainModule()).build().keychainAccess();
|
||||
Assertions.assertTrue(keychainAccess.isPresent());
|
||||
Assertions.assertTrue(keychainAccess.get() instanceof MapKeychainAccess);
|
||||
keychainAccess.get().storePassphrase("test", "asd");
|
||||
Assertions.assertArrayEquals("asd".toCharArray(), keychainAccess.get().loadPassphrase("test"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import dagger.Component;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = KeychainModule.class)
|
||||
interface TestKeychainComponent {
|
||||
|
||||
Optional<KeychainAccess> keychainAccess();
|
||||
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class TestKeychainModule extends KeychainModule {
|
||||
|
||||
@Override
|
||||
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain, LinuxSecretServiceKeychainAccess linKeychain) {
|
||||
return Set.of(new MapKeychainAccess());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.5.2</version>
|
||||
<version>1.5.7</version>
|
||||
</parent>
|
||||
<artifactId>launcher</artifactId>
|
||||
<name>Cryptomator Launcher</name>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.cryptomator.logging;
|
||||
|
||||
import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy;
|
||||
import ch.qos.logback.core.rolling.TriggeringPolicyBase;
|
||||
import ch.qos.logback.core.util.FileSize;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Triggers a roll-over either on the first log event or if watched log file reaches a certain size
|
||||
*
|
||||
* @param <E> Event type the policy possibly reacts to
|
||||
*/
|
||||
public class LaunchAndSizeBasedTriggerinPolicy<E> extends TriggeringPolicyBase<E> {
|
||||
|
||||
LaunchBasedTriggeringPolicy<E> launchBasedTriggeringPolicy;
|
||||
SizeBasedTriggeringPolicy<E> sizeBasedTriggeringPolicy;
|
||||
|
||||
public LaunchAndSizeBasedTriggerinPolicy(FileSize threshold) {
|
||||
this.launchBasedTriggeringPolicy = new LaunchBasedTriggeringPolicy<>();
|
||||
this.sizeBasedTriggeringPolicy = new SizeBasedTriggeringPolicy<>();
|
||||
sizeBasedTriggeringPolicy.setMaxFileSize(threshold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTriggeringEvent(File activeFile, E event) {
|
||||
return launchBasedTriggeringPolicy.isTriggeringEvent(activeFile, event) || sizeBasedTriggeringPolicy.isTriggeringEvent(activeFile, event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,10 +9,9 @@ import ch.qos.logback.core.Appender;
|
||||
import ch.qos.logback.core.ConsoleAppender;
|
||||
import ch.qos.logback.core.FileAppender;
|
||||
import ch.qos.logback.core.helpers.NOPAppender;
|
||||
import ch.qos.logback.core.hook.DelayingShutdownHook;
|
||||
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
|
||||
import ch.qos.logback.core.rolling.RollingFileAppender;
|
||||
import ch.qos.logback.core.util.Duration;
|
||||
import ch.qos.logback.core.util.FileSize;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.cryptomator.common.Environment;
|
||||
@@ -33,6 +32,8 @@ public class LoggerModule {
|
||||
private static final int LOGFILE_ROLLING_MIN = 1;
|
||||
private static final int LOGFILE_ROLLING_MAX = 9;
|
||||
private static final String LOG_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n";
|
||||
private static final String LOG_MAX_SIZE = "100mb";
|
||||
|
||||
static final Map<String, Level> DEFAULT_LOG_LEVELS = Map.of( //
|
||||
Logger.ROOT_LOGGER_NAME, Level.INFO, //
|
||||
"org.cryptomator", Level.INFO //
|
||||
@@ -84,7 +85,7 @@ public class LoggerModule {
|
||||
appender.setContext(context);
|
||||
appender.setFile(logDir.resolve(LOGFILE_NAME).toString());
|
||||
appender.setEncoder(encoder);
|
||||
LaunchBasedTriggeringPolicy triggeringPolicy = new LaunchBasedTriggeringPolicy();
|
||||
LaunchAndSizeBasedTriggerinPolicy triggeringPolicy = new LaunchAndSizeBasedTriggerinPolicy(FileSize.valueOf(LOG_MAX_SIZE));
|
||||
triggeringPolicy.setContext(context);
|
||||
triggeringPolicy.start();
|
||||
appender.setTriggeringPolicy(triggeringPolicy);
|
||||
|
||||
33
main/pom.xml
33
main/pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.5.2</version>
|
||||
<version>1.5.7</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator</name>
|
||||
|
||||
@@ -24,25 +24,26 @@
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<!-- cryptomator dependencies -->
|
||||
<cryptomator.cryptofs.version>1.9.9</cryptomator.cryptofs.version>
|
||||
<cryptomator.jni.version>2.2.2</cryptomator.jni.version>
|
||||
<cryptomator.cryptofs.version>1.9.12</cryptomator.cryptofs.version>
|
||||
<cryptomator.jni.version>2.2.3</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>
|
||||
<jwt.version>3.10.2</jwt.version>
|
||||
<commons-lang3.version>3.11</commons-lang3.version>
|
||||
<secret-service.version>1.0.0</secret-service.version>
|
||||
<jwt.version>3.10.3</jwt.version>
|
||||
<easybind.version>1.0.3</easybind.version>
|
||||
<guava.version>28.2-jre</guava.version>
|
||||
<dagger.version>2.27</dagger.version>
|
||||
<guava.version>29.0-jre</guava.version>
|
||||
<dagger.version>2.22</dagger.version>
|
||||
<gson.version>2.8.6</gson.version>
|
||||
<slf4j.version>1.7.30</slf4j.version>
|
||||
<logback.version>1.2.3</logback.version>
|
||||
|
||||
<!-- test dependencies -->
|
||||
<junit.jupiter.version>5.6.1</junit.jupiter.version>
|
||||
<junit.jupiter.version>5.6.2</junit.jupiter.version>
|
||||
<mockito.version>3.3.3</mockito.version>
|
||||
<hamcrest.version>2.2</hamcrest.version>
|
||||
</properties>
|
||||
@@ -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,7 +161,14 @@
|
||||
<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>
|
||||
<groupId>com.auth0</groupId>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.5.2</version>
|
||||
<version>1.5.7</version>
|
||||
</parent>
|
||||
<artifactId>ui</artifactId>
|
||||
<name>Cryptomator GUI</name>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.FxApplication;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Optional;
|
||||
|
||||
@AddVaultWizardScoped
|
||||
public class AddVaultSuccessController implements FxController {
|
||||
@@ -27,7 +28,7 @@ public class AddVaultSuccessController implements FxController {
|
||||
@FXML
|
||||
public void unlockAndClose() {
|
||||
close();
|
||||
fxApplication.showUnlockWindow(vault.get());
|
||||
fxApplication.startUnlockWorkflow(vault.get(), Optional.of(window));
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
||||
@@ -18,7 +18,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -57,7 +56,7 @@ public class ChooseExistingVaultController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
final String resource = SystemUtils.IS_OS_MAC ? "/select-masterkey-mac.png" : "/select-masterkey-win.png";
|
||||
final String resource = SystemUtils.IS_OS_MAC ? "/img/select-masterkey-mac.png" : "/img/select-masterkey-win.png";
|
||||
try (InputStream in = getClass().getResourceAsStream(resource)) {
|
||||
this.screenshot = new Image(in);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ResourceBundle;
|
||||
@@ -48,7 +49,6 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
private final StringProperty vaultName;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final BooleanBinding validVaultPath;
|
||||
private final BooleanBinding invalidVaultPath;
|
||||
private final BooleanProperty usePresetPath;
|
||||
private final StringProperty warningText;
|
||||
|
||||
@@ -71,7 +71,6 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
this.vaultName = vaultName;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.validVaultPath = Bindings.createBooleanBinding(this::isValidVaultPath, vaultPath);
|
||||
this.invalidVaultPath = validVaultPath.not();
|
||||
this.usePresetPath = new SimpleBooleanProperty();
|
||||
this.warningText = new SimpleStringProperty();
|
||||
}
|
||||
@@ -125,6 +124,9 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
LOG.warn("Can not use already existing vault path {}", vaultPath.get());
|
||||
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
||||
} catch (NoSuchFileException e) {
|
||||
LOG.warn("At least one path component does not exist of path {}", vaultPath.get());
|
||||
warningText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create and delete directory at chosen vault path.", e);
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
@@ -135,7 +137,11 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
public void chooseCustomVaultPath() {
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle(resourceBundle.getString("addvaultwizard.new.directoryPickerTitle"));
|
||||
directoryChooser.setInitialDirectory(customVaultPath.toFile());
|
||||
if (Files.exists(customVaultPath)) {
|
||||
directoryChooser.setInitialDirectory(customVaultPath.toFile());
|
||||
} else {
|
||||
directoryChooser.setInitialDirectory(DEFAULT_CUSTOM_VAULT_PATH.toFile());
|
||||
}
|
||||
final File file = directoryChooser.showDialog(window);
|
||||
if (file != null) {
|
||||
customVaultPath = file.toPath().toAbsolutePath();
|
||||
@@ -153,12 +159,12 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
return vaultPath;
|
||||
}
|
||||
|
||||
public BooleanBinding invalidVaultPathProperty() {
|
||||
return invalidVaultPath;
|
||||
public BooleanBinding validVaultPathProperty() {
|
||||
return validVaultPath;
|
||||
}
|
||||
|
||||
public Boolean getInvalidVaultPath() {
|
||||
return invalidVaultPath.get();
|
||||
public Boolean getValidVaultPath() {
|
||||
return validVaultPath.get();
|
||||
}
|
||||
|
||||
public LocationPresets getLocationPresets() {
|
||||
|
||||
@@ -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.KeychainAccessException;
|
||||
import org.cryptomator.keychain.KeychainManager;
|
||||
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,24 +36,27 @@ public class ChangePasswordController implements FxController {
|
||||
private final Vault vault;
|
||||
private final ObjectProperty<CharSequence> newPassword;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final Optional<KeychainManager> 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<KeychainManager> keychain) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.newPassword = newPassword;
|
||||
this.errorComponent = errorComponent;
|
||||
this.keychain = keychain;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
BooleanBinding hasNotConfirmedCheckbox = finalConfirmationCheckbox.selectedProperty().not();
|
||||
BooleanBinding isInvalidNewPassword = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword);
|
||||
finishButton.disableProperty().bind(hasNotConfirmedCheckbox.or(isInvalidNewPassword));
|
||||
BooleanBinding checkboxNotConfirmed = finalConfirmationCheckbox.selectedProperty().not();
|
||||
BooleanBinding oldPasswordFieldEmpty = oldPasswordField.textProperty().isEmpty();
|
||||
BooleanBinding newPasswordInvalid = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword);
|
||||
finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordInvalid));
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -69,8 +68,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,10 +81,21 @@ 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() {
|
||||
return vault;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ public class NewPasswordController implements FxController {
|
||||
public Label passwordStrengthLabel;
|
||||
public Label passwordMatchLabel;
|
||||
public FontAwesome5IconView checkmark;
|
||||
public FontAwesome5IconView warning;
|
||||
public FontAwesome5IconView cross;
|
||||
|
||||
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ObjectProperty<CharSequence> password) {
|
||||
@@ -36,11 +37,13 @@ public class NewPasswordController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters()), passwordField.textProperty()));
|
||||
|
||||
passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength));
|
||||
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
|
||||
|
||||
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::hasSamePasswordInBothFields, passwordField.textProperty(), reenterField.textProperty());
|
||||
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
|
||||
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters()), passwordField.textProperty()));
|
||||
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
|
||||
|
||||
passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty);
|
||||
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(checkmark).otherwise(cross));
|
||||
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("newPassword.passwordsMatch")).otherwise(resourceBundle.getString("newPassword.passwordsDoNotMatch")));
|
||||
@@ -49,6 +52,18 @@ public class NewPasswordController implements FxController {
|
||||
reenterField.textProperty().addListener(this::passwordsDidChange);
|
||||
}
|
||||
|
||||
private FontAwesome5IconView getIconViewForPasswordStrengthLabel() {
|
||||
if (passwordField.getCharacters().length() == 0) {
|
||||
return null;
|
||||
} else if (passwordStrength.intValue() <= -1) {
|
||||
return cross;
|
||||
} else if (passwordStrength.intValue() < 3) {
|
||||
return warning;
|
||||
} else {
|
||||
return checkmark;
|
||||
}
|
||||
}
|
||||
|
||||
private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
|
||||
if (hasSamePasswordInBothFields() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
|
||||
password.set(passwordField.getCharacters());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,16 +4,13 @@ import javafx.concurrent.Task;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.keychain.KeychainAccess;
|
||||
import org.cryptomator.keychain.KeychainManager;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@@ -28,10 +25,10 @@ public class VaultService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
|
||||
|
||||
private final ExecutorService executorService;
|
||||
private final Optional<KeychainAccess> keychain;
|
||||
private final Optional<KeychainManager> keychain;
|
||||
|
||||
@Inject
|
||||
public VaultService(ExecutorService executorService, Optional<KeychainAccess> keychain) {
|
||||
public VaultService(ExecutorService executorService, Optional<KeychainManager> keychain) {
|
||||
this.executorService = executorService;
|
||||
this.keychain = keychain;
|
||||
}
|
||||
@@ -52,62 +49,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 +150,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
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.value.ObservableObjectValue;
|
||||
|
||||
|
||||
/**
|
||||
* Contains a variety of method to create {@link java.util.function.Function#identity() identity}-bindings
|
||||
* to facilitate the Weak References used internally in JavaFX's Bindings.
|
||||
*/
|
||||
public final class WeakBindings {
|
||||
|
||||
/**
|
||||
* Create a new StringBinding that listens to changes from the given observable without being strongly referenced by it.
|
||||
*
|
||||
* @param observable The observable
|
||||
* @return a StringBinding weakly referenced from the given observable
|
||||
*/
|
||||
public static StringBinding bindString(ObservableObjectValue<String> observable) {
|
||||
return new StringBinding() {
|
||||
{
|
||||
bind(observable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String computeValue() {
|
||||
return observable.get();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,6 +11,7 @@ public enum FontAwesome5Icon {
|
||||
COGS("\uF085"), //
|
||||
COPY("\uF0C5"), //
|
||||
CROWN("\uF521"), //
|
||||
EDIT("\uF044"), //
|
||||
EXCLAMATION("\uF12A"), //
|
||||
EXCLAMATION_CIRCLE("\uF06A"), //
|
||||
EXCLAMATION_TRIANGLE("\uF071"), //
|
||||
@@ -32,10 +33,13 @@ public enum FontAwesome5Icon {
|
||||
PLUS("\uF067"), //
|
||||
PRINT("\uF02F"), //
|
||||
QUESTION("\uF128"), //
|
||||
REDO("\uF01E"), //
|
||||
SEARCH("\uF002"), //
|
||||
SPINNER("\uF110"), //
|
||||
SYNC("\uF021"), //
|
||||
TIMES("\uF00D"), //
|
||||
TRASH("\uF1F8"), //
|
||||
UNLINK("\uf127"), //
|
||||
WRENCH("\uF0AD"), //
|
||||
WINDOW_MINIMIZE("\uF2D1"), //
|
||||
;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -4,8 +4,8 @@ import javafx.beans.property.BooleanProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.keychain.KeychainAccess;
|
||||
import org.cryptomator.keychain.KeychainAccessException;
|
||||
import org.cryptomator.keychain.KeychainManager;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -20,14 +20,14 @@ public class ForgetPasswordController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final Optional<KeychainAccess> keychainAccess;
|
||||
private final Optional<KeychainManager> keychain;
|
||||
private final BooleanProperty confirmedResult;
|
||||
|
||||
@Inject
|
||||
public ForgetPasswordController(@ForgetPasswordWindow Stage window, @ForgetPasswordWindow Vault vault, Optional<KeychainAccess> keychainAccess, @ForgetPasswordWindow BooleanProperty confirmedResult) {
|
||||
public ForgetPasswordController(@ForgetPasswordWindow Stage window, @ForgetPasswordWindow Vault vault, Optional<KeychainManager> keychain, @ForgetPasswordWindow BooleanProperty confirmedResult) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.keychainAccess = keychainAccess;
|
||||
this.keychain = keychain;
|
||||
this.confirmedResult = confirmedResult;
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ public class ForgetPasswordController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void finish() {
|
||||
if (keychainAccess.isPresent()) {
|
||||
if (keychain.isPresent()) {
|
||||
try {
|
||||
keychainAccess.get().deletePassphrase(vault.getId());
|
||||
keychain.get().deletePassphrase(vault.getId());
|
||||
LOG.debug("Forgot password for vault {}.", vault.getDisplayableName());
|
||||
confirmedResult.setValue(true);
|
||||
} catch (KeychainAccessException e) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.stage.Stage;
|
||||
import org.cryptomator.common.LicenseHolder;
|
||||
@@ -27,6 +26,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 +38,26 @@ 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;
|
||||
|
||||
private Optional<String> macApperanceObserverIdentifier = Optional.empty();
|
||||
|
||||
@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 +75,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 +85,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, Optional<Stage> owner) {
|
||||
Platform.runLater(() -> {
|
||||
Stage stage = unlockWindowBuilder.vault(vault).build().showUnlockWindow();
|
||||
addVisibleStage(stage);
|
||||
unlockWindowBuilderProvider.get().vault(vault).owner(owner).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");
|
||||
});
|
||||
}
|
||||
@@ -123,22 +116,44 @@ public class FxApplication extends Application {
|
||||
}
|
||||
|
||||
private void themeChanged(@SuppressWarnings("unused") ObservableValue<? extends UiTheme> observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) {
|
||||
if (macApperanceObserverIdentifier.isPresent()) {
|
||||
macFunctions.map(MacFunctions::uiAppearance).ifPresent(uiAppearance -> uiAppearance.removeListener(macApperanceObserverIdentifier.get()));
|
||||
macApperanceObserverIdentifier = Optional.empty();
|
||||
}
|
||||
loadSelectedStyleSheet(newValue);
|
||||
}
|
||||
|
||||
private void loadSelectedStyleSheet(UiTheme desiredTheme) {
|
||||
UiTheme theme = licenseHolder.isValidLicense() ? desiredTheme : UiTheme.LIGHT;
|
||||
switch (theme) {
|
||||
case DARK:
|
||||
Application.setUserAgentStylesheet(getClass().getResource("/css/dark_theme.css").toString());
|
||||
macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToDarkAqua));
|
||||
break;
|
||||
case LIGHT:
|
||||
default:
|
||||
Application.setUserAgentStylesheet(getClass().getResource("/css/light_theme.css").toString());
|
||||
macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToAqua));
|
||||
break;
|
||||
case LIGHT -> setToLightTheme();
|
||||
case DARK -> setToDarkTheme();
|
||||
case AUTOMATIC -> {
|
||||
macFunctions.map(MacFunctions::uiAppearance).ifPresent(uiAppearance -> {
|
||||
macApperanceObserverIdentifier = Optional.of(uiAppearance.addListener(this::macInterfaceThemeChanged));
|
||||
});
|
||||
macInterfaceThemeChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void macInterfaceThemeChanged() {
|
||||
macFunctions.map(MacFunctions::uiAppearance).ifPresent(uiAppearance -> {
|
||||
switch (uiAppearance.getCurrentInterfaceStyle()) {
|
||||
case LIGHT -> setToLightTheme();
|
||||
case DARK -> setToDarkTheme();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setToLightTheme() {
|
||||
Application.setUserAgentStylesheet(getClass().getResource("/css/light_theme.css").toString());
|
||||
macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToAqua));
|
||||
}
|
||||
|
||||
private void setToDarkTheme() {
|
||||
Application.setUserAgentStylesheet(getClass().getResource("/css/dark_theme.css").toString());
|
||||
macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToDarkAqua));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@ import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import javafx.application.Application;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.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 +33,8 @@ abstract class FxApplicationModule {
|
||||
|
||||
@Provides
|
||||
@FxApplicationScoped
|
||||
static ObjectProperty<Vault> provideSelectedVault() {
|
||||
return new SimpleObjectProperty<>();
|
||||
static ObservableSet<Stage> provideVisibleStages() {
|
||||
return FXCollections.observableSet();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@@ -43,16 +44,30 @@ abstract class FxApplicationModule {
|
||||
if (SystemUtils.IS_OS_MAC) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try {
|
||||
return List.of( //
|
||||
createImageFromResource("/window_icon_32.png"), //
|
||||
createImageFromResource("/window_icon_512.png") //
|
||||
createImageFromResource("/img/window_icon_32.png"), //
|
||||
createImageFromResource("/img/window_icon_512.png") //
|
||||
);
|
||||
} catch (IOException e) {
|
||||
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)) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import javax.inject.Singleton;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Singleton
|
||||
public class FxApplicationStarter {
|
||||
@@ -19,17 +20,19 @@ public class FxApplicationStarter {
|
||||
|
||||
private final FxApplicationComponent.Builder fxAppComponent;
|
||||
private final ExecutorService executor;
|
||||
private final AtomicBoolean started;
|
||||
private final CompletableFuture<FxApplication> future;
|
||||
|
||||
@Inject
|
||||
public FxApplicationStarter(FxApplicationComponent.Builder fxAppComponent, ExecutorService executor) {
|
||||
this.fxAppComponent = fxAppComponent;
|
||||
this.executor = executor;
|
||||
this.started = new AtomicBoolean();
|
||||
this.future = new CompletableFuture<>();
|
||||
}
|
||||
|
||||
public synchronized CompletionStage<FxApplication> get(boolean hasTrayIcon) {
|
||||
if (!future.isDone()) {
|
||||
public CompletionStage<FxApplication> get(boolean hasTrayIcon) {
|
||||
if (!started.getAndSet(true)) {
|
||||
start(hasTrayIcon);
|
||||
}
|
||||
return future;
|
||||
|
||||
@@ -62,14 +62,22 @@ public class UiLauncher {
|
||||
Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(hasTrayIcon));
|
||||
|
||||
// auto unlock
|
||||
Collection<Vault> vaultsWithAutoUnlockEnabled = vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get());
|
||||
if (!vaultsWithAutoUnlockEnabled.isEmpty()) {
|
||||
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> app.getVaultService().attemptAutoUnlock(vaultsWithAutoUnlockEnabled));
|
||||
Collection<Vault> vaultsToAutoUnlock = vaults.filtered(this::shouldAttemptAutoUnlock);
|
||||
if (!vaultsToAutoUnlock.isEmpty()) {
|
||||
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> {
|
||||
for (Vault vault : vaultsToAutoUnlock) {
|
||||
app.startUnlockWorkflow(vault, Optional.empty());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);
|
||||
}
|
||||
|
||||
private boolean shouldAttemptAutoUnlock(Vault vault) {
|
||||
return vault.isLocked() && vault.getVaultSettings().unlockAfterStartup().get();
|
||||
}
|
||||
|
||||
private void showMainWindowAsync(boolean hasTrayIcon) {
|
||||
fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
package org.cryptomator.ui.mainwindow;
|
||||
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.keychain.KeychainManager;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.FxApplication;
|
||||
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Optional;
|
||||
|
||||
@MainWindowScoped
|
||||
public class VaultDetailLockedController implements FxController {
|
||||
@@ -16,24 +22,34 @@ public class VaultDetailLockedController implements FxController {
|
||||
private final ReadOnlyObjectProperty<Vault> vault;
|
||||
private final FxApplication application;
|
||||
private final VaultOptionsComponent.Builder vaultOptionsWindow;
|
||||
private final Optional<KeychainManager> keychainManagerOptional;
|
||||
private final Stage mainWindow;
|
||||
private final BooleanExpression passwordSaved;
|
||||
|
||||
@Inject
|
||||
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow) {
|
||||
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, Optional<KeychainManager> keychainManagerOptional, @MainWindow Stage mainWindow) {
|
||||
this.vault = vault;
|
||||
this.application = application;
|
||||
this.vaultOptionsWindow = vaultOptionsWindow;
|
||||
this.keychainManagerOptional = keychainManagerOptional;
|
||||
this.mainWindow = mainWindow;
|
||||
if (keychainManagerOptional.isPresent()) {
|
||||
this.passwordSaved = BooleanExpression.booleanExpression(EasyBind.select(vault).selectObject(v -> keychainManagerOptional.get().getPassphraseStoredProperty(v.getId())));
|
||||
} else {
|
||||
this.passwordSaved = new SimpleBooleanProperty(false);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void unlock() {
|
||||
application.showUnlockWindow(vault.get());
|
||||
application.startUnlockWorkflow(vault.get(), Optional.of(mainWindow));
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void showVaultOptions() {
|
||||
vaultOptionsWindow.vault(vault.get()).build().showVaultOptionsWindow();
|
||||
}
|
||||
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public ReadOnlyObjectProperty<Vault> vaultProperty() {
|
||||
@@ -43,4 +59,14 @@ public class VaultDetailLockedController implements FxController {
|
||||
public Vault getVault() {
|
||||
return vault.get();
|
||||
}
|
||||
|
||||
public BooleanExpression passwordSavedProperty() {
|
||||
return passwordSaved;
|
||||
}
|
||||
|
||||
public boolean isPasswordSaved() {
|
||||
if (keychainManagerOptional.isPresent() && vault.get() != null) {
|
||||
return keychainManagerOptional.get().getPassphraseStoredProperty(vault.get().getId()).get();
|
||||
} else return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,55 @@
|
||||
package org.cryptomator.ui.mainwindow;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.removevault.RemoveVaultComponent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@MainWindowScoped
|
||||
public class VaultDetailMissingVaultController implements FxController {
|
||||
|
||||
private final ReadOnlyObjectProperty<Vault> vault;
|
||||
private final ObjectProperty<Vault> vault;
|
||||
private final RemoveVaultComponent.Builder removeVault;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final Stage window;
|
||||
|
||||
|
||||
@Inject
|
||||
public VaultDetailMissingVaultController(ObjectProperty<Vault> vault) {
|
||||
public VaultDetailMissingVaultController(ObjectProperty<Vault> vault, RemoveVaultComponent.Builder removeVault, ResourceBundle resourceBundle, @MainWindow Stage window) {
|
||||
this.vault = vault;
|
||||
this.removeVault = removeVault;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.window = window;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void recheck() {
|
||||
VaultListManager.redetermineVaultState(vault.get());
|
||||
}
|
||||
|
||||
@FXML
|
||||
void didClickRemoveVault() {
|
||||
removeVault.vault(vault.get()).build().showRemoveVault();
|
||||
}
|
||||
|
||||
@FXML
|
||||
void changeLocation() {
|
||||
// copied from ChooseExistingVaultController class
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(resourceBundle.getString("addvaultwizard.existing.filePickerTitle"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
|
||||
File masterkeyFile = fileChooser.showOpenDialog(window);
|
||||
if (masterkeyFile != null) {
|
||||
vault.get().getVaultSettings().path().setValue(masterkeyFile.toPath().toAbsolutePath().getParent());
|
||||
recheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -42,4 +42,5 @@ public class WelcomeController implements FxController {
|
||||
public boolean isNoVaultPresent() {
|
||||
return noVaultPresent.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ import org.cryptomator.cryptofs.migration.Migrators;
|
||||
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
|
||||
import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.keychain.KeychainAccess;
|
||||
import org.cryptomator.keychain.KeychainAccessException;
|
||||
import org.cryptomator.keychain.KeychainManager;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
@@ -55,7 +55,7 @@ public class MigrationRunController implements FxController {
|
||||
private final Vault vault;
|
||||
private final ExecutorService executor;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final Optional<KeychainAccess> keychainAccess;
|
||||
private final Optional<KeychainManager> keychain;
|
||||
private final ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final Lazy<Scene> startScene;
|
||||
@@ -69,13 +69,13 @@ public class MigrationRunController implements FxController {
|
||||
public NiceSecurePasswordField passwordField;
|
||||
|
||||
@Inject
|
||||
public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, Optional<KeychainAccess> keychainAccess, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy<Scene> capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) Lazy<Scene> impossibleScene, ErrorComponent.Builder errorComponent) {
|
||||
public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, Optional<KeychainManager> keychain, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy<Scene> capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) Lazy<Scene> impossibleScene, ErrorComponent.Builder errorComponent) {
|
||||
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.executor = executor;
|
||||
this.scheduler = scheduler;
|
||||
this.keychainAccess = keychainAccess;
|
||||
this.keychain = keychain;
|
||||
this.missingCapability = missingCapability;
|
||||
this.errorComponent = errorComponent;
|
||||
this.startScene = startScene;
|
||||
@@ -88,7 +88,7 @@ public class MigrationRunController implements FxController {
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
if (keychainAccess.isPresent()) {
|
||||
if (keychain.isPresent()) {
|
||||
loadStoredPassword();
|
||||
}
|
||||
migrationButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.NEEDS_MIGRATION).or(passwordField.textProperty().isEmpty()));
|
||||
@@ -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 -> {
|
||||
@@ -167,10 +167,10 @@ public class MigrationRunController implements FxController {
|
||||
}
|
||||
|
||||
private void loadStoredPassword() {
|
||||
assert keychainAccess.isPresent();
|
||||
assert keychain.isPresent();
|
||||
char[] storedPw = null;
|
||||
try {
|
||||
storedPw = keychainAccess.get().loadPassphrase(vault.getId());
|
||||
storedPw = keychain.get().loadPassphrase(vault.getId());
|
||||
if (storedPw != null) {
|
||||
passwordField.setPassword(storedPw);
|
||||
passwordField.selectRange(storedPw.length, storedPw.length);
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
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;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.FxApplication;
|
||||
import org.cryptomator.ui.mainwindow.MainWindow;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Optional;
|
||||
|
||||
@MigrationScoped
|
||||
public class MigrationSuccessController implements FxController {
|
||||
@@ -17,18 +16,20 @@ public class MigrationSuccessController implements FxController {
|
||||
private final FxApplication fxApplication;
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final Stage mainWindow;
|
||||
|
||||
@Inject
|
||||
MigrationSuccessController(FxApplication fxApplication, @MigrationWindow Stage window, @MigrationWindow Vault vault) {
|
||||
MigrationSuccessController(FxApplication fxApplication, @MigrationWindow Stage window, @MigrationWindow Vault vault, @MainWindow Stage mainWindow) {
|
||||
this.fxApplication = fxApplication;
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void unlockAndClose() {
|
||||
close();
|
||||
fxApplication.showUnlockWindow(vault);
|
||||
fxApplication.startUnlockWorkflow(vault, Optional.of(mainWindow));
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
||||
@@ -5,23 +5,27 @@ import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.TextArea;
|
||||
import org.cryptomator.common.LicenseHolder;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.UiTheme;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@PreferencesScoped
|
||||
public class DonationKeyPreferencesController implements FxController {
|
||||
|
||||
|
||||
private static final String DONATION_URI = "https://store.cryptomator.org/desktop";
|
||||
|
||||
private final Application application;
|
||||
private final LicenseHolder licenseHolder;
|
||||
private final Settings settings;
|
||||
public TextArea donationKeyField;
|
||||
|
||||
@Inject
|
||||
DonationKeyPreferencesController(Application application, LicenseHolder licenseHolder) {
|
||||
DonationKeyPreferencesController(Application application, LicenseHolder licenseHolder, Settings settings) {
|
||||
this.application = application;
|
||||
this.licenseHolder = licenseHolder;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -32,6 +36,9 @@ public class DonationKeyPreferencesController implements FxController {
|
||||
|
||||
private void registrationKeyChanged(@SuppressWarnings("unused") ObservableValue<? extends String> observable, @SuppressWarnings("unused") String oldValue, String newValue) {
|
||||
licenseHolder.validateAndStoreLicense(newValue);
|
||||
if (!licenseHolder.isValidLicense()) {
|
||||
settings.theme().set(UiTheme.LIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.ui.preferences;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@@ -12,6 +13,7 @@ import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.util.StringConverter;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.LicenseHolder;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.UiTheme;
|
||||
@@ -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,26 @@ 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.getItems().addAll(UiTheme.applicableValues());
|
||||
if (!themeChoiceBox.getItems().contains(settings.theme().get())) {
|
||||
settings.theme().set(UiTheme.LIGHT);
|
||||
}
|
||||
themeChoiceBox.valueProperty().bindBidirectional(settings.theme());
|
||||
themeChoiceBox.setConverter(new UiThemeConverter());
|
||||
themeChoiceBox.setConverter(new UiThemeConverter(resourceBundle));
|
||||
|
||||
startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden());
|
||||
|
||||
@@ -112,13 +124,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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ public class VolumePreferencesController implements FxController {
|
||||
private final Settings settings;
|
||||
private final BooleanBinding showWebDavSettings;
|
||||
private final BooleanBinding showWebDavScheme;
|
||||
public ChoiceBox<VolumeImpl> volumeTypeChoicBox;
|
||||
public ChoiceBox<VolumeImpl> volumeTypeChoiceBox;
|
||||
public TextField webDavPortField;
|
||||
public Button changeWebDavPortButton;
|
||||
public ChoiceBox<WebDavUrlScheme> webDavUrlSchemeChoiceBox;
|
||||
@@ -38,9 +38,12 @@ public class VolumePreferencesController implements FxController {
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
volumeTypeChoicBox.getItems().addAll(Volume.getCurrentSupportedAdapters());
|
||||
volumeTypeChoicBox.valueProperty().bindBidirectional(settings.preferredVolumeImpl());
|
||||
volumeTypeChoicBox.setConverter(new VolumeImplConverter());
|
||||
volumeTypeChoiceBox.getItems().addAll(Volume.getCurrentSupportedAdapters());
|
||||
if (!volumeTypeChoiceBox.getItems().contains(settings.preferredVolumeImpl().get())) {
|
||||
settings.preferredVolumeImpl().set(VolumeImpl.WEBDAV);
|
||||
}
|
||||
volumeTypeChoiceBox.valueProperty().bindBidirectional(settings.preferredVolumeImpl());
|
||||
volumeTypeChoiceBox.setConverter(new VolumeImplConverter());
|
||||
|
||||
webDavPortField.setText(String.valueOf(settings.port().get()));
|
||||
changeWebDavPortButton.visibleProperty().bind(settings.port().asString().isNotEqualTo(webDavPortField.textProperty()));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ public class TrayIconController {
|
||||
trayMenuController.initTrayMenu();
|
||||
}
|
||||
|
||||
public void macInterfaceThemeChanged() {
|
||||
private void macInterfaceThemeChanged() {
|
||||
trayIcon.setImage(imageFactory.loadImage());
|
||||
}
|
||||
|
||||
|
||||
@@ -30,13 +30,13 @@ class TrayImageFactory {
|
||||
.map(MacApplicationUiAppearance::getCurrentInterfaceStyle) //
|
||||
.orElse(MacApplicationUiInterfaceStyle.LIGHT);
|
||||
return switch (interfaceStyle) {
|
||||
case DARK -> "/tray_icon_mac_white.png";
|
||||
case LIGHT -> "/tray_icon_mac_black.png";
|
||||
case DARK -> "/img/tray_icon_mac_white.png";
|
||||
case LIGHT -> "/img/tray_icon_mac_black.png";
|
||||
};
|
||||
}
|
||||
|
||||
private String getWinOrLinuxResourceName() {
|
||||
return "/tray_icon.png";
|
||||
return "/img/tray_icon.png";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import java.awt.PopupMenu;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.EventObject;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -103,7 +104,7 @@ class TrayMenuController {
|
||||
}
|
||||
|
||||
private void unlockVault(Vault vault) {
|
||||
fxApplicationStarter.get(true).thenAccept(app -> app.showUnlockWindow(vault));
|
||||
fxApplicationStarter.get(true).thenAccept(app -> app.startUnlockWorkflow(vault, Optional.empty()));
|
||||
}
|
||||
|
||||
private void lockVault(Vault vault) {
|
||||
|
||||
@@ -6,37 +6,38 @@
|
||||
package org.cryptomator.ui.unlock;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Lazy;
|
||||
import dagger.Subcomponent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
|
||||
import javax.inject.Named;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
@UnlockScoped
|
||||
@Subcomponent(modules = {UnlockModule.class})
|
||||
public interface UnlockComponent {
|
||||
|
||||
@UnlockWindow
|
||||
Stage window();
|
||||
ExecutorService defaultExecutorService();
|
||||
|
||||
@FxmlScene(FxmlFile.UNLOCK)
|
||||
Lazy<Scene> scene();
|
||||
UnlockWorkflow unlockWorkflow();
|
||||
|
||||
default Stage showUnlockWindow() {
|
||||
Stage stage = window();
|
||||
stage.setScene(scene().get());
|
||||
stage.show();
|
||||
return stage;
|
||||
default Future<Boolean> startUnlockWorkflow() {
|
||||
UnlockWorkflow workflow = unlockWorkflow();
|
||||
defaultExecutorService().submit(workflow);
|
||||
return workflow;
|
||||
}
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
||||
|
||||
@BindsInstance
|
||||
Builder vault(@UnlockWindow Vault vault);
|
||||
|
||||
@BindsInstance
|
||||
Builder owner(@Named("unlockWindowOwner") Optional<Stage> owner);
|
||||
|
||||
UnlockComponent build();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
package org.cryptomator.ui.unlock;
|
||||
|
||||
import dagger.Lazy;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
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.scene.image.ImageView;
|
||||
import javafx.scene.transform.Rotate;
|
||||
import javafx.scene.transform.Translate;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
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.keychain.KeychainManager;
|
||||
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.common.WeakBindings;
|
||||
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,142 +44,165 @@ 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<KeychainManager> keychain;
|
||||
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
|
||||
private final BooleanBinding userInteractionDisabled;
|
||||
private final BooleanProperty unlockButtonDisabled;
|
||||
private final StringBinding vaultName;
|
||||
|
||||
public NiceSecurePasswordField passwordField;
|
||||
public CheckBox savePassword;
|
||||
public CheckBox savePasswordCheckbox;
|
||||
public ImageView face;
|
||||
public ImageView leftArm;
|
||||
public ImageView rightArm;
|
||||
public ImageView legs;
|
||||
public ImageView body;
|
||||
public Animation unlockAnimation;
|
||||
|
||||
@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<KeychainManager> keychain) {
|
||||
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.keychain = keychain;
|
||||
this.unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, passwordEntryLock.awaitingInteraction());
|
||||
this.userInteractionDisabled = passwordEntryLock.awaitingInteraction().not();
|
||||
this.unlockButtonDisabled = new SimpleBooleanProperty();
|
||||
this.vaultName = WeakBindings.bindString(vault.displayableNameProperty());
|
||||
this.window.setOnCloseRequest(windowEvent -> cancel());
|
||||
}
|
||||
|
||||
@FXML
|
||||
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()));
|
||||
|
||||
var leftArmTranslation = new Translate(24, 0);
|
||||
var leftArmRotation = new Rotate(60, 16, 30, 0);
|
||||
var leftArmRetracted = new KeyValue(leftArmTranslation.xProperty(), 24);
|
||||
var leftArmExtended = new KeyValue(leftArmTranslation.xProperty(), 0.0);
|
||||
var leftArmHorizontal = new KeyValue(leftArmRotation.angleProperty(), 60, Interpolator.EASE_OUT);
|
||||
var leftArmHanging = new KeyValue(leftArmRotation.angleProperty(), 0);
|
||||
leftArm.getTransforms().setAll(leftArmTranslation, leftArmRotation);
|
||||
|
||||
var rightArmTranslation = new Translate(-24, 0);
|
||||
var rightArmRotation = new Rotate(60, 48, 30, 0);
|
||||
var rightArmRetracted = new KeyValue(rightArmTranslation.xProperty(), -24);
|
||||
var rightArmExtended = new KeyValue(rightArmTranslation.xProperty(), 0.0);
|
||||
var rightArmHorizontal = new KeyValue(rightArmRotation.angleProperty(), -60);
|
||||
var rightArmHanging = new KeyValue(rightArmRotation.angleProperty(), 0, Interpolator.EASE_OUT);
|
||||
rightArm.getTransforms().setAll(rightArmTranslation, rightArmRotation);
|
||||
|
||||
var legsRetractedY = new KeyValue(legs.scaleYProperty(), 0);
|
||||
var legsExtendedY = new KeyValue(legs.scaleYProperty(), 1, Interpolator.EASE_OUT);
|
||||
var legsRetractedX = new KeyValue(legs.scaleXProperty(), 0);
|
||||
var legsExtendedX = new KeyValue(legs.scaleXProperty(), 1, Interpolator.EASE_OUT);
|
||||
legs.setScaleY(0);
|
||||
legs.setScaleX(0);
|
||||
|
||||
var faceHidden = new KeyValue(face.opacityProperty(), 0.0);
|
||||
var faceVisible = new KeyValue(face.opacityProperty(), 1.0, Interpolator.LINEAR);
|
||||
face.setOpacity(0);
|
||||
|
||||
unlockAnimation = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, leftArmRetracted, leftArmHorizontal, rightArmRetracted, rightArmHorizontal, legsRetractedY, legsRetractedX, faceHidden),
|
||||
new KeyFrame(Duration.millis(200), leftArmExtended, leftArmHorizontal, rightArmRetracted, rightArmHorizontal),
|
||||
new KeyFrame(Duration.millis(400), leftArmExtended, leftArmHanging, rightArmExtended, rightArmHorizontal),
|
||||
new KeyFrame(Duration.millis(600), leftArmExtended, leftArmHanging, rightArmExtended, rightArmHanging),
|
||||
new KeyFrame(Duration.millis(800), legsExtendedY, legsExtendedX, faceHidden),
|
||||
new KeyFrame(Duration.millis(1000), faceVisible)
|
||||
);
|
||||
|
||||
passwordEntryLock.awaitingInteraction().addListener(observable -> stopUnlockAnimation());
|
||||
}
|
||||
|
||||
|
||||
@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();
|
||||
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);
|
||||
startUnlockAnimation();
|
||||
}
|
||||
|
||||
private void startUnlockAnimation() {
|
||||
leftArm.setVisible(true);
|
||||
rightArm.setVisible(true);
|
||||
legs.setVisible(true);
|
||||
face.setVisible(true);
|
||||
unlockAnimation.playFromStart();
|
||||
}
|
||||
|
||||
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);
|
||||
private void stopUnlockAnimation() {
|
||||
unlockAnimation.stop();
|
||||
leftArm.setVisible(false);
|
||||
rightArm.setVisible(false);
|
||||
legs.setVisible(false);
|
||||
face.setVisible(false);
|
||||
}
|
||||
|
||||
/* 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));
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public Vault getVault() {
|
||||
return vault;
|
||||
public String getVaultName() {
|
||||
return vaultName.get();
|
||||
}
|
||||
|
||||
public StringBinding vaultNameProperty() {
|
||||
return vaultName;
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -189,6 +214,6 @@ public class UnlockController implements FxController {
|
||||
}
|
||||
|
||||
public boolean isKeychainAccessAvailable() {
|
||||
return keychainAccess.isPresent();
|
||||
return keychain.isPresent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,27 +5,71 @@ import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.keychain.KeychainAccessException;
|
||||
import org.cryptomator.keychain.KeychainManager;
|
||||
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.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;
|
||||
|
||||
@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<KeychainManager> keychain, @UnlockWindow Vault vault) {
|
||||
return keychain.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 +80,16 @@ 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, @Named("unlockWindowOwner") Optional<Stage> owner) {
|
||||
Stage stage = factory.create();
|
||||
stage.setTitle(vault.getDisplayableName());
|
||||
stage.setResizable(false);
|
||||
stage.initModality(Modality.APPLICATION_MODAL);
|
||||
stage.getIcons().addAll(windowIcons);
|
||||
if (owner.isPresent()) {
|
||||
stage.initOwner(owner.get());
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
} else {
|
||||
stage.initModality(Modality.APPLICATION_MODAL);
|
||||
}
|
||||
return stage;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -30,6 +32,8 @@ public class UnlockSuccessController implements FxController {
|
||||
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) {
|
||||
this.window = window;
|
||||
@@ -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 */
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
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 javafx.stage.Window;
|
||||
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.KeychainAccessException;
|
||||
import org.cryptomator.keychain.KeychainManager;
|
||||
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<KeychainManager> 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<KeychainManager> 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();
|
||||
Window owner = window.getOwner();
|
||||
if (owner != null) {
|
||||
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
|
||||
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
|
||||
} else {
|
||||
window.centerOnScreen();
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package org.cryptomator.ui.vaultoptions;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.keychain.KeychainAccessException;
|
||||
import org.cryptomator.keychain.KeychainManager;
|
||||
import org.cryptomator.ui.changepassword.ChangePasswordComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Optional;
|
||||
|
||||
@VaultOptionsScoped
|
||||
public class MasterkeyOptionsController implements FxController {
|
||||
@@ -16,13 +22,20 @@ public class MasterkeyOptionsController implements FxController {
|
||||
private final Stage window;
|
||||
private final ChangePasswordComponent.Builder changePasswordWindow;
|
||||
private final RecoveryKeyComponent.Builder recoveryKeyWindow;
|
||||
private final Optional<KeychainManager> keychainManagerOptional;
|
||||
private final BooleanExpression passwordSaved;
|
||||
|
||||
|
||||
@Inject
|
||||
MasterkeyOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Builder recoveryKeyWindow) {
|
||||
MasterkeyOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Builder recoveryKeyWindow, Optional<KeychainManager> keychainManagerOptional) {
|
||||
this.vault = vault;
|
||||
this.window = window;
|
||||
this.changePasswordWindow = changePasswordWindow;
|
||||
this.recoveryKeyWindow = recoveryKeyWindow;
|
||||
this.keychainManagerOptional = keychainManagerOptional;
|
||||
if (keychainManagerOptional.isPresent()) {
|
||||
this.passwordSaved = Bindings.createBooleanBinding(this::isPasswordSaved, keychainManagerOptional.get().getPassphraseStoredProperty(vault.getId()));
|
||||
} else this.passwordSaved = new SimpleBooleanProperty(false);
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -39,4 +52,20 @@ public class MasterkeyOptionsController implements FxController {
|
||||
public void showRecoverVaultDialogue() {
|
||||
recoveryKeyWindow.vault(vault).owner(window).build().showRecoveryKeyRecoverWindow();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void removePasswordFromKeychain() throws KeychainAccessException {
|
||||
keychainManagerOptional.get().deletePassphrase(vault.getId());
|
||||
window.close();
|
||||
}
|
||||
|
||||
public BooleanExpression passwordSavedProperty() {
|
||||
return passwordSaved;
|
||||
}
|
||||
|
||||
public boolean isPasswordSaved() {
|
||||
if (keychainManagerOptional.isPresent() && vault != null) {
|
||||
return keychainManagerOptional.get().getPassphraseStoredProperty(vault.getId()).get();
|
||||
} else return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public class WrongFileAlertController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
final String resource = SystemUtils.IS_OS_MAC ? "/vault-volume-mac.png" : "/vault-volume-win.png";
|
||||
final String resource = SystemUtils.IS_OS_MAC ? "/img/vault-volume-mac.png" : "/img/vault-volume-win.png";
|
||||
try (InputStream in = getClass().getResourceAsStream(resource)) {
|
||||
this.screenshot = new Image(in);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,10 @@
|
||||
-fx-fill: RED_5;
|
||||
}
|
||||
|
||||
.glyph-icon-orange {
|
||||
-fx-fill: ORANGE_5;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Main Window *
|
||||
@@ -631,6 +635,10 @@
|
||||
-fx-background-color: TEXT_FILL;
|
||||
}
|
||||
|
||||
.check-box:selected:disabled > .box > .mark {
|
||||
-fx-background-color: TEXT_FILL_MUTED;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* RadioButton *
|
||||
|
||||
@@ -143,6 +143,10 @@
|
||||
-fx-fill: RED_5;
|
||||
}
|
||||
|
||||
.glyph-icon-orange {
|
||||
-fx-fill: ORANGE_5;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Main Window *
|
||||
@@ -630,6 +634,10 @@
|
||||
-fx-background-color: TEXT_FILL;
|
||||
}
|
||||
|
||||
.check-box:selected:disabled > .box > .mark {
|
||||
-fx-background-color: TEXT_FILL_MUTED;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* RadioButton *
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="B+X">
|
||||
<buttons>
|
||||
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" onAction="#back"/>
|
||||
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" onAction="#next" defaultButton="true" disable="${controller.invalidVaultPath}"/>
|
||||
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" onAction="#next" defaultButton="true" disable="${!controller.validVaultPath}"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</children>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
|
||||
<ImageView VBox.vgrow="ALWAYS" fitHeight="128" preserveRatio="true" smooth="true" cache="true">
|
||||
<Image url="/bot.png"/>
|
||||
<Image url="/img/bot/bot.png"/>
|
||||
</ImageView>
|
||||
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
|
||||
@@ -13,18 +13,19 @@
|
||||
alignment="CENTER_LEFT">
|
||||
<fx:define>
|
||||
<FontAwesome5IconView fx:id="checkmark" styleClass="glyph-icon-primary" glyph="CHECK"/>
|
||||
<FontAwesome5IconView fx:id="warning" styleClass="glyph-icon-orange" glyph="EXCLAMATION_TRIANGLE"/>
|
||||
<FontAwesome5IconView fx:id="cross" styleClass="glyph-icon-red" glyph="TIMES"/>
|
||||
</fx:define>
|
||||
<children>
|
||||
<Label text="%newPassword.promptText" labelFor="$passwordField"/>
|
||||
<NiceSecurePasswordField fx:id="passwordField"/>
|
||||
<PasswordStrengthIndicator spacing="6" prefHeight="6" strength="${controller.passwordStrength}"/>
|
||||
<Label fx:id="passwordStrengthLabel" styleClass="label-muted" alignment="CENTER_RIGHT" maxWidth="Infinity"/>
|
||||
|
||||
<Label fx:id="passwordStrengthLabel" styleClass="label-muted" alignment="CENTER_RIGHT" maxWidth="Infinity" graphicTextGap="6"/>
|
||||
|
||||
<Region/>
|
||||
|
||||
<Label text="%newPassword.reenterPassword" labelFor="$reenterField"/>
|
||||
<NiceSecurePasswordField fx:id="reenterField"/>
|
||||
<Label fx:id="passwordMatchLabel" styleClass="label-muted" alignment="CENTER_RIGHT" maxWidth="Infinity" graphicTextGap="6" contentDisplay="LEFT" />
|
||||
<Label fx:id="passwordMatchLabel" styleClass="label-muted" alignment="CENTER_RIGHT" maxWidth="Infinity" graphicTextGap="6"/>
|
||||
</children>
|
||||
</VBox>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<children>
|
||||
<HBox spacing="12" VBox.vgrow="NEVER">
|
||||
<ImageView VBox.vgrow="ALWAYS" fitHeight="64" preserveRatio="true" smooth="true" cache="true">
|
||||
<Image url="/bot.png"/>
|
||||
<Image url="/img/bot/bot.png"/>
|
||||
</ImageView>
|
||||
<VBox spacing="3" HBox.hgrow="ALWAYS" alignment="CENTER_LEFT">
|
||||
<FormattedLabel styleClass="label-large" format="Cryptomator %s" arg1="${controller.applicationVersion}"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<children>
|
||||
<HBox spacing="6" alignment="CENTER_LEFT">
|
||||
<Label text="%preferences.volume.type"/>
|
||||
<ChoiceBox fx:id="volumeTypeChoicBox"/>
|
||||
<ChoiceBox fx:id="volumeTypeChoiceBox"/>
|
||||
</HBox>
|
||||
|
||||
<HBox spacing="6" alignment="CENTER_LEFT" visible="${controller.showWebDavSettings}">
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.CheckBox?>
|
||||
<?import javafx.scene.control.ProgressIndicator?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import org.cryptomator.ui.controls.FormattedLabel?>
|
||||
<?import org.cryptomator.ui.controls.NiceSecurePasswordField?>
|
||||
@@ -19,17 +23,40 @@
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<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}"/>
|
||||
</VBox>
|
||||
<HBox spacing="12" VBox.vgrow="ALWAYS">
|
||||
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
|
||||
<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" fx:id="face" visible="false">
|
||||
<Image url="/img/bot/face.png"/>
|
||||
</ImageView>
|
||||
|
||||
<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" fx:id="leftArm" visible="false">
|
||||
<Image url="/img/bot/arm-l.png"/>
|
||||
</ImageView>
|
||||
|
||||
<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" fx:id="rightArm" visible="false">
|
||||
<Image url="/img/bot/arm-r.png"/>
|
||||
</ImageView>
|
||||
|
||||
<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" fx:id="legs" visible="false">
|
||||
<Image url="/img/bot/legs.png"/>
|
||||
</ImageView>
|
||||
|
||||
<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" fx:id="body">
|
||||
<Image url="/img/bot/body.png"/>
|
||||
</ImageView>
|
||||
</StackPane>
|
||||
<VBox spacing="6" HBox.hgrow="ALWAYS">
|
||||
<FormattedLabel format="%unlock.passwordPrompt" arg1="${controller.vaultName}" wrapText="true"/>
|
||||
<NiceSecurePasswordField fx:id="passwordField" disable="${controller.userInteractionDisabled}"/>
|
||||
<CheckBox fx:id="savePasswordCheckbox" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox" disable="${controller.userInteractionDisabled}" visible="${controller.keychainAccessAvailable}"/>
|
||||
</VBox>
|
||||
</HBox>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="org.cryptomator.ui.mainwindow.VaultDetailController"
|
||||
minWidth="300"
|
||||
spacing="36">
|
||||
spacing="60">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="24"/>
|
||||
</padding>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Hyperlink?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<VBox xmlns="http://javafx.com/javafx"
|
||||
@@ -10,11 +12,15 @@
|
||||
fx:controller="org.cryptomator.ui.mainwindow.VaultDetailLockedController"
|
||||
alignment="TOP_CENTER"
|
||||
spacing="9">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="24"/>
|
||||
</padding>
|
||||
<children>
|
||||
<Button styleClass="button-large" text="%main.vaultDetail.unlockBtn" minWidth="120" onAction="#unlock" defaultButton="${controller.vault.locked}">
|
||||
<Button styleClass="button-large" text="%main.vaultDetail.unlockBtn" minWidth="120" onAction="#unlock" defaultButton="${controller.vault.locked}" visible="${!controller.passwordSaved}"
|
||||
managed="${!controller.passwordSaved}">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="KEY" glyphSize="15"/>
|
||||
</graphic>
|
||||
</Button>
|
||||
<Button styleClass="button-large" text="%main.vaultDetail.unlockNowBtn" minWidth="120" onAction="#unlock" defaultButton="${controller.vault.locked}" visible="${controller.passwordSaved}"
|
||||
managed="${controller.passwordSaved}">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="KEY" glyphSize="15"/>
|
||||
</graphic>
|
||||
@@ -24,5 +30,13 @@
|
||||
<FontAwesome5IconView glyph="COG"/>
|
||||
</graphic>
|
||||
</Hyperlink>
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
<HBox alignment="CENTER_RIGHT" spacing="6">
|
||||
<Label styleClass="label-small,label-muted" text="%main.vaultDetail.passwordSavedInKeychain" visible="${controller.passwordSaved}">
|
||||
<graphic>
|
||||
<FontAwesome5IconView styleClass="glyph-icon-muted" glyph="LOCK"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
</HBox>
|
||||
</children>
|
||||
</VBox>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
@@ -8,21 +9,36 @@
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="org.cryptomator.ui.mainwindow.VaultDetailMissingVaultController"
|
||||
alignment="TOP_CENTER"
|
||||
spacing="9">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="24"/>
|
||||
</padding>
|
||||
spacing="24">
|
||||
<children>
|
||||
<StackPane alignment="CENTER">
|
||||
<Circle styleClass="glyph-icon-primary" radius="48"/>
|
||||
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="FILE" glyphSize="48"/>
|
||||
<FontAwesome5IconView styleClass="glyph-icon-primary" glyph="SEARCH" glyphSize="24">
|
||||
<StackPane.margin>
|
||||
<Insets top="12"/>
|
||||
</StackPane.margin>
|
||||
</FontAwesome5IconView>
|
||||
</StackPane>
|
||||
|
||||
<Label text="%main.vaultDetail.missing.info" wrapText="true"/>
|
||||
<VBox spacing="9" alignment="CENTER">
|
||||
<StackPane alignment="CENTER">
|
||||
<Circle styleClass="glyph-icon-primary" radius="48"/>
|
||||
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="FILE" glyphSize="48"/>
|
||||
<FontAwesome5IconView styleClass="glyph-icon-primary" glyph="SEARCH" glyphSize="24">
|
||||
<StackPane.margin>
|
||||
<Insets top="12"/>
|
||||
</StackPane.margin>
|
||||
</FontAwesome5IconView>
|
||||
</StackPane>
|
||||
<Label text="%main.vaultDetail.missing.info" wrapText="true"/>
|
||||
</VBox>
|
||||
<VBox spacing="6" alignment="CENTER">
|
||||
<Button text="%main.vaultDetail.missing.recheck" minWidth="120" onAction="#recheck">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="REDO"/>
|
||||
</graphic>
|
||||
</Button>
|
||||
<Button text="%main.vaultDetail.missing.changeLocation" minWidth="120" onAction="#changeLocation">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="EDIT"/>
|
||||
</graphic>
|
||||
</Button>
|
||||
<Button text="%main.vaultDetail.missing.remove" minWidth="120" onAction="#didClickRemoveVault">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="TRASH"/>
|
||||
</graphic>
|
||||
</Button>
|
||||
</VBox>
|
||||
</children>
|
||||
</VBox>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user