Merge branch 'develop' into feature/modular

This commit is contained in:
Sebastian Stenzel
2021-07-06 13:19:07 +02:00
committed by GitHub
35 changed files with 561 additions and 332 deletions

View File

@@ -59,8 +59,10 @@ public class KeychainManager implements KeychainAccessProvider {
@Override
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
getKeychainOrFail().changePassphrase(key, passphrase);
setPassphraseStored(key, true);
if (isPassphraseStored(key)) {
getKeychainOrFail().changePassphrase(key, passphrase);
setPassphraseStored(key, true);
}
}
@Override

View File

@@ -5,8 +5,8 @@ import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.VaultCipherCombo;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
@@ -182,7 +182,7 @@ public class CreateNewVaultPasswordController implements FxController {
// 2. initialize vault:
try {
MasterkeyLoader loader = ignored -> masterkey.clone();
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build();
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).withKeyLoader(loader).build();
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID);
// 3. write vault-internal readme file:

View File

@@ -39,7 +39,7 @@ public enum FontAwesome5Icon {
REDO("\uF01E"), //
SEARCH("\uF002"), //
SPINNER("\uF110"), //
STOPWATCH("\uF2F2"), //
STETHOSCOPE("\uF0f1"), //
SYNC("\uF021"), //
TIMES("\uF00D"), //
TRASH("\uF1F8"), //

View File

@@ -21,8 +21,8 @@ public class FontAwesome5IconView extends Text {
private static final String FONT_PATH = "/css/fontawesome5-free-solid.otf";
private static final Font FONT;
private ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
private DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE);
private final ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
private final DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE);
static {
try {
@@ -42,7 +42,7 @@ public class FontAwesome5IconView extends Text {
}
private void glyphChanged(@SuppressWarnings("unused") ObservableValue<? extends FontAwesome5Icon> observable, @SuppressWarnings("unused") FontAwesome5Icon oldValue, FontAwesome5Icon newValue) {
setText(newValue.unicode());
setText(newValue == null ? null : newValue.unicode());
}
private void glyphSizeChanged(@SuppressWarnings("unused") ObservableValue<? extends Number> observable, @SuppressWarnings("unused") Number oldValue, Number newValue) {

View File

@@ -16,7 +16,6 @@ public class BatchService extends Service<Void> {
private final Iterator<HealthCheckTask> remainingTasks;
@Inject
public BatchService(Iterable<HealthCheckTask> tasks) {
this.remainingTasks = tasks.iterator();
}

View File

@@ -23,7 +23,7 @@ import java.util.stream.Stream;
@HealthCheckScoped
public class CheckDetailController implements FxController {
private final EasyObservableList<DiagnosticResult> results;
private final EasyObservableList<Result> results;
private final OptionalBinding<Worker.State> taskState;
private final Binding<String> taskName;
private final Binding<String> taskDuration;
@@ -39,7 +39,7 @@ public class CheckDetailController implements FxController {
private final ResultListCellFactory resultListCellFactory;
private final ResourceBundle resourceBundle;
public ListView<DiagnosticResult> resultsListView;
public ListView<Result> resultsListView;
private Subscription resultSubscription;
@Inject
@@ -71,8 +71,8 @@ public class CheckDetailController implements FxController {
}
}
private Function<Stream<? extends DiagnosticResult>, Long> countSeverity(DiagnosticResult.Severity severity) {
return stream -> stream.filter(item -> severity.equals(item.getSeverity())).count();
private Function<Stream<? extends Result>, Long> countSeverity(DiagnosticResult.Severity severity) {
return stream -> stream.filter(item -> severity.equals(item.diagnosis().getSeverity())).count();
}
@FXML

View File

@@ -1,13 +1,10 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -15,101 +12,60 @@ import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ListCell;
import javafx.util.Callback;
import java.util.function.Predicate;
class CheckListCell extends ListCell<HealthCheckTask> {
private final FontAwesome5IconView stateIcon = new FontAwesome5IconView();
private final Callback<HealthCheckTask, BooleanProperty> selectedGetter;
private final ObjectProperty<State> stateProperty;
private CheckBox checkBox = new CheckBox();
private BooleanProperty selectedProperty;
CheckListCell(Callback<HealthCheckTask, BooleanProperty> selectedGetter, ObservableValue<Boolean> switchIndicator) {
this.selectedGetter = selectedGetter;
this.stateProperty = new SimpleObjectProperty<>(State.SELECTION);
switchIndicator.addListener(this::changeState);
CheckListCell() {
setPadding(new Insets(6));
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.LEFT);
getStyleClass().add("label");
}
private void changeState(ObservableValue<? extends Boolean> observableValue, boolean oldValue, boolean newValue) {
if (newValue) {
stateProperty.set(State.RUN);
} else {
stateProperty.set(State.SELECTION);
}
}
@Override
protected void updateItem(HealthCheckTask item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setText(item.getTitle());
}
switch (stateProperty.get()) {
case SELECTION -> updateItemSelection(item, empty);
case RUN -> updateItemRun(item, empty);
}
}
private void updateItemSelection(HealthCheckTask item, boolean empty) {
if (!empty) {
setGraphic(checkBox);
if (selectedProperty != null) {
checkBox.selectedProperty().unbindBidirectional(selectedProperty);
}
selectedProperty = selectedGetter.call(item);
if (selectedProperty != null) {
checkBox.selectedProperty().bindBidirectional(selectedProperty);
}
} else {
setGraphic(null);
setText(null);
}
}
private void updateItemRun(HealthCheckTask item, boolean empty) {
if (item != null) {
item.stateProperty().addListener(this::stateChanged);
graphicProperty().bind(Bindings.createObjectBinding(() -> graphicForState(item.getState()), item.stateProperty()));
stateIcon.setGlyph(glyphForState(item.getState()));
stateIcon.glyphProperty().bind(Bindings.createObjectBinding(() -> glyphForState(item), item.stateProperty()));
checkBox.selectedProperty().bindBidirectional(item.chosenForExecutionProperty());
} else {
graphicProperty().unbind();
setGraphic(null);
setText(null);
checkBox.selectedProperty().unbind();
}
}
private void stateChanged(ObservableValue<? extends Worker.State> observable, Worker.State oldState, Worker.State newState) {
stateIcon.setGlyph(glyphForState(newState));
stateIcon.setVisible(true);
}
private Node graphicForState(Worker.State state) {
return switch (state) {
case READY -> null;
case READY -> checkBox;
case SCHEDULED, RUNNING, FAILED, CANCELLED, SUCCEEDED -> stateIcon;
};
}
private FontAwesome5Icon glyphForState(Worker.State state) {
return switch (state) {
private FontAwesome5Icon glyphForState(HealthCheckTask item) {
return switch (item.getState()) {
case READY -> FontAwesome5Icon.COG; //just a placeholder
case SCHEDULED -> FontAwesome5Icon.CLOCK;
case RUNNING -> FontAwesome5Icon.SPINNER;
case FAILED -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
case CANCELLED -> FontAwesome5Icon.BAN;
case SUCCEEDED -> FontAwesome5Icon.CHECK;
case SUCCEEDED -> checkFoundProblems(item) ? FontAwesome5Icon.EXCLAMATION_TRIANGLE : FontAwesome5Icon.CHECK;
};
}
private enum State {
SELECTION,
RUN;
private boolean checkFoundProblems(HealthCheckTask item) {
Predicate<DiagnosticResult.Severity> isProblem = severity -> switch (severity) {
case WARN, CRITICAL -> true;
case INFO, GOOD -> false;
};
return item.results().stream().map(Result::diagnosis).map(DiagnosticResult::getSeverity).anyMatch(isProblem);
}
}

View File

@@ -1,6 +1,7 @@
package org.cryptomator.ui.health;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.tobiasdiez.easybind.EasyBind;
import dagger.Lazy;
import org.cryptomator.ui.common.ErrorComponent;
@@ -10,28 +11,24 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.concurrent.Worker;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
@@ -43,6 +40,7 @@ public class CheckListController implements FxController {
private final Stage window;
private final ObservableList<HealthCheckTask> tasks;
private final FilteredList<HealthCheckTask> chosenTasks;
private final ReportWriter reportWriter;
private final ExecutorService executorService;
private final ObjectProperty<HealthCheckTask> selectedTask;
@@ -50,19 +48,18 @@ public class CheckListController implements FxController {
private final SimpleObjectProperty<Worker<?>> runningTask;
private final Binding<Boolean> running;
private final Binding<Boolean> finished;
private final Map<HealthCheckTask, BooleanProperty> listPickIndicators;
private final IntegerProperty numberOfPickedChecks;
private final IntegerBinding chosenTaskCount;
private final BooleanBinding anyCheckSelected;
private final BooleanProperty showResultScreen;
/* FXML */
public ListView<HealthCheckTask> checksListView;
@Inject
public CheckListController(@HealthCheckWindow Stage window, Lazy<Collection<HealthCheckTask>> tasks, ReportWriter reportWriteTask, ObjectProperty<HealthCheckTask> selectedTask, ExecutorService executorService, Lazy<ErrorComponent.Builder> errorComponentBuilder) {
public CheckListController(@HealthCheckWindow Stage window, Lazy<List<HealthCheckTask>> tasks, ReportWriter reportWriteTask, ObjectProperty<HealthCheckTask> selectedTask, ExecutorService executorService, Lazy<ErrorComponent.Builder> errorComponentBuilder) {
this.window = window;
this.tasks = FXCollections.observableArrayList(tasks.get());
this.tasks = FXCollections.observableList(tasks.get(), HealthCheckTask::observables);
this.chosenTasks = this.tasks.filtered(HealthCheckTask::isChosenForExecution);
this.reportWriter = reportWriteTask;
this.executorService = executorService;
this.selectedTask = selectedTask;
@@ -70,13 +67,7 @@ public class CheckListController implements FxController {
this.runningTask = new SimpleObjectProperty<>();
this.running = EasyBind.wrapNullable(runningTask).mapObservable(Worker::runningProperty).orElse(false);
this.finished = EasyBind.wrapNullable(runningTask).mapObservable(Worker::stateProperty).map(END_STATES::contains).orElse(false);
this.listPickIndicators = new HashMap<>();
this.numberOfPickedChecks = new SimpleIntegerProperty(0);
this.tasks.forEach(task -> {
var entrySelectedProp = new SimpleBooleanProperty(false);
entrySelectedProp.addListener((observable, oldValue, newValue) -> numberOfPickedChecks.set(numberOfPickedChecks.get() + (newValue ? 1 : -1)));
listPickIndicators.put(task, entrySelectedProp);
});
this.chosenTaskCount = Bindings.size(this.chosenTasks);
this.anyCheckSelected = selectedTask.isNotNull();
this.showResultScreen = new SimpleBooleanProperty(false);
}
@@ -84,27 +75,31 @@ public class CheckListController implements FxController {
@FXML
public void initialize() {
checksListView.setItems(tasks);
checksListView.setCellFactory(view -> new CheckListCell(listPickIndicators::get, showResultScreen));
checksListView.setCellFactory(view -> new CheckListCell());
selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty());
}
@FXML
public void toggleSelectAll(ActionEvent event) {
if (event.getSource() instanceof CheckBox c) {
listPickIndicators.forEach( (task, pickProperty) -> pickProperty.set(c.isSelected()));
tasks.forEach(t -> t.chosenForExecutionProperty().set(c.isSelected()));
}
}
@FXML
public void runSelectedChecks() {
Preconditions.checkState(runningTask.get() == null);
var batch = checksListView.getItems().filtered(item -> listPickIndicators.get(item).get());
var batchService = new BatchService(batch);
// prevent further interaction by cancelling non-chosen tasks:
tasks.filtered(Predicates.not(chosenTasks::contains)).forEach(HealthCheckTask::cancel);
// run chosen tasks:
var batchService = new BatchService(chosenTasks);
batchService.setExecutor(executorService);
batchService.start();
runningTask.set(batchService);
showResultScreen.set(true);
checksListView.getSelectionModel().select(batch.get(0));
checksListView.getSelectionModel().select(chosenTasks.get(0));
checksListView.refresh();
window.sizeToScene();
}
@@ -158,13 +153,12 @@ public class CheckListController implements FxController {
return showResultScreen;
}
public int getNumberOfPickedChecks() {
return numberOfPickedChecks.get();
public int getChosenTaskCount() {
return chosenTaskCount.getValue();
}
public IntegerProperty numberOfPickedChecksProperty() {
return numberOfPickedChecks;
public IntegerBinding chosenTaskCountProperty() {
return chosenTaskCount;
}
}

View File

@@ -0,0 +1,41 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.cryptofs.health.api.HealthCheck;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.Masterkey;
import java.nio.file.Path;
import java.util.function.Consumer;
/**
* FIXME: Remove in production release
*/
public class DummyHealthChecks {
public static class DummyCheck1 implements HealthCheck {
@Override
public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer<DiagnosticResult> consumer) {
// no-op
}
}
public static class DummyCheck2 implements HealthCheck {
@Override
public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer<DiagnosticResult> consumer) {
// no-op
}
}
public static class DummyCheck3 implements HealthCheck {
@Override
public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer<DiagnosticResult> consumer) {
// no-op
}
}
}

View File

@@ -7,6 +7,7 @@ import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Named;
import javafx.scene.Scene;
import javafx.stage.Stage;
@@ -17,6 +18,9 @@ public interface HealthCheckComponent {
@HealthCheckWindow
Stage window();
@Named("windowToClose")
Stage windowToClose();
@FxmlScene(FxmlFile.HEALTH_START)
Lazy<Scene> scene();
@@ -24,6 +28,7 @@ public interface HealthCheckComponent {
Stage stage = window();
stage.setScene(scene().get());
stage.show();
windowToClose().close();
return stage;
}
@@ -33,6 +38,9 @@ public interface HealthCheckComponent {
@BindsInstance
Builder vault(@HealthCheckWindow Vault vault);
@BindsInstance
Builder windowToClose(@Named("windowToClose") Stage window);
HealthCheckComponent build();
}

View File

@@ -28,6 +28,7 @@ import javafx.stage.Modality;
import javafx.stage.Stage;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@@ -63,7 +64,7 @@ abstract class HealthCheckModule {
/* Only inject with Lazy-Wrapper!*/
@Provides
@HealthCheckScoped
static Collection<HealthCheckTask> provideAvailableHealthCheckTasks(Collection<HealthCheck> availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) {
static List<HealthCheckTask> provideAvailableHealthCheckTasks(Collection<HealthCheck> availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) {
return availableHealthChecks.stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check, resourceBundle)).toList();
}
@@ -86,10 +87,10 @@ abstract class HealthCheckModule {
@HealthCheckScoped
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle, ChangeListener<Boolean> showingListener) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("health.title"));
stage.setResizable(true);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.setTitle(resourceBundle.getString("health.title"));
stage.setResizable(true);
stage.showingProperty().addListener(showingListener); // bind masterkey lifecycle to window
return stage;
}

View File

@@ -3,12 +3,16 @@ package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.cryptofs.health.api.HealthCheck;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -31,8 +35,9 @@ class HealthCheckTask extends Task<Void> {
private final Masterkey masterkey;
private final SecureRandom csprng;
private final HealthCheck check;
private final ObservableList<DiagnosticResult> results;
private final ObservableList<Result> results;
private final LongProperty durationInMillis;
private final BooleanProperty chosenForExecution;
public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check, ResourceBundle resourceBundle) {
this.vaultPath = Objects.requireNonNull(vaultPath);
@@ -40,7 +45,7 @@ class HealthCheckTask extends Task<Void> {
this.masterkey = Objects.requireNonNull(masterkey);
this.csprng = Objects.requireNonNull(csprng);
this.check = Objects.requireNonNull(check);
this.results = FXCollections.observableArrayList();
this.results = FXCollections.observableArrayList(Result::observables);
try {
updateTitle(resourceBundle.getString("health." + check.identifier()));
} catch (MissingResourceException e) {
@@ -48,32 +53,22 @@ class HealthCheckTask extends Task<Void> {
updateTitle(check.identifier());
}
this.durationInMillis = new SimpleLongProperty(-1);
this.chosenForExecution = new SimpleBooleanProperty();
}
@Override
protected Void call() {
Instant start = Instant.now();
try (var masterkeyClone = masterkey.clone(); //
var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) {
check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, result -> {
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> {
if (isCancelled()) {
throw new CancellationException();
}
// FIXME: slowdown for demonstration purposes only:
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
if (isCancelled()) {
return;
} else {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
Platform.runLater(() -> results.add(result));
Platform.runLater(() -> results.add(Result.create(diagnosis)));
});
}
Platform.runLater(() ->durationInMillis.set(Duration.between(start, Instant.now()).toMillis()));
Platform.runLater(() -> durationInMillis.set(Duration.between(start, Instant.now()).toMillis()));
return null;
}
@@ -89,7 +84,11 @@ class HealthCheckTask extends Task<Void> {
/* Getter */
public ObservableList<DiagnosticResult> results() {
Observable[] observables() {
return new Observable[]{results, chosenForExecution};
}
public ObservableList<Result> results() {
return results;
}
@@ -105,4 +104,11 @@ class HealthCheckTask extends Task<Void> {
return durationInMillis.get();
}
public BooleanProperty chosenForExecutionProperty() {
return chosenForExecution;
}
public boolean isChosenForExecution() {
return chosenForExecution.get();
}
}

View File

@@ -72,7 +72,7 @@ public class ReportWriter {
case SUCCEEDED -> {
writer.write("STATUS: SUCCESS\nRESULTS:\n");
for (var result : task.results()) {
writer.write(REPORT_CHECK_RESULT.formatted(result.getSeverity(), result.toString()));
writer.write(REPORT_CHECK_RESULT.formatted(result.diagnosis().getSeverity(), result.getDescription()));
}
}
case CANCELLED -> writer.write("STATUS: CANCELED\n");

View File

@@ -0,0 +1,43 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
record Result(DiagnosticResult diagnosis, ObjectProperty<FixState> fixState) {
enum FixState {
NOT_FIXABLE,
FIXABLE,
FIXING,
FIXED,
FIX_FAILED
}
public static Result create(DiagnosticResult diagnosis) {
FixState initialState = switch (diagnosis.getSeverity()) {
case WARN -> FixState.FIXABLE;
default -> FixState.NOT_FIXABLE;
};
return new Result(diagnosis, new SimpleObjectProperty<>(initialState));
}
public Observable[] observables() {
return new Observable[]{fixState};
}
public String getDescription() {
return diagnosis.toString();
}
public FixState getState() {
return fixState.get();
}
public void setState(FixState state) {
this.fixState.set(state);
}
}

View File

@@ -4,25 +4,30 @@ import com.google.common.base.Preconditions;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.scene.control.Alert;
import javafx.application.Platform;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
@HealthCheckScoped
class ResultFixApplier {
private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class);
private final Path vaultPath;
private final SecureRandom csprng;
private final Masterkey masterkey;
private final VaultConfig vaultConfig;
private final ExecutorService sequentialExecutor;
@Inject
public ResultFixApplier(@HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng) {
@@ -30,18 +35,32 @@ class ResultFixApplier {
this.masterkey = masterkeyRef.get();
this.vaultConfig = vaultConfigRef.get();
this.csprng = csprng;
this.sequentialExecutor = Executors.newSingleThreadExecutor();
}
public void fix(DiagnosticResult result) {
Preconditions.checkArgument(result.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result");
public CompletionStage<Void> fix(Result result) {
Preconditions.checkArgument(result.getState() == Result.FixState.FIXABLE);
result.setState(Result.FixState.FIXING);
return CompletableFuture.runAsync(() -> fix(result.diagnosis()), sequentialExecutor)
.whenCompleteAsync((unused, throwable) -> {
var fixed = throwable == null ? Result.FixState.FIXED : Result.FixState.FIX_FAILED;
result.setState(fixed);
}, Platform::runLater);
}
public void fix(DiagnosticResult diagnosis) {
Preconditions.checkArgument(diagnosis.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result");
try (var masterkeyClone = masterkey.clone(); //
var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) {
result.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
diagnosis.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
} catch (Exception e) {
LOG.error("Failed to apply fix", e);
Alert alert = new Alert(Alert.AlertType.ERROR, e.getMessage());
alert.showAndWait();
//TODO: real error/not supported handling
throw new FixFailedException(e);
}
}
public static class FixFailedException extends CompletionException {
private FixFailedException(Throwable cause) {
super(cause);
}
}
}

View File

@@ -1,84 +1,89 @@
package org.cryptomator.ui.health;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import com.tobiasdiez.easybind.optional.OptionalBinding;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
// unscoped because each cell needs its own controller
public class ResultListCellController implements FxController {
private final ResultFixApplier fixApplier;
private final ObjectProperty<DiagnosticResult> result;
private final Logger LOG = LoggerFactory.getLogger(ResultListCellController.class);
private final ObjectProperty<Result> result;
private final Binding<String> description;
private final ResultFixApplier fixApplier;
private final OptionalBinding<Result.FixState> fixState;
private final ObjectBinding<FontAwesome5Icon> glyph;
private final BooleanBinding fixable;
private final BooleanBinding fixing;
private final BooleanBinding fixed;
public FontAwesome5IconView iconView;
public Button actionButton;
public Button fixButton;
@Inject
public ResultListCellController(ResultFixApplier fixApplier) {
this.result = new SimpleObjectProperty<>(null);
this.description = EasyBind.wrapNullable(result).map(DiagnosticResult::toString).orElse("");
this.description = EasyBind.wrapNullable(result).map(Result::getDescription).orElse("");
this.fixApplier = fixApplier;
result.addListener(this::updateCellContent);
}
private void updateCellContent(ObservableValue<? extends DiagnosticResult> observable, DiagnosticResult oldVal, DiagnosticResult newVal) {
iconView.getStyleClass().clear();
actionButton.setVisible(false);
//TODO: see comment in case WARN
actionButton.setManaged(false);
switch (newVal.getSeverity()) {
case INFO -> {
iconView.setGlyph(FontAwesome5Icon.INFO_CIRCLE);
iconView.getStyleClass().add("glyph-icon-muted");
}
case GOOD -> {
iconView.setGlyph(FontAwesome5Icon.CHECK);
iconView.getStyleClass().add("glyph-icon-primary");
}
case WARN -> {
iconView.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE);
iconView.getStyleClass().add("glyph-icon-orange");
//TODO: Neither is any fix implemented, nor it is ensured, that only fix is executed at a time with good ui indication
// before both are not fix, do not show the button
//actionButton.setVisible(true);
}
case CRITICAL -> {
iconView.setGlyph(FontAwesome5Icon.TIMES);
iconView.getStyleClass().add("glyph-icon-red");
}
}
this.fixState = EasyBind.wrapNullable(result).mapObservable(Result::fixState);
this.glyph = Bindings.createObjectBinding(this::getGlyph, result);
this.fixable = Bindings.createBooleanBinding(this::isFixable, fixState);
this.fixing = Bindings.createBooleanBinding(this::isFixing, fixState);
this.fixed = Bindings.createBooleanBinding(this::isFixed, fixState);
}
@FXML
public void runResultAction() {
final var realResult = result.get();
if (realResult != null) {
fixApplier.fix(realResult);
public void initialize() {
// see getGlyph() for relevant glyphs:
EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-muted", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.INFO_CIRCLE));
EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-primary", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.CHECK));
EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-orange", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.EXCLAMATION_TRIANGLE));
EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-red", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.TIMES));
}
@FXML
public void fix() {
Result r = result.get();
if (r != null) {
fixApplier.fix(r).whenCompleteAsync(this::fixFinished, Platform::runLater);
}
}
private void fixFinished(Void unused, Throwable exception) {
if (exception != null) {
LOG.error("Failed to apply fix", exception);
// TODO ...
}
}
/* Getter & Setter */
public DiagnosticResult getResult() {
public Result getResult() {
return result.get();
}
public void setResult(DiagnosticResult result) {
public void setResult(Result result) {
this.result.set(result);
}
public ObjectProperty<DiagnosticResult> resultProperty() {
public ObjectProperty<Result> resultProperty() {
return result;
}
@@ -86,7 +91,49 @@ public class ResultListCellController implements FxController {
return description.getValue();
}
public ObjectBinding<FontAwesome5Icon> glyphProperty() {
return glyph;
}
public FontAwesome5Icon getGlyph() {
var r = result.get();
if (r == null) {
return null;
}
return switch (r.diagnosis().getSeverity()) {
case INFO -> FontAwesome5Icon.INFO_CIRCLE;
case GOOD -> FontAwesome5Icon.CHECK;
case WARN -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
case CRITICAL -> FontAwesome5Icon.TIMES;
};
}
public Binding<String> descriptionProperty() {
return description;
}
public BooleanBinding fixableProperty() {
return fixable;
}
public boolean isFixable() {
return fixState.get().map(Result.FixState.FIXABLE::equals).orElse(false);
}
public BooleanBinding fixingProperty() {
return fixing;
}
public boolean isFixing() {
return fixState.get().map(Result.FixState.FIXING::equals).orElse(false);
}
public BooleanBinding fixedProperty() {
return fixed;
}
public boolean isFixed() {
return fixState.get().map(Result.FixState.FIXED::equals).orElse(false);
}
}

View File

@@ -1,7 +1,6 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import javax.inject.Inject;
@@ -15,7 +14,7 @@ import java.io.IOException;
import java.io.UncheckedIOException;
@HealthCheckScoped
public class ResultListCellFactory implements Callback<ListView<DiagnosticResult>, ListCell<DiagnosticResult>> {
public class ResultListCellFactory implements Callback<ListView<Result>, ListCell<Result>> {
private final FxmlLoaderFactory fxmlLoaders;
@@ -25,7 +24,7 @@ public class ResultListCellFactory implements Callback<ListView<DiagnosticResult
}
@Override
public ListCell<DiagnosticResult> call(ListView<DiagnosticResult> param) {
public ListCell<Result> call(ListView<Result> param) {
try {
FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_result_listcell.fxml");
return new ResultListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController());
@@ -34,7 +33,7 @@ public class ResultListCellFactory implements Callback<ListView<DiagnosticResult
}
}
private static class Cell extends ListCell<DiagnosticResult> {
private static class Cell extends ListCell<Result> {
private final Parent node;
private final ResultListCellController controller;
@@ -45,7 +44,7 @@ public class ResultListCellFactory implements Callback<ListView<DiagnosticResult
}
@Override
protected void updateItem(DiagnosticResult item, boolean empty) {
protected void updateItem(Result item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);

View File

@@ -18,11 +18,16 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.Optional;
import java.io.UncheckedIOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
@@ -31,38 +36,40 @@ public class StartController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(StartController.class);
private final Vault vault;
private final Stage window;
private final Optional<VaultConfig.UnverifiedVaultConfig> unverifiedVaultConfig;
private final CompletableFuture<VaultConfig.UnverifiedVaultConfig> unverifiedVaultConfig;
private final KeyLoadingStrategy keyLoadingStrategy;
private final ExecutorService executor;
private final AtomicReference<Masterkey> masterkeyRef;
private final AtomicReference<VaultConfig> vaultConfigRef;
private final Lazy<Scene> checkScene;
private final Lazy<ErrorComponent.Builder> errorComponent;
private final ObjectProperty<State> state = new SimpleObjectProperty<>(State.LOADING);
private final BooleanBinding loading = state.isEqualTo(State.LOADING);
private final BooleanBinding failed = state.isEqualTo(State.FAILED);
private final BooleanBinding loaded = state.isEqualTo(State.LOADED);
public enum State {
LOADING,
FAILED,
LOADED
}
/* FXML */
@Inject
public StartController(@HealthCheckWindow Vault vault, @HealthCheckWindow Stage window, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, Lazy<ErrorComponent.Builder> errorComponent) {
this.vault = vault;
this.window = window;
this.unverifiedVaultConfig = CompletableFuture.supplyAsync(this::loadConfig, executor);
this.keyLoadingStrategy = keyLoadingStrategy;
this.executor = executor;
this.masterkeyRef = masterkeyRef;
this.vaultConfigRef = vaultConfigRef;
this.checkScene = checkScene;
this.errorComponent = errorComponent;
//TODO: this is ugly
//idea: delay the loading of the vault config and show a spinner (something like "check/load config") and react to the result of the loading
//or: load vault config in a previous step to see if it is loadable.
VaultConfig.UnverifiedVaultConfig tmp;
try {
tmp = vault.getUnverifiedVaultConfig();
} catch (IOException e) {
e.printStackTrace();
tmp = null;
}
this.unverifiedVaultConfig = Optional.ofNullable(tmp);
this.unverifiedVaultConfig.whenCompleteAsync(this::loadedConfig, Platform::runLater);
}
@FXML
@@ -74,41 +81,64 @@ public class StartController implements FxController {
@FXML
public void next() {
LOG.trace("StartController.next()");
executor.submit(this::loadKey);
CompletableFuture.runAsync(this::loadKey, executor).whenCompleteAsync(this::loadedKey, Platform::runLater);
}
private VaultConfig.UnverifiedVaultConfig loadConfig() {
assert !Platform.isFxApplicationThread();
try {
return this.vault.getUnverifiedVaultConfig();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void loadedConfig(VaultConfig.UnverifiedVaultConfig cfg, Throwable exception) {
assert Platform.isFxApplicationThread();
if (exception != null) {
state.set(State.FAILED);
} else {
assert cfg != null;
state.set(State.LOADED);
}
}
private void loadKey() {
assert !Platform.isFxApplicationThread();
assert unverifiedVaultConfig.isPresent();
try (var masterkey = keyLoadingStrategy.loadKey(unverifiedVaultConfig.orElseThrow().getKeyId())) {
var unverifiedCfg = unverifiedVaultConfig.get();
assert unverifiedVaultConfig.isDone();
var unverifiedCfg = unverifiedVaultConfig.join();
try (var masterkey = keyLoadingStrategy.loadKey(unverifiedCfg.getKeyId())) {
var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion());
vaultConfigRef.set(verifiedCfg);
var old = masterkeyRef.getAndSet(masterkey.clone());
if (old != null) {
old.destroy();
}
Platform.runLater(this::loadedKey);
} catch (MasterkeyLoadingFailedException e) {
if (keyLoadingStrategy.recoverFromException(e)) {
// retry
loadKey();
} else {
Platform.runLater(() -> loadingKeyFailed(e));
throw new LoadingFailedException(e);
}
} catch (VaultKeyInvalidException e) {
Platform.runLater(() -> loadingKeyFailed(e));
} catch (VaultConfigLoadException e) {
Platform.runLater(() -> loadingKeyFailed(e));
throw new LoadingFailedException(e);
}
}
private void loadedKey() {
LOG.debug("Loaded valid key");
window.setScene(checkScene.get());
private void loadedKey(Void unused, Throwable exception) {
assert Platform.isFxApplicationThread();
if (exception instanceof LoadingFailedException) {
loadingKeyFailed(exception.getCause());
} else if (exception != null) {
loadingKeyFailed(exception);
} else {
LOG.debug("Loaded valid key");
window.setScene(checkScene.get());
}
}
private void loadingKeyFailed(Exception e) {
private void loadingKeyFailed(Throwable e) {
if (e instanceof UnlockCancelledException) {
// ok
} else if (e instanceof VaultKeyInvalidException) {
@@ -120,8 +150,37 @@ public class StartController implements FxController {
}
}
public boolean isInvalidConfig() {
return unverifiedVaultConfig.isEmpty();
/* Getter */
public BooleanBinding loadingProperty() {
return loading;
}
public boolean isLoading() {
return loading.get();
}
public BooleanBinding failedProperty() {
return failed;
}
public boolean isFailed() {
return failed.get();
}
public BooleanBinding loadedProperty() {
return loaded;
}
public boolean isLoaded() {
return loaded.get();
}
/* internal types */
private static class LoadingFailedException extends CompletionException {
LoadingFailedException(Throwable cause) {
super(cause);
}
}
}

View File

@@ -1,51 +0,0 @@
package org.cryptomator.ui.vaultoptions;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.NumericTextField;
import javax.inject.Inject;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.util.StringConverter;
@VaultOptionsScoped
public class AutoLockVaultOptionsController implements FxController {
private final Vault vault;
public CheckBox lockAfterTimeCheckbox;
public NumericTextField lockTimeInMinutesTextField;
@Inject
AutoLockVaultOptionsController(@VaultOptionsWindow Vault vault) {
this.vault = vault;
}
@FXML
public void initialize() {
lockAfterTimeCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().autoLockWhenIdle());
Bindings.bindBidirectional(lockTimeInMinutesTextField.textProperty(), vault.getVaultSettings().autoLockIdleSeconds(), new IdleTimeSecondsConverter());
}
private static class IdleTimeSecondsConverter extends StringConverter<Number> {
@Override
public String toString(Number seconds) {
int minutes = seconds.intValue() / 60; // int-truncate
return Integer.toString(minutes);
}
@Override
public Number fromString(String string) {
try {
int minutes = Integer.valueOf(string);
return minutes * 60;
} catch (NumberFormatException e) {
return 0;
}
}
}
}

View File

@@ -3,10 +3,11 @@ package org.cryptomator.ui.vaultoptions;
import org.cryptomator.common.settings.WhenUnlocked;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.health.HealthCheckComponent;
import org.cryptomator.ui.controls.NumericTextField;
import javax.inject.Inject;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
@@ -23,18 +24,18 @@ public class GeneralVaultOptionsController implements FxController {
private final Stage window;
private final Vault vault;
private final HealthCheckComponent.Builder healthCheckWindow;
private final ResourceBundle resourceBundle;
public TextField vaultName;
public CheckBox unlockOnStartupCheckbox;
public ChoiceBox<WhenUnlocked> actionAfterUnlockChoiceBox;
public CheckBox lockAfterTimeCheckbox;
public NumericTextField lockTimeInMinutesTextField;
@Inject
GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow, ResourceBundle resourceBundle) {
GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle) {
this.window = window;
this.vault = vault;
this.healthCheckWindow = healthCheckWindow;
this.resourceBundle = resourceBundle;
}
@@ -47,6 +48,8 @@ public class GeneralVaultOptionsController implements FxController {
actionAfterUnlockChoiceBox.getItems().addAll(WhenUnlocked.values());
actionAfterUnlockChoiceBox.valueProperty().bindBidirectional(vault.getVaultSettings().actionAfterUnlock());
actionAfterUnlockChoiceBox.setConverter(new WhenUnlockedConverter(resourceBundle));
lockAfterTimeCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().autoLockWhenIdle());
Bindings.bindBidirectional(lockTimeInMinutesTextField.textProperty(), vault.getVaultSettings().autoLockIdleSeconds(), new IdleTimeSecondsConverter());
}
private void trimVaultNameOnFocusLoss(Observable observable, Boolean wasFocussed, Boolean isFocussed) {
@@ -64,12 +67,6 @@ public class GeneralVaultOptionsController implements FxController {
}
}
@FXML
public void showHealthCheck() {
healthCheckWindow.vault(vault).build().showHealthCheckWindow();
}
private static class WhenUnlockedConverter extends StringConverter<WhenUnlocked> {
private final ResourceBundle resourceBundle;
@@ -89,4 +86,22 @@ public class GeneralVaultOptionsController implements FxController {
}
}
private static class IdleTimeSecondsConverter extends StringConverter<Number> {
@Override
public String toString(Number seconds) {
int minutes = seconds.intValue() / 60; // int-truncate
return Integer.toString(minutes);
}
@Override
public Number fromString(String string) {
try {
int minutes = Integer.valueOf(string);
return minutes * 60;
} catch (NumberFormatException e) {
return 0;
}
}
}
}

View File

@@ -0,0 +1,30 @@
package org.cryptomator.ui.vaultoptions;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.health.HealthCheckComponent;
import javax.inject.Inject;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.stage.Stage;
@VaultOptionsScoped
public class HealthVaultOptionsController implements FxController {
private final Stage window;
private final Vault vault;
private final HealthCheckComponent.Builder healthCheckWindow;
@Inject
public HealthVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow) {
this.window = window;
this.vault = vault;
this.healthCheckWindow = healthCheckWindow;
}
@FXML
public void startHealthCheck(ActionEvent event) {
healthCheckWindow.vault(vault).windowToClose(window).build().showHealthCheckWindow();
}
}

View File

@@ -23,7 +23,11 @@ public enum SelectedVaultOptionsTab {
/**
* Show Auto-Lock tab
*
*/
AUTOLOCK,
/**
* Show health tab
*/
HEALTH;
}

View File

@@ -24,6 +24,7 @@ public class VaultOptionsController implements FxController {
public Tab mountTab;
public Tab keyTab;
public Tab autoLockTab;
public Tab healthTab;
@Inject
VaultOptionsController(@VaultOptionsWindow Stage window, ObjectProperty<SelectedVaultOptionsTab> selectedTabProperty) {
@@ -49,6 +50,7 @@ public class VaultOptionsController implements FxController {
case MOUNT -> mountTab;
case KEY -> keyTab;
case AUTOLOCK -> autoLockTab;
case HEALTH -> healthTab;
};
}

View File

@@ -86,7 +86,6 @@ abstract class VaultOptionsModule {
@Binds
@IntoMap
@FxControllerKey(AutoLockVaultOptionsController.class)
abstract FxController bindAutoLockVaultOptionsController(AutoLockVaultOptionsController controller);
@FxControllerKey(HealthVaultOptionsController.class)
abstract FxController bindHealthOptionsController(HealthVaultOptionsController controller);
}