diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 4dd4242b3..459d3c52d 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -59,6 +59,7 @@ open module org.cryptomator.desktop {
uses org.cryptomator.common.locationpresets.LocationPresetsProvider;
uses SSLContextProvider;
+ uses org.cryptomator.event.NotificationHandler;
provides TrayMenuController with AwtTrayMenuController;
provides Configurator with LogbackConfiguratorFactory;
diff --git a/src/main/java/org/cryptomator/common/EventMap.java b/src/main/java/org/cryptomator/common/EventMap.java
new file mode 100644
index 000000000..2e8dbf035
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/EventMap.java
@@ -0,0 +1,160 @@
+package org.cryptomator.common;
+
+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 org.cryptomator.event.VaultEvent;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javafx.beans.InvalidationListener;
+import javafx.collections.FXCollections;
+import javafx.collections.MapChangeListener;
+import javafx.collections.ObservableMap;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Map containing {@link VaultEvent}s.
+ * The map is keyed by the ciphertext path of the affected resource _and_ the {@link FilesystemEvent}s class in order to group same events
+ *
+ * Use {@link EventMap#put(VaultEvent)} to add an element and {@link EventMap#remove(VaultEvent)} to remove it.
+ *
+ * The map is size restricted to {@value MAX_SIZE} elements. If a _new_ element (i.e. not already present) is added, the least recently added is removed.
+ */
+@Singleton
+public class EventMap implements ObservableMap {
+
+ private static final int MAX_SIZE = 300;
+
+ public record EventKey(Path ciphertextPath, Class extends FilesystemEvent> c) {}
+
+ private final ObservableMap delegate;
+
+ @Inject
+ public EventMap() {
+ delegate = FXCollections.observableHashMap();
+ }
+
+ @Override
+ public void addListener(MapChangeListener super EventKey, ? super VaultEvent> mapChangeListener) {
+ delegate.addListener(mapChangeListener);
+ }
+
+ @Override
+ public void removeListener(MapChangeListener super EventKey, ? super VaultEvent> mapChangeListener) {
+ delegate.removeListener(mapChangeListener);
+ }
+
+ @Override
+ public int size() {
+ return delegate.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return delegate.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return delegate.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ return delegate.containsValue(value);
+ }
+
+ @Override
+ public VaultEvent get(Object key) {
+ return delegate.get(key);
+ }
+
+ @Override
+ public @Nullable VaultEvent put(EventKey key, VaultEvent value) {
+ return delegate.put(key, value);
+ }
+
+ @Override
+ public VaultEvent remove(Object key) {
+ return delegate.remove(key);
+ }
+
+ @Override
+ public void putAll(@NotNull Map extends EventKey, ? extends VaultEvent> m) {
+ delegate.putAll(m);
+ }
+
+ @Override
+ public void clear() {
+ delegate.clear();
+ }
+
+ @Override
+ public @NotNull Set keySet() {
+ return delegate.keySet();
+ }
+
+ @Override
+ public @NotNull Collection values() {
+ return delegate.values();
+ }
+
+ @Override
+ public @NotNull Set> entrySet() {
+ return delegate.entrySet();
+ }
+
+ @Override
+ public void addListener(InvalidationListener invalidationListener) {
+ delegate.addListener(invalidationListener);
+ }
+
+ @Override
+ public void removeListener(InvalidationListener invalidationListener) {
+ delegate.removeListener(invalidationListener);
+ }
+
+ public synchronized void put(VaultEvent e) {
+ //compute key
+ var key = computeKey(e.actualEvent());
+ //if-else
+ var nullOrEntry = delegate.get(key);
+ if (nullOrEntry == null) {
+ if (size() == MAX_SIZE) {
+ delegate.entrySet().stream() //
+ .min(Comparator.comparing(entry -> entry.getValue().actualEvent().getTimestamp())) //
+ .ifPresent(oldestEntry -> delegate.remove(oldestEntry.getKey()));
+ }
+ delegate.put(key, e);
+ } else {
+ delegate.put(key, nullOrEntry.incrementCount(e.actualEvent()));
+ }
+ }
+
+ public synchronized VaultEvent remove(VaultEvent similar) {
+ //compute key
+ var key = computeKey(similar.actualEvent());
+ return this.remove(key);
+ }
+
+ private EventKey computeKey(FilesystemEvent e) {
+ var p = switch (e) {
+ 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 EventKey(p, e.getClass());
+ }
+}
diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java
index 8475e7184..cd92d8d12 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.common.EventMap;
import org.cryptomator.common.mount.Mounter;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
@@ -18,9 +19,11 @@ 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;
+import org.cryptomator.event.VaultEvent;
import org.cryptomator.integrations.mount.MountFailedException;
import org.cryptomator.integrations.mount.Mountpoint;
import org.cryptomator.integrations.mount.UnmountFailedException;
@@ -32,6 +35,7 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
+import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
@@ -74,6 +78,7 @@ public class Vault {
private final ObjectBinding mountPoint;
private final Mounter mounter;
private final Settings settings;
+ private final EventMap eventMap;
private final BooleanProperty showingStats;
private final AtomicReference mountHandle = new AtomicReference<>(null);
@@ -85,7 +90,8 @@ public class Vault {
VaultState state, //
@Named("lastKnownException") ObjectProperty lastKnownException, //
VaultStats stats, //
- Mounter mounter, Settings settings) {
+ Mounter mounter, Settings settings, //
+ EventMap eventMap) {
this.vaultSettings = vaultSettings;
this.configCache = configCache;
this.cryptoFileSystem = cryptoFileSystem;
@@ -102,6 +108,7 @@ public class Vault {
this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state);
this.mounter = mounter;
this.settings = settings;
+ this.eventMap = eventMap;
this.showingStats = new SimpleBooleanProperty(false);
this.quickAccessEntry = new AtomicReference<>(null);
}
@@ -143,6 +150,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 +259,14 @@ public class Vault {
}
}
+
+ private void consumeVaultEvent(FilesystemEvent e) {
+ var wrapper = new VaultEvent(this, e);
+ Platform.runLater(() -> {
+ eventMap.put(wrapper);
+ });
+ }
+
// ******************************************************************************
// Observable Properties
// *******************************************************************************
diff --git a/src/main/java/org/cryptomator/event/Answer.java b/src/main/java/org/cryptomator/event/Answer.java
new file mode 100644
index 000000000..bfb780e52
--- /dev/null
+++ b/src/main/java/org/cryptomator/event/Answer.java
@@ -0,0 +1,14 @@
+package org.cryptomator.event;
+
+public sealed interface Answer permits Answer.DoNothing, Answer.DoSomething {
+
+
+ record DoNothing() implements Answer {}
+
+ record DoSomething(Runnable action) implements Answer {
+
+ void run() {
+ action.run();
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/event/NotificationHandler.java b/src/main/java/org/cryptomator/event/NotificationHandler.java
new file mode 100644
index 000000000..983a5d2dd
--- /dev/null
+++ b/src/main/java/org/cryptomator/event/NotificationHandler.java
@@ -0,0 +1,15 @@
+package org.cryptomator.event;
+
+import org.cryptomator.integrations.common.IntegrationsLoader;
+
+import java.util.ServiceLoader;
+import java.util.stream.Stream;
+
+public interface NotificationHandler {
+
+ Answer handle(VaultEvent e);
+
+ static Stream loadAll() {
+ return IntegrationsLoader.loadAll(ServiceLoader.load(NotificationHandler.class), NotificationHandler.class);
+ }
+}
diff --git a/src/main/java/org/cryptomator/event/VaultEvent.java b/src/main/java/org/cryptomator/event/VaultEvent.java
new file mode 100644
index 000000000..8b31747cf
--- /dev/null
+++ b/src/main/java/org/cryptomator/event/VaultEvent.java
@@ -0,0 +1,27 @@
+package org.cryptomator.event;
+
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptofs.event.FilesystemEvent;
+
+import java.time.Instant;
+
+public record VaultEvent(Vault v, FilesystemEvent actualEvent, int count) implements Comparable {
+
+ public VaultEvent(Vault v, FilesystemEvent actualEvent) {
+ this(v, actualEvent, 1);
+ }
+
+ @Override
+ public int compareTo(VaultEvent other) {
+ var timeResult = actualEvent.getTimestamp().compareTo(other.actualEvent().getTimestamp());
+ if(timeResult != 0) {
+ return timeResult;
+ } else {
+ return this.equals(other) ? 0 : this.actualEvent.getClass().getName().compareTo(other.actualEvent.getClass().getName());
+ }
+ }
+
+ public VaultEvent incrementCount(FilesystemEvent update) {
+ return new VaultEvent(v, update, count+1);
+ }
+}
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..613ba43c5
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java
@@ -0,0 +1,323 @@
+package org.cryptomator.ui.eventview;
+
+import org.cryptomator.common.EventMap;
+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.event.VaultEvent;
+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.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 EventMap eventMap;
+ @Nullable
+ private final RevealPathService revealService;
+ private final ResourceBundle resourceBundle;
+ private final ObjectProperty event;
+ 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(EventMap eventMap, Optional revealService, ResourceBundle resourceBundle) {
+ this.eventMap = eventMap;
+ this.revealService = revealService.orElseGet(() -> null);
+ this.resourceBundle = resourceBundle;
+ this.event = 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.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 setEvent(@NotNull VaultEvent item) {
+ event.set(item);
+ eventActionsMenu.hide();
+ eventActionsMenu.getItems().clear();
+ eventTooltip.setText(item.v().getDisplayName());
+ addAction("generic.action.dismiss", () -> eventMap.remove(item));
+ switch (item.actualEvent()) {
+ 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 {
+ var e = event.getValue();
+ return resourceBundle.getString("eventView.entry.vaultLocked.description").formatted(e != null ? e.v().getDisplayName() : "");
+ }
+ }
+
+
+ @FXML
+ public void toggleEventActionsMenu() {
+ var e = event.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 = event.getValue().v();
+ 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..102bbfa05
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/eventview/EventListCellFactory.java
@@ -0,0 +1,64 @@
+package org.cryptomator.ui.eventview;
+
+import org.cryptomator.event.VaultEvent;
+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;
+
+@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(VaultEvent 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.setEvent(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..2a58381e5
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/eventview/EventViewController.java
@@ -0,0 +1,121 @@
+package org.cryptomator.ui.eventview;
+
+import org.cryptomator.common.EventMap;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.event.VaultEvent;
+import org.cryptomator.ui.common.FxController;
+
+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;
+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.ResourceBundle;
+
+@EventViewScoped
+public class EventViewController implements FxController {
+
+ private final EventMap eventMap;
+ private final ObservableList eventList;
+ private final FilteredList filteredEventList;
+ private final ObservableList vaults;
+ private final SortedList reversedEventList;
+ private final ObservableList choiceBoxEntries;
+ private final ResourceBundle resourceBundle;
+ private final EventListCellFactory cellFactory;
+
+ @FXML
+ ChoiceBox vaultFilterChoiceBox;
+ @FXML
+ 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);
+ this.vaults = vaults;
+ this.reversedEventList = new SortedList<>(filteredEventList, Comparator.reverseOrder());
+ this.choiceBoxEntries = FXCollections.observableArrayList();
+ this.resourceBundle = resourceBundle;
+ this.cellFactory = cellFactory;
+ }
+
+ @FXML
+ public void initialize() {
+ choiceBoxEntries.add(null);
+ choiceBoxEntries.addAll(vaults);
+ vaults.addListener((ListChangeListener super Vault>) c -> {
+ while (c.next()) {
+ choiceBoxEntries.removeAll(c.getRemoved());
+ choiceBoxEntries.addAll(c.getAddedSubList());
+ }
+ });
+
+ eventList.addAll(eventMap.values());
+ eventMap.addListener((MapChangeListener super EventMap.EventKey, ? super VaultEvent>) this::updateList);
+ eventListView.setCellFactory(cellFactory);
+ eventListView.setItems(reversedEventList);
+
+ vaultFilterChoiceBox.setItems(choiceBoxEntries);
+ vaultFilterChoiceBox.valueProperty().addListener(this::applyVaultFilter);
+ vaultFilterChoiceBox.setConverter(new VaultConverter(resourceBundle));
+ }
+
+ private void updateList(MapChangeListener.Change extends EventMap.EventKey, ? extends VaultEvent> 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 extends Vault> v, Vault oldV, Vault newV) {
+ if (newV == null) {
+ filteredEventList.setPredicate(_ -> true);
+ } else {
+ filteredEventList.setPredicate(e -> e.v().equals(newV));
+ }
+ }
+
+ @FXML
+ void clearEvents() {
+ eventMap.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..4d968ea5b
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/eventview/EventViewModule.java
@@ -0,0 +1,61 @@
+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 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) {
+ Stage stage = factory.create();
+ stage.setHeight(498);
+ stage.setTitle(resourceBundle.getString("eventView.title"));
+ stage.setResizable(true);
+ stage.initModality(Modality.NONE);
+ 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/eventview/UpdateEventViewController.java b/src/main/java/org/cryptomator/ui/eventview/UpdateEventViewController.java
new file mode 100644
index 000000000..19c475447
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/eventview/UpdateEventViewController.java
@@ -0,0 +1,14 @@
+package org.cryptomator.ui.eventview;
+
+import org.cryptomator.ui.common.FxController;
+
+import javax.inject.Inject;
+
+@EventViewScoped
+public class UpdateEventViewController implements FxController {
+
+ @Inject
+ public UpdateEventViewController() {
+
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
index af98e284c..a32059036 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;
@@ -33,7 +34,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 +68,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/mainwindow/VaultListController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
index 0c2c46204..b18ada465 100644
--- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
+++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
@@ -1,11 +1,13 @@
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;
@@ -23,6 +25,7 @@ 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;
@@ -35,7 +38,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 +69,8 @@ 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 VaultListManager vaultListManager;
private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
private final ResourceBundle resourceBundle;
@@ -92,7 +96,8 @@ public class VaultListController implements FxController {
ResourceBundle resourceBundle, //
FxApplicationWindows appWindows, //
Settings settings, //
- Dialogs dialogs) {
+ Dialogs dialogs, //
+ EventMap eventMap) {
this.mainWindow = mainWindow;
this.vaults = vaults;
this.selectedVault = selectedVault;
@@ -105,6 +110,13 @@ public class VaultListController implements FxController {
this.dialogs = dialogs;
this.emptyVaultList = Bindings.isEmpty(vaults);
+ this.eventMap = eventMap;
+ this.newEventsPresent = new SimpleBooleanProperty(false);
+ eventMap.addListener((MapChangeListener super EventMap.EventKey, ? super VaultEvent>) change -> {
+ if (change.wasAdded()) {
+ newEventsPresent.setValue(true);
+ }
+ });
selectedVault.addListener(this::selectedVaultDidChange);
cellSize = settings.compactMode.map(compact -> compact ? 30.0 : 60.0);
@@ -264,6 +276,11 @@ public class VaultListController implements FxController {
appWindows.showPreferencesWindow(SelectedPreferencesTab.ANY);
}
+ @FXML
+ public void showEventViewer() {
+ appWindows.showEventViewer();
+ newEventsPresent.setValue(false);
+ }
// Getter and Setter
public BooleanBinding emptyVaultListProperty() {
@@ -290,4 +307,11 @@ public class VaultListController implements FxController {
return cellSize.getValue();
}
+ public ObservableValue newEventsPresentProperty() {
+ return newEventsPresent;
+ }
+
+ public boolean getNewEventsPresent() {
+ return newEventsPresent.getValue();
+ }
}
diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css
index 41c2645ea..3bce815a4 100644
--- a/src/main/resources/css/dark_theme.css
+++ b/src/main/resources/css/dark_theme.css
@@ -341,6 +341,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 *
@@ -610,6 +646,19 @@
-fx-graphic-text-gap: 9px;
}
+/*******************************************************************************
+ * *
+ * Update indicator
+ * *
+ ******************************************************************************/
+
+.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);
+}
+
/*******************************************************************************
* *
* Hyperlinks *
diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css
index cda105fa0..7967f83fc 100644
--- a/src/main/resources/css/light_theme.css
+++ b/src/main/resources/css/light_theme.css
@@ -312,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;
@@ -340,6 +344,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 *
@@ -609,6 +649,19 @@
-fx-graphic-text-gap: 9px;
}
+/*******************************************************************************
+ * *
+ * Update indicator
+ * *
+ ******************************************************************************/
+
+.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);
+}
+
/*******************************************************************************
* *
* Hyperlinks *
@@ -821,11 +874,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..a6d69dd19 100644
--- a/src/main/resources/fxml/vault_list.fxml
+++ b/src/main/resources/fxml/vault_list.fxml
@@ -1,17 +1,17 @@
+
+
+
+
+
-
-
-
-
-
+
+
+
+