Преглед изворни кода

[skip ci] Merge branch 'main' into develop

Armin Schrenk пре 2 месеци
родитељ
комит
08e9f130e4

+ 3 - 0
dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml

@@ -83,6 +83,9 @@
 	</content_rating>
 
 	<releases>
+		<release date="2025-04-29" version="1.16.0">
+			<url type="details">https://github.com/cryptomator/cryptomator/releases/1.16.0</url>
+		</release>
 		<release date="2025-04-09" version="1.15.3">
 			<url type="details">https://github.com/cryptomator/cryptomator/releases/1.15.3</url>
 		</release>

+ 1 - 1
pom.xml

@@ -3,7 +3,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>org.cryptomator</groupId>
 	<artifactId>cryptomator</artifactId>
-	<version>1.16.0-SNAPSHOT</version>
+	<version>1.17.0-SNAPSHOT</version>
 	<name>Cryptomator Desktop App</name>
 
 	<organization>

+ 1 - 0
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;

+ 160 - 0
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
+ * <p>
+ * Use {@link EventMap#put(VaultEvent)} to add an element and {@link EventMap#remove(VaultEvent)} to remove it.
+ * <p>
+ * 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<EventMap.EventKey, VaultEvent> {
+
+	private static final int MAX_SIZE = 300;
+
+	public record EventKey(Path ciphertextPath, Class<? extends FilesystemEvent> c) {}
+
+	private final ObservableMap<EventMap.EventKey, VaultEvent> 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<EventKey> keySet() {
+		return delegate.keySet();
+	}
+
+	@Override
+	public @NotNull Collection<VaultEvent> values() {
+		return delegate.values();
+	}
+
+	@Override
+	public @NotNull Set<Entry<EventKey, VaultEvent>> 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());
+	}
+}

+ 2 - 0
src/main/java/org/cryptomator/common/vaults/Vault.java

@@ -23,6 +23,7 @@ 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;
@@ -34,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;

+ 14 - 0
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();
+		}
+	}
+}

+ 15 - 0
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<NotificationHandler> loadAll() {
+		return IntegrationsLoader.loadAll(ServiceLoader.load(NotificationHandler.class), NotificationHandler.class);
+	}
+}

+ 27 - 0
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<VaultEvent> {
+
+	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);
+	}
+}

+ 14 - 0
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() {
+
+	}
+}

+ 0 - 1
src/main/resources/fxml/vault_detail_unlocked.fxml

@@ -74,7 +74,6 @@
 				<Tooltip text="%main.vaultDetail.decryptName.tooltip"/>
 			</tooltip>
 		</Button>
-
 		<Region HBox.hgrow="ALWAYS"/>
 
 		<Button text="%main.vaultDetail.stats" minWidth="120" onAction="#showVaultStatistics" contentDisplay="BOTTOM" prefHeight="72">

+ 11 - 13
src/main/resources/fxml/vault_list_cell.fxml

@@ -14,17 +14,15 @@
 	  spacing="12"
 	  alignment="CENTER_LEFT">
 	<!-- Remark Check the containing list view for a fixed cell size before editing height properties -->
-	<children>
-		<VBox alignment="CENTER" minWidth="20">
-			<FontAwesome5IconView fx:id="vaultStateView" glyph="${controller.glyph}" HBox.hgrow="NEVER" glyphSize="16"/>
-		</VBox>
-		<VBox spacing="4" HBox.hgrow="ALWAYS">
-			<Label styleClass="header-label" text="${controller.vault.displayName}"/>
-			<Label styleClass="detail-label" text="${controller.vault.displayablePath}" textOverrun="CENTER_ELLIPSIS" visible="${!controller.compactMode}" managed="${!controller.compactMode}">
-				<tooltip>
-					<Tooltip text="${controller.vault.displayablePath}"/>
-				</tooltip>
-			</Label>
-		</VBox>
-	</children>
+	<VBox alignment="CENTER" minWidth="20">
+		<FontAwesome5IconView fx:id="vaultStateView" glyph="${controller.glyph}" HBox.hgrow="NEVER" glyphSize="16"/>
+	</VBox>
+	<VBox spacing="4" HBox.hgrow="ALWAYS">
+		<Label styleClass="header-label" text="${controller.vault.displayName}"/>
+		<Label styleClass="detail-label" text="${controller.vault.displayablePath}" textOverrun="CENTER_ELLIPSIS" visible="${!controller.compactMode}" managed="${!controller.compactMode}">
+			<tooltip>
+				<Tooltip text="${controller.vault.displayablePath}"/>
+			</tooltip>
+		</Label>
+	</VBox>
 </HBox>