|
@@ -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> 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<Vault, VaultStatisticsComponent> vaultStats;
|
|
|
private final VaultStatisticsComponent.Builder vaultStatsBuilder;
|
|
|
private final ObservableValue<Boolean> accessibleViaPath;
|
|
|
private final ObservableValue<Boolean> accessibleViaUri;
|
|
|
private final ObservableValue<String> mountPoint;
|
|
|
+ private final BooleanProperty draggingOver = new SimpleBooleanProperty();
|
|
|
+ private final BooleanProperty ciphertextPathsCopied = new SimpleBooleanProperty();
|
|
|
+
|
|
|
+ //FXML
|
|
|
+ public Button dropZone;
|
|
|
|
|
|
@Inject
|
|
|
- public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) {
|
|
|
+ public VaultDetailUnlockedController(ObjectProperty<Vault> 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<Path> 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<Path> 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<Path> 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<Path> 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<Path> 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<Vault> vaultProperty() {
|
|
@@ -119,5 +246,11 @@ public class VaultDetailUnlockedController implements FxController {
|
|
|
return mountPoint.getValue();
|
|
|
}
|
|
|
|
|
|
+ public BooleanProperty ciphertextPathsCopiedProperty() {
|
|
|
+ return ciphertextPathsCopied;
|
|
|
+ }
|
|
|
|
|
|
+ public boolean isCiphertextPathsCopied() {
|
|
|
+ return ciphertextPathsCopied.get();
|
|
|
+ }
|
|
|
}
|