diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b608b3e..baf940d14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,11 @@ Changes to prior versions can be found on the [Github release page](https://gith ## [Unreleased](https://github.com/cryptomator/cryptomator/compare/1.18.0...HEAD) ### Added -* New Self-Update Mechanism (#3948) +* Self-Update Mechanism (#3948) * Implemented `.dmg` update mechanism * Implemented Flatpak update mechanism +* App notifications (#4069) +* Mark files in-use for Hub vaults (#4078) ### Changed * Built using JDK 25 (#4031) diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh index 947fe56c4..99249cb35 100755 --- a/dist/linux/appimage/build.sh +++ b/dist/linux/appimage/build.sh @@ -115,6 +115,7 @@ cp ../common/org.cryptomator.Cryptomator.tray-unlocked.svg Cryptomator.AppDir/us cp ../common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop cp ../common/org.cryptomator.Cryptomator.metainfo.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.metainfo.xml cp ../common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml +cp ../common/application-vnd.cryptomator.encrypted.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.encrypted.xml ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon ln -s usr/share/applications/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/org.cryptomator.Cryptomator.desktop diff --git a/dist/linux/common/application-vnd.cryptomator.encrypted.xml b/dist/linux/common/application-vnd.cryptomator.encrypted.xml new file mode 100644 index 000000000..2ee72b9f0 --- /dev/null +++ b/dist/linux/common/application-vnd.cryptomator.encrypted.xml @@ -0,0 +1,9 @@ + + + + Cryptomator Encrypted Data + + + + + diff --git a/dist/linux/debian/cryptomator.install b/dist/linux/debian/cryptomator.install index 0e7427351..787c5aea0 100644 --- a/dist/linux/debian/cryptomator.install +++ b/dist/linux/debian/cryptomator.install @@ -6,4 +6,5 @@ common/org.cryptomator.Cryptomator.tray-unlocked.svg usr/share/icons/hicolor/sca common/org.cryptomator.Cryptomator256.png usr/share/icons/hicolor/256x256/apps common/org.cryptomator.Cryptomator512.png usr/share/icons/hicolor/512x512/apps common/org.cryptomator.Cryptomator.metainfo.xml usr/share/metainfo -common/application-vnd.cryptomator.vault.xml usr/share/mime/packages \ No newline at end of file +common/application-vnd.cryptomator.vault.xml usr/share/mime/packages +common/application-vnd.cryptomator.encrypted.xml usr/share/mime/packages \ No newline at end of file diff --git a/dist/linux/debian/postinst b/dist/linux/debian/postinst index 5668a5e29..d050e4840 100644 --- a/dist/linux/debian/postinst +++ b/dist/linux/debian/postinst @@ -25,6 +25,7 @@ case "$1" in fi xdg-desktop-menu install --novendor /usr/share/applications/org.cryptomator.Cryptomator.desktop xdg-mime install /usr/share/mime/packages/application-vnd.cryptomator.vault.xml + xdg-mime install /usr/share/mime/packages/application-vnd.cryptomator.encrypted.xml ;; abort-upgrade|abort-remove|abort-deconfigure) diff --git a/dist/linux/debian/prerm b/dist/linux/debian/prerm index 41a54cf33..518910a90 100644 --- a/dist/linux/debian/prerm +++ b/dist/linux/debian/prerm @@ -23,6 +23,7 @@ case "$1" in xdg-desktop-menu uninstall --novendor /usr/share/applications/org.cryptomator.Cryptomator.desktop xdg-mime uninstall /usr/share/mime/packages/application-vnd.cryptomator.vault.xml + xdg-mime uninstall /usr/share/mime/packages/application-vnd.cryptomator.encrypted.xml ;; failed-upgrade) diff --git a/dist/mac/resources/Info.plist b/dist/mac/resources/Info.plist index d59c3fe73..00487369c 100644 --- a/dist/mac/resources/Info.plist +++ b/dist/mac/resources/Info.plist @@ -105,6 +105,7 @@ c9r c9s + c9u public.mime-type diff --git a/dist/win/resources/main.wxs b/dist/win/resources/main.wxs index 1da7be13a..6310c6985 100644 --- a/dist/win/resources/main.wxs +++ b/dist/win/resources/main.wxs @@ -94,6 +94,7 @@ + diff --git a/pom.xml b/pom.xml index 24f35de0a..75ecf99d9 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents - 2.9.0 + 2.10.0-beta2 1.8.0-beta1 1.5.1 1.5.0-beta1 diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index f1f2aa5c6..450cd4ca7 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,5 +1,4 @@ import ch.qos.logback.classic.spi.Configurator; -import org.cryptomator.networking.SSLContextWithPKCS12TrustStore; import org.cryptomator.common.locationpresets.DropboxLinuxLocationPresetsProvider; import org.cryptomator.common.locationpresets.DropboxMacLocationPresetsProvider; import org.cryptomator.common.locationpresets.DropboxWindowsLocationPresetsProvider; @@ -14,11 +13,12 @@ import org.cryptomator.common.locationpresets.OneDriveLinuxLocationPresetsProvid import org.cryptomator.common.locationpresets.OneDriveMacLocationPresetsProvider; import org.cryptomator.common.locationpresets.OneDriveWindowsLocationPresetsProvider; import org.cryptomator.common.locationpresets.PCloudLocationPresetsProvider; -import org.cryptomator.networking.SSLContextWithMacKeychain; -import org.cryptomator.networking.SSLContextProvider; -import org.cryptomator.networking.SSLContextWithWindowsCertStore; import org.cryptomator.integrations.tray.TrayMenuController; import org.cryptomator.logging.LogbackConfiguratorFactory; +import org.cryptomator.networking.SSLContextProvider; +import org.cryptomator.networking.SSLContextWithMacKeychain; +import org.cryptomator.networking.SSLContextWithPKCS12TrustStore; +import org.cryptomator.networking.SSLContextWithWindowsCertStore; import org.cryptomator.ui.traymenu.AwtTrayMenuController; open module org.cryptomator.desktop { diff --git a/src/main/java/org/cryptomator/common/Constants.java b/src/main/java/org/cryptomator/common/Constants.java index 105fba3d6..b156554d5 100644 --- a/src/main/java/org/cryptomator/common/Constants.java +++ b/src/main/java/org/cryptomator/common/Constants.java @@ -13,5 +13,7 @@ public interface Constants { String CRYPTOMATOR_FILENAME_GLOB = "*.cryptomator"; URI DEFAULT_KEY_ID = URI.create(MasterkeyFileLoadingStrategy.SCHEME + ":" + MASTERKEY_FILENAME); byte[] PEPPER = new byte[0]; + // Separator used to concatenate Hub username and device name in the filesystem owner identifier. + String HUB_USER_DEVICE_SEPARATOR = "&"; } diff --git a/src/main/java/org/cryptomator/common/EventMap.java b/src/main/java/org/cryptomator/common/EventMap.java deleted file mode 100644 index 2e8dbf035..000000000 --- a/src/main/java/org/cryptomator/common/EventMap.java +++ /dev/null @@ -1,160 +0,0 @@ -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/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/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/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/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/eventview/EventListCellController.java b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java index 3157f9f40..2327cc262 100644 --- a/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java +++ b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java @@ -1,5 +1,8 @@ package org.cryptomator.ui.eventview; +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.Constants; +import org.cryptomator.cryptofs.event.FileIsInUseEvent; import org.cryptomator.event.FSEventBucket; import org.cryptomator.event.FSEventBucketContent; import org.cryptomator.event.FileSystemEventAggregator; @@ -115,7 +118,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,20 +127,41 @@ 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)); + if (revealService != null) { + addLocalizedAction("eventView.entry.inUse.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(fiiue.cleartextPath()))); + addLocalizedAction("eventView.entry.inUse.showEncrypted", () -> reveal(revealService, fiiue.ciphertextPath())); + } else { + addLocalizedAction("eventView.entry.inUse.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(fiiue.cleartextPath()).toString())); + addLocalizedAction("eventView.entry.inUse.copyEncrypted", () -> copyToClipboard(fiiue.ciphertextPath().toString())); + } + + 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())); + addLocalizedAction("eventView.entry.brokenFileNode.showEncrypted", () -> reveal(revealService, bfe.ciphertextPath())); } else { - addAction("eventView.entry.brokenFileNode.copyEncrypted", () -> copyToClipboard(bfe.ciphertextPath().toString())); + addLocalizedAction("eventView.entry.brokenFileNode.copyEncrypted", () -> copyToClipboard(bfe.ciphertextPath().toString())); } - addAction("eventView.entry.brokenFileNode.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(bfe.cleartextPath()).toString())); + addLocalizedAction("eventView.entry.brokenFileNode.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(bfe.cleartextPath()).toString())); } private void adjustToConflictResolvedEvent(ConflictResolvedEvent cre) { @@ -145,9 +169,9 @@ public class EventListCellController implements FxController { 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()))); + addLocalizedAction("eventView.entry.conflictResolved.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cre.resolvedCleartextPath()))); } else { - addAction("eventView.entry.conflictResolved.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cre.resolvedCleartextPath()).toString())); + addLocalizedAction("eventView.entry.conflictResolved.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cre.resolvedCleartextPath()).toString())); } } @@ -156,11 +180,11 @@ public class EventListCellController implements FxController { 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())); + addLocalizedAction("eventView.entry.conflict.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cfe.canonicalCleartextPath()))); + addLocalizedAction("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.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cfe.canonicalCleartextPath()).toString())); + addLocalizedAction("eventView.entry.conflict.copyEncrypted", () -> copyToClipboard(cfe.conflictingCiphertextPath().toString())); } } @@ -169,9 +193,9 @@ public class EventListCellController implements FxController { 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())); + addLocalizedAction("eventView.entry.decryptionFailed.showEncrypted", () -> reveal(revealService, dfe.ciphertextPath())); } else { - addAction("eventView.entry.decryptionFailed.copyEncrypted", () -> copyToClipboard(dfe.ciphertextPath().toString())); + addLocalizedAction("eventView.entry.decryptionFailed.copyEncrypted", () -> copyToClipboard(dfe.ciphertextPath().toString())); } } @@ -180,14 +204,19 @@ public class EventListCellController implements FxController { 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())); + addLocalizedAction("eventView.entry.brokenDirFile.showEncrypted", () -> reveal(revealService, bde.ciphertextPath())); } else { - addAction("eventView.entry.brokenDirFile.copyEncrypted", () -> copyToClipboard(bde.ciphertextPath().toString())); + addLocalizedAction("eventView.entry.brokenDirFile.copyEncrypted", () -> copyToClipboard(bde.ciphertextPath().toString())); } } - 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 +263,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..7fb5b523b 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -30,9 +30,20 @@ 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(@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 +54,7 @@ public class FxApplication { this.applicationTerminator = applicationTerminator; this.autoUnlocker = autoUnlocker; this.fxFSEventList = fxFSEventList; + this.notificationManager = notificationManager; } public void start() { @@ -88,6 +100,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 70319df5b..80a261bb8 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -13,6 +13,7 @@ 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; @@ -39,7 +40,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 { @@ -78,4 +80,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/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/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/health/HealthCheckModule.java b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java index c36f486e0..19595e0e9 100644 --- a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java +++ b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java @@ -63,8 +63,8 @@ abstract class HealthCheckModule { @Provides @HealthCheckWindow @HealthCheckScoped - static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) { - return compBuilder.vault(vault).window(window).build().keyloadingStrategy(); + static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Factory compFactory, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) { + return compFactory.create(vault, window).keyloadingStrategy(); } @Provides diff --git a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java index 190bf0e1f..f035388f6 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java +++ b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java @@ -3,11 +3,8 @@ package org.cryptomator.ui.keyloading; import dagger.BindsInstance; import dagger.Subcomponent; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.cryptolib.api.MasterkeyLoader; import javafx.stage.Stage; -import java.util.Map; -import java.util.function.Supplier; @KeyLoadingScoped @Subcomponent(modules = {KeyLoadingModule.class}) @@ -16,16 +13,10 @@ public interface KeyLoadingComponent { @KeyLoading KeyLoadingStrategy keyloadingStrategy(); - @Subcomponent.Builder - interface Builder { + @Subcomponent.Factory + interface Factory { - @BindsInstance - Builder vault(@KeyLoading Vault vault); - - @BindsInstance - Builder window(@KeyLoading Stage window); - - KeyLoadingComponent build(); + KeyLoadingComponent create(@BindsInstance @KeyLoading Vault vault, @KeyLoading @BindsInstance Stage window); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java index f8710b8c0..10a032a35 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java @@ -66,6 +66,13 @@ public abstract class HubKeyLoadingModule { return new AtomicReference<>(); } + @Provides + @Named("filesystemOwnerId") + @KeyLoadingScoped + static AtomicReference provideFilesystemOwnerIdRef() { + return new AtomicReference<>(); + } + @Provides @KeyLoadingScoped static CompletableFuture provideResult() { diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java index 40f845a63..4b48169c0 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java @@ -2,6 +2,7 @@ package org.cryptomator.ui.keyloading.hub; import com.google.common.base.Preconditions; import dagger.Lazy; +import org.cryptomator.common.FilesystemOwnerSupplier; import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.keychain.NoKeychainAccessProviderException; import org.cryptomator.common.settings.DeviceKey; @@ -23,25 +24,28 @@ import java.net.URI; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; @KeyLoading -public class HubKeyLoadingStrategy implements KeyLoadingStrategy { +public class HubKeyLoadingStrategy implements KeyLoadingStrategy, FilesystemOwnerSupplier { - private static final String SCHEME_PREFIX = "hub+"; + public static final String SCHEME_PREFIX = "hub+"; public static final String SCHEME_HUB_HTTP = SCHEME_PREFIX + "http"; public static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https"; private final Stage window; private final KeychainManager keychainManager; + private final AtomicReference fsOwnerId; private final Lazy authFlowScene; private final Lazy noKeychainScene; private final CompletableFuture result; private final DeviceKey deviceKey; @Inject - public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy noKeychainScene, CompletableFuture result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle) { + public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy noKeychainScene, CompletableFuture result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle, @Named("filesystemOwnerId") AtomicReference fsOwnerId) { this.window = window; this.keychainManager = keychainManager; + this.fsOwnerId = fsOwnerId; window.setTitle(windowTitle); window.setOnCloseRequest(_ -> result.cancel(true)); this.authFlowScene = authFlowScene; @@ -90,4 +94,13 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { }); } + @Override + public String getOwner() { + var name = fsOwnerId.get(); + if (name == null) { + throw new IllegalStateException("Owner is not yet determined"); + } + return name; + } + } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java index f94d882fa..353ac14ec 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Preconditions; import com.nimbusds.jose.JWEObject; import dagger.Lazy; +import org.cryptomator.common.Constants; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; @@ -41,7 +42,6 @@ import java.util.concurrent.atomic.AtomicReference; public class ReceiveKeyController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(ReceiveKeyController.class); - private static final String SCHEME_PREFIX = "hub+"; private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true); private static final Duration REQ_TIMEOUT = Duration.ofSeconds(10); @@ -50,6 +50,7 @@ public class ReceiveKeyController implements FxController { private final String vaultId; private final String deviceId; private final String bearerToken; + private final AtomicReference fsOwnerId; private final CompletableFuture result; private final Lazy registerDeviceScene; private final Lazy legacyRegisterDeviceScene; @@ -59,12 +60,25 @@ public class ReceiveKeyController implements FxController { private final HttpClient httpClient; @Inject - public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, HubConfig hubConfig, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference tokenRef, CompletableFuture result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy legacyRegisterDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, @FxmlScene(FxmlFile.HUB_REQUIRE_ACCOUNT_INIT) Lazy accountInitializationScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy invalidLicenseScene) { + public ReceiveKeyController(@KeyLoading Vault vault, // + ExecutorService executor, // + @KeyLoading Stage window, // + HubConfig hubConfig, // + @Named("deviceId") String deviceId, // + @Named("bearerToken") AtomicReference tokenRef, // + @Named("filesystemOwnerId") AtomicReference fsOwnerId, // + CompletableFuture result, // + @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, // + @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy legacyRegisterDeviceScene, // + @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, // + @FxmlScene(FxmlFile.HUB_REQUIRE_ACCOUNT_INIT) Lazy accountInitializationScene, // + @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy invalidLicenseScene) { this.window = window; this.hubConfig = hubConfig; this.vaultId = extractVaultId(vault.getVaultConfigCache().getUnchecked().getKeyId()); // TODO: access vault config's JTI directly (requires changes in cryptofs) this.deviceId = deviceId; this.bearerToken = Objects.requireNonNull(tokenRef.get()); + this.fsOwnerId = fsOwnerId; this.result = result; this.registerDeviceScene = registerDeviceScene; this.legacyRegisterDeviceScene = legacyRegisterDeviceScene; @@ -81,7 +95,34 @@ public class ReceiveKeyController implements FxController { } public void receiveKey() { - requestApiConfig(); + requestUserData(); + } + + private void requestUserData() { + var userUri = hubConfig.URIs.API.resolve("users/me?withDevices=false"); + var request = HttpRequest.newBuilder(userUri) // + .header("Authorization", "Bearer " + bearerToken) // + .GET() // + .timeout(REQ_TIMEOUT) // + .build(); + httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) // + .thenAcceptAsync(this::receivedUserData) // + .exceptionally(this::retrievalFailed); + } + + private void receivedUserData(HttpResponse response) { + LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode()); + try { + if (response.statusCode() == 200) { + var user = JSON.reader().readValue(response.body(), UserDto.class); + fsOwnerId.set(user.name); + requestApiConfig(); + } else { + throw new IllegalStateException("Unexpected response " + response.statusCode()); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } } /** @@ -144,6 +185,7 @@ public class ReceiveKeyController implements FxController { switch (response.statusCode()) { case 200 -> { var device = JSON.reader().readValue(response.body(), DeviceDto.class); + fsOwnerId.accumulateAndGet(device.name, (s1, s2) -> s1 + Constants.HUB_USER_DEVICE_SEPARATOR + s2); requestVaultMasterkey(device.userPrivateKey); } case 404 -> Platform.runLater(this::needsDeviceRegistration); @@ -289,13 +331,16 @@ public class ReceiveKeyController implements FxController { } private static String extractVaultId(URI vaultKeyUri) { - assert vaultKeyUri.getScheme().startsWith(SCHEME_PREFIX); + assert vaultKeyUri.getScheme().startsWith(HubKeyLoadingStrategy.SCHEME_PREFIX); var path = vaultKeyUri.getPath(); return path.substring(path.lastIndexOf('/') + 1); } @JsonIgnoreProperties(ignoreUnknown = true) - private record DeviceDto(@JsonProperty(value = "userPrivateKey", required = true) String userPrivateKey) {} + private record UserDto(@JsonProperty(value = "name", required = true) String name) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + private record DeviceDto(@JsonProperty(value = "name", required = true) String name, @JsonProperty(value = "userPrivateKey", required = true) String userPrivateKey) {} @JsonIgnoreProperties(ignoreUnknown = true) private record ConfigDto(@JsonProperty(value = "apiLevel") int apiLevel) {} diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java index f25528498..f3c3ccb83 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java @@ -15,8 +15,8 @@ import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.VaultService; import org.cryptomator.ui.dialogs.Dialogs; -import org.cryptomator.ui.fxapp.FxFSEventList; import org.cryptomator.ui.fxapp.FxApplicationWindows; +import org.cryptomator.ui.fxapp.FxFSEventList; import org.cryptomator.ui.preferences.SelectedPreferencesTab; import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import org.slf4j.Logger; @@ -139,8 +139,8 @@ public class VaultListController implements FxController { vaultList.setItems(vaults); vaultList.setCellFactory(cellFactory); - vaultList.prefHeightProperty().bind( - vaultList.fixedCellSizeProperty().multiply(Bindings.size(vaultList.getItems())) + vaultList.prefHeightProperty().bind( // + vaultList.fixedCellSizeProperty().multiply(Bindings.size(vaultList.getItems())) // ); selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty()); @@ -157,11 +157,11 @@ public class VaultListController implements FxController { //unlock vault on double click vaultList.addEventFilter(MouseEvent.MOUSE_CLICKED, click -> { if (click.getClickCount() >= 2) { - Optional.ofNullable(selectedVault.get()) - .filter(Vault::isLocked) + Optional.ofNullable(selectedVault.get()) // + .filter(Vault::isLocked) // .ifPresent(vault -> appWindows.startUnlockWorkflow(vault, mainWindow)); - Optional.ofNullable(selectedVault.get()) - .filter(Vault::isUnlocked) + Optional.ofNullable(selectedVault.get()) // + .filter(Vault::isUnlocked) // .ifPresent(vaultService::reveal); } }); diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationComponent.java b/src/main/java/org/cryptomator/ui/notification/NotificationComponent.java new file mode 100644 index 000000000..3ebcdf4ab --- /dev/null +++ b/src/main/java/org/cryptomator/ui/notification/NotificationComponent.java @@ -0,0 +1,35 @@ +package org.cryptomator.ui.notification; + +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; + +@NotificationScoped +@Subcomponent(modules = {NotificationModule.class}) +public interface NotificationComponent { + + @NotificationWindow + Stage window(); + + @FxmlScene(FxmlFile.NOTIFICATION) + Lazy scene(); + + default Stage showNotification() { + var window = window(); + window.setScene(scene().get()); + window.sizeToScene(); + window.show(); + window.requestFocus(); + return window; + } + + @Subcomponent.Factory + interface Factory { + NotificationComponent create(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationController.java b/src/main/java/org/cryptomator/ui/notification/NotificationController.java new file mode 100644 index 000000000..fe96e3721 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/notification/NotificationController.java @@ -0,0 +1,245 @@ +package org.cryptomator.ui.notification; + +import org.cryptomator.common.Constants; +import org.cryptomator.common.settings.Settings; +import org.cryptomator.cryptofs.event.FileIsInUseEvent; +import org.cryptomator.event.VaultEvent; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.fxapp.FxNotificationManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.beans.binding.Bindings; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ObservableStringValue; +import javafx.beans.value.ObservableValue; +import javafx.fxml.FXML; +import javafx.stage.Stage; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Locale; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.concurrent.ExecutorService; + +@NotificationScoped +public class NotificationController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(NotificationController.class); + private static final DateTimeFormatter LOCAL_TIME_FORMATTER_TEMPLATE = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault()); + private static final String BUG_MSG = "If you see this message, please report it on the Cryptomator issue tracker."; + + private final Stage window; + private final SimpleListProperty events; + private final ResourceBundle resourceBundle; + private final IntegerProperty selectionIndex; + private final ObservableStringValue paging; + private final ObjectProperty selectedEvent; + private final ObservableValue singleEvent; + private final StringProperty fileName; + private final StringProperty vaultName; + private final StringProperty eventTimestamp; + private final StringProperty message; + private final StringProperty description; + private final StringProperty actionText; + private final ExecutorService executorService; + private final DateTimeFormatter localizedTimeFormatter; + + @Inject + public NotificationController(@NotificationWindow Stage window, FxNotificationManager notificationManager, ExecutorService executorService, ResourceBundle resourceBundle, Settings settings) { + this.window = window; + var preferredLanguage = settings.language.get(); + this.localizedTimeFormatter = LOCAL_TIME_FORMATTER_TEMPLATE.withLocale(preferredLanguage == null ? Locale.getDefault() : Locale.forLanguageTag(preferredLanguage)); + this.events = new SimpleListProperty<>(notificationManager.getEventsRequiringNotification()); + this.resourceBundle = resourceBundle; + this.selectionIndex = new SimpleIntegerProperty(-1); + this.selectedEvent = new SimpleObjectProperty<>(); + this.singleEvent = events.sizeProperty().map(size -> size.intValue() == 1); + this.paging = Bindings.createStringBinding(() -> selectionIndex.get() + 1 + "/" + events.size(), selectionIndex, events); + this.vaultName = new SimpleStringProperty(); + this.eventTimestamp = new SimpleStringProperty(); + this.message = new SimpleStringProperty(); + this.fileName = new SimpleStringProperty(""); + this.description = new SimpleStringProperty(); + this.actionText = new SimpleStringProperty(); + this.executorService = executorService; + } + + @FXML + public void initialize() { + window.setOnShowing(_ -> selectionIndex.set(0)); + selectionIndex.addListener((_, _, n) -> { + if (!events.isEmpty()) { + selectedEvent.setValue(events.get(n.intValue())); + } + }); + selectedEvent.addListener(this::selectTexts); + } + + private void selectTexts(ObservableValue observable, VaultEvent oldEvent, VaultEvent newEvent) { + if (newEvent == null) { + vaultName.set(""); + message.set("NO CONTENT"); + description.set(BUG_MSG); + actionText.set(null); + return; + } + + vaultName.set(newEvent.v().getDisplayName()); + switch (newEvent.actualEvent()) { + case FileIsInUseEvent fiiue -> { + var userAndDevice = fiiue.owner().split(Constants.HUB_USER_DEVICE_SEPARATOR); + var user = userAndDevice[0]; + var device = userAndDevice.length == 1 ? userAndDevice[0] : userAndDevice[1]; + var cleartextFileName = fiiue.cleartextPath().substring(fiiue.cleartextPath().lastIndexOf('/') + 1); + eventTimestamp.set(localizedTimeFormatter.format(fiiue.lastUpdated())); + message.set("File is locked by another device"); + fileName.set(cleartextFileName); + description.set("The file is opened by %s on device %s. Ask the user to close the file and sync again. Otherwise, you can ignore the lock and open it anyway.".formatted(user, device)); + actionText.set("Ignore Lock"); + /* TODO: Once feature is out of beta, activate translations + message.set(resourceBundle.getString("notification.inUse.message")); + description.set(resourceBundle.getString("notification.inUse.description").formatted(fiiue.cleartextPath(), user, device)); + actionText.set(resourceBundle.getString("notification.inUse.action")); + */ + } + default -> { + message.set("NO CONTENT"); + description.set(BUG_MSG); + actionText.set(null); + } + } + } + + @FXML + public void processSelectedEvent() { + try { + var ev = selectedEvent.get(); + switch (ev.actualEvent()) { + case FileIsInUseEvent fiiue -> { + executorService.submit(fiiue.ignoreMethod()); + } + default -> { + } //normally nothing + } + } finally { + removeSelectedEvent(); + } + } + + private void removeSelectedEvent() { + int i = selectionIndex.get(); + var size = events.size(); + if (i < 0 || i >= size) { + LOG.error("Selection index {} is out of bounds of list size {} during event removal. Closing Window.", i, size); + window.close(); + return; + } + + events.remove(i); + if (events.isEmpty()) { + window.close(); //no more events + } else if (events.size() == i) { + selectionIndex.set(i - 1); //triggers event update + } else { + selectedEvent.set(events.get(i)); + } + } + + @FXML + public void previousNotification() { + int i = selectionIndex.get(); + if (i != 0) { + selectionIndex.set(i - 1); + } + } + + @FXML + public void nextNotification() { + int i = selectionIndex.get(); + if (i != events.size() - 1) { + selectionIndex.set(i + 1); + } + } + + @FXML + public void close() { + events.clear(); + window.close(); + } + + + //FXML bindings + public ObservableValue eventTimeProperty() { + return eventTimestamp; + } + + public String getEventTime() { + return eventTimestamp.get(); + } + + public ObservableValue vaultNameProperty() { + return vaultName; + } + + public String getVaultName() { + return vaultName.get(); + } + + public ObservableValue fileNameProperty() { + return fileName; + } + + public String getFileName() { + return fileName.get(); + } + + public ObservableValue messageProperty() { + return message; + } + + public String getMessage() { + return message.get(); + } + + public ObservableValue descriptionProperty() { + return description; + } + + public String getDescription() { + return description.get(); + } + + public StringProperty actionTextProperty() { + return actionText; + } + + public String getActionText() { + return Objects.requireNonNullElse(actionText.get(), ""); + } + + public ObservableStringValue pagingProperty() { + return paging; + } + + public String getPaging() { + return paging.get(); + } + + public ObservableValue singleEventProperty() { + return singleEvent; + } + + public boolean isSingleEvent() { + return singleEvent.getValue(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationModule.java b/src/main/java/org/cryptomator/ui/notification/NotificationModule.java new file mode 100644 index 000000000..64895a40c --- /dev/null +++ b/src/main/java/org/cryptomator/ui/notification/NotificationModule.java @@ -0,0 +1,99 @@ +package org.cryptomator.ui.notification; + +import dagger.Binds; +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.IntoMap; +import org.apache.commons.lang3.SystemUtils; +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.StageInitializer; +import org.cryptomator.ui.common.SystemBarUtil; + +import javax.inject.Provider; +import javafx.scene.Scene; +import javafx.stage.Modality; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import java.util.Map; +import java.util.ResourceBundle; + +@Module +abstract class NotificationModule { + + @Provides + @NotificationWindow + @NotificationScoped + static Stage provideStage(StageInitializer initializer) { + Stage stage = new Stage(StageStyle.TRANSPARENT); + stage.setTitle("Filesystem notification"); //TODO: translate + stage.setResizable(false); + stage.initModality(Modality.NONE); + stage.setAlwaysOnTop(true); + initializer.accept(stage); + stage.setOnShown(_ -> placeWindow(stage)); + return stage; + } + + /** + * Places the notification window on the screen according to some heuristic based on operating system and system bar placement. + *

+ * 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.
  • + *
+ *

+ * The method uses the visual bounds of the primary screen to avoid overlapping with system UI elements. + * Assumes the window size has already been set before calling this method. + * + * @param window the Stage representing the notification window to be placed + */ + static void placeWindow(Stage window) { + var screen = Screen.getPrimary(); + var vBounds = screen.getVisualBounds(); + if (SystemUtils.IS_OS_MAC) { //place to right top + window.setX(vBounds.getMaxX() - window.getWidth()); + window.setY(vBounds.getMinY()); + } else { + switch (SystemBarUtil.getPlacementOfSystembar(screen)) { + case TOP -> { //place to middle top + window.setX(vBounds.getMinX() + (vBounds.getWidth() - window.getWidth()) / 2.0); + window.setY(vBounds.getMinY()); + } + default -> { //place to right bottom + window.setX(vBounds.getMaxX() - window.getWidth()); + window.setY(vBounds.getMaxY() - window.getHeight()); + } + } + } + } + + // javafx setup + + @Provides + @FxmlScene(FxmlFile.NOTIFICATION) + @NotificationScoped + static Scene provideNotificationScene(@NotificationWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.NOTIFICATION); + } + + @Provides + @NotificationScoped + @NotificationWindow + static FxmlLoaderFactory provideFxmlLoaderFactory(Map, Provider> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle); + } + + @Binds + @IntoMap + @FxControllerKey(NotificationController.class) + abstract FxController bindNotificationController(NotificationController controller); + +} diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationScoped.java b/src/main/java/org/cryptomator/ui/notification/NotificationScoped.java new file mode 100644 index 000000000..ca2b9d4b5 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/notification/NotificationScoped.java @@ -0,0 +1,11 @@ +package org.cryptomator.ui.notification; + +import javax.inject.Scope; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Scope +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface NotificationScoped {} diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationWindow.java b/src/main/java/org/cryptomator/ui/notification/NotificationWindow.java new file mode 100644 index 000000000..5b541cee7 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/notification/NotificationWindow.java @@ -0,0 +1,12 @@ +package org.cryptomator.ui.notification; + +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) +public @interface NotificationWindow {} diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java index 1c8d758fc..50240c685 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java @@ -57,8 +57,8 @@ abstract class UnlockModule { @Provides @UnlockWindow @UnlockScoped - static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @UnlockWindow Vault vault, @UnlockWindow Stage window) { - return compBuilder.vault(vault).window(window).build().keyloadingStrategy(); + static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Factory compFactory, @UnlockWindow Vault vault, @UnlockWindow Stage window) { + return compFactory.create(vault, window).keyloadingStrategy(); } @Provides diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css index fff15e834..5b8c717c1 100644 --- a/src/main/resources/css/dark_theme.css +++ b/src/main/resources/css/dark_theme.css @@ -113,6 +113,11 @@ -fx-font-size: 1.2em; } +.label-window-title { + -fx-font-family: 'Open Sans SemiBold'; + -fx-font-size: 1.0em; +} + .label-small { -fx-font-size: 0.8em; } @@ -1186,3 +1191,32 @@ -fx-background-color: MAIN_BG; -fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */ } +/******************************************************************************* + * * + * Notification Window + * * + ******************************************************************************/ +.notification-window { + -fx-background-color: MAIN_BG; + -fx-background-radius: 8px; + -fx-border-radius: 8px; + -fx-background-insets: 0; + -fx-border-color: MUTED_BG; + -fx-border-width: 1px; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 28, 0.35, 0, 6); +} + +.notification-window .dialog-header { + -fx-alignment: center-left; +} + +.notification-window .window-bar-button { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-padding: 6 8 6 8; +} + +.notification-window .window-bar-button:armed { + -fx-background-color: CONTROL_BG_ARMED; + -fx-background-radius: 8; +} \ No newline at end of file diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css index 39e2892ac..38e476923 100644 --- a/src/main/resources/css/light_theme.css +++ b/src/main/resources/css/light_theme.css @@ -113,6 +113,11 @@ -fx-font-size: 1.2em; } +.label-window-title { + -fx-font-family: 'Open Sans SemiBold'; + -fx-font-size: 1.0em; +} + .label-small { -fx-font-size: 0.8em; } @@ -1186,3 +1191,33 @@ -fx-background-color: MAIN_BG; -fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */ } + +/******************************************************************************* + * * + * Notification Window + * * + ******************************************************************************/ +.notification-window { + -fx-background-color: MAIN_BG; + -fx-background-radius: 8px; + -fx-border-radius: 8px; + -fx-background-insets: 0; + -fx-border-color: MUTED_BG; + -fx-border-width: 1px; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 28, 0.35, 0, 6); +} + +.notification-window .dialog-header { + -fx-alignment: center-left; +} + +.notification-window .window-bar-button { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-padding: 6 8 6 8; +} + +.notification-window .window-bar-button:armed { + -fx-background-color: CONTROL_BG_ARMED; + -fx-background-radius: 8; +} \ No newline at end of file diff --git a/src/main/resources/fxml/notification.fxml b/src/main/resources/fxml/notification.fxml new file mode 100644 index 000000000..5f82f3113 --- /dev/null +++ b/src/main/resources/fxml/notification.fxml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + + + + + +
+ diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index 1e136dbe3..d60e3cfbb 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -13,6 +13,7 @@ generic.button.close=Close generic.button.copy=Copy generic.button.copied=Copied! generic.button.done=Done +generic.button.previous=Previous generic.button.next=Next generic.button.print=Print generic.button.remove=Remove @@ -695,3 +696,17 @@ eventView.entry.brokenFileNode.message=Broken filesystem node eventView.entry.brokenFileNode.showEncrypted=Show broken, encrypted node eventView.entry.brokenFileNode.copyEncrypted=Copy path of broken, encrypted node eventView.entry.brokenFileNode.copyDecrypted=Copy decrypted path +eventView.entry.inUse.message=Locked File +eventView.entry.inUse.showDecrypted=Show decrypted file +eventView.entry.inUse.copyDecrypted=Copy decrypted path +eventView.entry.inUse.showEncrypted=Show encrypted file +eventView.entry.inUse.copyEncrypted=Copy encrypted path +eventView.entry.inUse.copyUserAndDevice=Copy locking user and device name +eventView.entry.inUse.ignoreLock=Ignore Lock + + +# Notifications +## FileIsInUse Notification +#notification.inUse.message=File is locked +#notification.inUse.description=File %s is opened by user %s (%s). Ask the user to close the file. Otherwise, you can ignore the lock and open it anyway, but be aware of the data loss risk. +#notification.inUse.action=Ignore Lock \ No newline at end of file