Merge branch 'develop' into feature/admin-properties

This commit is contained in:
Armin Schrenk
2026-02-09 15:03:06 +01:00
80 changed files with 453 additions and 196 deletions

View File

@@ -4,24 +4,27 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.AccessibleRole;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.ResourceBundle;
public class NotificationBar extends HBox {
public class InfoBar extends HBox {
@FXML
private Label notificationLabel;
private Label infoMessage;
private final BooleanProperty dismissable = new SimpleBooleanProperty();
private final BooleanProperty notify = new SimpleBooleanProperty();
public NotificationBar() {
public InfoBar() {
setAlignment(Pos.CENTER);
setStyle("-fx-alignment: center;");
getStyleClass().addAll("info-bar");
Region spacer = new Region();
spacer.setMinWidth(40);
@@ -36,14 +39,21 @@ public class NotificationBar extends HBox {
vbox.setAlignment(Pos.CENTER);
HBox.setHgrow(vbox, javafx.scene.layout.Priority.ALWAYS);
notificationLabel = new Label();
notificationLabel.getStyleClass().add("notification-label");
notificationLabel.setStyle("-fx-alignment: center;");
vbox.getChildren().add(notificationLabel);
infoMessage = new Label();
infoMessage.setFocusTraversable(true);
infoMessage.setAccessibleRole(AccessibleRole.BUTTON);
vbox.getChildren().add(infoMessage);
Button closeButton = new Button("X");
var closeGraphic = new FontAwesome5IconView();
closeGraphic.setGlyph(FontAwesome5Icon.TIMES);
closeGraphic.setGlyphSize(12);
closeGraphic.getStyleClass().add("glyph");
Button closeButton = new Button();
closeButton.setGraphic(closeGraphic);
closeButton.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
closeButton.setAccessibleText(ResourceBundle.getBundle("i18n.strings").getString("main.notification.closeButton.tooltip"));
closeButton.setMinWidth(40);
closeButton.setStyle("-fx-background-color: transparent; -fx-text-fill: white; -fx-font-weight: bold;");
closeButton.visibleProperty().bind(dismissable);
closeButton.setOnAction(_ -> {
@@ -61,11 +71,11 @@ public class NotificationBar extends HBox {
}
public String getText() {
return notificationLabel.getText();
return infoMessage.getText();
}
public void setText(String text) {
notificationLabel.setText(text);
infoMessage.setText(text);
}
public void setStyleClass(String styleClass) {

View File

@@ -16,6 +16,7 @@ import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.layout.HBox;
import java.util.ResourceBundle;
// unscoped because each cell needs its own controller
public class VaultListCellController implements FxController {
@@ -23,9 +24,12 @@ public class VaultListCellController implements FxController {
private static final Insets COMPACT_INSETS = new Insets(6, 12, 6, 12);
private static final Insets DEFAULT_INSETS = new Insets(12);
private final ResourceBundle resourceBundle;
private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private final ObservableValue<VaultState.Value> vaultState;
private final ObservableValue<FontAwesome5Icon> glyph;
private final ObservableValue<Boolean> compactMode;
private final ObservableValue<String> accessibleText;
private AutoAnimator spinAnimation;
@@ -35,17 +39,21 @@ public class VaultListCellController implements FxController {
public HBox vaultListCell;
@Inject
VaultListCellController(Settings settings) {
this.glyph = vault.flatMap(Vault::stateProperty).map(this::getGlyphForVaultState);
VaultListCellController(Settings settings, ResourceBundle resourceBundle) {
this.resourceBundle = resourceBundle;
this.vaultState = vault.flatMap(Vault::stateProperty);
this.glyph = vaultState.map(this::getGlyphForVaultState);
this.accessibleText = vaultState.map(this::getAccessibleTextForVaultState);
this.compactMode = settings.compactMode;
}
public void initialize() {
this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) //
.onCondition(vault.flatMap(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals).orElse(false)) //
.onCondition(vaultState.map(VaultState.Value.PROCESSING::equals).orElse(false)) //
.afterStop(() -> vaultStateView.setRotate(0)) //
.build();
this.vaultListCell.paddingProperty().bind(compactMode.map(c -> c ? COMPACT_INSETS : DEFAULT_INSETS));
this.vaultListCell.accessibleTextProperty().bind(accessibleText);
}
// TODO deduplicate w/ VaultDetailController
@@ -62,6 +70,25 @@ public class VaultListCellController implements FxController {
}
}
private String getAccessibleTextForVaultState(VaultState.Value state) {
var v = vault.get();
if (state != null && v != null) {
var translationKey = switch (state) {
case LOCKED -> "vault.state.locked";
case PROCESSING -> "vault.state.processing";
case UNLOCKED -> "vault.state.unlocked";
case NEEDS_MIGRATION -> "vault.state.migrationNeeded";
case MISSING -> "vault.state.missing";
case VAULT_CONFIG_MISSING, ALL_MISSING, ERROR -> "vault.state.error";
};
var localizedState = resourceBundle.getString(translationKey);
return resourceBundle.getString("main.vaultlist.listEntry").formatted(v.getDisplayName(), localizedState);
} else {
return "";
}
}
/* Getter/Setter */
public ObservableValue<FontAwesome5Icon> glyphProperty() {

View File

@@ -104,7 +104,7 @@ public class RecoveryKeyCreationController implements FxController {
descriptionLabel.formatProperty().set(resourceBundle.getString("recoveryKey.recover.description"));
cancelButton.setOnAction((_) -> back());
cancelButton.setText(resourceBundle.getString("generic.button.back"));
nextButton.setOnAction((_) -> restoreWithPassword());
nextButton.setOnAction((_) -> restoreWithPasswordAsync());
}
}
@@ -137,11 +137,47 @@ public class RecoveryKeyCreationController implements FxController {
}
@FXML
public void restoreWithPassword() {
public void restoreWithPasswordAsync() {
Task<Void> task = RecoveryKeyTasks.createTask(this::restoreWithPassword);
task.setOnScheduled(_ -> {
LOG.debug("Restoring vault configuration with password for {}.", vault.getDisplayablePath());
});
task.setOnSucceeded(_ -> {
LOG.debug("Restored vault configuration for {}.", vault.getDisplayablePath());
try {
if (!vaultListManager.isAlreadyAdded(vault.getPath())) {
vaultListManager.add(vault.getPath());
}
window.close();
dialogs.prepareRecoverPasswordSuccess((Stage) window.getOwner()) //
.setTitleKey("recover.recoverVaultConfig.title") //
.setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message") //
.setDescriptionKey("recoveryKey.recover.resetMasterkeyFileSuccess.description")
.build().showAndWait();
} catch (IOException e) {
LOG.error("Failed to add vault to list.", e);
appWindows.showErrorWindow(e, window, null);
}
});
task.setOnFailed(_ -> {
if (task.getException() instanceof InvalidPassphraseException e) {
LOG.info("Password invalid", e);
Animations.createShakeWindowAnimation(window).play();
} else {
LOG.error("Recovery process failed.", task.getException());
appWindows.showErrorWindow(task.getException(), window, null);
}
});
executor.submit(task);
}
void restoreWithPassword() throws IOException, CryptoException {
try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) {
Path recoveryPath = recoveryDirectory.getRecoveryPath();
Path masterkeyFilePath = vault.getPath().resolve(MASTERKEY_FILENAME);
try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, masterkeyFilePath, passwordField.getCharacters())) {
@@ -152,23 +188,6 @@ public class RecoveryKeyCreationController implements FxController {
}
recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME);
if (!vaultListManager.isAlreadyAdded(vault.getPath())) {
vaultListManager.add(vault.getPath());
}
window.close();
dialogs.prepareRecoverPasswordSuccess((Stage)window.getOwner()) //
.setTitleKey("recover.recoverVaultConfig.title") //
.setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message") //
.setDescriptionKey("recoveryKey.recover.resetMasterkeyFileSuccess.description")
.build().showAndWait();
} catch (InvalidPassphraseException e) {
LOG.info("Password invalid", e);
Animations.createShakeWindowAnimation(window).play();
} catch (IOException | CryptoException | IllegalStateException e) {
LOG.error("Recovery process failed", e);
appWindows.showErrorWindow(e, window, null);
}
}

View File

@@ -117,14 +117,46 @@ public class RecoveryKeyResetPasswordController implements FxController {
@FXML
public void next() {
switch (recoverType.get()) {
case RESTORE_ALL -> restorePassword();
case RESTORE_ALL -> restorePasswordAsync();
case RESTORE_MASTERKEY, RESET_PASSWORD -> resetPassword();
default -> resetPassword(); // Fallback
}
}
@FXML
public void restorePassword() {
public void restorePasswordAsync() {
Task<Void> task = RecoveryKeyTasks.createTask(this::restorePassword);
task.setOnScheduled(_ -> {
LOG.debug("Restoring vault configuration for {}.", vault.getDisplayablePath());
});
task.setOnSucceeded(_ -> {
LOG.debug("Restored vault configuration for {}.", vault.getDisplayablePath());
try {
if (!vaultListManager.isAlreadyAdded(vault.getPath())) {
vaultListManager.add(vault.getPath());
}
window.close();
dialogs.prepareRecoverPasswordSuccess((Stage) window.getOwner()) //
.setTitleKey("recover.recoverVaultConfig.title") //
.setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message") //
.build().showAndWait();
} catch (IOException e) {
LOG.error("Failed to add vault to list.", e);
appWindows.showErrorWindow(e, window, null);
}
});
task.setOnFailed(_ -> {
LOG.error("Recovery process failed.", task.getException());
appWindows.showErrorWindow(task.getException(), window, null);
});
executor.submit(task);
}
void restorePassword() throws IOException, CryptoException {
try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) {
Path recoveryPath = recoveryDirectory.getRecoveryPath();
MasterkeyService.recoverFromRecoveryKey(recoveryKey.get(), recoveryKeyFactory, recoveryPath, newPasswordController.passwordField.getCharacters());
@@ -135,19 +167,6 @@ public class RecoveryKeyResetPasswordController implements FxController {
recoveryDirectory.moveRecoveredFile(MASTERKEY_FILENAME);
recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME);
if (!vaultListManager.isAlreadyAdded(vault.getPath())) {
vaultListManager.add(vault.getPath());
}
window.close();
dialogs.prepareRecoverPasswordSuccess((Stage)window.getOwner()) //
.setTitleKey("recover.recoverVaultConfig.title") //
.setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message") //
.build().showAndWait();
} catch (IOException | CryptoException e) {
LOG.error("Recovery process failed", e);
appWindows.showErrorWindow(e, window, null);
}
}

View File

@@ -0,0 +1,25 @@
package org.cryptomator.ui.recoverykey;
import javafx.concurrent.Task;
final class RecoveryKeyTasks {
private RecoveryKeyTasks() {
}
@FunctionalInterface
interface TaskAction {
void run() throws Exception;
}
static Task<Void> createTask(TaskAction action) {
return new Task<Void>() {
@Override
protected Void call() throws Exception {
action.run();
return null;
}
};
}
}