diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index a1ad1aa42..8c0b68e80 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -423,6 +423,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/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index c9e3b833e..ce8c65a37 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -12,6 +12,7 @@ public enum FxmlFile { CONVERTVAULT_HUBTOPASSWORD_START("/fxml/convertvault_hubtopassword_start.fxml"), // CONVERTVAULT_HUBTOPASSWORD_CONVERT("/fxml/convertvault_hubtopassword_convert.fxml"), // CONVERTVAULT_HUBTOPASSWORD_SUCCESS("/fxml/convertvault_hubtopassword_success.fxml"), // + DECRYPTNAMES("/fxml/decryptnames.fxml"), // ERROR("/fxml/error.fxml"), // EVENT_VIEW("/fxml/eventview.fxml"), // FORGET_PASSWORD("/fxml/forget_password.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/decryptname/CipherAndCleartext.java b/src/main/java/org/cryptomator/ui/decryptname/CipherAndCleartext.java new file mode 100644 index 000000000..909285b73 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/decryptname/CipherAndCleartext.java @@ -0,0 +1,25 @@ +package org.cryptomator.ui.decryptname; + +import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.value.ObservableValue; +import java.nio.file.Path; + +public record CipherAndCleartext(Path ciphertext, String cleartextName) { + + public String getCiphertextFilename() { + return ciphertext.getFileName().toString(); + } + + public ObservableValue ciphertextFilenameProperty() { + return new ReadOnlyStringWrapper(getCiphertextFilename()); + } + + public String getCleartextName() { + return cleartextName; + } + + public ObservableValue cleartextNameProperty() { + return new ReadOnlyStringWrapper(getCleartextName()); + } + +} diff --git a/src/main/java/org/cryptomator/ui/decryptname/DecryptFileNamesViewController.java b/src/main/java/org/cryptomator/ui/decryptname/DecryptFileNamesViewController.java new file mode 100644 index 000000000..453762f55 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/decryptname/DecryptFileNamesViewController.java @@ -0,0 +1,233 @@ +package org.cryptomator.ui.decryptname; + +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.controls.FontAwesome5Icon; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ListProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.fxml.FXML; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.input.Clipboard; +import javafx.scene.input.DataFormat; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.TransferMode; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@DecryptNameScoped +public class DecryptFileNamesViewController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(DecryptFileNamesViewController.class); + private static final KeyCodeCombination COPY_TO_CLIPBOARD_SHORTCUT = new KeyCodeCombination(KeyCode.C, KeyCodeCombination.SHORTCUT_DOWN); + private static final String COPY_TO_CLIPBOARD_SHORTCUT_STRING_WIN = "CTRL+C"; + private static final String COPY_TO_CLIPBOARD_SHORTCUT_STRING_MAC = "⌘C"; + private static final String COPY_TO_CLIPBOARD_SHORTCUT_STRING_LINUX = "CTRL+C"; + + private final ListProperty mapping; + private final StringProperty dropZoneText = new SimpleStringProperty(); + private final ObjectProperty dropZoneIcon = new SimpleObjectProperty<>(); + private final BooleanProperty wrongFilesSelected = new SimpleBooleanProperty(false); + private final Stage window; + private final Vault vault; + private final ResourceBundle resourceBundle; + private final List initialList; + + @FXML + public TableColumn ciphertextColumn; + @FXML + public TableColumn cleartextColumn; + @FXML + public TableView cipherToCleartextTable; + + @Inject + public DecryptFileNamesViewController(@DecryptNameWindow Stage window, @DecryptNameWindow Vault vault, @DecryptNameWindow List pathsToDecrypt, ResourceBundle resourceBundle) { + this.window = window; + this.vault = vault; + this.resourceBundle = resourceBundle; + this.mapping = new SimpleListProperty<>(FXCollections.observableArrayList()); + this.initialList = pathsToDecrypt; + } + + @FXML + public void initialize() { + cipherToCleartextTable.setItems(mapping); + cipherToCleartextTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS); + //DragNDrop + cipherToCleartextTable.setOnDragEntered(event -> { + if (event.getGestureSource() == null && event.getDragboard().hasFiles()) { + cipherToCleartextTable.setItems(FXCollections.emptyObservableList()); + } + }); + cipherToCleartextTable.setOnDragOver(event -> { + 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); + } + } + }); + cipherToCleartextTable.setOnDragDropped(event -> { + if (event.getGestureSource() == null && event.getDragboard().hasFiles()) { + checkAndDecrypt(event.getDragboard().getFiles().stream().map(File::toPath).toList()); + cipherToCleartextTable.setItems(mapping); + } + }); + cipherToCleartextTable.setOnDragExited(_ -> cipherToCleartextTable.setItems(mapping)); + //selectionModel and copy-to-clipboard action + cipherToCleartextTable.getSelectionModel().setCellSelectionEnabled(true); + cipherToCleartextTable.setOnKeyPressed(keyEvent -> { + if (COPY_TO_CLIPBOARD_SHORTCUT.match(keyEvent)) { + copySingleCelltoClipboard(); + } + }); + ciphertextColumn.setCellValueFactory(new PropertyValueFactory<>("ciphertextFilename")); + cleartextColumn.setCellValueFactory(new PropertyValueFactory<>("cleartextName")); + + dropZoneText.setValue(resourceBundle.getString("decryptNames.dropZone.message")); + dropZoneIcon.setValue(FontAwesome5Icon.FILE_IMPORT); + + wrongFilesSelected.addListener((_, _, areWrongFiles) -> { + if (areWrongFiles) { + CompletableFuture.delayedExecutor(5, TimeUnit.SECONDS, Platform::runLater).execute(() -> { + dropZoneText.setValue(resourceBundle.getString("decryptNames.dropZone.message")); + dropZoneIcon.setValue(FontAwesome5Icon.FILE_IMPORT); + wrongFilesSelected.setValue(false); + }); + } + }); + if (!initialList.isEmpty()) { + checkAndDecrypt(initialList); + } + } + + private void copySingleCelltoClipboard() { + cipherToCleartextTable.getSelectionModel().getSelectedCells().stream().findFirst().ifPresent(tablePosition -> { + var selectedItem = cipherToCleartextTable.getSelectionModel().getSelectedItem(); + //TODO: give user feedback, if content is copied -> must be done via a custom cell factory to access the actual table cell! + if (tablePosition.getTableColumn().equals(ciphertextColumn)) { + Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, selectedItem.ciphertext().toString())); + } else { + Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, selectedItem.cleartextName())); + } + }); + } + + @FXML + public void selectFiles() { + var fileChooser = new FileChooser(); + fileChooser.setTitle(resourceBundle.getString("decryptNames.filePicker.title")); + fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter(resourceBundle.getString("decryptNames.filePicker.extensionDescription"), List.of("*.c9r"))); + fileChooser.setInitialDirectory(vault.getPath().toFile()); + var ciphertextNodes = fileChooser.showOpenMultipleDialog(window); + if (ciphertextNodes != null) { + checkAndDecrypt(ciphertextNodes.stream().map(File::toPath).toList()); + } + } + + private void checkAndDecrypt(List pathsToDecrypt) { + mapping.clear(); + //Assumption: All files are in the same directory + var testPath = pathsToDecrypt.getFirst(); + if (!testPath.startsWith(vault.getPath())) { + setDropZoneError(resourceBundle.getString("decryptNames.dropZone.error.foreignFiles").formatted(vault.getDisplayName())); + return; + } + if (pathsToDecrypt.size() == 1 && testPath.endsWith(Constants.DIR_ID_BACKUP_FILE_NAME)) { + setDropZoneError(resourceBundle.getString("decryptNames.dropZone.error.vaultInternalFiles")); + return; + } + + try { + var newMapping = pathsToDecrypt.stream().filter(p -> !p.endsWith(Constants.DIR_ID_BACKUP_FILE_NAME)).map(this::getCleartextName).toList(); + mapping.addAll(newMapping); + } catch (UncheckedIOException e) { + setDropZoneError(resourceBundle.getString("decryptNames.dropZone.error.generic")); + LOG.info("Failed to decrypt filenames for directory {}", testPath.getParent(), e); + } catch (IllegalArgumentException e) { + setDropZoneError(resourceBundle.getString("decryptNames.dropZone.error.vaultInternalFiles")); + } catch (UnsupportedOperationException e) { + setDropZoneError(resourceBundle.getString("decryptNames.dropZone.error.noDirIdBackup")); + } + } + + private void setDropZoneError(String text) { + dropZoneIcon.setValue(FontAwesome5Icon.TIMES); + dropZoneText.setValue(text); + wrongFilesSelected.setValue(true); + } + + private CipherAndCleartext getCleartextName(Path ciphertextNode) { + try { + var cleartextName = vault.getCleartextName(ciphertextNode); + return new CipherAndCleartext(ciphertextNode, cleartextName); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + //obvservable getter + + public ObservableValue dropZoneTextProperty() { + return dropZoneText; + } + + public String getDropZoneText() { + return dropZoneText.get(); + } + + public ObservableValue dropZoneIconProperty() { + return dropZoneIcon; + } + + public FontAwesome5Icon getDropZoneIcon() { + return dropZoneIcon.get(); + } + + public void clearTable() { + mapping.clear(); + } + + public void copyTableToClipboard() { + var csv = mapping.stream().map(cipherAndClear -> "\"" + cipherAndClear.ciphertext() + "\", \"" + cipherAndClear.cleartextName() + "\"").collect(Collectors.joining("\n")); + Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, csv)); + } + + public String getCopyToClipboardShortcutString() { + if (SystemUtils.IS_OS_WINDOWS) { + return COPY_TO_CLIPBOARD_SHORTCUT_STRING_WIN; + } else if (SystemUtils.IS_OS_MAC) { + return COPY_TO_CLIPBOARD_SHORTCUT_STRING_MAC; + } else { + return COPY_TO_CLIPBOARD_SHORTCUT_STRING_LINUX; + } + } +} diff --git a/src/main/java/org/cryptomator/ui/decryptname/DecryptNameComponent.java b/src/main/java/org/cryptomator/ui/decryptname/DecryptNameComponent.java new file mode 100644 index 000000000..7684d4286 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/decryptname/DecryptNameComponent.java @@ -0,0 +1,50 @@ +package org.cryptomator.ui.decryptname; + +import dagger.BindsInstance; +import dagger.Lazy; +import dagger.Subcomponent; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Named; +import javafx.scene.Scene; +import javafx.stage.Stage; +import java.nio.file.Path; +import java.util.List; + +@DecryptNameScoped +@Subcomponent(modules = DecryptNameModule.class) +public interface DecryptNameComponent { + + Logger LOG = LoggerFactory.getLogger(DecryptNameComponent.class); + + @DecryptNameWindow + Stage window(); + + @FxmlScene(FxmlFile.DECRYPTNAMES) + Lazy decryptNamesView(); + + @DecryptNameWindow + Vault vault(); + + default void showDecryptFileNameWindow() { + Stage s = window(); + s.setScene(decryptNamesView().get()); + s.sizeToScene(); + if (vault().isUnlocked()) { + s.show(); + } else { + LOG.error("Aborted showing DecryptFileName window: vault state is not {}, but {}.", VaultState.Value.UNLOCKED, vault().getState()); + } + } + + @Subcomponent.Factory + interface Factory { + + DecryptNameComponent create(@BindsInstance @DecryptNameWindow Vault vault, @BindsInstance @Named("windowOwner") Stage owner, @BindsInstance @DecryptNameWindow List pathsToDecrypt); + } +} diff --git a/src/main/java/org/cryptomator/ui/decryptname/DecryptNameModule.java b/src/main/java/org/cryptomator/ui/decryptname/DecryptNameModule.java new file mode 100644 index 000000000..0dd573940 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/decryptname/DecryptNameModule.java @@ -0,0 +1,59 @@ +package org.cryptomator.ui.decryptname; + +import dagger.Binds; +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.IntoMap; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.DefaultSceneFactory; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxControllerKey; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.StageFactory; + +import javax.inject.Named; +import javax.inject.Provider; +import javafx.scene.Scene; +import javafx.stage.Modality; +import javafx.stage.Stage; +import java.util.Map; +import java.util.ResourceBundle; + +@Module +public abstract class DecryptNameModule { + + @Provides + @DecryptNameScoped + @DecryptNameWindow + static Stage provideStage(StageFactory factory, @Named("windowOwner") Stage owner, @DecryptNameWindow Vault vault, ResourceBundle resourceBundle) { + Stage stage = factory.create(); + stage.setResizable(true); + stage.initModality(Modality.WINDOW_MODAL); + stage.initOwner(owner); + stage.setTitle(resourceBundle.getString("decryptNames.title")); + vault.stateProperty().addListener(((_, _, _) -> stage.close())); //as soon as the state changes from unlocked, close the window + return stage; + } + + @Provides + @DecryptNameScoped + @DecryptNameWindow + static FxmlLoaderFactory provideFxmlLoaderFactory(Map, Provider> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle); + } + + @Provides + @FxmlScene(FxmlFile.DECRYPTNAMES) + @DecryptNameScoped + static Scene provideDecryptNamesViewScene(@DecryptNameWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.DECRYPTNAMES); + } + + @Binds + @IntoMap + @FxControllerKey(DecryptFileNamesViewController.class) + abstract FxController bindDecryptNamesViewController(DecryptFileNamesViewController controller); + +} diff --git a/src/main/java/org/cryptomator/ui/decryptname/DecryptNameScoped.java b/src/main/java/org/cryptomator/ui/decryptname/DecryptNameScoped.java new file mode 100644 index 000000000..2a7ac0fc8 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/decryptname/DecryptNameScoped.java @@ -0,0 +1,11 @@ +package org.cryptomator.ui.decryptname; + +import javax.inject.Scope; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Scope +@Documented +@Retention(RetentionPolicy.RUNTIME) +@interface DecryptNameScoped {} diff --git a/src/main/java/org/cryptomator/ui/decryptname/DecryptNameWindow.java b/src/main/java/org/cryptomator/ui/decryptname/DecryptNameWindow.java new file mode 100644 index 000000000..7d68c9559 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/decryptname/DecryptNameWindow.java @@ -0,0 +1,12 @@ +package org.cryptomator.ui.decryptname; + +import javax.inject.Qualifier; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Documented +@Retention(RUNTIME) +@interface DecryptNameWindow {} diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index 9110f8d50..8eb221883 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -7,6 +7,7 @@ package org.cryptomator.ui.fxapp; import dagger.Module; import dagger.Provides; +import org.cryptomator.ui.decryptname.DecryptNameComponent; import org.cryptomator.ui.error.ErrorComponent; import org.cryptomator.ui.eventview.EventViewComponent; import org.cryptomator.ui.health.HealthCheckComponent; @@ -28,6 +29,7 @@ import java.io.IOException; import java.io.InputStream; @Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, // + DecryptNameComponent.class, // MainWindowComponent.class, // PreferencesComponent.class, // VaultOptionsComponent.class, // diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java index ef6148c87..42a8fda7e 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java @@ -6,12 +6,14 @@ 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; import org.cryptomator.integrations.revealpath.RevealPathService; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.VaultService; +import org.cryptomator.ui.decryptname.DecryptNameComponent; import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.cryptomator.ui.stats.VaultStatisticsComponent; import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent; @@ -39,10 +41,13 @@ 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; @MainWindowScoped public class VaultDetailUnlockedController implements FxController { @@ -56,26 +61,39 @@ public class VaultDetailUnlockedController implements FxController { private final WrongFileAlertComponent.Builder wrongFileAlert; private final Stage mainWindow; private final Optional revealPathService; + private final DecryptNameComponent.Factory decryptNameWindowFactory; 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 draggingOverLocateEncrypted = new SimpleBooleanProperty(); + private final BooleanProperty draggingOverDecryptName = new SimpleBooleanProperty(); private final BooleanProperty ciphertextPathsCopied = 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) { + public VaultDetailUnlockedController(ObjectProperty vault, // + FxApplicationWindows appWindows, // + VaultService vaultService, // + VaultStatisticsComponent.Builder vaultStatsBuilder, // + WrongFileAlertComponent.Builder wrongFileAlert, // + @MainWindow Stage mainWindow, // + Optional revealPathService, // + DecryptNameComponent.Factory decryptNameWindowFactory, // + ResourceBundle resourceBundle) { this.vault = vault; this.appWindows = appWindows; this.vaultService = vaultService; this.wrongFileAlert = wrongFileAlert; this.mainWindow = mainWindow; this.revealPathService = revealPathService; + this.decryptNameWindowFactory = decryptNameWindowFactory; this.resourceBundle = resourceBundle; this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats)); this.vaultStatsBuilder = vaultStatsBuilder; @@ -92,89 +110,81 @@ 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 -> showDecryptNameWindow(e.getDragboard().getFiles().stream().map(File::toPath).toList())); + 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 showDecryptNameWindow() { + showDecryptNameWindow(List.of()); + } + + private void showDecryptNameWindow(List pathsToDecrypt) { + decryptNameWindowFactory.create(vault.get(), mainWindow, pathsToDecrypt).showDecryptFileNameWindow(); + } + private boolean startsWithVaultAccessPoint(Path path) { 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 +216,32 @@ 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(); + } + /* Getter/Setter */ public ReadOnlyObjectProperty vaultProperty() { @@ -247,4 +283,6 @@ public class VaultDetailUnlockedController implements FxController { public boolean isCiphertextPathsCopied() { return ciphertextPathsCopied.get(); } + } + diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css index 4ff354f67..fff15e834 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,12 @@ -fx-fill: TEXT_FILL; } +.cryptic-text { + -fx-fill: TEXT_FILL; + -fx-font-family: 'Fira Code'; + -fx-font-size: 1.1em; +} + /******************************************************************************* * * * Glyph Icons * @@ -1012,4 +1022,167 @@ -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL; -fx-background-insets: 0, 1px; -fx-background-radius: 4px; -} \ No newline at end of file +} + +/******************************************************************************* + * * + * Decrypt Name Window + * * + ******************************************************************************/ + +.decrypt-name-window .button-bar { + -fx-min-height:42px; + -fx-max-height:42px; + -fx-background-color: MAIN_BG; + -fx-border-color: transparent transparent CONTROL_BORDER_NORMAL transparent; + -fx-border-width: 0 0 1px 0; +} + +.decrypt-name-window .button-bar .button-right { + -fx-border-color: transparent transparent transparent CONTROL_BORDER_NORMAL; + -fx-border-width: 0 0 0 1px; + -fx-background-color: MAIN_BG; + -fx-background-radius: 0px; + -fx-min-height: 42px; + -fx-max-height: 42px; +} + +.decrypt-name-window .button-bar .button-right:armed { + -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_ARMED; +} + +.decrypt-name-window .table-view { + -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL; + -fx-background-insets: 0,1; + /* There is some oddness if padding is in em values rather than pixels, + in particular, the left border of the control doesn't show. */ + -fx-padding: 1; /* 0.083333em; */ +} + +.table-view > .placeholder { + -fx-background-color: transparent; + -fx-background-radius: 0px; +} + +.table-view > .placeholder > .button { + -fx-border-width: 0; + -fx-border-color: transparent; + -fx-background-radius: 0px; +} + +.table-view:focused { + -fx-background-color: CONTROL_BORDER_FOCUSED, CONTROL_BG_NORMAL; + -fx-background-insets: 0, 1; + -fx-background-radius: 0, 0; + /* There is some oddness if padding is in em values rather than pixels, + in particular, the left border of the control doesn't show. */ + -fx-padding: 1; /* 0.083333em; */ +} + +.table-view > .virtual-flow > .scroll-bar:vertical { + -fx-background-insets: 0, 0 0 0 1; + -fx-padding: -1 -1 -1 0; +} + +.table-view > .virtual-flow > .corner { + -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL ; + -fx-background-insets: 0, 1 0 0 1; +} + +/* Each row in the table is a table-row-cell. Inside a table-row-cell is any + number of table-cell. */ +.table-row-cell { + -fx-background-color: GRAY_3, CONTROL_BG_NORMAL; + -fx-background-insets: 0, 0 0 1 0; + -fx-padding: 0.0em; /* 0 */ + -fx-text-fill: TEXT_FILL; +} + +.table-row-cell:odd { + -fx-background-color: GRAY_3, GRAY_1; + -fx-background-insets: 0, 0 0 1 0; +} + +.table-cell { + -fx-padding: 3px 6px 3px 6px; + -fx-background-color: transparent; + -fx-border-color: transparent CONTROL_BORDER_NORMAL transparent transparent; + -fx-border-width: 1px; + -fx-cell-size: 30px; + -fx-text-fill: TEXT_FILL; + -fx-text-overrun: center-ellipsis; +} + +.table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled:selected > .table-cell { + -fx-text-fill: TEXT_FILL; +} + +/* selected, hover - not specified */ + +/* selected, focused, hover */ +/* selected, focused */ +/* selected */ +.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:selected, +.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused:selected, +.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused:selected:hover { + -fx-background-color: CONTROL_PRIMARY_BG_NORMAL, PRIMARY_D1; + -fx-background-insets: 0 0 0 0, 1 1 1 3; + -fx-text-fill: TEXT_FILL; +} +/* focused */ +.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused { + -fx-background-color: CONTROL_PRIMARY_BORDER_FOCUSED, CONTROL_PRIMARY_BG_NORMAL , CONTROL_BG_NORMAL; + -fx-background-insets: 0 1 0 0, 1 2 1 1, 2 3 2 2; + -fx-text-fill: TEXT_FILL; +} +/* focused, hover */ +.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused:hover { + -fx-background-color: CONTROL_PRIMARY_BORDER_FOCUSED, CONTROL_PRIMARY_BG_NORMAL , PRIMARY_D2; + -fx-background-insets: 0 1 0 0, 1 2 1 1, 2 3 2 2; + -fx-text-fill: TEXT_FILL; +} +/* hover */ +.table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:hover { + -fx-background-color: PRIMARY_D2; + -fx-text-fill: TEXT_FILL; + -fx-background-insets: 0 0 1 0; +} + +/* The column-resize-line is shown when the user is attempting to resize a column. */ +.table-view .column-resize-line { + -fx-background-color: CONTROL_BG_ARMED; + -fx-padding: 0.0em 0.0416667em 0.0em 0.0416667em; /* 0 0.571429 0 0.571429 */ +} + +/* This is the area behind the column headers. An ideal place to specify background + and border colors for the whole area (not individual column-header's). */ +.table-view .column-header-background { + -fx-background-color: GRAY_2; + -fx-padding: 0; +} + +/* The column header row is made up of a number of column-header, one for each + TableColumn, and a 'filler' area that extends from the right-most column + to the edge of the tableview, or up to the 'column control' button. */ +.table-view .column-header { + -fx-text-fill: TEXT_FILL; + -fx-font-size: 1.083333em; /* 13pt ; 1 more than the default font */ + -fx-size: 24; + -fx-border-style: solid; + -fx-border-color: + transparent + GRAY_3 + GRAY_3 + transparent; + -fx-border-insets: 0 0 0 0; + -fx-border-width: 0.083333em; +} + +.table-view .column-header .label { + -fx-alignment: center; +} + +.table-view .empty-table { + -fx-background-color: MAIN_BG; + -fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */ +} 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 052aec302..39e2892ac 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 * @@ -79,6 +83,7 @@ PROGRESS_INDICATOR_END: GRAY_4; PROGRESS_BAR_BG: GRAY_8; + -fx-background-color: MAIN_BG; -fx-text-fill: TEXT_FILL; -fx-font-family: 'Open Sans'; @@ -124,6 +129,12 @@ -fx-fill: TEXT_FILL; } +.cryptic-text { + -fx-fill: TEXT_FILL; + -fx-font-family: 'Fira Code'; + -fx-font-size: 1.1em; +} + /******************************************************************************* * * * Glyph Icons * @@ -1011,4 +1022,167 @@ -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL; -fx-background-insets: 0, 1px; -fx-background-radius: 4px; -} \ No newline at end of file +} + +/******************************************************************************* + * * + * Decrypt Name Window + * * + ******************************************************************************/ + +.decrypt-name-window .button-bar { + -fx-min-height:42px; + -fx-max-height:42px; + -fx-background-color: MAIN_BG; + -fx-border-color: transparent transparent CONTROL_BORDER_NORMAL transparent; + -fx-border-width: 0 0 1px 0; +} + +.decrypt-name-window .button-bar .button-right { + -fx-border-color: transparent transparent transparent CONTROL_BORDER_NORMAL; + -fx-border-width: 0 0 0 1px; + -fx-background-color: MAIN_BG; + -fx-background-radius: 0px; + -fx-min-height: 42px; + -fx-max-height: 42px; +} + +.decrypt-name-window .button-bar .button-right:armed { + -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_ARMED; +} + +.decrypt-name-window .table-view { + -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL; + -fx-background-insets: 0,1; + /* There is some oddness if padding is in em values rather than pixels, + in particular, the left border of the control doesn't show. */ + -fx-padding: 1; /* 0.083333em; */ +} + +.table-view > .placeholder { + -fx-background-color: transparent; + -fx-background-radius: 0px; +} + +.table-view > .placeholder > .button { + -fx-border-width: 0; + -fx-border-color: transparent; + -fx-background-radius: 0px; +} + +.table-view:focused { + -fx-background-color: CONTROL_BORDER_FOCUSED, CONTROL_BG_NORMAL; + -fx-background-insets: 0, 1; + -fx-background-radius: 0, 0; + /* There is some oddness if padding is in em values rather than pixels, + in particular, the left border of the control doesn't show. */ + -fx-padding: 1; /* 0.083333em; */ +} + +.table-view > .virtual-flow > .scroll-bar:vertical { + -fx-background-insets: 0, 0 0 0 1; + -fx-padding: -1 -1 -1 0; +} + +.table-view > .virtual-flow > .corner { + -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL ; + -fx-background-insets: 0, 1 0 0 1; +} + +/* Each row in the table is a table-row-cell. Inside a table-row-cell is any + number of table-cell. */ +.table-row-cell { + -fx-background-color: GRAY_6, CONTROL_BG_NORMAL; + -fx-background-insets: 0, 0 0 1 0; + -fx-padding: 0.0em; /* 0 */ + -fx-text-fill: TEXT_FILL; +} + +.table-row-cell:odd { + -fx-background-color: GRAY_6, GRAY_9; + -fx-background-insets: 0, 0 0 1 0; +} + +.table-cell { + -fx-padding: 3px 6px 3px 6px; + -fx-background-color: transparent; + -fx-border-color: transparent CONTROL_BORDER_NORMAL transparent transparent; + -fx-border-width: 1px; + -fx-cell-size: 30px; + -fx-text-fill: TEXT_FILL; + -fx-text-overrun: center-ellipsis; +} + +.table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled:selected > .table-cell { + -fx-text-fill: TEXT_FILL; +} + +/* selected, hover - not specified */ + +/* selected, focused, hover */ +/* selected, focused */ +/* selected */ +.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:selected, +.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused:selected, +.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused:selected:hover { + -fx-background-color: CONTROL_PRIMARY_BG_NORMAL, CONTROL_BG_SELECTED; + -fx-background-insets: 0 0 0 0, 1 1 1 3; + -fx-text-fill: TEXT_FILL; +} +/* focused */ +.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused { + -fx-background-color: CONTROL_PRIMARY_BORDER_FOCUSED, CONTROL_PRIMARY_BG_NORMAL , CONTROL_BG_NORMAL; + -fx-background-insets: 0 1 0 0, 1 2 1 1, 2 3 2 2; + -fx-text-fill: TEXT_FILL; +} +/* focused, hover */ +.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused:hover { + -fx-background-color: CONTROL_PRIMARY_BORDER_FOCUSED, CONTROL_PRIMARY_BG_NORMAL , PRIMARY_L2; + -fx-background-insets: 0 1 0 0, 1 2 1 1, 2 3 2 2; + -fx-text-fill: TEXT_FILL; +} +/* hover */ +.table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:hover { + -fx-background-color: PRIMARY_L2; + -fx-text-fill: TEXT_FILL; + -fx-background-insets: 0 0 1 0; +} + +/* The column-resize-line is shown when the user is attempting to resize a column. */ +.table-view .column-resize-line { + -fx-background-color: CONTROL_BG_ARMED; + -fx-padding: 0.0em 0.0416667em 0.0em 0.0416667em; /* 0 0.571429 0 0.571429 */ +} + +/* This is the area behind the column headers. An ideal place to specify background + and border colors for the whole area (not individual column-header's). */ +.table-view .column-header-background { + -fx-background-color: GRAY_7; + -fx-padding: 0; +} + +/* The column header row is made up of a number of column-header, one for each + TableColumn, and a 'filler' area that extends from the right-most column + to the edge of the tableview, or up to the 'column control' button. */ +.table-view .column-header { + -fx-text-fill: TEXT_FILL; + -fx-font-size: 1.083333em; /* 13pt ; 1 more than the default font */ + -fx-size: 24; + -fx-border-style: solid; + -fx-border-color: + CONTROL_BORDER_NORMAL + GRAY_5 + GRAY_5 + transparent; + -fx-border-insets: 0 0 0 0; + -fx-border-width: 0.083333em; +} + +.table-view .column-header .label { + -fx-alignment: center; +} + +.table-view .empty-table { + -fx-background-color: MAIN_BG; + -fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */ +} diff --git a/src/main/resources/fxml/decryptnames.fxml b/src/main/resources/fxml/decryptnames.fxml new file mode 100644 index 000000000..04e16b8e1 --- /dev/null +++ b/src/main/resources/fxml/decryptnames.fxml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/vault_detail_unlocked.fxml b/src/main/resources/fxml/vault_detail_unlocked.fxml index c035b2d88..d80d8b7b1 100644 --- a/src/main/resources/fxml/vault_detail_unlocked.fxml +++ b/src/main/resources/fxml/vault_detail_unlocked.fxml @@ -1,12 +1,14 @@ + + + - - + - + - - - + + + -