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 c) {} + + private final ObservableMap delegate; + + @Inject + public EventMap() { + delegate = FXCollections.observableHashMap(); + } + + @Override + public void addListener(MapChangeListener mapChangeListener) { + delegate.addListener(mapChangeListener); + } + + @Override + public void removeListener(MapChangeListener 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 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) c -> { + while (c.next()) { + choiceBoxEntries.removeAll(c.getRemoved()); + choiceBoxEntries.addAll(c.getAddedSubList()); + } + }); + + eventList.addAll(eventMap.values()); + eventMap.addListener((MapChangeListener) 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 change) { + if (change.wasAdded() && change.wasRemoved()) { + //entry updated + eventList.remove(change.getValueRemoved()); + eventList.addLast(change.getValueAdded()); + } else if (change.wasAdded()) { + eventList.addLast(change.getValueAdded()); + } else { //removed + eventList.remove(change.getValueRemoved()); + } + } + + private void applyVaultFilter(ObservableValue v, Vault oldV, Vault newV) { + if (newV == null) { + filteredEventList.setPredicate(_ -> true); + } else { + filteredEventList.setPredicate(e -> e.v().equals(newV)); + } + } + + @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) 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 @@ + + + + + - - - - - + + + +