Merge branch 'release/1.9.0'

This commit is contained in:
Armin Schrenk
2023-05-30 10:44:02 +02:00
52 changed files with 1315 additions and 400 deletions

29
.github/release.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
# .github/release.yml
# see https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes
changelog:
exclude:
authors:
- cryptobot
- dependabot
- github-actions
categories:
- title: What's New 🎉
labels:
- type:feature-request
- type:enhancement
- title: Bugfixes 🐛
labels:
- type:security-issue
- type:bug
- type:minor-bug
- title: Other Changes 📎
labels:
- "*"
exclude:
labels:
- type:feature-request
- type:enhancement
- type:security-issue
- type:bug
- type:minor-bug

View File

@@ -10,7 +10,7 @@ on:
required: false
env:
JAVA_VERSION: 19
JAVA_VERSION: 20
jobs:
get-version:

View File

@@ -6,7 +6,7 @@ on:
types: [labeled]
env:
JAVA_VERSION: 19
JAVA_VERSION: 20
defaults:
run:
@@ -53,4 +53,8 @@ jobs:
body: |-
:construction: Work in Progress
Please be patient, the builds are still running. We will publish new versions of Cryptomator here in a few moments.
As usual, the GPG signatures can be checked using [our public key `5811 7AFA 1F85 B3EE C154 677D 615D 449F E6E6 A235`](https://gist.github.com/cryptobot/211111cf092037490275f39d408f461a).
---
<!-- Don't forget to include the 💾 SHA-256 checksums of release artifacts: -->

View File

@@ -19,9 +19,9 @@ on:
type: boolean
env:
JAVA_VERSION: 19
OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/19.0.2.1/openjfx-19.0.2.1_linux-x64_bin-jmods.zip'
OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/19.0.2.1/openjfx-19.0.2.1_linux-aarch64_bin-jmods.zip'
JAVA_VERSION: 20
OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/20.0.1/openjfx-20.0.1_linux-x64_bin-jmods.zip'
OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/20.0.1/openjfx-20.0.1_linux-aarch64_bin-jmods.zip'
jobs:
build:
@@ -45,7 +45,7 @@ jobs:
run: |
sudo add-apt-repository ppa:coffeelibs/openjdk
sudo apt-get update
sudo apt-get install debhelper devscripts dput coffeelibs-jdk-19 libgtk2.0-0
sudo apt-get install debhelper devscripts dput coffeelibs-jdk-${{ env.JAVA_VERSION }} libgtk2.0-0
- name: Setup Java
uses: actions/setup-java@v3
with:

View File

@@ -22,7 +22,7 @@ on:
value: ${{ jobs.determine-version.outputs.type }}
env:
JAVA_VERSION: 19
JAVA_VERSION: 20
JAVA_DIST: 'temurin'
JAVA_CACHE: 'maven'

View File

@@ -10,7 +10,7 @@ on:
required: false
env:
JAVA_VERSION: 19
JAVA_VERSION: 20
jobs:
get-version:

View File

@@ -4,7 +4,7 @@ on:
pull_request:
env:
JAVA_VERSION: 19
JAVA_VERSION: 20
defaults:
run:

View File

@@ -7,7 +7,7 @@ on:
- 'hotfix/**'
env:
JAVA_VERSION: 19
JAVA_VERSION: 20
defaults:
run:

View File

@@ -14,11 +14,11 @@ on:
env:
JAVA_VERSION: 19
JAVA_VERSION: 20
JAVA_DIST: 'temurin'
JAVA_CACHE: 'maven'
JFX_JMODS_URL: 'https://download2.gluonhq.com/openjfx/19.0.2.1/openjfx-19.0.2.1_windows-x64_bin-jmods.zip'
JFX_JMODS_HASH: 'B7CF2CAD2468842B3B78D99F6C0555771499A36FA1F1EE3DD1B9A4597F1FAB86'
JFX_JMODS_URL: 'https://download2.gluonhq.com/openjfx/20.0.1/openjfx-20.0.1_windows-x64_bin-jmods.zip'
JFX_JMODS_HASH: 'D00767334C43B8832B5CF10267D34CA8F563D187C4655B73EB6020DD79C054B5'
defaults:
run:
@@ -51,7 +51,7 @@ jobs:
run: |
curl --output jfxjmods.zip -L "${{ env.JFX_JMODS_URL }}"
if(!(Get-FileHash -Path jfxjmods.zip -Algorithm SHA256).Hash.equals("${{ env.JFX_JMODS_HASH }}")) {
exit 1;
throw "Wrong checksum of JMOD archive downloaded from ${{ env.JFX_JMODS_URL }}.";
}
Expand-Archive -Path jfxjmods.zip -DestinationPath jfxjmods
Get-ChildItem -Path jfxjmods -Recurse -Filter "*.jmod" | ForEach-Object { Move-Item -Path $_ -Destination $_.Directory.Parent}

2
.idea/misc.xml generated
View File

@@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" default="true" project-jdk-name="19" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_20_PREVIEW" project-jdk-name="20" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -66,6 +66,7 @@
</content_rating>
<releases>
<release date="2023-05-30" version="1.9.0"/>
<release date="2023-04-25" version="1.8.0"/>
<release date="2023-04-07" version="1.7.5"/>
<release date="2023-04-05" version="1.7.4"/>

View File

@@ -2,7 +2,7 @@ Source: cryptomator
Maintainer: Cryptobot <releases@cryptomator.org>
Section: utils
Priority: optional
Build-Depends: debhelper (>=10), coffeelibs-jdk-19, libgtk2.0-0, libgtk-3-0, libxxf86vm1, libgl1
Build-Depends: debhelper (>=10), coffeelibs-jdk-20, libgtk2.0-0, libgtk-3-0, libxxf86vm1, libgl1
Standards-Version: 4.5.0
Homepage: https://cryptomator.org
Vcs-Git: https://github.com/cryptomator/cryptomator.git

View File

@@ -4,7 +4,7 @@
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
JAVA_HOME = /usr/lib/jvm/java-19-coffeelibs
JAVA_HOME = /usr/lib/jvm/java-20-coffeelibs
DEB_BUILD_ARCH ?= $(shell dpkg-architecture -qDEB_BUILD_ARCH)
ifeq ($(DEB_BUILD_ARCH),amd64)
JMODS_PATH = jmods/amd64:${JAVA_HOME}/jmods

View File

@@ -70,6 +70,9 @@
<CustomAction Id="JpDisallowDowngrade" Error="!(loc.DowngradeErrorMessage)" />
<?endif?>
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
<CustomAction Id="JpFindRelatedProducts" BinaryKey="JpCaDll" DllEntry="FindRelatedProductsEx" />
<?ifndef SkipCryptomatorLegacyCheck ?>
<!-- Block installation if innosetup entry of Cryptomator is found -->
<Property Id="OLDEXEINSTALLER">
@@ -172,11 +175,12 @@
<?endif?>
<?ifndef JpAllowUpgrades ?>
<Custom Action="JpDisallowUpgrade" After="FindRelatedProducts">JP_UPGRADABLE_FOUND</Custom>
<Custom Action="JpDisallowUpgrade" After="JpFindRelatedProducts">JP_UPGRADABLE_FOUND</Custom>
<?endif?>
<?ifndef JpAllowDowngrades ?>
<Custom Action="JpDisallowDowngrade" After="FindRelatedProducts">JP_DOWNGRADABLE_FOUND</Custom>
<Custom Action="JpDisallowDowngrade" After="JpFindRelatedProducts">JP_DOWNGRADABLE_FOUND</Custom>
<?endif?>
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
<!-- Check and fail if Cryptomator is running -->
<Custom Action="WixCloseApplications" Before="InstallValidate"></Custom>
@@ -188,6 +192,10 @@
<Custom Action="V170MigrateSettings" After="InstallFiles">NOT Installed OR REINSTALL</Custom>
</InstallExecuteSequence>
<InstallUISequence>
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
</InstallUISequence>
<WixVariable Id="WixUIBannerBmp" Value="$(env.JP_WIXWIZARD_RESOURCES)\banner.bmp" />
<WixVariable Id="WixUIDialogBmp" Value="$(env.JP_WIXWIZARD_RESOURCES)\background.bmp" />
</Product>

29
pom.xml
View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>cryptomator</artifactId>
<version>1.8.0</version>
<version>1.9.0</version>
<name>Cryptomator Desktop App</name>
<organization>
@@ -26,21 +26,21 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.jdk.version>19</project.jdk.version>
<project.jdk.version>20</project.jdk.version>
<!-- Group IDs of jars that need to stay on the class path for now -->
<!-- Once hypfvieh, swiesend, purejava and integrations-linux have module-info, remove them-->
<nonModularGroupIds>org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh</nonModularGroupIds>
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>2.6.2</cryptomator.cryptofs.version>
<cryptomator.cryptofs.version>2.6.4</cryptomator.cryptofs.version>
<cryptomator.integrations.version>1.2.0</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.2.0</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>1.2.0</cryptomator.integrations.mac.version>
<cryptomator.integrations.linux.version>1.2.0</cryptomator.integrations.linux.version>
<cryptomator.fuse.version>2.0.5</cryptomator.fuse.version>
<cryptomator.integrations.linux.version>1.2.1</cryptomator.integrations.linux.version>
<cryptomator.fuse.version>3.0.0</cryptomator.fuse.version>
<cryptomator.dokany.version>2.0.0</cryptomator.dokany.version>
<cryptomator.webdav.version>2.0.2</cryptomator.webdav.version>
<cryptomator.webdav.version>2.0.3</cryptomator.webdav.version>
<!-- 3rd party dependencies -->
<commons-lang3.version>3.12.0</commons-lang3.version>
@@ -48,23 +48,23 @@
<easybind.version>2.2</easybind.version>
<guava.version>31.1-jre</guava.version>
<gson.version>2.10.1</gson.version>
<javafx.version>19.0.2.1</javafx.version>
<jwt.version>4.3.0</jwt.version>
<javafx.version>20.0.1</javafx.version>
<jwt.version>4.4.0</jwt.version>
<nimbus-jose.version>9.31</nimbus-jose.version>
<logback.version>1.4.5</logback.version>
<slf4j.version>2.0.6</slf4j.version>
<logback.version>1.4.7</logback.version>
<slf4j.version>2.0.7</slf4j.version>
<tinyoauth2.version>0.5.1</tinyoauth2.version>
<zxcvbn.version>1.7.0</zxcvbn.version>
<!-- test dependencies -->
<junit.jupiter.version>5.9.2</junit.jupiter.version>
<mockito.version>5.1.1</mockito.version>
<junit.jupiter.version>5.9.3</junit.jupiter.version>
<mockito.version>5.3.1</mockito.version>
<hamcrest.version>2.2</hamcrest.version>
<!-- build-time dependencies -->
<jetbrains.annotations.version>23.0.0</jetbrains.annotations.version>
<dependency-check.version>8.1.0</dependency-check.version>
<jacoco.version>0.8.8</jacoco.version>
<jacoco.version>0.8.9</jacoco.version>
</properties>
<dependencies>
@@ -332,6 +332,9 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,8 @@ import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.function.Function;
// TODO make sealed, remove enum
interface IpcMessage {
//TODO can the enum be removed?
sealed interface IpcMessage permits HandleLaunchArgsMessage, RevealRunningAppMessage {
enum MessageType {
REVEAL_RUNNING_APP(RevealRunningAppMessage::decode),

View File

@@ -5,10 +5,9 @@ import java.util.List;
public interface IpcMessageListener {
default void handleMessage(IpcMessage message) {
if (message instanceof RevealRunningAppMessage) {
revealRunningApp();
} else if (message instanceof HandleLaunchArgsMessage m) {
handleLaunchArgs(m.args());
switch (message) {
case RevealRunningAppMessage m -> revealRunningApp(); // TODO: rename to _ with JEP 443
case HandleLaunchArgsMessage m -> handleLaunchArgs(m.args());
}
}

View File

@@ -1,12 +1,14 @@
package org.cryptomator.launcher;
import org.cryptomator.common.settings.Settings;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -14,26 +16,39 @@ import java.util.Locale;
public class SupportedLanguages {
private static final Logger LOG = LoggerFactory.getLogger(SupportedLanguages.class);
// these are BCP 47 language codes, not ISO. Note the "-" instead of the "_":
public static final List<String> LANGUAGAE_TAGS = List.of("en", "ar", "be", "bn", "bs", "ca", "cs", "da", "de", "el", "es", "fil", "fa", "fr", "gl", "he", //
"hi", "hr", "hu", "id", "it", "ja", "ko", "lv", "mk", "nb", "nl", "nn", "no", "pa", "pl", "pt", "pt-BR", "ro", "ru", "si", "sk", "sr", "sr-Latn", "sv", "sw", //
"ta", "te", "th", "tr", "uk", "vi", "zh", "zh-HK", "zh-TW");
// these are BCP 47 language codes, not ISO. Note the "-" instead of the "_".
// "en" is not part of this list - it is always inserted at the top.
public static final List<String> LANGUAGE_TAGS = List.of("ar", "be", "bn", "bs", "ca", "cs", "da", "de", "el", "es", "fr", "gl", "he", //
"hi", "hr", "hu", "id", "it", "ja", "ko", "lv", "nb", "nl", "nn", "pa", "pl", "pt", "pt-BR", "ro", "ru", "sk", "sr", "sr-Latn", "sv", "sw", //
"ta", "th", "tr", "uk", "vi", "zh", "zh-HK", "zh-TW");
public static final String ENGLISH = "en";
@Nullable
private final String preferredLanguage;
private final List<String> sortedLanguageTags;
private final Locale preferredLocale;
@Inject
public SupportedLanguages(Settings settings) {
this.preferredLanguage = settings.languageProperty().get();
var preferredLanguage = settings.languageProperty().get();
preferredLocale = preferredLanguage == null ? Locale.getDefault() : Locale.forLanguageTag(preferredLanguage);
var collator = Collator.getInstance(preferredLocale);
collator.setStrength(Collator.PRIMARY);
var sorted = new ArrayList<String>();
sorted.add(0, Settings.DEFAULT_LANGUAGE);
sorted.add(1, ENGLISH);
LANGUAGE_TAGS.stream() //
.sorted((a, b) -> collator.compare(Locale.forLanguageTag(a).getDisplayName(), Locale.forLanguageTag(b).getDisplayName())) //
.forEach(sorted::add);
sortedLanguageTags = Collections.unmodifiableList(sorted);
}
public void applyPreferred() {
if (preferredLanguage == null) {
LOG.debug("Using system locale");
return;
}
var preferredLocale = Locale.forLanguageTag(preferredLanguage);
LOG.debug("Applying preferred locale {}", preferredLocale.getDisplayName(Locale.ENGLISH));
LOG.debug("Using locale {}", preferredLocale);
Locale.setDefault(preferredLocale);
}
public List<String> getLanguageTags() {
return sortedLanguageTags;
}
}

View File

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

View File

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

View File

@@ -1,30 +1,85 @@
package org.cryptomator.ui.fxapp;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.collections.ObservableList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Stream;
@FxApplicationScoped
public class AutoUnlocker {
private static final Logger LOG = LoggerFactory.getLogger(AutoUnlocker.class);
private final ObservableList<Vault> vaults;
private final FxApplicationWindows appWindows;
private final ScheduledExecutorService scheduler;
private ScheduledFuture<?> unlockMissingFuture;
private ScheduledFuture<?> timeoutFuture;
@Inject
public AutoUnlocker(ObservableList<Vault> vaults, FxApplicationWindows appWindows) {
public AutoUnlocker(ObservableList<Vault> vaults, FxApplicationWindows appWindows, ScheduledExecutorService scheduler) {
this.vaults = vaults;
this.appWindows = appWindows;
this.scheduler = scheduler;
}
public void unlock() {
vaults.stream().filter(Vault::isLocked) //
.filter(v -> v.getVaultSettings().unlockAfterStartup().get()) //
.<CompletionStage<Void>>reduce(CompletableFuture.completedFuture(null), //
(unlockFlow, v) -> unlockFlow.handle((voit, ex) -> appWindows.startUnlockWorkflow(v, null)).thenCompose(stage -> stage), //we don't care here about the exception, logged elsewhere
(unlockChain1, unlockChain2) -> unlockChain1.handle((voit, ex) -> unlockChain2).thenCompose(stage -> stage));
public void tryUnlockForTimespan(int timespan, TimeUnit timeUnit) {
// Unlock all available auto unlock vaults
Predicate<Vault> shouldAutoUnlock = v -> v.getVaultSettings().unlockAfterStartup().get();
unlockSequentially(vaults.stream().filter(shouldAutoUnlock)).thenRun(() -> startUnlockMissing(timespan, timeUnit));
}
private CompletionStage<Void> unlockSequentially(Stream<Vault> vaultStream) {
// this is an attempt to run all the unlock workflows sequentially, i.e. start the next workflow only after completing/failing the previous workflow.
return vaultStream.filter(Vault::isLocked).reduce(CompletableFuture.completedFuture(null),
(prevUnlock, nextVault) -> prevUnlock.thenCompose(unused -> appWindows.startUnlockWorkflow(nextVault, null)),
(prevUnlock, nextUnlock) -> nextUnlock.exceptionally(e -> null) // we don't care here about the exception, logged elsewhere
);
}
private void startUnlockMissing(int timespan, TimeUnit timeUnit) {
// Start a temporary service if there are missing auto unlock vaults
if (getMissingAutoUnlockVaults().findAny().isPresent()) {
LOG.info("Found MISSING vaults, starting periodic check");
unlockMissingFuture = scheduler.scheduleWithFixedDelay(this::unlockMissing, 0, 1, TimeUnit.SECONDS);
timeoutFuture = scheduler.schedule(this::timeout, timespan, timeUnit);
}
}
private void unlockMissing() {
List<Vault> missingAutoUnlockVaults = getMissingAutoUnlockVaults().toList();
missingAutoUnlockVaults.forEach(VaultListManager::redetermineVaultState);
unlockSequentially(missingAutoUnlockVaults.stream()).thenRun(this::stopUnlockMissing);
}
private void stopUnlockMissing() {
// Stop checking if there are no more missing vaults
if (getMissingAutoUnlockVaults().findAny().isEmpty()) {
LOG.info("No more MISSING vaults, stopping periodic check");
unlockMissingFuture.cancel(false);
timeoutFuture.cancel(false);
}
}
private void timeout() {
LOG.info("MISSING vaults periodic check timed out");
unlockMissingFuture.cancel(false);
}
private Stream<Vault> getMissingAutoUnlockVaults() {
return vaults.stream()
.filter(Vault::isMissing)
.filter(v -> v.getVaultSettings().unlockAfterStartup().get());
}
}

View File

@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
import java.util.concurrent.TimeUnit;
@FxApplicationScoped
public class FxApplication {
@@ -68,7 +69,6 @@ public class FxApplication {
});
launchEventHandler.startHandlingLaunchEvents();
autoUnlocker.unlock();
autoUnlocker.tryUnlockForTimespan(2, TimeUnit.MINUTES);
}
}

View File

@@ -12,6 +12,7 @@ import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.quit.QuitComponent;
import org.cryptomator.ui.unlock.UnlockComponent;
import org.cryptomator.ui.unlock.UnlockWorkflow;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -114,7 +115,7 @@ public class FxApplicationWindows {
LOG.debug("Start unlock workflow for {}", vault.getDisplayName());
return unlockWorkflowFactory.create(vault, owner).unlockWorkflow();
}, Platform::runLater) //
.thenCompose(unlockWorkflow -> CompletableFuture.runAsync(unlockWorkflow, executor)) //
.thenAcceptAsync(UnlockWorkflow::run, executor)
.exceptionally(e -> {
showErrorWindow(e, owner == null ? primaryStage : owner, null);
return null;

View File

@@ -102,14 +102,16 @@ public class StartController implements FxController {
}
private void loadingKeyFailed(Throwable e) {
if (e instanceof UnlockCancelledException) {
// ok
} else if (e instanceof VaultKeyInvalidException) {
LOG.error("Invalid key"); //TODO: specific error screen
appWindows.showErrorWindow(e, window, null);
} else {
LOG.error("Failed to load key.", e);
appWindows.showErrorWindow(e, window, null);
switch (e) {
case UnlockCancelledException uce -> {} //ok
case VaultKeyInvalidException vkie -> {
LOG.error("Invalid key"); //TODO: specific error screen
appWindows.showErrorWindow(e, window, null);
}
default -> {
LOG.error("Failed to load key.", e);
appWindows.showErrorWindow(e, window, null);
}
}
}

View File

@@ -1,6 +1,5 @@
package org.cryptomator.ui.preferences;
import com.google.common.base.Strings;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
@@ -35,6 +34,7 @@ public class InterfacePreferencesController implements FxController {
private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
private final LicenseHolder licenseHolder;
private final ResourceBundle resourceBundle;
private final SupportedLanguages supportedLanguages;
public ChoiceBox<UiTheme> themeChoiceBox;
public CheckBox showMinimizeButtonCheckbox;
public CheckBox showTrayIconCheckbox;
@@ -44,13 +44,14 @@ public class InterfacePreferencesController implements FxController {
public RadioButton nodeOrientationRtl;
@Inject
InterfacePreferencesController(Settings settings, TrayMenuComponent trayMenu, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ResourceBundle resourceBundle) {
InterfacePreferencesController(Settings settings, SupportedLanguages supportedLanguages, TrayMenuComponent trayMenu, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ResourceBundle resourceBundle) {
this.settings = settings;
this.trayMenuInitialized = trayMenu.isInitialized();
this.trayMenuSupported = trayMenu.isSupported();
this.selectedTabProperty = selectedTabProperty;
this.licenseHolder = licenseHolder;
this.resourceBundle = resourceBundle;
this.supportedLanguages = supportedLanguages;
}
@FXML
@@ -66,8 +67,7 @@ public class InterfacePreferencesController implements FxController {
showTrayIconCheckbox.selectedProperty().bindBidirectional(settings.showTrayIcon());
preferredLanguageChoiceBox.getItems().add(null);
preferredLanguageChoiceBox.getItems().addAll(SupportedLanguages.LANGUAGAE_TAGS);
preferredLanguageChoiceBox.getItems().addAll(supportedLanguages.getLanguageTags());
preferredLanguageChoiceBox.valueProperty().bindBidirectional(settings.languageProperty());
preferredLanguageChoiceBox.setConverter(new LanguageTagConverter(resourceBundle));
@@ -141,9 +141,7 @@ public class InterfacePreferencesController implements FxController {
return resourceBundle.getString("preferences.interface.language.auto");
} else {
var locale = Locale.forLanguageTag(tag);
var lang = locale.getDisplayLanguage(locale);
var region = locale.getDisplayCountry(locale);
return lang + (Strings.isNullOrEmpty(region) ? "" : " (" + region + ")");
return locale.getDisplayName();
}
}

View File

@@ -84,18 +84,19 @@ public class AwtTrayMenuController implements TrayMenuController {
private void addChildren(Menu menu, List<TrayMenuItem> items) {
for (var item : items) {
// TODO: use Pattern Matching for switch, once available
if (item instanceof ActionItem a) {
var menuItem = new MenuItem(a.title());
menuItem.addActionListener(evt -> a.action().run());
menuItem.setEnabled(a.enabled());
menu.add(menuItem);
} else if (item instanceof SeparatorItem) {
menu.addSeparator();
} else if (item instanceof SubMenuItem s) {
var submenu = new Menu(s.title());
addChildren(submenu, s.items());
menu.add(submenu);
switch (item) {
case ActionItem a -> {
var menuItem = new MenuItem(a.title());
menuItem.addActionListener(evt -> a.action().run());
menuItem.setEnabled(a.enabled());
menu.add(menuItem);
}
case SeparatorItem s -> menu.addSeparator(); // TODO: rename to _ with JEP 443
case SubMenuItem s -> {
var submenu = new Menu(s.title());
addChildren(submenu, s.items());
menu.add(submenu);
}
}
}
}

View File

@@ -38,12 +38,11 @@ public class UnlockInvalidMountPointController implements FxController {
@FXML
public void initialize() {
var e = unlockException.get();
String translationKey = "unlock.error.customPath.description.generic";
if (e instanceof MountPointNotSupportedException) {
translationKey = "unlock.error.customPath.description.notSupported";
} else if (e instanceof MountPointNotExistsException) {
translationKey = "unlock.error.customPath.description.notExists";
}
var translationKey = switch (e) {
case MountPointNotSupportedException x -> "unlock.error.customPath.description.notSupported";
case MountPointNotExistsException x -> "unlock.error.customPath.description.notExists";
default -> "unlock.error.customPath.description.generic";
};
dialogDescription.setFormat(resourceBundle.getString(translationKey));
dialogDescription.setArg1(e.getMessage());
}

View File

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

View File

@@ -0,0 +1,233 @@
# Locale Specific CSS files such as CJK, RTL,...
# Generics
## Button
generic.button.apply=Прилагане
generic.button.back=Назад
generic.button.cancel=Отказ
generic.button.change=Променяне
generic.button.choose=Избиране…
generic.button.close=Затваряне
generic.button.copy=Копиране
generic.button.copied=Копирано!
generic.button.done=Готово
generic.button.next=Напред
generic.button.print=Отпечатване
# Error
error.message=Възникна грешка
error.description=Това е неочаквано за Криптоматор. Можете да потърсите съществуващи ревения за тази грешка. Или ако все още не е докладвана се чувствайте свободни да го направите.
error.hyperlink.lookup=Търсене на грешката
error.hyperlink.report=Докладване на грешката
error.technicalDetails=Подробности:
# Defaults
defaults.vault.vaultName=Хранилище
# Tray Menu
traymenu.showMainWindow=Показване
traymenu.showPreferencesWindow=Настройки
traymenu.lockAllVaults=Заключване на всички
traymenu.quitApplication=Изход
traymenu.vault.unlock=Отключване
traymenu.vault.lock=Заключване
traymenu.vault.reveal=Разкриване
# Add Vault Wizard
addvaultwizard.title=Добавяне на хранилище
## Welcome
addvaultwizard.welcome.newButton=Ново хранилище
addvaultwizard.welcome.existingButton=Отваряне на хранилище
## New
### Name
addvaultwizard.new.nameInstruction=Изберете име на хранилището
addvaultwizard.new.namePrompt=Наименование
### Location
addvaultwizard.new.locationInstruction=Къде Криптоматор ще държи шифрованите файлове на хранилището?
addvaultwizard.new.locationLabel=Местоположение на хранилището
addvaultwizard.new.locationPrompt=
addvaultwizard.new.directoryPickerLabel=Потребителско местоположение
addvaultwizard.new.directoryPickerButton=Избиране…
addvaultwizard.new.directoryPickerTitle=Избор на папка
addvaultwizard.new.fileAlreadyExists=Файл или папка с името на хранилището вече съществува
addvaultwizard.new.locationDoesNotExist=Папка на посоченото място не съществува или не може да бъде достъпена
addvaultwizard.new.locationIsNotWritable=Липсват права за писане на посоченото място
addvaultwizard.new.locationIsOk=Мястото е подходящо за хранилище
addvaultwizard.new.invalidName=Неприемливо име на хранилище
addvaultwizard.new.validName=Приемливо име на хранилище
addvaultwizard.new.validCharacters.message=Името на хранилището може да съдържа следните знаци:
addvaultwizard.new.validCharacters.chars=Букви (напр. a, ж или 수)
addvaultwizard.new.validCharacters.numbers=Числа
addvaultwizard.new.validCharacters.dashes=Тире (%s) или долна черта (%s)
### Password
addvaultwizard.new.createVaultBtn=Създаване
addvaultwizard.new.generateRecoveryKeyChoice=Без парола няма да имате достъп до данните си. Желаете ли да бъде създаден ключ за възстановяване, в случай че загубите паролата си?
addvaultwizard.new.generateRecoveryKeyChoice.yes=Да, нека имам за всеки случай
addvaultwizard.new.generateRecoveryKeyChoice.no=Не, няма да загубя паролата си
### Information
addvault.new.readme.storageLocation.1=⚠️ ФАЙЛОВЕ НА ХРАНИЛИЩЕ ⚠️
addvault.new.readme.storageLocation.2=Това е местоположението на хранилището.
addvault.new.readme.storageLocation.3=НЕДЕЙТЕ
addvault.new.readme.storageLocation.4=• да променяте файловете в тази папка или
addvault.new.readme.storageLocation.5=• да добавяте файлове, които да бъдат шифровани в папката.
addvault.new.readme.storageLocation.6=Ако искате някакви файлове да бъдат шифровани или да прегледате хранилището, направете следното:
addvault.new.readme.storageLocation.7=1. Добавете това хранилище към Криптоматор.
addvault.new.readme.storageLocation.8=2. Отключете хранилището в Криптоматор.
addvault.new.readme.storageLocation.9=3. Отворете местоположението на съдържанието чрез бутона „Разкриване“.
addvault.new.readme.storageLocation.10=Ако имате нужда от помощ, посетете документацията: %s
addvault.new.readme.accessLocation.1=🔐️ ШИФРОВАН ДЯЛ 🔐️
addvault.new.readme.accessLocation.2=Това е местоположението на съдържанието на хранилището.
addvault.new.readme.accessLocation.3=Файловете, в този дял са шифроване от Криптоматор. Можете да работите с тях както с всеки друг диск или папка. Това е само разшифрован вариант на съдуржанието. Файловете остават шифровани на твърдия диск през цялото време.
addvault.new.readme.accessLocation.4=При желание можете да премахнете този файл.
## Existing
addvaultwizard.existing.instruction=Изберете файла „vault.cryptomator“ от съществуващото хранилище. Но ако съществува файл „masterkey.cryptomator“, изберете него.
addvaultwizard.existing.chooseBtn=Избиране…
addvaultwizard.existing.filePickerTitle=Избор на файл на хранилището
addvaultwizard.existing.filePickerMimeDesc=Хранилище на Криптоматор
## Success
addvaultwizard.success.nextStepsInstructions=Хранилището „%s“ е добавено.\nЗа да го достъпите или да добавите съдържание в него трябва да го отключите. Можете да го направите сега или по всяко друго време.
addvaultwizard.success.unlockNow=Отключване сега
# Remove Vault
removeVault.title=Премахване на „%s“
removeVault.message=Премахване на хранилище?
removeVault.description=По този начин Криптоматор ще забрави за това хранилище. Можете да го добавите отново. Шифрованите файлове няма да бъдат премахнати от твърдия диск.
removeVault.confirmBtn=Премахване
# Change Password
changepassword.title=Промяна на парола
changepassword.enterOldPassword=Въведете текущата парола за %s
changepassword.finalConfirmation=Разбирам, че ще загубя достъпа до данните си ако забравя паролата
# Forget Password
forgetPassword.title=Забравяне на парола
forgetPassword.message=Забравяне на запазена парола?
forgetPassword.description=По този начин запазената парола за хранилището ще бъде премахната от ключодържателя на системата.
forgetPassword.confirmBtn=Забравяне на парола
# Unlock
unlock.title=Отключване на „%s“
unlock.passwordPrompt=Въведете паролата за „%s“:
unlock.savePassword=Запомняне на паролата
unlock.unlockBtn=Отключване
## Select
unlock.chooseMasterkey.message=Файлът на главния ключ не е намерен
unlock.chooseMasterkey.description=Криптоматор не може да намери файлът на главния ключ на хранилището „%s“. Изберете ръчно файла.
unlock.chooseMasterkey.filePickerTitle=Изберете файл на главен ключ
unlock.chooseMasterkey.filePickerMimeDesc=Гкавен ключ на Криптоматор
## Success
unlock.success.message=Отключено е успешно
unlock.success.description=Съдържанието на хранилището „%s“ е достъпно в точката му на монтиране.
unlock.success.revealBtn=Разкриване на диска
## Failure
## Hub
hub.noKeychain.openBtn=Към настройките
### Waiting
### Receive Key
### Register Device
### Registration Success
### Registration Failed
### Unauthorized
### License Exceeded
# Lock
## Force
## Failure
# Migration
## Start
## Run
## Success
migration.success.unlockNow=Отключване сега
## Missing file system capabilities
## Impossible
# Health Check
## Start
health.title=Проверка на състоянието на „%s“
health.intro.header=Проверка на състоянието
health.intro.text=Проверката на състоянието е набор от проверки и евентуално поправки за проблеми във вътрешната структура на хранилището. Имайте предвид, че:
## Start Failure
## Check Selection
## Detail view
health.check.detail.listFilters.label=Филтър
health.check.detail.fixAllSpecificBtn=Поправка всички от вида
health.check.exportBtn=Отчет
## Result view
health.result.severityFilter.all=Сериозност - Всички
health.result.severityFilter.good=Добре
health.result.severityFilter.info=Информация
health.result.severityFilter.warn=Предупреждение
health.result.severityFilter.crit=Криточно
## Fix Application
# Preferences
preferences.title=Настройки
## General
## Interface
preferences.interface.theme=Оформление
## Volume
## Updates
## Contribution
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
# Vault Statistics
## Read
## Write
## Accesses
# Main Window
main.closeBtn.tooltip=Затваряне
main.preferencesBtn.tooltip=Настройки
## Vault List
main.vaultlist.contextMenu.lock=Заключване
main.vaultlist.contextMenu.unlock=Отключване…
main.vaultlist.contextMenu.unlockNow=Отключване сега
main.vaultlist.contextMenu.vaultoptions=Настройки на хранилището
main.vaultlist.contextMenu.reveal=Разкриване на диска
main.vaultlist.addVaultBtn=Добавяне на хранилище
## Vault Detail
### Welcome
### Locked
main.vaultDetail.unlockBtn=Отключване…
main.vaultDetail.unlockNowBtn=Отключване сега
### Unlocked
main.vaultDetail.unlockedStatus=ОТКЛЮЧЕНО
main.vaultDetail.revealBtn=Разкриване на диска
main.vaultDetail.lockBtn=Заключване
### Missing
### Needs Migration
### Error
# Wrong File Alert
# Vault Options
## General
vaultOptions.general.vaultName=Наименование
vaultOptions.general.actionAfterUnlock.reveal=Разкриване на диска
vaultOptions.general.actionAfterUnlock.ask=Запитване
## Mount
vaultOptions.mount.mountPoint.directoryPickerButton=Избиране…
## Master Key
vaultOptions.masterkey.changePasswordBtn=Промяна на парола
## Hub
# Recovery Key
## Display Recovery Key
## Reset Password
### Enter Recovery Key
### Reset Password
### Recovery Key Password Reset Success
# Convert Vault
# New Password
# Quit
# Forced Quit

View File

@@ -172,8 +172,8 @@ migration.title=Tresor upgraden
## Start
migration.start.header=Tresor upgraden
migration.start.text=Um deinen Tresor "%s" in dieser neuen Version von Cryptomator zu öffnen, muss der Tresor auf ein neueres Format aktualisiert werden. Bevor du dies tust, solltest du Folgendes wissen:
migration.start.remarkUndone=Dieser Vorgang kann nicht rückgängig gemacht werden.
migration.start.remarkVersions=Ältere Versionen von Cryptomator können den aktualisierten Tresor nicht öffnen.
migration.start.remarkUndone=Diese Aktualisierung kann nicht rückgängig gemacht werden.
migration.start.remarkVersions=Ältere Versionen von Cryptomator werden den aktualisierten Tresor nicht öffnen können.
migration.start.remarkCanRun=Du musst sicherstellen, dass jedes Gerät, von dem aus du auf den Tresor zugreifst, diese Version von Cryptomator ausführen kann.
migration.start.remarkSynced=Du musst sicherstellen, dass dein Tresor auf diesem Gerät und auf deinen anderen Geräten vollständig synchronisiert ist, bevor du ihn aktualisierst.
migration.start.confirm=Ich habe die oben genannten Informationen gelesen und verstanden
@@ -225,7 +225,7 @@ health.check.detail.checkFinishedAndFound=Die Prüfung wurde abgeschlossen. Bitt
health.check.detail.checkFailed=Die Prüfung wurde wegen eines Fehlers abgebrochen.
health.check.detail.checkCancelled=Die Prüfung wurde abgebrochen.
health.check.detail.listFilters.label=Filter
health.check.detail.fixAllSpecificBtn=Alle dieses Typs reparieren
health.check.detail.fixAllSpecificBtn=Behebe alle vom Typ
health.check.exportBtn=Bericht exportieren
## Result view
health.result.severityFilter.all=Schweregrad - Alle
@@ -464,7 +464,7 @@ convertVault.title=Tresor konvertieren
convertVault.convert.convertBtn.before=Konvertieren
convertVault.convert.convertBtn.processing=Konvertierung läuft…
convertVault.success.message=Konvertierung erfolgreich
convertVault.hubToPassword.success.description=Du kannst nun den Tresor mit dem gewählten Passwort entsperren, ohne auf den Hub zuzugreifen.
convertVault.hubToPassword.success.description=Du kannst nun den Tresor mit dem gewählten Passwort entsperren, ohne Hub Zugriff zu benötigen.
# New Password
newPassword.promptText=Gib ein neues Passwort ein

View File

@@ -0,0 +1,179 @@
# Locale Specific CSS files such as CJK, RTL,...
# Generics
## Button
generic.button.apply=Vahvista
generic.button.back=Takaisin
generic.button.cancel=Peruuta
generic.button.change=Muuta
generic.button.choose=Valitse…
generic.button.close=Sulje
generic.button.copy=Kopioi
generic.button.copied=Kopioitu!
generic.button.done=Valmis
generic.button.next=Seuraava
generic.button.print=Tulosta
# Error
error.message=Tapahtui virhe
error.description=Cryptomator ei odottanut tämän tapahtuvan. Voit etsiä olemassa olevia ratkaisuja tähän virheeseen. Tai jos sitä ei ole vielä raportoitu, voit tehdä niin.
error.hyperlink.lookup=Etsi tämä virhe
error.hyperlink.report=Ilmoita ongelmasta
error.technicalDetails=Tiedot:
# Defaults
defaults.vault.vaultName=Vault
# Tray Menu
traymenu.showMainWindow=Näytä
traymenu.showPreferencesWindow=Asetukset
traymenu.lockAllVaults=Lukitse Kaikki
traymenu.quitApplication=Sulje
traymenu.vault.unlock=Avaa
traymenu.vault.lock=Lukitse
traymenu.vault.reveal=Paljasta
# Add Vault Wizard
addvaultwizard.title=Lisää Vault
## Welcome
addvaultwizard.welcome.newButton=Luo Uusi Vault
addvaultwizard.welcome.existingButton=Avaa Olemassaoleva Vault
## New
### Name
addvaultwizard.new.nameInstruction=Anna uusi nimi Vaultille
addvaultwizard.new.namePrompt=Vault Nimi
### Location
addvaultwizard.new.locationInstruction=Missä pitäisi Cryptomator tallentaa salattuja tiedostoja Vault?
addvaultwizard.new.locationLabel=Tallennustilan sijainti
addvaultwizard.new.locationPrompt=
addvaultwizard.new.directoryPickerLabel=Oma sijainti
addvaultwizard.new.directoryPickerButton=Valitse…
addvaultwizard.new.directoryPickerTitle=Valitse Hakemisto
addvaultwizard.new.fileAlreadyExists=Varoitus: samanniminen tiedosto tai Vault on jo olemassa!
addvaultwizard.new.locationDoesNotExist=Määritetyn polun hakemistoa ei ole olemassa tai sitä ei voi käyttää
addvaultwizard.new.locationIsNotWritable=Ei kirjoitusoikeuksia määritetyllä polulla
addvaultwizard.new.locationIsOk=Sopiva sijainti Vaultille
addvaultwizard.new.invalidName=Virheellinen vault nimi
addvaultwizard.new.validName=Kelvollinen vault nimi
addvaultwizard.new.validCharacters.message=Vaultin nimi voi sisältää seuraavat merkit:
addvaultwizard.new.validCharacters.chars=Sanamerkit (e.g. a, ж or 수)
addvaultwizard.new.validCharacters.numbers=Numerot
addvaultwizard.new.validCharacters.dashes=Hyphen (%s) tai alaviiva (%s)
### Password
addvaultwizard.new.createVaultBtn=Luo Uusi Vault
addvaultwizard.new.generateRecoveryKeyChoice=Et voi käyttää tietojasi ilman salasanaasi. Haluatko palautusavaimen siltä varalta, että menetät salasanasi?
addvaultwizard.new.generateRecoveryKeyChoice.yes=Kyllä kiitos, parempi olla varma kuin katua
addvaultwizard.new.generateRecoveryKeyChoice.no=Ei kiitos, en menetä salasanani
### Information
addvault.new.readme.storageLocation.fileName=IMPORTANT.rtf
addvault.new.readme.storageLocation.1=⚠️ VAULT TIEDOSTOT⚠
addvault.new.readme.storageLocation.2=Tämä on vaultin tallennuspaikka.
addvault.new.readme.storageLocation.3=ÄLÄ
addvault.new.readme.storageLocation.4=• muuttaa mitä tahansa tiedostoja tämän hakemiston tai
addvault.new.readme.storageLocation.5=• liitä kaikki tiedostot salaukseen tähän kansioon.
addvault.new.readme.storageLocation.6=Jos haluat salata tiedostoja ja tarkastella Vaultin sisältöä, tee seuraavat toimet:
addvault.new.readme.storageLocation.7=1. Lisää tämä Vault Cryptomatoriin.
addvault.new.readme.storageLocation.8=2. Avaa Vault Cryptomatorissa.
## Existing
addvaultwizard.existing.chooseBtn=Valitse…
## Success
# Remove Vault
# Change Password
# Forget Password
# Unlock
unlock.unlockBtn=Avaa
## Select
## Success
## Failure
## Hub
### Waiting
hub.auth.message=Odotetaan todennusta…
hub.auth.description=Pitäisi ohjata sinut automaattisesti uudelleen kirjautumissivulle.
### Receive Key
### Register Device
### Registration Success
### Registration Failed
### Unauthorized
### License Exceeded
# Lock
## Force
## Failure
# Migration
## Start
## Run
## Success
## Missing file system capabilities
## Impossible
# Health Check
## Start
## Start Failure
## Check Selection
## Detail view
## Result view
## Fix Application
# Preferences
preferences.title=Asetukset
## General
## Interface
## Volume
## Updates
## Contribution
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
# Vault Statistics
## Read
## Write
## Accesses
# Main Window
main.closeBtn.tooltip=Sulje
main.preferencesBtn.tooltip=Asetukset
## Vault List
main.vaultlist.contextMenu.lock=Lukitse
main.vaultlist.addVaultBtn=Lisää Vault
## Vault Detail
### Welcome
### Locked
### Unlocked
main.vaultDetail.lockBtn=Lukitse
### Missing
### Needs Migration
### Error
# Wrong File Alert
# Vault Options
## General
vaultOptions.general.vaultName=Vault Nimi
## Mount
vaultOptions.mount.mountPoint.directoryPickerButton=Valitse…
## Master Key
## Hub
# Recovery Key
## Display Recovery Key
## Reset Password
### Enter Recovery Key
### Reset Password
### Recovery Key Password Reset Success
# Convert Vault
# New Password
# Quit
# Forced Quit

View File

@@ -434,6 +434,7 @@ vaultOptions.masterkey.recoveryKeyExplanation=回復キーはパスワードを
vaultOptions.masterkey.showRecoveryKeyBtn=回復キーを表示
vaultOptions.masterkey.recoverPasswordBtn=パスワードをリセット
## Hub
vaultOptions.hub=回復
# Recovery Key
## Display Recovery Key
@@ -456,6 +457,10 @@ recoveryKey.recover.resetSuccess.message=パスワードをリセットしまし
recoveryKey.recover.resetSuccess.description=新しいパスワードで金庫の施錠ができます。
# Convert Vault
convertVault.title=金庫を変換
convertVault.convert.convertBtn.before=変換
convertVault.convert.convertBtn.processing=変換中…
convertVault.success.message=変換完了
# New Password
newPassword.promptText=新しいパスワードを入力してください

View File

@@ -168,6 +168,7 @@ lock.fail.description=O cofre "%s" não pode ser bloqueado. Certifique-se de que
migration.title=Atualizar Cofre
## Start
migration.start.header=Atualizar Cofre
migration.start.remarkUndone=Esta atualização não pode ser revertida.
migration.start.confirm=Li e entendi as informações acima
## Run
migration.run.enterPassword=Introduza a palavra-passe de "%s"
@@ -222,8 +223,11 @@ health.result.severityFilter.good=Ótimo
health.result.severityFilter.info=Informações
health.result.severityFilter.warn=Atenção
health.result.severityFilter.crit=Crítico
health.result.fixStateFilter.fixed=Corrigido
health.result.fixStateFilter.fixFailed=Falha na correção
## Fix Application
health.fix.fixBtn=Corrigir
health.fix.successTip=Correção bem-sucedida
# Preferences
preferences.title=Preferências
@@ -231,6 +235,7 @@ preferences.title=Preferências
preferences.general=Geral
preferences.general.startHidden=Ocultar janela ao iniciar o Cryptomator
preferences.general.autoCloseVaults=Bloquear cofres abertos automaticamente ao sair da aplicação
preferences.general.keychainBackend=Guardar palavras-passe com
## Interface
preferences.interface=Interface
preferences.interface.theme=Aspecto e Ambiente
@@ -246,7 +251,9 @@ preferences.interface.interfaceOrientation.rtl=Da direita para a esquerda
preferences.interface.showMinimizeButton=Mostrar botão de minimização
## Volume
preferences.volume=Unidade Virtual
preferences.volume.type=Tipo de Volume
preferences.volume.type.automatic=Automático
preferences.volume.feature.readOnly=Volume apenas-leitura
## Updates
preferences.updates=Atualizações
preferences.updates.currentVersion=Versão atual: %s
@@ -255,6 +262,10 @@ preferences.updates.checkNowBtn=Verificar Agora
preferences.updates.updateAvailable=Atualização para a versão %s disponível.
## Contribution
preferences.contribute=Apoie-nos
preferences.contribute.registeredFor=Certificado de apoiador registado para %s
preferences.contribute.noCertificate=Apoie o Cryptomator e receba um certificado de apoiador. É como uma chave de licença, mas para pessoas incríveis a usar um software gratuito. ;-)
preferences.contribute.getCertificate=Não o tem? Saiba como pode obtê-lo.
preferences.contribute.promptText=Insira o código do certificado de apoiador aqui
#<-- Add entries for donations and code/translation/documentation contribution -->
## About

View File

@@ -154,6 +154,8 @@ hub.registerFailed.description=Ocorreu um erro no processo de nomeação do disp
hub.unauthorized.message=Acesso negado
hub.unauthorized.description=Seu dispositivo ainda não foi autorizado a acessar este cofre. Peça ao proprietário ou a um administrador deste cofre para autorizá-lo.
### License Exceeded
hub.invalidLicense.message=Licença Invalida
hub.invalidLicense.description=Sua instância do Cryptomator Hub possui uma licença inválida. Por favor, informe um administrador do Hub para atualizar ou renovar a licença.
# Lock
## Force
@@ -272,7 +274,10 @@ preferences.interface.showMinimizeButton=Mostrar botão minimizar
preferences.interface.showTrayIcon=Mostrar ícone na barra do sistema (requer reinicialização)
## Volume
preferences.volume=Volume Virtual
preferences.volume.type=Tipo de Volume
preferences.volume.type.automatic=Automático
preferences.volume.docsTooltip=Abra a documentação para saber mais sobre os diferentes tipos de volumes.
preferences.volume.fuseRestartRequired=Para aplicar as mudanças, o Cryptomator precisa ser reiniciado.
preferences.volume.tcp.port=Porta TCP
preferences.volume.supportedFeatures=O tipo de volume escolhido suporta os seguintes recursos:
preferences.volume.feature.mountAuto=Seleção automática de ponto de montagem
@@ -302,21 +307,27 @@ stats.title=Estatísticas para %s
stats.cacheHitRate=Taxa de Utilização do Cache
## Read
stats.read.throughput.idle=Leitura: ociosa
stats.read.throughput.kibs=Leitura: %.2f MiB/s
stats.read.throughput.mibs=Leitura: %.2f MiB/s
stats.read.total.data.none=Dados lidos: -
stats.read.total.data.kib=Dados lidos: %.1f GiB
stats.read.total.data.mib=Dados lidos: %.1f MiB
stats.read.total.data.gib=Dados lidos: %.1f GiB
stats.decr.total.data.none=Dados descriptografados: -
stats.decr.total.data.kib=Dados descriptografados: %.1f GiB
stats.decr.total.data.mib=Dados descriptografados: %.1f MiB
stats.decr.total.data.gib=Dados descriptografados: %.1f GiB
stats.read.accessCount=Total de leituras: %d
## Write
stats.write.throughput.idle=Escrita: ociosa
stats.write.throughput.kibs=Escrita: %.2f MiB/s
stats.write.throughput.mibs=Escrita: %.2f MiB/s
stats.write.total.data.none=Dados gravados: -
stats.write.total.data.kib=Dados gravados: %.1f kiB
stats.write.total.data.mib=Dados gravados: %.1f MiB
stats.write.total.data.gib=Dados gravados: %.1f GiB
stats.encr.total.data.none=Dados criptografados: -
stats.encr.total.data.kib=Dados encriptados: %.1f KiB
stats.encr.total.data.mib=Dados criptografados: %.1f MiB
stats.encr.total.data.gib=Dados criptografados: %.1f GiB
stats.write.accessCount=Total gravado: %d
@@ -359,6 +370,7 @@ main.vaultDetail.lockBtn=Bloquear
main.vaultDetail.bytesPerSecondRead=Leitura:
main.vaultDetail.bytesPerSecondWritten=Escrita:
main.vaultDetail.throughput.idle=ocioso
main.vaultDetail.throughput.kbps=%.1f KiB/s
main.vaultDetail.throughput.mbps=%.1f MiB/s
main.vaultDetail.stats=Estatísticas do Cofre
main.vaultDetail.locateEncryptedFileBtn=Localizar Arquivo Criptografado
@@ -422,6 +434,9 @@ vaultOptions.masterkey.recoveryKeyExplanation=Se você perder a sua senha, a ún
vaultOptions.masterkey.showRecoveryKeyBtn=Exibir chave de recuperação
vaultOptions.masterkey.recoverPasswordBtn=Redefinir Senha
## Hub
vaultOptions.hub=Recuperação
vaultOptions.hub.convertInfo=Você pode usar a chave de recuperação para converter este cofre do Hub em um cofre protegido por senha de emergência.
vaultOptions.hub.convertBtn=Converter para Cofre protegido por senha
# Recovery Key
## Display Recovery Key
@@ -433,7 +448,10 @@ recoveryKey.display.StorageHints=Mantenha-a em um lugar bem seguro, por exemplo:
## Reset Password
### Enter Recovery Key
recoveryKey.recover.title=Redefinir Senha
recoveryKey.recover.prompt=Digite a chave de recuperação para "%s":
recoveryKey.recover.correctKey=Esta é uma chave de recuperação válida
recoveryKey.recover.wrongKey=Esta chave de recuperação pertence a um outro cofre
recoveryKey.recover.invalidKey=Esta chave de recuperação não é válida
recoveryKey.printout.heading=Chave de Recuperação do Cryptomator\n"%s"\n
### Reset Password
recoveryKey.recover.resetBtn=Redefinir
@@ -442,6 +460,11 @@ recoveryKey.recover.resetSuccess.message=Senha redefinida com sucesso
recoveryKey.recover.resetSuccess.description=Você pode desbloquear o seu cofre com a nova senha.
# Convert Vault
convertVault.title=Converter Cofre
convertVault.convert.convertBtn.before=Converter
convertVault.convert.convertBtn.processing=Convertendo...
convertVault.success.message=Conversão bem sucedida
convertVault.hubToPassword.success.description=Agora você pode desbloquear o cofre com a senha escolhida sem exigir acesso ao Hub.
# New Password
newPassword.promptText=Digite a nova senha

View File

@@ -434,6 +434,9 @@ vaultOptions.masterkey.recoveryKeyExplanation=Bir kurtarma anahtarı şifrenizi
vaultOptions.masterkey.showRecoveryKeyBtn=Kurtarma Anahtarını Göster
vaultOptions.masterkey.recoverPasswordBtn=Şifreyi Sıfırla
## Hub
vaultOptions.hub=Kurtarma
vaultOptions.hub.convertInfo=Acil bir durumda bu Hub kasasını parola tabanlı bir kasaya dönüştürmek için kurtarma anahtarını kullanabilirsiniz.
vaultOptions.hub.convertBtn=Parola Tabanlı Kasaya Dönüştür
# Recovery Key
## Display Recovery Key
@@ -445,6 +448,7 @@ recoveryKey.display.StorageHints=Bunu çok güvenli bir yerde saklayın, örneğ
## Reset Password
### Enter Recovery Key
recoveryKey.recover.title=Şifreyi Sıfırla
recoveryKey.recover.prompt="%s" için kurtarma anahtarını girin:
recoveryKey.recover.correctKey=Bu geçerli bir kurtarma anahtarı
recoveryKey.recover.wrongKey=Bu kurtarma anahtarı farklı bir kasaya ait
recoveryKey.recover.invalidKey=Bu kurtarma anahtarı geçerli değil
@@ -456,6 +460,11 @@ recoveryKey.recover.resetSuccess.message=Şifre sıfırlama başarılı
recoveryKey.recover.resetSuccess.description=Yeni şifre ile kasanızın kilidini açabilirsiniz.
# Convert Vault
convertVault.title=Kasayı Dönüştür
convertVault.convert.convertBtn.before=Dönüştür
convertVault.convert.convertBtn.processing=Dönüştürülüyor…
convertVault.success.message=Dönüştürme başarılı
convertVault.hubToPassword.success.description=Artık Hub erişimi gerektirmeden seçilen parola ile kasanın kilidini açabilirsiniz.
# New Password
newPassword.promptText=Yeni bir şifre girin

View File

@@ -79,17 +79,21 @@ addvault.new.readme.storageLocation.10=Якщо вам потрібна допо
addvault.new.readme.accessLocation.fileName=ПРИВІТ.rtf
addvault.new.readme.accessLocation.1=🔐️ ЗАШИФРОВАНИЙ ТОМ 🔐️
addvault.new.readme.accessLocation.2=Це місце розташування вашого vault.
addvault.new.readme.accessLocation.3=Будь-які файли, додані до цього тому, будуть зашифровані за допомогою Cryptomator. Ви можете працювати із ним як із будь-якою іншою директорією або накопичувачем. Це лише розшифрований вигляд його вмісту, ваші файли завжди знаходяться в зашифрованому вигляді на диску.
addvault.new.readme.accessLocation.4=Якщо хочете, то можете видалити цей файл.
## Existing
addvaultwizard.existing.instruction=Виберіть файл "vault.cryptomator" у вашому існуючому сховищі. Якщо існує лише файл з назвою "masterkey.cryptomator", виберіть його.
addvaultwizard.existing.chooseBtn=Обрати…
addvaultwizard.existing.filePickerTitle=Виберіть Vault Файл
addvaultwizard.existing.filePickerMimeDesc=Cryptomator Vault
## Success
addvaultwizard.success.nextStepsInstructions=Додано сховище "%s".\nДля доступу або додавання вмісту це сховище необхідно розблокувати. Також його можна розблокувати й пізніше.
addvaultwizard.success.unlockNow=Розблокувати
# Remove Vault
removeVault.title=Видалити "%s"
removeVault.message=Видалити vault?
removeVault.description=Це лише змусить Cryptomator забути про це сховище. Його можна буде додати пізніше. Зашифровані файли на жорсткому диску не будуть видалені.
removeVault.confirmBtn=Видалити сховище
# Change Password
@@ -119,6 +123,10 @@ unlock.success.description=Вміст vault "%s" тепер доступний
unlock.success.rememberChoice=Запам'ятайте мій вибір та більше не запитуйте
unlock.success.revealBtn=Розкрити Диск
## Failure
unlock.error.customPath.message=Не вдалося змонтувати сховище за вказаним шляхом
unlock.error.customPath.description.notSupported=Якщо ви хочете надалі використовувати власний шлях, будь ласка, перейдіть до налаштувань та виберіть тип тому, що його підтримує. В іншому випадку перейдіть до параметрів сховища та оберіть точку монтування, що підтримується.
unlock.error.customPath.description.notExists=Вказаний шлях підключення не існує. Створіть його в локальній файловій системі або змініть його в параметрах сховища.
unlock.error.customPath.description.generic=Ви створили власний шлях підключення для цього сховища, але скористатися ним не вдалося. Повідомлення про помилку: %s
## Hub
hub.noKeychain.message=Не вдалося отримати доступ до ключа пристрою
hub.noKeychain.description=Щоб розблокувати Hub vaults, необхідний ключ пристрою, який захищено за допомогою ланцюга ключів. Щоб продовжити, увімкніть “%s” та виберіть ланцюг ключів у налаштуваннях.
@@ -146,6 +154,8 @@ hub.registerFailed.description=Виникла помилка у процесі
hub.unauthorized.message=У доступі відмовлено
hub.unauthorized.description=Ваш пристрій ще не має прав доступу до цього vault. Попросіть власника vault надати їх.
### License Exceeded
hub.invalidLicense.message=Недійсна ліцензія Hub
hub.invalidLicense.description=У вашого Cryptomator Hub недійсна ліцензія. Будь ласка, повідомте адміністратору Hub, що потрібно оновити або продовжити ліцензію.
# Lock
## Force
@@ -324,16 +334,25 @@ vaultOptions.masterkey.recoveryKeyExplanation=У разі втрати паро
vaultOptions.masterkey.showRecoveryKeyBtn=Показати Ключ Відновлення
vaultOptions.masterkey.recoverPasswordBtn=Скинути Пароль
## Hub
vaultOptions.hub=Відновлення
vaultOptions.hub.convertInfo=Ключ відновлення використовується для перетворення цього Hub-сховища на сховище з паролем у надзвичайній ситуації.
vaultOptions.hub.convertBtn=Перетворити на сховище з паролем
# Recovery Key
## Display Recovery Key
## Reset Password
### Enter Recovery Key
recoveryKey.recover.title=Скинути Пароль
recoveryKey.recover.prompt=Введіть ключ відновлення для "%s:
### Reset Password
### Recovery Key Password Reset Success
# Convert Vault
convertVault.title=Перетворити сховище
convertVault.convert.convertBtn.before=Перетворити
convertVault.convert.convertBtn.processing=Перетворення…
convertVault.success.message=Перетворення виконано успішно
convertVault.hubToPassword.success.description=Тепер ви можете розблокувати сховище за допомогою обраного пароля без необхідності доступу до Hub.
# New Password

View File

@@ -25,7 +25,7 @@ public class SupportedLanguagesTest {
}
public static Stream<String> languageTags() {
return SupportedLanguages.LANGUAGAE_TAGS.stream() //
return SupportedLanguages.LANGUAGE_TAGS.stream() //
.filter(tag -> !"en".equals(tag)); // english uses the default bundle
}
}