From 7446c69cd807d2d44e351b3e07d97ce7d7775def Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 24 Mar 2025 14:38:12 +0100 Subject: [PATCH 01/20] Update org.cryptomator:fuse-nio-adapter from 5.0.3 to 5.0.4 fixes #3797 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7a7794567..6b94346ad 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 1.3.0 1.3.0 1.5.3 - 5.0.3 + 5.0.4 2.0.8 From c6193bc2591c19e629d890a38ce075e32eae84a4 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 24 Mar 2025 16:55:45 +0100 Subject: [PATCH 02/20] update JDK for release builds to 23.0.2 (except ppa-builds) --- .github/workflows/appimage.yml | 2 +- .github/workflows/debian.yml | 2 +- .github/workflows/mac-dmg-x64.yml | 2 +- .github/workflows/mac-dmg.yml | 2 +- .github/workflows/win-exe.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index b4e73e306..ab33cd626 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.27' jobs: get-version: 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..c208f73cb 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' From 7476e192a3a31123f4a8dae15fbc62fdb279e430 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 24 Mar 2025 17:16:28 +0100 Subject: [PATCH 03/20] [skip ci] fix wrong JDK version in appimage build --- .github/workflows/appimage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index ab33cd626..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.27' + JAVA_VERSION: '23.0.2' jobs: get-version: From 439d3d7529eb7b683c8daa3dfd18497f7979e13f Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Sat, 29 Mar 2025 18:00:22 +0100 Subject: [PATCH 04/20] Update CONTRIBUTING.md --- .github/CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) 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). From 6a26d95c1549f5a7a0c325c9159e00507e1c1d57 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Thu, 3 Apr 2025 18:10:22 +0200 Subject: [PATCH 05/20] Feature: Event view (#3780) --- .../org/cryptomator/common/vaults/Vault.java | 13 +- .../org/cryptomator/event/FSEventBucket.java | 8 + .../event/FSEventBucketContent.java | 5 + .../event/FileSystemEventAggregator.java | 107 ++++++ .../org/cryptomator/ui/common/FxmlFile.java | 1 + .../ui/controls/FontAwesome5Icon.java | 3 + .../ui/eventview/EventListCellController.java | 329 ++++++++++++++++++ .../ui/eventview/EventListCellFactory.java | 66 ++++ .../ui/eventview/EventViewComponent.java | 35 ++ .../ui/eventview/EventViewController.java | 132 +++++++ .../ui/eventview/EventViewModule.java | 67 ++++ .../ui/eventview/EventViewScoped.java | 13 + .../ui/eventview/EventViewWindow.java | 14 + .../cryptomator/ui/fxapp/FxApplication.java | 5 +- .../ui/fxapp/FxApplicationModule.java | 13 +- .../ui/fxapp/FxApplicationWindows.java | 10 +- .../cryptomator/ui/fxapp/FxFSEventList.java | 67 ++++ .../ui/mainwindow/VaultListController.java | 18 +- src/main/resources/css/dark_theme.css | 50 +++ src/main/resources/css/light_theme.css | 54 ++- src/main/resources/fxml/eventview.fxml | 35 ++ src/main/resources/fxml/eventview_cell.fxml | 45 +++ src/main/resources/fxml/vault_list.fxml | 35 +- src/main/resources/i18n/strings.properties | 30 +- 24 files changed, 1137 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/cryptomator/event/FSEventBucket.java create mode 100644 src/main/java/org/cryptomator/event/FSEventBucketContent.java create mode 100644 src/main/java/org/cryptomator/event/FileSystemEventAggregator.java create mode 100644 src/main/java/org/cryptomator/ui/eventview/EventListCellController.java create mode 100644 src/main/java/org/cryptomator/ui/eventview/EventListCellFactory.java create mode 100644 src/main/java/org/cryptomator/ui/eventview/EventViewComponent.java create mode 100644 src/main/java/org/cryptomator/ui/eventview/EventViewController.java create mode 100644 src/main/java/org/cryptomator/ui/eventview/EventViewModule.java create mode 100644 src/main/java/org/cryptomator/ui/eventview/EventViewScoped.java create mode 100644 src/main/java/org/cryptomator/ui/eventview/EventViewWindow.java create mode 100644 src/main/java/org/cryptomator/ui/fxapp/FxFSEventList.java create mode 100644 src/main/resources/fxml/eventview.fxml create mode 100644 src/main/resources/fxml/eventview_cell.fxml diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index f857d6ba1..a1ad1aa42 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -10,6 +10,7 @@ package org.cryptomator.common.vaults; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Constants; +import org.cryptomator.event.FileSystemEventAggregator; import org.cryptomator.common.mount.Mounter; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; @@ -18,6 +19,7 @@ import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags; import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; +import org.cryptomator.cryptofs.event.FilesystemEvent; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; @@ -74,6 +76,7 @@ public class Vault { private final ObjectBinding mountPoint; private final Mounter mounter; private final Settings settings; + private final FileSystemEventAggregator fileSystemEventAggregator; private final BooleanProperty showingStats; private final AtomicReference mountHandle = new AtomicReference<>(null); @@ -85,7 +88,8 @@ public class Vault { VaultState state, // @Named("lastKnownException") ObjectProperty lastKnownException, // VaultStats stats, // - Mounter mounter, Settings settings) { + Mounter mounter, Settings settings, // + FileSystemEventAggregator fileSystemEventAggregator) { this.vaultSettings = vaultSettings; this.configCache = configCache; this.cryptoFileSystem = cryptoFileSystem; @@ -102,6 +106,7 @@ public class Vault { this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state); this.mounter = mounter; this.settings = settings; + this.fileSystemEventAggregator = fileSystemEventAggregator; this.showingStats = new SimpleBooleanProperty(false); this.quickAccessEntry = new AtomicReference<>(null); } @@ -143,6 +148,7 @@ public class Vault { .withFlags(flags) // .withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength.get()) // .withVaultConfigFilename(Constants.VAULTCONFIG_FILENAME) // + .withFilesystemEventConsumer(this::consumeVaultEvent) // .build(); return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps); } @@ -251,6 +257,11 @@ public class Vault { } } + + private void consumeVaultEvent(FilesystemEvent e) { + fileSystemEventAggregator.put(this, e); + } + // ****************************************************************************** // Observable Properties // ******************************************************************************* 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 6bf8ac7db..c9e3b833e 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -13,6 +13,7 @@ public enum FxmlFile { CONVERTVAULT_HUBTOPASSWORD_CONVERT("/fxml/convertvault_hubtopassword_convert.fxml"), // CONVERTVAULT_HUBTOPASSWORD_SUCCESS("/fxml/convertvault_hubtopassword_success.fxml"), // ERROR("/fxml/error.fxml"), // + EVENT_VIEW("/fxml/eventview.fxml"), // FORGET_PASSWORD("/fxml/forget_password.fxml"), // HEALTH_START("/fxml/health_start.fxml"), // HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index 485e89304..348b3a26b 100644 --- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -7,6 +7,7 @@ public enum FontAwesome5Icon { ANCHOR("\uF13D"), // ARROW_UP("\uF062"), // BAN("\uF05E"), // + BELL("\uF0F3"), // BUG("\uF188"), // CARET_DOWN("\uF0D7"), // CARET_RIGHT("\uF0Da"), // @@ -15,10 +16,12 @@ public enum FontAwesome5Icon { CLIPBOARD("\uF328"), // COG("\uF013"), // COGS("\uF085"), // + COMPRESS_ALT("\uF422"), // COPY("\uF0C5"), // CROWN("\uF521"), // DONATE("\uF4B9"), // EDIT("\uF044"), // + ELLIPSIS_V("\uF142"), // EXCHANGE_ALT("\uF362"), // EXCLAMATION("\uF12A"), // EXCLAMATION_CIRCLE("\uF06A"), // diff --git a/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java new file mode 100644 index 000000000..1327ea62c --- /dev/null +++ b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java @@ -0,0 +1,329 @@ +package org.cryptomator.ui.eventview; + +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; +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.integrations.revealpath.RevealFailedException; +import org.cryptomator.integrations.revealpath.RevealPathService; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.controls.FontAwesome5Icon; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ObservableValue; +import javafx.fxml.FXML; +import javafx.geometry.Side; +import javafx.scene.control.Button; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tooltip; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.layout.HBox; +import javafx.util.Duration; +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; + +public class EventListCellController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(EventListCellController.class); + 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 FileSystemEventAggregator fileSystemEventAggregator; + @Nullable + private final RevealPathService revealService; + private final ResourceBundle resourceBundle; + private final ObjectProperty> eventEntry; + private final StringProperty eventMessage; + private final StringProperty eventDescription; + private final ObjectProperty eventIcon; + private final ObservableValue eventCount; + private final ObservableValue vaultUnlocked; + private final ObservableValue readableTime; + private final ObservableValue readableDate; + private final ObservableValue message; + private final ObservableValue description; + private final ObservableValue icon; + private final BooleanProperty actionsButtonVisible; + private final Tooltip eventTooltip; + + @FXML + HBox root; + @FXML + ContextMenu eventActionsMenu; + @FXML + Button eventActionsButton; + + @Inject + public EventListCellController(FileSystemEventAggregator fileSystemEventAggregator, Optional revealService, ResourceBundle resourceBundle) { + this.fileSystemEventAggregator = fileSystemEventAggregator; + this.revealService = revealService.orElseGet(() -> null); + this.resourceBundle = resourceBundle; + this.eventEntry = new SimpleObjectProperty<>(null); + this.eventMessage = new SimpleStringProperty(); + this.eventDescription = new SimpleStringProperty(); + this.eventIcon = new SimpleObjectProperty<>(); + 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); + this.actionsButtonVisible = new SimpleBooleanProperty(); + this.eventTooltip = new Tooltip(); + eventTooltip.setShowDelay(Duration.millis(500.0)); + } + + @FXML + public void initialize() { + actionsButtonVisible.bind(Bindings.createBooleanBinding(this::determineActionsButtonVisibility, root.hoverProperty(), eventActionsMenu.showingProperty(), vaultUnlocked)); + vaultUnlocked.addListener((_, _, newValue) -> eventActionsMenu.hide()); + Tooltip.install(root, eventTooltip); + } + + private boolean determineActionsButtonVisibility() { + return vaultUnlocked.getValue() && (eventActionsMenu.isShowing() || root.isHover()); + } + + public void setEventEntry(@NotNull Map.Entry item) { + eventEntry.set(item); + eventActionsMenu.hide(); + eventActionsMenu.getItems().clear(); + 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); + case BrokenDirFileEvent fse -> this.adjustToBrokenDirFileEvent(fse); + case BrokenFileNodeEvent fse -> this.adjustToBrokenFileNodeEvent(fse); + } + } + + + private void adjustToBrokenFileNodeEvent(BrokenFileNodeEvent bfe) { + eventIcon.setValue(FontAwesome5Icon.TIMES); + eventMessage.setValue(resourceBundle.getString("eventView.entry.brokenFileNode.message")); + eventDescription.setValue(bfe.ciphertextPath().getFileName().toString()); + if (revealService != null) { + addAction("eventView.entry.brokenFileNode.showEncrypted", () -> reveal(revealService, convertVaultPathToSystemPath(bfe.ciphertextPath()))); + } else { + addAction("eventView.entry.brokenFileNode.copyEncrypted", () -> copyToClipboard(convertVaultPathToSystemPath(bfe.ciphertextPath()).toString())); + } + addAction("eventView.entry.brokenFileNode.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(bfe.cleartextPath()).toString())); + } + + private void adjustToConflictResolvedEvent(ConflictResolvedEvent cre) { + eventIcon.setValue(FontAwesome5Icon.CHECK); + eventMessage.setValue(resourceBundle.getString("eventView.entry.conflictResolved.message")); + eventDescription.setValue(cre.resolvedCiphertextPath().getFileName().toString()); + if (revealService != null) { + addAction("eventView.entry.conflictResolved.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cre.resolvedCleartextPath()))); + } else { + addAction("eventView.entry.conflictResolved.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cre.resolvedCleartextPath()).toString())); + } + } + + private void adjustToConflictEvent(ConflictResolutionFailedEvent cfe) { + eventIcon.setValue(FontAwesome5Icon.COMPRESS_ALT); + eventMessage.setValue(resourceBundle.getString("eventView.entry.conflict.message")); + eventDescription.setValue(cfe.conflictingCiphertextPath().getFileName().toString()); + if (revealService != null) { + addAction("eventView.entry.conflict.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cfe.canonicalCleartextPath()))); + addAction("eventView.entry.conflict.showEncrypted", () -> reveal(revealService, cfe.conflictingCiphertextPath())); + } else { + addAction("eventView.entry.conflict.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cfe.canonicalCleartextPath()).toString())); + addAction("eventView.entry.conflict.copyEncrypted", () -> copyToClipboard(cfe.conflictingCiphertextPath().toString())); + } + } + + private void adjustToDecryptionFailedEvent(DecryptionFailedEvent dfe) { + eventIcon.setValue(FontAwesome5Icon.BAN); + eventMessage.setValue(resourceBundle.getString("eventView.entry.decryptionFailed.message")); + eventDescription.setValue(dfe.ciphertextPath().getFileName().toString()); + if (revealService != null) { + addAction("eventView.entry.decryptionFailed.showEncrypted", () -> reveal(revealService, dfe.ciphertextPath())); + } else { + addAction("eventView.entry.decryptionFailed.copyEncrypted", () -> copyToClipboard(dfe.ciphertextPath().toString())); + } + } + + private void adjustToBrokenDirFileEvent(BrokenDirFileEvent bde) { + eventIcon.setValue(FontAwesome5Icon.TIMES); + eventMessage.setValue(resourceBundle.getString("eventView.entry.brokenDirFile.message")); + eventDescription.setValue(bde.ciphertextPath().getParent().getFileName().toString()); + if (revealService != null) { + addAction("eventView.entry.brokenDirFile.showEncrypted", () -> reveal(revealService, bde.ciphertextPath())); + } else { + addAction("eventView.entry.brokenDirFile.copyEncrypted", () -> copyToClipboard(bde.ciphertextPath().toString())); + } + } + + private void addAction(String localizationKey, Runnable action) { + var entry = new MenuItem(resourceBundle.getString(localizationKey)); + entry.getStyleClass().addLast("dropdown-button-context-menu-item"); + entry.setOnAction(_ -> action.run()); + eventActionsMenu.getItems().addLast(entry); + } + + + private FontAwesome5Icon selectIcon() { + if (vaultUnlocked.getValue()) { + return eventIcon.getValue(); + } else { + return FontAwesome5Icon.LOCK; + } + } + + private String selectMessage() { + if (vaultUnlocked.getValue()) { + return eventMessage.getValue(); + } else { + return resourceBundle.getString("eventView.entry.vaultLocked.message"); + } + } + + 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 { + return ""; + } + } + + + @FXML + public void toggleEventActionsMenu() { + var e = eventEntry.get(); + if (e != null) { + if (eventActionsMenu.isShowing()) { + eventActionsMenu.hide(); + } else { + eventActionsMenu.show(eventActionsButton, Side.BOTTOM, 0.0, 0.0); + } + } + } + + private Path convertVaultPathToSystemPath(Path p) { + if (!(p instanceof CryptoPath)) { + throw new IllegalArgumentException("Path " + p + " is not a vault path"); + } + var v = eventEntry.getValue().getKey().vault(); + if (!v.isUnlocked()) { + return Path.of(System.getProperty("user.home")); + } + + var mountUri = v.getMountPoint().uri(); + var internalPath = p.toString().substring(1); + return Path.of(mountUri.getPath().concat(internalPath).substring(1)); + } + + private void reveal(RevealPathService s, Path p) { + try { + s.reveal(p); + } catch (RevealFailedException e) { + LOG.warn("Failed to show path {}", p, e); + } + } + + private void copyToClipboard(String s) { + var content = new ClipboardContent(); + content.putString(s); + Clipboard.getSystemClipboard().setContent(content); + } + + //-- property accessors -- + public ObservableValue messageProperty() { + return message; + } + + public String getMessage() { + return message.getValue(); + } + + public ObservableValue countProperty() { + return eventCount; + } + + public String getCount() { + return eventCount.getValue(); + } + + public ObservableValue descriptionProperty() { + return description; + } + + public String getDescription() { + return description.getValue(); + } + + public ObservableValue iconProperty() { + return icon; + } + + public FontAwesome5Icon getIcon() { + return icon.getValue(); + } + + public ObservableValue actionsButtonVisibleProperty() { + return actionsButtonVisible; + } + + public boolean isActionsButtonVisible() { + return actionsButtonVisible.getValue(); + } + + public ObservableValue eventLocalTimeProperty() { + return readableTime; + } + + public String getEventLocalTime() { + return readableTime.getValue(); + } + + public ObservableValue eventLocalDateProperty() { + return readableDate; + } + + public String getEventLocalDate() { + return readableDate.getValue(); + } + + public ObservableValue vaultUnlockedProperty() { + return vaultUnlocked; + } + + public boolean isVaultUnlocked() { + return vaultUnlocked.getValue(); + } +} diff --git a/src/main/java/org/cryptomator/ui/eventview/EventListCellFactory.java b/src/main/java/org/cryptomator/ui/eventview/EventListCellFactory.java new file mode 100644 index 000000000..e607b41a7 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/eventview/EventListCellFactory.java @@ -0,0 +1,66 @@ +package org.cryptomator.ui.eventview; + +import org.cryptomator.event.FSEventBucket; +import org.cryptomator.event.FSEventBucketContent; +import org.cryptomator.ui.common.FxmlLoaderFactory; + +import javax.inject.Inject; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.ListCell; +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>> { + + private static final String FXML_PATH = "/fxml/eventview_cell.fxml"; + + private final FxmlLoaderFactory fxmlLoaders; + + @Inject + EventListCellFactory(@EventViewWindow FxmlLoaderFactory fxmlLoaders) { + this.fxmlLoaders = fxmlLoaders; + } + + + @Override + public ListCell> call(ListView> eventListView) { + try { + FXMLLoader fxmlLoader = fxmlLoaders.load(FXML_PATH); + return new Cell(fxmlLoader.getRoot(), fxmlLoader.getController()); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load %s.".formatted(FXML_PATH), e); + } + } + + private static class Cell extends ListCell> { + + private final Parent root; + private final EventListCellController controller; + + public Cell(Parent root, EventListCellController controller) { + this.root = root; + this.controller = controller; + } + + @Override + protected void updateItem(Map.Entry item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setGraphic(null); + this.getStyleClass().remove("list-cell"); + } else { + this.getStyleClass().addLast("list-cell"); + setContentDisplay(ContentDisplay.GRAPHIC_ONLY); + setGraphic(root); + controller.setEventEntry(item); + } + } + } +} diff --git a/src/main/java/org/cryptomator/ui/eventview/EventViewComponent.java b/src/main/java/org/cryptomator/ui/eventview/EventViewComponent.java new file mode 100644 index 000000000..443885ec6 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/eventview/EventViewComponent.java @@ -0,0 +1,35 @@ +package org.cryptomator.ui.eventview; + +import dagger.Lazy; +import dagger.Subcomponent; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; + +import javafx.scene.Scene; +import javafx.stage.Stage; + +@EventViewScoped +@Subcomponent(modules = {EventViewModule.class}) +public interface EventViewComponent { + + @EventViewWindow + Stage window(); + + @FxmlScene(FxmlFile.EVENT_VIEW) + Lazy scene(); + + default Stage showEventViewerWindow() { + Stage stage = window(); + stage.setScene(scene().get()); + stage.sizeToScene(); + stage.show(); + stage.requestFocus(); + return stage; + } + + @Subcomponent.Factory + interface Factory { + + EventViewComponent create(); + } +} diff --git a/src/main/java/org/cryptomator/ui/eventview/EventViewController.java b/src/main/java/org/cryptomator/ui/eventview/EventViewController.java new file mode 100644 index 000000000..ca4fe9d55 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/eventview/EventViewController.java @@ -0,0 +1,132 @@ +package org.cryptomator.ui.eventview; + +import org.cryptomator.common.vaults.Vault; +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.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; +import javafx.fxml.FXML; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.ListView; +import javafx.util.StringConverter; +import java.util.Map; +import java.util.ResourceBundle; + +@EventViewScoped +public class EventViewController implements FxController { + + private final FilteredList> filteredEventList; + private final ObservableList vaults; + private final FileSystemEventAggregator aggregator; + private final SortedList> sortedEventList; + private final ObservableList choiceBoxEntries; + private final ResourceBundle resourceBundle; + private final EventListCellFactory cellFactory; + + @FXML + ChoiceBox vaultFilterChoiceBox; + @FXML + ListView> eventListView; + + @Inject + public EventViewController(FxFSEventList fxFSEventList, ObservableList vaults, ResourceBundle resourceBundle, EventListCellFactory cellFactory, FileSystemEventAggregator aggregator) { + this.filteredEventList = fxFSEventList.getObservableList().filtered(_ -> true); + this.vaults = vaults; + 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); + choiceBoxEntries.addAll(vaults); + vaults.addListener((ListChangeListener) c -> { + while (c.next()) { + choiceBoxEntries.removeAll(c.getRemoved()); + choiceBoxEntries.addAll(c.getAddedSubList()); + } + }); + + eventListView.setCellFactory(cellFactory); + eventListView.setItems(sortedEventList); + + vaultFilterChoiceBox.setItems(choiceBoxEntries); + vaultFilterChoiceBox.valueProperty().addListener(this::applyVaultFilter); + vaultFilterChoiceBox.setConverter(new VaultConverter(resourceBundle)); + } + + private void applyVaultFilter(ObservableValue v, Vault oldV, Vault newV) { + if (newV == null) { + filteredEventList.setPredicate(_ -> true); + } else { + filteredEventList.setPredicate(e -> e.getKey().vault().equals(newV)); + } + } + + @FXML + void clearEvents() { + aggregator.clear(); + } + + private static class VaultConverter extends StringConverter { + + private final ResourceBundle resourceBundle; + + VaultConverter(ResourceBundle resourceBundle) { + this.resourceBundle = resourceBundle; + } + + @Override + public String toString(Vault v) { + if (v == null) { + return resourceBundle.getString("eventView.filter.allVaults"); + } else { + return v.getDisplayName(); + } + } + + @Override + public Vault fromString(String displayLanguage) { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/src/main/java/org/cryptomator/ui/eventview/EventViewModule.java b/src/main/java/org/cryptomator/ui/eventview/EventViewModule.java new file mode 100644 index 000000000..94829023f --- /dev/null +++ b/src/main/java/org/cryptomator/ui/eventview/EventViewModule.java @@ -0,0 +1,67 @@ +package org.cryptomator.ui.eventview; + +import dagger.Binds; +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.IntoMap; +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 org.cryptomator.ui.fxapp.FxFSEventList; + +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 +abstract class EventViewModule { + + @Provides + @EventViewScoped + @EventViewWindow + 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; + } + + @Provides + @EventViewScoped + @EventViewWindow + static FxmlLoaderFactory provideFxmlLoaderFactory(Map, Provider> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle); + } + + @Provides + @FxmlScene(FxmlFile.EVENT_VIEW) + @EventViewScoped + static Scene provideEventViewerScene(@EventViewWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.EVENT_VIEW); + } + + + @Binds + @IntoMap + @FxControllerKey(EventViewController.class) + abstract FxController bindEventViewController(EventViewController controller); + + @Binds + @IntoMap + @FxControllerKey(EventListCellController.class) + abstract FxController bindEventListCellController(EventListCellController controller); +} diff --git a/src/main/java/org/cryptomator/ui/eventview/EventViewScoped.java b/src/main/java/org/cryptomator/ui/eventview/EventViewScoped.java new file mode 100644 index 000000000..5281db3cd --- /dev/null +++ b/src/main/java/org/cryptomator/ui/eventview/EventViewScoped.java @@ -0,0 +1,13 @@ +package org.cryptomator.ui.eventview; + +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 EventViewScoped { + +} diff --git a/src/main/java/org/cryptomator/ui/eventview/EventViewWindow.java b/src/main/java/org/cryptomator/ui/eventview/EventViewWindow.java new file mode 100644 index 000000000..44e9b312a --- /dev/null +++ b/src/main/java/org/cryptomator/ui/eventview/EventViewWindow.java @@ -0,0 +1,14 @@ +package org.cryptomator.ui.eventview; + +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 EventViewWindow { + +} 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 af98e284c..9110f8d50 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -8,6 +8,7 @@ package org.cryptomator.ui.fxapp; import dagger.Module; import dagger.Provides; import org.cryptomator.ui.error.ErrorComponent; +import org.cryptomator.ui.eventview.EventViewComponent; import org.cryptomator.ui.health.HealthCheckComponent; import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent; @@ -19,6 +20,9 @@ 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; @@ -33,7 +37,8 @@ import java.io.InputStream; ErrorComponent.class, // HealthCheckComponent.class, // UpdateReminderComponent.class, // - ShareVaultComponent.class}) + ShareVaultComponent.class, // + EventViewComponent.class}) abstract class FxApplicationModule { private static Image createImageFromResource(String resourceName) throws IOException { @@ -66,4 +71,10 @@ abstract class FxApplicationModule { return builder.build(); } + @Provides + @FxApplicationScoped + static EventViewComponent provideEventViewComponent(EventViewComponent.Factory factory) { + return factory.create(); + } + } \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java index 4ea0ace1b..c8a870fd8 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java @@ -8,6 +8,7 @@ import org.cryptomator.integrations.tray.TrayIntegrationProvider; import org.cryptomator.ui.dialogs.Dialogs; import org.cryptomator.ui.dialogs.SimpleDialog; import org.cryptomator.ui.error.ErrorComponent; +import org.cryptomator.ui.eventview.EventViewComponent; import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.preferences.PreferencesComponent; @@ -52,6 +53,7 @@ public class FxApplicationWindows { private final UpdateReminderComponent.Factory updateReminderWindowFactory; private final LockComponent.Factory lockWorkflowFactory; private final ErrorComponent.Factory errorWindowFactory; + private final Lazy eventViewWindow; private final ExecutorService executor; private final VaultOptionsComponent.Factory vaultOptionsWindow; private final ShareVaultComponent.Factory shareVaultWindow; @@ -70,6 +72,7 @@ public class FxApplicationWindows { ErrorComponent.Factory errorWindowFactory, // VaultOptionsComponent.Factory vaultOptionsWindow, // ShareVaultComponent.Factory shareVaultWindow, // + Lazy eventViewWindow, // ExecutorService executor, // Dialogs dialogs) { this.primaryStage = primaryStage; @@ -81,6 +84,7 @@ public class FxApplicationWindows { this.updateReminderWindowFactory = updateReminderWindowFactory; this.lockWorkflowFactory = lockWorkflowFactory; this.errorWindowFactory = errorWindowFactory; + this.eventViewWindow = eventViewWindow; this.executor = executor; this.vaultOptionsWindow = vaultOptionsWindow; this.shareVaultWindow = shareVaultWindow; @@ -184,6 +188,11 @@ public class FxApplicationWindows { }); } + + public CompletionStage showEventViewer() { + return CompletableFuture.supplyAsync(() -> eventViewWindow.get().showEventViewerWindow(), Platform::runLater).whenComplete(this::reportErrors); + } + /** * Displays the generic error scene in the given window. * @@ -201,5 +210,4 @@ public class FxApplicationWindows { LOG.error("Failed to display stage", error); } } - } 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/mainwindow/VaultListController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java index 0c2c46204..a457ade3f 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java @@ -10,6 +10,7 @@ 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; @@ -35,7 +36,6 @@ import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.input.TransferMode; -import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import java.io.File; @@ -67,6 +67,7 @@ public class VaultListController implements FxController { private final VaultListCellFactory cellFactory; private final AddVaultWizardComponent.Builder addVaultWizard; private final BooleanBinding emptyVaultList; + private final BooleanProperty unreadEvents; private final VaultListManager vaultListManager; private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty(); private final ResourceBundle resourceBundle; @@ -92,7 +93,8 @@ public class VaultListController implements FxController { ResourceBundle resourceBundle, // FxApplicationWindows appWindows, // Settings settings, // - Dialogs dialogs) { + Dialogs dialogs, // + FxFSEventList fxFSEventList) { this.mainWindow = mainWindow; this.vaults = vaults; this.selectedVault = selectedVault; @@ -105,6 +107,7 @@ public class VaultListController implements FxController { this.dialogs = dialogs; this.emptyVaultList = Bindings.isEmpty(vaults); + this.unreadEvents = fxFSEventList.unreadEventsProperty(); selectedVault.addListener(this::selectedVaultDidChange); cellSize = settings.compactMode.map(compact -> compact ? 30.0 : 60.0); @@ -264,6 +267,10 @@ public class VaultListController implements FxController { appWindows.showPreferencesWindow(SelectedPreferencesTab.ANY); } + @FXML + public void showEventViewer() { + appWindows.showEventViewer(); + } // Getter and Setter public BooleanBinding emptyVaultListProperty() { @@ -290,4 +297,11 @@ public class VaultListController implements FxController { return cellSize.getValue(); } + public ObservableValue unreadEventsPresentProperty() { + return unreadEvents; + } + + 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 4d3db3968..4ff354f67 100644 --- a/src/main/resources/css/dark_theme.css +++ b/src/main/resources/css/dark_theme.css @@ -302,6 +302,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; @@ -330,6 +334,42 @@ -fx-fill: transparent; } +/******************************************************************************* + * * + * Event List * + * * + ******************************************************************************/ + +.event-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; +} + +.event-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; +} + +.event-window .button-bar .button-right:armed { + -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_ARMED; +} + +.event-window .list-view .list-cell:hover { + -fx-background-color: CONTROL_BG_SELECTED; +} + +.event-window .list-view .list-cell:selected { + -fx-background-color: PRIMARY, CONTROL_BG_SELECTED; + -fx-background-insets: 0, 0 0 0 3px; +} + /******************************************************************************* * * * NotificationBar * @@ -599,6 +639,16 @@ -fx-graphic-text-gap: 9px; } +/******************************************************************************* + * * + * Update indicator + * * + ******************************************************************************/ + +.icon-update-indicator { + -fx-fill: RED_5; +} + /******************************************************************************* * * * Hyperlinks * diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css index 11cb1a9df..052aec302 100644 --- a/src/main/resources/css/light_theme.css +++ b/src/main/resources/css/light_theme.css @@ -301,6 +301,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; @@ -329,6 +333,42 @@ -fx-fill: transparent; } +/******************************************************************************* + * * + * Event List * + * * + ******************************************************************************/ + +.event-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; +} + +.event-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; +} + +.event-window .button-bar .button-right:armed { + -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_ARMED; +} + +.event-window .list-view .list-cell:hover { + -fx-background-color: CONTROL_BG_SELECTED; +} + +.event-window .list-view .list-cell:selected { + -fx-background-color: PRIMARY, CONTROL_BG_SELECTED; + -fx-background-insets: 0, 0 0 0 3px; +} + /******************************************************************************* * * * NotificationBar * @@ -598,6 +638,16 @@ -fx-graphic-text-gap: 9px; } +/******************************************************************************* + * * + * Update indicator + * * + ******************************************************************************/ + +.icon-update-indicator { + -fx-fill: RED_5; +} + /******************************************************************************* * * * Hyperlinks * @@ -810,11 +860,11 @@ /******************************************************************************* * * - * Add Vault - MenuItem * + * Dropdown button context menu * * ******************************************************************************/ -.add-vault-menu-item { +.dropdown-button-context-menu-item { -fx-padding: 4px 8px; } diff --git a/src/main/resources/fxml/eventview.fxml b/src/main/resources/fxml/eventview.fxml new file mode 100644 index 000000000..05399c72d --- /dev/null +++ b/src/main/resources/fxml/eventview.fxml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/eventview_cell.fxml b/src/main/resources/fxml/eventview_cell.fxml new file mode 100644 index 000000000..dc8a679f7 --- /dev/null +++ b/src/main/resources/fxml/eventview_cell.fxml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/vault_list.fxml b/src/main/resources/fxml/vault_list.fxml index 161a29e87..6706ec293 100644 --- a/src/main/resources/fxml/vault_list.fxml +++ b/src/main/resources/fxml/vault_list.fxml @@ -1,17 +1,21 @@ + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + - - + - + - - - + + + -