Kaynağa Gözat

Merge pull request #2918 from cryptomator/feature/refactor-location-presets

Feature: Refactor finding and showing cloud location presets
Armin Schrenk 1 yıl önce
ebeveyn
işleme
785cf7a9a6
19 değiştirilmiş dosya ile 644 ekleme ve 302 silme
  1. 22 0
      src/main/java/module-info.java
  2. 0 64
      src/main/java/org/cryptomator/common/LocationPreset.java
  3. 32 0
      src/main/java/org/cryptomator/common/locationpresets/DropboxLinuxLocationPresetsProvider.java
  4. 35 0
      src/main/java/org/cryptomator/common/locationpresets/DropboxMacLocationPresetsProvider.java
  5. 28 0
      src/main/java/org/cryptomator/common/locationpresets/DropboxWindowsLocationPresetsProvider.java
  6. 29 0
      src/main/java/org/cryptomator/common/locationpresets/GoogleDriveMacLocationPresetsProvider.java
  7. 28 0
      src/main/java/org/cryptomator/common/locationpresets/GoogleDriveWindowsLocationPresetsProvider.java
  8. 27 0
      src/main/java/org/cryptomator/common/locationpresets/ICloudMacLocationPresetsProvider.java
  9. 27 0
      src/main/java/org/cryptomator/common/locationpresets/ICloudWindowsLocationPresetsProvider.java
  10. 9 0
      src/main/java/org/cryptomator/common/locationpresets/LocationPreset.java
  11. 97 0
      src/main/java/org/cryptomator/common/locationpresets/LocationPresetsProvider.java
  12. 29 0
      src/main/java/org/cryptomator/common/locationpresets/MegaLocationPresetsProvider.java
  13. 28 0
      src/main/java/org/cryptomator/common/locationpresets/OneDriveLinuxLocationPresetsProvider.java
  14. 44 0
      src/main/java/org/cryptomator/common/locationpresets/OneDriveMacLocationPresetsProvider.java
  15. 108 0
      src/main/java/org/cryptomator/common/locationpresets/OneDriveWindowsLocationPresetsProvider.java
  16. 30 0
      src/main/java/org/cryptomator/common/locationpresets/PCloudLocationPresetsProvider.java
  17. 65 86
      src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java
  18. 0 141
      src/main/java/org/cryptomator/ui/addvaultwizard/ObservedLocationPresets.java
  19. 6 11
      src/main/resources/fxml/addvault_new_location.fxml

+ 22 - 0
src/main/java/module-info.java

@@ -1,4 +1,17 @@
 import ch.qos.logback.classic.spi.Configurator;
+import org.cryptomator.common.locationpresets.DropboxLinuxLocationPresetsProvider;
+import org.cryptomator.common.locationpresets.DropboxMacLocationPresetsProvider;
+import org.cryptomator.common.locationpresets.DropboxWindowsLocationPresetsProvider;
+import org.cryptomator.common.locationpresets.GoogleDriveWindowsLocationPresetsProvider;
+import org.cryptomator.common.locationpresets.GoogleDriveMacLocationPresetsProvider;
+import org.cryptomator.common.locationpresets.ICloudMacLocationPresetsProvider;
+import org.cryptomator.common.locationpresets.ICloudWindowsLocationPresetsProvider;
+import org.cryptomator.common.locationpresets.LocationPresetsProvider;
+import org.cryptomator.common.locationpresets.MegaLocationPresetsProvider;
+import org.cryptomator.common.locationpresets.OneDriveLinuxLocationPresetsProvider;
+import org.cryptomator.common.locationpresets.OneDriveMacLocationPresetsProvider;
+import org.cryptomator.common.locationpresets.OneDriveWindowsLocationPresetsProvider;
+import org.cryptomator.common.locationpresets.PCloudLocationPresetsProvider;
 import org.cryptomator.integrations.tray.TrayMenuController;
 import org.cryptomator.logging.LogbackConfiguratorFactory;
 import org.cryptomator.ui.traymenu.AwtTrayMenuController;
@@ -37,6 +50,15 @@ open module org.cryptomator.desktop {
 	/* TODO: filename-based modules: */
 	requires static javax.inject; /* ugly dagger/guava crap */
 
+	uses org.cryptomator.common.locationpresets.LocationPresetsProvider;
+
 	provides TrayMenuController with AwtTrayMenuController;
 	provides Configurator with LogbackConfiguratorFactory;
+	provides LocationPresetsProvider with DropboxMacLocationPresetsProvider, //
+			DropboxWindowsLocationPresetsProvider, DropboxLinuxLocationPresetsProvider, //
+			ICloudMacLocationPresetsProvider, ICloudWindowsLocationPresetsProvider, //
+			GoogleDriveWindowsLocationPresetsProvider, GoogleDriveMacLocationPresetsProvider, //
+			PCloudLocationPresetsProvider, MegaLocationPresetsProvider, //
+			OneDriveLinuxLocationPresetsProvider, OneDriveWindowsLocationPresetsProvider, //
+			OneDriveMacLocationPresetsProvider;
 }

+ 0 - 64
src/main/java/org/cryptomator/common/LocationPreset.java

@@ -1,64 +0,0 @@
-package org.cryptomator.common;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Enum of common cloud providers and their default local storage location path.
- */
-public enum LocationPreset {
-
-	DROPBOX("Dropbox", "~/Library/CloudStorage/Dropbox", "~/Dropbox"),
-	ICLOUDDRIVE("iCloud Drive", "~/Library/Mobile Documents/com~apple~CloudDocs", "~/iCloudDrive"),
-	GDRIVE("Google Drive", "~/Google Drive/My Drive", "~/Google Drive"),
-	MEGA("MEGA", "~/MEGA"),
-	ONEDRIVE("OneDrive", "~/OneDrive"),
-	PCLOUD("pCloud", "~/pCloudDrive"),
-
-	LOCAL("local");
-
-	private final String name;
-	private final List<Path> candidates;
-
-	LocationPreset(String name, String... candidates) {
-		this.name = name;
-		this.candidates = Arrays.stream(candidates).map(UserHome::resolve).map(Path::of).toList();
-	}
-
-	/**
-	 * Checks for this LocationPreset if any of the associated paths exist.
-	 *
-	 * @return the first existing path or null, if none exists.
-	 */
-	public Path existingPath() {
-		return candidates.stream().filter(Files::isDirectory).findFirst().orElse(null);
-	}
-
-	public String getDisplayName() {
-		return name;
-	}
-
-	@Override
-	public String toString() {
-		return getDisplayName();
-	}
-
-	//this contruct is needed, since static members are initialized after every enum member is initialized
-	//TODO: refactor this to normal class and use this also in different parts of the project
-	private static class UserHome {
-
-		private static final String USER_HOME = System.getProperty("user.home");
-
-		private static String resolve(String path) {
-			if (path.startsWith("~/")) {
-				return UserHome.USER_HOME + path.substring(1);
-			} else {
-				return path;
-			}
-		}
-	}
-
-}
-

+ 32 - 0
src/main/java/org/cryptomator/common/locationpresets/DropboxLinuxLocationPresetsProvider.java

@@ -0,0 +1,32 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.OperatingSystem;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import static org.cryptomator.integrations.common.OperatingSystem.Value.LINUX;
+
+@OperatingSystem(LINUX)
+public final class DropboxLinuxLocationPresetsProvider implements LocationPresetsProvider {
+
+	private static final Path USER_HOME = LocationPresetsProvider.resolveLocation("~/.").toAbsolutePath();
+	private static final Predicate<String> PATTERN = Pattern.compile("Dropbox \\(.+\\)").asMatchPredicate();
+
+	@Override
+	public Stream<LocationPreset> getLocations() {
+		try (var dirStream = Files.list(USER_HOME)) {
+			var presets = dirStream.filter(p -> Files.isDirectory(p) && PATTERN.test(p.getFileName().toString())) //
+					.map(p -> new LocationPreset(p.getFileName().toString(), p)) //
+					.toList();
+			return presets.stream(); //workaround to ensure that the directory stream is always closed
+		} catch (IOException | UncheckedIOException e) { //UncheckedIOException thrown by the stream of Files.list()
+			return Stream.of();
+		}
+	}
+}

+ 35 - 0
src/main/java/org/cryptomator/common/locationpresets/DropboxMacLocationPresetsProvider.java

@@ -0,0 +1,35 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.OperatingSystem;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import static org.cryptomator.integrations.common.OperatingSystem.Value.MAC;
+
+@OperatingSystem(MAC)
+@CheckAvailability
+public final class DropboxMacLocationPresetsProvider implements LocationPresetsProvider {
+
+	private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/Library/CloudStorage/Dropbox");
+	private static final Path FALLBACK_LOCATION = LocationPresetsProvider.resolveLocation("~/Dropbox");
+
+
+	@CheckAvailability
+	public static boolean isPresent() {
+		return Files.isDirectory(LOCATION) || Files.isDirectory(FALLBACK_LOCATION);
+	}
+
+	@Override
+	public Stream<LocationPreset> getLocations() {
+		if(Files.isDirectory(LOCATION)) {
+			return Stream.of(new LocationPreset("Dropbox", LOCATION));
+		} else if(Files.isDirectory(FALLBACK_LOCATION)) {
+			return Stream.of(new LocationPreset("Dropbox", FALLBACK_LOCATION));
+		} else {
+			return Stream.of();
+		}
+	}
+}

+ 28 - 0
src/main/java/org/cryptomator/common/locationpresets/DropboxWindowsLocationPresetsProvider.java

@@ -0,0 +1,28 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.OperatingSystem;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS;
+
+@OperatingSystem(WINDOWS)
+@CheckAvailability
+public final class DropboxWindowsLocationPresetsProvider implements LocationPresetsProvider {
+
+	private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/Dropbox");
+
+
+	@CheckAvailability
+	public static boolean isPresent() {
+		return Files.isDirectory(LOCATION);
+	}
+
+	@Override
+	public Stream<LocationPreset> getLocations() {
+		return Stream.of(new LocationPreset("Dropbox", LOCATION));
+	}
+}

+ 29 - 0
src/main/java/org/cryptomator/common/locationpresets/GoogleDriveMacLocationPresetsProvider.java

@@ -0,0 +1,29 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.OperatingSystem;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import static org.cryptomator.integrations.common.OperatingSystem.Value.MAC;
+import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS;
+
+@OperatingSystem(MAC)
+@CheckAvailability
+public final class GoogleDriveMacLocationPresetsProvider implements LocationPresetsProvider {
+
+	private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/Google Drive/My Drive");
+
+
+	@CheckAvailability
+	public static boolean isPresent() {
+		return Files.isDirectory(LOCATION);
+	}
+
+	@Override
+	public Stream<LocationPreset> getLocations() {
+		return Stream.of(new LocationPreset("Google Drive", LOCATION));
+	}
+}

+ 28 - 0
src/main/java/org/cryptomator/common/locationpresets/GoogleDriveWindowsLocationPresetsProvider.java

@@ -0,0 +1,28 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.OperatingSystem;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS;
+
+@OperatingSystem(WINDOWS)
+@CheckAvailability
+public final class GoogleDriveWindowsLocationPresetsProvider implements LocationPresetsProvider {
+
+	private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/Google Drive");
+
+
+	@CheckAvailability
+	public static boolean isPresent() {
+		return Files.isDirectory(LOCATION);
+	}
+
+	@Override
+	public Stream<LocationPreset> getLocations() {
+		return Stream.of(new LocationPreset("Google Drive", LOCATION));
+	}
+}

+ 27 - 0
src/main/java/org/cryptomator/common/locationpresets/ICloudMacLocationPresetsProvider.java

@@ -0,0 +1,27 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.OperatingSystem;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import static org.cryptomator.integrations.common.OperatingSystem.Value.MAC;
+
+@OperatingSystem(MAC)
+@CheckAvailability
+public final class ICloudMacLocationPresetsProvider implements LocationPresetsProvider {
+
+	private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/Library/Mobile Documents/com~apple~CloudDocs");
+
+	@CheckAvailability
+	public static boolean isPresent() {
+		return Files.isDirectory(LOCATION);
+	}
+
+	@Override
+	public Stream<LocationPreset> getLocations() {
+		return Stream.of(new LocationPreset("iCloud Drive", LOCATION));
+	}
+}

+ 27 - 0
src/main/java/org/cryptomator/common/locationpresets/ICloudWindowsLocationPresetsProvider.java

@@ -0,0 +1,27 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.OperatingSystem;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS;
+
+@OperatingSystem(WINDOWS)
+@CheckAvailability
+public final class ICloudWindowsLocationPresetsProvider implements LocationPresetsProvider {
+
+	private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/iCloudDrive");
+
+	@CheckAvailability
+	public static boolean isPresent() {
+		return Files.isDirectory(LOCATION);
+	}
+
+	@Override
+	public Stream<LocationPreset> getLocations() {
+		return Stream.of(new LocationPreset("iCloud Drive", LOCATION));
+	}
+}

+ 9 - 0
src/main/java/org/cryptomator/common/locationpresets/LocationPreset.java

@@ -0,0 +1,9 @@
+package org.cryptomator.common.locationpresets;
+
+import java.nio.file.Path;
+
+public record LocationPreset(String name, Path path) {
+
+
+
+}

+ 97 - 0
src/main/java/org/cryptomator/common/locationpresets/LocationPresetsProvider.java

@@ -0,0 +1,97 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.IntegrationsLoader;
+import org.cryptomator.integrations.common.OperatingSystem;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.ServiceLoader;
+import java.util.stream.Stream;
+
+public interface LocationPresetsProvider {
+
+	Logger LOG = LoggerFactory.getLogger(LocationPresetsProvider.class);
+	String USER_HOME = System.getProperty("user.home");
+
+	/**
+	 * Streams account-separated location presets found by this provider
+	 * @return Stream of LocationPresets
+	 */
+	Stream<LocationPreset> getLocations();
+
+	static Path resolveLocation(String p) {
+		if (p.startsWith("~/")) {
+			return Path.of(USER_HOME, p.substring(2));
+		} else {
+			return Path.of(p);
+		}
+	}
+
+	//copied from org.cryptomator.integrations.common.IntegrationsLoader
+	//TODO: delete, once migrated to integrations-api
+	static <T> Stream<T> loadAll(Class<T> clazz) {
+		return ServiceLoader.load(clazz)
+				.stream()
+				.filter(LocationPresetsProvider::isSupportedOperatingSystem)
+				.filter(LocationPresetsProvider::passesStaticAvailabilityCheck)
+				.map(ServiceLoader.Provider::get)
+				.peek(impl -> logServiceIsAvailable(clazz, impl.getClass()));
+	}
+
+
+	private static boolean isSupportedOperatingSystem(ServiceLoader.Provider<?> provider) {
+		var annotations = provider.type().getAnnotationsByType(OperatingSystem.class);
+		return annotations.length == 0 || Arrays.stream(annotations).anyMatch(OperatingSystem.Value::isCurrent);
+	}
+
+	private static boolean passesStaticAvailabilityCheck(ServiceLoader.Provider<?> provider) {
+		return passesStaticAvailabilityCheck(provider.type());
+	}
+
+	static boolean passesStaticAvailabilityCheck(Class<?> type) {
+		return passesAvailabilityCheck(type, null);
+	}
+
+	private static void logServiceIsAvailable(Class<?> apiType, Class<?> implType) {
+		if (LOG.isDebugEnabled()) {
+			LOG.debug("{}: Implementation is available: {}", apiType.getSimpleName(), implType.getName());
+		}
+	}
+
+	private static <T> boolean passesAvailabilityCheck(Class<? extends T> type, @Nullable T instance) {
+		if (!type.isAnnotationPresent(CheckAvailability.class)) {
+			return true; // if type is not annotated, skip tests
+		}
+		if (!type.getModule().isExported(type.getPackageName(), IntegrationsLoader.class.getModule())) {
+			LOG.error("Can't run @CheckAvailability tests for class {}. Make sure to export {} to {}!", type.getName(), type.getPackageName(), IntegrationsLoader.class.getPackageName());
+			return false;
+		}
+		return Arrays.stream(type.getMethods())
+				.filter(m -> isAvailabilityCheck(m, instance == null))
+				.allMatch(m -> passesAvailabilityCheck(m, instance));
+	}
+
+	private static boolean passesAvailabilityCheck(Method m, @Nullable Object instance) {
+		assert Boolean.TYPE.equals(m.getReturnType());
+		try {
+			return (boolean) m.invoke(instance);
+		} catch (ReflectiveOperationException e) {
+			LOG.warn("Failed to invoke @CheckAvailability test {}#{}", m.getDeclaringClass(), m.getName(), e);
+			return false;
+		}
+	}
+
+	private static boolean isAvailabilityCheck(Method m, boolean isStatic) {
+		return m.isAnnotationPresent(CheckAvailability.class)
+				&& Boolean.TYPE.equals(m.getReturnType())
+				&& m.getParameterCount() == 0
+				&& Modifier.isStatic(m.getModifiers()) == isStatic;
+	}
+
+}

+ 29 - 0
src/main/java/org/cryptomator/common/locationpresets/MegaLocationPresetsProvider.java

@@ -0,0 +1,29 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.OperatingSystem;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import static org.cryptomator.integrations.common.OperatingSystem.Value.MAC;
+import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS;
+
+@OperatingSystem(WINDOWS)
+@OperatingSystem(MAC)
+@CheckAvailability
+public final class MegaLocationPresetsProvider implements LocationPresetsProvider {
+
+	private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/MEGA");
+
+	@CheckAvailability
+	public static boolean isPresent() {
+		return Files.isDirectory(LOCATION);
+	}
+
+	@Override
+	public Stream<LocationPreset> getLocations() {
+		return Stream.of(new LocationPreset("MEGA", LOCATION));
+	}
+}

+ 28 - 0
src/main/java/org/cryptomator/common/locationpresets/OneDriveLinuxLocationPresetsProvider.java

@@ -0,0 +1,28 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.OperatingSystem;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import static org.cryptomator.integrations.common.OperatingSystem.Value.LINUX;
+
+@OperatingSystem(LINUX)
+@CheckAvailability
+public final class OneDriveLinuxLocationPresetsProvider implements LocationPresetsProvider {
+
+
+	private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/OneDrive");
+
+	@CheckAvailability
+	public static boolean isPresent() {
+		return Files.isDirectory(LOCATION);
+	}
+
+	@Override
+	public Stream<LocationPreset> getLocations() {
+		return Stream.of(new LocationPreset("OneDrive", LOCATION));
+	}
+}

+ 44 - 0
src/main/java/org/cryptomator/common/locationpresets/OneDriveMacLocationPresetsProvider.java

@@ -0,0 +1,44 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.OperatingSystem;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import static org.cryptomator.integrations.common.OperatingSystem.Value.MAC;
+
+@OperatingSystem(MAC)
+public final class OneDriveMacLocationPresetsProvider implements LocationPresetsProvider {
+
+	private static final Path FALLBACK_LOCATION = LocationPresetsProvider.resolveLocation("~/OneDrive");
+	private static final Path PARENT_LOCATION = LocationPresetsProvider.resolveLocation("~/Library/CloudStorage");
+
+	@Override
+	public Stream<LocationPreset> getLocations() {
+		var newLocations = getNewLocations().toList();
+		if (newLocations.size() >= 1) {
+			return newLocations.stream();
+		} else {
+			return getOldLocation();
+		}
+	}
+
+	private Stream<LocationPreset> getNewLocations() {
+		try (var dirStream = Files.newDirectoryStream(PARENT_LOCATION, "OneDrive*")) {
+			return StreamSupport.stream(dirStream.spliterator(), false) //
+					.filter(Files::isDirectory) //
+					.map(p -> new LocationPreset(String.join(" - ", p.getFileName().toString().split("-")), p));
+		} catch (IOException e) {
+			return Stream.of();
+		}
+	}
+
+	private Stream<LocationPreset> getOldLocation() {
+		return Stream.of(new LocationPreset("OneDrive", FALLBACK_LOCATION)).filter(preset -> Files.isDirectory(preset.path()));
+	}
+
+
+}

+ 108 - 0
src/main/java/org/cryptomator/common/locationpresets/OneDriveWindowsLocationPresetsProvider.java

@@ -0,0 +1,108 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.OperatingSystem;
+import org.jetbrains.annotations.Blocking;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS;
+
+@OperatingSystem(WINDOWS)
+public final class OneDriveWindowsLocationPresetsProvider implements LocationPresetsProvider {
+
+	private static final Logger LOG = LoggerFactory.getLogger(OneDriveWindowsLocationPresetsProvider.class);
+	private static final String REGSTR_TOKEN = "REG_SZ";
+	private static final String REG_ONEDRIVE_ACCOUNTS = "HKEY_CURRENT_USER\\Software\\Microsoft\\OneDrive\\Accounts\\";
+
+	@Override
+	public Stream<LocationPreset> getLocations() {
+		try {
+			var accountRegKeys = queryRegistry(REG_ONEDRIVE_ACCOUNTS, List.of(), l -> l.startsWith(REG_ONEDRIVE_ACCOUNTS)).toList();
+			var cloudLocations = new ArrayList<LocationPreset>();
+			for (var accountRegKey : accountRegKeys) {
+				var path = queryRegistry(accountRegKey, List.of("/v", "UserFolder"), l -> l.contains("UserFolder")).map(result -> result.substring(result.indexOf(REGSTR_TOKEN) + REGSTR_TOKEN.length()).trim()) //
+						.map(Path::of) //
+						.findFirst().orElseThrow();
+				var name = "OneDrive"; //we assume personal oneDrive account by default
+				if (!accountRegKey.endsWith("Personal")) {
+					name = queryRegistry(accountRegKey, List.of("/v", "DisplayName"), l -> l.contains("DisplayName")).map(result -> result.substring(result.indexOf(REGSTR_TOKEN) + REGSTR_TOKEN.length()).trim()) //
+							.map("OneDrive - "::concat) //
+							.findFirst().orElseThrow();
+				}
+				cloudLocations.add(new LocationPreset(name, path));
+			}
+			return cloudLocations.stream();
+		} catch (IOException | CommandFailedException | TimeoutException e) {
+			LOG.error("Unable to determine OneDrive location", e);
+			return Stream.of();
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+			LOG.error("Determination of OneDrive location interrupted", e);
+			return Stream.of();
+		}
+	}
+
+	private Stream<String> queryRegistry(String keyname, List<String> moreArgs, Predicate<String> outputFilter) throws InterruptedException, CommandFailedException, TimeoutException, IOException {
+		var args = new ArrayList<String>();
+		args.add("reg");
+		args.add("query");
+		args.add(keyname);
+		args.addAll(moreArgs);
+		ProcessBuilder command = new ProcessBuilder(args);
+		Process p = command.start();
+		waitForSuccess(p, 3, "`reg query`");
+		return p.inputReader(StandardCharsets.UTF_8).lines().filter(outputFilter);
+	}
+
+
+	/**
+	 * Waits {@code timeoutSeconds} seconds for {@code process} to finish with exit code {@code 0}.
+	 *
+	 * @param process The process to wait for
+	 * @param timeoutSeconds How long to wait (in seconds)
+	 * @param cmdDescription A short description of the process used to generate log and exception messages
+	 * @throws TimeoutException Thrown when the process doesn't finish in time
+	 * @throws InterruptedException Thrown when the thread is interrupted while waiting for the process to finish
+	 * @throws CommandFailedException Thrown when the process exit code is non-zero
+	 */
+	@Blocking
+	private static void waitForSuccess(Process process, int timeoutSeconds, String cmdDescription) throws TimeoutException, InterruptedException, CommandFailedException {
+		boolean exited = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
+		if (!exited) {
+			throw new TimeoutException(cmdDescription + " timed out after " + timeoutSeconds + "s");
+		}
+		if (process.exitValue() != 0) {
+			@SuppressWarnings("resource") var stdout = process.inputReader(StandardCharsets.UTF_8).lines().collect(Collectors.joining("\n"));
+			@SuppressWarnings("resource") var stderr = process.errorReader(StandardCharsets.UTF_8).lines().collect(Collectors.joining("\n"));
+			throw new CommandFailedException(cmdDescription, process.exitValue(), stdout, stderr);
+		}
+	}
+
+	private static class CommandFailedException extends Exception {
+
+		int exitCode;
+		String stdout;
+		String stderr;
+
+		private CommandFailedException(String cmdDescription, int exitCode, String stdout, String stderr) {
+			super(cmdDescription + " returned with non-zero exit code " + exitCode);
+			this.exitCode = exitCode;
+			this.stdout = stdout;
+			this.stderr = stderr;
+		}
+
+	}
+
+
+}

+ 30 - 0
src/main/java/org/cryptomator/common/locationpresets/PCloudLocationPresetsProvider.java

@@ -0,0 +1,30 @@
+package org.cryptomator.common.locationpresets;
+
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.OperatingSystem;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import static org.cryptomator.integrations.common.OperatingSystem.Value.MAC;
+import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS;
+
+@OperatingSystem(WINDOWS)
+@OperatingSystem(MAC)
+@CheckAvailability
+public final class PCloudLocationPresetsProvider implements LocationPresetsProvider {
+
+
+	private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/pCloudDrive");
+
+	@CheckAvailability
+	public static boolean isPresent() {
+		return Files.isDirectory(LOCATION);
+	}
+
+	@Override
+	public Stream<LocationPreset> getLocations() {
+		return Stream.of(new LocationPreset("pCloud", LOCATION));
+	}
+}

+ 65 - 86
src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java

@@ -1,6 +1,9 @@
 package org.cryptomator.ui.addvaultwizard;
 
 import dagger.Lazy;
+import org.cryptomator.common.ObservableUtil;
+import org.cryptomator.common.locationpresets.LocationPreset;
+import org.cryptomator.common.locationpresets.LocationPresetsProvider;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
@@ -10,22 +13,19 @@ import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
 import javax.inject.Named;
-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.SimpleObjectProperty;
-import javafx.beans.property.SimpleStringProperty;
 import javafx.beans.property.StringProperty;
 import javafx.beans.value.ObservableValue;
 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.VBox;
 import javafx.stage.DirectoryChooser;
 import javafx.stage.Stage;
 import java.io.File;
@@ -34,6 +34,9 @@ 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;
 
 @AddVaultWizardScoped
@@ -46,70 +49,72 @@ public class CreateNewVaultLocationController implements FxController {
 	private final Stage window;
 	private final Lazy<Scene> chooseNameScene;
 	private final Lazy<Scene> choosePasswordScene;
-	private final ObservedLocationPresets locationPresets;
+	private final List<RadioButton> locationPresetBtns;
 	private final ObjectProperty<Path> vaultPath;
 	private final StringProperty vaultName;
 	private final ResourceBundle resourceBundle;
-	private final BooleanBinding validVaultPath;
+	private final ObservableValue<VaultPathStatus> vaultPathStatus;
+	private final ObservableValue<Boolean> validVaultPath;
 	private final BooleanProperty usePresetPath;
-	private final StringProperty statusText;
-	private final ObjectProperty<Node> statusGraphic;
 
 	private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
 
 	//FXML
-	public ToggleGroup predefinedLocationToggler;
-	public RadioButton iclouddriveRadioButton;
-	public RadioButton dropboxRadioButton;
-	public RadioButton gdriveRadioButton;
-	public RadioButton onedriveRadioButton;
-	public RadioButton megaRadioButton;
-	public RadioButton pcloudRadioButton;
+	public ToggleGroup locationPresetsToggler;
+	public VBox radioButtonVBox;
 	public RadioButton customRadioButton;
-	public Label vaultPathStatus;
+	public Label locationStatusLabel;
 	public FontAwesome5IconView goodLocation;
 	public FontAwesome5IconView badLocation;
 
 	@Inject
-	CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ObservedLocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
+	CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
 		this.window = window;
 		this.chooseNameScene = chooseNameScene;
 		this.choosePasswordScene = choosePasswordScene;
-		this.locationPresets = locationPresets;
 		this.vaultPath = vaultPath;
 		this.vaultName = vaultName;
 		this.resourceBundle = resourceBundle;
-		this.validVaultPath = Bindings.createBooleanBinding(this::validateVaultPathAndSetStatus, this.vaultPath);
+		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.statusText = new SimpleStringProperty();
-		this.statusGraphic = new SimpleObjectProperty<>();
-	}
-
-	private boolean validateVaultPathAndSetStatus() {
-		final Path p = vaultPath.get();
-		if (p == null) {
-			statusText.set("Error: Path is NULL.");
-			statusGraphic.set(badLocation);
-			return false;
-		} else if (!Files.exists(p.getParent())) {
-			statusText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
-			statusGraphic.set(badLocation);
-			return false;
+		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();
+	}
+
+	private VaultPathStatus validatePath(Path p) throws NullPointerException {
+		if (!Files.exists(p.getParent())) {
+			return new VaultPathStatus(false, "addvaultwizard.new.locationDoesNotExist");
 		} else if (!isActuallyWritable(p.getParent())) {
-			statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsNotWritable"));
-			statusGraphic.set(badLocation);
-			return false;
+			return new VaultPathStatus(false, "addvaultwizard.new.locationIsNotWritable");
 		} else if (!Files.notExists(p)) {
-			statusText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
-			statusGraphic.set(badLocation);
-			return false;
+			return new VaultPathStatus(false, "addvaultwizard.new.fileAlreadyExists");
 		} else {
-			statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsOk"));
-			statusGraphic.set(goodLocation);
-			return true;
+			return new VaultPathStatus(true, "addvaultwizard.new.locationIsOk");
 		}
 	}
 
+	private void updateStatusLabel(ObservableValue<? extends VaultPathStatus> 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 tmpFile = p.resolve(TEMP_FILE_FORMAT);
 		try (var chan = Files.newByteChannel(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) {
@@ -127,26 +132,15 @@ public class CreateNewVaultLocationController implements FxController {
 
 	@FXML
 	public void initialize() {
-		predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
-		usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
+		radioButtonVBox.getChildren().addAll(1, locationPresetBtns); //first item is the list header
+		locationPresetsToggler.getToggles().addAll(locationPresetBtns);
+		locationPresetsToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
+		usePresetPath.bind(locationPresetsToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
 	}
 
 	private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
-		if (iclouddriveRadioButton.equals(newValue)) {
-			vaultPath.set(locationPresets.getIclouddriveLocation().resolve(vaultName.get()));
-		} else if (dropboxRadioButton.equals(newValue)) {
-			vaultPath.set(locationPresets.getDropboxLocation().resolve(vaultName.get()));
-		} else if (gdriveRadioButton.equals(newValue)) {
-			vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
-		} else if (onedriveRadioButton.equals(newValue)) {
-			vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
-		} else if (megaRadioButton.equals(newValue)) {
-			vaultPath.set(locationPresets.getMegaLocation().resolve(vaultName.get()));
-		} else if (pcloudRadioButton.equals(newValue)) {
-			vaultPath.set(locationPresets.getPcloudLocation().resolve(vaultName.get()));
-		} else if (customRadioButton.equals(newValue)) {
-			vaultPath.set(customVaultPath.resolve(vaultName.get()));
-		}
+		var storagePath = Optional.ofNullable((Path) newValue.getUserData()).orElse(customVaultPath);
+		vaultPath.set(storagePath.resolve(vaultName.get()));
 	}
 
 	@FXML
@@ -156,10 +150,8 @@ public class CreateNewVaultLocationController implements FxController {
 
 	@FXML
 	public void next() {
-		if (validateVaultPathAndSetStatus()) {
+		if (validVaultPath.getValue()) {
 			window.setScene(choosePasswordScene.get());
-		} else {
-			validVaultPath.invalidate();
 		}
 	}
 
@@ -179,6 +171,12 @@ public class CreateNewVaultLocationController implements FxController {
 		}
 	}
 
+	/* Internal classes */
+
+	private record VaultPathStatus(boolean valid, String localizationKey) {
+
+	}
+
 	/* Getter/Setter */
 
 	public Path getVaultPath() {
@@ -189,47 +187,28 @@ public class CreateNewVaultLocationController implements FxController {
 		return vaultPath;
 	}
 
-	public BooleanBinding validVaultPathProperty() {
+	public ObservableValue<Boolean> validVaultPathProperty() {
 		return validVaultPath;
 	}
 
-	public Boolean getValidVaultPath() {
-		return validVaultPath.get();
-	}
-
-	public ObservedLocationPresets getObservedLocationPresets() {
-		return locationPresets;
+	public boolean isValidVaultPath() {
+		return validVaultPath.getValue();
 	}
 
 	public BooleanProperty usePresetPathProperty() {
 		return usePresetPath;
 	}
 
-	public boolean getUsePresetPath() {
+	public boolean isUsePresetPath() {
 		return usePresetPath.get();
 	}
 
 	public BooleanBinding anyRadioButtonSelectedProperty() {
-		return predefinedLocationToggler.selectedToggleProperty().isNotNull();
+		return locationPresetsToggler.selectedToggleProperty().isNotNull();
 	}
 
 	public boolean isAnyRadioButtonSelected() {
 		return anyRadioButtonSelectedProperty().get();
 	}
 
-	public StringProperty statusTextProperty() {
-		return statusText;
-	}
-
-	public String getStatusText() {
-		return statusText.get();
-	}
-
-	public ObjectProperty<Node> statusGraphicProperty() {
-		return statusGraphic;
-	}
-
-	public Node getStatusGraphic() {
-		return statusGraphic.get();
-	}
 }

+ 0 - 141
src/main/java/org/cryptomator/ui/addvaultwizard/ObservedLocationPresets.java

@@ -1,141 +0,0 @@
-package org.cryptomator.ui.addvaultwizard;
-
-import org.cryptomator.common.LocationPreset;
-
-import javax.inject.Inject;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.property.ReadOnlyObjectProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import java.nio.file.Path;
-
-@AddVaultWizardScoped
-public class ObservedLocationPresets {
-
-	private final ReadOnlyObjectProperty<Path> iclouddriveLocation;
-	private final ReadOnlyObjectProperty<Path> dropboxLocation;
-	private final ReadOnlyObjectProperty<Path> gdriveLocation;
-	private final ReadOnlyObjectProperty<Path> onedriveLocation;
-	private final ReadOnlyObjectProperty<Path> megaLocation;
-	private final ReadOnlyObjectProperty<Path> pcloudLocation;
-	private final BooleanBinding foundIclouddrive;
-	private final BooleanBinding foundDropbox;
-	private final BooleanBinding foundGdrive;
-	private final BooleanBinding foundOnedrive;
-	private final BooleanBinding foundMega;
-	private final BooleanBinding foundPcloud;
-
-	@Inject
-	public ObservedLocationPresets() {
-		this.iclouddriveLocation = new SimpleObjectProperty<>(LocationPreset.ICLOUDDRIVE.existingPath());
-		this.dropboxLocation = new SimpleObjectProperty<>(LocationPreset.DROPBOX.existingPath());
-		this.gdriveLocation = new SimpleObjectProperty<>(LocationPreset.GDRIVE.existingPath());
-		this.onedriveLocation = new SimpleObjectProperty<>(LocationPreset.ONEDRIVE.existingPath());
-		this.megaLocation = new SimpleObjectProperty<>(LocationPreset.MEGA.existingPath());
-		this.pcloudLocation = new SimpleObjectProperty<>(LocationPreset.PCLOUD.existingPath());
-		this.foundIclouddrive = iclouddriveLocation.isNotNull();
-		this.foundDropbox = dropboxLocation.isNotNull();
-		this.foundGdrive = gdriveLocation.isNotNull();
-		this.foundOnedrive = onedriveLocation.isNotNull();
-		this.foundMega = megaLocation.isNotNull();
-		this.foundPcloud = pcloudLocation.isNotNull();
-	}
-
-	/* Observables */
-
-	public ReadOnlyObjectProperty<Path> iclouddriveLocationProperty() {
-		return iclouddriveLocation;
-	}
-
-	public Path getIclouddriveLocation() {
-		return iclouddriveLocation.get();
-	}
-
-	public BooleanBinding foundIclouddriveProperty() {
-		return foundIclouddrive;
-	}
-
-	public boolean isFoundIclouddrive() {
-		return foundIclouddrive.get();
-	}
-
-	public ReadOnlyObjectProperty<Path> dropboxLocationProperty() {
-		return dropboxLocation;
-	}
-
-	public Path getDropboxLocation() {
-		return dropboxLocation.get();
-	}
-
-	public BooleanBinding foundDropboxProperty() {
-		return foundDropbox;
-	}
-
-	public boolean isFoundDropbox() {
-		return foundDropbox.get();
-	}
-
-	public ReadOnlyObjectProperty<Path> gdriveLocationProperty() {
-		return gdriveLocation;
-	}
-
-	public Path getGdriveLocation() {
-		return gdriveLocation.get();
-	}
-
-	public BooleanBinding foundGdriveProperty() {
-		return foundGdrive;
-	}
-
-	public boolean isFoundGdrive() {
-		return foundGdrive.get();
-	}
-
-	public ReadOnlyObjectProperty<Path> onedriveLocationProperty() {
-		return onedriveLocation;
-	}
-
-	public Path getOnedriveLocation() {
-		return onedriveLocation.get();
-	}
-
-	public BooleanBinding foundOnedriveProperty() {
-		return foundOnedrive;
-	}
-
-	public boolean isFoundOnedrive() {
-		return foundOnedrive.get();
-	}
-
-	public ReadOnlyObjectProperty<Path> megaLocationProperty() {
-		return megaLocation;
-	}
-
-	public Path getMegaLocation() {
-		return megaLocation.get();
-	}
-
-	public BooleanBinding foundMegaProperty() {
-		return foundMega;
-	}
-
-	public boolean isFoundMega() {
-		return foundMega.get();
-	}
-
-	public ReadOnlyObjectProperty<Path> pcloudLocationProperty() {
-		return pcloudLocation;
-	}
-
-	public Path getPcloudLocation() {
-		return pcloudLocation.get();
-	}
-
-	public BooleanBinding foundPcloudProperty() {
-		return foundPcloud;
-	}
-
-	public boolean isFoundPcloud() {
-		return foundPcloud.get();
-	}
-
-}

+ 6 - 11
src/main/resources/fxml/addvault_new_location.fxml

@@ -19,7 +19,7 @@
 	  spacing="12"
 	  alignment="CENTER_LEFT">
 	<fx:define>
-		<ToggleGroup fx:id="predefinedLocationToggler"/>
+		<ToggleGroup fx:id="locationPresetsToggler"/>
 		<FontAwesome5IconView fx:id="badLocation" styleClass="glyph-icon-red" glyph="TIMES" />
 		<FontAwesome5IconView fx:id="goodLocation" styleClass="glyph-icon-primary" glyph="CHECK" />
 	</fx:define>
@@ -29,16 +29,11 @@
 	<children>
 		<Region VBox.vgrow="ALWAYS"/>
 
-		<VBox spacing="6">
+		<VBox fx:id="radioButtonVBox" spacing="6">
 			<Label wrapText="true" text="%addvaultwizard.new.locationInstruction"/>
-			<RadioButton fx:id="iclouddriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="iCloud Drive" visible="${controller.observedLocationPresets.foundIclouddrive}" managed="${controller.observedLocationPresets.foundIclouddrive}"/>
-			<RadioButton fx:id="dropboxRadioButton" toggleGroup="${predefinedLocationToggler}" text="Dropbox" visible="${controller.observedLocationPresets.foundDropbox}" managed="${controller.observedLocationPresets.foundDropbox}"/>
-			<RadioButton fx:id="gdriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="Google Drive" visible="${controller.observedLocationPresets.foundGdrive}" managed="${controller.observedLocationPresets.foundGdrive}"/>
-			<RadioButton fx:id="onedriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="OneDrive" visible="${controller.observedLocationPresets.foundOnedrive}" managed="${controller.observedLocationPresets.foundOnedrive}"/>
-			<RadioButton fx:id="megaRadioButton" toggleGroup="${predefinedLocationToggler}" text="MEGA" visible="${controller.observedLocationPresets.foundMega}" managed="${controller.observedLocationPresets.foundMega}"/>
-			<RadioButton fx:id="pcloudRadioButton" toggleGroup="${predefinedLocationToggler}" text="pCloud" visible="${controller.observedLocationPresets.foundPcloud}" managed="${controller.observedLocationPresets.foundPcloud}"/>
+			<!-- PLACEHOLDER, more radio buttons are added programmatically via controller -->
 			<HBox spacing="12" alignment="CENTER_LEFT">
-				<RadioButton fx:id="customRadioButton" toggleGroup="${predefinedLocationToggler}" text="%addvaultwizard.new.directoryPickerLabel"/>
+				<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"/>
@@ -51,8 +46,8 @@
 
 		<VBox spacing="6">
 			<Label text="%addvaultwizard.new.locationLabel" labelFor="$locationTextField"/>
-			<TextField promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" editable="false" disable="${!controller.anyRadioButtonSelected}" HBox.hgrow="ALWAYS"/>
-			<Label fx:id="vaultPathStatus" styleClass="label-muted" alignment="CENTER_RIGHT" wrapText="true" visible="${controller.anyRadioButtonSelected}" maxWidth="Infinity" graphicTextGap="6" text="${controller.statusText}" graphic="${controller.statusGraphic}" />
+			<TextField fx:id="locationTextField" promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" editable="false" disable="${!controller.anyRadioButtonSelected}" HBox.hgrow="ALWAYS"/>
+			<Label fx:id="locationStatusLabel" alignment="CENTER_RIGHT" wrapText="true" visible="${controller.anyRadioButtonSelected}" maxWidth="Infinity" graphicTextGap="6" />
 		</VBox>
 
 		<Region VBox.vgrow="ALWAYS"/>