From 5d12a62e38c66ae83c3a196f0ead0d1582f3954b Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Tue, 14 Jul 2026 17:22:45 +0200 Subject: [PATCH] Replace ImportTemplate stubs with impls Signed-off-by: Armin Schrenk --- .../ImportTemplateLocationController.java | 292 +++++++++++++++++- .../ImportTemplateSuccessController.java | 13 +- .../fxml/import_template_location.fxml | 65 +++- .../fxml/import_template_success.fxml | 27 +- src/main/resources/i18n/strings.properties | 6 +- 5 files changed, 388 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/importtemplate/ImportTemplateLocationController.java b/src/main/java/org/cryptomator/ui/importtemplate/ImportTemplateLocationController.java index c4e705f5d..70a83daa0 100644 --- a/src/main/java/org/cryptomator/ui/importtemplate/ImportTemplateLocationController.java +++ b/src/main/java/org/cryptomator/ui/importtemplate/ImportTemplateLocationController.java @@ -1,40 +1,328 @@ package org.cryptomator.ui.importtemplate; +import dagger.Lazy; +import org.cryptomator.common.ObservableUtil; +import org.cryptomator.common.locationpresets.LocationPreset; +import org.cryptomator.common.locationpresets.LocationPresetsProvider; +import org.cryptomator.common.settings.Settings; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultListManager; +import org.cryptomator.launcher.VaultTemplateExtractor; import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.Tasks; +import org.cryptomator.ui.controls.FontAwesome5IconView; +import org.cryptomator.ui.fxapp.FxApplicationWindows; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.ObjectBinding; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.stage.DirectoryChooser; import javafx.stage.Stage; +import javafx.stage.WindowEvent; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.concurrent.ExecutorService; @ImportTemplateScoped public class ImportTemplateLocationController implements FxController { + private static final Logger LOG = LoggerFactory.getLogger(ImportTemplateLocationController.class); + private static final Path DEFAULT_CUSTOM_VAULT_PATH = Path.of(System.getProperty("user.home")); + private static final String TEMP_FILE_PREFIX = ".locationTest.cryptomator"; + private final Stage window; private final String vaultName; private final byte[] template; private final ObjectProperty vaultPath; + private final ObjectProperty vault; + private final Lazy successScene; + private final FxApplicationWindows appWindows; + private final VaultListManager vaultListManager; + private final ExecutorService executor; + private final Settings settings; + private final ResourceBundle resourceBundle; + private final ObservableValue vaultPathStatus; + private final ObservableValue validVaultPath; + private final BooleanProperty usePresetPath; + private final BooleanProperty loadingPresetLocations = new SimpleBooleanProperty(false); + private final BooleanProperty processing = new SimpleBooleanProperty(false); + private final ObservableValue readyToImport; + private final ObjectBinding createButtonState; + private final ObservableList radioButtons; + private final ObservableList sortedRadioButtons; + + private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH; + + //FXML + public ToggleGroup locationPresetsToggler; + public VBox radioButtonVBox; + public HBox customLocationRadioBtn; + public RadioButton customRadioButton; + public Label locationStatusLabel; + public FontAwesome5IconView goodLocation; + public FontAwesome5IconView badLocation; @Inject ImportTemplateLocationController(@ImportTemplateWindow Stage window, // @Named("vaultName") String vaultName, // @Named("vaultTemplate") byte[] template, // - ObjectProperty vaultPath) { + ObjectProperty vaultPath, // + @ImportTemplateWindow ObjectProperty vault, // + @FxmlScene(FxmlFile.IMPORT_TEMPLATE_SUCCESS) Lazy successScene, // + FxApplicationWindows appWindows, // + VaultListManager vaultListManager, // + ExecutorService executor, // + Settings settings, // + ResourceBundle resourceBundle) { this.window = window; this.vaultName = vaultName; this.template = template; this.vaultPath = vaultPath; + this.vault = vault; + this.successScene = successScene; + this.appWindows = appWindows; + this.vaultListManager = vaultListManager; + this.executor = executor; + this.settings = settings; + this.resourceBundle = resourceBundle; + this.vaultPathStatus = ObservableUtil.mapWithDefault(vaultPath, this::validatePath, new VaultPathStatus(false, "error.message")); + this.validVaultPath = ObservableUtil.mapWithDefault(vaultPathStatus, VaultPathStatus::valid, false); + this.vaultPathStatus.addListener(this::updateStatusLabel); + this.usePresetPath = new SimpleBooleanProperty(); + this.readyToImport = Bindings.createBooleanBinding(() -> validVaultPath.getValue() && !processing.get(), validVaultPath, processing); + this.createButtonState = Bindings.when(processing).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY); + this.radioButtons = FXCollections.observableArrayList(); + this.sortedRadioButtons = radioButtons.sorted(this::compareLocationPresets); + + Path previouslyUsedDirectory = settings.previouslyUsedVaultDirectory.get(); + if (previouslyUsedDirectory != null) { + try { + if (Files.exists(previouslyUsedDirectory) && Files.isDirectory(previouslyUsedDirectory) && isActuallyWritable(previouslyUsedDirectory)) { + this.customVaultPath = previouslyUsedDirectory; + } + } catch (InvalidPathException | NullPointerException e) { + LOG.warn("Invalid previously used vault directory path: {}", previouslyUsedDirectory, e); + } + } + } + + private VaultPathStatus validatePath(Path p) throws NullPointerException { + if (!Files.exists(p.getParent())) { + return new VaultPathStatus(false, "addvaultwizard.new.locationDoesNotExist"); + } else if (!isActuallyWritable(p.getParent())) { + return new VaultPathStatus(false, "addvaultwizard.new.locationIsNotWritable"); + } else if (!Files.notExists(p)) { + return new VaultPathStatus(false, "addvaultwizard.new.fileAlreadyExists"); + } else { + return new VaultPathStatus(true, "addvaultwizard.new.locationIsOk"); + } + } + + private void updateStatusLabel(ObservableValue observable, VaultPathStatus oldValue, VaultPathStatus newValue) { + if (newValue.valid()) { + locationStatusLabel.setGraphic(goodLocation); + locationStatusLabel.getStyleClass().remove("label-red"); + locationStatusLabel.getStyleClass().add("label-muted"); + } else { + locationStatusLabel.setGraphic(badLocation); + locationStatusLabel.getStyleClass().remove("label-muted"); + locationStatusLabel.getStyleClass().add("label-red"); + } + this.locationStatusLabel.setText(resourceBundle.getString(newValue.localizationKey())); + } + + private boolean isActuallyWritable(Path p) { + Path tmpDir = null; + try { + tmpDir = Files.createTempDirectory(p, TEMP_FILE_PREFIX); + return true; + } catch (IOException e) { + return false; + } finally { + if (tmpDir != null) { + try { + Files.deleteIfExists(tmpDir); + } catch (IOException e) { + LOG.warn("Unable to delete temporary directory {}. Needs to be deleted manually.", tmpDir); + } + } + } } @FXML public void initialize() { - // TODO (import-template step 4b): location presets, path validation, finish() with spinner + Tasks.runOnce + var task = executor.submit(this::loadLocationPresets); + window.addEventHandler(WindowEvent.WINDOW_HIDING, _ -> task.cancel(true)); + locationPresetsToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation); + usePresetPath.bind(locationPresetsToggler.selectedToggleProperty().isNotEqualTo(customRadioButton)); + radioButtons.add(customLocationRadioBtn); + Bindings.bindContent(radioButtonVBox.getChildren(), sortedRadioButtons); //to prevent garbage collection of the binding, we bind explicitly to the sorted list } + private void loadLocationPresets() { + Platform.runLater(() -> loadingPresetLocations.set(true)); + try { + LocationPresetsProvider.loadAll(LocationPresetsProvider.class) // + .flatMap(LocationPresetsProvider::getLocations) //we do not use sorted(), because it evaluates the stream elements, blocking until all elements are gathered + .forEach(this::createRadioButtonFor); + } finally { + Platform.runLater(() -> loadingPresetLocations.set(false)); + } + } + + private void createRadioButtonFor(LocationPreset preset) { + Platform.runLater(() -> { + var btn = new RadioButton(preset.name()); + btn.setUserData(preset.path()); + radioButtons.add(btn); + locationPresetsToggler.getToggles().add(btn); + }); + } + + private int compareLocationPresets(Node left, Node right) { + if (customLocationRadioBtn.getId().equals(left.getId())) { + return 1; + } else if (customLocationRadioBtn.getId().equals(right.getId())) { + return -1; + } else { + return ((RadioButton) left).getText().compareToIgnoreCase(((RadioButton) right).getText()); + } + } + + private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) { + var storagePath = Optional.ofNullable((Path) newValue.getUserData()).orElse(customVaultPath); + vaultPath.set(storagePath.resolve(vaultName)); + } + + @FXML + public void chooseCustomVaultPath() { + DirectoryChooser directoryChooser = new DirectoryChooser(); + directoryChooser.setTitle(resourceBundle.getString("addvaultwizard.new.directoryPickerTitle")); + 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(); + vaultPath.set(customVaultPath.resolve(vaultName)); + } + } + + @FXML + public void finish() { + if (!isReadyToImport()) { + return; + } + Path destination = vaultPath.get(); + processing.set(true); + Tasks.create(() -> { + VaultTemplateExtractor.extractAndMove(template, destination); + return vaultListManager.add(destination); + }).onSuccess(newVault -> { + vault.set(newVault); + rememberParentDirectory(destination); + window.setScene(successScene.get()); + }).onError(IOException.class, e -> { + LOG.error("Failed to import vault template.", e); + appWindows.showErrorWindow(e, window, window.getScene()); + }).andFinally(() -> processing.set(false)).runOnce(executor); + } + + private void rememberParentDirectory(Path destination) { + Path parentPath = destination.getParent(); + if (parentPath != null) { + settings.previouslyUsedVaultDirectory.setValue(parentPath); + } + } + + /* Internal classes */ + + private record VaultPathStatus(boolean valid, String localizationKey) { + + } + + /* Getter/Setter */ + public String getVaultName() { return vaultName; } + public Path getVaultPath() { + return vaultPath.get(); + } + + public ObjectProperty vaultPathProperty() { + return vaultPath; + } + + public boolean isLoadingPresetLocations() { + return loadingPresetLocations.getValue(); + } + + public BooleanProperty loadingPresetLocationsProperty() { + return loadingPresetLocations; + } + + public BooleanProperty usePresetPathProperty() { + return usePresetPath; + } + + public boolean isUsePresetPath() { + return usePresetPath.get(); + } + + public ObservableValue readyToImportProperty() { + return readyToImport; + } + + public boolean isReadyToImport() { + return readyToImport.getValue(); + } + + public ObjectBinding createButtonStateProperty() { + return createButtonState; + } + + public ContentDisplay getCreateButtonState() { + return createButtonState.get(); + } + + public BooleanBinding anyRadioButtonSelectedProperty() { + return locationPresetsToggler.selectedToggleProperty().isNotNull(); + } + + public boolean isAnyRadioButtonSelected() { + return anyRadioButtonSelectedProperty().get(); + } + } diff --git a/src/main/java/org/cryptomator/ui/importtemplate/ImportTemplateSuccessController.java b/src/main/java/org/cryptomator/ui/importtemplate/ImportTemplateSuccessController.java index 8ce16e929..c150ca513 100644 --- a/src/main/java/org/cryptomator/ui/importtemplate/ImportTemplateSuccessController.java +++ b/src/main/java/org/cryptomator/ui/importtemplate/ImportTemplateSuccessController.java @@ -2,6 +2,7 @@ package org.cryptomator.ui.importtemplate; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.fxapp.FxApplicationWindows; import javax.inject.Inject; import javafx.beans.property.ObjectProperty; @@ -12,22 +13,28 @@ import javafx.stage.Stage; @ImportTemplateScoped public class ImportTemplateSuccessController implements FxController { + private final FxApplicationWindows appWindows; private final Stage window; private final ReadOnlyObjectProperty vault; @Inject - ImportTemplateSuccessController(@ImportTemplateWindow Stage window, @ImportTemplateWindow ObjectProperty vault) { + ImportTemplateSuccessController(FxApplicationWindows appWindows, @ImportTemplateWindow Stage window, @ImportTemplateWindow ObjectProperty vault) { + this.appWindows = appWindows; this.window = window; this.vault = vault; } + @FXML + public void unlockAndClose() { + close(); + appWindows.startUnlockWorkflow(vault.get(), window); + } + @FXML public void close() { window.close(); } - // TODO (import-template step 4b): unlockAndClose() -> appWindows.startUnlockWorkflow(vault.get(), window) - public ReadOnlyObjectProperty vaultProperty() { return vault; } diff --git a/src/main/resources/fxml/import_template_location.fxml b/src/main/resources/fxml/import_template_location.fxml index 35ee842dc..10dba486e 100644 --- a/src/main/resources/fxml/import_template_location.fxml +++ b/src/main/resources/fxml/import_template_location.fxml @@ -1,7 +1,17 @@ + + + + + + + + + + + + + + + - - diff --git a/src/main/resources/fxml/import_template_success.fxml b/src/main/resources/fxml/import_template_success.fxml index dafe35b06..58e409114 100644 --- a/src/main/resources/fxml/import_template_success.fxml +++ b/src/main/resources/fxml/import_template_success.fxml @@ -1,10 +1,15 @@ + + - + + + + - -