- * 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/FilesystemOwnerSupplier.java b/src/main/java/org/cryptomator/common/FilesystemOwnerSupplier.java
new file mode 100644
index 000000000..3e18e1981
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/FilesystemOwnerSupplier.java
@@ -0,0 +1,18 @@
+package org.cryptomator.common;
+
+import java.util.function.Supplier;
+
+/**
+ * Interface marking a class to be used in {@link org.cryptomator.cryptofs.CryptoFileSystemProperties.Builder#withOwnerGetter(Supplier)}.
+ */
+@FunctionalInterface
+public interface FilesystemOwnerSupplier {
+
+ /**
+ * Get the filesystem owner.
+ *
+ * @return the filesystem owner
+ */
+ String getOwner();
+
+}
diff --git a/src/main/java/org/cryptomator/common/SemVerComparator.java b/src/main/java/org/cryptomator/common/SemVerComparator.java
deleted file mode 100644
index 0f9148bd5..000000000
--- a/src/main/java/org/cryptomator/common/SemVerComparator.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2016, 2017 Sebastian Stenzel and others.
- * All rights reserved.
- * This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- *******************************************************************************/
-package org.cryptomator.common;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.util.Comparator;
-
-/**
- * Compares version strings according to SemVer 2.0.0.
- */
-public class SemVerComparator implements Comparator {
-
- private static final char VERSION_SEP = '.'; // http://semver.org/spec/v2.0.0.html#spec-item-2
- private static final String PRE_RELEASE_SEP = "-"; // http://semver.org/spec/v2.0.0.html#spec-item-9
- private static final String BUILD_SEP = "+"; // http://semver.org/spec/v2.0.0.html#spec-item-10
-
- @Override
- public int compare(String version1, String version2) {
- // "Build metadata SHOULD be ignored when determining version precedence.
- // Thus two versions that differ only in the build metadata, have the same precedence."
- String v1WithoutBuildMetadata = StringUtils.substringBefore(version1, BUILD_SEP);
- String v2WithoutBuildMetadata = StringUtils.substringBefore(version2, BUILD_SEP);
-
- if (v1WithoutBuildMetadata.equals(v2WithoutBuildMetadata)) {
- return 0;
- }
-
- String v1MajorMinorPatch = StringUtils.substringBefore(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
- String v2MajorMinorPatch = StringUtils.substringBefore(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
- String v1PreReleaseVersion = StringUtils.substringAfter(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
- String v2PreReleaseVersion = StringUtils.substringAfter(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
- return compare(v1MajorMinorPatch, v1PreReleaseVersion, v2MajorMinorPatch, v2PreReleaseVersion);
- }
-
- private int compare(String v1MajorMinorPatch, String v1PreReleaseVersion, String v2MajorMinorPatch, String v2PreReleaseVersion) {
- int comparisonResult = compareNumericallyThenLexicographically(v1MajorMinorPatch, v2MajorMinorPatch);
- if (comparisonResult == 0) {
- if (v1PreReleaseVersion.isEmpty()) {
- return 1; // 1.0.0 > 1.0.0-BETA
- } else if (v2PreReleaseVersion.isEmpty()) {
- return -1; // 1.0.0-BETA < 1.0.0
- } else {
- return compareNumericallyThenLexicographically(v1PreReleaseVersion, v2PreReleaseVersion);
- }
- } else {
- return comparisonResult;
- }
- }
-
- private int compareNumericallyThenLexicographically(String version1, String version2) {
- final String[] vComps1 = StringUtils.split(version1, VERSION_SEP);
- final String[] vComps2 = StringUtils.split(version2, VERSION_SEP);
- final int commonCompCount = Math.min(vComps1.length, vComps2.length);
-
- for (int i = 0; i < commonCompCount; i++) {
- int subversionComparisonResult = 0;
- try {
- final int v1 = Integer.parseInt(vComps1[i]);
- final int v2 = Integer.parseInt(vComps2[i]);
- subversionComparisonResult = v1 - v2;
- } catch (NumberFormatException ex) {
- // ok, lets compare this fragment lexicographically
- subversionComparisonResult = vComps1[i].compareTo(vComps2[i]);
- }
- if (subversionComparisonResult != 0) {
- return subversionComparisonResult;
- }
- }
-
- // all in common so far? longest version string is considered the higher version:
- return vComps1.length - vComps2.length;
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/common/SubstitutingProperties.java b/src/main/java/org/cryptomator/common/SubstitutingProperties.java
index 0536e3554..3120abde8 100644
--- a/src/main/java/org/cryptomator/common/SubstitutingProperties.java
+++ b/src/main/java/org/cryptomator/common/SubstitutingProperties.java
@@ -1,7 +1,7 @@
package org.cryptomator.common;
import org.jetbrains.annotations.VisibleForTesting;
-import org.slf4j.LoggerFactory;
+import org.slf4j.Logger;
import java.util.Map;
import java.util.Properties;
@@ -13,10 +13,12 @@ public class SubstitutingProperties extends PropertiesDecorator {
private static final Pattern TEMPLATE = Pattern.compile("@\\{(\\w+)}");
private final Map env;
+ private final Logger logger;
- public SubstitutingProperties(Properties props, Map systemEnvironment) {
+ public SubstitutingProperties(Properties props, Map systemEnvironment, Logger logger) {
super(props);
this.env = systemEnvironment;
+ this.logger = logger;
}
@Override
@@ -44,7 +46,7 @@ public class SubstitutingProperties extends PropertiesDecorator {
case "localappdata" -> resolveFrom("LOCALAPPDATA", Source.ENV);
case "userhome" -> resolveFrom("user.home", Source.PROPS);
default -> {
- LoggerFactory.getLogger(SubstitutingProperties.class).warn("Unknown variable {} in property value {}.", match.group(), value);
+ logger.warn("Unknown variable {} in property value {}.", match.group(), value);
yield match.group();
}
});
@@ -56,7 +58,7 @@ public class SubstitutingProperties extends PropertiesDecorator {
case PROPS -> delegate.getProperty(key);
};
if (val == null) {
- LoggerFactory.getLogger(SubstitutingProperties.class).warn("Variable {} used for substitution not found in {}. Replaced with empty string.", key, src);
+ logger.warn("Variable {} used for substitution not found in {}. Replaced with empty string.", key, src);
return "";
} else {
return Matcher.quoteReplacement(val);
diff --git a/src/main/java/org/cryptomator/common/mount/Mounter.java b/src/main/java/org/cryptomator/common/mount/Mounter.java
index 89f8fb782..def53cb9a 100644
--- a/src/main/java/org/cryptomator/common/mount/Mounter.java
+++ b/src/main/java/org/cryptomator/common/mount/Mounter.java
@@ -167,6 +167,7 @@ public class Mounter {
usedMountServices.add(mountService);
var builder = mountService.forFileSystem(cryptoFsRoot);
+ LOG.debug("Using mount service {} for mounting vault {}", mountService.getClass().getName(), vaultSettings.displayName);
var internal = new SettledMounter(mountService, builder, vaultSettings); // FIXME: no need for an inner class
var cleanup = internal.prepare();
return new MountHandle(builder.mount(), mountService.hasCapability(UNMOUNT_FORCED), cleanup);
diff --git a/src/main/java/org/cryptomator/common/recovery/MasterkeyService.java b/src/main/java/org/cryptomator/common/recovery/MasterkeyService.java
index 7d487ec54..dd40e519f 100644
--- a/src/main/java/org/cryptomator/common/recovery/MasterkeyService.java
+++ b/src/main/java/org/cryptomator/common/recovery/MasterkeyService.java
@@ -61,6 +61,7 @@ public final class MasterkeyService {
Optional c9rFile = paths //
.filter(p -> p.toString().endsWith(".c9r")) //
.filter(p -> !p.endsWith("dir.c9r")) //
+ .filter(Files::isRegularFile) //
.findFirst();
if (c9rFile.isEmpty()) {
LOG.info("Unable to detect Crypto scheme: No *.c9r file found in {}", vaultPath);
diff --git a/src/main/java/org/cryptomator/common/settings/Settings.java b/src/main/java/org/cryptomator/common/settings/Settings.java
index a711e6536..c30f606a3 100644
--- a/src/main/java/org/cryptomator/common/settings/Settings.java
+++ b/src/main/java/org/cryptomator/common/settings/Settings.java
@@ -25,10 +25,8 @@ import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.NodeOrientation;
-
import java.nio.file.Path;
import java.time.Instant;
-import java.util.function.Consumer;
public class Settings {
@@ -53,6 +51,7 @@ public class Settings {
static final String DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT.name();
public static final Instant DEFAULT_TIMESTAMP = Instant.parse("2000-01-01T00:00:00Z");
+ private final SettingsProvider provider;
public final ObservableList directories;
public final BooleanProperty startHidden;
public final BooleanProperty autoCloseVaults;
@@ -78,13 +77,12 @@ public class Settings {
public final ObjectProperty lastUpdateCheckReminder;
public final ObjectProperty lastSuccessfulUpdateCheck;
public final ObjectProperty previouslyUsedVaultDirectory;
+ public final StringProperty lastUpdateAttemptedByVersion;
- private Consumer saveCmd;
-
- public static Settings create(Environment env) {
+ public static Settings create(SettingsProvider provider, Environment env) {
var defaults = new SettingsJson();
defaults.showTrayIcon = env.showTrayIcon();
- return new Settings(defaults);
+ return new Settings(provider, defaults);
}
/**
@@ -92,7 +90,8 @@ public class Settings {
*
* @param json The parsed settings.json
*/
- Settings(SettingsJson json) {
+ Settings(SettingsProvider provider, SettingsJson json) {
+ this.provider = provider;
this.directories = FXCollections.observableArrayList(VaultSettings::observables);
this.startHidden = new SimpleBooleanProperty(this, "startHidden", json.startHidden);
this.autoCloseVaults = new SimpleBooleanProperty(this, "autoCloseVaults", json.autoCloseVaults);
@@ -118,6 +117,7 @@ public class Settings {
this.lastUpdateCheckReminder = new SimpleObjectProperty<>(this, "lastUpdateCheckReminder", json.lastReminderForUpdateCheck);
this.lastSuccessfulUpdateCheck = new SimpleObjectProperty<>(this, "lastSuccessfulUpdateCheck", json.lastSuccessfulUpdateCheck);
this.previouslyUsedVaultDirectory = new SimpleObjectProperty<>(this, "previouslyUsedVaultDirectory", json.previouslyUsedVaultDirectory);
+ this.lastUpdateAttemptedByVersion = new SimpleStringProperty(this, "lastUpdateAttemptedByVersion", json.lastUpdateAttemptedByVersion);
this.directories.addAll(json.directories.stream().map(VaultSettings::new).toList());
@@ -148,15 +148,11 @@ public class Settings {
lastUpdateCheckReminder.addListener(this::somethingChanged);
lastSuccessfulUpdateCheck.addListener(this::somethingChanged);
previouslyUsedVaultDirectory.addListener(this::somethingChanged);
+ lastUpdateAttemptedByVersion.addListener(this::somethingChanged);
}
@SuppressWarnings("deprecation")
private void migrateLegacySettings(SettingsJson json) {
- // migrate renamed keychainAccess
- if(this.keychainProvider.getValueSafe().equals("org.cryptomator.linux.keychain.SecretServiceKeychainAccess")) {
- this.keychainProvider.setValue("org.cryptomator.linux.keychain.GnomeKeyringKeychainAccess");
- }
-
// implicit migration of 1.6.x legacy setting "preferredVolumeImpl":
if (this.mountService.get() == null && json.preferredVolumeImpl != null) {
this.mountService.set(switch (json.preferredVolumeImpl) {
@@ -210,6 +206,7 @@ public class Settings {
json.lastReminderForUpdateCheck = lastUpdateCheckReminder.get();
json.lastSuccessfulUpdateCheck = lastSuccessfulUpdateCheck.get();
json.previouslyUsedVaultDirectory = previouslyUsedVaultDirectory.get();
+ json.lastUpdateAttemptedByVersion = lastUpdateAttemptedByVersion.get();
return json;
}
@@ -222,20 +219,12 @@ public class Settings {
}
}
-
- // TODO rename to setChangeListener
- void setSaveCmd(Consumer saveCmd) {
- this.saveCmd = saveCmd;
- }
-
private void somethingChanged(@SuppressWarnings("unused") Observable observable) {
- this.save();
+ provider.scheduleSave(this);
}
- void save() {
- if (saveCmd != null) {
- saveCmd.accept(this);
- }
+ public void saveNow() {
+ provider.saveNow(this);
}
}
diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJson.java b/src/main/java/org/cryptomator/common/settings/SettingsJson.java
index feb8a0bf2..e0cdb7b5e 100644
--- a/src/main/java/org/cryptomator/common/settings/SettingsJson.java
+++ b/src/main/java/org/cryptomator/common/settings/SettingsJson.java
@@ -96,4 +96,7 @@ class SettingsJson {
@JsonProperty("previouslyUsedVaultDirectory")
Path previouslyUsedVaultDirectory;
+
+ @JsonProperty("lastUpdateAttemptedByVersion")
+ String lastUpdateAttemptedByVersion;
}
diff --git a/src/main/java/org/cryptomator/common/settings/SettingsProvider.java b/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
index d9fab7108..0a4a0f190 100644
--- a/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
+++ b/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
@@ -26,7 +26,9 @@ import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
-import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -61,8 +63,7 @@ public class SettingsProvider implements Supplier {
Settings settings = env.getSettingsPath() //
.flatMap(this::tryLoad) //
.findFirst() //
- .orElseGet(() -> Settings.create(env));
- settings.setSaveCmd(this::scheduleSave);
+ .orElseGet(() -> Settings.create(this, env));
return settings;
}
@@ -71,7 +72,7 @@ public class SettingsProvider implements Supplier {
try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) {
var json = JSON.reader().readValue(in, SettingsJson.class);
LOG.info("Settings loaded from {}", path);
- var settings = new Settings(json);
+ var settings = new Settings(this, json);
return Stream.of(settings);
} catch (JacksonException e) {
LOG.warn("Failed to parse json file {}", path, e);
@@ -84,19 +85,33 @@ public class SettingsProvider implements Supplier {
}
}
- private void scheduleSave(Settings settings) {
- if (settings == null) {
- return;
+ void saveNow(Settings settings) {
+ try {
+ scheduleSave(settings, 0L).get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOG.error("Saving settings was interrupted.", e);
+ } catch (ExecutionException e) {
+ LOG.error("Unexpected exception while saving.", e);
}
- final Optional settingsPath = env.getSettingsPath().findFirst(); // always save to preferred (first) path
- settingsPath.ifPresent(path -> {
- Runnable saveCommand = () -> this.save(settings, path);
- ScheduledFuture> scheduledTask = scheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
- ScheduledFuture> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
- if (previouslyScheduledTask != null) {
- previouslyScheduledTask.cancel(false);
- }
- });
+ }
+
+ void scheduleSave(Settings settings) {
+ scheduleSave(settings, SAVE_DELAY_MS);
+ }
+
+ private Future> scheduleSave(Settings settings, long delayMillis) {
+ if (settings == null) {
+ return CompletableFuture.completedFuture(null);
+ }
+ final Path settingsPath = env.getSettingsPath().findFirst().orElseThrow(); // always save to preferred (first) path
+ Runnable saveCommand = () -> this.save(settings, settingsPath);
+ ScheduledFuture> scheduledTask = scheduler.schedule(saveCommand, delayMillis, TimeUnit.MILLISECONDS);
+ ScheduledFuture> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
+ if (previouslyScheduledTask != null) {
+ previouslyScheduledTask.cancel(false);
+ }
+ return scheduledTask;
}
private void save(Settings settings, Path settingsPath) {
@@ -107,7 +122,7 @@ public class SettingsProvider implements Supplier {
Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp");
try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
var jsonObj = settings.serialized();
- jsonObj.writtenByVersion = env.getAppVersion() + env.getBuildNumber().map("-"::concat).orElse("");
+ jsonObj.writtenByVersion = env.getAppVersionWithBuildNumber();
JSON.writerWithDefaultPrettyPrinter().writeValue(out, jsonObj);
}
Files.move(tmpPath, settingsPath, StandardCopyOption.REPLACE_EXISTING);
diff --git a/src/main/java/org/cryptomator/common/settings/UiTheme.java b/src/main/java/org/cryptomator/common/settings/UiTheme.java
index 9c3153060..a1c510f5a 100644
--- a/src/main/java/org/cryptomator/common/settings/UiTheme.java
+++ b/src/main/java/org/cryptomator/common/settings/UiTheme.java
@@ -10,14 +10,6 @@ public enum UiTheme {
DARK("preferences.interface.theme.dark"), //
AUTOMATIC("preferences.interface.theme.automatic");
- public static UiTheme[] applicableValues() {
- if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS) {
- return values();
- } else {
- return new UiTheme[]{LIGHT, DARK};
- }
- }
-
private final String displayName;
UiTheme(String displayName) {
diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java
index 2fa613e3e..64b763aeb 100644
--- a/src/main/java/org/cryptomator/common/vaults/Vault.java
+++ b/src/main/java/org/cryptomator/common/vaults/Vault.java
@@ -10,7 +10,7 @@ package org.cryptomator.common.vaults;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Constants;
-import org.cryptomator.event.FileSystemEventAggregator;
+import org.cryptomator.common.FilesystemOwnerSupplier;
import org.cryptomator.common.mount.Mounter;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
@@ -23,6 +23,8 @@ 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.FileSystemEventAggregator;
+import org.cryptomator.event.NotificationManager;
import org.cryptomator.integrations.mount.MountFailedException;
import org.cryptomator.integrations.mount.Mountpoint;
import org.cryptomator.integrations.mount.UnmountFailedException;
@@ -78,6 +80,7 @@ public class Vault {
private final Mounter mounter;
private final Settings settings;
private final FileSystemEventAggregator fileSystemEventAggregator;
+ private final NotificationManager notificationManager;
private final BooleanProperty showingStats;
private final AtomicReference mountHandle = new AtomicReference<>(null);
@@ -90,7 +93,8 @@ public class Vault {
@Named("lastKnownException") ObjectProperty lastKnownException, //
VaultStats stats, //
Mounter mounter, Settings settings, //
- FileSystemEventAggregator fileSystemEventAggregator) {
+ FileSystemEventAggregator fileSystemEventAggregator, //
+ NotificationManager notificationManager) {
this.vaultSettings = vaultSettings;
this.configCache = configCache;
this.cryptoFileSystem = cryptoFileSystem;
@@ -109,6 +113,7 @@ public class Vault {
this.mounter = mounter;
this.settings = settings;
this.fileSystemEventAggregator = fileSystemEventAggregator;
+ this.notificationManager = notificationManager;
this.showingStats = new SimpleBooleanProperty(false);
this.quickAccessEntry = new AtomicReference<>(null);
}
@@ -145,14 +150,17 @@ public class Vault {
LOG.warn("Limiting cleartext filename length on this device to {}.", vaultSettings.maxCleartextFilenameLength.get());
}
- CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
+ var fsPropsBuilder = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withKeyLoader(keyLoader) //
.withFlags(flags) //
.withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength.get()) //
.withVaultConfigFilename(Constants.VAULTCONFIG_FILENAME) //
- .withFilesystemEventConsumer(this::consumeVaultEvent) //
- .build();
- return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
+ .withFilesystemEventConsumer(this::consumeVaultEvent);
+ if (keyLoader instanceof FilesystemOwnerSupplier oo) {
+ fsPropsBuilder.withOwnerGetter(oo::getOwner);
+ }
+
+ return CryptoFileSystemProvider.newFileSystem(getPath(), fsPropsBuilder.build());
}
private void destroyCryptoFileSystem() {
@@ -262,6 +270,7 @@ public class Vault {
private void consumeVaultEvent(FilesystemEvent e) {
fileSystemEventAggregator.put(this, e);
+ notificationManager.offer(this, e);
}
// ******************************************************************************
diff --git a/src/main/java/org/cryptomator/event/FileSystemEventAggregator.java b/src/main/java/org/cryptomator/event/FileSystemEventAggregator.java
index c871436fd..b27238cee 100644
--- a/src/main/java/org/cryptomator/event/FileSystemEventAggregator.java
+++ b/src/main/java/org/cryptomator/event/FileSystemEventAggregator.java
@@ -6,6 +6,7 @@ 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.FileIsInUseEvent;
import org.cryptomator.cryptofs.event.FilesystemEvent;
import javax.inject.Inject;
@@ -101,6 +102,7 @@ public class FileSystemEventAggregator {
case ConflictResolutionFailedEvent(_, _, Path conflictingCiphertext, _) -> conflictingCiphertext;
case BrokenDirFileEvent(_, Path ciphertext) -> ciphertext;
case BrokenFileNodeEvent(_, _, Path ciphertext) -> ciphertext;
+ case FileIsInUseEvent(_, _, Path ciphertext, _, _, _) -> ciphertext;
};
return new FSEventBucket(v, p, event.getClass());
}
diff --git a/src/main/java/org/cryptomator/event/NotificationManager.java b/src/main/java/org/cryptomator/event/NotificationManager.java
new file mode 100644
index 000000000..7cd5ae1e1
--- /dev/null
+++ b/src/main/java/org/cryptomator/event/NotificationManager.java
@@ -0,0 +1,85 @@
+package org.cryptomator.event;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptofs.event.FileIsInUseEvent;
+import org.cryptomator.cryptofs.event.FilesystemEvent;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Manager for notifications.
+ *
+ * To add (filesystem) events, use method {@link #offer(Vault, FilesystemEvent)}. If the input event is eligible, it is added to an internal queue.
+ * An event is eligible, if
+ *
+ *
the event should trigger a notification and
+ *
it is not added within the last {@value DEBOUNCE_THRESHOLD_SECONDS} seconds
+ *
+ *
+ * @see org.cryptomator.ui.fxapp.FxNotificationManager
+ */
+@Singleton
+public class NotificationManager {
+
+ private static final int DEBOUNCE_THRESHOLD_SECONDS = 5;
+
+ private final Cache debounceCache;
+ private final ConcurrentLinkedQueue pendingEvents;
+
+ @Inject
+ public NotificationManager() {
+ debounceCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(DEBOUNCE_THRESHOLD_SECONDS)).build();
+ pendingEvents = new ConcurrentLinkedQueue<>();
+ }
+
+ /**
+ * Offers the given filesystem event to the notification manager.
+ *
+ * @param v The vault where the filesystem event happened
+ * @param e the actual filesystem event
+ * @return {@code true} if the filesystem event is accepted, otherwise {@code false}.
+ */
+ public boolean offer(Vault v, FilesystemEvent e) {
+ return switch (e) {
+ case FileIsInUseEvent fiiue -> addEvent(v, fiiue.ciphertextPath(), fiiue);
+ default -> false;
+ };
+ }
+
+ boolean addEvent(Vault v, Path keyPath, FilesystemEvent e) {
+ var key = new FSEventBucket(v, keyPath, e.getClass());
+ var isAdded = new AtomicBoolean(false);
+ debounceCache.asMap().computeIfAbsent(key, _ -> {
+ synchronized (this) {
+ pendingEvents.add(new VaultEvent(v, e));
+ isAdded.set(true);
+ }
+ return e;
+ });
+ return isAdded.get();
+ }
+
+ /**
+ * Adds all events to the target list and clears afterward the pending-event-queue
+ *
+ * @param target list where the filesystem events are copied to
+ * @return {@code true}, if elements were copied
+ */
+ public boolean appendToAndClear(List target) {
+ //it is not clear, if addAll iterates thread-safe over the pendingEvents
+ //hence we synchronize moving (copy then clear) and adding-single-element operations
+ synchronized (this) {
+ var result = target.addAll(pendingEvents);
+ pendingEvents.clear();
+ return result;
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/event/VaultEvent.java b/src/main/java/org/cryptomator/event/VaultEvent.java
index 8b31747cf..1095c812b 100644
--- a/src/main/java/org/cryptomator/event/VaultEvent.java
+++ b/src/main/java/org/cryptomator/event/VaultEvent.java
@@ -3,25 +3,6 @@ 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) {
-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/launcher/AdminPropertiesFactory.java b/src/main/java/org/cryptomator/launcher/AdminPropertiesFactory.java
new file mode 100644
index 000000000..6a77f31cd
--- /dev/null
+++ b/src/main/java/org/cryptomator/launcher/AdminPropertiesFactory.java
@@ -0,0 +1,97 @@
+package org.cryptomator.launcher;
+
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * Factory to generate admin properties.
+ *
+ *
+ * Admin properties are {@link Properties} using system properties as defaults, but allow overwriting a specific set of properties with an external config file.
+ * Those properties are created by calling {@link #create()}. The method first reads system property {@value #ADMIN_PROP_FILE_KEY}. If it contains a path to a valid properties file, all overridable properties from the file are loaded into the returned admin properties.
+ *
+ * The overridable properties are:
+ *
+ *
cryptomator.logDir
+ *
cryptomator.pluginDir
+ *
cryptomator.p12Path
+ *
cryptomator.mountPointsDir
+ *
cryptomator.disableUpdateCheck
+ *
+ *
+ * @see Properties
+ * @see System#getProperties()
+ */
+class AdminPropertiesFactory {
+
+ private static final Logger LOG = EventualLogger.INSTANCE;
+ private static final long MAX_CONFIG_SIZE_BYTES = 8192;
+ private static final String ADMIN_PROP_FILE_KEY = "cryptomator.adminConfigPath";
+ private static final Set ALLOWED_OVERRIDES = Set.of( //
+ "cryptomator.logDir", //
+ "cryptomator.pluginDir", //
+ "cryptomator.p12Path", //
+ "cryptomator.mountPointsDir", //
+ "cryptomator.disableUpdateCheck");
+
+
+ /**
+ * Creates new {@link Properties} containing overridable properties from the admin config.
+ *
+ * The returned properties object uses as default the {@link System} properties.
+ * For a list of overridable properties, see {@link AdminPropertiesFactory}
+ *
+ * @return {@link Properties} containing overridable properties from the admin config and defaulting to system properties.
+ */
+ static Properties create() {
+ var systemProps = System.getProperties();
+ var adminProps = new Properties(systemProps);
+
+ final String adminCfgPath = System.getProperty(ADMIN_PROP_FILE_KEY);
+ if (adminCfgPath == null) {
+ LOG.debug("Admin config property is not defined. Skipping.");
+ return adminProps;
+ }
+ var propsFromFile = loadPropertiesFromFile(Path.of(adminCfgPath));
+
+ for (var key : propsFromFile.stringPropertyNames()) {
+ if (ALLOWED_OVERRIDES.contains(key)) {
+ var value = propsFromFile.getProperty(key);
+ LOG.info("Overwriting {} with value {} from admin config.", key, value);
+ adminProps.setProperty(key, value);
+ } else {
+ LOG.debug("Property {} in admin config is not supported for override.", key);
+ }
+ }
+ return adminProps;
+ }
+
+ //visible for testing
+ static Properties loadPropertiesFromFile(Path adminPropertiesPath) {
+ var adminProps = new Properties();
+ try (FileChannel ch = FileChannel.open(adminPropertiesPath, StandardOpenOption.READ); //
+ Reader reader = Channels.newReader(ch, StandardCharsets.UTF_8)) {
+ if (ch.size() > MAX_CONFIG_SIZE_BYTES) {
+ throw new IOException("Config file %s exceeds maximum size of %d".formatted(adminPropertiesPath, MAX_CONFIG_SIZE_BYTES));
+ }
+ adminProps.load(reader);
+ } catch (NoSuchFileException _) {
+ //NO-OP
+ LOG.debug("No admin properties found at {}.", adminPropertiesPath);
+ } catch (IOException | IllegalArgumentException e) {
+ LOG.warn("Failed to read administrative properties from {}. Returning empty properties.", adminPropertiesPath, e);
+ }
+ return adminProps;
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/launcher/Cryptomator.java b/src/main/java/org/cryptomator/launcher/Cryptomator.java
index 3e0a613ca..d12438e37 100644
--- a/src/main/java/org/cryptomator/launcher/Cryptomator.java
+++ b/src/main/java/org/cryptomator/launcher/Cryptomator.java
@@ -11,9 +11,9 @@ import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.SubstitutingProperties;
-import org.cryptomator.networking.SSLContextProvider;
import org.cryptomator.ipc.IpcCommunicator;
import org.cryptomator.logging.DebugMode;
+import org.cryptomator.networking.SSLContextProvider;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,7 +35,8 @@ public class Cryptomator {
private static final long STARTUP_TIME = System.currentTimeMillis();
static {
- var lazyProcessedProps = new SubstitutingProperties(System.getProperties(), System.getenv());
+ var adminProps = AdminPropertiesFactory.create();
+ var lazyProcessedProps = new SubstitutingProperties(adminProps, System.getenv(), EventualLogger.INSTANCE);
System.setProperties(lazyProcessedProps);
CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.factory().create(STARTUP_TIME);
LOG = LoggerFactory.getLogger(Cryptomator.class);
@@ -89,10 +90,11 @@ public class Cryptomator {
* @return Nonzero exit code in case of an error.
*/
private int run(String[] args) {
+ debugMode.initialize();
+ EventualLogger.INSTANCE.drainTo(LOG);
env.log();
LOG.debug("Dagger graph initialized after {}ms", System.currentTimeMillis() - STARTUP_TIME);
LOG.info("Starting Cryptomator {} on {} {} ({})", env.getAppVersion(), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
- debugMode.initialize();
supportedLanguages.applyPreferred();
changeDefaultSSLContext();
/*
diff --git a/src/main/java/org/cryptomator/launcher/CryptomatorModule.java b/src/main/java/org/cryptomator/launcher/CryptomatorModule.java
index 42e908df2..5ff8d63ee 100644
--- a/src/main/java/org/cryptomator/launcher/CryptomatorModule.java
+++ b/src/main/java/org/cryptomator/launcher/CryptomatorModule.java
@@ -4,7 +4,6 @@ import dagger.Module;
import dagger.Provides;
import org.cryptomator.integrations.autostart.AutoStartProvider;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
-import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import javax.inject.Named;
@@ -30,11 +29,6 @@ class CryptomatorModule {
return new ArrayBlockingQueue<>(10);
}
- @Provides
- @Singleton
- static Optional provideAppearanceProvider() {
- return UiAppearanceProvider.get();
- }
@Provides
@Singleton
diff --git a/src/main/java/org/cryptomator/launcher/EventualLogger.java b/src/main/java/org/cryptomator/launcher/EventualLogger.java
new file mode 100644
index 000000000..c14e5070b
--- /dev/null
+++ b/src/main/java/org/cryptomator/launcher/EventualLogger.java
@@ -0,0 +1,106 @@
+package org.cryptomator.launcher;
+
+import org.slf4j.Logger;
+import org.slf4j.Marker;
+import org.slf4j.event.DefaultLoggingEvent;
+import org.slf4j.event.Level;
+import org.slf4j.event.LoggingEvent;
+import org.slf4j.helpers.AbstractLogger;
+
+import java.util.ArrayDeque;
+import java.util.List;
+import java.util.Objects;
+import java.util.Queue;
+
+class EventualLogger extends AbstractLogger {
+
+ static final EventualLogger INSTANCE = new EventualLogger();
+
+ private final Queue bufferedEvents = new ArrayDeque<>();
+
+ private EventualLogger() {
+ }
+
+ synchronized void drainTo(Logger gutter) {
+ for (var event : bufferedEvents) {
+ var builder = gutter.atLevel(event.getLevel()) //
+ .setCause(event.getThrowable()) //
+ .setMessage(event.getMessage());
+ Objects.requireNonNullElse(event.getArguments(), List.of()).forEach(builder::addArgument);
+ Objects.requireNonNullElse(event.getMarkers(), List.of()).forEach(builder::addMarker);
+ builder.log();
+ }
+ bufferedEvents.clear();
+ }
+
+ @Override
+ protected synchronized void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments, Throwable throwable) {
+ var event = new DefaultLoggingEvent(level, this);
+ if (marker != null) {
+ event.addMarker(marker);
+ }
+ event.setMessage(messagePattern);
+ for (var arg : Objects.requireNonNullElse(arguments, new Object[]{})) {
+ event.addArgument(arg);
+ }
+ event.setThrowable(throwable);
+ bufferedEvents.add(event);
+ }
+
+ //Unclear, unused and undocumented method of slf4j, see also https://github.com/qos-ch/slf4j/discussions/348
+ @Override
+ protected String getFullyQualifiedCallerName() {
+ return getClass().getCanonicalName();
+ }
+
+
+ @Override
+ public boolean isTraceEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isTraceEnabled(Marker marker) {
+ return true;
+ }
+
+ @Override
+ public boolean isDebugEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isDebugEnabled(Marker marker) {
+ return true;
+ }
+
+ @Override
+ public boolean isInfoEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isInfoEnabled(Marker marker) {
+ return true;
+ }
+
+ @Override
+ public boolean isWarnEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isWarnEnabled(Marker marker) {
+ return true;
+ }
+
+ @Override
+ public boolean isErrorEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isErrorEnabled(Marker marker) {
+ return true;
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java
index be9ea15c7..7862c3b20 100644
--- a/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java
+++ b/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java
@@ -59,7 +59,7 @@ public class ChooseExistingVaultController implements FxController {
this.vault = vault;
this.vaultListManager = vaultListManager;
this.resourceBundle = resourceBundle;
- this.screenshot = applicationStyle.appliedThemeProperty().map(this::selectScreenshot);
+ this.screenshot = applicationStyle.appliedAppThemeProperty().map(this::selectScreenshot);
}
private Image selectScreenshot(Theme theme) {
diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
index 68607808d..85a64f293 100644
--- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java
+++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
@@ -38,6 +38,7 @@ public enum FxmlFile {
MIGRATION_RUN("/fxml/migration_run.fxml"), //
MIGRATION_START("/fxml/migration_start.fxml"), //
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
+ NOTIFICATION("/fxml/notification.fxml"), //
PREFERENCES("/fxml/preferences.fxml"), //
QUIT("/fxml/quit.fxml"), //
QUIT_FORCED("/fxml/quit_forced.fxml"), //
@@ -59,6 +60,7 @@ public enum FxmlFile {
VAULT_STATISTICS("/fxml/stats.fxml"), //
WRONGFILEALERT("/fxml/wrongfilealert.fxml");
+
private final String ressourcePathString;
FxmlFile(String ressourcePathString) {
diff --git a/src/main/java/org/cryptomator/ui/common/SystemBarUtil.java b/src/main/java/org/cryptomator/ui/common/SystemBarUtil.java
new file mode 100644
index 000000000..9faaa60db
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/common/SystemBarUtil.java
@@ -0,0 +1,56 @@
+package org.cryptomator.ui.common;
+
+import javafx.stage.Screen;
+
+/**
+ * Utility class providing methods regarding the OS bar.
+ */
+public class SystemBarUtil {
+
+ public enum Placement {
+ /**
+ * OS Bar placed at the left screen edge
+ */
+ LEFT,
+ /**
+ * OS Bar placed at the top screen edge
+ */
+ TOP,
+ /**
+ * OS Bar placed at the right screen edge
+ */
+ RIGHT,
+ /**
+ * OS Bar placed at the bottom screen edge
+ */
+ BOTTOM;
+ }
+
+ /**
+ * Determines the placement of the OS bar on the given screen.
+ *
+ * Assuming the OS bar fills one screen edge completely,
+ * this method determines that screen edge by comparing the actual screen bounds with the visual ones.
+ *
+ * If the screen does not have a system bar, the bottom placement is returned.
+ * If the screen does have multiple system bars, the first in following priority is returned:
+ * LEFT, TOP, RIGHT, BOTTOM.
+ *
+ * @param screen a {@link Screen} where an OS bar exists
+ * @return {@link Placement} indicating the screen edge.
+ */
+ public static Placement getPlacementOfSystembar(Screen screen) {
+ var bounds = screen.getBounds();
+ var vBounds = screen.getVisualBounds();
+ //assumption: the system bar fills a whole screen side
+ if (bounds.getMinX() != vBounds.getMinX()) {
+ return Placement.LEFT;
+ } else if (bounds.getMinY() != vBounds.getMinY()) {
+ return Placement.TOP;
+ } else if (bounds.getMaxX() != vBounds.getMaxX()) {
+ return Placement.RIGHT;
+ } else {
+ return Placement.BOTTOM;
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/common/VaultService.java b/src/main/java/org/cryptomator/ui/common/VaultService.java
index 95129f6ee..d02064caa 100644
--- a/src/main/java/org/cryptomator/ui/common/VaultService.java
+++ b/src/main/java/org/cryptomator/ui/common/VaultService.java
@@ -1,17 +1,16 @@
package org.cryptomator.ui.common;
-import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.integrations.mount.Mountpoint;
import org.cryptomator.integrations.mount.UnmountFailedException;
+import org.cryptomator.integrations.revealpath.RevealFailedException;
+import org.cryptomator.integrations.revealpath.RevealPathService;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
-import javafx.application.Application;
-import javafx.application.HostServices;
import javafx.concurrent.Task;
import javafx.stage.Stage;
import java.io.IOException;
@@ -28,12 +27,12 @@ public class VaultService {
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
- private final Lazy application;
+ private final RevealPathService revealPathService;
private final ExecutorService executorService;
@Inject
- public VaultService(Lazy application, ExecutorService executorService) {
- this.application = application;
+ public VaultService(RevealPathService revealPathService, ExecutorService executorService) {
+ this.revealPathService = revealPathService;
this.executorService = executorService;
}
@@ -47,9 +46,9 @@ public class VaultService {
* @param vault The vault to reveal
*/
public Task createRevealTask(Vault vault) {
- Task task = new RevealVaultTask(vault, application.get().getHostServices());
- task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName()));
- task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException()));
+ Task task = new RevealVaultTask(vault, revealPathService);
+ task.setOnSucceeded(_ -> LOG.info("Revealed {}", vault.getDisplayName()));
+ task.setOnFailed(evt -> LOG.warn("Failed to reveal {}", vault.getDisplayName(), evt.getSource().getException()));
return task;
}
@@ -110,19 +109,18 @@ public class VaultService {
private static class RevealVaultTask extends Task {
private final Vault vault;
- private final HostServices hostServices;
+ private final RevealPathService rs;
- public RevealVaultTask(Vault vault, HostServices hostServices) {
+ public RevealVaultTask(Vault vault, RevealPathService revealPathService) {
this.vault = vault;
- this.hostServices = hostServices;
- setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException()));
+ this.rs = revealPathService;
}
@Override
- protected Vault call() {
+ protected Vault call() throws RevealFailedException {
switch (vault.getMountPoint()) {
case null -> LOG.warn("Not currently mounted");
- case Mountpoint.WithPath m -> hostServices.showDocument(m.uri().toString());
+ case Mountpoint.WithPath m -> rs.reveal(m.path());
case Mountpoint.WithUri m -> LOG.info("Vault mounted at {}", m.uri()); // TODO show in UI?
}
return vault;
diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
index 348b3a26b..ca1793e6d 100644
--- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
+++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
@@ -12,6 +12,8 @@ public enum FontAwesome5Icon {
CARET_DOWN("\uF0D7"), //
CARET_RIGHT("\uF0Da"), //
CHECK("\uF00C"), //
+ CHEVRON_LEFT("\uF053"), //
+ CHEVRON_RIGHT("\uF054"), //
CLOCK("\uF017"), //
CLIPBOARD("\uF328"), //
COG("\uF013"), //
@@ -60,6 +62,7 @@ public enum FontAwesome5Icon {
TRASH("\uF1F8"), //
UNLINK("\uf127"), //
USER_COG("\uf4fe"), //
+ USER_LOCK("\uf502"), //
WRENCH("\uF0AD"), //
WINDOW_MINIMIZE("\uF2D1"), //
;
diff --git a/src/main/java/org/cryptomator/ui/controls/NotificationBar.java b/src/main/java/org/cryptomator/ui/controls/InfoBar.java
similarity index 67%
rename from src/main/java/org/cryptomator/ui/controls/NotificationBar.java
rename to src/main/java/org/cryptomator/ui/controls/InfoBar.java
index 64a08f220..02efa00f5 100644
--- a/src/main/java/org/cryptomator/ui/controls/NotificationBar.java
+++ b/src/main/java/org/cryptomator/ui/controls/InfoBar.java
@@ -4,24 +4,27 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
+import javafx.scene.AccessibleRole;
import javafx.scene.control.Button;
+import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
+import java.util.ResourceBundle;
-public class NotificationBar extends HBox {
+public class InfoBar extends HBox {
@FXML
- private Label notificationLabel;
+ private Label infoMessage;
private final BooleanProperty dismissable = new SimpleBooleanProperty();
private final BooleanProperty notify = new SimpleBooleanProperty();
- public NotificationBar() {
+ public InfoBar() {
setAlignment(Pos.CENTER);
- setStyle("-fx-alignment: center;");
+ getStyleClass().addAll("info-bar");
Region spacer = new Region();
spacer.setMinWidth(40);
@@ -36,14 +39,21 @@ public class NotificationBar extends HBox {
vbox.setAlignment(Pos.CENTER);
HBox.setHgrow(vbox, javafx.scene.layout.Priority.ALWAYS);
- notificationLabel = new Label();
- notificationLabel.getStyleClass().add("notification-label");
- notificationLabel.setStyle("-fx-alignment: center;");
- vbox.getChildren().add(notificationLabel);
+ infoMessage = new Label();
+ infoMessage.setFocusTraversable(true);
+ infoMessage.setAccessibleRole(AccessibleRole.BUTTON);
+ vbox.getChildren().add(infoMessage);
- Button closeButton = new Button("X");
+ var closeGraphic = new FontAwesome5IconView();
+ closeGraphic.setGlyph(FontAwesome5Icon.TIMES);
+ closeGraphic.setGlyphSize(12);
+ closeGraphic.getStyleClass().add("glyph");
+
+ Button closeButton = new Button();
+ closeButton.setGraphic(closeGraphic);
+ closeButton.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
+ closeButton.setAccessibleText(ResourceBundle.getBundle("i18n.strings").getString("main.notification.closeButton.tooltip"));
closeButton.setMinWidth(40);
- closeButton.setStyle("-fx-background-color: transparent; -fx-text-fill: white; -fx-font-weight: bold;");
closeButton.visibleProperty().bind(dismissable);
closeButton.setOnAction(_ -> {
@@ -61,11 +71,11 @@ public class NotificationBar extends HBox {
}
public String getText() {
- return notificationLabel.getText();
+ return infoMessage.getText();
}
public void setText(String text) {
- notificationLabel.setText(text);
+ infoMessage.setText(text);
}
public void setStyleClass(String styleClass) {
diff --git a/src/main/java/org/cryptomator/ui/decryptname/DecryptFileNamesViewController.java b/src/main/java/org/cryptomator/ui/decryptname/DecryptFileNamesViewController.java
index 453762f55..5450e1d48 100644
--- a/src/main/java/org/cryptomator/ui/decryptname/DecryptFileNamesViewController.java
+++ b/src/main/java/org/cryptomator/ui/decryptname/DecryptFileNamesViewController.java
@@ -194,7 +194,7 @@ public class DecryptFileNamesViewController implements FxController {
}
}
- //obvservable getter
+ //observable getter
public ObservableValue dropZoneTextProperty() {
return dropZoneText;
diff --git a/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java b/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java
index 07ce3c4f6..452acf02a 100644
--- a/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java
+++ b/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java
@@ -61,6 +61,15 @@ public class Dialogs {
.setOkButtonKey(BUTTON_KEY_CLOSE);
}
+ public SimpleDialog.Builder prepareHubVaultArchived(Stage window, Vault vault) {
+ return createDialogBuilder().setOwner(window) //
+ .setTitleKey("unlock.title", vault.getDisplayName()) //
+ .setMessageKey("hub.archived.message") //
+ .setDescriptionKey("hub.archived.description") //
+ .setIcon(FontAwesome5Icon.BAN)//
+ .setOkButtonKey(BUTTON_KEY_CLOSE);
+ }
+
public SimpleDialog.Builder prepareRecoveryVaultAdded(Stage window, String displayName) {
return createDialogBuilder().setOwner(window) //
.setTitleKey("recover.existing.title") //
diff --git a/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java
index 3157f9f40..46bebbdb8 100644
--- a/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java
+++ b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java
@@ -1,16 +1,18 @@
package org.cryptomator.ui.eventview;
-import org.cryptomator.event.FSEventBucket;
-import org.cryptomator.event.FSEventBucketContent;
-import org.cryptomator.event.FileSystemEventAggregator;
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.Constants;
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.cryptofs.event.FileIsInUseEvent;
+import org.cryptomator.event.FSEventBucket;
+import org.cryptomator.event.FSEventBucketContent;
+import org.cryptomator.event.FileSystemEventAggregator;
import org.cryptomator.integrations.revealpath.RevealFailedException;
import org.cryptomator.integrations.revealpath.RevealPathService;
import org.cryptomator.ui.common.FxController;
@@ -43,7 +45,6 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Map;
-import java.util.Optional;
import java.util.ResourceBundle;
import java.util.function.Function;
@@ -54,7 +55,6 @@ public class EventListCellController implements FxController {
private static final DateTimeFormatter LOCAL_TIME_FORMATTER = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault());
private final FileSystemEventAggregator fileSystemEventAggregator;
- @Nullable
private final RevealPathService revealService;
private final ResourceBundle resourceBundle;
private final ObjectProperty> eventEntry;
@@ -79,15 +79,17 @@ public class EventListCellController implements FxController {
Button eventActionsButton;
@Inject
- public EventListCellController(FileSystemEventAggregator fileSystemEventAggregator, Optional revealService, ResourceBundle resourceBundle) {
+ public EventListCellController(FileSystemEventAggregator fileSystemEventAggregator,
+ RevealPathService revealService,
+ ResourceBundle resourceBundle) {
this.fileSystemEventAggregator = fileSystemEventAggregator;
- this.revealService = revealService.orElseGet(() -> null);
+ this.revealService = revealService;
this.resourceBundle = resourceBundle;
this.eventEntry = new SimpleObjectProperty<>(null);
this.eventMessage = new SimpleStringProperty();
this.eventDescription = new SimpleStringProperty();
this.eventIcon = new SimpleObjectProperty<>();
- this.eventCount = ObservableUtil.mapWithDefault(eventEntry, e -> e.getValue().count() == 1? "" : "("+ e.getValue().count() +")", "");
+ this.eventCount = ObservableUtil.mapWithDefault(eventEntry, e -> e.getValue().count() == 1 ? "" : "(" + e.getValue().count() + ")", "");
this.vaultUnlocked = ObservableUtil.mapWithDefault(eventEntry.flatMap(e -> e.getKey().vault().unlockedProperty()), Function.identity(), false);
this.readableTime = ObservableUtil.mapWithDefault(eventEntry, e -> LOCAL_TIME_FORMATTER.format(e.getValue().mostRecentEvent().getTimestamp()), "");
this.readableDate = ObservableUtil.mapWithDefault(eventEntry, e -> LOCAL_DATE_FORMATTER.format(e.getValue().mostRecentEvent().getTimestamp()), "");
@@ -115,7 +117,7 @@ public class EventListCellController implements FxController {
eventActionsMenu.hide();
eventActionsMenu.getItems().clear();
eventTooltip.setText(item.getKey().vault().getDisplayName());
- addAction("generic.action.dismiss", () -> {
+ addLocalizedAction("generic.action.dismiss", () -> {
fileSystemEventAggregator.remove(item.getKey());
});
switch (item.getValue().mostRecentEvent()) {
@@ -124,70 +126,70 @@ public class EventListCellController implements FxController {
case DecryptionFailedEvent fse -> this.adjustToDecryptionFailedEvent(fse);
case BrokenDirFileEvent fse -> this.adjustToBrokenDirFileEvent(fse);
case BrokenFileNodeEvent fse -> this.adjustToBrokenFileNodeEvent(fse);
+ case FileIsInUseEvent fse -> this.adjustToFileInUseEvent(fse);
}
}
+ private void adjustToFileInUseEvent(FileIsInUseEvent fiiue) {
+ eventIcon.setValue(FontAwesome5Icon.USER_LOCK);
+ eventMessage.setValue(resourceBundle.getString("eventView.entry.inUse.message"));
+ var indexFileName = fiiue.cleartextPath().lastIndexOf("/");
+ eventDescription.setValue(fiiue.cleartextPath().substring(indexFileName + 1));
+ addLocalizedAction("eventView.entry.inUse.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(fiiue.cleartextPath())));
+ addLocalizedAction("eventView.entry.inUse.showEncrypted", () -> reveal(revealService, fiiue.ciphertextPath()));
+
+ var userAndDevice = fiiue.owner().split(Constants.HUB_USER_DEVICE_SEPARATOR);
+ var user = userAndDevice[0];
+ var device = userAndDevice.length == 1 ? userAndDevice[0] : userAndDevice[1];
+ addLocalizedAction("eventView.entry.inUse.copyUserAndDevice", () -> copyToClipboard(user + ", " + device));
+ addLocalizedAction("eventView.entry.inUse.ignoreLock", fiiue.ignoreMethod());
+ }
+
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, bfe.ciphertextPath()));
- } else {
- addAction("eventView.entry.brokenFileNode.copyEncrypted", () -> copyToClipboard(bfe.ciphertextPath().toString()));
- }
- addAction("eventView.entry.brokenFileNode.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(bfe.cleartextPath()).toString()));
+ addLocalizedAction("eventView.entry.brokenFileNode.showEncrypted", () -> reveal(revealService, bfe.ciphertextPath()));
+ addLocalizedAction("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()));
- }
+ addLocalizedAction("eventView.entry.conflictResolved.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cre.resolvedCleartextPath())));
}
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()));
- }
+ addLocalizedAction("eventView.entry.conflict.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cfe.canonicalCleartextPath())));
+ addLocalizedAction("eventView.entry.conflict.showEncrypted", () -> reveal(revealService, cfe.conflictingCiphertextPath()));
}
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()));
- }
+ addLocalizedAction("eventView.entry.decryptionFailed.showEncrypted", () -> reveal(revealService, dfe.ciphertextPath()));
}
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()));
- }
+ addLocalizedAction("eventView.entry.brokenDirFile.showEncrypted", () -> reveal(revealService, bde.ciphertextPath()));
}
- private void addAction(String localizationKey, Runnable action) {
- var entry = new MenuItem(resourceBundle.getString(localizationKey));
+ private void addLocalizedAction(String localizationKey, Runnable action) {
+ var entryText = resourceBundle.getString(localizationKey);
+ addAction(entryText, action);
+ }
+
+ private void addAction(String entryText, Runnable action) {
+ var entry = new MenuItem(entryText);
entry.getStyleClass().addLast("dropdown-button-context-menu-item");
entry.setOnAction(_ -> action.run());
eventActionsMenu.getItems().addLast(entry);
@@ -234,18 +236,17 @@ public class EventListCellController implements FxController {
}
}
- private Path convertVaultPathToSystemPath(Path p) {
- if (!(p instanceof CryptoPath)) {
- throw new IllegalArgumentException("Path " + p + " is not a vault path");
- }
+ private Path convertVaultPathToSystemPath(String vaultInternalPath) {
var v = eventEntry.getValue().getKey().vault();
if (!v.isUnlocked()) {
return Path.of(System.getProperty("user.home"));
}
- var mountUri = v.getMountPoint().uri();
- var internalPath = p.toString().substring(1);
- return Path.of(mountUri.getPath().concat(internalPath).substring(1));
+ var mountPoint = v.getMountPoint().uri().getPath();
+ if (SystemUtils.IS_OS_WINDOWS) {
+ mountPoint = mountPoint.substring(1); //strip away any leading "/", otherwise there are errors
+ }
+ return Path.of(mountPoint, vaultInternalPath.substring(1)); //vaultPaths are always absolute
}
private void reveal(RevealPathService s, Path p) {
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
index ccc0684af..a6ae45efc 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
@@ -10,16 +10,20 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
+import javafx.application.Application;
import javafx.application.Platform;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
@FxApplicationScoped
public class FxApplication {
private static final Logger LOG = LoggerFactory.getLogger(FxApplication.class);
+ static final AtomicReference INSTANCE = new AtomicReference<>();
+
private final long startupTime;
private final Environment environment;
private final Settings settings;
@@ -30,9 +34,21 @@ public class FxApplication {
private final FxApplicationTerminator applicationTerminator;
private final AutoUnlocker autoUnlocker;
private final FxFSEventList fxFSEventList;
+ private final FxNotificationManager notificationManager;
@Inject
- FxApplication(@Named("startupTime") long startupTime, Environment environment, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker, FxFSEventList fxFSEventList) {
+ FxApplication(Application fxApp,
+ @Named("startupTime") long startupTime, //
+ Environment environment, //
+ Settings settings, //
+ AppLaunchEventHandler launchEventHandler, //
+ Lazy trayMenu, //
+ FxApplicationWindows appWindows, //
+ FxApplicationStyle applicationStyle, //
+ FxApplicationTerminator applicationTerminator, //
+ AutoUnlocker autoUnlocker, //
+ FxFSEventList fxFSEventList, //
+ FxNotificationManager notificationManager) {
this.startupTime = startupTime;
this.environment = environment;
this.settings = settings;
@@ -43,6 +59,9 @@ public class FxApplication {
this.applicationTerminator = applicationTerminator;
this.autoUnlocker = autoUnlocker;
this.fxFSEventList = fxFSEventList;
+ this.notificationManager = notificationManager;
+
+ INSTANCE.set(fxApp);
}
public void start() {
@@ -88,6 +107,7 @@ public class FxApplication {
launchEventHandler.startHandlingLaunchEvents();
fxFSEventList.schedulePollForUpdates();
+ notificationManager.schedulePollForUpdates();
autoUnlocker.tryUnlockForTimespan(2, TimeUnit.MINUTES);
}
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
index 74abac546..6b19429b2 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
@@ -7,12 +7,14 @@ package org.cryptomator.ui.fxapp;
import dagger.Module;
import dagger.Provides;
+import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.ui.decryptname.DecryptNameComponent;
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;
+import org.cryptomator.ui.notification.NotificationComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.quit.QuitComponent;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
@@ -25,8 +27,9 @@ import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
import javafx.scene.image.Image;
import java.io.IOException;
import java.io.InputStream;
+import java.util.Optional;
-@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, //
+@Module(subcomponents = {TrayMenuComponent.class, //
DecryptNameComponent.class, //
MainWindowComponent.class, //
PreferencesComponent.class, //
@@ -39,7 +42,8 @@ import java.io.InputStream;
UpdateReminderComponent.class, //
ShareVaultComponent.class, //
EventViewComponent.class, //
- RecoveryKeyComponent.class})
+ RecoveryKeyComponent.class, //
+ NotificationComponent.class})
abstract class FxApplicationModule {
private static Image createImageFromResource(String resourceName) throws IOException {
@@ -48,6 +52,12 @@ abstract class FxApplicationModule {
}
}
+ @Provides
+ @FxApplicationScoped
+ static Optional provideAppearanceProvider() {
+ return UiAppearanceProvider.get();
+ }
+
@Provides
@FxApplicationScoped
static TrayMenuComponent provideTrayMenuComponent(TrayMenuComponent.Builder builder) {
@@ -78,4 +88,10 @@ abstract class FxApplicationModule {
return factory.create();
}
+ @Provides
+ @FxApplicationScoped
+ static NotificationComponent provideNotificationComponent(NotificationComponent.Factory factory) {
+ return factory.create();
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java
index ee187fd65..bfdb22196 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java
@@ -36,82 +36,91 @@ public class FxApplicationStyle {
}
public void initialize() {
+ var uiTheme = settings.theme.get();
+ if (uiTheme == UiTheme.AUTOMATIC) {
+ registerOsThemeListener();
+ }
+ applyTheme(uiTheme);
settings.theme.addListener(this::appThemeChanged);
- loadSelectedStyleSheet(settings.theme.get());
}
- private void appThemeChanged(@SuppressWarnings("unused") ObservableValue extends UiTheme> observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) {
- if (appearanceProvider.isPresent() && oldValue == UiTheme.AUTOMATIC && newValue != UiTheme.AUTOMATIC) {
+ private void appThemeChanged(@SuppressWarnings("unused") ObservableValue extends UiTheme> observable, UiTheme oldValue, UiTheme newValue) {
+ if (oldValue == newValue) {
+ // no-op
+ } else if (newValue == UiTheme.AUTOMATIC) {
+ registerOsThemeListener();
+ } else if (oldValue == UiTheme.AUTOMATIC) {
+ removeOsThemeListener();
+ }
+
+ applyTheme(newValue);
+ }
+
+ private void removeOsThemeListener() {
+ if (appearanceProvider.isPresent()) {
try {
appearanceProvider.get().removeListener(systemInterfaceThemeListener);
} catch (UiAppearanceException e) {
- LOG.error("Failed to disable automatic theme switching.");
- }
- }
- loadSelectedStyleSheet(newValue);
- }
-
- private void loadSelectedStyleSheet(UiTheme desiredTheme) {
- UiTheme theme = licenseHolder.isValidLicense() ? desiredTheme : UiTheme.LIGHT;
- switch (theme) {
- case LIGHT -> applyLightTheme();
- case DARK -> applyDarkTheme();
- case AUTOMATIC -> {
- appearanceProvider.ifPresent(provider -> {
- try {
- provider.addListener(systemInterfaceThemeListener);
- } catch (UiAppearanceException e) {
- LOG.error("Failed to enable automatic theme switching.");
- }
- });
- applySystemTheme();
+ LOG.warn("Failed to disable automatic theme switching.", e);
}
+ } else {
+ LOG.debug("Unable to remove listener os theme changes: No supported UiAppearanceProvider present");
}
}
- private void systemInterfaceThemeChanged(Theme theme) {
- switch (theme) {
- case LIGHT -> applyLightTheme();
- case DARK -> applyDarkTheme();
- }
- }
-
- private void applySystemTheme() {
+ private void registerOsThemeListener() {
if (appearanceProvider.isPresent()) {
- systemInterfaceThemeChanged(appearanceProvider.get().getSystemTheme());
+ try {
+ appearanceProvider.get().addListener(systemInterfaceThemeListener);
+ } catch (UiAppearanceException e) {
+ LOG.warn("Failed to enable automatic theme switching.", e);
+ }
} else {
- LOG.warn("No UiAppearanceProvider present, assuming LIGHT theme...");
- applyLightTheme();
+ LOG.warn("Unable to register for os theme changes: No supported UiAppearanceProvider present");
}
}
- private void applyLightTheme() {
- var stylesheet = Optional //
- .ofNullable(getClass().getResource("/css/light_theme.bss")) //
- .orElse(getClass().getResource("/css/light_theme.css"));
+ private void applyTheme(UiTheme uiTheme) {
+ if (!licenseHolder.isValidLicense()) {
+ loadAndApplyLightTheme();
+ } else {
+ switch (uiTheme) {
+ case AUTOMATIC -> {
+ var osTheme = appearanceProvider.map(UiAppearanceProvider::getSystemTheme).orElse(Theme.LIGHT);
+ systemInterfaceThemeChanged(osTheme);
+ }
+ case LIGHT -> loadAndApplyLightTheme();
+ case DARK -> loadAndApplyDarkTheme();
+ }
+ }
+ }
+
+ private void systemInterfaceThemeChanged(Theme osTheme) {
+ switch (osTheme) {
+ case LIGHT -> loadAndApplyLightTheme();
+ case DARK -> loadAndApplyDarkTheme();
+ }
+ }
+
+ private void loadAndApplyLightTheme() {
+ loadAndApplyTheme(Theme.LIGHT, "/css/light_theme.css");
+ }
+
+ private void loadAndApplyDarkTheme() {
+ loadAndApplyTheme(Theme.DARK, "/css/dark_theme.css");
+ }
+
+ private void loadAndApplyTheme(Theme appTheme, String cssFile) {
+ var stylesheet = getClass().getResource(cssFile);
if (stylesheet == null) {
- LOG.warn("Failed to load light_theme stylesheet");
- } else {
- Application.setUserAgentStylesheet(stylesheet.toString());
- appearanceProvider.ifPresent(provider -> provider.adjustToTheme(Theme.LIGHT));
- appliedTheme.set(Theme.LIGHT);
+ throw new IllegalStateException("Cannot find resource %s".formatted(cssFile));
}
+ Application.setUserAgentStylesheet(stylesheet.toString());
+ appearanceProvider.ifPresent(provider -> provider.adjustToTheme(appTheme));
+ appliedTheme.set(appTheme);
}
- private void applyDarkTheme() {
- var stylesheet = Optional //
- .ofNullable(getClass().getResource("/css/dark_theme.bss")) //
- .orElse(getClass().getResource("/css/dark_theme.css"));
- if (stylesheet == null) {
- LOG.warn("Failed to load dark_theme stylesheet");
- } else {
- Application.setUserAgentStylesheet(stylesheet.toString());
- appearanceProvider.ifPresent(provider -> provider.adjustToTheme(Theme.DARK));
- appliedTheme.set(Theme.DARK);
- }
- }
-
- public ObjectProperty appliedThemeProperty() {
+ public ObjectProperty appliedAppThemeProperty() {
return appliedTheme;
}
}
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java
index c8a870fd8..94c4fe330 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java
@@ -11,6 +11,7 @@ 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.notification.NotificationComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.quit.QuitComponent;
@@ -54,6 +55,7 @@ public class FxApplicationWindows {
private final LockComponent.Factory lockWorkflowFactory;
private final ErrorComponent.Factory errorWindowFactory;
private final Lazy eventViewWindow;
+ private final Lazy notificationWindow;
private final ExecutorService executor;
private final VaultOptionsComponent.Factory vaultOptionsWindow;
private final ShareVaultComponent.Factory shareVaultWindow;
@@ -73,6 +75,7 @@ public class FxApplicationWindows {
VaultOptionsComponent.Factory vaultOptionsWindow, //
ShareVaultComponent.Factory shareVaultWindow, //
Lazy eventViewWindow, //
+ Lazy notificationWindow,
ExecutorService executor, //
Dialogs dialogs) {
this.primaryStage = primaryStage;
@@ -85,6 +88,7 @@ public class FxApplicationWindows {
this.lockWorkflowFactory = lockWorkflowFactory;
this.errorWindowFactory = errorWindowFactory;
this.eventViewWindow = eventViewWindow;
+ this.notificationWindow = notificationWindow;
this.executor = executor;
this.vaultOptionsWindow = vaultOptionsWindow;
this.shareVaultWindow = shareVaultWindow;
@@ -193,6 +197,10 @@ public class FxApplicationWindows {
return CompletableFuture.supplyAsync(() -> eventViewWindow.get().showEventViewerWindow(), Platform::runLater).whenComplete(this::reportErrors);
}
+ public CompletionStage showNotification() {
+ return CompletableFuture.supplyAsync(() -> notificationWindow.get().showNotification(), Platform::runLater).whenComplete(this::reportErrors);
+ }
+
/**
* Displays the generic error scene in the given window.
*
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxFSEventList.java b/src/main/java/org/cryptomator/ui/fxapp/FxFSEventList.java
index e9e574b95..f1dd20d53 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxFSEventList.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxFSEventList.java
@@ -3,6 +3,8 @@ package org.cryptomator.ui.fxapp;
import org.cryptomator.event.FSEventBucket;
import org.cryptomator.event.FSEventBucketContent;
import org.cryptomator.event.FileSystemEventAggregator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Platform;
@@ -11,6 +13,7 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.Map;
+import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -23,6 +26,8 @@ import java.util.concurrent.TimeUnit;
@FxApplicationScoped
public class FxFSEventList {
+ private static final Logger LOG = LoggerFactory.getLogger(FxFSEventList.class);
+
private final ObservableList> events;
private final FileSystemEventAggregator eventAggregator;
private final ScheduledExecutorService scheduler;
@@ -37,7 +42,13 @@ public class FxFSEventList {
}
public void schedulePollForUpdates() {
- scheduler.schedule(this::checkForEventUpdates, 1000, TimeUnit.MILLISECONDS);
+ try {
+ scheduler.schedule(this::checkForEventUpdates, 1000, TimeUnit.MILLISECONDS);
+ } catch ( RejectedExecutionException e) {
+ if(!scheduler.isShutdown()) {
+ LOG.warn("Failed to poll for filesystem events", e);
+ }
+ }
}
/**
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxNotificationManager.java b/src/main/java/org/cryptomator/ui/fxapp/FxNotificationManager.java
new file mode 100644
index 000000000..c1100a2f4
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxNotificationManager.java
@@ -0,0 +1,57 @@
+package org.cryptomator.ui.fxapp;
+
+import org.cryptomator.event.NotificationManager;
+import org.cryptomator.event.VaultEvent;
+
+import javax.inject.Inject;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Notification manager inside the UI domain.
+ *
+ * Polls the {@link NotificationManager} for pending events every {@value POLL_INTERVAL_SECONDS } seconds and
+ * triggers the notification window display when events are available.
+ * Returns an observable list of events requiring a user notification with {@link #getEventsRequiringNotification()}.
+ *
+ * @see NotificationManager
+ */
+@FxApplicationScoped
+public class FxNotificationManager {
+
+ private static final int POLL_INTERVAL_SECONDS = 1;
+
+ private final NotificationManager notificationManager;
+ private final ScheduledExecutorService scheduler;
+ private final FxApplicationWindows applicationWindows;
+ private final ObservableList eventsRequiringNotification;
+
+ @Inject
+ public FxNotificationManager(NotificationManager notificationManager, ScheduledExecutorService scheduler, FxApplicationWindows applicationWindows) {
+ this.notificationManager = notificationManager;
+ this.scheduler = scheduler;
+ this.applicationWindows = applicationWindows;
+ this.eventsRequiringNotification = FXCollections.observableArrayList();
+ }
+
+ public void schedulePollForUpdates() {
+ scheduler.scheduleAtFixedRate(this::checkForPendingNotifications, 0, POLL_INTERVAL_SECONDS, TimeUnit.SECONDS);
+ }
+
+ private void checkForPendingNotifications() {
+ Platform.runLater(() -> {
+ if (notificationManager.appendToAndClear(eventsRequiringNotification)) {
+ applicationWindows.showNotification();
+ }
+ });
+
+ }
+
+ public ObservableList getEventsRequiringNotification() {
+ return eventsRequiringNotification;
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/fxapp/JfxRevealPathService.java b/src/main/java/org/cryptomator/ui/fxapp/JfxRevealPathService.java
new file mode 100644
index 000000000..1271f42e8
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/fxapp/JfxRevealPathService.java
@@ -0,0 +1,37 @@
+package org.cryptomator.ui.fxapp;
+
+import org.cryptomator.integrations.common.DisplayName;
+import org.cryptomator.integrations.common.OperatingSystem;
+import org.cryptomator.integrations.common.Priority;
+import org.cryptomator.integrations.revealpath.RevealFailedException;
+import org.cryptomator.integrations.revealpath.RevealPathService;
+
+import java.nio.file.Path;
+
+/**
+ * A {@link RevealPathService} service implementation using the JavaFX {@link javafx.application.HostServices#showDocument(String)} to reveal documents.
+ *
+ * On macOS, the window is placed in the top-right corner of the primary screen, following platform conventions.
+ * On other operating systems, the window placement depends on the location of the system bar:
+ *
+ *
If the system bar is at the top, the window is centered horizontally at the top of the screen.
+ *
Otherwise (e.g., system bar at the bottom or elsewhere), the window is placed in the bottom-right corner.