~exprez135/cryptomator-libre

ff9453256002d9bd08f675115576161e9fdb33df — Sebastian Stenzel 5 months ago b781cf6 + e949023
Merge branch 'release/1.5.11'
77 files changed, 1087 insertions(+), 609 deletions(-)

M .github/ISSUE_TEMPLATE/bug.md
A .github/ISSUE_TEMPLATE/config.yml
M main/buildkit/pom.xml
M main/commons/pom.xml
D main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java
M main/commons/src/main/java/org/cryptomator/common/mountpoint/MacVolumeMountChooser.java
R main/commons/src/main/java/org/cryptomator/common/mountpoint/{IrregularUnmountCleaner.java => MountPointHelper.java}
M main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java
M main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
M main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java
M main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java
M main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java
M main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java
M main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java
M main/launcher/pom.xml
M main/pom.xml
A main/suppression.xml
M main/ui/pom.xml
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java
M main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java
M main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java
M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
A main/ui/src/main/java/org/cryptomator/ui/lock/LockComponent.java
A main/ui/src/main/java/org/cryptomator/ui/lock/LockFailedController.java
A main/ui/src/main/java/org/cryptomator/ui/lock/LockForcedController.java
A main/ui/src/main/java/org/cryptomator/ui/lock/LockModule.java
A main/ui/src/main/java/org/cryptomator/ui/lock/LockScoped.java
A main/ui/src/main/java/org/cryptomator/ui/lock/LockWindow.java
A main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java
D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java
D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java
D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java
M main/ui/src/main/java/org/cryptomator/ui/quit/QuitComponent.java
M main/ui/src/main/java/org/cryptomator/ui/quit/QuitController.java
M main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java
M main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java
M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java
A main/ui/src/main/resources/fxml/lock_failed.fxml
A main/ui/src/main/resources/fxml/lock_forced.fxml
M main/ui/src/main/resources/i18n/strings.properties
M main/ui/src/main/resources/i18n/strings_ar.properties
M main/ui/src/main/resources/i18n/strings_bs.properties
M main/ui/src/main/resources/i18n/strings_ca.properties
M main/ui/src/main/resources/i18n/strings_cs.properties
M main/ui/src/main/resources/i18n/strings_de.properties
M main/ui/src/main/resources/i18n/strings_el.properties
M main/ui/src/main/resources/i18n/strings_es.properties
M main/ui/src/main/resources/i18n/strings_fr.properties
M main/ui/src/main/resources/i18n/strings_hi.properties
M main/ui/src/main/resources/i18n/strings_hr.properties
M main/ui/src/main/resources/i18n/strings_id.properties
M main/ui/src/main/resources/i18n/strings_it.properties
M main/ui/src/main/resources/i18n/strings_ja.properties
M main/ui/src/main/resources/i18n/strings_ko.properties
M main/ui/src/main/resources/i18n/strings_lv.properties
M main/ui/src/main/resources/i18n/strings_nb.properties
M main/ui/src/main/resources/i18n/strings_nl.properties
M main/ui/src/main/resources/i18n/strings_nn.properties
M main/ui/src/main/resources/i18n/strings_pa.properties
M main/ui/src/main/resources/i18n/strings_pl.properties
M main/ui/src/main/resources/i18n/strings_pt.properties
M main/ui/src/main/resources/i18n/strings_pt_BR.properties
M main/ui/src/main/resources/i18n/strings_ro.properties
M main/ui/src/main/resources/i18n/strings_ru.properties
M main/ui/src/main/resources/i18n/strings_sk.properties
M main/ui/src/main/resources/i18n/strings_sv.properties
M main/ui/src/main/resources/i18n/strings_tr.properties
M main/ui/src/main/resources/i18n/strings_zh.properties
M main/ui/src/main/resources/i18n/strings_zh_TW.properties
M main/ui/src/main/resources/license/THIRD-PARTY.txt
M .github/ISSUE_TEMPLATE/bug.md => .github/ISSUE_TEMPLATE/bug.md +13 -3
@@ 5,8 5,11 @@ labels: type:bug
---

<!--
⚠️⚠️⚠️ READ CAREFULLY ⚠️⚠️⚠️

**************************************
*                                    *
*   ⚠️⚠️⚠️ READ CAREFULLY ⚠️⚠️⚠️   *
*                                    *
**************************************
Do you want to ask a QUESTION? Are you looking for SUPPORT?
We're happy to help you via our support channels! Please read: https://github.com/cryptomator/cryptomator/blob/develop/SUPPORT.md



@@ 14,7 17,14 @@ By filing an issue, you are expected to comply with our code of conduct: https:/

Of course, we also expect you to search for existing similar issues first! ;) https://github.com/cryptomator/cryptomator/issues?q=

⚠️ IMPORTANT: If you don't stick to this template, the issue will get closed. To proof that you read this, please remove the X from the following line:

⚠️ IMPORTANT: If you don't stick to this template, the issue will get closed.

*****************************************************************************
*                                                                           *
*   To proof that you read this, please remove the X from the line below:   *
*                                                                           *
*****************************************************************************
-->
<!-- oooXooo -->


A .github/ISSUE_TEMPLATE/config.yml => .github/ISSUE_TEMPLATE/config.yml +8 -0
@@ 0,0 1,8 @@
blank_issues_enabled: false
contact_links:
  - name: Cryptomator Community
    url: https://community.cryptomator.org/
    about: Please ask and answer questions here
  - name: Documentation
    url: https://docs.cryptomator.org/
    about: Get instructions on how to use Cryptomator

M main/buildkit/pom.xml => main/buildkit/pom.xml +1 -1
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.10</version>
		<version>1.5.11</version>
	</parent>
	<artifactId>buildkit</artifactId>
	<packaging>pom</packaging>

M main/commons/pom.xml => main/commons/pom.xml +1 -1
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.10</version>
		<version>1.5.11</version>
	</parent>
	<artifactId>commons</artifactId>
	<name>Cryptomator Commons</name>

D main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java => main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java +0 -79
@@ 1,79 0,0 @@
/*******************************************************************************
 * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the accompanying LICENSE file.
 *******************************************************************************/
package org.cryptomator.common;

import com.google.common.base.Throwables;

import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

public final class LazyInitializer {

	private LazyInitializer() {
	}

	/**
	 * Same as {@link #initializeLazily(AtomicReference, SupplierThrowingException, Class)} except that no checked exception may be thrown by the factory function.
	 *
	 * @param <T> Type of the value
	 * @param reference A reference to a maybe not yet initialized value.
	 * @param factory A factory providing a value for the reference, if it doesn't exist yet. The factory may be invoked multiple times, but only one result will survive.
	 * @return The initialized value
	 */
	public static <T> T initializeLazily(AtomicReference<T> reference, Supplier<T> factory) {
		SupplierThrowingException<T, RuntimeException> factoryThrowingRuntimeExceptions = () -> factory.get();
		return initializeLazily(reference, factoryThrowingRuntimeExceptions, RuntimeException.class);
	}

	/**
	 * Threadsafe lazy initialization pattern as proposed on http://stackoverflow.com/a/30247202/4014509
	 *
	 * @param <T> Type of the value
	 * @param <E> Type of the any expected exception that may occur during initialization
	 * @param reference A reference to a maybe not yet initialized value.
	 * @param factory A factory providing a value for the reference, if it doesn't exist yet. The factory may be invoked multiple times, but only one result will survive.
	 * @param exceptionType Expected exception type.
	 * @return The initialized value
	 * @throws E Exception thrown by the factory function.
	 */
	public static <T, E extends Exception> T initializeLazily(AtomicReference<T> reference, SupplierThrowingException<T, E> factory, Class<E> exceptionType) throws E {
		final T existing = reference.get();
		if (existing != null) {
			return existing;
		} else {
			try {
				return reference.updateAndGet(invokeFactoryIfNull(factory));
			} catch (InitializationException e) {
				Throwables.throwIfUnchecked(e.getCause());
				Throwables.throwIfInstanceOf(e.getCause(), exceptionType);
				throw e;
			}
		}
	}

	private static <T, E extends Exception> UnaryOperator<T> invokeFactoryIfNull(SupplierThrowingException<T, E> factory) throws InitializationException {
		return currentValue -> {
			if (currentValue == null) {
				try {
					return factory.get();
				} catch (Exception e) {
					throw new InitializationException(e);
				}
			} else {
				return currentValue;
			}
		};
	}

	private static class InitializationException extends RuntimeException {

		public InitializationException(Throwable cause) {
			super(cause);
		}

	}
}

M main/commons/src/main/java/org/cryptomator/common/mountpoint/MacVolumeMountChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/MacVolumeMountChooser.java +4 -26
@@ 3,25 3,22 @@ package org.cryptomator.common.mountpoint;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Volume;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;

class MacVolumeMountChooser implements MountPointChooser {

	private static final Logger LOG = LoggerFactory.getLogger(MacVolumeMountChooser.class);
	private static final int MAX_MOUNTPOINT_CREATION_RETRIES = 10;
	private static final Path VOLUME_PATH = Path.of("/Volumes");

	private final VaultSettings vaultSettings;
	private final MountPointHelper helper;

	@Inject
	public MacVolumeMountChooser(VaultSettings vaultSettings) {
	public MacVolumeMountChooser(VaultSettings vaultSettings, MountPointHelper helper) {
		this.vaultSettings = vaultSettings;
		this.helper = helper;
	}

	@Override


@@ 31,26 28,7 @@ class MacVolumeMountChooser implements MountPointChooser {

	@Override
	public Optional<Path> chooseMountPoint(Volume caller) {
		String basename = this.vaultSettings.mountName().get();
		// regular
		Path mountPoint = VOLUME_PATH.resolve(basename);
		if (Files.notExists(mountPoint)) {
			return Optional.of(mountPoint);
		}
		// with id
		mountPoint = VOLUME_PATH.resolve(basename + " (" + vaultSettings.getId() + ")");
		if (Files.notExists(mountPoint)) {
			return Optional.of(mountPoint);
		}
		// with id and count
		for (int i = 1; i < MAX_MOUNTPOINT_CREATION_RETRIES; i++) {
			mountPoint = VOLUME_PATH.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
			if (Files.notExists(mountPoint)) {
				return Optional.of(mountPoint);
			}
		}
		LOG.error("Failed to find feasible mountpoint at /Volumes/{}_x. Giving up after {} attempts.", basename, MAX_MOUNTPOINT_CREATION_RETRIES);
		return Optional.empty();
		return Optional.of(helper.chooseTemporaryMountPoint(vaultSettings, VOLUME_PATH));
	}

	@Override

R main/commons/src/main/java/org/cryptomator/common/mountpoint/IrregularUnmountCleaner.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointHelper.java +39 -8
@@ 1,11 1,13 @@
package org.cryptomator.common.mountpoint;

import org.cryptomator.common.Environment;
import org.cryptomator.common.settings.VaultSettings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.Files;


@@ 15,27 17,50 @@ import java.nio.file.attribute.BasicFileAttributes;
import java.util.Optional;

@Singleton
class IrregularUnmountCleaner {
class MountPointHelper {

	public static Logger LOG = LoggerFactory.getLogger(IrregularUnmountCleaner.class);
	public static Logger LOG = LoggerFactory.getLogger(MountPointHelper.class);
	private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;

	private final Optional<Path> tmpMountPointDir;
	private volatile boolean alreadyChecked = false;
	private volatile boolean unmountDebrisCleared = false;

	@Inject
	public IrregularUnmountCleaner(Environment env) {
	public MountPointHelper(Environment env) {
		this.tmpMountPointDir = env.getMountPointsDir();
	}

	public Path chooseTemporaryMountPoint(VaultSettings vaultSettings, Path parentDir) {
		String basename = vaultSettings.mountName().get();
		//regular
		Path mountPoint = parentDir.resolve(basename);
		if (Files.notExists(mountPoint)) {
			return mountPoint;
		}
		//with id
		mountPoint = parentDir.resolve(basename + " (" + vaultSettings.getId() + ")");
		if (Files.notExists(mountPoint)) {
			return mountPoint;
		}
		//with id and count
		for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
			mountPoint = parentDir.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
			if (Files.notExists(mountPoint)) {
				return mountPoint;
			}
		}
		LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parentDir, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
		return null;
	}

	public synchronized void clearIrregularUnmountDebrisIfNeeded() {
		if (alreadyChecked || tmpMountPointDir.isEmpty()) {
			return; //nuthin to do
		if (unmountDebrisCleared || tmpMountPointDir.isEmpty()) {
			return; // nothing to do
		}
		if (Files.exists(tmpMountPointDir.get(), LinkOption.NOFOLLOW_LINKS)) {
			clearIrregularUnmountDebris(tmpMountPointDir.get());
		}
		alreadyChecked = true;
		unmountDebrisCleared = true;
	}

	private void clearIrregularUnmountDebris(Path dirContainingMountPoints) {


@@ 66,13 91,14 @@ class IrregularUnmountCleaner {
		} catch (IOException e) {
			LOG.warn("Unable to perform cleanup of mountpoint dir {}.", dirContainingMountPoints, e);
		} finally {
			alreadyChecked = true;
			unmountDebrisCleared = true;
		}
	}

	private void deleteEmptyDir(Path dir) throws IOException {
		assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS);
		try {
			ensureIsEmpty(dir);
			Files.delete(dir); // attempt to delete dir non-recursively (will fail, if there are contents)
		} catch (DirectoryNotEmptyException e) {
			LOG.info("Found non-empty directory in mountpoint dir: {}", dir);


@@ 86,4 112,9 @@ class IrregularUnmountCleaner {
		}
	}

	private void ensureIsEmpty(Path dir) throws IOException {
		if (Files.newDirectoryStream(dir).iterator().hasNext()) {
			throw new DirectoryNotEmptyException(dir.toString());
		}
	}
}

M main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java => main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java +5 -32
@@ 7,7 7,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;


@@ 16,18 15,16 @@ import java.util.Optional;
class TemporaryMountPointChooser implements MountPointChooser {

	private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class);
	private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;

	private final VaultSettings vaultSettings;
	private final Environment environment;
	private final IrregularUnmountCleaner cleaner;
	private volatile boolean clearedDebris;
	private final MountPointHelper helper;

	@Inject
	public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, IrregularUnmountCleaner cleaner) {
	public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, MountPointHelper helper) {
		this.vaultSettings = vaultSettings;
		this.environment = environment;
		this.cleaner = cleaner;
		this.helper = helper;
	}

	@Override


@@ 44,32 41,8 @@ class TemporaryMountPointChooser implements MountPointChooser {
		assert environment.getMountPointsDir().isPresent();
		//clean leftovers of not-regularly unmounted vaults
		//see https://github.com/cryptomator/cryptomator/issues/1013 and https://github.com/cryptomator/cryptomator/issues/1061
		cleaner.clearIrregularUnmountDebrisIfNeeded();
		return this.environment.getMountPointsDir().map(this::choose);
	}


	private Path choose(Path parent) {
		String basename = this.vaultSettings.mountName().get();
		//regular
		Path mountPoint = parent.resolve(basename);
		if (Files.notExists(mountPoint)) {
			return mountPoint;
		}
		//with id
		mountPoint = parent.resolve(basename + " (" + vaultSettings.getId() + ")");
		if (Files.notExists(mountPoint)) {
			return mountPoint;
		}
		//with id and count
		for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
			mountPoint = parent.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
			if (Files.notExists(mountPoint)) {
				return mountPoint;
			}
		}
		LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parent, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
		return null;
		helper.clearIrregularUnmountDebrisIfNeeded();
		return this.environment.getMountPointsDir().map(dir -> this.helper.chooseTemporaryMountPoint(this.vaultSettings, dir));
	}

	@Override

M main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java => main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java +3 -3
@@ 8,13 8,13 @@
 *******************************************************************************/
package org.cryptomator.common.settings;

import com.google.common.base.Suppliers;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import org.cryptomator.common.Environment;
import org.cryptomator.common.LazyInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;



@@ 48,7 48,7 @@ public class SettingsProvider implements Supplier<Settings> {
	private static final long SAVE_DELAY_MS = 1000;

	private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
	private final AtomicReference<Settings> settings = new AtomicReference<>();
	private final Supplier<Settings> settings = Suppliers.memoize(this::load);
	private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter();
	private final Environment env;
	private final ScheduledExecutorService scheduler;


@@ 66,7 66,7 @@ public class SettingsProvider implements Supplier<Settings> {

	@Override
	public Settings get() {
		return LazyInitializer.initializeLazily(settings, this::load);
		return settings.get();
	}

	private Settings load() {

M main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java => main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java +3 -2
@@ 20,7 20,8 @@ public abstract class AbstractVolume implements Volume {
	}

	protected Path determineMountPoint() throws InvalidMountPointException {
		for (var chooser : Iterables.filter(choosers, c -> c.isApplicable(this))) {
		var applicableChoosers = Iterables.filter(choosers, c -> c.isApplicable(this));
		for (var chooser : applicableChoosers) {
			Optional<Path> chosenPath = chooser.chooseMountPoint(this);
			if (chosenPath.isEmpty()) { // chooser couldn't find a feasible mountpoint
				continue;


@@ 29,7 30,7 @@ public abstract class AbstractVolume implements Volume {
			this.usedChooser = chooser;
			return chosenPath.get();
		}
		throw new InvalidMountPointException("No feasible MountPoint found!");
		throw new InvalidMountPointException(String.format("No feasible MountPoint found by choosers: %s", applicableChoosers));
	}

	protected void cleanupMountPoint() {

M main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java => main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java +16 -4
@@ 13,7 13,6 @@ import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Named;
import java.util.SortedSet;
import java.util.concurrent.ExecutorService;

public class DokanyVolume extends AbstractVolume {


@@ 42,7 41,6 @@ public class DokanyVolume extends AbstractVolume {
	@Override
	public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
		this.mountPoint = determineMountPoint();
		String mountName = vaultSettings.mountName().get();
		try {
			this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip());
		} catch (MountFailedException e) {


@@ 62,12 60,26 @@ public class DokanyVolume extends AbstractVolume {
	}

	@Override
	public void unmount() {
		mount.close();
	public void unmount() throws VolumeException {
		try {
			mount.unmount();
		} catch (IllegalStateException e) {
			throw new VolumeException("Unmount Failed.", e);
		}
		cleanupMountPoint();
	}

	@Override
	public void unmountForced() {
		mount.unmountForced();
		cleanupMountPoint();
	}

	@Override
	public boolean supportsForcedUnmount() {
		return true;
	}
	@Override
	public boolean isSupported() {
		return DokanyVolume.isSupportedStatic();
	}

M main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java => main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java +9 -9
@@ 10,7 10,6 @@ package org.cryptomator.common.vaults;

import com.google.common.base.Strings;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.LazyInitializer;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Volume.VolumeException;


@@ 100,11 99,7 @@ public class Vault {
	// Commands
	// ********************************************************************************/

	private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
		return LazyInitializer.initializeLazily(cryptoFileSystem, () -> unlockCryptoFileSystem(passphrase), IOException.class);
	}

	private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
	private CryptoFileSystem createCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
		Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
		if (vaultSettings.usesReadOnlyMode().get()) {
			flags.add(FileSystemFlags.READONLY);


@@ 127,9 122,14 @@ public class Vault {
	}

	public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
		CryptoFileSystem fs = getCryptoFileSystem(passphrase);
		volume = volumeProvider.get();
		volume.mount(fs, getEffectiveMountFlags());
		if (cryptoFileSystem.get() == null) {
			CryptoFileSystem fs = createCryptoFileSystem(passphrase);
			cryptoFileSystem.set(fs);
			volume = volumeProvider.get();
			volume.mount(fs, getEffectiveMountFlags());
		} else {
			throw new IllegalStateException("Already unlocked.");
		}
	}

	public synchronized void lock(boolean forced) throws VolumeException {

M main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java => main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java +1 -1
@@ 156,7 156,7 @@ public class VaultModule {
		//See: https://github.com/billziss-gh/winfsp/issues/319
		if (!readOnly.get()) {
			flags.append(" -ouid=-1");
			flags.append(" -ogid=-1");
			flags.append(" -ogid=11");
		}
		flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
		//Dokany requires this option to be set, WinFSP doesn't seem to share this peculiarity,

M main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java => main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java +1 -1
@@ 43,7 43,7 @@ public class VaultModuleTest {

		StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings);

		MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ovolname=TEST"));
		MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ovolname=\"TEST\""));
		MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ordonly"));
	}


M main/launcher/pom.xml => main/launcher/pom.xml +1 -1
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.10</version>
		<version>1.5.11</version>
	</parent>
	<artifactId>launcher</artifactId>
	<name>Cryptomator Launcher</name>

M main/pom.xml => main/pom.xml +45 -5
@@ 3,7 3,7 @@
	<modelVersion>4.0.0</modelVersion>
	<groupId>org.cryptomator</groupId>
	<artifactId>main</artifactId>
	<version>1.5.10</version>
	<version>1.5.11</version>
	<packaging>pom</packaging>
	<name>Cryptomator</name>



@@ 26,12 26,12 @@
		<!-- cryptomator dependencies -->
		<cryptomator.cryptofs.version>1.9.13</cryptomator.cryptofs.version>
		<cryptomator.integrations.version>0.1.6</cryptomator.integrations.version>
		<cryptomator.integrations.win.version>0.1.0-beta1</cryptomator.integrations.win.version>
		<cryptomator.integrations.win.version>0.2.0</cryptomator.integrations.win.version>
		<cryptomator.integrations.mac.version>0.1.0-beta3</cryptomator.integrations.mac.version>
		<cryptomator.integrations.linux.version>0.1.0-beta2</cryptomator.integrations.linux.version>
		<cryptomator.fuse.version>1.2.5</cryptomator.fuse.version>
		<cryptomator.dokany.version>1.2.0</cryptomator.dokany.version>
		<cryptomator.webdav.version>1.0.13</cryptomator.webdav.version>
		<cryptomator.fuse.version>1.2.6</cryptomator.fuse.version>
		<cryptomator.dokany.version>1.2.1</cryptomator.dokany.version>
		<cryptomator.webdav.version>1.0.14</cryptomator.webdav.version>

		<!-- 3rd party dependencies -->
		<javafx.version>15</javafx.version>


@@ 175,6 175,13 @@
				<artifactId>java-jwt</artifactId>
				<version>${jwt.version}</version>
			</dependency>
			<!-- fixes CVE-2020-25649, can be removed once https://github.com/auth0/java-jwt/pull/463 is closed and released -->
			<dependency>
				<groupId>com.fasterxml.jackson.core</groupId>
				<artifactId>jackson-databind</artifactId>
				<version>2.10.5.1</version>
			</dependency>


			<!-- EasyBind -->
			<dependency>


@@ 223,6 230,13 @@
				<version>${javafx.version}</version>
				<scope>test</scope>
			</dependency>

			<!-- TODO: temporary fix for XXE attack, can be removed once java-jwt is updated -->
			<dependency>
				<groupId>com.fasterxml.jackson.core</groupId>
				<artifactId>jackson-databind</artifactId>
				<version>2.10.5.1</version>
			</dependency>
		</dependencies>
	</dependencyManagement>



@@ 325,6 339,32 @@
				</dependency>
			</dependencies>
		</profile>
		<profile>
			<id>dependency-check</id>
			<build>
				<plugins>
					<plugin>
						<groupId>org.owasp</groupId>
						<artifactId>dependency-check-maven</artifactId>
						<version>6.0.3</version>
						<configuration>
							<cveValidForHours>24</cveValidForHours>
							<failBuildOnCVSS>0</failBuildOnCVSS>
							<skipTestScope>true</skipTestScope>
							<detail>true</detail>
							<suppressionFile>suppression.xml</suppressionFile>
						</configuration>
						<executions>
							<execution>
								<goals>
									<goal>check</goal>
								</goals>
							</execution>
						</executions>
					</plugin>
				</plugins>
			</build>
		</profile>
	</profiles>

	<build>

A main/suppression.xml => main/suppression.xml +19 -0
@@ 0,0 1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This file lists false positives found by org.owasp:dependency-check-maven build plugin -->
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.2.xsd">
	<suppress>
		<notes><![CDATA[ Upstream fix backported from 2.11.0 to 2.10.5.1, see https://github.com/FasterXML/jackson-databind/issues/2589#issuecomment-714833837. ]]></notes>
		<gav>com.fasterxml.jackson.core:jackson-databind:2.10.5.1</gav>
		<cve>CVE-2020-25649</cve>
	</suppress>
	<suppress>
		<notes><![CDATA[ Suppress known vulnerabilities in FUSE libraries for fuse-nio-adapter. For more info, see suppression.xml of https://github.com/cryptomator/fuse-nio-adapter ]]></notes>
		<gav regex="true">^org\.cryptomator:fuse-nio-adapter:.*$</gav>
		<cvssBelow>9</cvssBelow>
	</suppress>
	<suppress>
		<notes><![CDATA[ Suppress known vulnerabilities in FUSE libraries for jnr-fuse (dependency of fuse-nio-adapter). ]]></notes>
		<gav regex="true">^com\.github\.serceman:jnr-fuse:.*$</gav>
		<cvssBelow>9</cvssBelow>
	</suppress>
</suppressions>
\ No newline at end of file

M main/ui/pom.xml => main/ui/pom.xml +1 -1
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.10</version>
		<version>1.5.11</version>
	</parent>
	<artifactId>ui</artifactId>
	<name>Cryptomator GUI</name>

M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java => main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java +3 -4
@@ 1,6 1,5 @@
package org.cryptomator.ui.addvaultwizard;

import com.tobiasdiez.easybind.EasyBind;
import dagger.Lazy;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;


@@ 84,11 83,11 @@ public class CreateNewVaultLocationController implements FxController {
	public void initialize() {
		predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
		usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
		EasyBind.subscribe(vaultPath, this::vaultPathDidChange);
		vaultPath.addListener(this::vaultPathDidChange);
	}

	private void vaultPathDidChange(Path newValue) {
		if (newValue != null && !Files.notExists(newValue)) {
	private void vaultPathDidChange(@SuppressWarnings("unused") ObservableValue<? extends Path> observable, @SuppressWarnings("unused") Path oldValue, Path newValue) {
		if (!Files.notExists(newValue)) {
			warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
		} else {
			warningText.set(null);

M main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java => main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java +2 -0
@@ 11,6 11,8 @@ public enum FxmlFile {
	CHANGEPASSWORD("/fxml/changepassword.fxml"), //
	ERROR("/fxml/error.fxml"), //
	FORGET_PASSWORD("/fxml/forget_password.fxml"), //
	LOCK_FORCED("/fxml/lock_forced.fxml"), //
	LOCK_FAILED("/fxml/lock_failed.fxml"), //
	MAIN_WINDOW("/fxml/main_window.fxml"), //
	MIGRATION_CAPABILITY_ERROR("/fxml/migration_capability_error.fxml"), //
	MIGRATION_IMPOSSIBLE("/fxml/migration_impossible.fxml"),

M main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java => main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java +1 -0
@@ 65,6 65,7 @@ public class VaultService {
	public Task<Vault> createLockTask(Vault vault, boolean forced) {
		Task<Vault> task = new LockVaultTask(vault, forced);
		task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayName()));
		task.setOnFailed(evt -> LOG.info("Failed to lock {}.", vault.getDisplayName(), evt.getSource().getException()));
		return task;
	}


M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java => main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +23 -10
@@ 1,6 1,5 @@
package org.cryptomator.ui.fxapp;

import com.tobiasdiez.easybind.EasyBind;
import dagger.Lazy;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;


@@ 12,6 11,7 @@ import org.cryptomator.integrations.uiappearance.UiAppearanceException;
import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;


@@ 27,8 27,9 @@ import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableSet;
import javafx.collections.ObservableList;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.awt.desktop.QuitResponse;
import java.util.Optional;



@@ 40,34 41,38 @@ public class FxApplication extends Application {
	private final Settings settings;
	private final Lazy<MainWindowComponent> mainWindow;
	private final Lazy<PreferencesComponent> preferencesWindow;
	private final Lazy<QuitComponent> quitWindow;
	private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
	private final Provider<QuitComponent.Builder> quitWindowBuilderProvider;
	private final Provider<LockComponent.Builder> lockWindowBuilderProvider;
	private final Optional<TrayIntegrationProvider> trayIntegration;
	private final Optional<UiAppearanceProvider> appearanceProvider;
	private final VaultService vaultService;
	private final LicenseHolder licenseHolder;
	private final BooleanBinding hasVisibleStages;
	private final ObservableList<Window> visibleWindows;
	private final BooleanBinding hasVisibleWindows;
	private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;

	@Inject
	FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<QuitComponent.Builder> quitWindowBuilderProvider, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder, ObservableSet<Stage> visibleStages) {
	FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<LockComponent.Builder> lockWindowBuilderProvider, Lazy<QuitComponent> quitWindow, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) {
		this.settings = settings;
		this.mainWindow = mainWindow;
		this.preferencesWindow = preferencesWindow;
		this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
		this.quitWindowBuilderProvider = quitWindowBuilderProvider;
		this.lockWindowBuilderProvider = lockWindowBuilderProvider;
		this.quitWindow = quitWindow;
		this.trayIntegration = trayIntegration;
		this.appearanceProvider = appearanceProvider;
		this.vaultService = vaultService;
		this.licenseHolder = licenseHolder;
		this.hasVisibleStages = Bindings.isNotEmpty(visibleStages);
		this.visibleWindows = Stage.getWindows().filtered(Window::isShowing);
		this.hasVisibleWindows = Bindings.isNotEmpty(visibleWindows);
	}

	public void start() {
		LOG.trace("FxApplication.start()");
		Platform.setImplicitExit(false);

		EasyBind.subscribe(hasVisibleStages, this::hasVisibleStagesChanged);
		hasVisibleWindows.addListener(this::hasVisibleStagesChanged);

		settings.theme().addListener(this::appThemeChanged);
		loadSelectedStyleSheet(settings.theme().get());


@@ 78,7 83,8 @@ public class FxApplication extends Application {
		throw new UnsupportedOperationException("Use start() instead.");
	}

	private void hasVisibleStagesChanged(boolean newValue) {
	private void hasVisibleStagesChanged(@SuppressWarnings("unused") ObservableValue<? extends Boolean> observableValue, @SuppressWarnings("unused") boolean oldValue, boolean newValue) {
		LOG.warn("has visible stages: {}", newValue);
		if (newValue) {
			trayIntegration.ifPresent(TrayIntegrationProvider::restoredFromTray);
		} else {


@@ 107,9 113,16 @@ public class FxApplication extends Application {
		});
	}

	public void startLockWorkflow(Vault vault, Optional<Stage> owner) {
		Platform.runLater(() -> {
			lockWindowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
			LOG.debug("Start lock workflow for {}", vault.getDisplayName());
		});
	}

	public void showQuitWindow(QuitResponse response) {
		Platform.runLater(() -> {
			quitWindowBuilderProvider.get().quitResponse(response).build().showQuitWindow();
			quitWindow.get().showQuitWindow(response);
			LOG.debug("Showing QuitWindow");
		});
	}

M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java => main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +7 -16
@@ 11,6 11,7 @@ import dagger.Provides;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.quit.QuitComponent;


@@ 18,7 19,6 @@ import org.cryptomator.ui.unlock.UnlockComponent;

import javax.inject.Named;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.scene.image.Image;
import javafx.stage.Stage;


@@ 28,16 28,10 @@ import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.List;

@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, QuitComponent.class, ErrorComponent.class})
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, ErrorComponent.class})
abstract class FxApplicationModule {

	@Provides
	@FxApplicationScoped
	static ObservableSet<Stage> provideVisibleStages() {
		return FXCollections.observableSet();
	}

	@Provides
	@Named("windowIcons")
	@FxApplicationScoped
	static List<Image> provideWindowIcons() {


@@ 56,16 50,9 @@ abstract class FxApplicationModule {

	@Provides
	@FxApplicationScoped
	static StageFactory provideStageFactory(@Named("windowIcons") List<Image> windowIcons, ObservableSet<Stage> visibleStages) {
	static StageFactory provideStageFactory(@Named("windowIcons") List<Image> windowIcons) {
		return new StageFactory(stage -> {
			stage.getIcons().addAll(windowIcons);
			stage.showingProperty().addListener((observableValue, wasShowing, isShowing) -> {
				if (isShowing) {
					visibleStages.add(stage);
				} else {
					visibleStages.remove(stage);
				}
			});
		});
	}



@@ 88,4 75,8 @@ abstract class FxApplicationModule {
		return builder.build();
	}

	@Provides
	static QuitComponent provideQuitComponent(QuitComponent.Builder builder) {
		return builder.build();
	}
}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockComponent.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockComponent.java +39 -0
@@ 0,0 1,39 @@
package org.cryptomator.ui.lock;

import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.vaults.Vault;

import javax.inject.Named;
import javafx.stage.Stage;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;


@LockScoped
@Subcomponent(modules = {LockModule.class})
public interface LockComponent {

	ExecutorService defaultExecutorService();

	LockWorkflow lockWorkflow();

	default Future<Void> startLockWorkflow() {
		LockWorkflow workflow = lockWorkflow();
		defaultExecutorService().submit(workflow);
		return workflow;
	}

	@Subcomponent.Builder
	interface Builder {

		@BindsInstance
		LockComponent.Builder vault(@LockWindow Vault vault);

		@BindsInstance
		LockComponent.Builder owner(@Named("lockWindowOwner") Optional<Stage> owner);

		LockComponent build();
	}
}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockFailedController.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockFailedController.java +31 -0
@@ 0,0 1,31 @@
package org.cryptomator.ui.lock;

import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;

import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;

@LockScoped
public class LockFailedController implements FxController {

	private final Stage window;
	private final Vault vault;

	@Inject
	public LockFailedController(@LockWindow Stage window, @LockWindow Vault vault) {
		this.window = window;
		this.vault = vault;
	}

	@FXML
	public void close() {
		window.close();
	}

	// ----- Getter & Setter -----
	public String getVaultName() {
		return vault.getDisplayName();
	}
}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockForcedController.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockForcedController.java +57 -0
@@ 0,0 1,57 @@
package org.cryptomator.ui.lock;

import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;

@LockScoped
public class LockForcedController implements FxController {

	private static final Logger LOG = LoggerFactory.getLogger(LockForcedController.class);

	private final Stage window;
	private final Vault vault;
	private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;

	@Inject
	public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock) {
		this.window = window;
		this.vault = vault;
		this.forceLockDecisionLock = forceLockDecisionLock;
		this.window.setOnHiding(this::windowClosed);
	}

	@FXML
	public void cancel() {
		forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL);
		window.close();
	}

	@FXML
	public void confirmForcedLock() {
		forceLockDecisionLock.interacted(LockModule.ForceLockDecision.FORCE);
		window.close();
	}

	private void windowClosed(WindowEvent windowEvent) {
		// if not already interacted, set the decision to CANCEL
		if (forceLockDecisionLock.awaitingInteraction().get()) {
			LOG.debug("Lock canceled in force-lock-phase by user.");
			forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL);
		}
	}

	// ----- Getter & Setter -----

	public String getVaultName() {
		return vault.getDisplayName();
	}

}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockModule.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockModule.java +89 -0
@@ 0,0 1,89 @@
package org.cryptomator.ui.lock;

import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.common.UserInteractionLock;

import javax.inject.Named;
import javax.inject.Provider;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;

@Module
abstract class LockModule {

	enum ForceLockDecision {
		CANCEL,
		FORCE;
	}

	@Provides
	@LockScoped
	static UserInteractionLock<LockModule.ForceLockDecision> provideForceLockDecisionLock() {
		return new UserInteractionLock<>(null);
	}

	@Provides
	@LockWindow
	@LockScoped
	static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
		return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
	}

	@Provides
	@LockWindow
	@LockScoped
	static Stage provideWindow(StageFactory factory, @LockWindow Vault vault, @Named("lockWindowOwner") Optional<Stage> owner) {
		Stage stage = factory.create();
		stage.setTitle(vault.getDisplayName());
		stage.setResizable(false);
		if (owner.isPresent()) {
			stage.initOwner(owner.get());
			stage.initModality(Modality.WINDOW_MODAL);
		} else {
			stage.initModality(Modality.APPLICATION_MODAL);
		}
		return stage;
	}

	@Provides
	@FxmlScene(FxmlFile.LOCK_FORCED)
	@LockScoped
	static Scene provideForceLockScene(@LockWindow FXMLLoaderFactory fxmlLoaders) {
		return fxmlLoaders.createScene("/fxml/lock_forced.fxml");
	}

	@Provides
	@FxmlScene(FxmlFile.LOCK_FAILED)
	@LockScoped
	static Scene provideLockFailedScene(@LockWindow FXMLLoaderFactory fxmlLoaders) {
		return fxmlLoaders.createScene("/fxml/lock_failed.fxml");
	}

	// ------------------

	@Binds
	@IntoMap
	@FxControllerKey(LockForcedController.class)
	abstract FxController bindLockForcedController(LockForcedController controller);

	@Binds
	@IntoMap
	@FxControllerKey(LockFailedController.class)
	abstract FxController bindLockFailedController(LockFailedController controller);

}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockScoped.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockScoped.java +13 -0
@@ 0,0 1,13 @@
package org.cryptomator.ui.lock;

import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface LockScoped {

}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockWindow.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockWindow.java +14 -0
@@ 0,0 1,14 @@
package org.cryptomator.ui.lock;

import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Qualifier
@Documented
@Retention(RUNTIME)
@interface LockWindow {

}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java +105 -0
@@ 0,0 1,105 @@
package org.cryptomator.ui.lock;

import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;

/**
 * The sequence of actions performed and checked during lock of a vault.
 * <p>
 * This class implements the Task interface, sucht that it can run in the background with some possible forground operations/requests to the ui, without blocking the main app.
 * If the task state is
 * <li>succeeded, the vault was successfully locked;</li>
 * <li>canceled, the lock was canceled;</li>
 * <li>failed, the lock failed due to an exception.</li>
 */
public class LockWorkflow extends Task<Void> {

	private static final Logger LOG = LoggerFactory.getLogger(LockWorkflow.class);

	private final Stage lockWindow;
	private final Vault vault;
	private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
	private final Lazy<Scene> lockForcedScene;
	private final Lazy<Scene> lockFailedScene;

	@Inject
	public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene) {
		this.lockWindow = lockWindow;
		this.vault = vault;
		this.forceLockDecisionLock = forceLockDecisionLock;
		this.lockForcedScene = lockForcedScene;
		this.lockFailedScene = lockFailedScene;
	}

	@Override
	protected Void call() throws Volume.VolumeException, InterruptedException {
		try {
			vault.lock(false);
		} catch (Volume.VolumeException e) {
			LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e);
			var decision = askUserForAction();
			switch (decision) {
				case FORCE -> vault.lock(true);
				case CANCEL -> cancel(false);
			}
		}
		return null;
	}

	private LockModule.ForceLockDecision askUserForAction() throws InterruptedException {
		// show forcedLock dialogue ...
		Platform.runLater(() -> {
			lockWindow.setScene(lockForcedScene.get());
			lockWindow.show();
			Window owner = lockWindow.getOwner();
			if (owner != null) {
				lockWindow.setX(owner.getX() + (owner.getWidth() - lockWindow.getWidth()) / 2);
				lockWindow.setY(owner.getY() + (owner.getHeight() - lockWindow.getHeight()) / 2);
			} else {
				lockWindow.centerOnScreen();
			}
		});
		// ... and wait for answer
		return forceLockDecisionLock.awaitInteraction();
	}

	@Override
	protected void scheduled() {
		vault.setState(VaultState.PROCESSING);
	}

	@Override
	protected void succeeded() {
		LOG.info("Lock of {} succeeded.", vault.getDisplayName());
		vault.setState(VaultState.LOCKED);
	}

	@Override
	protected void failed() {
		LOG.warn("Failed to lock {}.", vault.getDisplayName());
		vault.setState(VaultState.UNLOCKED);
		lockWindow.setScene(lockFailedScene.get());
		lockWindow.show();
	}

	@Override
	protected void cancelled() {
		LOG.debug("Lock of {} canceled.", vault.getDisplayName());
		vault.setState(VaultState.UNLOCKED);
	}

}

M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java => main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java +9 -3
@@ 6,25 6,32 @@ import com.google.common.cache.LoadingCache;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.stats.VaultStatisticsComponent;

import javax.inject.Inject;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.Optional;

@MainWindowScoped
public class VaultDetailUnlockedController implements FxController {

	private final ReadOnlyObjectProperty<Vault> vault;
	private final FxApplication application;
	private final VaultService vaultService;
	private final Stage mainWindow;
	private final LoadingCache<Vault, VaultStatisticsComponent> vaultStats;
	private final VaultStatisticsComponent.Builder vaultStatsBuilder;

	@Inject
	public VaultDetailUnlockedController(ObjectProperty<Vault> vault, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder) {
	public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplication application, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) {
		this.vault = vault;
		this.application = application;
		this.vaultService = vaultService;
		this.mainWindow = mainWindow;
		this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats));
		this.vaultStatsBuilder = vaultStatsBuilder;
	}


@@ 40,8 47,7 @@ public class VaultDetailUnlockedController implements FxController {

	@FXML
	public void lock() {
		vaultService.lock(vault.get(), false);
		// TODO count lock attempts, and allow forced lock
		application.startLockWorkflow(vault.get(), Optional.of(mainWindow));
	}

	@FXML

M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java => main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +6 -5
@@ 1,6 1,5 @@
package org.cryptomator.ui.mainwindow;

import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;


@@ 13,6 12,7 @@ import javax.inject.Inject;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;


@@ 41,7 41,7 @@ public class VaultListController implements FxController {
		this.removeVault = removeVault;
		this.noVaultSelected = selectedVault.isNull();
		this.emptyVaultList = Bindings.isEmpty(vaults);
		EasyBind.subscribe(selectedVault, this::selectedVaultDidChange);
		selectedVault.addListener(this::selectedVaultDidChange);
	}

	public void initialize() {


@@ 58,10 58,11 @@ public class VaultListController implements FxController {
		});
	}

	private void selectedVaultDidChange(Vault newValue) {
		if (newValue != null) {
			VaultListManager.redetermineVaultState(newValue);
	private void selectedVaultDidChange(@SuppressWarnings("unused") ObservableValue<? extends Vault> observableValue, @SuppressWarnings("unused") Vault oldValue, Vault newValue) {
		if (newValue == null) {
			return;
		}
		VaultListManager.redetermineVaultState(newValue);
	}

	@FXML

D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java => main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java +0 -46
@@ 1,46 0,0 @@
package org.cryptomator.ui.preferences;

import org.cryptomator.integrations.autostart.AutoStartProvider;
import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

@Deprecated
class AutoStartMacStrategy implements AutoStartStrategy {

	private static final Logger LOG = LoggerFactory.getLogger(AutoStartMacStrategy.class);

	private final AutoStartProvider autoStartProvider;

	public AutoStartMacStrategy(AutoStartProvider autoStartProvider) {
		this.autoStartProvider = autoStartProvider;
	}

	@Override
	public CompletionStage<Boolean> isAutoStartEnabled() {
		return CompletableFuture.completedFuture(autoStartProvider.isEnabled());
	}

	@Override
	public void enableAutoStart() throws TogglingAutoStartFailedException {
		try {
			autoStartProvider.enable();
			LOG.debug("Added login item.");
		} catch (ToggleAutoStartFailedException e) {
			throw new TogglingAutoStartFailedException("Failed to add login item.");
		}
	}

	@Override
	public void disableAutoStart() throws TogglingAutoStartFailedException {
		try {
			autoStartProvider.disable();
			LOG.debug("Removed login item.");
		} catch (ToggleAutoStartFailedException e) {
			throw new TogglingAutoStartFailedException("Failed to remove login item.");
		}
	}
}

D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java => main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java +0 -27
@@ 1,27 0,0 @@
package org.cryptomator.ui.preferences;

import dagger.Module;
import dagger.Provides;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.integrations.autostart.AutoStartProvider;

import java.util.Optional;

@Deprecated
@Module
abstract class AutoStartModule {

	@Provides
	@PreferencesScoped
	public static Optional<AutoStartStrategy> provideAutoStartStrategy(Optional<AutoStartProvider> autoStartProvider) {
		if (SystemUtils.IS_OS_MAC_OSX && autoStartProvider.isPresent()) {
			return Optional.of(new AutoStartMacStrategy(autoStartProvider.get()));
		} else if (SystemUtils.IS_OS_WINDOWS) {
			Optional<String> exeName = ProcessHandle.current().info().command();
			return exeName.map(AutoStartWinStrategy::new);
		} else {
			return Optional.empty();
		}
	}

}

D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java => main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java +0 -25
@@ 1,25 0,0 @@
package org.cryptomator.ui.preferences;

import java.util.concurrent.CompletionStage;

@Deprecated
public interface AutoStartStrategy {

	CompletionStage<Boolean> isAutoStartEnabled();

	void enableAutoStart() throws TogglingAutoStartFailedException;

	void disableAutoStart() throws TogglingAutoStartFailedException;

	class TogglingAutoStartFailedException extends Exception {

		public TogglingAutoStartFailedException(String message) {
			super(message);
		}

		public TogglingAutoStartFailedException(String message, Throwable cause) {
			super(message, cause);
		}

	}
}

D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java => main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java +0 -209
@@ 1,209 0,0 @@
package org.cryptomator.ui.preferences;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * OS specific class to check, en- and disable the auto start on Windows.
 * <p>
 * Two strategies are implemented for this feature, the first uses the registry and the second one the autostart folder.
 * <p>
 * The registry strategy checks/add/removes at the registry key {@value HKCU_AUTOSTART_KEY} an entry for Cryptomator.
 * The folder strategy checks/add/removes at the location {@value WINDOWS_START_MENU_ENTRY}.
 * <p>
 * To check if the feature is active, both strategies are applied.
 * To enable the feature, first the registry is tried and only on failure the autostart folder is used.
 * To disable it, first it is determined by an internal state, which strategies must be used and in the second step those are executed.
 *
 * @apiNote This class is not thread safe, hence it should be avoided to call its methods simultaniously by different threads.
 * @deprecated To be moved to integration-win project
 */
@Deprecated
class AutoStartWinStrategy implements AutoStartStrategy {

	private static final Logger LOG = LoggerFactory.getLogger(AutoStartWinStrategy.class);
	private static final String HKCU_AUTOSTART_KEY = "\"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\"";
	private static final String AUTOSTART_VALUE = "Cryptomator";
	private static final String WINDOWS_START_MENU_ENTRY = "\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Cryptomator.lnk";

	private final String exePath;

	private boolean activatedUsingFolder;
	private boolean activatedUsingRegistry;

	public AutoStartWinStrategy(String exePath) {
		this.exePath = exePath;
		this.activatedUsingFolder = false;
		this.activatedUsingRegistry = false;
	}

	@Override
	public CompletionStage<Boolean> isAutoStartEnabled() {
		return isAutoStartEnabledUsingRegistry().thenCombine(isAutoStartEnabledUsingFolder(), (bReg, bFolder) -> bReg || bFolder);
	}

	private CompletableFuture<Boolean> isAutoStartEnabledUsingFolder() {
		Path autoStartEntry = Path.of(System.getProperty("user.home") + WINDOWS_START_MENU_ENTRY);
		this.activatedUsingFolder = Files.exists(autoStartEntry);
		return CompletableFuture.completedFuture(activatedUsingFolder);
	}

	private CompletableFuture<Boolean> isAutoStartEnabledUsingRegistry() {
		ProcessBuilder regQuery = new ProcessBuilder("reg", "query", HKCU_AUTOSTART_KEY, //
				"/v", AUTOSTART_VALUE);
		try {
			Process proc = regQuery.start();
			return proc.onExit().thenApply(p -> {
				this.activatedUsingRegistry = p.exitValue() == 0;
				return activatedUsingRegistry;
			});
		} catch (IOException e) {
			LOG.debug("Failed to query {} from registry key {}", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY);
			return CompletableFuture.completedFuture(false);
		}
	}

	@Override
	public void enableAutoStart() throws TogglingAutoStartFailedException {
		try {
			enableAutoStartUsingRegistry().thenAccept((Void v) -> this.activatedUsingRegistry = true).exceptionallyCompose(e -> {
				LOG.debug("Falling back to using autostart folder.");
				return this.enableAutoStartUsingFolder();
			}).thenAccept((Void v) -> this.activatedUsingFolder = true).get();
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			throw new TogglingAutoStartFailedException("Execution of enabling auto start setting was interrupted.");
		} catch (ExecutionException e) {
			throw new TogglingAutoStartFailedException("Enabling auto start failed both using registry and auto start folder.");
		}
	}

	private CompletableFuture<Void> enableAutoStartUsingRegistry() {
		ProcessBuilder regAdd = new ProcessBuilder("reg", "add", HKCU_AUTOSTART_KEY, //
				"/v", AUTOSTART_VALUE, //
				"/t", "REG_SZ", //
				"/d", "\"" + exePath + "\"", //
				"/f");
		try {
			Process proc = regAdd.start();
			boolean finishedInTime = waitForProcessOrCancel(proc, 5, TimeUnit.SECONDS);
			if (finishedInTime && proc.exitValue() == 0) {
				LOG.debug("Added {} to registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY);
				return CompletableFuture.completedFuture(null);
			} else {
				throw new IOException("Process exited with error code " + proc.exitValue());
			}
		} catch (IOException e) {
			LOG.debug("Registry could not be edited to set auto start.", e);
			return CompletableFuture.failedFuture(new SystemCommandException("Adding registry value failed."));
		}
	}

	private CompletableFuture<Void> enableAutoStartUsingFolder() {
		String autoStartFolderEntry = System.getProperty("user.home") + WINDOWS_START_MENU_ENTRY;
		String createShortcutCommand = "$s=(New-Object -COM WScript.Shell).CreateShortcut('" + autoStartFolderEntry + "');$s.TargetPath='" + exePath + "';$s.Save();";
		ProcessBuilder shortcutAdd = new ProcessBuilder("cmd", "/c", "Start powershell " + createShortcutCommand);
		try {
			Process proc = shortcutAdd.start();
			boolean finishedInTime = waitForProcessOrCancel(proc, 5, TimeUnit.SECONDS);
			if (finishedInTime && proc.exitValue() == 0) {
				LOG.debug("Created file {} for auto start.", autoStartFolderEntry);
				return CompletableFuture.completedFuture(null);
			} else {
				throw new IOException("Process exited with error code " + proc.exitValue());
			}
		} catch (IOException e) {
			LOG.debug("Adding entry to auto start folder failed.", e);
			return CompletableFuture.failedFuture(new SystemCommandException("Adding entry to auto start folder failed."));
		}
	}


	@Override
	public void disableAutoStart() throws TogglingAutoStartFailedException {
		if (activatedUsingRegistry) {
			disableAutoStartUsingRegistry().whenComplete((voit, ex) -> {
				if (ex == null) {
					this.activatedUsingRegistry = false;
				}
			});
		}

		if (activatedUsingFolder) {
			disableAutoStartUsingFolder().whenComplete((voit, ex) -> {
				if (ex == null) {
					this.activatedUsingFolder = false;
				}
			});
		}

		if (activatedUsingRegistry || activatedUsingFolder) {
			throw new TogglingAutoStartFailedException("Disabling auto start failed using registry and/or auto start folder.");
		}
	}

	public CompletableFuture<Void> disableAutoStartUsingRegistry() {
		ProcessBuilder regRemove = new ProcessBuilder("reg", "delete", HKCU_AUTOSTART_KEY, //
				"/v", AUTOSTART_VALUE, //
				"/f");
		try {
			Process proc = regRemove.start();
			boolean finishedInTime = waitForProcessOrCancel(proc, 5, TimeUnit.SECONDS);
			if (finishedInTime && proc.exitValue() == 0) {
				LOG.debug("Removed {} from registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY);
				return CompletableFuture.completedFuture(null);
			} else {
				throw new IOException("Process exited with error code " + proc.exitValue());
			}
		} catch (IOException e) {
			LOG.debug("Registry could not be edited to remove auto start.", e);
			return CompletableFuture.failedFuture(new SystemCommandException("Removing registry value failed."));
		}
	}

	private CompletableFuture<Void> disableAutoStartUsingFolder() {
		try {
			Files.delete(Path.of(WINDOWS_START_MENU_ENTRY));
			LOG.debug("Successfully deleted {}.", WINDOWS_START_MENU_ENTRY);
			return CompletableFuture.completedFuture(null);
		} catch (NoSuchFileException e) {
			//that is also okay
			return CompletableFuture.completedFuture(null);
		} catch (IOException e) {
			LOG.debug("Failed to delete entry from auto start folder.", e);
			return CompletableFuture.failedFuture(e);
		}
	}

	private static boolean waitForProcessOrCancel(Process proc, int timeout, TimeUnit timeUnit) {
		boolean finishedInTime = false;
		try {
			finishedInTime = proc.waitFor(timeout, timeUnit);
		} catch (InterruptedException e) {
			LOG.error("Timeout while reading registry", e);
			Thread.currentThread().interrupt();
		} finally {
			if (!finishedInTime) {
				proc.destroyForcibly();
			}
		}
		return finishedInTime;
	}

	private class SystemCommandException extends RuntimeException {

		public SystemCommandException(String msg) {
			super(msg);
		}
	}

}

M main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java => main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java +11 -0
@@ 1,5 1,6 @@
package org.cryptomator.ui.preferences;

import com.google.common.base.CharMatcher;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;


@@ 10,6 11,7 @@ import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextFormatter;

@PreferencesScoped
public class DonationKeyPreferencesController implements FxController {


@@ 32,6 34,15 @@ public class DonationKeyPreferencesController implements FxController {
	public void initialize() {
		donationKeyField.setText(licenseHolder.getLicenseKey().orElse(null));
		donationKeyField.textProperty().addListener(this::registrationKeyChanged);
		donationKeyField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength));
	}

	private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) {
		if (change.isContentChange()) {
			var strippedText = CharMatcher.whitespace().removeFrom(change.getText());
			change.setText(strippedText);
		}
		return change;
	}

	private void registrationKeyChanged(@SuppressWarnings("unused") ObservableValue<? extends String> observable, @SuppressWarnings("unused") String oldValue, String newValue) {

M main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java => main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +18 -41
@@ 5,6 5,8 @@ import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.KeychainBackend;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.integrations.autostart.AutoStartProvider;
import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;


@@ 14,10 16,8 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.geometry.NodeOrientation;
import javafx.scene.control.CheckBox;


@@ 42,7 42,7 @@ public class GeneralPreferencesController implements FxController {
	private final Stage window;
	private final Settings settings;
	private final boolean trayMenuSupported;
	private final Optional<AutoStartStrategy> autoStartStrategy;
	private final Optional<AutoStartProvider> autoStartProvider;
	private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
	private final LicenseHolder licenseHolder;
	private final ExecutorService executor;


@@ 61,11 61,11 @@ public class GeneralPreferencesController implements FxController {
	public RadioButton nodeOrientationRtl;

	@Inject
	GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartStrategy> autoStartStrategy, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) {
	GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartProvider> autoStartProvider, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) {
		this.window = window;
		this.settings = settings;
		this.trayMenuSupported = trayMenuSupported;
		this.autoStartStrategy = autoStartStrategy;
		this.autoStartProvider = autoStartProvider;
		this.keychainAccessProviders = keychainAccessProviders;
		this.selectedTabProperty = selectedTabProperty;
		this.licenseHolder = licenseHolder;


@@ 89,11 89,7 @@ public class GeneralPreferencesController implements FxController {

		debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode());

		autoStartStrategy.ifPresent(autoStart -> {
			autoStart.isAutoStartEnabled().thenAccept(enabled -> {
				Platform.runLater(() -> autoStartCheckbox.setSelected(enabled));
			});
		});
		autoStartProvider.ifPresent(autoStart -> autoStartCheckbox.setSelected(autoStart.isEnabled()));

		nodeOrientationLtr.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.LEFT_TO_RIGHT);
		nodeOrientationRtl.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.RIGHT_TO_LEFT);


@@ 114,7 110,7 @@ public class GeneralPreferencesController implements FxController {
	}

	public boolean isAutoStartSupported() {
		return autoStartStrategy.isPresent();
		return autoStartProvider.isPresent();
	}

	private void toggleNodeOrientation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {


@@ 129,15 125,19 @@ public class GeneralPreferencesController implements FxController {

	@FXML
	public void toggleAutoStart() {
		autoStartStrategy.ifPresent(autoStart -> {
		autoStartProvider.ifPresent(autoStart -> {
			boolean enableAutoStart = autoStartCheckbox.isSelected();
			Task<Void> toggleTask = new ToggleAutoStartTask(autoStart, enableAutoStart);
			toggleTask.setOnFailed(event -> {
			try {
				if (enableAutoStart) {
					autoStart.enable();
				} else {
					autoStart.disable();
				}
			} catch (ToggleAutoStartFailedException e) {
				autoStartCheckbox.setSelected(!enableAutoStart); // restore previous state
				LOG.error("Failed to toggle autostart.", event.getSource().getException());
				errorComponent.cause(event.getSource().getException()).window(window).returnToScene(window.getScene()).build().showErrorScene();
			});
			executor.execute(toggleTask);
				LOG.error("Failed to toggle autostart.", e);
				errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
			}
		});
	}



@@ 196,27 196,4 @@ public class GeneralPreferencesController implements FxController {
		}
	}

	private static class ToggleAutoStartTask extends Task<Void> {

		private final AutoStartStrategy autoStart;
		private final boolean enable;

		public ToggleAutoStartTask(AutoStartStrategy autoStart, boolean enable) {
			this.autoStart = autoStart;
			this.enable = enable;

			setOnFailed(event -> LOG.error("Failed to toggle Autostart", getException()));
		}

		@Override
		protected Void call() throws Exception {
			if (enable) {
				autoStart.enableAutoStart();
			} else {
				autoStart.disableAutoStart();
			}
			return null;
		}
	}

}

M main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java => main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java +1 -1
@@ 20,7 20,7 @@ import javafx.stage.Stage;
import java.util.Map;
import java.util.ResourceBundle;

@Module(includes = {AutoStartModule.class})
@Module
abstract class PreferencesModule {

	@Provides

M main/ui/src/main/java/org/cryptomator/ui/quit/QuitComponent.java => main/ui/src/main/java/org/cryptomator/ui/quit/QuitComponent.java +4 -5
@@ 5,7 5,6 @@
 *******************************************************************************/
package org.cryptomator.ui.quit;

import dagger.BindsInstance;
import dagger.Lazy;
import dagger.Subcomponent;
import org.cryptomator.ui.common.FxmlFile;


@@ 25,7 24,10 @@ public interface QuitComponent {
	@FxmlScene(FxmlFile.QUIT)
	Lazy<Scene> scene();

	default Stage showQuitWindow() {
	QuitController controller();

	default Stage showQuitWindow(QuitResponse response) {
		controller().updateQuitRequest(response);
		Stage stage = window();
		stage.setScene(scene().get());
		stage.show();


@@ 36,9 38,6 @@ public interface QuitComponent {
	@Subcomponent.Builder
	interface Builder {

		@BindsInstance
		Builder quitResponse(QuitResponse response);

		QuitComponent build();
	}


M main/ui/src/main/java/org/cryptomator/ui/quit/QuitController.java => main/ui/src/main/java/org/cryptomator/ui/quit/QuitController.java +24 -8
@@ 16,6 16,8 @@ import javafx.stage.Stage;
import java.awt.desktop.QuitResponse;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors;

@QuitScoped


@@ 24,26 26,40 @@ public class QuitController implements FxController {
	private static final Logger LOG = LoggerFactory.getLogger(QuitController.class);

	private final Stage window;
	private final QuitResponse response;
	private final ObservableList<Vault> unlockedVaults;
	private final ExecutorService executorService;
	private final VaultService vaultService;
	private final AtomicReference<QuitResponse> quitResponse = new AtomicReference<>();
	public Button lockAndQuitButton;

	@Inject
	QuitController(@QuitWindow Stage window, QuitResponse response, ObservableList<Vault> vaults, ExecutorService executorService, VaultService vaultService) {
	QuitController(@QuitWindow Stage window, ObservableList<Vault> vaults, ExecutorService executorService, VaultService vaultService) {
		this.window = window;
		this.response = response;
		this.unlockedVaults = vaults.filtered(Vault::isUnlocked);
		this.executorService = executorService;
		this.vaultService = vaultService;
		window.setOnCloseRequest(windowEvent -> cancel());
	}

	public void updateQuitRequest(QuitResponse newResponse) {
		var oldResponse = quitResponse.getAndSet(newResponse);
		if (oldResponse != null) {
			oldResponse.cancelQuit();
		}
	}

	private void respondToQuitRequest(Consumer<QuitResponse> action) {
		var response = quitResponse.getAndSet(null);
		if (response != null) {
			action.accept(response);
		}
	}

	@FXML
	public void cancel() {
		LOG.info("Quitting application canceled by user.");
		window.close();
		response.cancelQuit();
		respondToQuitRequest(QuitResponse::cancelQuit);
	}

	@FXML


@@ 56,16 72,16 @@ public class QuitController implements FxController {
			LOG.info("Locked {}", lockAllTask.getValue().stream().map(Vault::getDisplayName).collect(Collectors.joining(", ")));
			if (unlockedVaults.isEmpty()) {
				window.close();
				response.performQuit();
				respondToQuitRequest(QuitResponse::performQuit);
			}
		});
		lockAllTask.setOnFailed(evt -> {
			LOG.warn("Locking failed", lockAllTask.getException());
			lockAndQuitButton.setDisable(false);
			lockAndQuitButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
			// TODO: show force lock or force quit scene (and DO NOT cancelQuit() here!)
			// see https://github.com/cryptomator/cryptomator/blob/1.4.16/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java#L151-L163
			response.cancelQuit();
			// TODO: show force lock or force quit scene (and DO NOT cancelQuit() here!) (see https://github.com/cryptomator/cryptomator/pull/1416)
			window.close();
			respondToQuitRequest(QuitResponse::cancelQuit);
		});
		executorService.execute(lockAllTask);
	}

M main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java => main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java +1 -1
@@ 43,7 43,7 @@ abstract class QuitModule {
	@Provides
	@FxmlScene(FxmlFile.QUIT)
	@QuitScoped
	static Scene provideUnlockScene(@QuitWindow FXMLLoaderFactory fxmlLoaders) {
	static Scene provideQuitScene(@QuitWindow FXMLLoaderFactory fxmlLoaders) {
		return fxmlLoaders.createScene("/fxml/quit.fxml");
	}


M main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java => main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java +1 -1
@@ 108,7 108,7 @@ class TrayMenuController {
	}

	private void lockVault(Vault vault) {
		fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().lock(vault, false));
		fxApplicationStarter.get(true).thenAccept(app -> app.startLockWorkflow(vault, Optional.empty()));
	}

	private void lockAllVaults(ActionEvent actionEvent) {

M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java => main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java +2 -2
@@ 39,7 39,7 @@ public class GeneralVaultOptionsController implements FxController {
	public void initialize() {
		vaultName.textProperty().set(vault.getVaultSettings().displayName().get());
		vaultName.focusedProperty().addListener(this::trimVaultNameOnFocusLoss);
		vaultName.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength));
		vaultName.setTextFormatter(new TextFormatter<>(this::removeWhitespaces));
		unlockOnStartupCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().unlockAfterStartup());
		actionAfterUnlockChoiceBox.getItems().addAll(WhenUnlocked.values());
		actionAfterUnlockChoiceBox.valueProperty().bindBidirectional(vault.getVaultSettings().actionAfterUnlock());


@@ 53,7 53,7 @@ public class GeneralVaultOptionsController implements FxController {
		}
	}

	private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) {
	private TextFormatter.Change removeWhitespaces(TextFormatter.Change change) {
		if (change.isContentChange() && change.getControlNewText().length() > VAULTNAME_TRUNCATE_THRESHOLD) {
			return null; // reject any change that would lead to a text exceeding threshold
		} else {

A main/ui/src/main/resources/fxml/lock_failed.fxml => main/ui/src/main/resources/fxml/lock_failed.fxml +37 -0
@@ 0,0 1,37 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<VBox xmlns:fx="http://javafx.com/fxml"
	  xmlns="http://javafx.com/javafx"
	  fx:controller="org.cryptomator.ui.lock.LockFailedController"
	  minWidth="400"
	  maxWidth="400"
	  minHeight="145"
	  spacing="12">
	<padding>
		<Insets topRightBottomLeft="12"/>
	</padding>
	<children>
		<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
			<StackPane alignment="CENTER" HBox.hgrow="NEVER">
				<Circle styleClass="glyph-icon-red" radius="24"/>
				<FontAwesome5IconView styleClass="glyph-icon-white" glyph="TIMES" glyphSize="24"/>
			</StackPane>
			<VBox spacing="6">
				<Label styleClass="label-large" text="%lock.fail.heading"/>
				<FormattedLabel format="%lock.fail.message" arg1="${controller.vaultName}" wrapText="true"/>
			</VBox>
		</HBox>
		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
			<Button text="OK" defaultButton="false" VBox.vgrow="ALWAYS" cancelButton="true" onAction="#close"/>
		</VBox>
	</children>
</VBox>

A main/ui/src/main/resources/fxml/lock_forced.fxml => main/ui/src/main/resources/fxml/lock_forced.fxml +45 -0
@@ 0,0 1,45 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<VBox xmlns:fx="http://javafx.com/fxml"
	  xmlns="http://javafx.com/javafx"
	  fx:controller="org.cryptomator.ui.lock.LockForcedController"
	  minWidth="400"
	  maxWidth="400"
	  minHeight="145"
	  spacing="12">
	<padding>
		<Insets topRightBottomLeft="12"/>
	</padding>
	<children>
		<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
			<StackPane alignment="CENTER" HBox.hgrow="NEVER">
				<Circle styleClass="glyph-icon-orange" radius="24"/>
				<FontAwesome5IconView styleClass="glyph-icon-white" glyph="EXCLAMATION" glyphSize="24"/>
			</StackPane>
			<VBox spacing="6">
				<Label styleClass="label-large" text="%lock.forced.heading"/>
				<FormattedLabel format="%lock.forced.message" arg1="${controller.vaultName}" wrapText="true"/>
			</VBox>
		</HBox>

		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
			<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
				<buttons>
					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="true" cancelButton="true" onAction="#cancel"/>
					<!-- TODO: third button with retry? -->
					<Button text="%lock.forced.confirmBtn" ButtonBar.buttonData="FINISH" onAction="#confirmForcedLock"/>
				</buttons>
			</ButtonBar>
		</VBox>
	</children>
</VBox>

M main/ui/src/main/resources/i18n/strings.properties => main/ui/src/main/resources/i18n/strings.properties +9 -0
@@ 108,6 108,15 @@ unlock.error.heading=Unable to unlock vault
unlock.error.invalidMountPoint.notExisting=Mount point "%s" is not a directory, not empty or does not exist.
unlock.error.invalidMountPoint.existing=Mount point "%s" already exists or parent folder is missing.

# Lock
## Force
lock.forced.heading=Graceful lock failed
lock.forced.message=Locking "%s" was blocked by pending operations or open files. You can force lock this vault, however interrupting I/O may result in the loss of unsaved data.
lock.forced.confirmBtn=Force Lock
## Failure
lock.fail.heading=Locking vault failed.
lock.fail.message=Vault "%s" could not be locked. Ensure unsaved work is saved elsewhere and important Read/Write operations are finished. In order to close the vault, kill the Cryptomator process.

# Migration
migration.title=Upgrade Vault
## Start

M main/ui/src/main/resources/i18n/strings_ar.properties => main/ui/src/main/resources/i18n/strings_ar.properties +4 -0
@@ 106,6 106,10 @@ unlock.success.revealBtn=افتح الحافظة
unlock.error.invalidMountPoint.notExisting=نقطة التحميل ليست مجلد فارغ أو غير موجودة: %s
unlock.error.invalidMountPoint.existing=نقطة/مجلد التحميل موجود بالفعل أو المجلد الأصل مفقود: %s

# Lock
## Force
## Failure

# Migration
migration.title=ترقية الحافظة
## Start

M main/ui/src/main/resources/i18n/strings_bs.properties => main/ui/src/main/resources/i18n/strings_bs.properties +4 -0
@@ 106,6 106,10 @@ unlock.error.heading=Sef nije moguće otključati
### Invalid Mount Point
unlock.error.invalidMountPoint.notExisting=Tačka postavljanja "%s" nije direktorij, nije prazna ili ne postoji.

# Lock
## Force
## Failure

# Migration
## Start
## Run

M main/ui/src/main/resources/i18n/strings_ca.properties => main/ui/src/main/resources/i18n/strings_ca.properties +53 -2
@@ 17,6 17,7 @@ generic.error.title=S'ha produït un error inesperat
generic.error.instruction=Això no hauria d'haver passat. Si us plau, informeu del text de l'error i inclogueu una descripció de quins passos han dut a aquest error.

# Defaults
defaults.vault.vaultName=Caixa forta

# Tray Menu
traymenu.showMainWindow=Mostra


@@ 44,6 45,7 @@ addvaultwizard.new.directoryPickerLabel=Ubicació personalitzada
addvaultwizard.new.directoryPickerButton=Trieu…
addvaultwizard.new.directoryPickerTitle=Seleccioneu el directori
addvaultwizard.new.fileAlreadyExists=No es pot crear una caixa forta en aquest camí perquè ja hi ha algun objecte.
addvaultwizard.new.locationDoesNotExist=No ha estat possible crear la caixa forta en aquest camí perquè, si més no, un component no existeix.
addvaultwizard.new.invalidName=El nom de la caixa forta no és vàlid. Si us plau, escribiu un mom de directori amb caràcters estàndard.
### Password
addvaultwizard.new.createVaultBtn=Crea la caixa forta


@@ 100,10 102,19 @@ unlock.success.message="%s" s'ha desbloquejat correctament! Ja es pot accedir a 
unlock.success.rememberChoice=Recorda l'elecció. No ho tornis a mostrar.
unlock.success.revealBtn=Mostra la caixa forta
## Failure
unlock.error.heading=No ha estat possible desblocar la caixa forta
### Invalid Mount Point
unlock.error.invalidMountPoint.notExisting=El punt de muntatge no és un directori buit, o no existeix: %s
unlock.error.invalidMountPoint.existing=El punt de muntatge o la carpeta ja existeix, o no es pot accedir al directori superior: %s

# Lock
## Force
lock.forced.message=No s'ha blocat "%s" perquè hi ha operacions pendents o fitxers oberts. Podeu forçar-ne el blocatge però heu de saber que interrompre l'entrada/sortida pot produir la pèrdua de dades.
lock.forced.confirmBtn=Força el blocatge
## Failure
lock.fail.heading=Ha fallat el blocatge de la caixa forta.
lock.fail.message=La caixa forta "%s" no s'ha pogut blocar. Assegureu-vos que el treball s'ha desat en algun altre lloc i que les operacions de lectura/escriptura han acabat. Per tal de tancar la caixa, mateu el procés Cryptomator.

# Migration
migration.title=Actualitza la caixa forta
## Start


@@ 133,13 144,18 @@ preferences.title=Preferències
## General
preferences.general=General
preferences.general.theme=Apariència
preferences.general.theme.automatic=Automàtic
preferences.general.theme.light=Clar
preferences.general.theme.dark=Fosc
preferences.general.unlockThemes=Desbloqueja el tema fosc
preferences.general.startHidden=Amaga la finestra al iniciar Cryptomator
preferences.general.startHidden=Amaga la finestra quan s'inicia Cryptomator
preferences.general.debugLogging=Habilita el registre de depuració
preferences.general.debugDirectory=Mostra els fitxers de registres
preferences.general.autoStart=Executa Cryptomator en engegar el sistema
preferences.general.keychainBackend=Desar contrasenyes amb
preferences.general.keychainBackend.org.cryptomator.linux.keychain.SecretServiceKeychainAccess=Anell de claus de Gnome
preferences.general.keychainBackend.org.cryptomator.linux.keychain.KDEWalletKeychainAccess=Cartera de KDE
preferences.general.keychainBackend.org.cryptomator.macos.keychain.MacSystemKeychainAccess=Accés a clauers macOS
preferences.general.interfaceOrientation=Orientació de la interfície
preferences.general.interfaceOrientation.ltr=Esquerra a dreta
preferences.general.interfaceOrientation.rtl=Dreta a esquerra


@@ 163,38 179,72 @@ preferences.donationKey.getDonationKey=Obtingau una clau de donació
preferences.about=Quant a

# Vault Statistics
stats.title=Estadístiques per a %s
## Read
stats.read.throughput.idle=Llegit: inactiu
stats.read.throughput.kibs=Llegit: %.2f kiB/s
stats.read.throughput.mibs=Llegit: %.2f MiB/s
stats.read.total.data.none=Dades llegides: -
stats.read.total.data.kib=Dades llegides: %.1f kiB
stats.read.total.data.mib=Dades llegides: %.1f MiB
stats.read.total.data.gib=Dades llegides: %.1f GiB
stats.decr.total.data.none=Dades desxifrades: -
stats.decr.total.data.kib=Dades desxifrades: %.1f kiB
stats.decr.total.data.mib=Dades desxifrades: %.1f MiB
stats.decr.total.data.gib=Dades desxifrades: %.1f GiB
stats.read.accessCount=Lectures en total: %d
## Write
stats.write.throughput.idle=Escriu: inactiu
stats.write.throughput.kibs=Escriu: %.2f kiB/s
stats.write.throughput.mibs=Escriu: %.2f MiB/s
stats.write.total.data.none=Dades llegides: -
stats.write.total.data.kib=Dades escrites: %.1f kiB
stats.write.total.data.mib=Dades escrites: %.1f MiB
stats.write.total.data.gib=Dades escrites: %.1f GiB
stats.encr.total.data.none=Dades xifrades: -
stats.encr.total.data.kib=Dades xifrades: %.1f kiB
stats.encr.total.data.mib=Dades xifrades: %.1f MiB
stats.encr.total.data.gib=Dades xifrades: %.1f GiB
stats.write.accessCount=Total escrits: %d

# Main Window
main.closeBtn.tooltip=Tanca
main.minimizeBtn.tooltip=Minimitza
main.preferencesBtn.tooltip=Preferències
main.debugModeEnabled.tooltip=Mode depuració activat
main.donationKeyMissing.tooltip=Si us plau, considereu fer una donació
## Drag 'n' Drop
main.dropZone.dropVault=Afegeix aquesta caixa forta
main.dropZone.unknownDragboardContent=Si voleu afegir una caixa forta, arrossegueu-la a aquesta finestra
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Feu clic aquí per afegir una caixa forta
main.vaultlist.contextMenu.remove=Elimina la caixa forta…
main.vaultlist.addVaultBtn=Afegir una caixa forta
## Vault Detail
### Welcome
main.vaultDetail.welcomeOnboarding=Gràcies per escollir Cryptomator per protegir els vostres fitxers. Si vos cal ajuda, llegiu les nostres guies per donar els Primers passos:
### Locked
main.vaultDetail.lockedStatus=BLOQUEJADA
main.vaultDetail.unlockBtn=Desbloca…
main.vaultDetail.unlockNowBtn=Desbloqueja ara
main.vaultDetail.optionsBtn=Opcions de la caixa forta
main.vaultDetail.passwordSavedInKeychain=Contrasenya desada
### Unlocked
main.vaultDetail.unlockedStatus=DESBLOQUEJADA
main.vaultDetail.accessLocation=Els continguts de la vostra caixa forta són accessibles aquí:
main.vaultDetail.revealBtn=Mostra la unitat
main.vaultDetail.lockBtn=Bloqueja
main.vaultDetail.bytesPerSecondRead=Lectura:
main.vaultDetail.bytesPerSecondWritten=Escriu:
main.vaultDetail.throughput.idle=inactiu
main.vaultDetail.throughput.kbps=%.1f kiB/s
main.vaultDetail.throughput.mbps=%.1f MiB/s
main.vaultDetail.stats=Estadístiques de la caixa forta
### Missing
main.vaultDetail.missing.info=Cryptomator no ha trobat una caixa forta en aquest camí.
main.vaultDetail.missing.recheck=Torna a comprovar
main.vaultDetail.missing.remove=Eliminar de la llista de la caixa forta…
main.vaultDetail.missing.changeLocation=Canvia la localització de la caixa forta…
### Needs Migration
main.vaultDetail.migrateButton=Actualitza la caixa forta
main.vaultDetail.migratePrompt=Per accedir a la vostra caixa forta abans cal actualitzar-la al nou format


@@ 213,7 263,7 @@ wrongFileAlert.link=Per rebre assistència, visiteu
## General
vaultOptions.general=General
vaultOptions.general.vaultName=Nom de la caixa forta
vaultOptions.general.unlockAfterStartup=Desbloqueja la caixa forta al iniciar Cryptomator
vaultOptions.general.unlockAfterStartup=Desbloqueja la caixa forta quan s'inicia Cryptomator
vaultOptions.general.actionAfterUnlock=Després d'un desbloqueig correcte
vaultOptions.general.actionAfterUnlock.ignore=No facis res
vaultOptions.general.actionAfterUnlock.reveal=Mostra la unitat


@@ 232,6 282,7 @@ vaultOptions.mount.mountPoint.directoryPickerTitle=Esculliu un directori buit
## Master Key
vaultOptions.masterkey=Contrasenya
vaultOptions.masterkey.changePasswordBtn=Canvi de contrasenya
vaultOptions.masterkey.forgetSavedPasswordBtn=Oblida la contrasenya desada
vaultOptions.masterkey.recoveryKeyExpanation=La clau de recuperació és l'unic mitjà de restaurar l'accès a la caixa forta en cas de perdre la contrasenya.
vaultOptions.masterkey.showRecoveryKeyBtn=Mostra la clau de recuperació
vaultOptions.masterkey.recoverPasswordBtn=Recupera la contrasenya

M main/ui/src/main/resources/i18n/strings_cs.properties => main/ui/src/main/resources/i18n/strings_cs.properties +4 -0
@@ 107,6 107,10 @@ unlock.error.heading=Nelze odemknout trezor
unlock.error.invalidMountPoint.notExisting=Připojovací bod %s není složkou, není prázdný nebo neexistuje.
unlock.error.invalidMountPoint.existing=Připojovací bod %s již existuje nebo nadřazená složka chybí.

# Lock
## Force
## Failure

# Migration
migration.title=Upgrade trezoru
## Start

M main/ui/src/main/resources/i18n/strings_de.properties => main/ui/src/main/resources/i18n/strings_de.properties +9 -0
@@ 107,6 107,15 @@ unlock.error.heading=Tresor konnte nicht entsperrt werden
unlock.error.invalidMountPoint.notExisting=Einhängepunkt ist kein leeres Verzeichnis oder existiert nicht: %s
unlock.error.invalidMountPoint.existing=Einhängepunkt/Ordner bereits vorhanden oder übergeordneter Ordner fehlt: %s

# Lock
## Force
lock.forced.heading=Tresor konnte nicht kontrolliert gesperrt werden
lock.forced.message=Das Sperren von „%s“ wurde durch noch ablaufende Vorgänge oder offene Dateien verhindert. Sie können das Sperren dieses Tresors erzwingen, allerdings kann dies zum Verlust ungespeicherter Daten führen.
lock.forced.confirmBtn=Sperren erzwingen
## Failure
lock.fail.heading=Der Tresor konnte nicht gesperrt werden.
lock.fail.message=Der Tresor „%s“ konnte nicht gesperrt werden. Stellen Sie sicher, dass Sie Ihre ungespeicherten Arbeit an anderer Stelle speichern und wichtige Lese-/Schreibvorgänge abgeschlossen sind. Um den Tresor zu schließen, beenden Sie den Cryptomator-Prozess.

# Migration
migration.title=Tresor aktualisieren
## Start

M main/ui/src/main/resources/i18n/strings_el.properties => main/ui/src/main/resources/i18n/strings_el.properties +10 -0
@@ 102,10 102,20 @@ unlock.success.message="%s" ξεκλειδώθηκε επιτυχώς! Το vaul
unlock.success.rememberChoice=Απομνημόνευση επιλογής, μην ρωτήσεις ξανά
unlock.success.revealBtn=Αποκάλυψη Vault
## Failure
unlock.error.heading=Αδυναμία ξεκλειδώματος vault
### Invalid Mount Point
unlock.error.invalidMountPoint.notExisting=Το σημείο προσάρτησης δεν είναι κενός φάκελος ή δεν υπάρχει: %s
unlock.error.invalidMountPoint.existing=Το σημείο/φάκελος προσάρτησης υπάρχει ήδη ή ο γονικός φάκελος λείπει: %s

# Lock
## Force
lock.forced.heading=Το κανονικό κλείδωμα απέτυχε
lock.forced.message=Το κλείδωμα "%s" μπλοκαρίστηκε από εκκρεμείς διεργασίες ή ανοιχτά αρχεία. Μπορείτε να εξαναγκάσετε το κλείδωμα του vault, αλλά η διακοπή Ι/Ο ενδέχεται να οδηγήσει σε απώλεια μη αποθηκευμένων δεδομένων.
lock.forced.confirmBtn=Εξαναγκασμένο κλείδωμα
## Failure
lock.fail.heading=Το κλείδωμα του vault απέτυχε.
lock.fail.message=Το Vault "%s" δεν κλειδώθηκε. Εξασφαλίστε την αποθήκευση της εργασίας σε άλλο σημείο και πως οι σημαντικές διεργασίας Ανάγνωσης/Εγγραφής έχουν ολοκληρωθεί. Για να κλείσετε το vault, τερματίστε τη διεργασία του Cryptomator.

# Migration
migration.title=Αναβάθμιση Vault
## Start

M main/ui/src/main/resources/i18n/strings_es.properties => main/ui/src/main/resources/i18n/strings_es.properties +8 -0
@@ 107,6 107,14 @@ unlock.error.heading=No se puede desbloquear la bóveda
unlock.error.invalidMountPoint.notExisting=El punto de montaje no es un directorio vacío o no existe: %s
unlock.error.invalidMountPoint.existing=El punto de montaje/carpeta ya existe o falta la carpeta padre: %s

# Lock
## Force
lock.forced.message=El bloqueo de "%s" fue bloqueado por operaciones pendientes o archivos abiertos. Puede forzar el bloqueo de esta bóveda, sin embargo, interrumpir la I/O puede provocar la pérdida de datos no guardados.
lock.forced.confirmBtn=Forzar bloqueo
## Failure
lock.fail.heading=Falló al bloquear la bóveda.
lock.fail.message=No se pudo bloquear la bóveda "%s". Asegúrese de que el trabajo no guardado se ha guardado en otro lugar y las operaciones de lectura/escritura importantes han finalizado. Para cerrar la bóveda termine el proceso de Cryptomator.

# Migration
migration.title=Mejorar bóveda
## Start

M main/ui/src/main/resources/i18n/strings_fr.properties => main/ui/src/main/resources/i18n/strings_fr.properties +9 -0
@@ 107,6 107,15 @@ unlock.error.heading=Impossible de déverrouiller le coffre
unlock.error.invalidMountPoint.notExisting=Le point de montage «%s» n'est pas un répertoire, n'est pas vide ou n'existe pas.
unlock.error.invalidMountPoint.existing=Le point de montage/le répertoire existe déjà ou le répertoire parent est manquant: %s

# Lock
## Force
lock.forced.heading=Le verrouillage normal a échoué
lock.forced.message=Le verrouillage de «%s» a été bloqué par des opérations en attente ou des fichiers ouverts. Vous pouvez forcer le verrouillage de ce coffre, mais l'interruption d'E/S peut entraîner la perte de données non enregistrées.
lock.forced.confirmBtn=Forcer le verrouillage
## Failure
lock.fail.heading=Le verrouillage du coffre-fort a échoué.
lock.fail.message=Le coffre-fort "%s" n'a pas pu être verrouillé. Assurez-vous que le travail non sauvegardé est sauvegardé ailleurs et que les opérations importantes de lecture/écriture sont bien terminées. Pour fermer le coffre-fort, tuez le processus Cryptomator.

# Migration
migration.title=Mettre à jour le coffre
## Start

M main/ui/src/main/resources/i18n/strings_hi.properties => main/ui/src/main/resources/i18n/strings_hi.properties +4 -0
@@ 62,6 62,10 @@ unlock.unlockBtn=अनलॉक करें
## Failure
### Invalid Mount Point

# Lock
## Force
## Failure

# Migration
migration.title=वाउल्ट को अपग्रेड करें
## Start

M main/ui/src/main/resources/i18n/strings_hr.properties => main/ui/src/main/resources/i18n/strings_hr.properties +4 -0
@@ 29,6 29,10 @@
## Failure
### Invalid Mount Point

# Lock
## Force
## Failure

# Migration
## Start
## Run

M main/ui/src/main/resources/i18n/strings_id.properties => main/ui/src/main/resources/i18n/strings_id.properties +4 -0
@@ 81,6 81,10 @@ unlock.unlockBtn=Buka Gembok
## Failure
### Invalid Mount Point

# Lock
## Force
## Failure

# Migration
## Start
## Run

M main/ui/src/main/resources/i18n/strings_it.properties => main/ui/src/main/resources/i18n/strings_it.properties +8 -0
@@ 102,10 102,15 @@ unlock.success.message=Sbloccato "%s" con successo! La tua cassaforte è ora acc
unlock.success.rememberChoice=Ricorda la scelta, non mostrare ancora
unlock.success.revealBtn=Rivela Cassaforte
## Failure
unlock.error.heading=Impossibile sbloccare la cassaforte
### Invalid Mount Point
unlock.error.invalidMountPoint.notExisting=Il punto di montaggio non è una directory vuota o non esiste: %s
unlock.error.invalidMountPoint.existing=Il punto di Mount/cartella esiste già o la cartella superiore è mancante: %s

# Lock
## Force
## Failure

# Migration
migration.title=Aggiorna Cassaforte
## Start


@@ 171,8 176,11 @@ preferences.donationKey.getDonationKey=Ottieni una chiave di donazione
preferences.about=Informazioni

# Vault Statistics
stats.title=Statistiche per %s
## Read
stats.read.total.data.none=Dati letti: -
## Write
stats.write.total.data.none=Dati letti: -

# Main Window
main.closeBtn.tooltip=Chiudi

M main/ui/src/main/resources/i18n/strings_ja.properties => main/ui/src/main/resources/i18n/strings_ja.properties +9 -0
@@ 106,6 106,15 @@ unlock.error.heading=金庫の解錠に失敗
### Invalid Mount Point
unlock.error.invalidMountPoint.notExisting=マウントポイントが空のディレクトリか存在していません: %s

# Lock
## Force
lock.forced.heading=正常な施錠に失敗しました
lock.forced.message=保留中の操作または、開かれたファイルによって、"%s" の施錠が中断されました。この金庫を強制的に施錠することはできますが、I/O を中断すると保存されていないデータを失う可能性があります。
lock.forced.confirmBtn=強制施錠
## Failure
lock.fail.heading=金庫の施錠に失敗しました。
lock.fail.message=金庫 "%s" を施錠できませんでした。保存されていないデータがほかの場所に保存され、重要な読み込み/書き込み操作が完了していることを確認してください。金庫を閉じるには、Cryptomator のプロセスを強制終了してください。

# Migration
migration.title=金庫をアップグレード
## Start

M main/ui/src/main/resources/i18n/strings_ko.properties => main/ui/src/main/resources/i18n/strings_ko.properties +9 -0
@@ 107,6 107,15 @@ unlock.error.heading=Vault 잠금을 해제 할 수 없습니다.
unlock.error.invalidMountPoint.notExisting=구성지점이 존재하지 않거나 빈 디렉터리가 아닙니다: %s
unlock.error.invalidMountPoint.existing=구성지점/폴더가 이미 존재하거나 부모 폴더가 없습니다: %s

# Lock
## Force
lock.forced.heading=정상적인 잠금을 실패하였습니다.
lock.forced.message=대기 중인 작동이나 파일이 열려있어 "%s"를 잠그는데 실패하였습니다. 이 Vault를 강제로 잠글 수 있으나, 입/출력의 중단은 저장되지 않은 데이터의 유실을 초래할 수 있습니다.
lock.forced.confirmBtn=강제 잠금
## Failure
lock.fail.heading=Vault 잠금에 실패하였습니다.
lock.fail.message="%s" Vault를 잠글 수 없습니다. 저장되지 않은 작업이 다른 곳에 저장된 것과 중요한 읽기/쓰기 동작이 완료되었는지 확인 하십시요. Vault를 닫기 위해, Cryptomator 프로세스를 강제로 종료 하십시요.

# Migration
migration.title=Vault 업그레이드
## Start

M main/ui/src/main/resources/i18n/strings_lv.properties => main/ui/src/main/resources/i18n/strings_lv.properties +4 -0
@@ 101,6 101,10 @@ unlock.success.revealBtn=Atklāt glabātuvi
## Failure
### Invalid Mount Point

# Lock
## Force
## Failure

# Migration
migration.title=Jaunināt glabātuvi
## Start

M main/ui/src/main/resources/i18n/strings_nb.properties => main/ui/src/main/resources/i18n/strings_nb.properties +38 -0
@@ 102,7 102,16 @@ unlock.success.message=Vellykket opplåsning av "%s"! Hvelvet ditt er nå tilgje
unlock.success.rememberChoice=Husk valget - ikke vis dette igjen
unlock.success.revealBtn=Gjør hvelvet synlig
## Failure
unlock.error.heading=Klarer ikke å låse opp hvelvet
### Invalid Mount Point
unlock.error.invalidMountPoint.notExisting=Monteringspunktet "%s" er enten ikke en mappe, ikke tom eller eksisterer ikke.
unlock.error.invalidMountPoint.existing=Monteringspunktet "%s" finnes enten allerede eller at overordnet mappe mangler.

# Lock
## Force
lock.forced.confirmBtn=Tving låsing
## Failure
lock.fail.heading=Låsing av hvelvet mislyktes.

# Migration
migration.title=Oppgrader hvelv


@@ 141,6 150,7 @@ preferences.general.startHidden=Skjul vinduet når du starter Cryptomator
preferences.general.debugLogging=Aktiver loggføring av feilsøk
preferences.general.debugDirectory=Vis loggfiler
preferences.general.autoStart=Start Cryptomator ved systemstart
preferences.general.keychainBackend=Lagre passord med
preferences.general.interfaceOrientation=Grensesnittorientering
preferences.general.interfaceOrientation.ltr=Fra venstre til høyre
preferences.general.interfaceOrientation.rtl=Fra høyre til venstre


@@ 164,8 174,34 @@ preferences.donationKey.getDonationKey=Få en donasjonsnøkkel
preferences.about=Om

# Vault Statistics
stats.title=Statistikk for %s
stats.cacheHitRate=Cache treffrate
## Read
stats.read.throughput.idle=Les: inaktiv
stats.read.throughput.kibs=Lest: %.2f kiB/s
stats.read.throughput.mibs=Lest: %.2f MiB/s
stats.read.total.data.none=Data lest: -
stats.read.total.data.kib=Data lest: %.1f kiB
stats.read.total.data.mib=Data lest: %.1f MiB
stats.read.total.data.gib=Data lest: %.1f GiB
stats.decr.total.data.none=Data dekryptert: -
stats.decr.total.data.kib=Data dekryptert: %.1f kiB
stats.decr.total.data.mib=Data dekryptert: %.1f MiB
stats.decr.total.data.gib=Data dekryptert: %.1f GiB
stats.read.accessCount=Lesninger totalt: %d
## Write
stats.write.throughput.idle=Skrive: inaktiv
stats.write.throughput.kibs=Skriver: %.2f kiB/s
stats.write.throughput.mibs=Skriver: %.2f MiB/s
stats.write.total.data.none=Data lest: -
stats.write.total.data.kib=Data skrevet: %.1f kiB
stats.write.total.data.mib=Data skrevet: %.1f MiB
stats.write.total.data.gib=Data skrevet: %.1f GiB
stats.encr.total.data.none=Data kryptert: -
stats.encr.total.data.kib=Data kryptert: %.1f kiB
stats.encr.total.data.mib=Data kryptert: %.1f MiB
stats.encr.total.data.gib=Data kryptert: %.1f GiB
stats.write.accessCount=Skrivninger totalt: %d

# Main Window
main.closeBtn.tooltip=Lukk


@@ 195,9 231,11 @@ main.vaultDetail.accessLocation=Innholdet i hvelvet ditt er tilgjengelig her:
main.vaultDetail.revealBtn=Gjør enheten synlig
main.vaultDetail.lockBtn=Lås
main.vaultDetail.bytesPerSecondRead=Lesehastighet:
main.vaultDetail.bytesPerSecondWritten=Skriv:
main.vaultDetail.throughput.idle=inaktiv
main.vaultDetail.throughput.kbps=%.1f kiB/s
main.vaultDetail.throughput.mbps=%.1f MiB/s
main.vaultDetail.stats=Hvelvstatistikk
### Missing
main.vaultDetail.missing.info=Cryptomator kunne ikke finne et hvelv på denne søkestien.
main.vaultDetail.missing.recheck=Kontroller igjen

M main/ui/src/main/resources/i18n/strings_nl.properties => main/ui/src/main/resources/i18n/strings_nl.properties +6 -0
@@ 102,6 102,10 @@ unlock.success.revealBtn=Toon kluis
## Failure
### Invalid Mount Point

# Lock
## Force
## Failure

# Migration
migration.title=Kluis upgraden
## Start


@@ 165,6 169,7 @@ main.dropZone.dropVault=Voeg deze kluis toe
main.dropZone.unknownDragboardContent=Als u een kluis wilt toevoegen, sleep deze dan naar dit venster
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Klik hier om een kluis toe te voegen
main.vaultlist.contextMenu.remove=Verwijder Kluis…
main.vaultlist.addVaultBtn=Kluis toevoegen
## Vault Detail
### Welcome


@@ 184,6 189,7 @@ main.vaultDetail.throughput.kbps=%.1f kiB/s
main.vaultDetail.throughput.mbps=%.1f MiB/s
### Missing
main.vaultDetail.missing.info=Cryptomator kon op dit pad geen kluis vinden.
main.vaultDetail.missing.remove=Verwijderen van kluislijst…
### Needs Migration
main.vaultDetail.migrateButton=Kluis upgraden
main.vaultDetail.migratePrompt=Uw kluis moet worden bijgewerkt naar een nieuw formaat, voordat u deze kunt openen

M main/ui/src/main/resources/i18n/strings_nn.properties => main/ui/src/main/resources/i18n/strings_nn.properties +4 -0
@@ 103,6 103,10 @@ unlock.success.revealBtn=Gjer kvelven synleg
## Failure
### Invalid Mount Point

# Lock
## Force
## Failure

# Migration
migration.title=Oppgrader kvelv
## Start

M main/ui/src/main/resources/i18n/strings_pa.properties => main/ui/src/main/resources/i18n/strings_pa.properties +9 -0
@@ 107,6 107,15 @@ unlock.error.heading=ਵਾਲਟ ਅਣ-ਲਾਕ ਕਰਨ ਲਈ ਅਸਮਰ
unlock.error.invalidMountPoint.notExisting="%s" ਮਾਊਂਟ ਪੁਆਇੰਟ ਡਾਇਰੈਕਟਰੀ ਨਹੀਂ, ਖਾਲੀ ਨਹੀਂ ਜਾਂ ਮੌਜੂਦ ਹੀ ਨਹੀਂ ਹੈ।
unlock.error.invalidMountPoint.existing="%s" ਮਾਊਂਟ ਪੁਆਇੰਟ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ ਜਾਂ ਉਸ ਦਾ ਮੂਲ ਫੋਲਡਰ ਗੁੰਮ ਹੈ।

# Lock
## Force
lock.forced.heading=ਸਧਾਰਨ ਲਾਕ ਕਰਨਾ ਅਸਫ਼ਲ ਹੈ
lock.forced.message=ਬਾਕੀ ਰਹਿੰਦੀਆਂ ਕਾਰਵਾਈਆਂ ਜਾਂ ਫ਼ਾਈਲਾਂ ਖੁੱਲ੍ਹਣ ਕਰਕੇ "%s" ਲਾਕ ਕਰਨ ਨੂੰ ਰੋਕਿਆ ਗਿਆ ਹੈ। ਤੁਸੀਂ ਇਸ ਵਾਲਟ ਨੂੰ ਧੱਕੇ ਨਾਲ ਲਾਕ ਕਰ ਸਕਦੇ ਹੋ, ਪਰ I/O ਵਿੱਚ ਰੁਕਾਵਟ ਪਾਉਣ ਨਾਲ ਨਾ-ਸੰਭਾਲਿਆ ਡਾਟਾ ਖਤਮ ਹੋ ਜਾ ਸਕਦਾ ਹੈ।
lock.forced.confirmBtn=ਧੱਕੇ ਨਾਲ ਲਾਕ ਕਰੋ
## Failure
lock.fail.heading=ਵਾਲਟ ਲਾਕ ਕਰਨਾ ਅਸਫ਼ਲ ਹੈ।
lock.fail.message=ਵਾਲਟ "%s" ਨੂੰ ਲਾਕ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ। ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਨਾ-ਸੰਭਾਲੇ ਕੰਮ ਨੂੰ ਹੋਰ ਥਾਂ ਸੰਭਾਲ ਲਿਆ ਹੈ ਅਤੇ ਖਾਸ ਪੜ੍ਹਨ/ਲਿਖਣ ਕਾਰਵਾਈਆਂ ਪੂਰੀਆਂ ਹੋਈਆਂ ਹਨ। ਵਾਲਟ ਨੂੰ ਬੰਦ ਕਰਨ ਲਈ Cryptomator ਕਾਰਵਾਈ ਨੂੰ ਖਤਮ ਕਰੋ।

# Migration
migration.title=ਵਾਲਟ ਅੱਪਗਰੇਡ ਕਰੋ
## Start

M main/ui/src/main/resources/i18n/strings_pl.properties => main/ui/src/main/resources/i18n/strings_pl.properties +12 -0
@@ 107,6 107,15 @@ unlock.error.heading=Nie można odblokować sejfu
unlock.error.invalidMountPoint.notExisting=Punkt montowania nie jest pustym katalogiem lub nie istnieje: %s
unlock.error.invalidMountPoint.existing=Punkt montowania już istnieje lub brakuje katalogu nadrzędnego: %s

# Lock
## Force
lock.forced.heading=Blokada nie powiodła się
lock.forced.message=Zamknięcie "%s" zostało zablokowane przez oczekujące operacje lub otwarte pliki. Możesz wymusić zamknięcie tego sejfu, ale może to spowodować utratę niezapisanych danych.
lock.forced.confirmBtn=Wymuś zablokowanie
## Failure
lock.fail.heading=Błąd blokowania sejfu.
lock.fail.message=Nie można zablokować sejfu "%s". Zapisz wszelkie zmiany w bezpiecznym miejscu i upewnij się, że nie ma żadnych ważnych oczekujących operacji odczytu/zapisu. W celu zamknięcia sejfu ubij Cryptomator.

# Migration
migration.title=Aktualizuj sejf
## Start


@@ 144,6 153,7 @@ preferences.general.startHidden=Ukryj okno podczas uruchamiania programu Cryptom
preferences.general.debugLogging=Włącz logowanie w trybie debug
preferences.general.debugDirectory=Pokaż pliki logowania
preferences.general.autoStart=Uruchom Cryptomator po uruchomieniu systemu
preferences.general.keychainBackend=Przechowuj hasła za pomocą
preferences.general.keychainBackend.org.cryptomator.linux.keychain.SecretServiceKeychainAccess=Brelok kluczy Gnome
preferences.general.keychainBackend.org.cryptomator.linux.keychain.KDEWalletKeychainAccess=Portfel KDE
preferences.general.keychainBackend.org.cryptomator.macos.keychain.MacSystemKeychainAccess=Dostęp do pęku kluczy macOS


@@ 171,6 181,8 @@ preferences.donationKey.getDonationKey=Zdobądź klucz donacji
preferences.about=O programie

# Vault Statistics
stats.title=Statystyki dla %s
stats.cacheHitRate=Trafność cache
## Read
stats.read.throughput.idle=Odczyt: bezczynny
stats.read.throughput.kibs=Odczyt: %.2f kiB/s

M main/ui/src/main/resources/i18n/strings_pt.properties => main/ui/src/main/resources/i18n/strings_pt.properties +4 -0
@@ 95,6 95,10 @@ unlock.success.revealBtn=Revelar Cofre
## Failure
### Invalid Mount Point

# Lock
## Force
## Failure

# Migration
migration.title=Atualizar Cofre
## Start

M main/ui/src/main/resources/i18n/strings_pt_BR.properties => main/ui/src/main/resources/i18n/strings_pt_BR.properties +4 -0
@@ 106,6 106,10 @@ unlock.success.revealBtn=Revelar Cofre
unlock.error.invalidMountPoint.notExisting=O ponto de montagem não é um diretório vazio ou não existe: %s
unlock.error.invalidMountPoint.existing=Ponto de montagem/pasta já existe ou a pasta pai está faltando: %s

# Lock
## Force
## Failure

# Migration
migration.title=Atualizar Cofre
## Start

M main/ui/src/main/resources/i18n/strings_ro.properties => main/ui/src/main/resources/i18n/strings_ro.properties +4 -0
@@ 29,6 29,10 @@
## Failure
### Invalid Mount Point

# Lock
## Force
## Failure

# Migration
## Start
## Run

M main/ui/src/main/resources/i18n/strings_ru.properties => main/ui/src/main/resources/i18n/strings_ru.properties +16 -6
@@ 38,7 38,7 @@ addvaultwizard.welcome.existingButton=Открыть имеющееся хран
addvaultwizard.new.nameInstruction=Выберите имя для хранилища
addvaultwizard.new.namePrompt=Имя хранилища
### Location
addvaultwizard.new.locationInstruction=Где Cryptomator должен хранить зашифрованные файлы вашего хранилища?
addvaultwizard.new.locationInstruction=Где Cryptomator должен хранить зашифрованные файлы хранилища?
addvaultwizard.new.locationLabel=Место хранения
addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Своё место


@@ 51,7 51,7 @@ addvaultwizard.new.invalidName=Неверное имя хранилища. Ук
addvaultwizard.new.createVaultBtn=Создать хранилище
addvaultwizard.new.generateRecoveryKeyChoice=Вы не сможете получить доступ к своим данным без пароля. Хотите создать ключ для восстановления на случай потери пароля?
addvaultwizard.new.generateRecoveryKeyChoice.yes=Да, лучше предостеречься, чем потом жалеть
addvaultwizard.new.generateRecoveryKeyChoice.no=Нет, спасибо, я не потеряю свой пароль
addvaultwizard.new.generateRecoveryKeyChoice.no=Нет, спасибо, я не потеряю пароль
### Information
addvault.new.readme.storageLocation.fileName=ВАЖНО.rtf
addvault.new.readme.storageLocation.1=⚠️  ФАЙЛЫ ХРАНИЛИЩА  ⚠️


@@ 107,6 107,15 @@ unlock.error.heading=Невозможно разблокировать хран
unlock.error.invalidMountPoint.notExisting=Точка монтирования %s - не папка, не пуста или не существует.
unlock.error.invalidMountPoint.existing=Точка монтирования %s уже существует, либо отсутствует родительская папка.

# Lock
## Force
lock.forced.heading=Ошибка корректной блокировки
lock.forced.message=Блокировка "%s" была отменена из-за текущих операций или открытых файлов. Вы можете заблокировать это хранилище принудительно, однако прерывание ввода-вывода может привести к потере несохранённых данных.
lock.forced.confirmBtn=Принудительная блокировка
## Failure
lock.fail.heading=Ошибка блокировки хранилища.
lock.fail.message=Хранилище "%s" не удалось заблокировать. Убедитесь, что несохранённая работа сохранена в другом месте и завершены важные операции чтения/записи. Чтобы закрыть хранилище, удалите процесс Cryptomator.

# Migration
migration.title=Обновить хранилище
## Start


@@ 144,11 153,12 @@ preferences.general.startHidden=Скрывать окно при запуске 
preferences.general.debugLogging=Вести журнал отладки
preferences.general.debugDirectory=Показать файлы журнала
preferences.general.autoStart=Запускать Cryptomator при старте системы
preferences.general.keychainBackend=Хранить пароли с
preferences.general.keychainBackend.org.cryptomator.linux.keychain.SecretServiceKeychainAccess=Связка ключей Gnome
preferences.general.keychainBackend.org.cryptomator.linux.keychain.KDEWalletKeychainAccess=Бумажник KDE
preferences.general.keychainBackend.org.cryptomator.macos.keychain.MacSystemKeychainAccess=Доступ к связке ключей macOS
preferences.general.keychainBackend.org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess=Защита данных Windows
preferences.general.interfaceOrientation=Ориентация интерфейса
preferences.general.interfaceOrientation=Интерфейс
preferences.general.interfaceOrientation.ltr=Слева направо
preferences.general.interfaceOrientation.rtl=Справа налево
## Volume


@@ 165,7 175,7 @@ preferences.updates.updateAvailable=Доступно обновление до 
## Donation Key
preferences.donationKey=Пожертвование
preferences.donationKey.registeredFor=Зарегистрировано: %s
preferences.donationKey.noDonationKey=Не найден ключ пожертвования. Он напоминает лицензионный ключ, но для удивительных людей, использующих свободное ПО ;-)
preferences.donationKey.noDonationKey=Не найден ключ пожертвования. Этот ключ похож на лицензионный, но для тех, кто использует бесплатное ПО. ;-)
preferences.donationKey.getDonationKey=Получить ключ пожертвования
## About
preferences.about=О программе


@@ 221,7 231,7 @@ main.vaultDetail.lockedStatus=ЗАБЛОКИРОВАНО
main.vaultDetail.unlockBtn=Разблокировка…
main.vaultDetail.unlockNowBtn=Разблокировать
main.vaultDetail.optionsBtn=Параметры хранилища
main.vaultDetail.passwordSavedInKeychain=Пароль сохранен
main.vaultDetail.passwordSavedInKeychain=Пароль сохранён
### Unlocked
main.vaultDetail.unlockedStatus=РАЗБЛОКИРОВАНО
main.vaultDetail.accessLocation=Содержимое хранилища доступно здесь:


@@ 247,7 257,7 @@ wrongFileAlert.title=Как шифровать файлы
wrongFileAlert.header.title=Вы пытались зашифровать эти файлы?
wrongFileAlert.header.lead=Для этого Cryptomator создаёт том в системном диспетчере файлов.
wrongFileAlert.instruction.0=Чтобы зашифровать файлы, выполните следующее:
wrongFileAlert.instruction.1=1. Разблокируйте ваше хранилище.
wrongFileAlert.instruction.1=1. Разблокируйте хранилище.
wrongFileAlert.instruction.2=2. Нажмите кнопку "Показать", чтобы открыть том в диспетчере файлов.
wrongFileAlert.instruction.3=3. Добавьте файлы в этот том.
wrongFileAlert.link=Если нужна помощь, посетите

M main/ui/src/main/resources/i18n/strings_sk.properties => main/ui/src/main/resources/i18n/strings_sk.properties +9 -0
@@ 104,6 104,15 @@ unlock.error.heading=Nie je možné odomknúť účet
unlock.error.invalidMountPoint.notExisting=Bod pripojenia "%s" nie je adresár, nie je prázdny alebo neexistuje.
unlock.error.invalidMountPoint.existing=Bod pripojenia "%s" už existuje alebo chýba nadradený adresár.

# Lock
## Force
lock.forced.heading=Bežné uzamknutie zlyhalo
lock.forced.message=Zamknutie "%s" bolo zablokované prebiehajúcimi operáciami alebo otvorenými súbormi. Smiete vynútiť uzamknutie tejto peňaženky, ale prerušením I/O môže viesť k strate alebo neuloženiu dát.
lock.forced.confirmBtn=Vynútené uzamknutie
## Failure
lock.fail.heading=Uzatváranie peňaženky zlyhalo.
lock.fail.message=Peňaženku "%s" nie je možné uzamknúť. Uistite sa že neuložená páca je uložená inde a dôležité Read/Write operácie sú ukončené. Ináč uzavretím peňaženky, ukončíte proces Cryptomator-a.

# Migration
## Start
## Run

M main/ui/src/main/resources/i18n/strings_sv.properties => main/ui/src/main/resources/i18n/strings_sv.properties +4 -0
@@ 107,6 107,10 @@ unlock.error.heading=Kan inte låsa upp valvet
unlock.error.invalidMountPoint.notExisting=Monteringspunkten "%s" saknas eller är inte tom.
unlock.error.invalidMountPoint.existing=Monteringspunkten "%s" finns redan eller så saknas överordnad mapp.

# Lock
## Force
## Failure

# Migration
migration.title=Uppgradera valv
## Start

M main/ui/src/main/resources/i18n/strings_tr.properties => main/ui/src/main/resources/i18n/strings_tr.properties +40 -0
@@ 102,10 102,20 @@ unlock.success.message="%s" 'nin kilidi başarıyla açıldı! Kasanız şimdi e
unlock.success.rememberChoice=Seçimi hatırla, bunu bir daha gösterme
unlock.success.revealBtn=Kasayı Göster
## Failure
unlock.error.heading=Kasanın kilidi açılamıyor
### Invalid Mount Point
unlock.error.invalidMountPoint.notExisting=Bağlantı noktası boş bir dizin değil veya mevcut değil: %s
unlock.error.invalidMountPoint.existing=Bağlama noktası / klasör zaten var veya ana klasör eksik: %s

# Lock
## Force
lock.forced.heading=Normal kilitleme başarısız oldu
lock.forced.message="%s" nin kilitlenmesi, bekleyen işlemler veya açık dosyalar tarafından engellendi. Bu kasayı zorla kilitleyebilirsiniz, ancak G/Ç'nin kesilmesi kaydedilmemiş verilerin kaybına neden olabilir.
lock.forced.confirmBtn=Kilitlemeyi Zorla
## Failure
lock.fail.heading=Kasa kilitlenemedi.
lock.fail.message="%s" kasası kilitlenemedi. Kaydedilmemiş çalışmanın başka bir yere kaydedildiğinden ve önemli Okuma / Yazma işlemlerinin tamamlandığından emin olun. Kasayı kapatmak için Cryptomator işlemini sonlandırın.

# Migration
migration.title=Kasayı Güncelle
## Start


@@ 145,7 155,9 @@ preferences.general.debugDirectory=Kayıt dosyalarını göster
preferences.general.autoStart=Cryptomator'u sistem başlangıcında çalıştır
preferences.general.keychainBackend=Şifreleri şununla depola:
preferences.general.keychainBackend.org.cryptomator.linux.keychain.SecretServiceKeychainAccess=Gnome Keyring
preferences.general.keychainBackend.org.cryptomator.linux.keychain.KDEWalletKeychainAccess=KDE Cüzdan
preferences.general.keychainBackend.org.cryptomator.macos.keychain.MacSystemKeychainAccess=macOS Anahtar Zinciri Erişimi
preferences.general.keychainBackend.org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess=Windows Veri Koruması
preferences.general.interfaceOrientation=Arayüz Yönü
preferences.general.interfaceOrientation.ltr=Sola Yaslı
preferences.general.interfaceOrientation.rtl=Sağa Yaslı


@@ 169,8 181,34 @@ preferences.donationKey.getDonationKey=Bir bağış anahtarı al
preferences.about=Hakkında

# Vault Statistics
stats.title=%s İçin İstatistikler
stats.cacheHitRate=Önbellek Kullanım Oranı
## Read
stats.read.throughput.idle=Okuma: boşta
stats.read.throughput.kibs=Okuma: %.2f kB/s
stats.read.throughput.mibs=Okuma: %.2f MB/s
stats.read.total.data.none=Okunan veri: -
stats.read.total.data.kib=Okunan veri: %.1f kB
stats.read.total.data.mib=Okunan veri: %.1f MB
stats.read.total.data.gib=Okunan veri: %.1f GB
stats.decr.total.data.none=Şifresi çözülen veri: -
stats.decr.total.data.kib=Şifresi çözülen veri: %.1f kB
stats.decr.total.data.mib=Şifresi çözülen veri: %.1f MB
stats.decr.total.data.gib=Şifresi çözülen veri: %.1f GB
stats.read.accessCount=Toplam okuma: %d
## Write
stats.write.throughput.idle=Yazma: boşta
stats.write.throughput.kibs=Yazma: %.2f kB/s
stats.write.throughput.mibs=Yazma: %.2f MB/s
stats.write.total.data.none=Yazılan veri: -
stats.write.total.data.kib=Yazılan veri: %.1f kB
stats.write.total.data.mib=Yazılan veri: %.1f MB
stats.write.total.data.gib=Yazılan veri: %.1f GB
stats.encr.total.data.none=Şifrelenen veri: -
stats.encr.total.data.kib=Şifrelenen veri: %.1f kB
stats.encr.total.data.mib=Şifrelenen veri: %.1f MB
stats.encr.total.data.gib=Şifrelenen veri: %.1f GB
stats.write.accessCount=Toplam yazma: %d

# Main Window
main.closeBtn.tooltip=Kapat


@@ 200,9 238,11 @@ main.vaultDetail.accessLocation=Kasa içeriğinize buradan erişilebilir:
main.vaultDetail.revealBtn=Sürücüyü Göster
main.vaultDetail.lockBtn=Kilitle
main.vaultDetail.bytesPerSecondRead=Okuma:
main.vaultDetail.bytesPerSecondWritten=Yazma:
main.vaultDetail.throughput.idle=boşta
main.vaultDetail.throughput.kbps=%.1f kiB/s
main.vaultDetail.throughput.mbps=%.1f MiB/s
main.vaultDetail.stats=Kasa İstatistikleri
### Missing
main.vaultDetail.missing.info=Şifreleyici bu dosya yolunda bir kasa bulamadı.
main.vaultDetail.missing.recheck=Yeniden denetle

M main/ui/src/main/resources/i18n/strings_zh.properties => main/ui/src/main/resources/i18n/strings_zh.properties +10 -1
@@ 99,7 99,7 @@ unlock.savePassword=保存密码
unlock.unlockBtn=解锁
## Success
unlock.success.message=已成功解锁 "%s"! 您现在可以访问该保险库
unlock.success.rememberChoice=记住该选择,不要再显示
unlock.success.rememberChoice=记住选项且不再显示
unlock.success.revealBtn=显示保险库
## Failure
unlock.error.heading=无法解锁保险库


@@ 107,6 107,15 @@ unlock.error.heading=无法解锁保险库
unlock.error.invalidMountPoint.notExisting=挂载点 "%s" 不是目录、非空或不存在
unlock.error.invalidMountPoint.existing=挂载点 "%s" 已存在或缺少父文件夹

# Lock
## Force
lock.forced.heading=常规锁定失败
lock.forced.message=锁定 "%s" 被挂起的操作或使用中的文件中断。您可以强制锁定此保险库,不过请注意打断 I/O 可能导致未保存的数据丢失
lock.forced.confirmBtn=强制锁定
## Failure
lock.fail.heading=锁定保险库失败
lock.fail.message=保险库 "%s" 无法锁定。请确保在其他地方保存未保存的工作,以及重要的 "读/写" 操作已完成。为了顺利关闭保险库,请查杀 Cryptomator 进程

# Migration
migration.title=升级保险库
## Start

M main/ui/src/main/resources/i18n/strings_zh_TW.properties => main/ui/src/main/resources/i18n/strings_zh_TW.properties +41 -0
@@ 102,10 102,20 @@ unlock.success.message=成功解鎖 "%s"!您現在可以存取您的加密檔
unlock.success.rememberChoice=記得這個決定,不要再顯示
unlock.success.revealBtn=顯示加密檔案庫
## Failure
unlock.error.heading=無法解鎖加密檔案庫
### Invalid Mount Point
unlock.error.invalidMountPoint.notExisting=掛載點不是空目錄或是不存在:%s
unlock.error.invalidMountPoint.existing=掛載點已經存在或上層資料夾不存在:%s

# Lock
## Force
lock.forced.heading=正常鎖定失敗
lock.forced.message=仍有未完成的操作或開啟中的檔案以致無法鎖定 "%s"。您可以強制鎖定這個加密檔案庫,不過中斷讀寫可能會導致資料遺失或未被儲存。
lock.forced.confirmBtn=強制鎖定
## Failure
lock.fail.heading=鎖定加密檔案庫失敗。
lock.fail.message=加密檔案庫 "%s" 無法被鎖定。請確保未存檔的工作已儲存在別的地方以及重要的讀寫工作都已經完成。請強制結束 Cryptomator 以關閉加密檔案庫。

# Migration
migration.title=升級加密檔案庫
## Start


@@ 143,8 153,11 @@ preferences.general.startHidden=啟動 Cryptomator 時隱藏視窗
preferences.general.debugLogging=啟用除錯日誌
preferences.general.debugDirectory=顯示日誌檔
preferences.general.autoStart=系統啟動時同時啟動 Cryptomator
preferences.general.keychainBackend=儲存密碼使用
preferences.general.keychainBackend.org.cryptomator.linux.keychain.SecretServiceKeychainAccess=Gnome 鑰匙圈
preferences.general.keychainBackend.org.cryptomator.linux.keychain.KDEWalletKeychainAccess=KDE 錢包
preferences.general.keychainBackend.org.cryptomator.macos.keychain.MacSystemKeychainAccess=macOS 鑰匙圈
preferences.general.keychainBackend.org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess=Windows 數據保護
preferences.general.interfaceOrientation=界面排版方向
preferences.general.interfaceOrientation.ltr=由左至右
preferences.general.interfaceOrientation.rtl=由右至左


@@ 168,8 181,34 @@ preferences.donationKey.getDonationKey=取得贊助金鑰
preferences.about=關於

# Vault Statistics
stats.title=%s 統計數據
stats.cacheHitRate=快取命中率
## Read
stats.read.throughput.idle=讀取:閒置
stats.read.throughput.kibs=讀取:%.2f kiB/s
stats.read.throughput.mibs=讀取:%.2f MiB/s
stats.read.total.data.none=資料讀取:無
stats.read.total.data.kib=資料讀取:%.1f kiB
stats.read.total.data.mib=資料讀取:%.1f MiB
stats.read.total.data.gib=資料讀取:%.1f GiB
stats.decr.total.data.none=資料解密:無
stats.decr.total.data.kib=資料解密:%.1f kiB
stats.decr.total.data.mib=資料解密:%.1f MiB
stats.decr.total.data.gib=資料解密:%.1f GiB
stats.read.accessCount=總讀取:%d
## Write
stats.write.throughput.idle=寫入:閒置
stats.write.throughput.kibs=寫入:%.2f kiB/s
stats.write.throughput.mibs=寫入:%.2f MiB/s
stats.write.total.data.none=資料讀取:無
stats.write.total.data.kib=資料寫入:%.1f kiB
stats.write.total.data.mib=資料寫入:%.1f MiB
stats.write.total.data.gib=資料寫入:%.1f GiB
stats.encr.total.data.none=資料加密:無
stats.encr.total.data.kib=資料加密:%.1f kiB
stats.encr.total.data.mib=資料加密:%.1f MiB
stats.encr.total.data.gib=資料加密:%.1f GiB
stats.write.accessCount=總寫入:%d

# Main Window
main.closeBtn.tooltip=關閉


@@ 199,9 238,11 @@ main.vaultDetail.accessLocation=您可以從這裡取得您加密檔案庫的內
main.vaultDetail.revealBtn=顯示磁碟
main.vaultDetail.lockBtn=鎖定
main.vaultDetail.bytesPerSecondRead=讀取:
main.vaultDetail.bytesPerSecondWritten=寫入:
main.vaultDetail.throughput.idle=閒置
main.vaultDetail.throughput.kbps=%.1f kiB/s
main.vaultDetail.throughput.mbps=%.1f MiB/s
main.vaultDetail.stats=加密檔案庫統計數據
### Missing
main.vaultDetail.missing.info=Cryptomator 無法在指定位置找到加密檔案庫。
main.vaultDetail.missing.recheck=重新檢查

M main/ui/src/main/resources/license/THIRD-PARTY.txt => main/ui/src/main/resources/license/THIRD-PARTY.txt +21 -19
@@ 11,7 11,7 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program.  If not, see http://www.gnu.org/licenses/.

Cryptomator uses 46 third-party dependencies under the following licenses:
Cryptomator uses 47 third-party dependencies under the following licenses:
        Apache License v2.0:
			- jffi (com.github.jnr:jffi:1.2.23 - http://github.com/jnr/jffi)
			- jnr-a64asm (com.github.jnr:jnr-a64asm:1.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm)


@@ 28,19 28,20 @@ Cryptomator uses 46 third-party dependencies under the following licenses:
			- Apache Commons CLI (commons-cli:commons-cli:1.4 - http://commons.apache.org/proper/commons-cli/)
			- Apache Commons IO (commons-io:commons-io:2.6 - http://commons.apache.org/proper/commons-io/)
			- javax.inject (javax.inject:javax.inject:1 - http://code.google.com/p/atinject/)
			- Java Native Access (net.java.dev.jna:jna:5.5.0 - https://github.com/java-native-access/jna)
			- Java Native Access (net.java.dev.jna:jna:5.6.0 - https://github.com/java-native-access/jna)
			- Java Native Access Platform (net.java.dev.jna:jna-platform:5.5.0 - https://github.com/java-native-access/jna)
			- Apache Commons Lang (org.apache.commons:commons-lang3:3.11 - https://commons.apache.org/proper/commons-lang/)
			- Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.13 - http://hc.apache.org/httpcomponents-core-ga)
			- Jackrabbit WebDAV Library (org.apache.jackrabbit:jackrabbit-webdav:2.21.3 - http://jackrabbit.apache.org/jackrabbit-webdav/)
			- Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-http)
			- Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-io)
			- Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-security)
			- Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-server)
			- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-servlet)
			- Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-util)
			- Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-webapp)
			- Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-xml)
			- Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-http)
			- Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-io)
			- Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-security)
			- Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-server)
			- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-servlet)
			- Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-util)
			- Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-util-ajax)
			- Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-webapp)
			- Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-xml)
        BSD:
			- asm (org.ow2.asm:asm:7.1 - http://asm.ow2.org/)
			- asm-analysis (org.ow2.asm:asm-analysis:7.1 - http://asm.ow2.org/)


@@ 48,14 49,15 @@ Cryptomator uses 46 third-party dependencies under the following licenses:
			- asm-tree (org.ow2.asm:asm-tree:7.1 - http://asm.ow2.org/)
			- asm-util (org.ow2.asm:asm-util:7.1 - http://asm.ow2.org/)
        Eclipse Public License - Version 1.0:
			- Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-http)
			- Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-io)
			- Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-security)
			- Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-server)
			- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-servlet)
			- Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-util)
			- Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-webapp)
			- Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.33.v20201020 - https://eclipse.org/jetty/jetty-xml)
			- Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-http)
			- Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-io)
			- Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-security)
			- Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-server)
			- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-servlet)
			- Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-util)
			- Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-util-ajax)
			- Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-webapp)
			- Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.35.v20201120 - https://eclipse.org/jetty/jetty-xml)
        Eclipse Public License - v 2.0:
			- jnr-posix (com.github.jnr:jnr-posix:3.0.54 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
        GPLv2:


@@ 68,7 70,7 @@ Cryptomator uses 46 third-party dependencies under the following licenses:
			- javafx-graphics (org.openjfx:javafx-graphics:15 - https://openjdk.java.net/projects/openjfx/javafx-graphics/)
        LGPL 2.1:
			- jnr-posix (com.github.jnr:jnr-posix:3.0.54 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
			- Java Native Access (net.java.dev.jna:jna:5.5.0 - https://github.com/java-native-access/jna)
			- Java Native Access (net.java.dev.jna:jna:5.6.0 - https://github.com/java-native-access/jna)
			- Java Native Access Platform (net.java.dev.jna:jna-platform:5.5.0 - https://github.com/java-native-access/jna)
        MIT License:
			- java jwt (com.auth0:java-jwt:3.11.0 - https://github.com/auth0/java-jwt)