diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index f857d6ba1..8475e7184 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -412,6 +412,17 @@ public class Vault { } } + /** + * Gets the cleartext name from a given path to an encrypted vault file + */ + public String getCleartextName(Path ciphertextPath) throws IOException { + if (!state.getValue().equals(VaultState.Value.UNLOCKED)) { + throw new IllegalStateException("Vault is not unlocked"); + } + var fs = cryptoFileSystem.get(); + return fs.getCleartextName(ciphertextPath); + } + public VaultConfigCache getVaultConfigCache() { return configCache; } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java index ef6148c87..e743f2c94 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java @@ -6,6 +6,7 @@ import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.tobiasdiez.easybind.EasyBind; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.Nullable; import org.cryptomator.common.vaults.Vault; import org.cryptomator.integrations.mount.Mountpoint; import org.cryptomator.integrations.revealpath.RevealFailedException; @@ -39,10 +40,14 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.ResourceBundle; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; @MainWindowScoped public class VaultDetailUnlockedController implements FxController { @@ -62,11 +67,15 @@ public class VaultDetailUnlockedController implements FxController { private final ObservableValue accessibleViaPath; private final ObservableValue accessibleViaUri; private final ObservableValue mountPoint; - private final BooleanProperty draggingOver = new SimpleBooleanProperty(); + private final BooleanProperty draggingOverLocateEncrypted = new SimpleBooleanProperty(); + private final BooleanProperty draggingOverDecryptName = new SimpleBooleanProperty(); private final BooleanProperty ciphertextPathsCopied = new SimpleBooleanProperty(); + private final BooleanProperty cleartextNamesCopied = new SimpleBooleanProperty(); - //FXML - public Button dropZone; + @FXML + public Button revealEncryptedDropZone; + @FXML + public Button decryptNameDropZone; @Inject public VaultDetailUnlockedController(ObjectProperty vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, WrongFileAlertComponent.Builder wrongFileAlert, @MainWindow Stage mainWindow, Optional revealPathService, ResourceBundle resourceBundle) { @@ -92,72 +101,90 @@ public class VaultDetailUnlockedController implements FxController { } public void initialize() { - dropZone.setOnDragEntered(this::handleDragEvent); - dropZone.setOnDragOver(this::handleDragEvent); - dropZone.setOnDragDropped(this::handleDragEvent); - dropZone.setOnDragExited(this::handleDragEvent); + revealEncryptedDropZone.setOnDragOver(e -> handleDragOver(e, draggingOverLocateEncrypted)); + revealEncryptedDropZone.setOnDragDropped(e -> handleDragDropped(e, this::getCiphertextPath, this::revealOrCopyPaths)); + revealEncryptedDropZone.setOnDragExited(_ -> draggingOverLocateEncrypted.setValue(false)); - EasyBind.includeWhen(dropZone.getStyleClass(), ACTIVE_CLASS, draggingOver); + decryptNameDropZone.setOnDragOver(e -> handleDragOver(e, draggingOverDecryptName)); + decryptNameDropZone.setOnDragDropped(e -> handleDragDropped(e, this::getCleartextName, this::copyDecryptedNamesToClipboard)); + decryptNameDropZone.setOnDragExited(_ -> draggingOverDecryptName.setValue(false)); + + EasyBind.includeWhen(revealEncryptedDropZone.getStyleClass(), ACTIVE_CLASS, draggingOverLocateEncrypted); + EasyBind.includeWhen(decryptNameDropZone.getStyleClass(), ACTIVE_CLASS, draggingOverDecryptName); } - private void handleDragEvent(DragEvent event) { - if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) { - if(SystemUtils.IS_OS_WINDOWS || SystemUtils.IS_OS_MAC) { + private void handleDragOver(DragEvent event, BooleanProperty prop) { + if (event.getGestureSource() == null && event.getDragboard().hasFiles()) { + if (SystemUtils.IS_OS_WINDOWS || SystemUtils.IS_OS_MAC) { event.acceptTransferModes(TransferMode.LINK); } else { event.acceptTransferModes(TransferMode.ANY); } - 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); + prop.set(true); } } - private VaultStatisticsComponent buildVaultStats(Vault vault) { - return vaultStatsBuilder.vault(vault).build(); + private void handleDragDropped(DragEvent event, Function computation, Consumer> positiveAction) { + if (event.getGestureSource() == null && event.getDragboard().hasFiles()) { + List objects = event.getDragboard().getFiles().stream().map(File::toPath).map(computation).filter(Objects::nonNull).toList(); + if (objects.isEmpty()) { + wrongFileAlert.build().showWrongFileAlertWindow(); + } else { + positiveAction.accept(objects); + } + event.setDropCompleted(!objects.isEmpty()); + event.consume(); + } } @FXML - public void revealAccessLocation() { - vaultService.reveal(vault.get()); - } - - @FXML - public void copyMountUri() { - ClipboardContent clipboardContent = new ClipboardContent(); - clipboardContent.putString(mountPoint.getValue()); - Clipboard.getSystemClipboard().setContent(clipboardContent); - } - - @FXML - public void lock() { - appWindows.startLockWorkflow(vault.get(), mainWindow); - } - - @FXML - public void showVaultStatistics() { - vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow(); - } - - @FXML - public void chooseFileAndReveal() { + public void chooseDecryptedFileAndReveal() { Preconditions.checkState(accessibleViaPath.getValue()); var fileChooser = new FileChooser(); - fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.filePickerTitle")); + fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.locateEncrypted.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); + var ciphertextPath = getCiphertextPath(cleartextFile.toPath()); + if (ciphertextPath != null) { + revealOrCopyPaths(List.of(ciphertextPath)); + } + } + } + + @FXML + public void chooseEncryptedFileAndCopyNames() { + var fileChooser = new FileChooser(); + fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.decryptName.filePickerTitle")); + + fileChooser.setInitialDirectory(vault.getValue().getPath().toFile()); + var ciphertextNode = fileChooser.showOpenDialog(mainWindow); + if (ciphertextNode != null) { + var nodeName = getCleartextName(ciphertextNode.toPath()); + copyDecryptedNamesToClipboard(List.of(nodeName)); + } + } + + private void copyDecryptedNamesToClipboard(List mapping) { + if (mapping.size() == 1) { + Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, mapping.getFirst().cleartext)); + } else { + var content = mapping.stream().map(CipherToCleartext::toString).collect(Collectors.joining("\n")); + Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, content)); + } + cleartextNamesCopied.setValue(true); + CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS, Platform::runLater).execute(() -> { + cleartextNamesCopied.set(false); + }); + } + + @Nullable + private CipherToCleartext getCleartextName(Path ciphertextNode) { + try { + return new CipherToCleartext(ciphertextNode.getFileName().toString(), vault.get().getCleartextName(ciphertextNode)); + } catch (IOException e) { + LOG.warn("Failed to decrypt filename for {}", ciphertextNode, e); + return null; } } @@ -165,16 +192,17 @@ public class VaultDetailUnlockedController implements FxController { return path.startsWith(Path.of(mountPoint.getValue())); } - private Optional getCiphertextPath(Path path) { + @Nullable + private Path getCiphertextPath(Path path) { if (!startsWithVaultAccessPoint(path)) { - LOG.debug("Path does not start with access point of selected vault: {}", path); - return Optional.empty(); + LOG.debug("Path does not start with mount point of selected vault: {}", path); + return null; } try { - return Optional.of(vault.get().getCiphertextPath(path)); + return vault.get().getCiphertextPath(path); } catch (IOException e) { LOG.warn("Unable to get ciphertext path from path: {}", path, e); - return Optional.empty(); + return null; } } @@ -206,6 +234,40 @@ public class VaultDetailUnlockedController implements FxController { }); } + private VaultStatisticsComponent buildVaultStats(Vault vault) { + return vaultStatsBuilder.vault(vault).build(); + } + + @FXML + public void revealAccessLocation() { + vaultService.reveal(vault.get()); + } + + @FXML + public void copyMountUri() { + ClipboardContent clipboardContent = new ClipboardContent(); + clipboardContent.putString(mountPoint.getValue()); + Clipboard.getSystemClipboard().setContent(clipboardContent); + } + + @FXML + public void lock() { + appWindows.startLockWorkflow(vault.get(), mainWindow); + } + + @FXML + public void showVaultStatistics() { + vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow(); + } + + record CipherToCleartext(String ciphertext, String cleartext) { + + @Override + public String toString() { + return ciphertext + " > " + cleartext; + } + } + /* Getter/Setter */ public ReadOnlyObjectProperty vaultProperty() { @@ -247,4 +309,12 @@ public class VaultDetailUnlockedController implements FxController { public boolean isCiphertextPathsCopied() { return ciphertextPathsCopied.get(); } + + public BooleanProperty cleartextNamesCopiedProperty() { + return cleartextNamesCopied; + } + + public boolean isCleartextNamesCopied() { + return cleartextNamesCopied.get(); + } } diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css index 4d3db3968..41c2645ea 100644 --- a/src/main/resources/css/dark_theme.css +++ b/src/main/resources/css/dark_theme.css @@ -16,6 +16,10 @@ src: url('opensans_bold.ttf'); } +@font-face { + src: url('firacode_regular.ttf'); +} + /******************************************************************************* * * * Root Styling & Colors * @@ -125,6 +129,13 @@ -fx-fill: TEXT_FILL; } +.cryptic-text { + -fx-background-color: MAIN_BG; + -fx-text-fill: TEXT_FILL; + -fx-font-family: 'Fira Code'; + -fx-font-size: 1.1em; +} + /******************************************************************************* * * * Glyph Icons * diff --git a/src/main/resources/css/firacode_regular.ttf b/src/main/resources/css/firacode_regular.ttf new file mode 100644 index 000000000..b8a44d2db Binary files /dev/null and b/src/main/resources/css/firacode_regular.ttf differ diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css index 11cb1a9df..cda105fa0 100644 --- a/src/main/resources/css/light_theme.css +++ b/src/main/resources/css/light_theme.css @@ -16,6 +16,10 @@ src: url('opensans_bold.ttf'); } +@font-face { + src: url('firacode_regular.ttf'); +} + /******************************************************************************* * * * Root Styling & Colors * @@ -124,6 +128,13 @@ -fx-fill: TEXT_FILL; } +.cryptic-text { + -fx-background-color: MAIN_BG; + -fx-text-fill: TEXT_FILL; + -fx-font-family: 'Fira Code'; + -fx-font-size: 1.1em; +} + /******************************************************************************* * * * Glyph Icons * diff --git a/src/main/resources/fxml/vault_detail_unlocked.fxml b/src/main/resources/fxml/vault_detail_unlocked.fxml index c035b2d88..01c367db7 100644 --- a/src/main/resources/fxml/vault_detail_unlocked.fxml +++ b/src/main/resources/fxml/vault_detail_unlocked.fxml @@ -1,12 +1,14 @@ + + + - - + - + - - - + + + + + + + + + -