diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index ea3c7c3e6..0a75c44ca 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -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 diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh index acd5e9e35..607423dcf 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.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,7 +40,7 @@ ${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 - 2023 Skymatic GmbH" \ @@ -46,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" \ @@ -54,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 @@ -83,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/pom.xml b/pom.xml index 14090a08d..1218ce6fb 100644 --- a/pom.xml +++ b/pom.xml @@ -29,11 +29,11 @@ 2.1.1 - 2.5.3 - 1.2.0-beta3 - 1.1.2 - 1.1.2 - 1.1.0 + 2.6.1 + 1.2.0-beta4 + 1.2.0-beta1 + 1.2.0-beta1 + 1.2.0-beta1 2.0.0-beta3 2.0.0-beta2 2.0.0-beta2 diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 7a6129edb..e4dbd9b2c 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -67,7 +67,6 @@ 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; @@ -89,7 +88,6 @@ 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); } @@ -316,6 +314,22 @@ public class Vault { return vaultSettings.path().getValue(); } + /** + * 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; } 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/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 5188f9d25..1e5185c06 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java @@ -1,56 +1,86 @@ 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 accessibleViaPath; private final ObservableValue accessibleViaUri; 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; 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) { + if (m instanceof Mountpoint.WithPath mwp) { return mwp.path().toString(); } else { return m.uri().toASCIIString(); @@ -58,6 +88,33 @@ public class VaultDetailUnlockedController implements FxController { }); } + 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) { return vaultStatsBuilder.vault(vault).build(); } @@ -69,7 +126,6 @@ public class VaultDetailUnlockedController implements FxController { @FXML public void copyMountUri() { - //TODO: add popup that conent is copied ClipboardContent clipboardContent = new ClipboardContent(); clipboardContent.putString(mountPoint.getValue()); Clipboard.getSystemClipboard().setContent(clipboardContent); @@ -85,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() { @@ -119,5 +246,11 @@ public class VaultDetailUnlockedController implements FxController { 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/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css index 86467bb1a..fe510d420 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,40 @@ -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; +} diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css index ddc872eb2..4e47af6df 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,40 @@ -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; +} 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/vault_detail_unlocked.fxml b/src/main/resources/fxml/vault_detail_unlocked.fxml index 02337162a..c035b2d88 100644 --- a/src/main/resources/fxml/vault_detail_unlocked.fxml +++ b/src/main/resources/fxml/vault_detail_unlocked.fxml @@ -5,6 +5,8 @@ + + - + + + + + + + + + + + - + + + + + + + + + + + + + + + + diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index 07319a63e..73b43470a 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -343,9 +343,6 @@ main.minimizeBtn.tooltip=Minimize main.preferencesBtn.tooltip=Preferences main.debugModeEnabled.tooltip=Debug mode is enabled main.supporterCertificateMissing.tooltip=Please consider donating -## Drag 'n' Drop -main.dropZone.dropVault=Add this vault -main.dropZone.unknownDragboardContent=If you want to add a vault, drag it to this window ## Vault List main.vaultlist.emptyList.onboardingInstruction=Click here to add a vault main.vaultlist.contextMenu.remove=Remove… @@ -376,6 +373,10 @@ main.vaultDetail.throughput.idle=idle main.vaultDetail.throughput.kbps=%.1f kiB/s main.vaultDetail.throughput.mbps=%.1f MiB/s main.vaultDetail.stats=Vault Statistics +main.vaultDetail.locateEncryptedFileBtn=Locate Encrypted File +main.vaultDetail.locateEncryptedFileBtn.tooltip=Choose a file from your vault to locate its encrypted counterpart +main.vaultDetail.encryptedPathsCopied=Paths Copied to Clipboard! +main.vaultDetail.filePickerTitle=Select File Inside Vault ### Missing main.vaultDetail.missing.info=Cryptomator could not find a vault at this path. main.vaultDetail.missing.recheck=Recheck