diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index a82e89ba7..abb1b4a92 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,7 +1,14 @@ name: Bug Report description: Create a report to help us improve -labels: ["type:bug"] +type: "Bug" body: + - type: input + id: summary + attributes: + label: Summary + placeholder: Please summarize your problem. + validations: + required: true - type: checkboxes id: terms attributes: @@ -11,13 +18,6 @@ body: required: true - label: I agree to follow this project's [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md) required: true - - type: input - id: summary - attributes: - label: Summary - placeholder: Please summarize your problem. - validations: - required: true - type: textarea id: software-versions attributes: @@ -97,4 +97,4 @@ body: id: further-info attributes: label: Anything else? - description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue? + description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue? \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 652f27234..826f3410a 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -1,7 +1,14 @@ name: Feature Request description: Suggest an idea for this project -labels: ["type:feature-request"] +type: "Feature" body: + - type: input + id: summary + attributes: + label: Summary + placeholder: Please summarize your feature request. + validations: + required: true - type: checkboxes id: terms attributes: @@ -11,13 +18,6 @@ body: required: true - label: I agree to follow this project's [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md) required: true - - type: input - id: summary - attributes: - label: Summary - placeholder: Please summarize your feature request. - validations: - required: true - type: textarea id: motivation attributes: diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 16c5bc530..fa021d441 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -28,7 +28,7 @@ env: jobs: build: name: Build Debian Package - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - id: versions diff --git a/dist/linux/common/org.cryptomator.Cryptomator.tray-unlocked.svg b/dist/linux/common/org.cryptomator.Cryptomator.tray-unlocked.svg index f8e79de1b..be0642364 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.tray-unlocked.svg +++ b/dist/linux/common/org.cryptomator.Cryptomator.tray-unlocked.svg @@ -1,12 +1,16 @@ - - - - + + + + + + \ No newline at end of file diff --git a/dist/linux/common/org.cryptomator.Cryptomator.tray.svg b/dist/linux/common/org.cryptomator.Cryptomator.tray.svg index 205998020..62cccb911 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.tray.svg +++ b/dist/linux/common/org.cryptomator.Cryptomator.tray.svg @@ -1,8 +1,10 @@ - - + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5aef0b15b..0e742157c 100644 --- a/pom.xml +++ b/pom.xml @@ -33,10 +33,10 @@ org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents - 2.8.0 + 2.9.0-beta2 1.5.0 1.3.0 - 1.2.4 + 1.3.0 1.5.2 5.0.2 2.0.7 @@ -46,7 +46,7 @@ 2.55 2.2 2.18.2 - 23.0.1 + 23.0.2 4.4.0 9.37.3 1.5.16 diff --git a/src/main/java/org/cryptomator/JavaFXUtil.java b/src/main/java/org/cryptomator/JavaFXUtil.java new file mode 100644 index 000000000..e1ec90587 --- /dev/null +++ b/src/main/java/org/cryptomator/JavaFXUtil.java @@ -0,0 +1,22 @@ +package org.cryptomator; + +import javafx.application.Platform; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class JavaFXUtil { + + private JavaFXUtil() {} + + public static boolean startPlatform() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + try { + Platform.startup(latch::countDown); + } catch (IllegalStateException e) { + //already initialized + latch.countDown(); + } + return latch.await(5, TimeUnit.SECONDS); + } + +} diff --git a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java index ac03e5ed6..04a46e742 100644 --- a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java +++ b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java @@ -2,6 +2,7 @@ package org.cryptomator.common.keychain; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; +import org.cryptomator.common.Passphrase; import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.integrations.keychain.KeychainAccessProvider; @@ -13,20 +14,24 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; @Singleton public class KeychainManager implements KeychainAccessProvider { private final ObjectExpression keychain; private final LoadingCache passphraseStoredProperties; + private final ReentrantReadWriteLock lock; @Inject KeychainManager(ObjectExpression selectedKeychain) { this.keychain = selectedKeychain; this.passphraseStoredProperties = Caffeine.newBuilder() // - .weakValues() // + .softValues() // .build(this::createStoredPassphraseProperty); keychain.addListener(ignored -> passphraseStoredProperties.invalidateAll()); + this.lock = new ReentrantReadWriteLock(false); } private KeychainAccessProvider getKeychainOrFail() throws KeychainAccessException { @@ -42,29 +47,59 @@ public class KeychainManager implements KeychainAccessProvider { return getClass().getName(); } + @Override + public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { + storePassphrase(key, displayName, passphrase, true); + } + + //TODO: remove ignored parameter once the API is fixed @Override public void storePassphrase(String key, String displayName, CharSequence passphrase, boolean ignored) throws KeychainAccessException { - getKeychainOrFail().storePassphrase(key, displayName, passphrase); + try { + lock.writeLock().lock(); + var kc = getKeychainOrFail(); + //this is the only keychain actually using the parameter + var usesOSAuth = (kc.getClass().getName().equals("org.cryptomator.macos.keychain.TouchIdKeychainAccess")); + kc.storePassphrase(key, displayName, passphrase, usesOSAuth); + } finally { + lock.writeLock().unlock(); + } setPassphraseStored(key, true); } @Override public char[] loadPassphrase(String key) throws KeychainAccessException { - char[] passphrase = getKeychainOrFail().loadPassphrase(key); + char[] passphrase = null; + try { + lock.readLock().lock(); + passphrase = getKeychainOrFail().loadPassphrase(key); + } finally { + lock.readLock().unlock(); + } setPassphraseStored(key, passphrase != null); return passphrase; } @Override public void deletePassphrase(String key) throws KeychainAccessException { - getKeychainOrFail().deletePassphrase(key); + try { + lock.writeLock().lock(); + getKeychainOrFail().deletePassphrase(key); + } finally { + lock.writeLock().unlock(); + } setPassphraseStored(key, false); } @Override public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { if (isPassphraseStored(key)) { - getKeychainOrFail().changePassphrase(key, displayName, passphrase); + try { + lock.writeLock().lock(); + getKeychainOrFail().changePassphrase(key, displayName, passphrase); + } finally { + lock.writeLock().unlock(); + } setPassphraseStored(key, true); } } @@ -101,13 +136,11 @@ public class KeychainManager implements KeychainAccessProvider { } private void setPassphraseStored(String key, boolean value) { - BooleanProperty property = passphraseStoredProperties.getIfPresent(key); - if (property != null) { - if (Platform.isFxApplicationThread()) { - property.set(value); - } else { - Platform.runLater(() -> property.set(value)); - } + BooleanProperty property = passphraseStoredProperties.get(key, _ -> new SimpleBooleanProperty(value)); + if (Platform.isFxApplicationThread()) { + property.set(value); + } else { + Platform.runLater(() -> property.set(value)); } } @@ -134,4 +167,22 @@ public class KeychainManager implements KeychainAccessProvider { } } + public ObjectExpression getKeychainImplementation() { + return this.keychain; + } + + public static void migrate(KeychainAccessProvider oldProvider, KeychainAccessProvider newProvider, Map idsAndNames) throws KeychainAccessException { + if (oldProvider instanceof KeychainManager || newProvider instanceof KeychainManager) { + throw new IllegalArgumentException("KeychainManger must not be the source or target of migration"); + } + for (var entry : idsAndNames.entrySet()) { + var passphrase = oldProvider.loadPassphrase(entry.getKey()); + if (passphrase != null) { + var wrapper = new Passphrase(passphrase); + oldProvider.deletePassphrase(entry.getKey()); //we cannot apply "first-write-then-delete" pattern here, since we can potentially write to the same passphrase store (e.g., touchID and regular keychain) + newProvider.storePassphrase(entry.getKey(), entry.getValue(), wrapper); + wrapper.destroy(); + } + } + } } diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java b/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java index 2ffda4d73..a25384c78 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java @@ -76,8 +76,10 @@ public class ReadmeGenerator { input.chars().forEachOrdered(c -> { if (c < 128) { sb.append((char) c); + } else if (c <= 0xFF) { + sb.append("\\'").append(String.format("%02X", c)); } else if (c < 0xFFFF) { - sb.append("\\u").append(c); + sb.append("\\uc1\\u").append(c); } }); } diff --git a/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java b/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java index 5e625deb2..807c3a9f1 100644 --- a/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java +++ b/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java @@ -38,7 +38,7 @@ public class Dialogs { .setMessageKey("removeVault.message") // .setDescriptionKey("removeVault.description") // .setIcon(FontAwesome5Icon.QUESTION) // - .setOkButtonKey("removeVault.confirmBtn") // + .setOkButtonKey("generic.button.remove") // .setCancelButtonKey("generic.button.cancel") // .setOkAction(stage -> { LOG.debug("Removing vault {}.", vault.getDisplayName()); @@ -64,7 +64,7 @@ public class Dialogs { .setMessageKey("removeCert.message") // .setDescriptionKey("removeCert.description") // .setIcon(FontAwesome5Icon.QUESTION) // - .setOkButtonKey("removeCert.confirmBtn") // + .setOkButtonKey("generic.button.remove") // .setCancelButtonKey("generic.button.cancel") // .setOkAction(stage -> { settings.licenseKey.set(null); diff --git a/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java b/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java index 84d9e4f75..08f77849e 100644 --- a/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java +++ b/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java @@ -31,8 +31,9 @@ public class SimpleDialog { FxmlLoaderFactory loaderFactory = FxmlLoaderFactory.forController( // new SimpleDialogController(resolveText(builder.messageKey, null), // resolveText(builder.descriptionKey, null), // - builder.icon, resolveText(builder.okButtonKey, null), // - resolveText(builder.cancelButtonKey, null), // + builder.icon, // + resolveText(builder.okButtonKey, null), // + builder.cancelButtonKey != null ? resolveText(builder.cancelButtonKey, null) : null, // () -> builder.okAction.accept(dialogStage), // () -> builder.cancelAction.accept(dialogStage)), // Scene::new, builder.resourceBundle); @@ -67,7 +68,6 @@ public class SimpleDialog { private String descriptionKey; private String okButtonKey; private String cancelButtonKey; - private FontAwesome5Icon icon; private Consumer okAction = Stage::close; private Consumer cancelAction = Stage::close; @@ -128,7 +128,6 @@ public class SimpleDialog { Objects.requireNonNull(messageKey, "SimpleDialog messageKey must be set."); Objects.requireNonNull(descriptionKey, "SimpleDialog descriptionKey must be set."); Objects.requireNonNull(okButtonKey, "SimpleDialog okButtonKey must be set."); - Objects.requireNonNull(cancelButtonKey, "SimpleDialog cancelButtonKey must be set."); try { return new SimpleDialog(this); diff --git a/src/main/java/org/cryptomator/ui/dialogs/SimpleDialogController.java b/src/main/java/org/cryptomator/ui/dialogs/SimpleDialogController.java index 0eee1b308..bbf590145 100644 --- a/src/main/java/org/cryptomator/ui/dialogs/SimpleDialogController.java +++ b/src/main/java/org/cryptomator/ui/dialogs/SimpleDialogController.java @@ -14,6 +14,7 @@ public class SimpleDialogController implements FxController { private final String cancelButtonText; private final Runnable okAction; private final Runnable cancelAction; + private final boolean cancelButtonVisible; public SimpleDialogController(String message, String description, FontAwesome5Icon icon, String okButtonText, String cancelButtonText, Runnable okAction, Runnable cancelAction) { this.message = message; @@ -23,6 +24,11 @@ public class SimpleDialogController implements FxController { this.cancelButtonText = cancelButtonText; this.okAction = okAction; this.cancelAction = cancelAction; + this.cancelButtonVisible = cancelButtonText != null && !cancelButtonText.isEmpty(); + } + + public boolean isCancelButtonVisible() { + return cancelButtonVisible; } public String getMessage() { diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java index 68877430a..a13f3e223 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -112,12 +112,12 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { } private void savePasswordToSystemkeychain(Passphrase passphrase) { - if (keychain.isSupported()) { - try { + try { + if (keychain.isSupported() && !keychain.getPassphraseStoredProperty(vault.getId()).get()) { keychain.storePassphrase(vault.getId(), vault.getDisplayName(), passphrase); - } catch (KeychainAccessException e) { - LOG.error("Failed to store passphrase in system keychain.", e); } + } catch (KeychainAccessException e) { + LOG.error("Failed to store passphrase in system keychain.", e); } } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java index 13412dd27..c6e084518 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java @@ -19,9 +19,11 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.fxml.FXML; +import javafx.geometry.Rectangle2D; import javafx.scene.layout.StackPane; import javafx.stage.Screen; import javafx.stage.Stage; +import javafx.stage.WindowEvent; @MainWindowScoped public class MainWindowController implements FxController { @@ -68,18 +70,15 @@ public class MainWindowController implements FxController { int y = settings.windowYPosition.get(); int width = settings.windowWidth.get(); int height = settings.windowHeight.get(); - if (windowPositionSaved(x, y, width, height) ) { - if(isWithinDisplayBounds(x, y, width, height)) { //use stored window position - window.setX(x); - window.setY(y); - window.setWidth(Math.clamp(width, window.getMinWidth(), window.getMaxWidth())); - window.setHeight(Math.clamp(height, window.getMinHeight(), window.getMaxHeight())); - } else if(isWithinDisplayBounds((int) window.getX(), (int) window.getY(), width, height)) { //just reset position of upper left corner, keep window size - window.setWidth(Math.clamp(width, window.getMinWidth(), window.getMaxWidth())); - window.setHeight(Math.clamp(height, window.getMinHeight(), window.getMaxHeight())); - } //else reset window completely + if (windowPositionSaved(x, y, width, height)) { + window.setX(x); + window.setY(y); + window.setWidth(Math.clamp(width, window.getMinWidth(), window.getMaxWidth())); + window.setHeight(Math.clamp(height, window.getMinHeight(), window.getMaxHeight())); } + window.setOnShowing(this::checkDisplayBounds); + settings.windowXPosition.bind(window.xProperty()); settings.windowYPosition.bind(window.yProperty()); settings.windowWidth.bind(window.widthProperty()); @@ -90,6 +89,29 @@ public class MainWindowController implements FxController { return x != 0 || y != 0 || width != 0 || height != 0; } + private void checkDisplayBounds(WindowEvent windowEvent) { + int x = settings.windowXPosition.get(); + int y = settings.windowYPosition.get(); + int width = settings.windowWidth.get(); + int height = settings.windowHeight.get(); + + Rectangle2D primaryScreenBounds = Screen.getPrimary().getBounds(); + if (!isWithinDisplayBounds(x, y, width, height)) { //use stored window position + LOG.debug("Resetting window position due to insufficient screen overlap"); + var centeredX = (primaryScreenBounds.getWidth() - window.getMinWidth()) / 2; + var centeredY = (primaryScreenBounds.getHeight() - window.getMinHeight()) / 2; + //check if we can keep width and height + if (isWithinDisplayBounds((int) centeredX, (int) centeredY, width, height)) { + //if so, keep window size + window.setWidth(Math.clamp(width, window.getMinWidth(), window.getMaxWidth())); + window.setHeight(Math.clamp(height, window.getMinHeight(), window.getMaxHeight())); + } + //reset position of upper left corner + window.setX(centeredX); + window.setY(centeredY); + } + } + private boolean isWithinDisplayBounds(int x, int y, int width, int height) { // define a rect which is inset on all sides from the window's rect: final int shrinkedX = x + 20; // 20px left @@ -144,7 +166,7 @@ public class MainWindowController implements FxController { return updateAvailable.get(); } - public BooleanBinding licenseValidProperty(){ + public BooleanBinding licenseValidProperty() { return licenseHolder.validLicenseProperty(); } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java index 8212f598f..6a7c046f2 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java @@ -8,9 +8,9 @@ import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab; import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; import javax.inject.Inject; +import javafx.beans.binding.Bindings; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.stage.Stage; @@ -21,7 +21,6 @@ public class VaultDetailLockedController implements FxController { private final ReadOnlyObjectProperty vault; private final FxApplicationWindows appWindows; private final VaultOptionsComponent.Factory vaultOptionsWindow; - private final KeychainManager keychain; private final Stage mainWindow; private final ObservableValue passwordSaved; @@ -30,13 +29,11 @@ public class VaultDetailLockedController implements FxController { this.vault = vault; this.appWindows = appWindows; this.vaultOptionsWindow = vaultOptionsWindow; - this.keychain = keychain; this.mainWindow = mainWindow; - if (keychain.isSupported() && !keychain.isLocked()) { - this.passwordSaved = vault.flatMap(v -> keychain.getPassphraseStoredProperty(v.getId())).orElse(false); - } else { - this.passwordSaved = new SimpleBooleanProperty(false); - } + this.passwordSaved = Bindings.createBooleanBinding(() -> { + var v = vault.get(); + return v != null && keychain.getPassphraseStoredProperty(v.getId()).getValue(); + }, vault, keychain.getKeychainImplementation()); } @FXML diff --git a/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java index 800a292a9..4505f412d 100644 --- a/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java @@ -1,10 +1,13 @@ package org.cryptomator.ui.preferences; +import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Environment; +import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.settings.Settings; import org.cryptomator.integrations.autostart.AutoStartProvider; import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException; import org.cryptomator.integrations.common.NamedServiceProvider; +import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.integrations.keychain.KeychainAccessProvider; import org.cryptomator.integrations.quickaccess.QuickAccessService; import org.cryptomator.ui.common.FxController; @@ -14,6 +17,7 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.application.Application; +import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; @@ -23,6 +27,10 @@ import javafx.stage.Stage; import javafx.util.StringConverter; import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; @PreferencesScoped public class GeneralPreferencesController implements FxController { @@ -36,6 +44,8 @@ public class GeneralPreferencesController implements FxController { private final Application application; private final Environment environment; private final List keychainAccessProviders; + private final KeychainManager keychain; + private final ExecutorService backgroundExecutor; private final FxApplicationWindows appWindows; public CheckBox useKeychainCheckbox; public ChoiceBox keychainBackendChoiceBox; @@ -47,12 +57,18 @@ public class GeneralPreferencesController implements FxController { public CheckBox autoStartCheckbox; public ToggleGroup nodeOrientation; + private CompletionStage keychainMigrations = CompletableFuture.completedFuture(null); + @Inject - GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, Optional autoStartProvider, List keychainAccessProviders, Application application, Environment environment, FxApplicationWindows appWindows) { + GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, Optional autoStartProvider, // + List keychainAccessProviders, KeychainManager keychain, Application application, // + Environment environment, FxApplicationWindows appWindows, ExecutorService backgroundExecutor) { this.window = window; this.settings = settings; this.autoStartProvider = autoStartProvider; this.keychainAccessProviders = keychainAccessProviders; + this.keychain = keychain; + this.backgroundExecutor = backgroundExecutor; this.quickAccessServices = QuickAccessService.get().toList(); this.application = application; this.environment = environment; @@ -73,6 +89,7 @@ public class GeneralPreferencesController implements FxController { Bindings.bindBidirectional(settings.keychainProvider, keychainBackendChoiceBox.valueProperty(), keychainSettingsConverter); useKeychainCheckbox.selectedProperty().bindBidirectional(settings.useKeychain); keychainBackendChoiceBox.disableProperty().bind(useKeychainCheckbox.selectedProperty().not()); + keychainBackendChoiceBox.valueProperty().addListener(this::migrateKeychainEntries); useQuickAccessCheckbox.selectedProperty().bindBidirectional(settings.useQuickAccess); var quickAccessSettingsConverter = new ServiceToSettingsConverter<>(quickAccessServices); @@ -83,6 +100,25 @@ public class GeneralPreferencesController implements FxController { quickAccessServiceChoiceBox.disableProperty().bind(useQuickAccessCheckbox.selectedProperty().not()); } + private void migrateKeychainEntries(Observable observable, KeychainAccessProvider oldProvider, KeychainAccessProvider newProvider) { + //currently, we only migrate on macOS (touchID vs regular keychain) + if (SystemUtils.IS_OS_MAC) { + var idsAndNames = settings.directories.stream().collect(Collectors.toMap(vs -> vs.id, vs -> vs.displayName.getValue())); + if (!idsAndNames.isEmpty()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Migrating keychain entries {} from {} to {}", idsAndNames.keySet(), oldProvider.displayName(), newProvider.displayName()); + } + keychainMigrations = keychainMigrations.thenRunAsync(() -> { + try { + KeychainManager.migrate(oldProvider, newProvider, idsAndNames); + } catch (KeychainAccessException e) { + LOG.warn("Failed to migrate all entries from {} to {}", oldProvider.displayName(), newProvider.displayName(), e); + } + }, backgroundExecutor); + } + } + } + public boolean isAutoStartSupported() { return autoStartProvider.isPresent(); } diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css index abb6efe57..4d3db3968 100644 --- a/src/main/resources/css/dark_theme.css +++ b/src/main/resources/css/dark_theme.css @@ -359,6 +359,12 @@ -fx-background-color: PRIMARY; } +.notification-debug:hover .notification-label, +.notification-update:hover .notification-label, +.notification-support:hover .notification-label { + -fx-underline:true; +} + /******************************************************************************* * * * ScrollBar * diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css index 516dd0b26..11cb1a9df 100644 --- a/src/main/resources/css/light_theme.css +++ b/src/main/resources/css/light_theme.css @@ -358,6 +358,12 @@ -fx-background-color: PRIMARY; } +.notification-debug:hover .notification-label, +.notification-update:hover .notification-label, +.notification-support:hover .notification-label { + -fx-underline:true; +} + /******************************************************************************* * * * ScrollBar * diff --git a/src/main/resources/fxml/simple_dialog.fxml b/src/main/resources/fxml/simple_dialog.fxml index 32ad63abf..0e9b01776 100644 --- a/src/main/resources/fxml/simple_dialog.fxml +++ b/src/main/resources/fxml/simple_dialog.fxml @@ -1,16 +1,16 @@ - - - - - - - - - + + + + + + + + + -