diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml
index 71e2fe336..8698d58ab 100644
--- a/.github/workflows/appimage.yml
+++ b/.github/workflows/appimage.yml
@@ -16,7 +16,7 @@ jobs:
get-version:
uses: ./.github/workflows/get-version.yml
with:
- version: ${{ github.event.inputs.version }}
+ version: ${{ inputs.version }}
build:
name: Build AppImage
@@ -55,7 +55,7 @@ jobs:
--verbose
--output runtime
--module-path "${JAVA_HOME}/jmods"
- --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
+ --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.security.auth,jdk.accessibility,jdk.management.jfr
--strip-native-commands
--no-header-files
--no-man-pages
@@ -78,7 +78,7 @@ jobs:
--dest appdir
--name Cryptomator
--vendor "Skymatic GmbH"
- --copyright "(C) 2016 - 2022 Skymatic GmbH"
+ --copyright "(C) 2016 - 2023 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
--java-options "--enable-preview"
--java-options "--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64"
diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml
index 178d6ce96..c4d67319d 100644
--- a/.github/workflows/debian.yml
+++ b/.github/workflows/debian.yml
@@ -151,7 +151,7 @@ jobs:
uses: softprops/action-gh-release@v1
with:
fail_on_unmatched_files: true
- tag_name: ${{ github.env.TAG_NAME }}
+ tag_name: ${{ inputs.ref }}
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
files: |
cryptomator_*_amd64.deb
diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml
index 7e9a8c0d5..55b1f325d 100644
--- a/.github/workflows/mac-dmg.yml
+++ b/.github/workflows/mac-dmg.yml
@@ -16,7 +16,7 @@ jobs:
get-version:
uses: ./.github/workflows/get-version.yml
with:
- version: ${{ github.event.inputs.version }}
+ version: ${{ inputs.version }}
build:
name: Build Cryptomator.app for ${{ matrix.output-suffix }}
@@ -87,7 +87,7 @@ jobs:
--dest appdir
--name Cryptomator
--vendor "Skymatic GmbH"
- --copyright "(C) 2016 - 2022 Skymatic GmbH"
+ --copyright "(C) 2016 - 2023 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}"
--java-options "--enable-preview"
--java-options "--enable-native-access=org.cryptomator.jfuse.mac"
diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml
index a20f58d44..0a75c44ca 100644
--- a/.github/workflows/win-exe.yml
+++ b/.github/workflows/win-exe.yml
@@ -22,7 +22,7 @@ jobs:
get-version:
uses: ./.github/workflows/get-version.yml
with:
- version: ${{ github.event.inputs.version }}
+ version: ${{ inputs.version }}
build-msi:
name: Build .msi Installer
@@ -81,7 +81,7 @@ jobs:
--dest appdir
--name Cryptomator
--vendor "Skymatic GmbH"
- --copyright "(C) 2016 - 2022 Skymatic GmbH"
+ --copyright "(C) 2016 - 2023 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
--java-options "--enable-preview"
--java-options "--enable-native-access=org.cryptomator.jfuse.win"
@@ -118,6 +118,9 @@ jobs:
- name: Fix permissions
run: attrib -r appdir/Cryptomator/Cryptomator.exe
shell: pwsh
+ - name: Extract integrations DLL for code signing
+ shell: pwsh
+ run: gci ./appdir/Cryptomator/app/mods/ -File integrations-win-*.jar | ForEach-Object {Set-Location -Path $_.Directory; jar --file=$($_.FullName) --extract integrations.dll }
- name: Codesign
uses: skymatic/code-sign-action@v2
with:
@@ -128,6 +131,10 @@ jobs:
timestampUrl: 'http://timestamp.digicert.com'
folder: appdir/Cryptomator
recursive: true
+ - name: Repack signed DLL into jar
+ shell: pwsh
+ run: |
+ gci ./appdir/Cryptomator/app/mods/ -File integrations-win-*.jar | ForEach-Object {Set-Location -Path $_.Directory; jar --file=$($_.FullName) --update integrations.dll; Remove-Item integrations.dll}
- name: Generate license for MSI
run: >
mvn -B license:add-third-party
@@ -149,7 +156,7 @@ jobs:
--dest installer
--name Cryptomator
--vendor "Skymatic GmbH"
- --copyright "(C) 2016 - 2022 Skymatic GmbH"
+ --copyright "(C) 2016 - 2023 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}"
--win-menu
--win-dir-chooser
@@ -247,7 +254,7 @@ jobs:
-out dist/win/bundle/
-dBundleVersion="${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
-dBundleVendor="Skymatic GmbH"
- -dBundleCopyright="(C) 2016 - 2022 Skymatic GmbH"
+ -dBundleCopyright="(C) 2016 - 2023 Skymatic GmbH"
-dAboutUrl="https://cryptomator.org"
-dHelpUrl="https://cryptomator.org/contact"
-dUpdateUrl="https://cryptomator.org/downloads/"
diff --git a/README.md b/README.md
index fc7d7df55..b742846c2 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,7 @@ Cryptomator is provided free of charge as an open-source project despite the hig
 |
 |
+  |
diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh
index 06e3a7341..7fc9f04a8 100755
--- a/dist/linux/appimage/build.sh
+++ b/dist/linux/appimage/build.sh
@@ -11,15 +11,20 @@ command -v curl >/dev/null 2>&1 || { echo >&2 "curl not found."; exit 1; }
VERSION=$(mvn -f ../../../pom.xml help:evaluate -Dexpression=project.version -q -DforceStdout)
SEMVER_STR=${VERSION}
+mvn -f ../../../pom.xml versions:set -DnewVersion=${SEMVER_STR}
+
# compile
-mvn -B -f ../../../pom.xml clean package -DskipTests -Plinux
+mvn -B -f ../../../pom.xml clean package -Plinux -DskipTests
+cp ../../../LICENSE.txt ../../../target
+cp ../launcher.sh ../../../target
cp ../../../target/cryptomator-*.jar ../../../target/mods
# add runtime
${JAVA_HOME}/bin/jlink \
+ --verbose \
--output runtime \
--module-path "${JAVA_HOME}/jmods" \
- --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
+ --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.security.auth,jdk.accessibility,jdk.management.jfr \
--strip-native-commands \
--no-header-files \
--no-man-pages \
@@ -27,7 +32,7 @@ ${JAVA_HOME}/bin/jlink \
--compress=1
# create app dir
-envsubst '${SEMVER_STR} ${REVISION_NUM}' < dist/linux/launcher-gtk2.properties > launcher-gtk2.properties
+envsubst '${SEMVER_STR} ${REVISION_NUM}' < ../launcher-gtk2.properties > launcher-gtk2.properties
${JAVA_HOME}/bin/jpackage \
--verbose \
--type app-image \
@@ -35,12 +40,10 @@ ${JAVA_HOME}/bin/jpackage \
--input ../../../target/libs \
--module-path ../../../target/mods \
--module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator \
- --dest . \
+ --dest appdir \
--name Cryptomator \
--vendor "Skymatic GmbH" \
- --copyright "(C) 2016 - 2022 Skymatic GmbH" \
- --java-options "--enable-preview" \
- --java-options "--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64" \
+ --copyright "(C) 2016 - 2023 Skymatic GmbH" \
--java-options "-Xss5m" \
--java-options "-Xmx256m" \
--app-version "${VERSION}.${REVISION_NO}" \
@@ -48,6 +51,7 @@ ${JAVA_HOME}/bin/jpackage \
--java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\"" \
--java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\"" \
--java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\"" \
+ --java-options "-Dcryptomator.p12Path=\"~/.config/Cryptomator/key.p12\"" \
--java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" \
--java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" \
--java-options "-Dcryptomator.showTrayIcon=false" \
@@ -56,9 +60,8 @@ ${JAVA_HOME}/bin/jpackage \
--resource-dir ../resources
# transform AppDir
-mv Cryptomator Cryptomator.AppDir
+mv appdir/Cryptomator Cryptomator.AppDir
cp -r resources/AppDir/* Cryptomator.AppDir/
-chmod +x Cryptomator.AppDir/lib/runtime/bin/java
envsubst '${REVISION_NO}' < resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh
cp ../common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png
cp ../common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png
@@ -85,5 +88,11 @@ chmod +x /tmp/appimagetool.AppImage
# create AppImage
/tmp/appimagetool.AppImage \
Cryptomator.AppDir \
- cryptomator-SNAPSHOT-x86_64.AppImage \
+ cryptomator-${SEMVER_STR}-x86_64.AppImage \
-u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync'
+
+echo ""
+echo "Done. AppImage successfully created: cryptomator-${SEMVER_STR}-x86_64.AppImage"
+echo ""
+echo >&2 "To clean up, run: rm -rf Cryptomator.AppDir appdir jni runtime squashfs-root; rm launcher-gtk2.properties /tmp/appimagetool.AppImage"
+echo ""
\ No newline at end of file
diff --git a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml
index 9cb44d090..9797dd6a7 100644
--- a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml
+++ b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml
@@ -66,6 +66,7 @@
+
diff --git a/dist/linux/debian/copyright b/dist/linux/debian/copyright
index 34be0a4c9..745218b67 100644
--- a/dist/linux/debian/copyright
+++ b/dist/linux/debian/copyright
@@ -4,11 +4,11 @@ Upstream-Contact: Cryptomator
Source: https://cryptomator.org
Files: *
-Copyright: 2016-2022 Skymatic GmbH
+Copyright: 2016-2023 Skymatic GmbH
License: GPL-3+
Files: debian/org.cryptomator.Cryptomator.appdata.xml
-Copyright: 2016-2022 Skymatic GmbH
+Copyright: 2016-2023 Skymatic GmbH
License: FSFAP
License: GPL-3+
diff --git a/dist/linux/debian/rules b/dist/linux/debian/rules
index e35dbb0a7..ec82dea47 100755
--- a/dist/linux/debian/rules
+++ b/dist/linux/debian/rules
@@ -27,7 +27,7 @@ override_dh_auto_build:
$(JAVA_HOME)/bin/jlink \
--output runtime \
--module-path "${JMODS_PATH}" \
- --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
+ --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.security.auth,jdk.accessibility,jdk.management.jfr \
--strip-native-commands \
--no-header-files \
--no-man-pages \
@@ -42,9 +42,7 @@ override_dh_auto_build:
--dest . \
--name cryptomator \
--vendor "Skymatic GmbH" \
- --copyright "(C) 2016 - 2022 Skymatic GmbH" \
- --java-options "--enable-preview" \
- --java-options "--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64" \
+ --copyright "(C) 2016 - 2023 Skymatic GmbH" \
--java-options "-Xss5m" \
--java-options "-Xmx256m" \
--java-options "-Dfile.encoding=\"utf-8\"" \
diff --git a/dist/mac/dmg/build.sh b/dist/mac/dmg/build.sh
index 906072ebd..75044bf86 100755
--- a/dist/mac/dmg/build.sh
+++ b/dist/mac/dmg/build.sh
@@ -21,7 +21,7 @@ rm -rf runtime dmg *.app *.dmg
# set variables
APP_NAME="Cryptomator"
VENDOR="Skymatic GmbH"
-COPYRIGHT_YEARS="2016 - 2022"
+COPYRIGHT_YEARS="2016 - 2023"
PACKAGE_IDENTIFIER="org.cryptomator"
MAIN_JAR_GLOB="cryptomator-*.jar"
MODULE_AND_MAIN_CLASS="org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator"
diff --git a/dist/mac/dmg/resources/licenseTemplate.ftl b/dist/mac/dmg/resources/licenseTemplate.ftl
index e4d7fd476..98178151c 100644
--- a/dist/mac/dmg/resources/licenseTemplate.ftl
+++ b/dist/mac/dmg/resources/licenseTemplate.ftl
@@ -17,7 +17,7 @@
\f1\b0 \
\
-\f0\b \'a9 2016 \'96 2022 Skymatic GmbH
+\f0\b \'a9 2016 \'96 2023 Skymatic GmbH
\f1\b0 \
\
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\
diff --git a/dist/mac/resources/Cryptomator.icns b/dist/mac/resources/Cryptomator.icns
index 25da5b5be..dfdb4499d 100644
Binary files a/dist/mac/resources/Cryptomator.icns and b/dist/mac/resources/Cryptomator.icns differ
diff --git a/dist/win/bundle/resources/licenseTemplate.ftl b/dist/win/bundle/resources/licenseTemplate.ftl
index 8a568b85d..40d55e292 100644
--- a/dist/win/bundle/resources/licenseTemplate.ftl
+++ b/dist/win/bundle/resources/licenseTemplate.ftl
@@ -10,7 +10,7 @@
\vieww12000\viewh15840\viewkind0
\pard\tx283\tx567\tx850\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\b\fs16\lang7 Cryptomator is distributed under the GPLv3 License, found below. Please see the bottom of this document for any other license applicable to code used within Cryptomator.\b0\par
\par
-\b\'a9 2016 \'96 2022 Skymatic GmbH \b0\par
+\b\'a9 2016 \'96 2023 Skymatic GmbH \b0\par
\par
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\par
\par
diff --git a/dist/win/resources/licenseTemplate.ftl b/dist/win/resources/licenseTemplate.ftl
index 0ee793cb1..88a80f8b6 100644
--- a/dist/win/resources/licenseTemplate.ftl
+++ b/dist/win/resources/licenseTemplate.ftl
@@ -10,7 +10,7 @@
\vieww12000\viewh15840\viewkind0
\pard\tx283\tx567\tx850\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\b\fs16\lang7 Cryptomator is distributed under the GPLv3 License, found below. Please see the bottom of this document for any other license applicable to code used within Cryptomator.\b0\par
\par
-\b\'a9 2016 \'96 2022 Skymatic GmbH \b0\par
+\b\'a9 2016 \'96 2023 Skymatic GmbH \b0\par
\par
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\par
\par
diff --git a/pom.xml b/pom.xml
index b2fde6904..171a97083 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,18 +24,19 @@
19
-
- com.github.jnr,org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh
+
+ org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava
- 2.5.3
- 1.2.0-beta2
- 1.1.2
- 1.1.2
- 1.1.0
- 2.0.0-beta2
- 2.0.0-beta1
- 2.0.0-beta1
+ 2.1.1
+ 2.6.1
+ 1.2.0-beta4
+ 1.2.0-beta1
+ 1.2.0-beta2
+ 1.2.0-beta1
+ 2.0.0-beta4
+ 2.0.0-beta2
+ 2.0.0-beta4
3.12.0
@@ -58,12 +59,18 @@
23.0.0
- 7.4.0
+ 7.4.4
0.8.8
+
+
+ org.cryptomator
+ cryptolib
+ ${cryptomator.cryptolib.version}
+
org.cryptomator
cryptofs
diff --git a/src/main/java/org/cryptomator/common/ObservableUtil.java b/src/main/java/org/cryptomator/common/ObservableUtil.java
new file mode 100644
index 000000000..289f6e929
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/ObservableUtil.java
@@ -0,0 +1,18 @@
+package org.cryptomator.common;
+
+import javafx.beans.binding.Bindings;
+import javafx.beans.value.ObservableValue;
+import java.util.function.Function;
+
+public class ObservableUtil {
+
+ public static ObservableValue mapWithDefault(ObservableValue observable, Function super T, ? extends U> mapper, U defaultValue) {
+ return Bindings.createObjectBinding(() -> {
+ if (observable.getValue() == null) {
+ return defaultValue;
+ } else {
+ return mapper.apply(observable.getValue());
+ }
+ }, observable);
+ }
+}
diff --git a/src/main/java/org/cryptomator/common/mount/ActualMountService.java b/src/main/java/org/cryptomator/common/mount/ActualMountService.java
new file mode 100644
index 000000000..a96cc8e37
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/ActualMountService.java
@@ -0,0 +1,6 @@
+package org.cryptomator.common.mount;
+
+import org.cryptomator.integrations.mount.MountService;
+
+public record ActualMountService(MountService service, boolean isDesired) {
+}
diff --git a/src/main/java/org/cryptomator/common/mount/IllegalMountPointException.java b/src/main/java/org/cryptomator/common/mount/IllegalMountPointException.java
new file mode 100644
index 000000000..5fdb1d91c
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/IllegalMountPointException.java
@@ -0,0 +1,9 @@
+package org.cryptomator.common.mount;
+
+public class IllegalMountPointException extends IllegalArgumentException {
+
+ public IllegalMountPointException(String msg) {
+ super(msg);
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/common/mount/MountModule.java b/src/main/java/org/cryptomator/common/mount/MountModule.java
index ac2e5f598..d78cc3216 100644
--- a/src/main/java/org/cryptomator/common/mount/MountModule.java
+++ b/src/main/java/org/cryptomator/common/mount/MountModule.java
@@ -6,6 +6,7 @@ import org.cryptomator.common.settings.Settings;
import org.cryptomator.integrations.mount.MountService;
import javax.inject.Singleton;
+import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import java.util.List;
@@ -18,13 +19,33 @@ public class MountModule {
return MountService.get().toList();
}
+ //currently not used, because macFUSE and FUSE-T cannot be used in the same JVM
+ /*
@Provides
@Singleton
- static ObservableValue provideMountService(Settings settings, List serviceImpls) {
- return settings.mountService().map(desiredServiceImpl -> {
- var fallbackProvider = serviceImpls.stream().findFirst().orElse(null);
- return serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(desiredServiceImpl)).findAny().orElse(fallbackProvider);
- });
+ static ObservableValue provideMountService(Settings settings, List serviceImpls) {
+ var fallbackProvider = serviceImpls.stream().findFirst().orElse(null);
+ return ObservableUtil.mapWithDefault(settings.mountService(), //
+ desiredServiceImpl -> { //
+ var desiredService = serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(desiredServiceImpl)).findAny(); //
+ return new ActualMountService(desiredService.orElse(fallbackProvider), desiredService.isPresent()); //
+ }, //
+ new ActualMountService(fallbackProvider, true));
+ }
+ */
+
+ @Provides
+ @Singleton
+ static ActualMountService provideActualMountService(Settings settings, List serviceImpls) {
+ var fallbackProvider = serviceImpls.stream().findFirst().orElse(null);
+ var desiredService = serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(settings.mountService().getValue())).findFirst(); //
+ return new ActualMountService(desiredService.orElse(fallbackProvider), desiredService.isPresent()); //
+ }
+
+ @Provides
+ @Singleton
+ static ObservableValue provideMountService(ActualMountService service) {
+ return new SimpleObjectProperty<>(service);
}
}
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointNotExistsException.java b/src/main/java/org/cryptomator/common/mount/MountPointNotExistsException.java
new file mode 100644
index 000000000..e90523bc2
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/MountPointNotExistsException.java
@@ -0,0 +1,8 @@
+package org.cryptomator.common.mount;
+
+public class MountPointNotExistsException extends IllegalMountPointException {
+
+ public MountPointNotExistsException(String msg) {
+ super(msg);
+ }
+}
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointNotSupportedException.java b/src/main/java/org/cryptomator/common/mount/MountPointNotSupportedException.java
new file mode 100644
index 000000000..e321f23b1
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/MountPointNotSupportedException.java
@@ -0,0 +1,8 @@
+package org.cryptomator.common.mount;
+
+public class MountPointNotSupportedException extends IllegalMountPointException {
+
+ public MountPointNotSupportedException(String msg) {
+ super(msg);
+ }
+}
diff --git a/src/main/java/org/cryptomator/common/mount/MountPointPreparationException.java b/src/main/java/org/cryptomator/common/mount/MountPointPreparationException.java
new file mode 100644
index 000000000..fb481167c
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/MountPointPreparationException.java
@@ -0,0 +1,8 @@
+package org.cryptomator.common.mount;
+
+public class MountPointPreparationException extends RuntimeException {
+
+ public MountPointPreparationException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java b/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java
new file mode 100644
index 000000000..ca82b54da
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java
@@ -0,0 +1,110 @@
+package org.cryptomator.common.mount;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+
+public final class MountWithinParentUtil {
+
+ private static final Logger LOG = LoggerFactory.getLogger(Mounter.class);
+ private static final String HIDEAWAY_PREFIX = ".~$";
+ private static final String HIDEAWAY_SUFFIX = ".tmp";
+ private static final String WIN_HIDDEN_ATTR = "dos:hidden";
+
+ private MountWithinParentUtil() {}
+
+ static void prepareParentNoMountPoint(Path mountPoint) throws MountPointPreparationException {
+ Path hideaway = getHideaway(mountPoint);
+ var mpExists = Files.exists(mountPoint, LinkOption.NOFOLLOW_LINKS);
+ var hideExists = Files.exists(hideaway, LinkOption.NOFOLLOW_LINKS);
+
+ //TODO: possible improvement by just deleting an _empty_ hideaway
+ if (mpExists && hideExists) { //both resources exist (whatever type)
+ throw new MountPointPreparationException(new FileAlreadyExistsException(hideaway.toString()));
+ } else if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist
+ throw new MountPointPreparationException(new NoSuchFileException(mountPoint.toString()));
+ } else if (!mpExists) { //only hideaway exists
+ checkIsDirectory(hideaway);
+ LOG.info("Mountpoint {} seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint);
+ try {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ Files.setAttribute(hideaway, WIN_HIDDEN_ATTR, true, LinkOption.NOFOLLOW_LINKS);
+ }
+ } catch (IOException e) {
+ throw new MountPointPreparationException(e);
+ }
+ } else { //only mountpoint exists
+ try {
+ checkIsDirectory(mountPoint);
+ checkIsEmpty(mountPoint);
+
+ Files.move(mountPoint, hideaway);
+ if (SystemUtils.IS_OS_WINDOWS) {
+ Files.setAttribute(hideaway, WIN_HIDDEN_ATTR, true, LinkOption.NOFOLLOW_LINKS);
+ }
+ } catch (IOException e) {
+ throw new MountPointPreparationException(e);
+ }
+ }
+ }
+
+ static void cleanup(Path mountPoint) {
+ Path hideaway = getHideaway(mountPoint);
+ try {
+ waitForMountpointRestoration(mountPoint);
+ Files.move(hideaway, mountPoint);
+ if (SystemUtils.IS_OS_WINDOWS) {
+ Files.setAttribute(mountPoint, WIN_HIDDEN_ATTR, false);
+ }
+ } catch (IOException e) {
+ LOG.error("Unable to restore hidden directory to mountpoint {}.", mountPoint, e);
+ }
+ }
+
+ //on Windows removing the mountpoint takes some time, so we poll for at most 3 seconds
+ private static void waitForMountpointRestoration(Path mountPoint) throws FileAlreadyExistsException {
+ int attempts = 0;
+ while (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) {
+ attempts++;
+ if (attempts >= 5) {
+ throw new FileAlreadyExistsException("Timeout waiting for mountpoint cleanup for " + mountPoint + " .");
+ }
+
+ try {
+ Thread.sleep(300);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new FileAlreadyExistsException("Interrupted before mountpoint " + mountPoint + " was cleared");
+ }
+ }
+ }
+
+ private static void checkIsDirectory(Path toCheck) throws MountPointPreparationException {
+ if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) {
+ throw new MountPointPreparationException(new NotDirectoryException(toCheck.toString()));
+ }
+ }
+
+ private static void checkIsEmpty(Path toCheck) throws MountPointPreparationException, IOException {
+ try (var dirStream = Files.list(toCheck)) {
+ if (dirStream.findFirst().isPresent()) {
+ throw new MountPointPreparationException(new DirectoryNotEmptyException(toCheck.toString()));
+ }
+ }
+ }
+
+ //visible for testing
+ static Path getHideaway(Path mountPoint) {
+ return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX);
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/common/mount/Mounter.java b/src/main/java/org/cryptomator/common/mount/Mounter.java
new file mode 100644
index 000000000..2c75d3f89
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/Mounter.java
@@ -0,0 +1,138 @@
+package org.cryptomator.common.mount;
+
+import org.cryptomator.common.Environment;
+import org.cryptomator.common.settings.Settings;
+import org.cryptomator.common.settings.VaultSettings;
+import org.cryptomator.integrations.mount.Mount;
+import org.cryptomator.integrations.mount.MountBuilder;
+import org.cryptomator.integrations.mount.MountFailedException;
+import org.cryptomator.integrations.mount.MountService;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javafx.beans.value.ObservableValue;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+
+import static org.cryptomator.integrations.mount.MountCapability.MOUNT_AS_DRIVE_LETTER;
+import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR;
+import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_SYSTEM_CHOSEN_PATH;
+import static org.cryptomator.integrations.mount.MountCapability.MOUNT_WITHIN_EXISTING_PARENT;
+import static org.cryptomator.integrations.mount.MountCapability.UNMOUNT_FORCED;
+
+@Singleton
+public class Mounter {
+
+ private final Settings settings;
+ private final Environment env;
+ private final WindowsDriveLetters driveLetters;
+ private final ObservableValue mountServiceObservable;
+
+ @Inject
+ public Mounter(Settings settings, Environment env, WindowsDriveLetters driveLetters, ObservableValue mountServiceObservable) {
+ this.settings = settings;
+ this.env = env;
+ this.driveLetters = driveLetters;
+ this.mountServiceObservable = mountServiceObservable;
+ }
+
+ private class SettledMounter {
+
+ private MountService service;
+ private MountBuilder builder;
+ private VaultSettings vaultSettings;
+
+ public SettledMounter(MountService service, MountBuilder builder, VaultSettings vaultSettings) {
+ this.service = service;
+ this.builder = builder;
+ this.vaultSettings = vaultSettings;
+ }
+
+ Runnable prepare() throws IOException {
+ for (var capability : service.capabilities()) {
+ switch (capability) {
+ case FILE_SYSTEM_NAME -> builder.setFileSystemName("cryptoFs");
+ case LOOPBACK_PORT ->
+ builder.setLoopbackPort(settings.port().get()); //TODO: move port from settings to vaultsettings (see https://github.com/cryptomator/cryptomator/tree/feature/mount-setting-per-vault)
+ case LOOPBACK_HOST_NAME -> env.getLoopbackAlias().ifPresent(builder::setLoopbackHostName);
+ case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode().get());
+ case MOUNT_FLAGS -> {
+ var mountFlags = vaultSettings.mountFlags().get();
+ if( mountFlags == null || mountFlags.isBlank()) {
+ builder.setMountFlags(service.getDefaultMountFlags());
+ } else {
+ builder.setMountFlags(mountFlags);
+ }
+ }
+ case VOLUME_ID -> builder.setVolumeId(vaultSettings.getId());
+ case VOLUME_NAME -> builder.setVolumeName(vaultSettings.mountName().get());
+ }
+ }
+
+ return prepareMountPoint();
+ }
+
+ private Runnable prepareMountPoint() throws IOException {
+ Runnable cleanup = () -> {};
+ var userChosenMountPoint = vaultSettings.getMountPoint();
+ var defaultMountPointBase = env.getMountPointsDir().orElseThrow();
+ var canMountToDriveLetter = service.hasCapability(MOUNT_AS_DRIVE_LETTER);
+ var canMountToParent = service.hasCapability(MOUNT_WITHIN_EXISTING_PARENT);
+ var canMountToDir = service.hasCapability(MOUNT_TO_EXISTING_DIR);
+
+ if (userChosenMountPoint == null) {
+ if (service.hasCapability(MOUNT_TO_SYSTEM_CHOSEN_PATH)) {
+ // no need to set a mount point
+ } else if (canMountToDriveLetter) {
+ builder.setMountpoint(driveLetters.getFirstDesiredAvailable().orElseThrow()); //TODO: catch exception and translate
+ } else if (canMountToParent) {
+ Files.createDirectories(defaultMountPointBase);
+ builder.setMountpoint(defaultMountPointBase);
+ } else if (canMountToDir) {
+ var mountPoint = defaultMountPointBase.resolve(vaultSettings.mountName().get());
+ Files.createDirectories(mountPoint);
+ builder.setMountpoint(mountPoint);
+ }
+ } else {
+ if (canMountToParent && !canMountToDir) {
+ MountWithinParentUtil.prepareParentNoMountPoint(userChosenMountPoint);
+ cleanup = () -> {
+ MountWithinParentUtil.cleanup(userChosenMountPoint);
+ };
+ }
+ try {
+ builder.setMountpoint(userChosenMountPoint);
+ } catch (IllegalArgumentException e) {
+ var mpIsDriveLetter = userChosenMountPoint.toString().matches("[A-Z]:\\\\");
+ var configNotSupported = (!canMountToDriveLetter && mpIsDriveLetter) || (!canMountToDir && !mpIsDriveLetter) || (!canMountToParent && !mpIsDriveLetter);
+ if (configNotSupported) {
+ throw new MountPointNotSupportedException(e.getMessage());
+ } else if (canMountToDir && !canMountToParent && !Files.exists(userChosenMountPoint)) {
+ //mountpoint must exist
+ throw new MountPointNotExistsException(e.getMessage());
+ } else {
+ //TODO: add specific exception for !canMountToDir && canMountToParent && !Files.notExists(userChosenMountPoint)
+ throw new IllegalMountPointException(e.getMessage());
+ }
+ }
+ }
+ return cleanup;
+ }
+
+ }
+
+ public MountHandle mount(VaultSettings vaultSettings, Path cryptoFsRoot) throws IOException, MountFailedException {
+ var mountService = this.mountServiceObservable.getValue().service();
+ var builder = mountService.forFileSystem(cryptoFsRoot);
+ var internal = new SettledMounter(mountService, builder, vaultSettings);
+ var cleanup = internal.prepare();
+ return new MountHandle(builder.mount(), mountService.hasCapability(UNMOUNT_FORCED), cleanup);
+ }
+
+ public record MountHandle(Mount mountObj, boolean supportsUnmountForced, Runnable specialCleanup) {
+
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/common/settings/DeviceKey.java b/src/main/java/org/cryptomator/common/settings/DeviceKey.java
index 04e9ebd0f..d3431440a 100644
--- a/src/main/java/org/cryptomator/common/settings/DeviceKey.java
+++ b/src/main/java/org/cryptomator/common/settings/DeviceKey.java
@@ -76,6 +76,11 @@ public class DeviceKey {
private P384KeyPair createAndStoreNewKeyPair(char[] passphrase, Path p12File) throws IOException {
var keyPair = P384KeyPair.generate();
+ var tmpFile = p12File.resolveSibling(p12File.getFileName().toString() + ".tmp");
+ if(Files.exists(tmpFile)) {
+ LOG.debug("Leftover from devicekey creation detected. Deleting {}", tmpFile);
+ Files.delete(tmpFile);
+ }
LOG.debug("Store new device key to {}", p12File);
keyPair.store(p12File, passphrase);
return keyPair;
diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
index 91299bb2a..b6c5c426c 100644
--- a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
+++ b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
@@ -9,6 +9,7 @@ import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
+import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -73,7 +74,9 @@ public class SettingsJsonAdapter extends TypeAdapter {
@Override
public Settings read(JsonReader in) throws IOException {
Settings settings = new Settings(env);
-
+ //1.6.x legacy
+ String volumeImpl = null;
+ //legacy end
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
@@ -105,18 +108,49 @@ public class SettingsJsonAdapter extends TypeAdapter {
settings.mountService().set(in.nextString());
}
}
-
+ //1.6.x legacy
+ case "preferredVolumeImpl" -> volumeImpl = in.nextString();
+ //legacy end
default -> {
LOG.warn("Unsupported vault setting found in JSON: {}", name);
in.skipValue();
}
}
+
}
in.endObject();
+ //1.6.x legacy
+ if (volumeImpl != null) {
+ settings.mountService().set(convertLegacyVolumeImplToMountService(volumeImpl));
+ }
+ //legacy end
+
return settings;
}
+ private String convertLegacyVolumeImplToMountService(String volumeImpl) {
+ if (volumeImpl.equals("Dokany")) {
+ return "org.cryptomator.frontend.dokany.mount.DokanyMountProvider";
+ } else if (volumeImpl.equals("FUSE")) {
+ if(SystemUtils.IS_OS_WINDOWS) {
+ return "org.cryptomator.frontend.fuse.mount.WinFspNetworkMountProvider";
+ } else if (SystemUtils.IS_OS_MAC) {
+ return "org.cryptomator.frontend.fuse.mount.MacFuseMountProvider";
+ } else {
+ return "org.cryptomator.frontend.fuse.mount.LinuxFuseMountProvider";
+ }
+ } else {
+ if(SystemUtils.IS_OS_WINDOWS) {
+ return "org.cryptomator.frontend.webdav.mount.WindowsMounter";
+ } else if (SystemUtils.IS_OS_MAC) {
+ return "org.cryptomator.frontend.webdav.mount.MacAppleScriptMounter";
+ } else {
+ return "org.cryptomator.frontend.webdav.mount.LinuxGioMounter";
+ }
+ }
+ }
+
private UiTheme parseUiTheme(String uiThemeName) {
try {
return UiTheme.valueOf(uiThemeName.toUpperCase());
diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/src/main/java/org/cryptomator/common/settings/VaultSettings.java
index 57c114718..3a116b4ce 100644
--- a/src/main/java/org/cryptomator/common/settings/VaultSettings.java
+++ b/src/main/java/org/cryptomator/common/settings/VaultSettings.java
@@ -45,7 +45,7 @@ public class VaultSettings {
private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REVEAL_AFTER_MOUNT);
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
- private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
+ private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS); //TODO: remove empty default mount flags and let this property be null if not used
private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
private final ObjectProperty actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
private final BooleanProperty autoLockWhenIdle = new SimpleBooleanProperty(DEFAULT_AUTOLOCK_WHEN_IDLE);
diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java
index 0f1aa7edd..cffeb3aa7 100644
--- a/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java
+++ b/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java
@@ -38,8 +38,6 @@ class VaultSettingsJsonAdapter {
out.endObject();
}
- //TODO: usesCustomMountPath, customMountPath and winDriveLetter removed
- // -> migration required
public VaultSettings read(JsonReader in) throws IOException {
String id = null;
String path = null;
@@ -55,6 +53,12 @@ class VaultSettingsJsonAdapter {
boolean autoLockWhenIdle = VaultSettings.DEFAULT_AUTOLOCK_WHEN_IDLE;
int autoLockIdleSeconds = VaultSettings.DEFAULT_AUTOLOCK_IDLE_SECONDS;
+ //legacy from 1.6.x
+ boolean useCustomMountPath = false;
+ String customMountPath = "";
+ String winDriveLetter = "";
+ //legacy end
+
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
@@ -78,6 +82,11 @@ class VaultSettingsJsonAdapter {
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
case "autoLockWhenIdle" -> autoLockWhenIdle = in.nextBoolean();
case "autoLockIdleSeconds" -> autoLockIdleSeconds = in.nextInt();
+ //legacy from 1.6.x
+ case "winDriveLetter" -> winDriveLetter = in.nextString();
+ case "usesIndividualMountPath", "useCustomMountPath" -> useCustomMountPath = in.nextBoolean();
+ case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
+ //legacy end
default -> {
LOG.warn("Unsupported vault setting found in JSON: {}", name);
in.skipValue();
@@ -102,6 +111,13 @@ class VaultSettingsJsonAdapter {
vaultSettings.autoLockWhenIdle().set(autoLockWhenIdle);
vaultSettings.autoLockIdleSeconds().set(autoLockIdleSeconds);
vaultSettings.mountPoint().set(mountPoint);
+ //legacy from 1.6.x
+ if(useCustomMountPath && !customMountPath.isBlank()) {
+ vaultSettings.mountPoint().set(parseMountPoint(customMountPath));
+ } else if(!winDriveLetter.isBlank() ) {
+ vaultSettings.mountPoint().set(parseMountPoint(winDriveLetter+":\\"));
+ }
+ //legacy end
return vaultSettings;
}
diff --git a/src/main/java/org/cryptomator/common/settings/VolumeImpl.java b/src/main/java/org/cryptomator/common/settings/VolumeImpl.java
deleted file mode 100644
index 473596fc4..000000000
--- a/src/main/java/org/cryptomator/common/settings/VolumeImpl.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.cryptomator.common.settings;
-
-public enum VolumeImpl {
- WEBDAV("WebDAV"),
- FUSE("FUSE"),
- DOKANY("Dokany");
-
- private String displayName;
-
- VolumeImpl(String displayName) {
- this.displayName = displayName;
- }
-
- public String getDisplayName() {
- return displayName;
- }
-
-}
diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java
index 835560120..e4dbd9b2c 100644
--- a/src/main/java/org/cryptomator/common/vaults/Vault.java
+++ b/src/main/java/org/cryptomator/common/vaults/Vault.java
@@ -8,12 +8,10 @@
*******************************************************************************/
package org.cryptomator.common.vaults;
-import com.google.common.base.Strings;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Constants;
-import org.cryptomator.common.Environment;
+import org.cryptomator.common.mount.Mounter;
import org.cryptomator.common.mount.WindowsDriveLetters;
-import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
@@ -23,11 +21,7 @@ import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
-import org.cryptomator.integrations.mount.Mount;
-import org.cryptomator.integrations.mount.MountBuilder;
-import org.cryptomator.integrations.mount.MountCapability;
import org.cryptomator.integrations.mount.MountFailedException;
-import org.cryptomator.integrations.mount.MountService;
import org.cryptomator.integrations.mount.Mountpoint;
import org.cryptomator.integrations.mount.UnmountFailedException;
import org.slf4j.Logger;
@@ -44,9 +38,7 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.value.ObservableValue;
import java.io.IOException;
-import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
@@ -54,11 +46,6 @@ import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
-import static org.cryptomator.integrations.mount.MountCapability.MOUNT_AS_DRIVE_LETTER;
-import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR;
-import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_SYSTEM_CHOSEN_PATH;
-import static org.cryptomator.integrations.mount.MountCapability.MOUNT_WITHIN_EXISTING_PARENT;
-
@PerVault
public class Vault {
@@ -66,14 +53,10 @@ public class Vault {
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
- private final Environment env;
- private final Settings settings;
private final VaultSettings vaultSettings;
private final AtomicReference cryptoFileSystem;
private final VaultState state;
private final ObjectProperty lastKnownException;
- private final ObservableValue mountService;
- private final ObservableValue defaultMountFlags;
private final VaultConfigCache configCache;
private final VaultStats stats;
private final StringBinding displayablePath;
@@ -84,22 +67,18 @@ public class Vault {
private final BooleanBinding needsMigration;
private final BooleanBinding unknownError;
private final ObjectBinding mountPoint;
- private final WindowsDriveLetters windowsDriveLetters;
+ private final Mounter mounter;
private final BooleanProperty showingStats;
- private AtomicReference mountHandle = new AtomicReference<>(null);
+ private AtomicReference mountHandle = new AtomicReference<>(null);
@Inject
- Vault(Environment env, Settings settings, VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty lastKnownException, ObservableValue mountService, VaultStats stats, WindowsDriveLetters windowsDriveLetters) {
- this.env = env;
- this.settings = settings;
+ Vault(VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty lastKnownException, VaultStats stats, WindowsDriveLetters windowsDriveLetters, Mounter mounter) {
this.vaultSettings = vaultSettings;
this.configCache = configCache;
this.cryptoFileSystem = cryptoFileSystem;
this.state = state;
this.lastKnownException = lastKnownException;
- this.mountService = mountService;
- this.defaultMountFlags = mountService.map(MountService::getDefaultMountFlags);
this.stats = stats;
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
this.locked = Bindings.createBooleanBinding(this::isLocked, state);
@@ -109,7 +88,7 @@ public class Vault {
this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state);
this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state);
- this.windowsDriveLetters = windowsDriveLetters;
+ this.mounter = mounter;
this.showingStats = new SimpleBooleanProperty(false);
}
@@ -159,45 +138,6 @@ public class Vault {
}
}
- private MountBuilder prepareMount(Path cryptoRoot) throws IOException {
- var mountProvider = mountService.getValue();
- var builder = mountProvider.forFileSystem(cryptoRoot);
-
- for (var capability : mountProvider.capabilities()) {
- switch (capability) {
- case FILE_SYSTEM_NAME -> builder.setFileSystemName("crypto");
- case LOOPBACK_PORT -> builder.setLoopbackPort(settings.port().get()); //TODO: move port from settings to vaultsettings?
- case LOOPBACK_HOST_NAME -> builder.setLoopbackHostName("cryptomator-vault"); //TODO: Read from system property
- case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode().get());
- case MOUNT_FLAGS -> builder.setMountFlags(defaultMountFlags.getValue()); // TODO use custom mount flags (pre-populated with default mount flags)
- case VOLUME_ID -> builder.setVolumeId(vaultSettings.getId());
- case VOLUME_NAME -> builder.setVolumeName(vaultSettings.mountName().get());
- }
- }
-
- var userChosenMountPoint = vaultSettings.getMountPoint();
- var defaultMountPointBase = env.getMountPointsDir().orElseThrow();
- if (userChosenMountPoint == null) {
- if (mountProvider.hasCapability(MOUNT_TO_SYSTEM_CHOSEN_PATH)) {
- // no need to set a mount point
- } else if (mountProvider.hasCapability(MOUNT_AS_DRIVE_LETTER)) {
- builder.setMountpoint(windowsDriveLetters.getFirstDesiredAvailable().orElseThrow());
- } else if (mountProvider.hasCapability(MOUNT_WITHIN_EXISTING_PARENT)) {
- Files.createDirectories(defaultMountPointBase);
- builder.setMountpoint(defaultMountPointBase);
- } else if (mountProvider.hasCapability(MOUNT_TO_EXISTING_DIR) ) {
- var mountPoint = defaultMountPointBase.resolve(vaultSettings.mountName().get());
- Files.createDirectories(mountPoint);
- builder.setMountpoint(mountPoint);
- }
- } else if (mountProvider.hasCapability(MOUNT_TO_EXISTING_DIR) || mountProvider.hasCapability(MOUNT_WITHIN_EXISTING_PARENT) || mountProvider.hasCapability(MOUNT_AS_DRIVE_LETTER)) {
- // TODO: move the mount point away in case of MOUNT_WITHIN_EXISTING_PARENT?
- builder.setMountpoint(userChosenMountPoint);
- }
-
- return builder;
- }
-
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, MountFailedException {
if (cryptoFileSystem.get() != null) {
throw new IllegalStateException("Already unlocked.");
@@ -207,8 +147,7 @@ public class Vault {
try {
cryptoFileSystem.set(fs);
var rootPath = fs.getRootDirectories().iterator().next();
- var supportsForcedUnmount = mountService.getValue().hasCapability(MountCapability.UNMOUNT_FORCED);
- var mountHandle = new MountHandle(prepareMount(rootPath).mount(), supportsForcedUnmount);
+ var mountHandle = mounter.mount(vaultSettings, rootPath);
success = this.mountHandle.compareAndSet(null, mountHandle);
} finally {
if (!success) {
@@ -217,7 +156,6 @@ public class Vault {
}
}
-
public synchronized void lock(boolean forced) throws UnmountFailedException, IOException {
var mountHandle = this.mountHandle.get();
if (mountHandle == null) {
@@ -225,14 +163,15 @@ public class Vault {
return;
}
- if (forced && mountHandle.supportsUnmountForced) {
- mountHandle.mount.unmountForced();
+ if (forced && mountHandle.supportsUnmountForced()) {
+ mountHandle.mountObj().unmountForced();
} else {
- mountHandle.mount.unmount();
+ mountHandle.mountObj().unmount();
}
try {
- mountHandle.mount.close();
+ mountHandle.mountObj().close();
+ mountHandle.specialCleanup().run();
} finally {
destroyCryptoFileSystem();
}
@@ -327,7 +266,7 @@ public class Vault {
public Mountpoint getMountPoint() {
var handle = mountHandle.get();
- return handle == null ? null : handle.mount.getMountpoint();
+ return handle == null ? null : handle.mountObj().getMountpoint();
}
public StringBinding displayablePathProperty() {
@@ -375,35 +314,26 @@ public class Vault {
return vaultSettings.path().getValue();
}
- public boolean isHavingCustomMountFlags() {
- return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
- }
-
- public ObservableValue defaultMountFlagsProperty() {
- return defaultMountFlags;
- }
-
- public String getDefaultMountFlags() {
- return defaultMountFlags.getValue();
- }
-
- public String getEffectiveMountFlags() {
- String mountFlags = vaultSettings.mountFlags().get();
- if (Strings.isNullOrEmpty(mountFlags)) {
- return ""; //TODO: should the provider provide dem defaults??
- } else {
- return mountFlags;
+ /**
+ * Gets from the cleartext path its ciphertext counterpart.
+ * The cleartext path has to start from the vault root (by starting with "/").
+ *
+ * @return Local os path to the ciphertext resource
+ * @throws IOException if an I/O error occurs
+ */
+ public Path getCiphertextPath(String cleartextPath) throws IOException {
+ if (!cleartextPath.startsWith("/")) {
+ throw new IllegalArgumentException("Input path must be absolute from vault root by starting with \"/\".");
}
+ var fs = cryptoFileSystem.get();
+ var cryptoPath = fs.getPath(cleartextPath);
+ return fs.getCiphertextPath(cryptoPath);
}
public VaultConfigCache getVaultConfigCache() {
return configCache;
}
- public void setCustomMountFlags(String mountFlags) {
- vaultSettings.mountFlags().set(mountFlags);
- }
-
public String getId() {
return vaultSettings.getId();
}
@@ -426,15 +356,12 @@ public class Vault {
}
}
-
- /* TODO: reactivate/ needed at all?
public boolean supportsForcedUnmount() {
- return volume.supportsForcedUnmount();
+ var mh = mountHandle.get();
+ if (mh == null) {
+ throw new IllegalStateException("Vault is not mounted");
+ }
+ return mountHandle.get().supportsUnmountForced();
}
- */
-
- private record MountHandle(Mount mount, boolean supportsUnmountForced) {
-
- }
}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java
index db35b5a11..0148686f3 100644
--- a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java
+++ b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java
@@ -176,7 +176,7 @@ public class CreateNewVaultPasswordController implements FxController {
// 2. initialize vault:
try {
MasterkeyLoader loader = ignored -> masterkey.copy();
- CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).withKeyLoader(loader).build();
+ CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(CryptorProvider.Scheme.SIV_GCM).withKeyLoader(loader).build();
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID);
// 3. write vault-internal readme file:
diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
index 73e805273..eac58eb86 100644
--- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java
+++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
@@ -13,6 +13,7 @@ public enum FxmlFile {
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
HEALTH_START("/fxml/health_start.fxml"), //
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
+ HUB_NO_KEYCHAIN("/fxml/hub_no_keychain.fxml"), //
HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), //
HUB_LICENSE_EXCEEDED("/fxml/hub_license_exceeded.fxml"), //
HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), //
diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
index ea6ba00d3..997bfa41f 100644
--- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
+++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
@@ -25,6 +25,7 @@ public enum FontAwesome5Icon {
EYE_SLASH("\uF070"), //
FAST_FORWARD("\uF050"), //
FILE("\uF15B"), //
+ FILE_DOWNLOAD("\uF56D"), //
FILE_IMPORT("\uF56F"), //
FOLDER_OPEN("\uF07C"), //
FUNNEL("\uF0B0"), //
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java
index a23a5f1b3..5adcb243d 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java
@@ -87,6 +87,13 @@ public abstract class HubKeyLoadingModule {
@StringKey(HubKeyLoadingStrategy.SCHEME_HUB_HTTPS)
abstract KeyLoadingStrategy bindHubKeyLoadingStrategyToHubHttps(HubKeyLoadingStrategy strategy);
+ @Provides
+ @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN)
+ @KeyLoadingScoped
+ static Scene provideHubNoKeychainScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
+ return fxmlLoaders.createScene(FxmlFile.HUB_NO_KEYCHAIN);
+ }
+
@Provides
@FxmlScene(FxmlFile.HUB_AUTH_FLOW)
@KeyLoadingScoped
@@ -136,6 +143,11 @@ public abstract class HubKeyLoadingModule {
return fxmlLoaders.createScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE);
}
+ @Binds
+ @IntoMap
+ @FxControllerKey(NoKeychainController.class)
+ abstract FxController bindNoKeychainController(NoKeychainController controller);
+
@Binds
@IntoMap
@FxControllerKey(AuthFlowController.class)
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
index dcb5722d2..d1a3742ae 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
@@ -3,6 +3,8 @@ package org.cryptomator.ui.keyloading.hub;
import com.google.common.base.Preconditions;
import com.nimbusds.jose.JWEObject;
import dagger.Lazy;
+import org.cryptomator.common.keychain.KeychainManager;
+import org.cryptomator.common.keychain.NoKeychainAccessProviderException;
import org.cryptomator.common.settings.DeviceKey;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
@@ -31,15 +33,19 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https";
private final Stage window;
+ private final KeychainManager keychainManager;
private final Lazy authFlowScene;
+ private final Lazy noKeychainScene;
private final CompletableFuture result;
private final DeviceKey deviceKey;
@Inject
- public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, CompletableFuture result, DeviceKey deviceKey, @Named("windowTitle") String windowTitle) {
+ public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy noKeychainScene, CompletableFuture result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle) {
this.window = window;
+ this.keychainManager = keychainManager;
window.setTitle(windowTitle);
this.authFlowScene = authFlowScene;
+ this.noKeychainScene = noKeychainScene;
this.result = result;
this.deviceKey = deviceKey;
}
@@ -48,9 +54,16 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX));
try {
- startAuthFlow();
+ if (!keychainManager.isSupported()) {
+ throw new NoKeychainAccessProviderException();
+ }
+ var keypair = deviceKey.get();
+ showWindow(authFlowScene);
var jwe = result.get();
- return JWEHelper.decrypt(jwe, deviceKey.get().getPrivate());
+ return JWEHelper.decrypt(jwe, keypair.getPrivate());
+ } catch (NoKeychainAccessProviderException e) {
+ showWindow(noKeychainScene);
+ throw new UnlockCancelledException("Unlock canceled due to missing prerequisites", e);
} catch (DeviceKey.DeviceKeyRetrievalException e) {
throw new MasterkeyLoadingFailedException("Failed to load keypair", e);
} catch (CancellationException e) {
@@ -63,9 +76,9 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
}
}
- private void startAuthFlow() {
+ private void showWindow(Lazy scene) {
Platform.runLater(() -> {
- window.setScene(authFlowScene.get());
+ window.setScene(scene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/NoKeychainController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/NoKeychainController.java
new file mode 100644
index 000000000..118152e1f
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/NoKeychainController.java
@@ -0,0 +1,31 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.fxapp.FxApplicationWindows;
+import org.cryptomator.ui.keyloading.KeyLoading;
+import org.cryptomator.ui.preferences.SelectedPreferencesTab;
+
+import javax.inject.Inject;
+import javafx.stage.Stage;
+
+public class NoKeychainController implements FxController {
+
+ private final Stage window;
+ private final FxApplicationWindows appWindows;
+
+ @Inject
+ public NoKeychainController(@KeyLoading Stage window, FxApplicationWindows appWindows) {
+ this.window = window;
+ this.appWindows = appWindows;
+ }
+
+
+ public void cancel() {
+ window.close();
+ }
+
+ public void openPreferences() {
+ appWindows.showPreferencesWindow(SelectedPreferencesTab.GENERAL);
+ window.close();
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/lock/LockForcedController.java b/src/main/java/org/cryptomator/ui/lock/LockForcedController.java
index 9b653816a..15cf119be 100644
--- a/src/main/java/org/cryptomator/ui/lock/LockForcedController.java
+++ b/src/main/java/org/cryptomator/ui/lock/LockForcedController.java
@@ -53,7 +53,7 @@ public class LockForcedController implements FxController {
}
public boolean isForceSupported() {
- return false;//vault.supportsForcedUnmount(); TODO
+ return vault.supportsForcedUnmount();
}
}
diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
index c81aff125..b2c912834 100644
--- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
+++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
@@ -3,33 +3,17 @@ package org.cryptomator.ui.mainwindow;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
-import org.cryptomator.cryptofs.CryptoFileSystemProvider;
-import org.cryptomator.cryptofs.DirStructure;
import org.cryptomator.ui.common.FxController;
-import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.beans.Observable;
-import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
-import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
-import javafx.scene.input.DragEvent;
-import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_EXT;
-import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
-import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
@MainWindowScoped
public class MainWindowController implements FxController {
@@ -37,28 +21,19 @@ public class MainWindowController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(MainWindowController.class);
private final Stage window;
- private final VaultListManager vaultListManager;
private final ReadOnlyObjectProperty selectedVault;
- private final WrongFileAlertComponent.Builder wrongFileAlert;
- private final BooleanProperty draggingOver = new SimpleBooleanProperty();
- private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
+
public StackPane root;
@Inject
- public MainWindowController(@MainWindow Stage window, VaultListManager vaultListManager, ObjectProperty selectedVault, WrongFileAlertComponent.Builder wrongFileAlert) {
+ public MainWindowController(@MainWindow Stage window, ObjectProperty selectedVault) {
this.window = window;
- this.vaultListManager = vaultListManager;
this.selectedVault = selectedVault;
- this.wrongFileAlert = wrongFileAlert;
}
@FXML
public void initialize() {
LOG.trace("init MainWindowController");
- root.setOnDragEntered(this::handleDragEvent);
- root.setOnDragOver(this::handleDragEvent);
- root.setOnDragDropped(this::handleDragEvent);
- root.setOnDragExited(this::handleDragEvent);
if (SystemUtils.IS_OS_WINDOWS) {
root.getStyleClass().add("os-windows");
}
@@ -72,65 +47,4 @@ public class MainWindowController implements FxController {
}
}
- private void handleDragEvent(DragEvent event) {
- if (DragEvent.DRAG_ENTERED.equals(event.getEventType()) && event.getGestureSource() == null) {
- draggingOver.set(true);
- } else if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
- event.acceptTransferModes(TransferMode.ANY);
- draggingVaultOver.set(event.getDragboard().getFiles().stream().map(File::toPath).anyMatch(this::containsVault));
- } else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
- Set vaultPaths = event.getDragboard().getFiles().stream().map(File::toPath).filter(this::containsVault).collect(Collectors.toSet());
- if (vaultPaths.isEmpty()) {
- wrongFileAlert.build().showWrongFileAlertWindow();
- } else {
- vaultPaths.forEach(this::addVault);
- }
- event.setDropCompleted(!vaultPaths.isEmpty());
- event.consume();
- } else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) {
- draggingOver.set(false);
- draggingVaultOver.set(false);
- }
- }
-
- private boolean containsVault(Path path) {
- try {
- if (path.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) {
- path = path.getParent();
- }
- return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED;
- } catch (IOException e) {
- return false;
- }
- }
-
- private void addVault(Path pathToVault) {
- try {
- if (pathToVault.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) {
- vaultListManager.add(pathToVault.getParent());
- } else {
- vaultListManager.add(pathToVault);
- }
- } catch (IOException e) {
- LOG.debug("Not a vault: {}", pathToVault);
- }
- }
-
- /* Getter/Setter */
-
- public BooleanProperty draggingOverProperty() {
- return draggingOver;
- }
-
- public boolean isDraggingOver() {
- return draggingOver.get();
- }
-
- public BooleanProperty draggingVaultOverProperty() {
- return draggingVaultOver;
- }
-
- public boolean isDraggingVaultOver() {
- return draggingVaultOver.get();
- }
}
diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java
index c86ea1929..1e5185c06 100644
--- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java
+++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java
@@ -1,54 +1,118 @@
package org.cryptomator.ui.mainwindow;
+import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
+import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.integrations.mount.Mountpoint;
+import org.cryptomator.integrations.revealpath.RevealFailedException;
+import org.cryptomator.integrations.revealpath.RevealPathService;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.stats.VaultStatisticsComponent;
+import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javax.inject.Inject;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.binding.StringBinding;
+import javafx.application.Platform;
+import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.ClipboardContent;
+import javafx.scene.input.DataFormat;
+import javafx.scene.input.DragEvent;
+import javafx.scene.input.TransferMode;
+import javafx.stage.FileChooser;
import javafx.stage.Stage;
-import java.net.URI;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
import java.util.Optional;
+import java.util.ResourceBundle;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
@MainWindowScoped
public class VaultDetailUnlockedController implements FxController {
+ private static final Logger LOG = LoggerFactory.getLogger(VaultDetailUnlockedController.class);
+ private static final String ACTIVE_CLASS = "active";
+
private final ReadOnlyObjectProperty vault;
private final FxApplicationWindows appWindows;
private final VaultService vaultService;
+ private final WrongFileAlertComponent.Builder wrongFileAlert;
private final Stage mainWindow;
+ private final ResourceBundle resourceBundle;
private final LoadingCache vaultStats;
private final VaultStatisticsComponent.Builder vaultStatsBuilder;
- private final ObservableValue mountPoint;
private final ObservableValue accessibleViaPath;
private final ObservableValue accessibleViaUri;
- private final ObservableValue mountUri;
+ private final ObservableValue mountPoint;
+ private final BooleanProperty draggingOver = new SimpleBooleanProperty();
+ private final BooleanProperty ciphertextPathsCopied = new SimpleBooleanProperty();
+
+ //FXML
+ public Button dropZone;
@Inject
- public VaultDetailUnlockedController(ObjectProperty vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) {
+ public VaultDetailUnlockedController(ObjectProperty vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, WrongFileAlertComponent.Builder wrongFileAlert, @MainWindow Stage mainWindow, ResourceBundle resourceBundle) {
this.vault = vault;
this.appWindows = appWindows;
this.vaultService = vaultService;
+ this.wrongFileAlert = wrongFileAlert;
this.mainWindow = mainWindow;
+ this.resourceBundle = resourceBundle;
this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats));
this.vaultStatsBuilder = vaultStatsBuilder;
- this.mountPoint = vault.flatMap(Vault::mountPointProperty);
- this.accessibleViaPath = mountPoint.map(m -> m instanceof Mountpoint.WithPath).orElse(false);
- this.accessibleViaUri = mountPoint.map(m -> m instanceof Mountpoint.WithUri).orElse(false);
- this.mountUri = mountPoint.map(Mountpoint::uri).map(URI::toASCIIString).orElse("");
+ var mp = vault.flatMap(Vault::mountPointProperty);
+ this.accessibleViaPath = mp.map(m -> m instanceof Mountpoint.WithPath).orElse(false);
+ this.accessibleViaUri = mp.map(m -> m instanceof Mountpoint.WithUri).orElse(false);
+ this.mountPoint = mp.map(m -> {
+ if (m instanceof Mountpoint.WithPath mwp) {
+ return mwp.path().toString();
+ } else {
+ return m.uri().toASCIIString();
+ }
+ });
+ }
+
+ public void initialize() {
+ dropZone.setOnDragEntered(this::handleDragEvent);
+ dropZone.setOnDragOver(this::handleDragEvent);
+ dropZone.setOnDragDropped(this::handleDragEvent);
+ dropZone.setOnDragExited(this::handleDragEvent);
+
+ EasyBind.includeWhen(dropZone.getStyleClass(), ACTIVE_CLASS, draggingOver);
+ }
+
+ private void handleDragEvent(DragEvent event) {
+ if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
+ event.acceptTransferModes(TransferMode.LINK);
+ draggingOver.set(true);
+ } else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
+ List ciphertextPaths = event.getDragboard().getFiles().stream().map(File::toPath).map(this::getCiphertextPath).flatMap(Optional::stream).toList();
+ if (ciphertextPaths.isEmpty()) {
+ wrongFileAlert.build().showWrongFileAlertWindow();
+ } else {
+ revealOrCopyPaths(ciphertextPaths);
+ }
+ event.setDropCompleted(!ciphertextPaths.isEmpty());
+ event.consume();
+ } else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) {
+ draggingOver.set(false);
+ }
}
private VaultStatisticsComponent buildVaultStats(Vault vault) {
@@ -62,7 +126,9 @@ public class VaultDetailUnlockedController implements FxController {
@FXML
public void copyMountUri() {
- // TODO
+ ClipboardContent clipboardContent = new ClipboardContent();
+ clipboardContent.putString(mountPoint.getValue());
+ Clipboard.getSystemClipboard().setContent(clipboardContent);
}
@FXML
@@ -75,6 +141,77 @@ public class VaultDetailUnlockedController implements FxController {
vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow();
}
+ @FXML
+ public void chooseFileAndReveal() {
+ Preconditions.checkState(accessibleViaPath.getValue());
+ var fileChooser = new FileChooser();
+ fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.filePickerTitle"));
+ fileChooser.setInitialDirectory(Path.of(mountPoint.getValue()).toFile());
+ var cleartextFile = fileChooser.showOpenDialog(mainWindow);
+ if (cleartextFile != null) {
+ var ciphertextPaths = getCiphertextPath(cleartextFile.toPath()).stream().toList();
+ revealOrCopyPaths(ciphertextPaths);
+ }
+ }
+
+ private boolean startsWithVaultAccessPoint(Path path) {
+ return path.startsWith(Path.of(mountPoint.getValue()));
+ }
+
+ private Optional getCiphertextPath(Path path) {
+ if (!startsWithVaultAccessPoint(path)) {
+ LOG.debug("Path does not start with access point of selected vault: {}", path);
+ return Optional.empty();
+ }
+ try {
+ var accessPoint = mountPoint.getValue();
+ var cleartextPath = path.toString().substring(accessPoint.length());
+ if (!cleartextPath.startsWith("/")) {
+ cleartextPath = "/" + cleartextPath;
+ }
+ return Optional.of(vault.get().getCiphertextPath(cleartextPath));
+ } catch (IOException e) {
+ LOG.warn("Unable to get ciphertext path from path: {}", path);
+ return Optional.empty();
+ }
+ }
+
+ private void revealOrCopyPaths(List paths) {
+ if (!revealPaths(paths)) {
+ LOG.warn("No service provider to reveal files found.");
+ copyPathsToClipboard(paths);
+ }
+ }
+
+ /**
+ * Reveals the paths over the {@link RevealPathService} in the file system
+ *
+ * @param paths List of Paths to reveal
+ * @return true, if at least one service provider was present, false otherwise
+ */
+ private boolean revealPaths(List paths) {
+ return RevealPathService.get().findAny().map(s -> {
+ paths.forEach(path -> {
+ try {
+ s.reveal(path);
+ } catch (RevealFailedException e) {
+ LOG.error("Revealing ciphertext file failed.", e);
+ }
+ });
+ return true;
+ }).orElse(false);
+ }
+
+ private void copyPathsToClipboard(List paths) {
+ StringBuilder clipboardString = new StringBuilder();
+ paths.forEach(p -> clipboardString.append(p.toString()).append("\n"));
+ Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, clipboardString.toString()));
+ ciphertextPathsCopied.setValue(true);
+ CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS, Platform::runLater).execute(() -> {
+ ciphertextPathsCopied.set(false);
+ });
+ }
+
/* Getter/Setter */
public ReadOnlyObjectProperty vaultProperty() {
@@ -101,13 +238,19 @@ public class VaultDetailUnlockedController implements FxController {
return accessibleViaUri.getValue();
}
- public ObservableValue mountUriProperty() {
- return mountUri;
+ public ObservableValue mountPointProperty() {
+ return mountPoint;
}
- public String getMountUri() {
- return mountUri.getValue();
+ public String getMountPoint() {
+ return mountPoint.getValue();
}
+ public BooleanProperty ciphertextPathsCopiedProperty() {
+ return ciphertextPathsCopied;
+ }
+ public boolean isCiphertextPathsCopied() {
+ return ciphertextPathsCopied.get();
+ }
}
diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
index adb9a961b..f0aadfdfc 100644
--- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
+++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
@@ -3,26 +3,43 @@ package org.cryptomator.ui.mainwindow;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
+import org.cryptomator.cryptofs.CryptoFileSystemProvider;
+import org.cryptomator.cryptofs.DirStructure;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javax.inject.Inject;
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.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.scene.input.ContextMenuEvent;
+import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
+import javafx.scene.input.TransferMode;
+import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
import java.util.EnumSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_EXT;
+import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
+import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
import static org.cryptomator.common.vaults.VaultState.Value.LOCKED;
import static org.cryptomator.common.vaults.VaultState.Value.MISSING;
@@ -31,6 +48,7 @@ import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION;
@MainWindowScoped
public class VaultListController implements FxController {
+ private static final Logger LOG = LoggerFactory.getLogger(VaultListController.class);
private final Stage mainWindow;
private final ObservableList vaults;
@@ -39,17 +57,21 @@ public class VaultListController implements FxController {
private final AddVaultWizardComponent.Builder addVaultWizard;
private final BooleanBinding emptyVaultList;
private final RemoveVaultComponent.Builder removeVaultDialogue;
+ private final VaultListManager vaultListManager;
+ private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
public ListView vaultList;
+ public StackPane root;
@Inject
- VaultListController(@MainWindow Stage mainWindow, ObservableList vaults, ObjectProperty selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue) {
+ VaultListController(@MainWindow Stage mainWindow, ObservableList vaults, ObjectProperty selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue, VaultListManager vaultListManager) {
this.mainWindow = mainWindow;
this.vaults = vaults;
this.selectedVault = selectedVault;
this.cellFactory = cellFactory;
this.addVaultWizard = addVaultWizard;
this.removeVaultDialogue = removeVaultDialogue;
+ this.vaultListManager = vaultListManager;
this.emptyVaultList = Bindings.isEmpty(vaults);
@@ -100,6 +122,11 @@ public class VaultListController implements FxController {
keyEvent.consume();
}
});
+
+ root.setOnDragEntered(this::handleDragEvent);
+ root.setOnDragOver(this::handleDragEvent);
+ root.setOnDragDropped(this::handleDragEvent);
+ root.setOnDragExited(this::handleDragEvent);
}
private void deselect(MouseEvent released) {
@@ -128,6 +155,47 @@ public class VaultListController implements FxController {
}
}
+ private void handleDragEvent(DragEvent event) {
+ if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
+ draggingVaultOver.set(event.getDragboard().getFiles().stream().map(File::toPath).anyMatch(this::containsVault));
+ if (draggingVaultOver.get()) {
+ event.acceptTransferModes(TransferMode.ANY);
+ }
+ } else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
+ Set vaultPaths = event.getDragboard().getFiles().stream().map(File::toPath).filter(this::containsVault).collect(Collectors.toSet());
+ if (!vaultPaths.isEmpty()) {
+ vaultPaths.forEach(this::addVault);
+ }
+ event.setDropCompleted(!vaultPaths.isEmpty());
+ event.consume();
+ } else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) {
+ draggingVaultOver.set(false);
+ }
+ }
+
+ private boolean containsVault(Path path) {
+ try {
+ if (path.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) {
+ path = path.getParent();
+ }
+ return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ private void addVault(Path pathToVault) {
+ try {
+ if (pathToVault.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) {
+ vaultListManager.add(pathToVault.getParent());
+ } else {
+ vaultListManager.add(pathToVault);
+ }
+ } catch (IOException e) {
+ LOG.debug("Not a vault: {}", pathToVault);
+ }
+ }
+
// Getter and Setter
public BooleanBinding emptyVaultListProperty() {
@@ -138,4 +206,13 @@ public class VaultListController implements FxController {
return emptyVaultList.get();
}
+ public BooleanProperty draggingVaultOverProperty() {
+ return draggingVaultOver;
+ }
+
+ public boolean isDraggingVaultOver() {
+ return draggingVaultOver.get();
+ }
+
+
}
diff --git a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java
index ac4903d11..00e680d2d 100644
--- a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java
+++ b/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java
@@ -1,5 +1,6 @@
package org.cryptomator.ui.preferences;
+import org.cryptomator.common.ObservableUtil;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.integrations.mount.MountCapability;
import org.cryptomator.integrations.mount.MountService;
@@ -14,39 +15,54 @@ import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TextField;
import javafx.util.StringConverter;
import java.util.List;
+import java.util.Optional;
+import java.util.ResourceBundle;
-/**
- * TODO: if WebDAV is selected under Windows, show warning that specific mount options (like selecting a directory as mount point) are _not_ supported
- */
@PreferencesScoped
public class VolumePreferencesController implements FxController {
private final Settings settings;
private final ObservableValue selectedMountService;
+ private final ResourceBundle resourceBundle;
private final BooleanExpression loopbackPortSupported;
+ private final ObservableValue mountToDirSupported;
+ private final ObservableValue mountToDriveLetterSupported;
+ private final ObservableValue mountFlagsSupported;
+ private final ObservableValue readonlySupported;
private final List mountProviders;
public ChoiceBox volumeTypeChoiceBox;
public TextField loopbackPortField;
public Button loopbackPortApplyButton;
@Inject
- VolumePreferencesController(Settings settings, List mountProviders, ObservableValue selectedMountService) {
+ VolumePreferencesController(Settings settings, List mountProviders, ResourceBundle resourceBundle) {
this.settings = settings;
this.mountProviders = mountProviders;
- this.selectedMountService = selectedMountService;
+ this.resourceBundle = resourceBundle;
+
+ var fallbackProvider = mountProviders.stream().findFirst().orElse(null);
+ this.selectedMountService = ObservableUtil.mapWithDefault(settings.mountService(), serviceName -> mountProviders.stream().filter(s -> s.getClass().getName().equals(serviceName)).findFirst().orElse(fallbackProvider), fallbackProvider);
this.loopbackPortSupported = BooleanExpression.booleanExpression(selectedMountService.map(s -> s.hasCapability(MountCapability.LOOPBACK_PORT)));
+ this.mountToDirSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT) || s.hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR));
+ this.mountToDriveLetterSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER));
+ this.mountFlagsSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_FLAGS));
+ this.readonlySupported = selectedMountService.map(s -> s.hasCapability(MountCapability.READ_ONLY));
}
public void initialize() {
+ volumeTypeChoiceBox.getItems().add(null);
volumeTypeChoiceBox.getItems().addAll(mountProviders);
volumeTypeChoiceBox.setConverter(new MountServiceConverter());
- volumeTypeChoiceBox.getSelectionModel().select(selectedMountService.getValue());
- volumeTypeChoiceBox.valueProperty().addListener((observableValue, oldProvide, newProvider) -> settings.mountService().set(newProvider.getClass().getName()));
+ boolean autoSelected = settings.mountService().get() == null;
+ volumeTypeChoiceBox.getSelectionModel().select(autoSelected ? null : selectedMountService.getValue());
+ volumeTypeChoiceBox.valueProperty().addListener((observableValue, oldProvider, newProvider) -> {
+ var toSet = Optional.ofNullable(newProvider).map(nP -> nP.getClass().getName()).orElse(null);
+ settings.mountService().set(toSet);
+ });
loopbackPortField.setText(String.valueOf(settings.port().get()));
loopbackPortApplyButton.visibleProperty().bind(settings.port().asString().isNotEqualTo(loopbackPortField.textProperty()));
loopbackPortApplyButton.disableProperty().bind(Bindings.createBooleanBinding(this::validateLoopbackPort, loopbackPortField.textProperty()).not());
-
}
private boolean validateLoopbackPort() {
@@ -75,13 +91,49 @@ public class VolumePreferencesController implements FxController {
return loopbackPortSupported.get();
}
+ public ObservableValue readonlySupportedProperty() {
+ return readonlySupported;
+ }
+
+ public boolean isReadonlySupported() {
+ return readonlySupported.getValue();
+ }
+
+ public ObservableValue mountToDirSupportedProperty() {
+ return mountToDirSupported;
+ }
+
+ public boolean isMountToDirSupported() {
+ return mountToDirSupported.getValue();
+ }
+
+ public ObservableValue mountToDriveLetterSupportedProperty() {
+ return mountToDriveLetterSupported;
+ }
+
+ public boolean isMountToDriveLetterSupported() {
+ return mountToDriveLetterSupported.getValue();
+ }
+
+ public ObservableValue mountFlagsSupportedProperty() {
+ return mountFlagsSupported;
+ }
+
+ public boolean isMountFlagsSupported() {
+ return mountFlagsSupported.getValue();
+ }
+
/* Helpers */
- private static class MountServiceConverter extends StringConverter {
+ private class MountServiceConverter extends StringConverter {
@Override
public String toString(MountService provider) {
- return provider== null? "None" : provider.displayName(); //TODO: adjust message
+ if (provider == null) {
+ return resourceBundle.getString("preferences.volume.type.automatic");
+ } else {
+ return provider.displayName();
+ }
}
@Override
@@ -89,6 +141,4 @@ public class VolumePreferencesController implements FxController {
throw new UnsupportedOperationException();
}
}
-
-
}
diff --git a/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java b/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java
index 36553e56c..704ffa9ab 100644
--- a/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java
+++ b/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java
@@ -39,8 +39,8 @@ public class AwtTrayMenuController implements TrayMenuController {
}
@Override
- public void showTrayIcon(byte[] rawImageData, Runnable defaultAction, String tooltip) throws TrayMenuException {
- var image = Toolkit.getDefaultToolkit().createImage(rawImageData);
+ public void showTrayIcon(byte[] imageData, Runnable defaultAction, String tooltip) throws TrayMenuException {
+ var image = Toolkit.getDefaultToolkit().createImage(imageData);
trayIcon = new TrayIcon(image, tooltip, menu);
trayIcon.setImageAutoSize(true);
@@ -56,6 +56,15 @@ public class AwtTrayMenuController implements TrayMenuController {
}
}
+ @Override
+ public void updateTrayIcon(byte[] imageData) {
+ if (trayIcon == null) {
+ throw new IllegalStateException("Failed to update the icon as it has not yet been added");
+ }
+ var image = Toolkit.getDefaultToolkit().createImage(imageData);
+ trayIcon.setImage(image);
+ }
+
@Override
public void updateTrayMenu(List items) {
menu.removeAll();
@@ -67,7 +76,7 @@ public class AwtTrayMenuController implements TrayMenuController {
Preconditions.checkNotNull(this.trayIcon);
this.trayIcon.addMouseListener(new MouseAdapter() {
@Override
- public void mouseClicked(MouseEvent e) {
+ public void mousePressed(MouseEvent e) {
listener.run();
}
});
diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java
index e96446143..fd0a0d987 100644
--- a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java
+++ b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java
@@ -33,7 +33,9 @@ public class TrayMenuBuilder {
private static final Logger LOG = LoggerFactory.getLogger(TrayMenuBuilder.class);
private static final String TRAY_ICON_MAC = "/img/tray_icon_mac@2x.png";
- private static final String TRAY_ICON = "/img/window_icon_32.png";
+ private static final String TRAY_ICON_UNLOCKED_MAC = "/img/tray_icon_unlocked_mac@2x.png";
+ private static final String TRAY_ICON = "/img/tray_icon.png";
+ private static final String TRAY_ICON_UNLOCKED = "/img/tray_icon_unlocked.png";
private final ResourceBundle resourceBundle;
private final VaultService vaultService;
@@ -62,8 +64,8 @@ public class TrayMenuBuilder {
v.displayNameProperty().addListener(this::vaultListChanged);
});
- try (var image = getClass().getResourceAsStream(SystemUtils.IS_OS_MAC_OSX ? TRAY_ICON_MAC : TRAY_ICON)) {
- trayMenu.showTrayIcon(image.readAllBytes(), this::showMainWindow, "Cryptomator");
+ try {
+ trayMenu.showTrayIcon(getAppropriateTrayIconImage(), this::showMainWindow, "Cryptomator");
trayMenu.onBeforeOpenMenu(() -> {
for (Vault vault : vaults) {
VaultListManager.redetermineVaultState(vault);
@@ -71,8 +73,6 @@ public class TrayMenuBuilder {
});
rebuildMenu();
initialized = true;
- } catch (IOException e) {
- throw new UncheckedIOException("Failed to load embedded resource", e);
} catch (TrayMenuException e) {
LOG.error("Adding tray icon failed", e);
}
@@ -84,6 +84,7 @@ public class TrayMenuBuilder {
private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
assert Platform.isFxApplicationThread();
+ trayMenu.updateTrayIcon(getAppropriateTrayIconImage());
rebuildMenu();
}
@@ -154,4 +155,22 @@ public class TrayMenuBuilder {
appWindows.showPreferencesWindow(SelectedPreferencesTab.ANY);
}
+ private byte[] getAppropriateTrayIconImage() {
+ boolean isAnyVaultUnlocked = vaults.stream().anyMatch(Vault::isUnlocked);
+
+ String resourceName;
+ if (SystemUtils.IS_OS_MAC_OSX) {
+ resourceName = isAnyVaultUnlocked ? TRAY_ICON_UNLOCKED_MAC : TRAY_ICON_MAC;
+ } else {
+ resourceName = isAnyVaultUnlocked ? TRAY_ICON_UNLOCKED : TRAY_ICON;
+ }
+
+ try (var image = getClass().getResourceAsStream(resourceName)) {
+ assert image != null;
+ return image.readAllBytes();
+ } catch (IOException e) {
+ throw new UncheckedIOException("Failed to load tray icon image: " + resourceName, e);
+ }
+ }
+
}
diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java b/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java
index 928953c9e..aeb4a9166 100644
--- a/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java
@@ -1,11 +1,18 @@
package org.cryptomator.ui.unlock;
+import org.cryptomator.common.mount.MountPointNotExistsException;
+import org.cryptomator.common.mount.MountPointNotSupportedException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.controls.FormattedLabel;
+import org.cryptomator.ui.fxapp.FxApplicationWindows;
+import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
+import java.util.ResourceBundle;
+import java.util.concurrent.atomic.AtomicReference;
//At the current point in time only the CustomMountPointChooser may cause this window to be shown.
@UnlockScoped
@@ -13,11 +20,32 @@ public class UnlockInvalidMountPointController implements FxController {
private final Stage window;
private final Vault vault;
+ private final AtomicReference unlockException;
+ private final FxApplicationWindows appWindows;
+ private final ResourceBundle resourceBundle;
+
+ public FormattedLabel dialogDescription;
@Inject
- UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault) {
+ UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault, @UnlockWindow AtomicReference unlockException, FxApplicationWindows appWindows, ResourceBundle resourceBundle) {
this.window = window;
this.vault = vault;
+ this.unlockException = unlockException;
+ this.appWindows = appWindows;
+ this.resourceBundle = resourceBundle;
+ }
+
+ @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";
+ }
+ dialogDescription.setFormat(resourceBundle.getString(translationKey));
+ dialogDescription.setArg1(e.getMessage());
}
@FXML
@@ -25,4 +53,10 @@ public class UnlockInvalidMountPointController implements FxController {
window.close();
}
+ @FXML
+ public void closeAndOpenPreferences() {
+ appWindows.showPreferencesWindow(SelectedPreferencesTab.VOLUME);
+ window.close();
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
index b14286698..b59fa272d 100644
--- a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
@@ -23,6 +23,7 @@ import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Map;
import java.util.ResourceBundle;
+import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {KeyLoadingComponent.class})
abstract class UnlockModule {
@@ -57,6 +58,13 @@ abstract class UnlockModule {
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
}
+ @Provides
+ @UnlockWindow
+ @UnlockScoped
+ static AtomicReference unlockException() {
+ return new AtomicReference<>();
+ }
+
@Provides
@FxmlScene(FxmlFile.UNLOCK_SUCCESS)
@UnlockScoped
diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
index 9c47aa281..985f1b471 100644
--- a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
@@ -2,6 +2,7 @@ package org.cryptomator.ui.unlock;
import com.google.common.base.Throwables;
import dagger.Lazy;
+import org.cryptomator.common.mount.IllegalMountPointException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.cryptolib.api.CryptoException;
@@ -20,6 +21,7 @@ import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
/**
* A multi-step task that consists of background activities as well as user interaction.
@@ -38,9 +40,10 @@ public class UnlockWorkflow extends Task {
private final Lazy invalidMountPointScene;
private final FxApplicationWindows appWindows;
private final KeyLoadingStrategy keyLoadingStrategy;
+ private final AtomicReference unlockFailedException;
@Inject
- UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy) {
+ UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, @UnlockWindow AtomicReference unlockFailedException) {
this.window = window;
this.vault = vault;
this.vaultService = vaultService;
@@ -48,6 +51,7 @@ public class UnlockWorkflow extends Task {
this.invalidMountPointScene = invalidMountPointScene;
this.appWindows = appWindows;
this.keyLoadingStrategy = keyLoadingStrategy;
+ this.unlockFailedException = unlockFailedException;
}
@Override
@@ -67,13 +71,15 @@ public class UnlockWorkflow extends Task {
} catch (Exception e) {
Throwables.propagateIfPossible(e, IOException.class);
Throwables.propagateIfPossible(e, CryptoException.class);
+ Throwables.propagateIfPossible(e, IllegalMountPointException.class);
Throwables.propagateIfPossible(e, MountFailedException.class);
throw new IllegalStateException("unexpected exception type", e);
}
}
- private void showInvalidMountPointScene() {
+ private void handleIllegalMountPointError(IllegalMountPointException impe) {
Platform.runLater(() -> {
+ unlockFailedException.set(impe);
window.setScene(invalidMountPointScene.get());
window.show();
});
@@ -107,7 +113,11 @@ public class UnlockWorkflow extends Task {
protected void failed() {
LOG.info("Unlock of '{}' failed.", vault.getDisplayName());
Throwable throwable = super.getException();
- handleGenericError(throwable);
+ if(throwable instanceof IllegalMountPointException impe) {
+ handleIllegalMountPointError(impe);
+ } else {
+ handleGenericError(throwable);
+ }
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
}
diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
index 8c28b1a5d..502d4312a 100644
--- a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
+++ b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
@@ -1,11 +1,14 @@
package org.cryptomator.ui.vaultoptions;
-import org.cryptomator.common.Environment;
+import com.google.common.base.Strings;
+import org.cryptomator.common.mount.ActualMountService;
import org.cryptomator.common.mount.WindowsDriveLetters;
+import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.mount.MountCapability;
-import org.cryptomator.integrations.mount.MountService;
import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.fxapp.FxApplicationWindows;
+import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import javax.inject.Inject;
import javafx.beans.value.ObservableValue;
@@ -30,16 +33,17 @@ import java.util.Set;
public class MountOptionsController implements FxController {
private final Stage window;
- private final Vault vault;
+ private final VaultSettings vaultSettings;
private final WindowsDriveLetters windowsDriveLetters;
private final ResourceBundle resourceBundle;
+ private final ObservableValue defaultMountFlags;
private final ObservableValue mountpointDirSupported;
private final ObservableValue mountpointDriveLetterSupported;
private final ObservableValue readOnlySupported;
private final ObservableValue mountFlagsSupported;
- private final ObservableValue driveLetter;
private final ObservableValue directoryPath;
+ private final FxApplicationWindows applicationWindows;
//-- FXML objects --
@@ -54,59 +58,75 @@ public class MountOptionsController implements FxController {
public ChoiceBox driveLetterSelection;
@Inject
- MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObservableValue mountService, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, Environment environment) {
+ MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObservableValue mountService, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, FxApplicationWindows applicationWindows) {
this.window = window;
- this.vault = vault;
+ this.vaultSettings = vault.getVaultSettings();
this.windowsDriveLetters = windowsDriveLetters;
this.resourceBundle = resourceBundle;
- this.mountpointDirSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR) || s.hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT));
- this.mountpointDriveLetterSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER));
- this.mountFlagsSupported = mountService.map(s -> s.hasCapability(MountCapability.MOUNT_FLAGS));
- this.readOnlySupported = mountService.map(s -> s.hasCapability(MountCapability.READ_ONLY));
- this.driveLetter = vault.getVaultSettings().mountPoint().map(p -> isDriveLetter(p) ? p : null);
+ this.defaultMountFlags = mountService.map(as -> {
+ if (as.service().hasCapability(MountCapability.MOUNT_FLAGS)) {
+ return as.service().getDefaultMountFlags();
+ } else {
+ return "";
+ }
+ });
+ this.mountpointDirSupported = mountService.map(as -> as.service().hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR) || as.service().hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT));
+ this.mountpointDriveLetterSupported = mountService.map(as -> as.service().hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER));
+ this.mountFlagsSupported = mountService.map(as -> as.service().hasCapability(MountCapability.MOUNT_FLAGS));
+ this.readOnlySupported = mountService.map(as -> as.service().hasCapability(MountCapability.READ_ONLY));
this.directoryPath = vault.getVaultSettings().mountPoint().map(p -> isDriveLetter(p) ? null : p.toString());
+ this.applicationWindows = applicationWindows;
}
@FXML
public void initialize() {
// readonly:
- readOnlyCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().usesReadOnlyMode());
+ readOnlyCheckbox.selectedProperty().bindBidirectional(vaultSettings.usesReadOnlyMode());
// custom mount flags:
mountFlagsField.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().not());
- customMountFlagsCheckbox.setSelected(vault.isHavingCustomMountFlags());
+ customMountFlagsCheckbox.setSelected(!Strings.isNullOrEmpty(vaultSettings.mountFlags().getValue()));
+ toggleUseCustomMountFlags();
//driveLetter choice box
driveLetterSelection.getItems().addAll(windowsDriveLetters.getAll());
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle));
- driveLetterSelection.setOnShowing(event -> driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle))); //TODO: does this work?
//mountPoint toggle group
- var mountPoint = vault.getVaultSettings().getMountPoint();
+ var mountPoint = vaultSettings.getMountPoint();
if (mountPoint == null) {
//prepare and select auto
mountPointToggleGroup.selectToggle(mountPointAutoBtn);
} else if (mountPoint.getParent() == null && isDriveLetter(mountPoint)) {
//prepare and select drive letter
mountPointToggleGroup.selectToggle(mountPointDriveLetterBtn);
- } else if (driveLetterSelection.getValue() == null) {
+ driveLetterSelection.valueProperty().bindBidirectional(vaultSettings.mountPoint());
+ } else {
//prepare and select dir
mountPointToggleGroup.selectToggle(mountPointDirBtn);
}
mountPointToggleGroup.selectedToggleProperty().addListener(this::selectedToggleChanged);
}
+ @FXML
+ public void openVolumePreferences() {
+ applicationWindows.showPreferencesWindow(SelectedPreferencesTab.VOLUME);
+ }
+
@FXML
public void toggleUseCustomMountFlags() {
if (customMountFlagsCheckbox.isSelected()) {
readOnlyCheckbox.setSelected(false); // to prevent invalid states
mountFlagsField.textProperty().unbind();
- vault.setCustomMountFlags(vault.defaultMountFlagsProperty().getValue());
- mountFlagsField.textProperty().bindBidirectional(vault.getVaultSettings().mountFlags());
+ var mountFlags = vaultSettings.mountFlags().get();
+ if (mountFlags == null || mountFlags.isBlank()) {
+ vaultSettings.mountFlags().set(defaultMountFlags.getValue());
+ }
+ mountFlagsField.textProperty().bindBidirectional(vaultSettings.mountFlags());
} else {
- mountFlagsField.textProperty().unbindBidirectional(vault.getVaultSettings().mountFlags());
- vault.setCustomMountFlags(null);
- mountFlagsField.textProperty().bind(vault.defaultMountFlagsProperty());
+ mountFlagsField.textProperty().unbindBidirectional(vaultSettings.mountFlags());
+ vaultSettings.mountFlags().set(null);
+ mountFlagsField.textProperty().bind(defaultMountFlags);
}
}
@@ -114,7 +134,7 @@ public class MountOptionsController implements FxController {
public void chooseCustomMountPoint() {
try {
Path chosenPath = chooseCustomMountPointInternal();
- vault.getVaultSettings().mountPoint().set(chosenPath);
+ vaultSettings.mountPoint().set(chosenPath);
} catch (NoDirSelectedException e) {
//no-op
}
@@ -131,7 +151,7 @@ public class MountOptionsController implements FxController {
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.mountPoint.directoryPickerTitle"));
try {
- var mp = vault.getVaultSettings().mountPoint().get();
+ var mp = vaultSettings.mountPoint().get();
var initialDir = mp != null && !isDriveLetter(mp) ? mp : Path.of(System.getProperty("user.home"));
if (Files.isDirectory(initialDir)) {
@@ -149,27 +169,30 @@ public class MountOptionsController implements FxController {
}
private void selectedToggleChanged(ObservableValue extends Toggle> observable, Toggle oldToggle, Toggle newToggle) {
- Path mountPointToBe = null;
- try {
- //Remark: the mountpoint corresponding to the newToggle must be null, otherwise it would not be new!
- if (mountPointDriveLetterBtn.equals(newToggle)) {
- mountPointToBe = driveLetterSelection.getItems().get(0);
- } else if (mountPointDirBtn.equals(newToggle)) {
- mountPointToBe = chooseCustomMountPointInternal();
- }
- vault.getVaultSettings().mountPoint().set(mountPointToBe);
- } catch (NoDirSelectedException e) {
- if (!mountPointDirBtn.equals(oldToggle)) {
- mountPointToggleGroup.selectToggle(oldToggle);
-
+ //Remark: the mountpoint corresponding to the newToggle must be null, otherwise it would not be new!
+ driveLetterSelection.valueProperty().unbindBidirectional(vaultSettings.mountPoint());
+ if (mountPointDriveLetterBtn.equals(newToggle)) {
+ vaultSettings.mountPoint().set(windowsDriveLetters.getFirstDesiredAvailable().orElse(windowsDriveLetters.getAll().stream().findAny().get()));
+ driveLetterSelection.valueProperty().bindBidirectional(vaultSettings.mountPoint());
+ } else if (mountPointDirBtn.equals(newToggle)) {
+ try {
+ vaultSettings.mountPoint().set(chooseCustomMountPointInternal());
+ } catch (NoDirSelectedException e) {
+ if (oldToggle != null && !mountPointDirBtn.equals(oldToggle)) {
+ mountPointToggleGroup.selectToggle(oldToggle);
+ } else {
+ mountPointToggleGroup.selectToggle(mountPointAutoBtn);
+ }
}
+ } else {
+ vaultSettings.mountPoint().set(null);
}
}
private boolean isDriveLetter(Path mountPoint) {
if (mountPoint != null) {
var s = mountPoint.toString();
- return s.length() == 3 && mountPoint.toString().endsWith(":\\");
+ return s.length() == 3 && s.endsWith(":\\");
}
return false;
}
@@ -237,21 +260,13 @@ public class MountOptionsController implements FxController {
}
public ObservableValue readOnlySupportedProperty() {
- return mountpointDriveLetterSupported;
+ return readOnlySupported;
}
public boolean isReadOnlySupported() {
return readOnlySupported.getValue();
}
- public ObservableValue driveLetterProperty() {
- return driveLetter;
- }
-
- public Path getDriveLetter() {
- return driveLetter.getValue();
- }
-
public ObservableValue directoryPathProperty() {
return directoryPath;
}
@@ -259,5 +274,4 @@ public class MountOptionsController implements FxController {
public String getDirectoryPath() {
return directoryPath.getValue();
}
-
}
diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css
index 86467bb1a..5f0877842 100644
--- a/src/main/resources/css/dark_theme.css
+++ b/src/main/resources/css/dark_theme.css
@@ -204,16 +204,6 @@
-fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.8), 2, 0, 0, 0);
}
-.main-window .drag-n-drop-indicator {
- -fx-border-color: SECONDARY;
- -fx-border-width: 3px;
-}
-
-.main-window .drag-n-drop-indicator .drag-n-drop-header {
- -fx-background-color: SECONDARY;
- -fx-padding: 3px;
-}
-
/*******************************************************************************
* *
* TabPane *
@@ -884,3 +874,51 @@
-fx-fill: linear-gradient(to bottom, PRIMARY, transparent);
-fx-stroke: transparent;
}
+
+/*******************************************************************************
+ * *
+ * Drag and Drop *
+ * *
+ ******************************************************************************/
+
+.drag-n-drop-border {
+ -fx-border-color: SECONDARY;
+ -fx-border-width: 3px;
+}
+
+.button.drag-n-drop {
+ -fx-background-color: CONTROL_BG_NORMAL;
+ -fx-background-insets: 0;
+ -fx-padding: 1.4em 1em 1.4em 1em;
+ -fx-text-fill: TEXT_FILL_MUTED;
+ -fx-font-size: 0.8em;
+ -fx-border-color: CONTROL_BORDER_NORMAL;
+ -fx-border-radius: 4px;
+ -fx-border-style: dashed inside;
+ -fx-border-width: 1px;
+}
+
+.button.drag-n-drop:focused {
+ -fx-border-color: CONTROL_BORDER_FOCUSED;
+}
+
+.button.drag-n-drop:armed {
+ -fx-background-color: CONTROL_BG_ARMED;
+}
+
+.button.drag-n-drop.active {
+ -fx-border-color: SECONDARY;
+ -fx-border-style: solid inside;
+ -fx-border-width: 1px;
+}
+
+/*******************************************************************************
+ * *
+ * Separator *
+ * *
+ ******************************************************************************/
+
+.separator {
+ -fx-padding: 0.5px;
+ -fx-background-color: CONTROL_BORDER_NORMAL;
+}
diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css
index ddc872eb2..decf64b64 100644
--- a/src/main/resources/css/light_theme.css
+++ b/src/main/resources/css/light_theme.css
@@ -203,16 +203,6 @@
-fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.8), 2, 0, 0, 0);
}
-.main-window .drag-n-drop-indicator {
- -fx-border-color: SECONDARY;
- -fx-border-width: 3px;
-}
-
-.main-window .drag-n-drop-indicator .drag-n-drop-header {
- -fx-background-color: SECONDARY;
- -fx-padding: 3px;
-}
-
/*******************************************************************************
* *
* TabPane *
@@ -883,3 +873,51 @@
-fx-fill: linear-gradient(to bottom, PRIMARY, transparent);
-fx-stroke: transparent;
}
+
+/*******************************************************************************
+ * *
+ * Drag and Drop *
+ * *
+ ******************************************************************************/
+
+.drag-n-drop-border {
+ -fx-border-color: SECONDARY;
+ -fx-border-width: 3px;
+}
+
+.button.drag-n-drop {
+ -fx-background-color: CONTROL_BG_NORMAL;
+ -fx-background-insets: 0;
+ -fx-padding: 1.4em 1em 1.4em 1em;
+ -fx-text-fill: TEXT_FILL_MUTED;
+ -fx-font-size: 0.8em;
+ -fx-border-color: CONTROL_BORDER_NORMAL;
+ -fx-border-radius: 4px;
+ -fx-border-style: dashed inside;
+ -fx-border-width: 1px;
+}
+
+.button.drag-n-drop:focused {
+ -fx-border-color: CONTROL_BORDER_FOCUSED;
+}
+
+.button.drag-n-drop:armed {
+ -fx-background-color: CONTROL_BG_ARMED;
+}
+
+.button.drag-n-drop.active {
+ -fx-border-color: SECONDARY;
+ -fx-border-style: solid inside;
+ -fx-border-width: 1px;
+}
+
+/*******************************************************************************
+ * *
+ * Separator *
+ * *
+ ******************************************************************************/
+
+.separator {
+ -fx-padding: 0.5px;
+ -fx-background-color: CONTROL_BORDER_NORMAL;
+}
diff --git a/src/main/resources/fxml/hub_no_keychain.fxml b/src/main/resources/fxml/hub_no_keychain.fxml
new file mode 100644
index 000000000..031eac956
--- /dev/null
+++ b/src/main/resources/fxml/hub_no_keychain.fxml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/fxml/main_window.fxml b/src/main/resources/fxml/main_window.fxml
index 91e7512a4..2796455d3 100644
--- a/src/main/resources/fxml/main_window.fxml
+++ b/src/main/resources/fxml/main_window.fxml
@@ -1,10 +1,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/src/main/resources/fxml/migration_start.fxml b/src/main/resources/fxml/migration_start.fxml
index 6c10bcd8b..547bd7e26 100644
--- a/src/main/resources/fxml/migration_start.fxml
+++ b/src/main/resources/fxml/migration_start.fxml
@@ -6,16 +6,20 @@
+
+
+
+
+
@@ -23,13 +27,47 @@
-
-
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/fxml/preferences_about.fxml b/src/main/resources/fxml/preferences_about.fxml
index b6940b0ed..d403319fd 100644
--- a/src/main/resources/fxml/preferences_about.fxml
+++ b/src/main/resources/fxml/preferences_about.fxml
@@ -22,7 +22,7 @@
-
+
diff --git a/src/main/resources/fxml/preferences_volume.fxml b/src/main/resources/fxml/preferences_volume.fxml
index ec2f9db7b..92df77e3d 100644
--- a/src/main/resources/fxml/preferences_volume.fxml
+++ b/src/main/resources/fxml/preferences_volume.fxml
@@ -1,10 +1,12 @@
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/fxml/unlock_invalid_mount_point.fxml b/src/main/resources/fxml/unlock_invalid_mount_point.fxml
index d6be54dac..1b52f568c 100644
--- a/src/main/resources/fxml/unlock_invalid_mount_point.fxml
+++ b/src/main/resources/fxml/unlock_invalid_mount_point.fxml
@@ -34,16 +34,19 @@
-
diff --git a/src/main/resources/fxml/vault_detail_unlocked.fxml b/src/main/resources/fxml/vault_detail_unlocked.fxml
index 22cdc258b..c035b2d88 100644
--- a/src/main/resources/fxml/vault_detail_unlocked.fxml
+++ b/src/main/resources/fxml/vault_detail_unlocked.fxml
@@ -2,10 +2,11 @@
-
+
+
+
@@ -27,8 +29,8 @@
-
-
+
+
@@ -41,7 +43,28 @@
-
+
+
+
+
+
+
+
+
+
+
+