Browse Source

Merge branch 'develop' into feature/share-vault

Jan-Peter Klein 1 year ago
parent
commit
4de8434f1b

+ 4 - 4
.github/workflows/appimage.yml

@@ -29,12 +29,12 @@ jobs:
         include:
           - os: ubuntu-latest
             appimage-suffix: x86_64
-            openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-x64_bin-jmods.zip'
-            openjfx-sha: 'f522ac2ae4bdd61f0219b7b8d2058ff72a22f36a44378453bcfdcd82f8f5e08c'
+            openjfx-url: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-x64_bin-jmods.zip'
+            openjfx-sha: '7baed11ca56d5fee85995fa6612d4299f1e8b7337287228f7f12fd50407c56f8'
           - os: [self-hosted, Linux, ARM64]
             appimage-suffix: aarch64
-            openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-aarch64_bin-jmods.zip'
-            openjfx-sha: 'c0d80ebbe0aab404ef9ad8b46c05bf533a1e40b39b2720eebd9238d81f6326ca'
+            openjfx-url: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-aarch64_bin-jmods.zip'
+            openjfx-sha: '871e7b9d7af16aef2e55c1b7830d0e0b2503b13dd8641374ba7e55ecb81d2ef9'
     steps:
       - uses: actions/checkout@v4
       - name: Setup Java

+ 4 - 4
.github/workflows/debian.yml

@@ -20,10 +20,10 @@ env:
   JAVA_VERSION: '21.0.1+12'
   COFFEELIBS_JDK: 21
   COFFEELIBS_JDK_VERSION: '21.0.1+12-0ppa1'
-  OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-x64_bin-jmods.zip'
-  OPENJFX_JMODS_AMD64_HASH: 'f522ac2ae4bdd61f0219b7b8d2058ff72a22f36a44378453bcfdcd82f8f5e08c'
-  OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-aarch64_bin-jmods.zip'
-  OPENJFX_JMODS_AARCH64_HASH: 'c0d80ebbe0aab404ef9ad8b46c05bf533a1e40b39b2720eebd9238d81f6326ca'
+  OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-x64_bin-jmods.zip'
+  OPENJFX_JMODS_AMD64_HASH: '7baed11ca56d5fee85995fa6612d4299f1e8b7337287228f7f12fd50407c56f8'
+  OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-aarch64_bin-jmods.zip'
+  OPENJFX_JMODS_AARCH64_HASH: '871e7b9d7af16aef2e55c1b7830d0e0b2503b13dd8641374ba7e55ecb81d2ef9'
 
 jobs:
   build:

+ 8 - 47
.github/workflows/dependency-check.yml

@@ -7,50 +7,11 @@ on:
 
 jobs:
   check-dependencies:
-    name: Check dependencies
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v4
-        with:
-          show-progress: false
-      - name: Setup Java
-        uses: actions/setup-java@v4
-        with:
-          distribution: 'temurin'
-          java-version: 21
-          cache: 'maven'
-      - name: Cache NVD DB
-        uses: actions/cache@v3
-        with:
-          path: ~/.m2/repository/org/owasp/dependency-check-data/
-          key: dependency-check-${{ github.run_id }}
-          restore-keys: |
-            dependency-check
-        env:
-          SEGMENT_DOWNLOAD_TIMEOUT_MINS: 5
-      - name: Run org.owasp:dependency-check plugin
-        id: dependency-check
-        continue-on-error: true
-        run: mvn -B validate -Pdependency-check
-        env:
-          NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
-      - name: Upload report on failure
-        if: steps.dependency-check.outcome == 'failure'
-        uses: actions/upload-artifact@v3
-        with:
-          name: dependency-check-report
-          path: target/dependency-check-report.html
-          if-no-files-found: error
-      - name: Slack Notification on regular check
-        if: github.event_name == 'schedule' && steps.dependency-check.outcome == 'failure'
-        uses: rtCamp/action-slack-notify@v2
-        env:
-          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
-          SLACK_USERNAME: 'Cryptobot'
-          SLACK_ICON: false
-          SLACK_ICON_EMOJI: ':bot:'
-          SLACK_CHANNEL: 'cryptomator-desktop'
-          SLACK_TITLE: "Vulnerabilities in ${{ github.event.repository.name }} detected."
-          SLACK_MESSAGE: "Download the <https://github.com/${{ github.repository }}/actions/run/${{ github.run_id }}|report> for more details."
-          SLACK_FOOTER: false
-          MSG_MINIMAL: true
+    uses: skymatic/workflows/.github/workflows/run-dependency-check.yml@main
+    with:
+      runner-os: 'ubuntu-latest'
+      java-distribution: 'temurin'
+      java-version: 21
+    secrets:
+      nvd-api-key: ${{ secrets.NVD_API_KEY }}
+      slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 4 - 4
.github/workflows/mac-dmg.yml

@@ -37,15 +37,15 @@ jobs:
           output-suffix: x64
           xcode-path: '/Applications/Xcode_13.2.1.app'
           fuse-lib: macFUSE
-          openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_osx-x64_bin-jmods.zip'
-          openjfx-sha: '55b8ff7453d59c89ae129f6c9c5ad7b09a5d359568811b376ac1766c14d6a17c'
+          openjfx-url: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_osx-x64_bin-jmods.zip'
+          openjfx-sha: 'bd6abab20da73d5a968dcf2fd915d81b5fb919340e3bb84979ee9a888a829939'
         - os: [self-hosted, macOS, ARM64]
           architecture: aarch64
           output-suffix: arm64
           xcode-path: '/Applications/Xcode_13.2.1.app'
           fuse-lib: FUSE-T
-          openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_osx-aarch64_bin-jmods.zip'
-          openjfx-sha: 'c60f5f19aa847e0e620e0b011e5de68f2c6755641c2141cec27a0b89f612beaf'
+          openjfx-url: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_osx-aarch64_bin-jmods.zip'
+          openjfx-sha: '7afaa1c57a6cc3c384d636e597b9a5364693e2db4aaec0a6e63d2fa964400b58'
     steps:
       - uses: actions/checkout@v4
       - name: Setup Java

+ 2 - 2
.github/workflows/win-exe.yml

@@ -16,8 +16,8 @@ on:
 env:
   JAVA_DIST: 'zulu'
   JAVA_VERSION: '21.0.1+12'
-  OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_windows-x64_bin-jmods.zip'
-  OPENJFX_JMODS_AMD64_HASH: '18625bbc13c57dbf802486564247a8d8cab72ec558c240a401bf6440384ebd77'
+  OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_windows-x64_bin-jmods.zip'
+  OPENJFX_JMODS_AMD64_HASH: 'daf8acae631c016c24cfe23f88469400274d3441dd890615a42dfb501f3eb94a'
   WINFSP_MSI: 'https://github.com/winfsp/winfsp/releases/download/v2.0/winfsp-2.0.23075.msi'
   WINFSP_UNINSTALLER: 'https://github.com/cryptomator/winfsp-uninstaller/releases/download/1.0.0/winfsp-uninstaller.exe'
 

+ 4 - 4
dist/linux/appimage/build.sh

@@ -25,10 +25,10 @@ cp ../../../target/cryptomator-*.jar ../../../target/mods
 
 
 # download javaFX jmods
-OPENJFX_URL='https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-x64_bin-jmods.zip' #by default we assume x64
-OPENJFX_SHA='f522ac2ae4bdd61f0219b7b8d2058ff72a22f36a44378453bcfdcd82f8f5e08c'
-OPENJFX_URL_aarch64='https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-aarch64_bin-jmods.zip'
-OPENJFX_SHA_aarch64='c0d80ebbe0aab404ef9ad8b46c05bf533a1e40b39b2720eebd9238d81f6326ca'
+OPENJFX_URL='https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-x64_bin-jmods.zip'
+OPENJFX_SHA='7baed11ca56d5fee85995fa6612d4299f1e8b7337287228f7f12fd50407c56f8'
+OPENJFX_URL_aarch64='https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-aarch64_bin-jmods.zip'
+OPENJFX_SHA_aarch64='871e7b9d7af16aef2e55c1b7830d0e0b2503b13dd8641374ba7e55ecb81d2ef9'
 
 if [[ "${MACHINE_TYPE}" = "aarch64" ]]; then
 	OPENJFX_URL="${OPENJFX_URL_aarch64}";

+ 1 - 1
dist/mac/dmg/build.sh

@@ -35,7 +35,7 @@ if [ "$(machine)" = "arm64e" ]; then
 else
     ARCH="x64"
 fi
-OPENJFX_JMODS="https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_osx-${ARCH}_bin-jmods.zip"
+OPENJFX_JMODS="https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_osx-${ARCH}_bin-jmods.zip"
 
 # check preconditions
 if [ -z "${JAVA_HOME}" ]; then echo "JAVA_HOME not set. Run using JAVA_HOME=/path/to/jdk ./build.sh"; exit 1; fi

+ 2 - 2
dist/win/build.ps1

@@ -51,9 +51,9 @@ if ($clean -and (Test-Path -Path $runtimeImagePath)) {
 }
 
 ## download jfx jmods
-$jmodsVersion='20.0.2'
+$jmodsVersion='21.0.1'
 $jmodsUrl = "https://download2.gluonhq.com/openjfx/${jmodsVersion}/openjfx-${jmodsVersion}_windows-x64_bin-jmods.zip"
-$jfxJmodsChecksum = '18625bbc13c57dbf802486564247a8d8cab72ec558c240a401bf6440384ebd77'
+$jfxJmodsChecksum = 'daf8acae631c016c24cfe23f88469400274d3441dd890615a42dfb501f3eb94a'
 $jfxJmodsZip = '.\resources\jfxJmods.zip'
 if( !(Test-Path -Path $jfxJmodsZip) ) {
 	Write-Output "Downloading ${jmodsUrl}..."

+ 58 - 14
src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java

@@ -13,31 +13,37 @@ 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.property.BooleanProperty;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleBooleanProperty;
 import javafx.beans.property.StringProperty;
 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.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.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardOpenOption;
-import java.util.Comparator;
-import java.util.List;
 import java.util.Optional;
 import java.util.ResourceBundle;
+import java.util.concurrent.ExecutorService;
 
 @AddVaultWizardScoped
 public class CreateNewVaultLocationController implements FxController {
@@ -49,19 +55,22 @@ public class CreateNewVaultLocationController implements FxController {
 	private final Stage window;
 	private final Lazy<Scene> chooseNameScene;
 	private final Lazy<Scene> chooseExpertSettingsScene;
-	private final List<RadioButton> locationPresetBtns;
 	private final ObjectProperty<Path> vaultPath;
 	private final StringProperty vaultName;
+	private final ExecutorService backgroundExecutor;
 	private final ResourceBundle resourceBundle;
 	private final ObservableValue<VaultPathStatus> vaultPathStatus;
 	private final ObservableValue<Boolean> validVaultPath;
 	private final BooleanProperty usePresetPath;
+	private final BooleanProperty loadingPresetLocations = new SimpleBooleanProperty(false);
+	private final ObservableList<Node> radioButtons;
 
 	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;
@@ -73,25 +82,19 @@ public class CreateNewVaultLocationController implements FxController {
 									 @FxmlScene(FxmlFile.ADDVAULT_NEW_EXPERT_SETTINGS) Lazy<Scene> chooseExpertSettingsScene, //
 									 ObjectProperty<Path> vaultPath, //
 									 @Named("vaultName") StringProperty vaultName, //
-									 ResourceBundle resourceBundle) {
+									 ExecutorService backgroundExecutor, ResourceBundle resourceBundle) {
 		this.window = window;
 		this.chooseNameScene = chooseNameScene;
 		this.chooseExpertSettingsScene = chooseExpertSettingsScene;
 		this.vaultPath = vaultPath;
 		this.vaultName = vaultName;
+		this.backgroundExecutor = backgroundExecutor;
 		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.locationPresetBtns = LocationPresetsProvider.loadAll(LocationPresetsProvider.class) //
-				.flatMap(LocationPresetsProvider::getLocations) //
-				.sorted(Comparator.comparing(LocationPreset::name)) //
-				.map(preset -> { //
-					var btn = new RadioButton(preset.name());
-					btn.setUserData(preset.path());
-					return btn;
-				}).toList();
+		this.radioButtons = FXCollections.observableArrayList();
 	}
 
 	private VaultPathStatus validatePath(Path p) throws NullPointerException {
@@ -137,12 +140,45 @@ public class CreateNewVaultLocationController implements FxController {
 
 	@FXML
 	public void initialize() {
-		radioButtonVBox.getChildren().addAll(1, locationPresetBtns); //first item is the list header
-		locationPresetsToggler.getToggles().addAll(locationPresetBtns);
+		var task = backgroundExecutor.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(), radioButtons.sorted(this::compareLocationPresets));
 	}
 
+	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<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
 		var storagePath = Optional.ofNullable((Path) newValue.getUserData()).orElse(customVaultPath);
 		vaultPath.set(storagePath.resolve(vaultName.get()));
@@ -200,6 +236,14 @@ public class CreateNewVaultLocationController implements FxController {
 		return validVaultPath.getValue();
 	}
 
+	public boolean isLoadingPresetLocations() {
+		return loadingPresetLocations.getValue();
+	}
+
+	public BooleanProperty loadingPresetLocationsProperty() {
+		return loadingPresetLocations;
+	}
+
 	public BooleanProperty usePresetPathProperty() {
 		return usePresetPath;
 	}

+ 6 - 1
src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java

@@ -1,7 +1,6 @@
 package org.cryptomator.ui.keyloading.hub;
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.net.URI;
@@ -19,6 +18,12 @@ public class HubConfig {
 	@Deprecated // use apiBaseUrl + "/devices/"
 	public String devicesResourceUrl;
 
+	/**
+	 * Get the URI pointing to the <code>/api/</code> base resource.
+	 *
+	 * @return <code>/api/</code> URI
+	 * @apiNote URI is guaranteed to end on <code>/</code>
+	 */
 	public URI getApiBaseUrl() {
 		if (apiBaseUrl != null) {
 			// make sure to end on "/":

File diff suppressed because it is too large
+ 82 - 41
src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java


+ 22 - 3
src/main/java/org/cryptomator/ui/keyloading/hub/RegisterSuccessController.java

@@ -1,24 +1,43 @@
 package org.cryptomator.ui.keyloading.hub;
 
+import dagger.Lazy;
 import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
 import org.cryptomator.ui.keyloading.KeyLoading;
 
 import javax.inject.Inject;
 import javafx.fxml.FXML;
+import javafx.scene.Scene;
 import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import java.util.concurrent.CompletableFuture;
 
 public class RegisterSuccessController implements FxController {
 
 	private final Stage window;
+	private final CompletableFuture<ReceivedKey> result;
+	private final Lazy<Scene> receiveKeyScene;
+	private final ReceiveKeyController receiveKeyController;
 
 	@Inject
-	public RegisterSuccessController(@KeyLoading Stage window) {
+	public RegisterSuccessController(@KeyLoading Stage window, CompletableFuture<ReceivedKey> result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> receiveKeyScene, ReceiveKeyController receiveKeyController) {
 		this.window = window;
+		this.result = result;
+		this.receiveKeyScene = receiveKeyScene;
+		this.receiveKeyController = receiveKeyController;
+		this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
 	}
 
 	@FXML
-	public void close() {
-		window.close();
+	public void complete() {
+		window.setScene(receiveKeyScene.get());
+		receiveKeyController.receiveKey();
 	}
 
+	private void windowClosed(WindowEvent windowEvent) {
+		result.cancel(true);
+	}
+
+
 }

+ 5 - 0
src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java

@@ -1,6 +1,7 @@
 package org.cryptomator.ui.vaultoptions;
 
 import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultState;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy;
 import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
@@ -48,6 +49,10 @@ public class VaultOptionsController implements FxController {
 		if(!(vaultScheme.equals(HubKeyLoadingStrategy.SCHEME_HUB_HTTP) || vaultScheme.equals(HubKeyLoadingStrategy.SCHEME_HUB_HTTPS))){
 			tabPane.getTabs().remove(hubTab);
 		}
+
+		vault.stateProperty().addListener(observable -> {
+			tabPane.setDisable(vault.getState().equals(VaultState.Value.UNLOCKED));
+		});
 	}
 
 	private void selectChosenTab() {

+ 22 - 12
src/main/resources/fxml/addvault_new_location.fxml

@@ -1,11 +1,13 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <?import org.cryptomator.ui.controls.FontAwesome5IconView?>
+<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
 <?import javafx.geometry.Insets?>
 <?import javafx.scene.control.Button?>
 <?import javafx.scene.control.ButtonBar?>
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.control.RadioButton?>
+<?import javafx.scene.control.ScrollPane?>
 <?import javafx.scene.control.TextField?>
 <?import javafx.scene.control.ToggleGroup?>
 <?import javafx.scene.layout.HBox?>
@@ -29,18 +31,26 @@
 	<children>
 		<Region VBox.vgrow="ALWAYS"/>
 
-		<VBox fx:id="radioButtonVBox" spacing="6">
-			<Label wrapText="true" text="%addvaultwizard.new.locationInstruction"/>
-			<!-- PLACEHOLDER, more radio buttons are added programmatically via controller -->
-			<HBox spacing="12" alignment="CENTER_LEFT">
-				<RadioButton fx:id="customRadioButton" toggleGroup="${locationPresetsToggler}" text="%addvaultwizard.new.directoryPickerLabel"/>
-				<Button contentDisplay="LEFT" text="%addvaultwizard.new.directoryPickerButton" onAction="#chooseCustomVaultPath" disable="${controller.usePresetPath}">
-					<graphic>
-						<FontAwesome5IconView glyph="FOLDER_OPEN"/>
-					</graphic>
-				</Button>
-			</HBox>
-		</VBox>
+		<Label wrapText="true" text="%addvaultwizard.new.locationInstruction"/>
+		<ScrollPane hbarPolicy="NEVER">
+			<VBox fx:id="radioButtonVBox" spacing="6">
+				<!-- PLACEHOLDER, more radio buttons are added programmatically via controller -->
+				<HBox fx:id="customLocationRadioBtn" spacing="12" alignment="CENTER_LEFT">
+					<RadioButton fx:id="customRadioButton" toggleGroup="${locationPresetsToggler}" text="%addvaultwizard.new.directoryPickerLabel"/>
+					<Button contentDisplay="LEFT" text="%addvaultwizard.new.directoryPickerButton" onAction="#chooseCustomVaultPath" disable="${controller.usePresetPath}">
+						<graphic>
+							<FontAwesome5IconView glyph="FOLDER_OPEN"/>
+						</graphic>
+					</Button>
+				</HBox>
+			</VBox>
+		</ScrollPane>
+		<Region prefHeight="2"/>
+		<Label wrapText="true" text="%addvaultwizard.new.locationLoading" visible="${controller.loadingPresetLocations}" managed="${controller.loadingPresetLocations}" graphicTextGap="8">
+			<graphic>
+				<FontAwesome5Spinner/>
+			</graphic>
+		</Label>
 
 		<Region prefHeight="12" VBox.vgrow="NEVER"/>
 

+ 2 - 2
src/main/resources/fxml/hub_register_success.fxml

@@ -41,9 +41,9 @@
 			<Label text="%hub.registerSuccess.description" wrapText="true"/>
 
 			<Region VBox.vgrow="ALWAYS" minHeight="18"/>
-			<ButtonBar buttonMinWidth="120" buttonOrder="+C">
+			<ButtonBar buttonMinWidth="120" buttonOrder="+X">
 				<buttons>
-					<Button text="%generic.button.close" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="true" onAction="#close"/>
+					<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#complete"/>
 					<!-- TODO: add request access button -->
 				</buttons>
 			</ButtonBar>

+ 3 - 0
src/main/resources/i18n/strings.properties

@@ -48,6 +48,7 @@ addvaultwizard.new.nameInstruction=Choose a name for the vault
 addvaultwizard.new.namePrompt=Vault Name
 ### Location
 addvaultwizard.new.locationInstruction=Where should Cryptomator store the encrypted files of your vault?
+addvaultwizard.new.locationLoading=Checking local filesystem for default cloud storage directories…
 addvaultwizard.new.locationLabel=Storage location
 addvaultwizard.new.locationPrompt=…
 addvaultwizard.new.directoryPickerLabel=Custom location
@@ -162,6 +163,8 @@ hub.register.description=This is the first Hub access from this device. Please a
 hub.register.nameLabel=Device Name
 hub.register.invalidAccountKeyLabel=Invalid Account Key
 hub.register.registerBtn=Authorize
+### Register Device Legacy
+hub.register.occupiedMsg=Name already in use
 ### Registration Success
 hub.registerSuccess.message=Device registered
 hub.registerSuccess.description=To access the vault, your device needs to be authorized by the vault owner.