diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a4670277a..93066b9d9 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -16,6 +16,10 @@ - Suggest your change by [submitting a new issue](https://github.com/cryptomator/cryptomator/issues/new/choose) and start writing code. +## Do you intend to add a new translation or change an existing one? + +Translations are not managed directly in this repository. Instead, we use [Crowdin](https://translate.cryptomator.org/), which automatically synchronizes translations with this repository. If you want to help us with translations, please visit our translation project on Crowdin. + ## Code of Conduct Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md). diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index b4e73e306..7f3cb4652 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -11,7 +11,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '23.0.1+11' + JAVA_VERSION: '23.0.2' jobs: get-version: diff --git a/.github/workflows/av-whitelist.yml b/.github/workflows/av-whitelist.yml index aeece4e83..3cc164b30 100644 --- a/.github/workflows/av-whitelist.yml +++ b/.github/workflows/av-whitelist.yml @@ -13,15 +13,48 @@ on: description: "Url to the file to upload" required: true type: string + avast: + description: "Upload to Avast" + required: false + type: boolean + default: false + kaspersky: + description: "Upload to Kaspersky" + required: false + type: boolean + default: false jobs: - allowlist: - name: Anti Virus Allowlisting + download-file: + name: Downloads the file into the VM runs-on: ubuntu-latest + outputs: + fileName: ${{ steps.extractName.outputs.fileName}} steps: - - name: Download file + - name: Extract file name + id: extractName run: | - curl --remote-name ${{ inputs.url }} -L + url="${{ inputs.url }}" + echo "fileName=${url##*/}" >> $GITHUB_OUTPUT + - name: Download file + run: curl --remote-name ${{ inputs.url }} -L -o ${{steps.extractName.outputs.fileName}} + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.extractName.outputs.fileName }} + path: ${{ steps.extractName.outputs.fileName }} + if-no-files-found: error + allowlist-kaspersky: + name: Anti Virus Allowlisting Kaspersky + runs-on: ubuntu-latest + needs: download-file + if: github.event_name == 'workflow_call' || inputs.kaspersky + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: ${{ needs.download-file.outputs.fileName }} + path: upload - name: Upload to Kaspersky uses: SamKirkland/FTP-Deploy-Action@v4.3.5 with: @@ -30,11 +63,26 @@ jobs: port: 990 username: ${{ secrets.ALLOWLIST_KASPERSKY_USERNAME }} password: ${{ secrets.ALLOWLIST_KASPERSKY_PASSWORD }} - - name: Upload to Avast - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + local-dir: ./upload/ + allowlist-avast: + name: Anti Virus Allowlisting Avast + runs-on: ubuntu-latest + needs: download-file + if: github.event_name == 'workflow_call' || inputs.avast + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: ${{ needs.download-file.outputs.fileName }} + path: upload + - name: Upload to Avast + uses: wlixcc/SFTP-Deploy-Action@v1.2.5 with: - protocol: ftp server: whitelisting.avast.com - port: 21 + port: 22 username: ${{ secrets.ALLOWLIST_AVAST_USERNAME }} - password: ${{ secrets.ALLOWLIST_AVAST_PASSWORD }} \ No newline at end of file + password: ${{ secrets.ALLOWLIST_AVAST_PASSWORD }} + ssh_private_key: '' + sftp_only: true + local_path: './upload/*' + remote_path: '/data' \ No newline at end of file diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index fa021d441..2cfe8adb4 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -17,7 +17,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '23.0.1+11' + JAVA_VERSION: '23.0.2+7' COFFEELIBS_JDK: 23 COFFEELIBS_JDK_VERSION: '23.0.1+11-0ppa1' OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/23.0.1/openjfx-23.0.1_linux-x64_bin-jmods.zip' diff --git a/.github/workflows/mac-dmg-x64.yml b/.github/workflows/mac-dmg-x64.yml index c7ef8b8b6..25fc9e64f 100644 --- a/.github/workflows/mac-dmg-x64.yml +++ b/.github/workflows/mac-dmg-x64.yml @@ -15,7 +15,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '23.0.1+11' + JAVA_VERSION: '23.0.2+7' jobs: get-version: diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index d724e6ed2..dc37e9eca 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -16,7 +16,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '23.0.1+11' + JAVA_VERSION: '23.0.2+7' jobs: get-version: diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index 14b9dd5aa..e42c8486b 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -16,7 +16,7 @@ on: env: JAVA_DIST: 'zulu' - JAVA_VERSION: '23.0.1+11' + JAVA_VERSION: '23.0.2+7' OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/23.0.1/openjfx-23.0.1_windows-x64_bin-jmods.zip' OPENJFX_JMODS_AMD64_HASH: 'ee176dcee3bd78bde7910735bd67f67c792882f5b89626796ae06f7a1c0119d3' WINFSP_MSI: 'https://github.com/winfsp/winfsp/releases/download/v2.0/winfsp-2.0.23075.msi' @@ -394,7 +394,7 @@ jobs: allowlist-exe: uses: ./.github/workflows/av-whitelist.yml - needs: [publish] + needs: [publish, allowlist-msi] with: url: ${{ needs.publish.outputs.download-url-exe }} secrets: inherit diff --git a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml index d69716e06..ac23b67a8 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml +++ b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml @@ -83,6 +83,12 @@ + + https://github.com/cryptomator/cryptomator/releases/1.15.3 + + + https://github.com/cryptomator/cryptomator/releases/1.15.2 + https://github.com/cryptomator/cryptomator/releases/1.15.1 diff --git a/pom.xml b/pom.xml index 18c013d1c..ce058b4f4 100644 --- a/pom.xml +++ b/pom.xml @@ -38,37 +38,37 @@ 1.3.0 1.3.0 1.5.3 - 5.0.3 - 2.0.8 + 5.0.5 + 2.0.10 3.17.0 - 2.55 + 2.56.1 2.2 2.18.3 23.0.2 4.5.0 9.37.3 - 1.5.17 + 1.5.18 2.0.17 0.8.1 1.9.0 - 5.11.4 - 5.15.2 + 5.12.2 + 5.17.0 3.0 26.0.2 - 12.1.0 - 0.8.12 + 12.1.1 + 0.8.13 2.5.0 1.4.0 - 3.13.0 + 3.14.0 3.3.1 3.8.1 - 3.5.2 + 3.5.3 3.4.2 diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index cd92d8d12..2e1ae4bba 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -10,7 +10,7 @@ package org.cryptomator.common.vaults; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Constants; -import org.cryptomator.common.EventMap; +import org.cryptomator.event.FileSystemEventAggregator; import org.cryptomator.common.mount.Mounter; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; @@ -78,7 +78,7 @@ public class Vault { private final ObjectBinding mountPoint; private final Mounter mounter; private final Settings settings; - private final EventMap eventMap; + private final FileSystemEventAggregator fileSystemEventAggregator; private final BooleanProperty showingStats; private final AtomicReference mountHandle = new AtomicReference<>(null); @@ -91,7 +91,7 @@ public class Vault { @Named("lastKnownException") ObjectProperty lastKnownException, // VaultStats stats, // Mounter mounter, Settings settings, // - EventMap eventMap) { + FileSystemEventAggregator fileSystemEventAggregator) { this.vaultSettings = vaultSettings; this.configCache = configCache; this.cryptoFileSystem = cryptoFileSystem; @@ -108,7 +108,7 @@ public class Vault { this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state); this.mounter = mounter; this.settings = settings; - this.eventMap = eventMap; + this.fileSystemEventAggregator = fileSystemEventAggregator; this.showingStats = new SimpleBooleanProperty(false); this.quickAccessEntry = new AtomicReference<>(null); } @@ -261,10 +261,7 @@ public class Vault { private void consumeVaultEvent(FilesystemEvent e) { - var wrapper = new VaultEvent(this, e); - Platform.runLater(() -> { - eventMap.put(wrapper); - }); + fileSystemEventAggregator.put(this, e); } // ****************************************************************************** diff --git a/src/main/java/org/cryptomator/event/FSEventBucket.java b/src/main/java/org/cryptomator/event/FSEventBucket.java new file mode 100644 index 000000000..370a9557a --- /dev/null +++ b/src/main/java/org/cryptomator/event/FSEventBucket.java @@ -0,0 +1,8 @@ +package org.cryptomator.event; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptofs.event.FilesystemEvent; + +import java.nio.file.Path; + +public record FSEventBucket(Vault vault, Path idPath, Class c) {} diff --git a/src/main/java/org/cryptomator/event/FSEventBucketContent.java b/src/main/java/org/cryptomator/event/FSEventBucketContent.java new file mode 100644 index 000000000..b252608c7 --- /dev/null +++ b/src/main/java/org/cryptomator/event/FSEventBucketContent.java @@ -0,0 +1,5 @@ +package org.cryptomator.event; + +import org.cryptomator.cryptofs.event.FilesystemEvent; + +public record FSEventBucketContent(FilesystemEvent mostRecentEvent, int count) {} diff --git a/src/main/java/org/cryptomator/event/FileSystemEventAggregator.java b/src/main/java/org/cryptomator/event/FileSystemEventAggregator.java new file mode 100644 index 000000000..c871436fd --- /dev/null +++ b/src/main/java/org/cryptomator/event/FileSystemEventAggregator.java @@ -0,0 +1,107 @@ +package org.cryptomator.event; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptofs.event.BrokenDirFileEvent; +import org.cryptomator.cryptofs.event.BrokenFileNodeEvent; +import org.cryptomator.cryptofs.event.ConflictResolutionFailedEvent; +import org.cryptomator.cryptofs.event.ConflictResolvedEvent; +import org.cryptomator.cryptofs.event.DecryptionFailedEvent; +import org.cryptomator.cryptofs.event.FilesystemEvent; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Aggregator for {@link FilesystemEvent}s. + *

+ * The aggregator groups filesystem events by the vault where the event occurred, an identifying path (clear- or ciphertext) and the event class (aka type). + * A group is called an {@link FSEventBucket}, its {@link FSEventBucketContent} is the most recent event object and a count of how often the event already occurred. + */ +@Singleton +public class FileSystemEventAggregator { + + private final ConcurrentHashMap map; + private final AtomicBoolean hasUpdates; + + @Inject + public FileSystemEventAggregator() { + this.map = new ConcurrentHashMap<>(); + this.hasUpdates = new AtomicBoolean(false); + } + + /** + * Adds the given event to the map. If a bucket for this event already exists, only the count is updated and the event set as the most recent one. + * + * @param v Vault where the event occurred + * @param e Actual {@link FilesystemEvent} + */ + public void put(Vault v, FilesystemEvent e) { + var key = computeKey(v, e); + map.compute(key, (k, val) -> { + if (val == null) { + return new FSEventBucketContent(e, 1); + } else { + return new FSEventBucketContent(e, val.count() + 1); + } + }); + hasUpdates.set(true); + } + + /** + * Removes an event bucket from the map. + */ + public FSEventBucketContent remove(FSEventBucket key) { + var content = map.remove(key); + hasUpdates.set(true); + return content; + } + + /** + * Clears the event map. + */ + public void clear() { + map.clear(); + hasUpdates.set(true); + } + + + public boolean hasMaybeUpdates() { + return hasUpdates.get(); + } + + /** + * Clones the map entries into a collection. + *

+ * The collection is first cleared, then all map entries are added in one bulk operation. Cleans the hasUpdates status. + * + * @param target collection which is first cleared and then the EntrySet copied to. + */ + public void cloneTo(Collection> target) { + hasUpdates.set(false); + target.clear(); + target.addAll(map.entrySet()); + } + + /** + * Method to compute the identifying key for a given filesystem event + * + * @param v Vault where the event occurred + * @param event Actual {@link FilesystemEvent} + * @return a {@link FSEventBucket} used in the map and lru cache + */ + private static FSEventBucket computeKey(Vault v, FilesystemEvent event) { + var p = switch (event) { + case DecryptionFailedEvent(_, Path ciphertextPath, _) -> ciphertextPath; + case ConflictResolvedEvent(_, _, _, _, Path resolvedCiphertext) -> resolvedCiphertext; + case ConflictResolutionFailedEvent(_, _, Path conflictingCiphertext, _) -> conflictingCiphertext; + case BrokenDirFileEvent(_, Path ciphertext) -> ciphertext; + case BrokenFileNodeEvent(_, _, Path ciphertext) -> ciphertext; + }; + return new FSEventBucket(v, p, event.getClass()); + } +} 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/eventview/EventListCellController.java b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java index 613ba43c5..487049d4c 100644 --- a/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java +++ b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java @@ -1,6 +1,8 @@ package org.cryptomator.ui.eventview; -import org.cryptomator.common.EventMap; +import org.cryptomator.event.FSEventBucket; +import org.cryptomator.event.FSEventBucketContent; +import org.cryptomator.event.FileSystemEventAggregator; import org.cryptomator.common.Nullable; import org.cryptomator.common.ObservableUtil; import org.cryptomator.cryptofs.CryptoPath; @@ -9,7 +11,6 @@ import org.cryptomator.cryptofs.event.BrokenFileNodeEvent; import org.cryptomator.cryptofs.event.ConflictResolutionFailedEvent; import org.cryptomator.cryptofs.event.ConflictResolvedEvent; import org.cryptomator.cryptofs.event.DecryptionFailedEvent; -import org.cryptomator.event.VaultEvent; import org.cryptomator.integrations.revealpath.RevealFailedException; import org.cryptomator.integrations.revealpath.RevealPathService; import org.cryptomator.ui.common.FxController; @@ -41,6 +42,7 @@ import java.nio.file.Path; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; +import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; import java.util.function.Function; @@ -51,11 +53,11 @@ public class EventListCellController implements FxController { private static final DateTimeFormatter LOCAL_DATE_FORMATTER = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withZone(ZoneId.systemDefault()); private static final DateTimeFormatter LOCAL_TIME_FORMATTER = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault()); - private final EventMap eventMap; + private final FileSystemEventAggregator fileSystemEventAggregator; @Nullable private final RevealPathService revealService; private final ResourceBundle resourceBundle; - private final ObjectProperty event; + private final ObjectProperty> eventEntry; private final StringProperty eventMessage; private final StringProperty eventDescription; private final ObjectProperty eventIcon; @@ -77,18 +79,18 @@ public class EventListCellController implements FxController { Button eventActionsButton; @Inject - public EventListCellController(EventMap eventMap, Optional revealService, ResourceBundle resourceBundle) { - this.eventMap = eventMap; + public EventListCellController(FileSystemEventAggregator fileSystemEventAggregator, Optional revealService, ResourceBundle resourceBundle) { + this.fileSystemEventAggregator = fileSystemEventAggregator; this.revealService = revealService.orElseGet(() -> null); this.resourceBundle = resourceBundle; - this.event = new SimpleObjectProperty<>(null); + this.eventEntry = new SimpleObjectProperty<>(null); this.eventMessage = new SimpleStringProperty(); this.eventDescription = new SimpleStringProperty(); this.eventIcon = new SimpleObjectProperty<>(); - this.eventCount = ObservableUtil.mapWithDefault(event, e -> e.count() == 1? "" : "("+ e.count() +")", ""); - this.vaultUnlocked = ObservableUtil.mapWithDefault(event.flatMap(e -> e.v().unlockedProperty()), Function.identity(), false); - this.readableTime = ObservableUtil.mapWithDefault(event, e -> LOCAL_TIME_FORMATTER.format(e.actualEvent().getTimestamp()), ""); - this.readableDate = ObservableUtil.mapWithDefault(event, e -> LOCAL_DATE_FORMATTER.format(e.actualEvent().getTimestamp()), ""); + this.eventCount = ObservableUtil.mapWithDefault(eventEntry, e -> e.getValue().count() == 1? "" : "("+ e.getValue().count() +")", ""); + this.vaultUnlocked = ObservableUtil.mapWithDefault(eventEntry.flatMap(e -> e.getKey().vault().unlockedProperty()), Function.identity(), false); + this.readableTime = ObservableUtil.mapWithDefault(eventEntry, e -> LOCAL_TIME_FORMATTER.format(e.getValue().mostRecentEvent().getTimestamp()), ""); + this.readableDate = ObservableUtil.mapWithDefault(eventEntry, e -> LOCAL_DATE_FORMATTER.format(e.getValue().mostRecentEvent().getTimestamp()), ""); this.message = Bindings.createStringBinding(this::selectMessage, vaultUnlocked, eventMessage); this.description = Bindings.createStringBinding(this::selectDescription, vaultUnlocked, eventDescription); this.icon = Bindings.createObjectBinding(this::selectIcon, vaultUnlocked, eventIcon); @@ -108,13 +110,15 @@ public class EventListCellController implements FxController { return vaultUnlocked.getValue() && (eventActionsMenu.isShowing() || root.isHover()); } - public void setEvent(@NotNull VaultEvent item) { - event.set(item); + public void setEventEntry(@NotNull Map.Entry item) { + eventEntry.set(item); eventActionsMenu.hide(); eventActionsMenu.getItems().clear(); - eventTooltip.setText(item.v().getDisplayName()); - addAction("generic.action.dismiss", () -> eventMap.remove(item)); - switch (item.actualEvent()) { + eventTooltip.setText(item.getKey().vault().getDisplayName()); + addAction("generic.action.dismiss", () -> { + fileSystemEventAggregator.remove(item.getKey()); + }); + switch (item.getValue().mostRecentEvent()) { case ConflictResolvedEvent fse -> this.adjustToConflictResolvedEvent(fse); case ConflictResolutionFailedEvent fse -> this.adjustToConflictEvent(fse); case DecryptionFailedEvent fse -> this.adjustToDecryptionFailedEvent(fse); @@ -202,23 +206,25 @@ public class EventListCellController implements FxController { if (vaultUnlocked.getValue()) { return eventMessage.getValue(); } else { - return resourceBundle.getString("eventView.entry.vaultLocked.message"); + return "***********"; } } private String selectDescription() { if (vaultUnlocked.getValue()) { return eventDescription.getValue(); + } else if (eventEntry.getValue() != null) { + var e = eventEntry.getValue().getKey(); + return resourceBundle.getString("eventView.entry.vaultLocked.description").formatted(e != null ? e.vault().getDisplayName() : ""); } else { - var e = event.getValue(); - return resourceBundle.getString("eventView.entry.vaultLocked.description").formatted(e != null ? e.v().getDisplayName() : ""); + return ""; } } @FXML public void toggleEventActionsMenu() { - var e = event.get(); + var e = eventEntry.get(); if (e != null) { if (eventActionsMenu.isShowing()) { eventActionsMenu.hide(); @@ -232,7 +238,7 @@ public class EventListCellController implements FxController { if (!(p instanceof CryptoPath)) { throw new IllegalArgumentException("Path " + p + " is not a vault path"); } - var v = event.getValue().v(); + var v = eventEntry.getValue().getKey().vault(); if (!v.isUnlocked()) { return Path.of(System.getProperty("user.home")); } diff --git a/src/main/java/org/cryptomator/ui/eventview/EventListCellFactory.java b/src/main/java/org/cryptomator/ui/eventview/EventListCellFactory.java index 102bbfa05..e607b41a7 100644 --- a/src/main/java/org/cryptomator/ui/eventview/EventListCellFactory.java +++ b/src/main/java/org/cryptomator/ui/eventview/EventListCellFactory.java @@ -1,6 +1,7 @@ package org.cryptomator.ui.eventview; -import org.cryptomator.event.VaultEvent; +import org.cryptomator.event.FSEventBucket; +import org.cryptomator.event.FSEventBucketContent; import org.cryptomator.ui.common.FxmlLoaderFactory; import javax.inject.Inject; @@ -12,9 +13,10 @@ import javafx.scene.control.ListView; import javafx.util.Callback; import java.io.IOException; import java.io.UncheckedIOException; +import java.util.Map; @EventViewScoped -public class EventListCellFactory implements Callback, ListCell> { +public class EventListCellFactory implements Callback>, ListCell>> { private static final String FXML_PATH = "/fxml/eventview_cell.fxml"; @@ -27,7 +29,7 @@ public class EventListCellFactory implements Callback, List @Override - public ListCell call(ListView eventListView) { + public ListCell> call(ListView> eventListView) { try { FXMLLoader fxmlLoader = fxmlLoaders.load(FXML_PATH); return new Cell(fxmlLoader.getRoot(), fxmlLoader.getController()); @@ -36,7 +38,7 @@ public class EventListCellFactory implements Callback, List } } - private static class Cell extends ListCell { + private static class Cell extends ListCell> { private final Parent root; private final EventListCellController controller; @@ -47,7 +49,7 @@ public class EventListCellFactory implements Callback, List } @Override - protected void updateItem(VaultEvent item, boolean empty) { + protected void updateItem(Map.Entry item, boolean empty) { super.updateItem(item, empty); if (empty || item == null) { @@ -57,7 +59,7 @@ public class EventListCellFactory implements Callback, List this.getStyleClass().addLast("list-cell"); setContentDisplay(ContentDisplay.GRAPHIC_ONLY); setGraphic(root); - controller.setEvent(item); + controller.setEventEntry(item); } } } diff --git a/src/main/java/org/cryptomator/ui/eventview/EventViewController.java b/src/main/java/org/cryptomator/ui/eventview/EventViewController.java index 2a58381e5..ca4fe9d55 100644 --- a/src/main/java/org/cryptomator/ui/eventview/EventViewController.java +++ b/src/main/java/org/cryptomator/ui/eventview/EventViewController.java @@ -1,15 +1,16 @@ package org.cryptomator.ui.eventview; -import org.cryptomator.common.EventMap; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.event.VaultEvent; +import org.cryptomator.event.FSEventBucket; +import org.cryptomator.event.FSEventBucketContent; +import org.cryptomator.event.FileSystemEventAggregator; import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.fxapp.FxFSEventList; import javax.inject.Inject; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; -import javafx.collections.MapChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; @@ -17,17 +18,16 @@ import javafx.fxml.FXML; import javafx.scene.control.ChoiceBox; import javafx.scene.control.ListView; import javafx.util.StringConverter; -import java.util.Comparator; +import java.util.Map; import java.util.ResourceBundle; @EventViewScoped public class EventViewController implements FxController { - private final EventMap eventMap; - private final ObservableList eventList; - private final FilteredList filteredEventList; + private final FilteredList> filteredEventList; private final ObservableList vaults; - private final SortedList reversedEventList; + private final FileSystemEventAggregator aggregator; + private final SortedList> sortedEventList; private final ObservableList choiceBoxEntries; private final ResourceBundle resourceBundle; private final EventListCellFactory cellFactory; @@ -35,20 +35,45 @@ public class EventViewController implements FxController { @FXML ChoiceBox vaultFilterChoiceBox; @FXML - ListView eventListView; + ListView> eventListView; @Inject - public EventViewController(EventMap eventMap, ObservableList vaults, ResourceBundle resourceBundle, EventListCellFactory cellFactory) { - this.eventMap = eventMap; - this.eventList = FXCollections.observableArrayList(); - this.filteredEventList = eventList.filtered(_ -> true); + public EventViewController(FxFSEventList fxFSEventList, ObservableList vaults, ResourceBundle resourceBundle, EventListCellFactory cellFactory, FileSystemEventAggregator aggregator) { + this.filteredEventList = fxFSEventList.getObservableList().filtered(_ -> true); this.vaults = vaults; - this.reversedEventList = new SortedList<>(filteredEventList, Comparator.reverseOrder()); + this.aggregator = aggregator; + this.sortedEventList = new SortedList<>(filteredEventList, this::compareBuckets); this.choiceBoxEntries = FXCollections.observableArrayList(); this.resourceBundle = resourceBundle; this.cellFactory = cellFactory; } + /** + * Comparison method for the lru cache. During comparsion the map is accessed. + * First the entries are compared by the event timestamp, then vaultId, then identifying path and lastly by class name. + * + * @param left an entry of a {@link FSEventBucket} and its content + * @param right another entry of a {@link FSEventBucket} plus content, compared to {@code left} + * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second. + */ + private int compareBuckets(Map.Entry left, Map.Entry right) { + var t1 = left.getValue().mostRecentEvent().getTimestamp(); + var t2 = right.getValue().mostRecentEvent().getTimestamp(); + var timeComparison = t1.compareTo(t2); + if (timeComparison != 0) { + return -timeComparison; //we need the reverse timesorting + } + var vaultIdComparison = left.getKey().vault().getId().compareTo(right.getKey().vault().getId()); + if (vaultIdComparison != 0) { + return vaultIdComparison; + } + var pathComparison = left.getKey().idPath().compareTo(right.getKey().idPath()); + if (pathComparison != 0) { + return pathComparison; + } + return left.getKey().c().getName().compareTo(right.getKey().c().getName()); + } + @FXML public void initialize() { choiceBoxEntries.add(null); @@ -60,39 +85,25 @@ public class EventViewController implements FxController { } }); - eventList.addAll(eventMap.values()); - eventMap.addListener((MapChangeListener) this::updateList); eventListView.setCellFactory(cellFactory); - eventListView.setItems(reversedEventList); + eventListView.setItems(sortedEventList); vaultFilterChoiceBox.setItems(choiceBoxEntries); vaultFilterChoiceBox.valueProperty().addListener(this::applyVaultFilter); vaultFilterChoiceBox.setConverter(new VaultConverter(resourceBundle)); } - private void updateList(MapChangeListener.Change change) { - if (change.wasAdded() && change.wasRemoved()) { - //entry updated - eventList.remove(change.getValueRemoved()); - eventList.addLast(change.getValueAdded()); - } else if (change.wasAdded()) { - eventList.addLast(change.getValueAdded()); - } else { //removed - eventList.remove(change.getValueRemoved()); - } - } - private void applyVaultFilter(ObservableValue v, Vault oldV, Vault newV) { if (newV == null) { filteredEventList.setPredicate(_ -> true); } else { - filteredEventList.setPredicate(e -> e.v().equals(newV)); + filteredEventList.setPredicate(e -> e.getKey().vault().equals(newV)); } } @FXML void clearEvents() { - eventMap.clear(); + aggregator.clear(); } private static class VaultConverter extends StringConverter { diff --git a/src/main/java/org/cryptomator/ui/eventview/EventViewModule.java b/src/main/java/org/cryptomator/ui/eventview/EventViewModule.java index 4d968ea5b..94829023f 100644 --- a/src/main/java/org/cryptomator/ui/eventview/EventViewModule.java +++ b/src/main/java/org/cryptomator/ui/eventview/EventViewModule.java @@ -11,6 +11,7 @@ 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 org.cryptomator.ui.fxapp.FxFSEventList; import javax.inject.Provider; import javafx.scene.Scene; @@ -25,12 +26,17 @@ abstract class EventViewModule { @Provides @EventViewScoped @EventViewWindow - static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle) { + static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle, FxFSEventList fxFSEventList) { Stage stage = factory.create(); stage.setHeight(498); stage.setTitle(resourceBundle.getString("eventView.title")); stage.setResizable(true); stage.initModality(Modality.NONE); + stage.focusedProperty().addListener((_,_,isFocused) -> { + if(isFocused) { + fxFSEventList.unreadEventsProperty().setValue(false); + } + }); return stage; } diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index fd480033c..ccc0684af 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -29,9 +29,10 @@ public class FxApplication { private final FxApplicationStyle applicationStyle; private final FxApplicationTerminator applicationTerminator; private final AutoUnlocker autoUnlocker; + private final FxFSEventList fxFSEventList; @Inject - FxApplication(@Named("startupTime") long startupTime, Environment environment, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker) { + FxApplication(@Named("startupTime") long startupTime, Environment environment, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker, FxFSEventList fxFSEventList) { this.startupTime = startupTime; this.environment = environment; this.settings = settings; @@ -41,6 +42,7 @@ public class FxApplication { this.applicationStyle = applicationStyle; this.applicationTerminator = applicationTerminator; this.autoUnlocker = autoUnlocker; + this.fxFSEventList = fxFSEventList; } public void start() { @@ -85,6 +87,7 @@ public class FxApplication { migrateAndInformDokanyRemoval(); launchEventHandler.startHandlingLaunchEvents(); + fxFSEventList.schedulePollForUpdates(); autoUnlocker.tryUnlockForTimespan(2, TimeUnit.MINUTES); } diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index a32059036..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; @@ -20,11 +21,15 @@ import org.cryptomator.ui.unlock.UnlockComponent; import org.cryptomator.ui.updatereminder.UpdateReminderComponent; import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; +import javax.inject.Named; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.image.Image; 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/fxapp/FxFSEventList.java b/src/main/java/org/cryptomator/ui/fxapp/FxFSEventList.java new file mode 100644 index 000000000..e9e574b95 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/fxapp/FxFSEventList.java @@ -0,0 +1,67 @@ +package org.cryptomator.ui.fxapp; + +import org.cryptomator.event.FSEventBucket; +import org.cryptomator.event.FSEventBucketContent; +import org.cryptomator.event.FileSystemEventAggregator; + +import javax.inject.Inject; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * List of all occurred filesystem events. + *

+ * The list exposes an observable list and a property to listen for updates. Internally it polls the {@link FileSystemEventAggregator} in a regular interval for updates. + * If an update is available, the list from the {@link FileSystemEventAggregator } is cloned to this list on the FX application thread. + */ +@FxApplicationScoped +public class FxFSEventList { + + private final ObservableList> events; + private final FileSystemEventAggregator eventAggregator; + private final ScheduledExecutorService scheduler; + private final BooleanProperty unreadEvents; + + @Inject + public FxFSEventList(FileSystemEventAggregator fsEventAggregator, ScheduledExecutorService scheduler) { + this.events = FXCollections.observableArrayList(); + this.eventAggregator = fsEventAggregator; + this.scheduler = scheduler; + this.unreadEvents = new SimpleBooleanProperty(false); + } + + public void schedulePollForUpdates() { + scheduler.schedule(this::checkForEventUpdates, 1000, TimeUnit.MILLISECONDS); + } + + /** + * Checks for event updates and reschedules. + * If updates are available, the aggregated events are copied from back- to the frontend. + * Reschedules itself on successful execution + */ + private void checkForEventUpdates() { + if (eventAggregator.hasMaybeUpdates()) { + Platform.runLater(() -> { + eventAggregator.cloneTo(events); + unreadEvents.setValue(true); + schedulePollForUpdates(); + }); + } else { + schedulePollForUpdates(); + } + } + + public ObservableList> getObservableList() { + return events; + } + + public BooleanProperty unreadEventsProperty() { + return unreadEvents; + } +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java index 6d52362b3..37b2f2e1f 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java @@ -36,6 +36,7 @@ import javafx.stage.Stage; import javafx.stage.WindowEvent; import javafx.util.Duration; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; @PassphraseEntryScoped public class PassphraseEntryController implements FxController { @@ -49,6 +50,7 @@ public class PassphraseEntryController implements FxController { private final ForgetPasswordComponent.Builder forgetPassword; private final KeychainManager keychain; private final StringBinding vaultName; + private final ExecutorService backgroundExecutorService; private final BooleanProperty unlockInProgress = new SimpleBooleanProperty(); private final ObjectBinding unlockButtonContentDisplay = Bindings.when(unlockInProgress).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY); private final BooleanProperty unlockButtonDisabled = new SimpleBooleanProperty(); @@ -64,7 +66,7 @@ public class PassphraseEntryController implements FxController { public Animation unlockAnimation; @Inject - public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) { + public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain, ExecutorService backgroundExecutorService) { this.window = window; this.vault = vault; this.result = result; @@ -72,8 +74,8 @@ public class PassphraseEntryController implements FxController { this.forgetPassword = forgetPassword; this.keychain = keychain; this.vaultName = WeakBindings.bindString(vault.displayNameProperty()); + this.backgroundExecutorService = backgroundExecutorService; window.setOnHiding(this::windowClosed); - result.whenCompleteAsync((r, t) -> unlockInProgress.set(false), Platform::runLater); } @FXML @@ -119,8 +121,6 @@ public class PassphraseEntryController implements FxController { new KeyFrame(Duration.millis(800), legsExtendedY, legsExtendedX, faceHidden), // new KeyFrame(Duration.millis(1000), faceVisible) // ); - - result.whenCompleteAsync((r, t) -> stopUnlockAnimation()); } @FXML @@ -133,6 +133,9 @@ public class PassphraseEntryController implements FxController { result.cancel(true); LOG.debug("Unlock canceled by user."); } + if( passwordField != null) { + passwordField.getCharacters().destroy(); + } } @@ -142,7 +145,7 @@ public class PassphraseEntryController implements FxController { unlockInProgress.set(true); CharSequence pwFieldContents = passwordField.getCharacters(); Passphrase pw = Passphrase.copyOf(pwFieldContents); - result.complete(new PassphraseEntryResult(pw, savePasswordCheckbox.isSelected())); + result.completeAsync(() -> new PassphraseEntryResult(pw, savePasswordCheckbox.isSelected()), backgroundExecutorService); startUnlockAnimation(); } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java index e743f2c94..42a8fda7e 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java @@ -13,6 +13,7 @@ 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; @@ -47,7 +48,6 @@ 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 { @@ -61,6 +61,7 @@ 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; @@ -70,7 +71,6 @@ public class VaultDetailUnlockedController implements FxController { 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 revealEncryptedDropZone; @@ -78,13 +78,22 @@ public class VaultDetailUnlockedController implements FxController { 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; @@ -106,7 +115,7 @@ public class VaultDetailUnlockedController implements FxController { revealEncryptedDropZone.setOnDragExited(_ -> draggingOverLocateEncrypted.setValue(false)); decryptNameDropZone.setOnDragOver(e -> handleDragOver(e, draggingOverDecryptName)); - decryptNameDropZone.setOnDragDropped(e -> handleDragDropped(e, this::getCleartextName, this::copyDecryptedNamesToClipboard)); + decryptNameDropZone.setOnDragDropped(e -> showDecryptNameWindow(e.getDragboard().getFiles().stream().map(File::toPath).toList())); decryptNameDropZone.setOnDragExited(_ -> draggingOverDecryptName.setValue(false)); EasyBind.includeWhen(revealEncryptedDropZone.getStyleClass(), ACTIVE_CLASS, draggingOverLocateEncrypted); @@ -153,39 +162,12 @@ public class VaultDetailUnlockedController implements FxController { } @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)); - } + public void showDecryptNameWindow() { + showDecryptNameWindow(List.of()); } - 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; - } + private void showDecryptNameWindow(List pathsToDecrypt) { + decryptNameWindowFactory.create(vault.get(), mainWindow, pathsToDecrypt).showDecryptFileNameWindow(); } private boolean startsWithVaultAccessPoint(Path path) { @@ -260,14 +242,6 @@ public class VaultDetailUnlockedController implements FxController { vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow(); } - record CipherToCleartext(String ciphertext, String cleartext) { - - @Override - public String toString() { - return ciphertext + " > " + cleartext; - } - } - /* Getter/Setter */ public ReadOnlyObjectProperty vaultProperty() { @@ -310,11 +284,5 @@ public class VaultDetailUnlockedController implements FxController { return ciphertextPathsCopied.get(); } - public BooleanProperty cleartextNamesCopiedProperty() { - return cleartextNamesCopied; - } - - public boolean isCleartextNamesCopied() { - return cleartextNamesCopied.get(); - } } + diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java index b18ada465..a457ade3f 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java @@ -1,17 +1,16 @@ package org.cryptomator.ui.mainwindow; import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.common.EventMap; import org.cryptomator.common.settings.Settings; 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.event.VaultEvent; import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.VaultService; import org.cryptomator.ui.dialogs.Dialogs; +import org.cryptomator.ui.fxapp.FxFSEventList; import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.cryptomator.ui.preferences.SelectedPreferencesTab; import org.slf4j.Logger; @@ -25,7 +24,6 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; -import javafx.collections.MapChangeListener; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.geometry.Side; @@ -69,8 +67,7 @@ public class VaultListController implements FxController { private final VaultListCellFactory cellFactory; private final AddVaultWizardComponent.Builder addVaultWizard; private final BooleanBinding emptyVaultList; - private final EventMap eventMap; - private final BooleanProperty newEventsPresent; + private final BooleanProperty unreadEvents; private final VaultListManager vaultListManager; private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty(); private final ResourceBundle resourceBundle; @@ -97,7 +94,7 @@ public class VaultListController implements FxController { FxApplicationWindows appWindows, // Settings settings, // Dialogs dialogs, // - EventMap eventMap) { + FxFSEventList fxFSEventList) { this.mainWindow = mainWindow; this.vaults = vaults; this.selectedVault = selectedVault; @@ -110,13 +107,7 @@ public class VaultListController implements FxController { this.dialogs = dialogs; this.emptyVaultList = Bindings.isEmpty(vaults); - this.eventMap = eventMap; - this.newEventsPresent = new SimpleBooleanProperty(false); - eventMap.addListener((MapChangeListener) change -> { - if (change.wasAdded()) { - newEventsPresent.setValue(true); - } - }); + this.unreadEvents = fxFSEventList.unreadEventsProperty(); selectedVault.addListener(this::selectedVaultDidChange); cellSize = settings.compactMode.map(compact -> compact ? 30.0 : 60.0); @@ -279,7 +270,6 @@ public class VaultListController implements FxController { @FXML public void showEventViewer() { appWindows.showEventViewer(); - newEventsPresent.setValue(false); } // Getter and Setter @@ -307,11 +297,11 @@ public class VaultListController implements FxController { return cellSize.getValue(); } - public ObservableValue newEventsPresentProperty() { - return newEventsPresent; + public ObservableValue unreadEventsPresentProperty() { + return unreadEvents; } - public boolean getNewEventsPresent() { - return newEventsPresent.getValue(); + public boolean getUnreadEventsPresent() { + return unreadEvents.getValue(); } } diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css index 3bce815a4..fff15e834 100644 --- a/src/main/resources/css/dark_theme.css +++ b/src/main/resources/css/dark_theme.css @@ -130,8 +130,7 @@ } .cryptic-text { - -fx-background-color: MAIN_BG; - -fx-text-fill: TEXT_FILL; + -fx-fill: TEXT_FILL; -fx-font-family: 'Fira Code'; -fx-font-size: 1.1em; } @@ -313,6 +312,10 @@ -fx-font-size: 1.0em; } +.list-cell .header-misc { + -fx-font-size: 1.0em; +} + .list-cell .detail-label { -fx-text-fill: TEXT_FILL_MUTED; -fx-font-size: 0.8em; @@ -652,11 +655,8 @@ * * ******************************************************************************/ -.update-indicator { - -fx-background-color: white, RED_5; - -fx-background-insets: 1px, 2px; - -fx-background-radius: 6px, 5px; - -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.8), 2, 0, 0, 0); +.icon-update-indicator { + -fx-fill: RED_5; } /******************************************************************************* @@ -1022,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/light_theme.css b/src/main/resources/css/light_theme.css index 7967f83fc..39e2892ac 100644 --- a/src/main/resources/css/light_theme.css +++ b/src/main/resources/css/light_theme.css @@ -83,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'; @@ -129,8 +130,7 @@ } .cryptic-text { - -fx-background-color: MAIN_BG; - -fx-text-fill: TEXT_FILL; + -fx-fill: TEXT_FILL; -fx-font-family: 'Fira Code'; -fx-font-size: 1.1em; } @@ -655,11 +655,8 @@ * * ******************************************************************************/ -.update-indicator { - -fx-background-color: white, RED_5; - -fx-background-insets: 1px, 2px; - -fx-background-radius: 6px, 5px; - -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.8), 2, 0, 0, 0); +.icon-update-indicator { + -fx-fill: RED_5; } /******************************************************************************* @@ -1025,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/eventview.fxml b/src/main/resources/fxml/eventview.fxml index 05399c72d..3aeb17281 100644 --- a/src/main/resources/fxml/eventview.fxml +++ b/src/main/resources/fxml/eventview.fxml @@ -20,7 +20,7 @@ - + + - - - - - - - - + - + + +