ソースを参照

Merge branch 'release/1.10.1'

Armin Schrenk 1 年間 前
コミット
bbe7255901

+ 2 - 1
.github/ISSUE_TEMPLATE/bug.yml

@@ -43,6 +43,7 @@ body:
         - WinFsp (Local Drive)
         - FUSE-T
         - macFUSE
+        - FUSE
         - WebDAV (Windows Explorer)
         - WebDAV (AppleScript)
         - WebDAV (gio)
@@ -95,4 +96,4 @@ body:
     id: further-info
     attributes:
       label: Anything else?
-      description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue?
+      description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue?

+ 2 - 1
.github/workflows/debian.yml

@@ -97,7 +97,8 @@ jobs:
         run: |
           cp -r dist/linux/debian/ pkgdir
           export RFC2822_TIMESTAMP=`date --rfc-2822`
-          envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM}' < dist/linux/debian/rules > pkgdir/debian/rules
+          export DISABLE_UPDATE_CHECK=${{ inputs.dput }}
+          envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM} ${DISABLE_UPDATE_CHECK}' < dist/linux/debian/rules > pkgdir/debian/rules
           envsubst '${PPA_VERSION} ${RFC2822_TIMESTAMP}' < dist/linux/debian/changelog > pkgdir/debian/changelog
           find . -name "*.jar" >> pkgdir/debian/source/include-binaries
           mv pkgdir cryptomator_${{ inputs.ppaver }}

+ 1 - 0
dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml

@@ -66,6 +66,7 @@
 	</content_rating>
 
 	<releases>
+		<release date="2023-09-20" version="1.10.1"/>
 		<release date="2023-09-11" version="1.10.0"/>
 		<release date="2023-08-11" version="1.9.4"/>
 		<release date="2023-08-07" version="1.9.3"/>

+ 1 - 0
dist/linux/debian/rules

@@ -59,6 +59,7 @@ override_dh_auto_build:
 		--java-options "-Dcryptomator.integrationsLinux.trayIconsDir=\"/usr/share/icons/hicolor/symbolic/apps\"" \
 		--java-options "-Dcryptomator.buildNumber=\"deb-${REVISION_NUM}\"" \
 		--java-options "-Dcryptomator.appVersion=\"${SEMVER_STR}\"" \
+		--java-options "-Dcryptomator.disableUpdateCheck=\"${DISABLE_UPDATE_CHECK}\"" \
 		--app-version "${VERSION_NUM}.${REVISION_NUM}" \
 		--resource-dir resources \
 		--verbose

+ 5 - 5
pom.xml

@@ -3,7 +3,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>org.cryptomator</groupId>
 	<artifactId>cryptomator</artifactId>
-	<version>1.10.0</version>
+	<version>1.10.1</version>
 	<name>Cryptomator Desktop App</name>
 
 	<organization>
@@ -35,12 +35,12 @@
 		<!-- cryptomator dependencies -->
 		<cryptomator.cryptofs.version>2.6.7</cryptomator.cryptofs.version>
 		<cryptomator.integrations.version>1.3.0</cryptomator.integrations.version>
-		<cryptomator.integrations.win.version>1.2.2</cryptomator.integrations.win.version>
-		<cryptomator.integrations.mac.version>1.2.1</cryptomator.integrations.mac.version>
-		<cryptomator.integrations.linux.version>1.3.0-beta6</cryptomator.integrations.linux.version>
+		<cryptomator.integrations.win.version>1.2.3</cryptomator.integrations.win.version>
+		<cryptomator.integrations.mac.version>1.2.2</cryptomator.integrations.mac.version>
+		<cryptomator.integrations.linux.version>1.3.0</cryptomator.integrations.linux.version>
 		<cryptomator.fuse.version>3.0.0</cryptomator.fuse.version>
 		<cryptomator.dokany.version>2.0.0</cryptomator.dokany.version>
-		<cryptomator.webdav.version>2.0.3</cryptomator.webdav.version>
+		<cryptomator.webdav.version>2.0.4</cryptomator.webdav.version>
 
 		<!-- 3rd party dependencies -->
 		<commons-lang3.version>3.13.0</commons-lang3.version>

+ 20 - 13
src/main/java/org/cryptomator/common/Environment.java

@@ -2,6 +2,7 @@ package org.cryptomator.common;
 
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
+import org.jetbrains.annotations.VisibleForTesting;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -31,6 +32,7 @@ public class Environment {
 	private static final String BUILD_NUMBER_PROP_NAME = "cryptomator.buildNumber";
 	private static final String PLUGIN_DIR_PROP_NAME = "cryptomator.pluginDir";
 	private static final String TRAY_ICON_PROP_NAME = "cryptomator.showTrayIcon";
+	private static final String DISABLE_UPDATE_CHECK_PROP_NAME = "cryptomator.disableUpdateCheck";
 
 	private Environment() {}
 
@@ -43,15 +45,16 @@ public class Environment {
 		logCryptomatorSystemProperty(SETTINGS_PATH_PROP_NAME);
 		logCryptomatorSystemProperty(IPC_SOCKET_PATH_PROP_NAME);
 		logCryptomatorSystemProperty(KEYCHAIN_PATHS_PROP_NAME);
+		logCryptomatorSystemProperty(P12_PATH_PROP_NAME);
 		logCryptomatorSystemProperty(LOG_DIR_PROP_NAME);
 		logCryptomatorSystemProperty(LOOPBACK_ALIAS_PROP_NAME);
-		logCryptomatorSystemProperty(PLUGIN_DIR_PROP_NAME);
 		logCryptomatorSystemProperty(MOUNTPOINT_DIR_PROP_NAME);
 		logCryptomatorSystemProperty(MIN_PW_LENGTH_PROP_NAME);
 		logCryptomatorSystemProperty(APP_VERSION_PROP_NAME);
 		logCryptomatorSystemProperty(BUILD_NUMBER_PROP_NAME);
+		logCryptomatorSystemProperty(PLUGIN_DIR_PROP_NAME);
 		logCryptomatorSystemProperty(TRAY_ICON_PROP_NAME);
-		logCryptomatorSystemProperty(P12_PATH_PROP_NAME);
+		logCryptomatorSystemProperty(DISABLE_UPDATE_CHECK_PROP_NAME);
 	}
 
 	public static Environment getInstance() {
@@ -74,10 +77,6 @@ public class Environment {
 		return getPaths(SETTINGS_PATH_PROP_NAME);
 	}
 
-	public Stream<Path> getP12Path() {
-		return getPaths(P12_PATH_PROP_NAME);
-	}
-
 	public Stream<Path> getIpcSocketPath() {
 		return getPaths(IPC_SOCKET_PATH_PROP_NAME);
 	}
@@ -86,6 +85,10 @@ public class Environment {
 		return getPaths(KEYCHAIN_PATHS_PROP_NAME);
 	}
 
+	public Stream<Path> getP12Path() {
+		return getPaths(P12_PATH_PROP_NAME);
+	}
+
 	public Optional<Path> getLogDir() {
 		return getPath(LOG_DIR_PROP_NAME);
 	}
@@ -94,14 +97,14 @@ public class Environment {
 		return Optional.ofNullable(System.getProperty(LOOPBACK_ALIAS_PROP_NAME));
 	}
 
-	public Optional<Path> getPluginDir() {
-		return getPath(PLUGIN_DIR_PROP_NAME);
-	}
-
 	public Optional<Path> getMountPointsDir() {
 		return getPath(MOUNTPOINT_DIR_PROP_NAME);
 	}
 
+	public int getMinPwLength() {
+		return Integer.getInteger(MIN_PW_LENGTH_PROP_NAME, DEFAULT_MIN_PW_LENGTH);
+	}
+
 	/**
 	 * Returns the app version defined in the {@value APP_VERSION_PROP_NAME} property or returns "SNAPSHOT".
 	 *
@@ -115,20 +118,24 @@ public class Environment {
 		return Optional.ofNullable(System.getProperty(BUILD_NUMBER_PROP_NAME));
 	}
 
-	public int getMinPwLength() {
-		return Integer.getInteger(MIN_PW_LENGTH_PROP_NAME, DEFAULT_MIN_PW_LENGTH);
+	public Optional<Path> getPluginDir() {
+		return getPath(PLUGIN_DIR_PROP_NAME);
 	}
 
 	public boolean showTrayIcon() {
 		return Boolean.getBoolean(TRAY_ICON_PROP_NAME);
 	}
 
+	public boolean disableUpdateCheck() {
+		return Boolean.getBoolean(DISABLE_UPDATE_CHECK_PROP_NAME);
+	}
+
 	private Optional<Path> getPath(String propertyName) {
 		String value = System.getProperty(propertyName);
 		return Optional.ofNullable(value).map(Paths::get);
 	}
 
-	// visible for testing
+	@VisibleForTesting
 	Stream<Path> getPaths(String propertyName) {
 		Stream<String> rawSettingsPaths = getRawList(propertyName, System.getProperty("path.separator").charAt(0));
 		return rawSettingsPaths.filter(Predicate.not(Strings::isNullOrEmpty)).map(Path::of);

+ 3 - 2
src/main/java/org/cryptomator/common/ErrorCode.java

@@ -3,6 +3,7 @@ package org.cryptomator.common;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
+import org.jetbrains.annotations.VisibleForTesting;
 
 import java.util.Locale;
 import java.util.Objects;
@@ -114,7 +115,7 @@ public class ErrorCode {
 	 * @param bottomFrames Other stack frames, potentially forming the bottom of the stack of <code>allFrames</code>
 	 * @return The number of additional frames in <code>allFrames</code>. In most cases this should be equal to the difference in size.
 	 */
-	// visible for testing
+	@VisibleForTesting
 	static int countTopmostFrames(StackTraceElement[] allFrames, StackTraceElement[] bottomFrames) {
 		if (allFrames.length < bottomFrames.length) {
 			// if frames had been stacked on top of bottomFrames, allFrames would be larger
@@ -124,7 +125,7 @@ public class ErrorCode {
 		}
 	}
 
-	// visible for testing
+	@VisibleForTesting
 	static <T> int commonSuffixLength(T[] set, T[] subset) {
 		Preconditions.checkArgument(set.length >= subset.length);
 		// iterate items backwards as long as they are identical

+ 5 - 4
src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java

@@ -1,6 +1,7 @@
 package org.cryptomator.common.mount;
 
 import org.apache.commons.lang3.SystemUtils;
+import org.jetbrains.annotations.VisibleForTesting;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -66,7 +67,7 @@ public final class MountWithinParentUtil {
 		}
 	}
 
-	//visible for testing
+	@VisibleForTesting
 	static MountPointState getMountPointState(Path path) throws IOException, IllegalMountPointException {
 		if (Files.notExists(path, LinkOption.NOFOLLOW_LINKS)) {
 			return MountPointState.NOT_EXISTING;
@@ -82,7 +83,7 @@ public final class MountWithinParentUtil {
 		return MountPointState.BROKEN_JUNCTION;
 	}
 
-	//visible for testing
+	@VisibleForTesting
 	enum MountPointState {
 
 		NOT_EXISTING,
@@ -93,7 +94,7 @@ public final class MountWithinParentUtil {
 
 	}
 
-	//visible for testing
+	@VisibleForTesting
 	static void removeResidualHideaway(Path mountPoint, Path hideaway) throws IOException {
 		checkIsHideawayDirectory(mountPoint, hideaway);
 		Files.delete(hideaway); //Fails if not empty
@@ -155,7 +156,7 @@ public final class MountWithinParentUtil {
 		}
 	}
 
-	//visible for testing
+	@VisibleForTesting
 	static Path getHideaway(Path mountPoint) {
 		return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX);
 	}

+ 2 - 1
src/main/java/org/cryptomator/common/settings/VaultSettings.java

@@ -9,6 +9,7 @@ import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import com.google.common.io.BaseEncoding;
 import org.apache.commons.lang3.SystemUtils;
+import org.jetbrains.annotations.VisibleForTesting;
 
 import javafx.beans.Observable;
 import javafx.beans.binding.Bindings;
@@ -126,7 +127,7 @@ public class VaultSettings {
 		return json;
 	}
 
-	//visible for testing
+	@VisibleForTesting
 	static String normalizeDisplayName(String original) {
 		if (original.isBlank() || ".".equals(original) || "..".equals(original)) {
 			return "_";

+ 2 - 1
src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java

@@ -6,6 +6,7 @@
  *******************************************************************************/
 package org.cryptomator.launcher;
 
+import org.jetbrains.annotations.VisibleForTesting;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -48,7 +49,7 @@ class FileOpenRequestHandler {
 		handleLaunchArgs(FileSystems.getDefault(), args);
 	}
 
-	// visible for testing
+	@VisibleForTesting
 	void handleLaunchArgs(FileSystem fs, List<String> args) {
 		Collection<Path> pathsToOpen = args.stream().map(str -> {
 			try {

+ 4 - 2
src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java

@@ -1,5 +1,7 @@
 package org.cryptomator.ui.addvaultwizard;
 
+import org.jetbrains.annotations.VisibleForTesting;
+
 import javax.inject.Inject;
 import java.util.List;
 import java.util.ResourceBundle;
@@ -51,7 +53,7 @@ public class ReadmeGenerator {
 				resourceBundle.getString("addvault.new.readme.accessLocation.4")));
 	}
 
-	// visible for testing
+	@VisibleForTesting
 	String createDocument(Iterable<String> paragraphs) {
 		StringBuilder sb = new StringBuilder(RTF_HEADER);
 		for (String p : paragraphs) {
@@ -63,7 +65,7 @@ public class ReadmeGenerator {
 		return sb.toString();
 	}
 
-	// visible for testing
+	@VisibleForTesting
 	String escapeNonAsciiChars(CharSequence input) {
 		StringBuilder sb = new StringBuilder();
 		appendEscaped(sb, input);

+ 4 - 3
src/main/java/org/cryptomator/ui/convertvault/HubToPasswordConvertController.java

@@ -16,6 +16,7 @@ import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
 import org.cryptomator.ui.fxapp.FxApplicationWindows;
 import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
+import org.jetbrains.annotations.VisibleForTesting;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -116,7 +117,7 @@ public class HubToPasswordConvertController implements FxController {
 				}, Platform::runLater); //
 	}
 
-	//visible for testing
+	@VisibleForTesting
 	void convertInternal() throws CompletionException, IllegalArgumentException {
 		var passphrase = newPasswordController.getNewPassword();
 		var vaultPath = vault.getPath();
@@ -141,7 +142,7 @@ public class HubToPasswordConvertController implements FxController {
 		}
 	}
 
-	//visible for testing
+	@VisibleForTesting
 	void backupHubConfig(Path hubConfigPath) throws IOException {
 		byte[] hubConfigBytes = Files.readAllBytes(hubConfigPath);
 		Path backupPath = hubConfigPath.resolveSibling(VAULTCONFIG_FILENAME + BackupHelper.generateFileIdSuffix(hubConfigBytes) + MASTERKEY_BACKUP_SUFFIX);
@@ -149,7 +150,7 @@ public class HubToPasswordConvertController implements FxController {
 		LOG.debug("Successfully created hub config backup {}", backupPath.getFileName());
 	}
 
-	//visible for testing
+	@VisibleForTesting
 	Path createPasswordConfig(Path passwordConfigPath, Path masterkeyFile, Passphrase passphrase) throws IOException, MasterkeyLoadingFailedException {
 		var unverifiedVaultConfig = vault.getVaultConfigCache().get();
 		try (var masterkey = masterkeyFileAccess.load(masterkeyFile, passphrase)) {

+ 10 - 2
src/main/java/org/cryptomator/ui/error/ErrorController.java

@@ -42,7 +42,8 @@ public class ErrorController implements FxController {
 
 	private static final ObjectMapper JSON = new ObjectMapper();
 	private static final Logger LOG = LoggerFactory.getLogger(ErrorController.class);
-	private static final String ERROR_CODES_URL = "https://api.cryptomator.org/desktop/error-codes.json";
+	private static final String USER_AGENT_FORMAT = "Cryptomator/%s (Build %s) (%s %s %s)";
+	private static final String ERROR_CODES_URL_FORMAT = "https://api.cryptomator.org/desktop/error-codes.json?error-code=%s";
 	private static final String SEARCH_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/categories/errors?discussions_q=category:Errors+%s";
 	private static final String REPORT_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/new?category=Errors&title=Error+%s&body=%s";
 	private static final String SEARCH_ERRORCODE_DELIM = " OR ";
@@ -142,11 +143,18 @@ public class ErrorController implements FxController {
 
 	@FXML
 	public void lookUpSolution() {
+		String userAgent = USER_AGENT_FORMAT.formatted( //
+				environment.getAppVersion(), //
+				environment.getBuildNumber().orElse("undefined"), //
+				System.getProperty("os.name"), //
+				System.getProperty("os.version"), //
+				System.getProperty("os.arch"));
 		isLoadingHttpResponse.set(true);
 		askedForLookupDatabasePermission.set(true);
 		HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
 		HttpRequest httpRequest = HttpRequest.newBuilder()//
-				.uri(URI.create(ERROR_CODES_URL))//
+				.header("User-Agent", userAgent)
+				.uri(URI.create(ERROR_CODES_URL_FORMAT.formatted(URLEncoder.encode(errorCode.toString(),StandardCharsets.UTF_8))))//
 				.build();
 		httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofInputStream())//
 				.thenAcceptAsync(this::loadHttpResponse, executorService)//

+ 7 - 2
src/main/java/org/cryptomator/ui/fxapp/FxApplication.java

@@ -1,6 +1,7 @@
 package org.cryptomator.ui.fxapp;
 
 import dagger.Lazy;
+import org.cryptomator.common.Environment;
 import org.cryptomator.common.settings.Settings;
 import org.cryptomator.ui.traymenu.TrayMenuComponent;
 import org.slf4j.Logger;
@@ -17,6 +18,7 @@ public class FxApplication {
 	private static final Logger LOG = LoggerFactory.getLogger(FxApplication.class);
 
 	private final long startupTime;
+	private final Environment environment;
 	private final Settings settings;
 	private final AppLaunchEventHandler launchEventHandler;
 	private final Lazy<TrayMenuComponent> trayMenu;
@@ -26,8 +28,9 @@ public class FxApplication {
 	private final AutoUnlocker autoUnlocker;
 
 	@Inject
-	FxApplication(@Named("startupTime") long startupTime, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy<TrayMenuComponent> trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker) {
+	FxApplication(@Named("startupTime") long startupTime, Environment environment, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy<TrayMenuComponent> trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker) {
 		this.startupTime = startupTime;
+		this.environment = environment;
 		this.settings = settings;
 		this.launchEventHandler = launchEventHandler;
 		this.trayMenu = trayMenu;
@@ -68,7 +71,9 @@ public class FxApplication {
 			return null;
 		});
 
-		appWindows.checkAndShowUpdateReminderWindow();
+		if (!environment.disableUpdateCheck()) {
+			appWindows.checkAndShowUpdateReminderWindow();
+		}
 
 		launchEventHandler.startHandlingLaunchEvents();
 		autoUnlocker.tryUnlockForTimespan(2, TimeUnit.MINUTES);

+ 6 - 6
src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java

@@ -22,23 +22,23 @@ public class UpdateChecker {
 	private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class);
 	private static final Duration AUTOCHECK_DELAY = Duration.seconds(5);
 
+	private final Environment env;
 	private final Settings settings;
-	private final String currentVersion;
 	private final StringProperty latestVersionProperty;
 	private final Comparator<String> semVerComparator;
 	private final ScheduledService<String> updateCheckerService;
 
 	@Inject
 	UpdateChecker(Settings settings, Environment env, @Named("latestVersion") StringProperty latestVersionProperty, @Named("SemVer") Comparator<String> semVerComparator, ScheduledService<String> updateCheckerService) {
+		this.env = env;
 		this.settings = settings;
 		this.latestVersionProperty = latestVersionProperty;
 		this.semVerComparator = semVerComparator;
 		this.updateCheckerService = updateCheckerService;
-		this.currentVersion = env.getAppVersion();
 	}
 
 	public void automaticallyCheckForUpdatesIfEnabled() {
-		if (settings.checkForUpdates.get()) {
+		if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) {
 			startCheckingForUpdates(AUTOCHECK_DELAY);
 		}
 	}
@@ -63,9 +63,9 @@ public class UpdateChecker {
 
 	private void checkSucceeded(WorkerStateEvent event) {
 		String latestVersion = updateCheckerService.getValue();
-		LOG.info("Current version: {}, lastest version: {}", currentVersion, latestVersion);
+		LOG.info("Current version: {}, lastest version: {}", getCurrentVersion(), latestVersion);
 
-		if (semVerComparator.compare(currentVersion, latestVersion) < 0) {
+		if (semVerComparator.compare(getCurrentVersion(), latestVersion) < 0) {
 			// update is available
 			latestVersionProperty.set(latestVersion);
 		} else {
@@ -88,7 +88,7 @@ public class UpdateChecker {
 	}
 
 	public String getCurrentVersion() {
-		return currentVersion;
+		return env.getAppVersion();
 	}
 
 }

+ 10 - 7
src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java

@@ -20,9 +20,10 @@ import javafx.beans.property.SimpleBooleanProperty;
 import javafx.beans.value.ObservableValue;
 import javafx.collections.ListChangeListener;
 import javafx.collections.ObservableList;
-import javafx.event.Event;
 import javafx.fxml.FXML;
+import javafx.geometry.Side;
 import javafx.scene.control.Button;
+import javafx.scene.control.ContextMenu;
 import javafx.scene.control.ListView;
 import javafx.scene.input.ContextMenuEvent;
 import javafx.scene.input.DragEvent;
@@ -67,6 +68,8 @@ public class VaultListController implements FxController {
 	public ListView<Vault> vaultList;
 	public StackPane root;
 	public Button addVaultBtn;
+	@FXML
+	private ContextMenu addVaultContextMenu;
 
 	@Inject
 	VaultListController(@MainWindow Stage mainWindow, //
@@ -140,15 +143,15 @@ public class VaultListController implements FxController {
 		root.setOnDragOver(this::handleDragEvent);
 		root.setOnDragDropped(this::handleDragEvent);
 		root.setOnDragExited(this::handleDragEvent);
-
-		addVaultBtn.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume);
 	}
 
 	@FXML
-	private void showMenu() {
-		double screenX = addVaultBtn.localToScreen(addVaultBtn.getBoundsInLocal()).getMinX();
-		double screenY = addVaultBtn.localToScreen(addVaultBtn.getBoundsInLocal()).getMaxY();
-		addVaultBtn.getContextMenu().show(addVaultBtn, screenX, screenY);
+	private void toggleMenu() {
+		if (addVaultContextMenu.isShowing()) {
+			addVaultContextMenu.hide();
+		} else {
+			addVaultContextMenu.show(addVaultBtn, Side.BOTTOM, 0.0, 0.0);
+		}
 	}
 
 	private void deselect(MouseEvent released) {

+ 7 - 1
src/main/java/org/cryptomator/ui/preferences/PreferencesController.java

@@ -1,5 +1,6 @@
 package org.cryptomator.ui.preferences;
 
+import org.cryptomator.common.Environment;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.fxapp.UpdateChecker;
 import org.slf4j.Logger;
@@ -19,6 +20,7 @@ public class PreferencesController implements FxController {
 
 	private static final Logger LOG = LoggerFactory.getLogger(PreferencesController.class);
 
+	private final Environment env;
 	private final Stage window;
 	private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
 	private final BooleanBinding updateAvailable;
@@ -31,7 +33,8 @@ public class PreferencesController implements FxController {
 	public Tab aboutTab;
 
 	@Inject
-	public PreferencesController(@PreferencesWindow Stage window, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, UpdateChecker updateChecker) {
+	public PreferencesController(Environment env, @PreferencesWindow Stage window, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, UpdateChecker updateChecker) {
+		this.env = env;
 		this.window = window;
 		this.selectedTabProperty = selectedTabProperty;
 		this.updateAvailable = updateChecker.latestVersionProperty().isNotNull();
@@ -42,6 +45,9 @@ public class PreferencesController implements FxController {
 		window.setOnShowing(this::windowWillAppear);
 		selectedTabProperty.addListener(observable -> this.selectChosenTab());
 		tabPane.getSelectionModel().selectedItemProperty().addListener(observable -> this.selectedTabChanged());
+		if (env.disableUpdateCheck()) {
+			tabPane.getTabs().remove(updatesTab);
+		}
 	}
 
 	private void selectChosenTab() {

+ 2 - 1
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java

@@ -8,6 +8,7 @@ import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.cryptolib.api.Masterkey;
 import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
 import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.VisibleForTesting;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -58,7 +59,7 @@ public class RecoveryKeyFactory {
 		}
 	}
 
-	// visible for testing
+	@VisibleForTesting
 	String createRecoveryKey(byte[] rawKey) {
 		Preconditions.checkArgument(rawKey.length == 64, "key should be 64 bytes");
 		byte[] paddedKey = Arrays.copyOf(rawKey, 66);

+ 17 - 17
src/main/resources/fxml/vault_list.fxml

@@ -28,27 +28,27 @@
 				<Arc VBox.vgrow="NEVER" styleClass="onboarding-overlay-arc" type="OPEN" centerX="50" centerY="0" radiusY="100" radiusX="50" startAngle="0" length="-60" strokeWidth="1"/>
 			</VBox>
 		</StackPane>
-		<Button fx:id="addVaultBtn" onAction="#showMenu" styleClass="toolbar-button" text="%main.vaultlist.addVaultBtn" alignment="BASELINE_CENTER" maxWidth="Infinity" contentDisplay="RIGHT">
+		<Button fx:id="addVaultBtn" onAction="#toggleMenu" styleClass="toolbar-button" text="%main.vaultlist.addVaultBtn" alignment="BASELINE_CENTER" maxWidth="Infinity" contentDisplay="RIGHT">
 			<graphic>
 				<FontAwesome5IconView glyph="CARET_DOWN"/>
 			</graphic>
-			<contextMenu>
-				<ContextMenu>
-					<items>
-						<MenuItem styleClass="add-vault-menu-item" text="%main.vaultlist.addVaultBtn.menuItemNew" onAction="#didClickAddNewVault" >
-							<graphic>
-								<FontAwesome5IconView glyph="PLUS" textAlignment="CENTER" wrappingWidth="14" />
-							</graphic>
-						</MenuItem>
-						<MenuItem styleClass="add-vault-menu-item" text="%main.vaultlist.addVaultBtn.menuItemExisting" onAction="#didClickAddExistingVault" >
-							<graphic>
-								<FontAwesome5IconView glyph="FOLDER_OPEN" textAlignment="CENTER" wrappingWidth="14" />
-							</graphic>
-						</MenuItem>
-					</items>
-				</ContextMenu>
-			</contextMenu>
 		</Button>
+		<fx:define>
+			<ContextMenu fx:id="addVaultContextMenu">
+				<items>
+					<MenuItem styleClass="add-vault-menu-item" text="%main.vaultlist.addVaultBtn.menuItemNew" onAction="#didClickAddNewVault" >
+						<graphic>
+							<FontAwesome5IconView glyph="PLUS" textAlignment="CENTER" wrappingWidth="14" />
+						</graphic>
+					</MenuItem>
+					<MenuItem styleClass="add-vault-menu-item" text="%main.vaultlist.addVaultBtn.menuItemExisting" onAction="#didClickAddExistingVault" >
+						<graphic>
+							<FontAwesome5IconView glyph="FOLDER_OPEN" textAlignment="CENTER" wrappingWidth="14" />
+						</graphic>
+					</MenuItem>
+				</items>
+			</ContextMenu>
+		</fx:define>
 	</VBox>
 	<Region styleClass="drag-n-drop-border" visible="${controller.draggingVaultOver}"/>
 </StackPane>

+ 1 - 0
src/main/resources/i18n/strings_bg.properties

@@ -153,6 +153,7 @@ hub.invalidLicense.message=Лиценза за Hub е недействителе
 
 # Lock
 ## Force
+lock.forced.retryBtn=Повторен опит
 ## Failure
 
 # Migration

+ 4 - 0
src/main/resources/i18n/strings_zh_TW.properties

@@ -22,6 +22,9 @@ error.hyperlink.report=回報錯誤
 error.technicalDetails=詳情:
 error.existingSolutionDescription=Cryptomator 沒有預料到會發生這種情況。但我們找到了一個現有的解決方案來解決這個錯誤。請查看以下連結。
 error.hyperlink.solution=查詢解決方案
+error.lookupPermissionMessage=Cryptomator 可以在線查找此問題的解決方案。 這將從您的 IP 地址向我們的問題數據庫發送請求。
+error.dismiss=忽略
+error.lookUpSolution=查詢解決方案
 
 # Defaults
 defaults.vault.vaultName=加密檔案庫
@@ -134,6 +137,7 @@ unlock.error.customPath.message=無法將檔案庫掛載至自訂路徑
 unlock.error.customPath.description.notSupported=如果要繼續使用自訂的掛載路徑,必須變更成支援的磁區空間類型,不然就必須使用不同的掛載路徑
 unlock.error.customPath.description.notExists=自訂的掛載路徑並不存在‧ 請在本機創立該路徑,或者在加密庫選項中更改
 unlock.error.customPath.description.inUse=磁碟代號或自訂掛載路徑「%s」已被使用。
+unlock.error.customPath.description.hideawayNotDir=無法移除用於解鎖的臨時隱藏檔案「%3$s」。請檢查該檔案,然後手動刪除。
 unlock.error.customPath.description.couldNotBeCleaned=無法將您的保險庫掛載至路徑「%s」。請再試一次或選擇不同的路徑。
 unlock.error.customPath.description.notEmptyDir=自訂掛載路徑「%s」不是一個空資料夾。請選擇一個空資料夾並重試。
 unlock.error.customPath.description.generic=您為此保險庫選擇了自訂掛載路徑,但使用時出現了錯誤訊息:%2$s